library for building extreme scale, millions of models and intelligent agent pipelines, and evaluations, on AWS, Google Cloud


Keywords
automlops, configurable-ai, controllable-ai, evaluations, ml, mlops, testing, trustful-ai
License
MIT
Install
pip install genome-automl==0.9.41

Documentation

AutoML Platform Services for Trustful, and Stakeholder Centric AI - Solving for Millions of Models

Genome is a highly scalable (K8) cloud AutoML services platform following a modular vision of AI to solve large scale multi-task and multi-objective learning problems in a highly modular way. It enables, combining human domain expertise, and ML automation at an extreme level. In our approach, we enable ML solutions via many, potentially millions of so called micro-specialists, enabled via configurable/controllable parts, each with their own distinct lifecycle, that work on just a (modular) slice of data and specialize on particular tasks. Modular AI is in our view, key to trustful and democratic AI, as it empowers many different stakeholders, to gain control of model behavior as well as to capture human intent and domain expertise through configuration or low code tools at massive scale.

Genome is centered around managing production of an extremely high number of modular models (millions of models) with different human stakeholders defining their behaviour and objective via tools that they understand. We adhere to a philosophy of modularity, compositionality and configurability of AI-s, enabling consistent ML Testing where different stakeholders can provide their definition of what is good/bad. In a setting where we produce very large numbers of models/representations, managing quality becomes the fundamental concern of the platform. Our platform comes with a suite of components that can be used in tandem or isolated to achieve defining and managing a very large number of models. In addition to our modular paradigm, a special focus is given to evolve tooling and principled concepts for testing/evaluations for such a large number of models. A structured representation of quality and tests, supported by dedicated AI testing systems where different stakeholders provide their quality definitions, via tools that feel natural to them, is a key requirement for trustful AI. Our platform is ultimately built on top of scalable technologies that lend themselves well to be operated at extreme scale in the cloud.

Table of Contents

  1. Foundational Modular Paradigm: Generalists and Micro-Specialists
  2. Composition via Configurable Graphs
  3. Developing with Genome: Components
  4. Evaluation Store and Framework
  5. Model Store and Framework
  6. Data Store and Framework
  7. Cards and Card Components Framework for Extensible Insights and Reporting
  8. AutoML Application Templates/Recipes
  9. Explaining Model Predictions
  10. Model Visualizations
  11. Sequencer - Chaining Compute Steps via Configurable Pipelines
  12. Run Locally
  13. Testing

Foundational Modular Paradigm: Micro-Specialists - Towards Millions of Models

Modularity of ML artifacts is central to trustful and controllable AI. That means breaking our intelligent solutions into many, (millions) "parts" each one representing a different owner and solution for a particular granular concern. AI modularity means, breaking up the data that models are trained/learned from, breaking their objective function/goals into more granular and specific definitions as it pertains to a particular stakeholder, and breaking quality, good/vs bad definitions. These dimensions, are central for trusted AI. It allows human stakeholders to have real say in the functional definition of intelligent solutions, in defining what their models should do. At a technical level, we think of these modular micro "parts" as artifacts with their own lifecycle, controlled by stakeholders or domain experts, and we call them Micro-Specialists. They are small/modular learned artifacts that represent a task or a specialized domain, and can range from simple rules, to learned representations to full blown models. Micro-Specialists ultimately reflect the decisions and intents of the stakeholders that control them for a more human centric and controllable AI. Their lifecycle follows the lifecycle of decisions from their stakeholders. They are expressed via configuration changes of data definitions, goals and quality settings and are backed by AutoML techniques under the hood, to operationalize those configurations nuances as models. Micro-specialists fit well with foundation models as currently developed in the language space, but not only. If micro-specialists represent specialized, task specific and modular intelligent solutions, we still need so called Generalists that are trained to learn general capabilities, from larger, more general datasets.

In the diagram below a simple setup of a generalist and micro-specialist is displayed during their training and a possible setup with the larger generalist providing predictions to the specialist models. The micro-specialists use the generalist are are trained in multiple epochs, whereas the generalist does not need to change and remains fixed. There are many possible setups like this to combine generalists and micro-specialists that can be expressed via execution graphs.

Modularity and Micro-Specialists

Compositionality via Highly Scalable Configurable Graphs/Flows

Modularity of AI artifacts being front and center in our platform, mandates having a way to compose those parts together, keeping in mind they may be controlled by different stakeholders, technical or non-technical and changed at any time in decouple fashion. Ultimately, breaking current monolithic approaches apart and enabling configurability, controllability and trust to our ML solutions, means we need to enable and solve composition of millions of AI artifacts at different points in time as well. We do that via relying on highly scalable and configurable graph/orchestration engines to express execution graphs and cascades of arbitrary generalists and micro-specialists setups working together (both for offline/training and for realtime inference). We rely only on highly scalable and configurable graph/flow execution engines as enabled by currently just a few solutions in both AWS (Step Functions) and Google Cloud (Workflows) to provide a configuration driven set of solutions. Our platform relies on these highly scalable graph/flow engines under the hood while exposing a simple flow python definition language to define and test flows locally. The flow pythonic definition is just an interface on how to define graph dependencies, they are compiled into the underlying graph specs of the multiple engines we support. We can however extend to different engines via simply writing a new plugin for our compiler and/or expose full blown UI-s to define graphs.

Our Graph definitions are backed by REST/JSON based API-s and can be published/managed fully automatically. The compilation process to Highly configuration driven

Developing Genome Flows and ML applications

Flow Development

Genome expresses a large scale program as they occur typically in the data or ML space, as a directed graph, a flow of steps. More specifically, a program representing an entire ML application is typically represented via more than just a single flow (more on that in ML Application Flows) and usually involves multiple sophisticated flows. However to get started developers usually start from a single flow that can grow to become more complex and can be modularized.

Genome flows are defined via Idiomatic Python classes. The classes have to initiate with a start step, and end with an end step. Our Python Flows can execute locally or within a notebook environment. They can also execute remotely in the cloud (AWS, Google are supported) at extreme scale, as a foundational goal of this project.

Sequences

Defining a sequence within a flow looks like following:

Sequences of Steps

import logging

from genome_automl.pipelines.flows import BaseGraph

from genome_automl.pipelines.flow_annotations import flow
from genome_automl.pipelines.step_annotations import step, transform_step


@flow
class FlowWithSequence(BaseGraph):
    """
    Flow that does a sequence of steps, plus a branch
    """


    @step
    def start(self, input:dict):

      self.next(self.step_a, input)

    # annotation for steps that perform compute
    @transform_step
    def step_a(self, input:dict):

      import numpy
      # ...
      # do some computation
      # produce some output

      self.next(self.step_b, output)


    @step
    def step_b(self, input:dict):

      self.next(self.end, input)


    @step
    def end(self, input:dict):
      pass

Static Branches

Parallel Branches should be used for running steps in parallel. The number of branches in this case is static (known ahead). Parallel branches need to be closed via a join step. The join step receives a list of the inputs composed of each individual branch's output.

Branches of Steps

import logging

from genome_automl.pipelines.flows import BaseGraph

from genome_automl.pipelines.flow_annotations import flow
from genome_automl.pipelines.step_annotations import step, transform_step


@flow
class FlowWithBranches(BaseGraph):
    """
    Flow that does a branch
    """

    @step
    def start(self, input:dict):
      self.next(self.step_a, input)


    @step
    def step_a(self, input:dict):
      # this expresses a branching of execution into two branches
      self.next(self.step_b, self.step_c, input)


    @transform_step
    def step_b(self, input:dict):
      import pandas
      # do some work
      self.next(self.join, join_input=input)


    @transform_step
    def step_c(self, input:dict):
      import pandas
      # do some work
      self.next(self.join, join_input=input)

    # join needs to follow branches
    @step
    def join(self, join_input:dict = None):
      self.next(self.end, input)

    @step
    def end(self, input:dict):
      pass

Conditionals

Defining a conditional step involves specifying an expression. Flor closing conditionals a join is required here as well.

Conditionals

....

@flow
class FlowWithConditional(BaseGraph):
    """
    Flow that does a conditional
    """

    @step
    def start(self, input:dict):
      self.next(self.step_a, input)


    @step
    def step_a(self, input:dict):
      # this expresses a conditional branching,
      self.next(self.step_true, self.step_false, input, condition={
        "Variable": "step_input.param",
        "NumericEquals": 3
      })


    @transform_step
    def step_true(self, input:dict):
      # do some work
      self.next(self.join, join_input=input)


    @step
    def step_false(self, input:dict):
      self.next(self.join, join_input=input)


    @step
    def join(self, join_input:dict = None):
      self.next(self.end, input)

    @step
    def end(self, input:dict):
      pass

Dynamic (Foreach) Branches

For branches where the number is dynamic and dependent on an input variable, use foreach branching as below. Similar to static branches dynamic branches also need a join step to close them out. Foreach provides expressions to manipulate the input

Branches of Steps

import logging

from genome_automl.pipelines.flows import BaseGraph

from genome_automl.pipelines.flow_annotations import flow
from genome_automl.pipelines.step_annotations import step, transform_step


@flow
class FlowWithForeach(BaseGraph):
    """
    Flow that does a dynamic branch (foreach)
    """


    @step
    def start(self, input:dict):

      self.next(self.step_a, input)


    @step
    def step_a(self, input:list):
      # use a **foreach** expression to manipulate the input passed to each branch
      # for a list input, step_b is invoked with each element in the list in parallel
      self.next(self.step_b, input, foreach="step_input")


    @transform_step
    def step_b(self, input:dict):
      # do some transforms

      self.next(self.join, input)


    @step
    def join(self, input:dict):

      self.next(self.end, input)


    @step
    def end(self, input:dict):
      pass

Inputs and Outputs: Specs, Artifacts, Expressions

Inputs and outputs are represented via specs and artifacts. Specs define configurations, artifacts define typically transformation outputs. They can be be data related: DataSpecs, DataArtifacts or model related: TransformSpec, ModelArtifact, or evaluation related: TransformSpec, EvaluationArtifact, CardArtifact. Pipeline steps designed in a reusable way combine these types of artifacts in their inputs and outputs. A TransformStep for example typically has multiple data configurations (DataSpecs), a transformation definition (TransformSpec) and several evaluation definitions (EvaluationSpec).

Expressions are used to adapt inputs to the steps they are attached to, so it can fit what the step expects. They support predefined json object variables such as, step_input. These variables provide access to their json properties via the usual dot notation or the dict notation (step_input.property1, or step_input['property1']). In addition, Expressions allow for invocation of a set of predefined functions such as base64 or json encodings, array operations, math operations and similar. The list of supported intrinsic functions is the same as the one provided by the underlying cloud orchestration solution (AWS, Google).

@flow
class FlowWithExpressions(BaseGraph):

    # example of a conditional step adapting its input via an expression_input
    # to then use the condition expression during runtime for branching
    @step
    def start(input: dict):
        self.next(self.check_to_proceed, input)


    # an expression_input can manipulate the input passed to the step,
    # it defines what the step's input, and accepts expressions in any of the fields
    # to construct arbitrary structures
    # below we are constructing a TransformInput object utilizing versionName and specVersionName
    # from the  flow input
    @step(
      expression_input = TransformInput(
        [DataSpec()],
        TransformSpec(
          canonicalName = "first/flow/transform-start",
          application = "search",

          code = CodeRef("ecr://my-first-image:latest", "docker"),

          framework = "tensorflow",
          inputModality = "image",

          versionName = Expression("step_input.versionName"), # version name of the whole config for this model
          specVersionName = Expression("step_input.specVersionName")

        ), None)
    )
    def check_to_proceed(self, input: TransformInput):

        """
        defines conditional branches, self.next(onTrue, onFalse, input, condition)
        condition follows the notation supported by either AWS step function, or by Google Workflow condition expressions
        """
        self.next(
          self.train,
          self.condition_false_branch_step,
          input,
          condition={
            "Variable":"step_input[\"transform_spec\"][\"framework\"]",
            "StringEquals": "tensorflow"
          })

Cards and Evaluations for Visualizations

Evaluations represent ML tests, whereas Cards define arbitrary visualizations catering to human decision makers. There are base cards for all the artifact types available in the platform Models, Evaluations, etc. that our UI can visualize. Cards can however be custom. Below a few examples of provided custom cards, that are put together via composition of card components. The example below involves, an Image plotly component, a Markdown component, then a bokeh, and an evidently visualization component for a rich experience of providing insights.

    @transform_step(
      image="card-image:latest" # defining a docker image that this step uses to run the code below
    )
    def card_step(input:TransformInput):

        # ----- generate card for human evaluators

        from genome_automl.materializers.factory import CardMaterializer
        from genome_automl.materializers.card_html_materializer import CardHTMLMaterializer
        from genome_automl.evaluationstore import EvaluationStore
        from genome_automl.card import Card, CardArtifact, Header, Table, Row, Column, PltImage, Markdown, InnerHtml, BokehComponent, EvidentlyComponent, ArtifactComponent

        eval_store_card = EvaluationStore(StoreContext())
        eval_store_card.register_materializer(CardHTMLMaterializer())



        # ----- Mathplotlib Component example in card artifacts

        # draw chart a
        # create random data
        no_of_balls = 35
        x = [random.triangular() for i in range(no_of_balls)]
        y = [random.gauss(0.5, 0.25) for i in range(no_of_balls)]
        colors = [random.randint(1, 4) for i in range(no_of_balls)]
        areas = [math.pi * random.randint(5, 15)**2 for i in range(no_of_balls)]

        # draw the plot
        figure_chart_a = plt.figure(figsize=(6, 5), dpi=80)
        plt.scatter(x, y, s=areas, c=colors, alpha=0.85)
        plt.axis([0.0, 1.0, 0.0, 1.0])
        plt.xlabel("X")
        plt.ylabel("Y")


        # using a plotly image component
        chart_a = PltImage(fig = figure_chart_a).to_html()



        # draw chart b
        # create random data
        no_of_balls = 65
        x = [random.triangular() for i in range(no_of_balls)]
        y = [random.gauss(0.5, 0.25) for i in range(no_of_balls)]
        colors = [random.randint(1, 4) for i in range(no_of_balls)]
        areas = [math.pi * random.randint(5, 15)**2 for i in range(no_of_balls)]

        # draw the plot
        figure_chart_b = plt.figure(figsize=(6, 5), dpi=80)
        plt.scatter(x, y, s=areas, c=colors, alpha=0.85)
        plt.axis([0.0, 1.0, 0.0, 1.0])
        plt.xlabel("X")
        plt.ylabel("Y")

        chart_b = PltImage(fig = figure_chart_b).to_html()

        # place the plotly images into columns
        charta_img_column = Column(InnerHtml(chart_a))
        chartb_img_column = Column(InnerHtml(chart_b))


        # add a markdown component
        header_card = Header("First Card with image")

        some_markdown_comp = Markdown((f""
          f"The two charts above delineate random ganerated bubble charts. They are for __display__ only.\n"
          f"* the first item shows 45 balls randomly generated\n"
          f"* the second item shows 65 balls randomly generated\n"
          f"Both charts are rendered using pythons markdown library\n"
        ))
        markdown_column = Column(some_markdown_comp)

        # putting it all together
        body_card = Table([
          Row([charta_img_column, chartb_img_column]),
          Row([markdown_column])
        ])


        # ----- bokeh component example in Card artifacts
        from bokeh.plotting import figure

        plot = figure()

        bokeh_plot_x = [1, 2, 3, 4, 5]
        bokeh_plot_y = [6, 7, 8, 7, 3]

        # add both a line and circles on the same plot
        plot.line(bokeh_plot_x, bokeh_plot_y, line_width=2)
        plot.circle(bokeh_plot_x, bokeh_plot_y, fill_color="white", size=8)

        # add a bokeh component to the card body
        bokeh_component = BokehComponent(plot)
        body_card.append(Row([Column(bokeh_component)]))


        # ----- Evidently Component example in card artifacts
        from sklearn.datasets import fetch_california_housing

        dataset_cal_train = fetch_california_housing()
        dataset_cal_df = pd.DataFrame(data=dataset_cal_train.data, columns=dataset_cal_train.feature_names)

        # add evidently component
        from evidently.report import Report
        from evidently.metric_preset import DataDriftPreset, TargetDriftPreset


        drift_report = Report(metrics=[DataDriftPreset(), TargetDriftPreset()])

        # now add evidently component
        ev_component = EvidentlyComponent(report=drift_report, reference_data=dataset_cal_df, current_data=dataset_cal_df)
        ev_column = Column(ev_component)
        ev_row = Row([ev_column])
        # append evidently component
        body_card.append(ev_row)
        # end evidently component end


        # finalize card
        card = Card(header_card, body_card)


        # construct step output
        card_artifact = CardArtifact(
            canonicalName = canonicalName,
            application = application_parameter or "search",
            code = CodeRef("ecr://card-image:latest", "docker"),

            pipelineName = pipelinename_parameter or "pipeline-sklearn-test",
            pipelineRunId = pipelinerun_parameter,
            pipelineStage = stepname_parameter or "model",

            versionName = "sklearn.1.2.2",
            specVersionName = "template-0.0.5",

            deployment = deployment_id,
            specDeployment = "deployment-0.0.1",
            format = "html",
            dimension = EvaluationDimension.PERFORMANCE.value,

            cardTarget = BaseRef(savedModelResp.id, "model"))


        eval_store_card.save_card(card, card_artifact)


        logging.info("trained text classifier Model and saved on ModelStore")


        # now register the step output
        transform_output = CardTransformOutput([
            card_artifact
        ])



        # defines next step in sequence
        self.next(self.another_step, transform_output)



ML Tests via Evaluation Store and the Evaluation Framework

Evaluations in Genome are akin to unit testing in software engineering and represent a critical stage in the ML development lifecycle. With Genome we treat ML tests as first class citizens via the Evaluation Store and the associated framework. The Evaluation Store is the system of record for representing and tracking ML tests in a structured way. We use following concepts for testing:

  • Evaluations: represent full test suites testing a behavioral scenario holistically. Evaluations can have multiple tasks.
  • Task(s): represents a unit test, a specific part/unit of the full behavioral scenario. Tasks can operate on datasets, segments or single data points, what we call prototypes, in order to allow for different levels of data coverage for the test scenario. Tasks can contain multiple expectations.
  • Expectation(s): are single (boolean) checks for particular low level conditions. Metric checks can happen here, as well as raw data point comparisons.

Evaluations as Tests

To use the Evaluation Store create an evaluation class like in the example below, then run it with the trained model. Note the similarity with unit testing.

import logging
from genome_automl.store import StoreContext
from genome_automl.modelstore import ModelStore
from genome_automl.estimator import GenomeEstimator
from genome_automl import evaluationstore
from genome_automl.evaluationspec import EvaluationDimension, GenomeEvaluationRun, evaluation, task



# use the @evaluation annotation to declare a class as an evaluation
# supported evaluation dimensions (PERFORMANCE | ROBOUSTNESS | SECURITY | PRIVACY | ETHICS | COST )
@evaluation(
  name="test/skill/annotations/better-than-last",
  versionName="1.2.sklearn",
  versionName="1.2.sklearn",
  specVersionName="appspec-1.1.2",
  dimension = EvaluationDimension.PERFORMANCE,
  code = "ensemble-training:local.1",
  targetModel="target-id")
class TrainTestEvaluation(GenomeEvaluationRun):


    # use the @task annotation to declare a method as an evaluation task
    # task metadata for functions annotated this way, will be stored in
    # our Evaluation Store
    @task(dataset={"ref": "mllake://datasets/benchmark/california-housing-test"})
    def evaluateTrainTestSplit(self, t, dataset):
        my_split = 0.7

        t.add_metric("f2", 2.34)
        t.expect(my_split, var="my_split").toBeLess(0.76)

    # how to test for errors
    @task(dataset={"ref": "mllake://datasets/benchmark/california-housing-test"})
    def checkError(self, t, dataset):
        my_split = 0.7

        t.add_metric("f2", 2.34)
        t.expect(lambda: 5/0, var="myFunction").toRaise(ZeroDivisionError)


    # how to test for specific records in a dataset (named prototypes)
    # behaving in a certain way for a model
    @task(dataset={"ref": "mllake://datasets/benchmark/california-housing-test"})
    def prototypeTest(self, t, dataset):

        logging.info("running evaluation task:")
        logging.info(t)

        intersect = 0.82
        t.add_metric("intersection", intersect) \
          .expect(intersect, var="intersection") \
          .toBeGreater(0.52)

        # now some dummy prototypes
        for record in range(5):
            m = record * 1.24
            t.prototype(ref="id-123") \
              .add_metric("f1", record * 2.34) \
              .expect(m, var="f1") \
              .toBe([1,0], var="metric")


# now that the evaluation definition with its annotations is provided
# we can run the test either as part of the training stage, or separately

model_store = ModelStore()

canonicalName = modelMeta["canonicalName"]


categories = ['alt.atheism', 'soc.religion.christian',
          'comp.graphics', 'sci.med']

twenty_train = fetch_20newsgroups(
  subset='train',
  categories=categories,
  shuffle=True,
  random_state=42,
  remove=('headers', 'footers'),
)

#pipeline is composed out of tfid +LSA
vec = TfidfVectorizer(min_df=3, stop_words='english',
                  ngram_range=(1, 2))
svd = TruncatedSVD(n_components=100, n_iter=7, random_state=42)
lsa = make_pipeline(vec, svd)
forest_model = RandomForestClassifier(n_estimators=205,max_depth=5)

pipe = make_pipeline(lsa, forest_model)

# fit and score
pipe.fit(twenty_train.data, twenty_train.target)

model = GenomeEstimator(pipe,
    estimator_predict = "predict_proba",
    target_classes = twenty_train.target_names,
    modality = "text")


# save model
saved_model = model_store.save_model(model, {
  "canonicalName": canonicalName,
  "application": application_parameter or "search",
  "pipelineName": pipelinename_parameter or "pipeline-keras-test",
  "pipelineRunId": pipelinerun_parameter,
  "pipelineStage": stepname_parameter or "model",
  "framework": "sklearn",
  "inputModality": "text",
  "versionName": "sklearn-text.1.2.2",
  "predictionType": "classification"
})

# now finally run the evaluation defined in TrainTestEvaluation
# in target we are dynamically passing the trained model id from model store
split_eval = TrainTestEvaluation(None, target = saved_model["id"])
split_eval.to_run()

Explaining Models on Tabular Data

In this example we'll be creating and training a tree based model, specifically a random forest regressor that predicts on the CA housing dataset. Then the model will be stored in the Model Store to get realtime explanations.

# other imports
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import RandomForestRegressor

# data
from sklearn.datasets import fetch_20newsgroups
from sklearn.datasets import fetch_california_housing


from genome_automl.store import StoreContext
from genome_automl.modelstore import ModelStore
from genome_automl.estimator import GenomeEstimator



# using the california housing dataset
dataset_train=fetch_california_housing()

# creating and fitting an sklearn random forest
forest_model = RandomForestRegressor(n_estimators=120,max_depth=5)
forest_model = forest_model.fit(dataset_train.data, dataset_train.target)


model_store = ModelStore()



# creating a genome model
# an explainer will be chosen based on modality and model type
genome_model = GenomeEstimator(forest_model,
      target_classes=["price"],
      feature_names=dataset_train.feature_names,
      modality="tabular")



# creating global model explanations via invoking sampleExplanations of the model
# for a representative data sample (sample sizes in the range of 1-3k work well)
data_to_explain = dataset_train.data[ np.random.choice( dataset_train.data.shape[0], 1200, replace=False), : ]

# generating and storing explanations in the genome model on a sample dataset
# a global view of model explanations is derived from the sample dataset
# the global model explanations are served in realtime via our realtime explainer  
genome_model.explainer.sampleExplanations(data_to_explain)


# save the genome model in model store
model_store.save_model(genome_model, {
    "canonicalName": canonicalName,
    "application": application_parameter or "search",
    "pipelineName": pipelinename_parameter or "pipeline-keras-test",
    "pipelineRunId": pipelinerun_parameter,
    "pipelineStage": stepname_parameter or "model",
    "framework": "sklearn",
    "inputModality": "tabular",
    "versionName": "1.2.3"
  })

To run this code first it needs to be built as a docker image, say with a name housing-ca-forest-model and tag local.1

After running this sequencer pipeline the trained model and the explainer will be available in the UI. We can than get realtime explanations via either the UI or programmatically via the API.

Explanations from UI - both the precomputed set from the example above and new explanations via a json entry can be obtained: Explanations UI

Explanations can be obtained via the API as well. The API will use the latest trained model with the specified canonicalName.

POST http://127.0.0.1:8080/v1.0/genome/routing/explain
{
  "application": "your-app",
  "canonicalName": "model-canonical-name",
  // list of entries to explain, will be converted to a numpy array, or a panda dataframe in that sequence
  "entries": [
    [1,2.3,-0.243,...], //record 1
    [2.21,0.3,-0.443,...] //record 2 ...
  ]
}

RESPONSE:

// RESPONSE:
{
  "expected": [2.0697080214631782], // base line prediction
  "number_labels": 1,
  "shapley": [  // shapley values for each feature of the records
    [2.385286622444727, -0.018421615109792185, -0.014984237058267673, -0.005339789397244816,],
    [1.410856622444727, -0.016421615109792185, -0.044984237058267673, -0.005339789397244816,]
  ]
}

Explaining Models for Images

For models working on images we support explanations of classification use cases via GradCAM. Our basic assumption for models working on images is that they are using CNN architectures internally. While black box approaches would also be possible (i.e. shapley values would be a candidate here as well), the restrictions associated with realtime explanations prevent us from going that route. We think focusing on the most widely used architectures for image classification is a reasonable way to remain within time budgets.

Again, the code to train our image classification model or even the code to use a popular pretrained model (VGG, ResNet50 etc.) needs to be provided and then built as a docker image. An example code below:

#... other imports
from genome_automl.store import StoreContext
from genome_automl.modelstore import ModelStore
from genome_automl.estimator import GenomeEstimator


from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2
from tensorflow.keras.applications.mobilenet_v2 import preprocess_input


canonicalName = "/classifier/mobilenet/v2"

# load keras pre-trained MobileNetV2 model
model = MobileNetV2(weights='imagenet', include_top=True)


genome = GenomeEstimator(model,
    data_preprocessor = preprocess_input, # function for preprocessing image input
    modality = "image")




model_store = ModelStore()

# save model to model store
model_store.save_model(genome, {
    "canonicalName": canonicalName,
    "application": "search",
    "pipelineName": "pipeline-keras-test",
    "pipelineRunId": "run-id", # or use the env variable to get the run id from the sequencer
    "pipelineStage": "image-class-step",
    "framework": "keras",
    "inputModality": "image", # note here the difference with the example with tabular data.
    "versionName": "test.1.2",
    "predictionType": "classification"
})

Follow the same steps as in the tabular data example to build the image and then run a simple single-step pipeline to run the code. Then you should be able to see the model saved in the UI, ready to provide explanations on images (note the image dimensions for input images need to be the same that the model accepts):

Image Explanations UI

The UI provides the predicted label and the probability (softmax layer) for the prediction. It highlights the areas important for that prediction. The first explanation takes a while (especially for large models) because the model is loaded on the fly from the model store and then subsequently cached

Similar to the tabular data example, the same can be achieved via the API:

POST http://127.0.0.1:8080/v1.0/genome/routing/explain
{
  "application": "your-app",
  "canonicalName": "model-canonical-name",
  // your image needs to be base64 encoded and has to have the same size expected by the model
  "image": "your-image-base64-encoded"
}

RESPONSE:

{
  "class": "rocket",
  "score": 0.41567, //softmax score
  "number_labels":1,
  "image": "explanation-image-base64-encoded-showing-important-areas..."
}

The base64 encoded image response can be directly attached in the src attribute of an img tag in html.

Explaining Models for text

For models working on text we support explanations of classification via LIME. The intuition behind explanations on text models is that we train a surrogate (simpler explainable model) in realtime with generated synthetic data points similar to the original input to be scored. The surrogate model then provides its weights for all the tokens or words to explain the original input, which is the real target of our explanation. An example would be to have a surrogate model be a simple linear model to approximate and explain a much more complex model (i.e transformer based) for a single data point.

The example below uses a text classification pipeline with a tokenization and tf/idf phase and a random forest classifier as the primary model/pipeline to train and classify on the 20_newsgroup dataset. After training the primary model it is passed to a genome estimator along with its prediction method and saved.

#... other imports
from genome_automl.store import StoreContext
from genome_automl.modelstore import ModelStore
from genome_automl.estimator import GenomeEstimator


canonicalName = "/classifier/text/randomforest"

categories = ['alt.atheism', 'soc.religion.christian',
          'comp.graphics', 'sci.med']

twenty_train = fetch_20newsgroups(
  subset='train',
  categories=categories,
  shuffle=True,
  random_state=42,
  remove=('headers', 'footers'),
)



#text classification pipeline is composed out of tfid + LSA + a final random forest
vec = TfidfVectorizer(min_df=3, stop_words='english',
                  ngram_range=(1, 2))
svd = TruncatedSVD(n_components=100, n_iter=7, random_state=42)
lsa = make_pipeline(vec, svd)
forest_model = RandomForestClassifier(n_estimators=205,max_depth=5)
pipe = make_pipeline(lsa, forest_model)

# fit the primary text model
pipe.fit(twenty_train.data, twenty_train.target)


model_store = ModelStore()

model = GenomeEstimator(pipe, # passing the model to genome estimator
    estimator_predict = "predict_proba", # provide prediction function name of the pipeline/estimator to use for training the surrogate model
    target_classes = twenty_train.target_names,
    modality = "text")


# save genome estimator to model store
model_store.save_model(model, {
  "canonicalName": canonicalName,
  "application": "search",
  "pipelineName": "pipeline-text-test",
  "pipelineRunId": "run-1-text",
  "pipelineStage": "model",
  "framework": "sklearn",
  "inputModality": "text",
  "versionName": "sklearn-text.1.2.2",
  "predictionType": "classification"
})

The code above needs to be built into an image and run as a simple single-step pipeline via the sequencer API very similar to the other examples we have provided above for the tabular or image explanation use cases. After that the trained text model/pipeline will show up in our Model Store UI and the model detail page will contain a form for getting the explanation given a text input document like below:

Text Explanations UI

Getting explanations for text documents can also be performed programmatically via the explanation API similar to the tabular and image use cases:

POST http://127.0.0.1:8080/v1.0/genome/routing/explain
{
  "application": "your-app",
  "canonicalName": "model-canonical-name",
  "text": "your text document as string..."
}

RESPONSE:

{
  "metrics": {"score": 0.523, "mean_KL_divergence": 0.0232},
  "textExplanation": {
    "estimator": "SGDClassifier", // surrogate sklearn model used
    "method": "linear model",
    "targets": [ // each prediction class gets a target object with its score
      {
        "proba": 0.07,
        "score": -1.45,
        "target": "class-name-1",
        "weighted_spans": { // the words associated with their weight for this target's class
          "spans": [["the", [[0, 3]], -0.22912537016629908], [...], ...]
        }
      },
    ]
  }
}

Significant effort has gone into the visualizer implementation for text explanations to adjust it to a pure javascript/react version as opposed to its original lime/eli5 implementation so we recommend using that UI for visualizing the API results.

Model Visualizations

To dissect and debug model decisions on tabular data we provide visualizations of linear, logistic and tree based models (including ensembles) for sklearn, xgboost and Spark ML. Visualizing models internals, especially decision trees, can be helpful in understanding the path in the tree that the prediction took and dissecting the role of each feature value in the prediction, in addition to understanding distribution of the data points in the leaves. In the Model Store UI the model detail page provides a model visualizer. We do not have an API defined for this (yet).

The example below shows the first tree visualization of the random forest we trained in the tabular data example:

Explanations UI

Components:

  • Genome Store - API-s to manage, store and deploy holistic configuration packages for an entire ML solution/application as well as ML application pipelines. Enables managing configurations provided by low code personas as well as configurations provided by ML teams for their AutoML recipes.
  • Genome Model Store - API-s to store, version and track models
  • Genome Evaluation Store - API-s to store and track ML tests and evaluations
  • Genome Data Store - API-s to store and track Datasets and data artifacts produced by pipelines
  • Compute and Sequencer - API-s to create pipeline executions and schedule them
  • Realtime Explainer - API-s to explain models from Model Store
  • Realtime Visualizer - API-s to visualize Models from Model Store
  • Routing - Routes to correct explainer or visualizer type
  • Auth - Auth[n|z] for external facing API-s
  • UI - UI for pipelines and models
  • Gateway

Run Locally

Install Docker

Follow instructions on Docker site

Install Minikube (Local Kubernetes)

For MacOS run

brew install minikube

otherwise follow instructions at minikube site: https://minikube.sigs.k8s.io/docs/start/

Install Terraform

Follow instructions at terraform site: https://learn.hashicorp.com/tutorials/terraform/install-cli

Build Component Images

to build the genome service images run

./build-images.sh

To have a few example working models run:

./build-example-image.sh

This will create images for the model code in the example folder. A pipeline run needs to be created via a call to the sequencer API, like described in the Sequencer section, for the model code to actually execute and a few models to be stored in the Genome model store.

Running

First do a terraform apply

cd terraform/local-test
terraform apply

After this the services will be running in minikube. The last step is to port-forward the nginx gateway in minikube to localhost:

 kubectl -n local port-forward service/genome-a-nginx-service 8080

Note: The Genome services will be running in the namespace local

Now all services are reachable. Go to the below address in the browser:

http://127.0.0.1:8080/static/index.html

Genome Login

Testing

To run tests for our components start:

./test-images.sh

To run tests only for specific components disable undesired components in the script above.

Built with

  • SHAP
  • LIME
  • React
  • NodeJS
  • ElasticSearch
  • K8

Our visualizations borrow heavily from the dtreeviz project, but with significant changes in stack and serving philosophy.