Skip to content

Overview

ModelCards are cards for storing, versioning, and tracking model objects.

Features

  • shareable: All cards including ModelCards are shareable and searchable.
  • auto-onnx: Automatic conversion of trained model into onnx model format.
  • auto-schema: Auto-infer data schema and input and output signature.
  • versioning: SemVer for your model.

Create a Card

# load data card from earlier
from sklearn.linear_model import LinearRegression

# Opsml
from opsml import CardRegistry, ModelCard, CardInfo

# set up registries
data_registry = CardRegistry(registry_name="data")
model_registry = CardRegistry(registry_name="model")

card_info = CardInfo(name="linnerrud", repository="opsml", contact="user@email.com")


# load datacard
datacard = data_registry.load_card(name=card_info.name, version="1.0.0")

# data is not loaded by default (helps when sharing cards with large data)
datacard.load_data()
data_splits = datacard.split_data()


X_train = data_splits["train"].X
y_train = data_splits["train"].y

# fit model
linreg = LinearRegression()
linreg = linreg.fit(X=X_train, y=y_train)
model_interface = SklearnModel(model=linreg, sample_data=X_train)

# lets test the onnx model before registering
modelcard = ModelCard(
    info=card_info,
    interface = model_interface,
    datacard_uid=datacard.uid,
    to_onnx=True,
)

# if you'd like to convert to onnx before registering, you can do that as well
modelcard.convert_to_onnx()

# custom onnx testing logic
...

# everything looks good
model_registry.register_card(modelcard)

ModelCard Args

name: str
Name for the data (Required)
repository: str
repository data belongs to (Required)
contact: str
Email to associate with data (Required)
interface: ModelInterface
ModelInterface used to interact with model. See ModelInterface for more information
datacard_uid
uid of DataCard that contains training data. This is not required to instantiate a ModelCard, but it is required to register a ModelCard
to_onnx
Whether to convert model to onnx or not. Default is False
metadata: ModelCardMetadata
Optional ModelCardMetadata used to store metadata about the model. See ModelCardMetadata for more information. If not provided, a default object is created. When registering a card, the metadata is updated with the latest information.

Docs

opsml.ModelCard

Bases: ArtifactCard

Create a ModelCard from your trained machine learning model. This Card is used in conjunction with the ModelCardCreator class.

Parameters:

Name Type Description Default
interface
Trained model interface.
required
name

Name for the model specific to your current project

required
repository

Repository that this model is associated with

required
contact

Contact to associate with card

required
info

CardInfo object containing additional metadata. If provided, it will override any values provided for name, repository, contact, and version.

Name, repository, and contact are required arguments for all cards. They can be provided directly or through a CardInfo object.

required
uid

Unique id (assigned if card has been registered)

required
version

Current version (assigned if card has been registered)

required
datacard_uid

Uid of the DataCard associated with training the model

required
to_onnx

Whether to convert the model to onnx or not

required
metadata

ModelCardMetadata associated with the model

required
Source code in opsml/cards/model.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
class ModelCard(ArtifactCard):
    """Create a ModelCard from your trained machine learning model.
    This Card is used in conjunction with the ModelCardCreator class.

    Args:
        interface:
                Trained model interface.
        name:
            Name for the model specific to your current project
        repository:
            Repository that this model is associated with
        contact:
            Contact to associate with card
        info:
            `CardInfo` object containing additional metadata. If provided, it will override any
            values provided for `name`, `repository`, `contact`, and `version`.

            Name, repository, and contact are required arguments for all cards. They can be provided
            directly or through a `CardInfo` object.

        uid:
            Unique id (assigned if card has been registered)
        version:
            Current version (assigned if card has been registered)
        datacard_uid:
            Uid of the DataCard associated with training the model
        to_onnx:
            Whether to convert the model to onnx or not
        metadata:
            `ModelCardMetadata` associated with the model
    """

    model_config = ConfigDict(
        arbitrary_types_allowed=True,
        protected_namespaces=("protect_",),
        validate_assignment=True,
        extra="forbid",
    )

    interface: SerializeAsAny[ModelInterface]
    datacard_uid: Optional[str] = None
    to_onnx: bool = False
    metadata: ModelCardMetadata = ModelCardMetadata()

    @field_validator("datacard_uid", mode="before")
    @classmethod
    def check_uid(cls, datacard_uid: Optional[str] = None) -> Optional[str]:
        if datacard_uid is None:
            return datacard_uid

        try:
            UUID(datacard_uid, version=4)  # we use uuid4
            return datacard_uid

        except ValueError as exc:
            raise ValueError("Datacard uid is not a valid uuid") from exc

    def load_model(self, load_preprocessor: bool = False, **kwargs: Any) -> None:
        """Loads model, preprocessor and sample data to interface

        Args:
            load_preprocessor:
                Whether to load preprocessor or not. Default is False

            **kwargs:
                additional kwargs to pass depending on the model type

                By default, all kwargs are passed to the framework specific loader
                e.g. torch.load(path, **kwargs)

                ### Opsml custom kwargs

                - In some instances the model architecture will be required when loading a model state.
                This is sometimes the case for torch, tensorflow and huggingface models.
                - If you need to load the model into a custom architecture use the
                `model_arch` kwarg (e.g. card.load_model(model_arch=my_custom_arch))
        """
        # load modelcard loader
        from opsml.storage.card_loader import ModelCardLoader

        ModelCardLoader(self).load_model(load_preprocessor, **kwargs)

    def download_drift_profile(self, path: Path) -> None:
        """Downloads drift profile to path

        Args:
            path:
                Path to download drift profile
        """

        if not path.exists():
            path.mkdir(parents=True, exist_ok=True)
        elif not path.is_dir():
            raise ValueError(f"The path {path} exists but is not a directory.")

        profile = self.load_drift_profile()

        if profile is None:
            logger.info("No drift profile found")
            return

        write_path = (path / SaveName.DRIFT_PROFILE.value).with_suffix(Suffix.JSON.value)

        profile.save_to_json(write_path)

    def download_model_metadata(self, path: Path) -> None:
        """Downloads model metadata to path

        Args:
            path:
                Path to download model metadata. Should be a directory path
        """
        if not path.exists():
            path.mkdir(parents=True, exist_ok=True)
        elif not path.is_dir():
            raise ValueError(f"The path {path} exists but is not a directory.")

        metadata = self.model_metadata
        metadata = metadata.model_dump_json()

        write_path = (path / SaveName.MODEL_METADATA.value).with_suffix(Suffix.JSON.value)

        # write json
        with write_path.open("w", encoding="utf-8") as file_:
            json.dump(metadata, file_)

    def download_model(
        self,
        path: Path,
        load_preprocessor: bool = False,
        load_onnx: bool = False,
        load_quantized: bool = False,
    ) -> None:
        """Downloads model, preprocessor and metadata to path

        Args:
            path:
                Path to download model
            load_preprocessor:
                Whether to load preprocessor or not. Default is False
            load_onnx:
                Whether to load onnx model or not. Default is False
            load_quantized:
                Whether to load quantized model or not. Default is False

        """

        from opsml.storage.card_loader import ModelCardLoader

        ModelCardLoader(self).download_model(
            lpath=path,
            load_preprocessor=load_preprocessor,
            load_onnx=load_onnx,
            load_quantized=load_quantized,
        )

    def load_onnx_model(self, load_preprocessor: bool = False, load_quantized: bool = False) -> None:
        """Loads onnx model to interface

        Args:
            load_preprocessor:
                Whether to load preprocessor or not. Default is False

            load_quantized:
                Whether to load quantized model or not. Default is False

        """

        from opsml.storage.card_loader import ModelCardLoader

        ModelCardLoader(self).load_onnx_model(load_preprocessor, load_quantized)

    def load_preprocessor(self, lpath: Optional[Path] = None, rpath: Optional[Path] = None) -> None:
        """Loads onnx model to interface

        Args:
            lpath (optional):
                Local path to load preprocessor from
            rpath (optional):
                Remote path to load preprocessor from

        """

        if self.preprocessor is not None:
            return

        from opsml.storage.card_loader import ModelCardLoader

        if lpath is None and rpath is None:
            with tempfile.TemporaryDirectory() as tmp_dir:
                lpath = Path(tmp_dir)
                rpath = self.uri
                ModelCardLoader(self).load_preprocessor(lpath, rpath)

        else:
            ModelCardLoader(self).load_preprocessor(lpath, rpath)

    def create_registry_record(self) -> Dict[str, Any]:
        """Creates a registry record from the current ModelCard"""

        exclude_vars = {"interface": {"model", "preprocessor", "sample_data", "onnx_model"}}
        dumped_model = self.model_dump(exclude=exclude_vars)
        dumped_model["interface_type"] = self.interface.name()
        dumped_model["task_type"] = self.interface.task_type

        return dumped_model

    @property
    def model(self) -> Any:
        """Quick access to model from interface"""
        return self.interface.model

    @property
    def sample_data(self) -> Any:
        """Quick access to sample data from interface"""
        return self.interface.sample_data

    @property
    def preprocessor(self) -> Any:
        """Quick access to preprocessor from interface"""

        if hasattr(self.interface, "preprocessor"):
            return self.interface.preprocessor

        if hasattr(self.interface, "tokenizer"):
            if self.interface.tokenizer is not None:
                return self.interface.tokenizer

        if hasattr(self.interface, "feature_extractor"):
            if self.interface.feature_extractor is not None:
                return self.interface.feature_extractor

        return None

    @property
    def onnx_model(self) -> Optional[OnnxModel]:
        """Quick access to onnx model from interface"""
        return self.interface.onnx_model

    @property
    def model_metadata(self) -> ModelMetadata:
        """Loads `ModelMetadata` class"""

        from opsml.storage.card_loader import ModelCardLoader

        return ModelCardLoader(self).load_model_metadata()

    @property
    def drift_profile(self) -> Optional[Union[SpcDriftProfile]]:
        """Loads drift profile from scouter server"""

        return self.interface.drift_profile

    def load_drift_profile(self) -> Optional[Union[SpcDriftProfile]]:
        """Loads drift profile from model registry"""

        from opsml.storage.card_loader import ModelCardLoader

        ModelCardLoader(self).load_drift_profile()

        return self.drift_profile

    @property
    def card_type(self) -> str:
        return CardType.MODELCARD.value

onnx_model: Optional[OnnxModel] property

Quick access to onnx model from interface