Proposal for Major Change in API #371

Description

@TomFinley

In this issue we describe a proposal to change the API. The core of the
proposal is, instead of working via the entry-point runtime abstraction lying
on top of the implementing code, we encourage people to use the implementing
code directly.

Current State

Within ML.NET, for a component to be exposed in the "public" API, a component
author follows the following steps (from an extremely high level):

  1. The author writes a component, implementing some sort of central interface.
    Often this is something like IDataLoader, IDataTransform, ITrainer,
    or some other such type of object.
  2. An "entry-point" wrapping object is created for that component. This is a
    purely functional view of components as having inputs (as fields in some
    sort of input class) and outputs (as fields in some sort of output class).
    This is decorated with attributes, to allow the dependency injection
    framework to do its work.
  3. A JSON "manifest" describing all such components is created, through some
    process involving a scan of all .dlls and the aforementioned attributes.
  4. Some other code reads this JSON "manifest" and out of it generates a number
    of C# classes. (This process being the code in CSharpApiGenerator.cs, the
    artifact of which is described in CSharpApi.cs.)

A user then works with this component in the following fashion.

  1. The user constructs a LearningPipeline object.
  2. They adds implementations of ILearningPipelineItem, which are sort of
    configuration objects. (These are some of the objects that were code
    generated.)
  3. Through some process that is probably too complex to describe here, these
    ILearningPipelineItem are transmuted into a sort of abstract "graph"
    structure comprised of inputs and outputs. (This is an "entry-point"
    experiment graph.)
  4. This graph structure is then serialized to JSON, de-serialized back out of
    JSON, then the actual underlying code that implements the operations is
    loaded using dependency injection.
  5. Once loaded, the associated "settings" objects (which are actual types
    explicitly written in ML.NET) have their fields populated from values in
    this JSON.
  6. There is some higher level runtime coordinating this process of graph nodes
    (the entry-point graph runner). This is a sort of runtime for the nodes,
    and handles job scheduling, variable setting, and whatnot.

The way this process works is via something called entry-points. Entry-points
were conceived as a mechanism to enable a "regular" way to invoke ML.NET
components from native code, that was more expressive and powerful than the
command line. Essentially: they are a command-line on steroids, that instead
of inventing a new DSL utilizes JSON. This is effective at alleviating the
burden of writing "bridges" from R and Python into ML.NET. It also has
advantages in situations where you need to send a sequence of commands "over
the wire" in some complex fashion. While a few types would need to be handled
(e.g., standard numeric types, IDataView, IFileHandle, and some others),
so long as the entry-points used only those supported types, composing an
experiment in those non-.NET environments would be possible.

Possible Alternate State

Instead of working indirectly with ML.NET components through the entry-point
abstraction, you could just instantiate and use the existing classes directly.
That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
so forth would be instantiated and operated on directly.

While entry-points would still be necessary for any components we wished to
expose through R or Python, we would constrain our usage to those applications
where the added level of abstraction served some purpose.

This alternate pattern of usage is already well tested, as it actually
reflects how ML.NET itself is written.

Changes for ML.NET

In order to move towards this state, a few high level adjustments will be
necessary.

  • Low level API is based direct instantiations of IDataViews/ITrainer and
    other fundamental types and utilities already used within ML.NET code.
  • We will work to actively identify and improve that low level API from the
    point of view of usage. See the sequel for more in depth discussion of this
    point.
  • Writing higher level abstractions to make things easier should be
    encouraged, however always with the aim of making them non-opaque. That is,
    in edge cases when the abstraction fails, integrating what can be done
    with the abstraction with the lower level explicit API should be possible.
    Generally: Easy things should be easy and hard things should be possible.
  • To clarify: We are not getting rid of entry-points, because it remains the
    mechanism by which interop from non-.NET programming environments into TLC
    will continue to happen, and is therefore important. The shift is: the lower
    level C# API will not use entry-points. For the purpose of servicing
    GUI/Python/non-.NET bindings, we will continue in our own code to provide
    entry points, while allowing user code to work by implementing the core
    interfaces directly.

Examples of Potential Improvements in "Direct Access" API

We give the following concrete examples of areas that probably need
improvement. The examples are meant to be illustrative only. That is: the list
is not exhaustive, nor are specific "solutions" to problems meant to convey
that something must be done in a particular way.

  • Instantiation of late binding components was previously always done via
    dependency injection. Therefore, all components have constructors or static
    create methods that have had identical signatures (e.g., for transforms,
    IHostEnvironment env, Arguments args, IDataView input). Direct
    instantiation by the user could use that, but would doubtless be better
    served by a more contextually appropriate constructor that reflects common
    use-cases. For example, this:

    IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

    may become this:

    IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

    This can work both ways: if these objects are directly instantiated, the
    objects could provide richer information than merely being an
    IDataTransform, or what have you. Due to working via the command line,
    entry-points, or a GUI, it is considered almost useless for a component to
    have any purely programmatic access. So for example: we could have had the
    AffineNormalizer expose its slope and intercept, but we instead expose it
    by metadata instead. A direct accessor in ML.NET may be appropriate if we
    directly use these components.

  • Creating a transform and loader feels similar. However, creating a trainer,
    using it to provide a predictor, and then ultimately parameterizing a scorer
    transform with that predictor. Where possible we can try to harmonize the
    interfaces to make them seem more consistent. (Obviously not always possible
    since the underlying abstraction may in fact be genuinely different.)

  • Some parts of the current library introduce needless complexity: Train
    method on trainer is void, always followed by CreatePredictor. Other
    incidents of needless complexity may be less easy to resolve.

  • Some parts of the current library introduce needful complexity, but could
    probably be improved somehow. RoleMappedData creation and usage, while
    providing an essential service ("use this column for this purpose"), is
    incredibly difficult to use. When it was just an "internal" structure we
    just sort of dealt with it, but we would like to improve it. (In some cases
    we can hide its creation into auxillary helper methods, for example.)

  • Simple things like improving naming of things may just help a lot. For
    example: ScoreUtils.GetScorer returns a transform with the predictor's
    scores applied to data. ScoreUtils.GetScoredData or something may be a
    better name.

  • Our so-called "internal" methods do not always direct people towards pits of
    success. For example: some pipeline components should probably apply only
    during training (e.g., filtering, sampling, caching). Some distinction or
    other engineering nicety (e.g., have the utilities for saving models throw
    by default) may help warn people off this common misuse case.

  • Components of the existing API that deal with
    late-binding/dependency-injection stuff could potentially use delegates or
    something like entry-point style factory interfaces instead. This means
    among other things lifting out things like SubComponent from most code.
    Whether these delegates happen to be composed from the command line parser
    calling SubComponent.CreateInstance, or some entry-point "subgraph"
    generating a delegate out of its own graph, is the business of the command
    line parser and entry-point engine, not the component code itself. (Maybe
    the delegate just calls Run graph or something then binds the values.)

    So for example what is currently this:

    newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

    might become this:

    newOva(env, host =>newSdcaRegression(host));
  • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

    Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

    This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

Note that many of these enhancements will serve not only users, but component
authors (including us), and so improve the whole platform.

Miscellaneous Details

Note that C# code generation from entry-point graphs will still be possible:
all entry-point invocations come down to (1) defining input objects, (2)
calling a static method and (3) doing something with the output object.
However it will probably not be possible to make it seem "natural" any more
than an attempt to do code-generation from a mml command line would seem
"natural."

When we decided to make the public facing API entry-points based, this
necessarily required shifting related infrastructure (e.g., GraphRunner,
JsonManifestUtils) into more central assemblies. Once that "idiom" is
deconstructed, this infrastructure should resume its prior state of being in
an isolated assembly.

Along similar lines of isolation, once we shift the components to not use
SubComponent directly, we can "uplift" what is currently the command line
parsing code out into a separate assembly.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    APIIssues pertaining the friendly API

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Proposal for Major Change in API #371

      Description

      @TomFinley

      In this issue we describe a proposal to change the API. The core of the
      proposal is, instead of working via the entry-point runtime abstraction lying
      on top of the implementing code, we encourage people to use the implementing
      code directly.

      Current State

      Within ML.NET, for a component to be exposed in the "public" API, a component
      author follows the following steps (from an extremely high level):

      1. The author writes a component, implementing some sort of central interface.
        Often this is something like IDataLoader, IDataTransform, ITrainer,
        or some other such type of object.
      2. An "entry-point" wrapping object is created for that component. This is a
        purely functional view of components as having inputs (as fields in some
        sort of input class) and outputs (as fields in some sort of output class).
        This is decorated with attributes, to allow the dependency injection
        framework to do its work.
      3. A JSON "manifest" describing all such components is created, through some
        process involving a scan of all .dlls and the aforementioned attributes.
      4. Some other code reads this JSON "manifest" and out of it generates a number
        of C# classes. (This process being the code in CSharpApiGenerator.cs, the
        artifact of which is described in CSharpApi.cs.)

      A user then works with this component in the following fashion.

      1. The user constructs a LearningPipeline object.
      2. They adds implementations of ILearningPipelineItem, which are sort of
        configuration objects. (These are some of the objects that were code
        generated.)
      3. Through some process that is probably too complex to describe here, these
        ILearningPipelineItem are transmuted into a sort of abstract "graph"
        structure comprised of inputs and outputs. (This is an "entry-point"
        experiment graph.)
      4. This graph structure is then serialized to JSON, de-serialized back out of
        JSON, then the actual underlying code that implements the operations is
        loaded using dependency injection.
      5. Once loaded, the associated "settings" objects (which are actual types
        explicitly written in ML.NET) have their fields populated from values in
        this JSON.
      6. There is some higher level runtime coordinating this process of graph nodes
        (the entry-point graph runner). This is a sort of runtime for the nodes,
        and handles job scheduling, variable setting, and whatnot.

      The way this process works is via something called entry-points. Entry-points
      were conceived as a mechanism to enable a "regular" way to invoke ML.NET
      components from native code, that was more expressive and powerful than the
      command line. Essentially: they are a command-line on steroids, that instead
      of inventing a new DSL utilizes JSON. This is effective at alleviating the
      burden of writing "bridges" from R and Python into ML.NET. It also has
      advantages in situations where you need to send a sequence of commands "over
      the wire" in some complex fashion. While a few types would need to be handled
      (e.g., standard numeric types, IDataView, IFileHandle, and some others),
      so long as the entry-points used only those supported types, composing an
      experiment in those non-.NET environments would be possible.

      Possible Alternate State

      Instead of working indirectly with ML.NET components through the entry-point
      abstraction, you could just instantiate and use the existing classes directly.
      That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
      so forth would be instantiated and operated on directly.

      While entry-points would still be necessary for any components we wished to
      expose through R or Python, we would constrain our usage to those applications
      where the added level of abstraction served some purpose.

      This alternate pattern of usage is already well tested, as it actually
      reflects how ML.NET itself is written.

      Changes for ML.NET

      In order to move towards this state, a few high level adjustments will be
      necessary.

      • Low level API is based direct instantiations of IDataViews/ITrainer and
        other fundamental types and utilities already used within ML.NET code.
      • We will work to actively identify and improve that low level API from the
        point of view of usage. See the sequel for more in depth discussion of this
        point.
      • Writing higher level abstractions to make things easier should be
        encouraged, however always with the aim of making them non-opaque. That is,
        in edge cases when the abstraction fails, integrating what can be done
        with the abstraction with the lower level explicit API should be possible.
        Generally: Easy things should be easy and hard things should be possible.
      • To clarify: We are not getting rid of entry-points, because it remains the
        mechanism by which interop from non-.NET programming environments into TLC
        will continue to happen, and is therefore important. The shift is: the lower
        level C# API will not use entry-points. For the purpose of servicing
        GUI/Python/non-.NET bindings, we will continue in our own code to provide
        entry points, while allowing user code to work by implementing the core
        interfaces directly.

      Examples of Potential Improvements in "Direct Access" API

      We give the following concrete examples of areas that probably need
      improvement. The examples are meant to be illustrative only. That is: the list
      is not exhaustive, nor are specific "solutions" to problems meant to convey
      that something must be done in a particular way.

      • Instantiation of late binding components was previously always done via
        dependency injection. Therefore, all components have constructors or static
        create methods that have had identical signatures (e.g., for transforms,
        IHostEnvironment env, Arguments args, IDataView input). Direct
        instantiation by the user could use that, but would doubtless be better
        served by a more contextually appropriate constructor that reflects common
        use-cases. For example, this:

        IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

        may become this:

        IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

        This can work both ways: if these objects are directly instantiated, the
        objects could provide richer information than merely being an
        IDataTransform, or what have you. Due to working via the command line,
        entry-points, or a GUI, it is considered almost useless for a component to
        have any purely programmatic access. So for example: we could have had the
        AffineNormalizer expose its slope and intercept, but we instead expose it
        by metadata instead. A direct accessor in ML.NET may be appropriate if we
        directly use these components.

      • Creating a transform and loader feels similar. However, creating a trainer,
        using it to provide a predictor, and then ultimately parameterizing a scorer
        transform with that predictor. Where possible we can try to harmonize the
        interfaces to make them seem more consistent. (Obviously not always possible
        since the underlying abstraction may in fact be genuinely different.)

      • Some parts of the current library introduce needless complexity: Train
        method on trainer is void, always followed by CreatePredictor. Other
        incidents of needless complexity may be less easy to resolve.

      • Some parts of the current library introduce needful complexity, but could
        probably be improved somehow. RoleMappedData creation and usage, while
        providing an essential service ("use this column for this purpose"), is
        incredibly difficult to use. When it was just an "internal" structure we
        just sort of dealt with it, but we would like to improve it. (In some cases
        we can hide its creation into auxillary helper methods, for example.)

      • Simple things like improving naming of things may just help a lot. For
        example: ScoreUtils.GetScorer returns a transform with the predictor's
        scores applied to data. ScoreUtils.GetScoredData or something may be a
        better name.

      • Our so-called "internal" methods do not always direct people towards pits of
        success. For example: some pipeline components should probably apply only
        during training (e.g., filtering, sampling, caching). Some distinction or
        other engineering nicety (e.g., have the utilities for saving models throw
        by default) may help warn people off this common misuse case.

      • Components of the existing API that deal with
        late-binding/dependency-injection stuff could potentially use delegates or
        something like entry-point style factory interfaces instead. This means
        among other things lifting out things like SubComponent from most code.
        Whether these delegates happen to be composed from the command line parser
        calling SubComponent.CreateInstance, or some entry-point "subgraph"
        generating a delegate out of its own graph, is the business of the command
        line parser and entry-point engine, not the component code itself. (Maybe
        the delegate just calls Run graph or something then binds the values.)

        So for example what is currently this:

        newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

        might become this:

        newOva(env, host =>newSdcaRegression(host));
      • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

        Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

        This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

      Note that many of these enhancements will serve not only users, but component
      authors (including us), and so improve the whole platform.

      Miscellaneous Details

      Note that C# code generation from entry-point graphs will still be possible:
      all entry-point invocations come down to (1) defining input objects, (2)
      calling a static method and (3) doing something with the output object.
      However it will probably not be possible to make it seem "natural" any more
      than an attempt to do code-generation from a mml command line would seem
      "natural."

      When we decided to make the public facing API entry-points based, this
      necessarily required shifting related infrastructure (e.g., GraphRunner,
      JsonManifestUtils) into more central assemblies. Once that "idiom" is
      deconstructed, this infrastructure should resume its prior state of being in
      an isolated assembly.

      Along similar lines of isolation, once we shift the components to not use
      SubComponent directly, we can "uplift" what is currently the command line
      parsing code out into a separate assembly.

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        APIIssues pertaining the friendly API

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Proposal for Major Change in API #371

          Description

          @TomFinley

          In this issue we describe a proposal to change the API. The core of the
          proposal is, instead of working via the entry-point runtime abstraction lying
          on top of the implementing code, we encourage people to use the implementing
          code directly.

          Current State

          Within ML.NET, for a component to be exposed in the "public" API, a component
          author follows the following steps (from an extremely high level):

          1. The author writes a component, implementing some sort of central interface.
            Often this is something like IDataLoader, IDataTransform, ITrainer,
            or some other such type of object.
          2. An "entry-point" wrapping object is created for that component. This is a
            purely functional view of components as having inputs (as fields in some
            sort of input class) and outputs (as fields in some sort of output class).
            This is decorated with attributes, to allow the dependency injection
            framework to do its work.
          3. A JSON "manifest" describing all such components is created, through some
            process involving a scan of all .dlls and the aforementioned attributes.
          4. Some other code reads this JSON "manifest" and out of it generates a number
            of C# classes. (This process being the code in CSharpApiGenerator.cs, the
            artifact of which is described in CSharpApi.cs.)

          A user then works with this component in the following fashion.

          1. The user constructs a LearningPipeline object.
          2. They adds implementations of ILearningPipelineItem, which are sort of
            configuration objects. (These are some of the objects that were code
            generated.)
          3. Through some process that is probably too complex to describe here, these
            ILearningPipelineItem are transmuted into a sort of abstract "graph"
            structure comprised of inputs and outputs. (This is an "entry-point"
            experiment graph.)
          4. This graph structure is then serialized to JSON, de-serialized back out of
            JSON, then the actual underlying code that implements the operations is
            loaded using dependency injection.
          5. Once loaded, the associated "settings" objects (which are actual types
            explicitly written in ML.NET) have their fields populated from values in
            this JSON.
          6. There is some higher level runtime coordinating this process of graph nodes
            (the entry-point graph runner). This is a sort of runtime for the nodes,
            and handles job scheduling, variable setting, and whatnot.

          The way this process works is via something called entry-points. Entry-points
          were conceived as a mechanism to enable a "regular" way to invoke ML.NET
          components from native code, that was more expressive and powerful than the
          command line. Essentially: they are a command-line on steroids, that instead
          of inventing a new DSL utilizes JSON. This is effective at alleviating the
          burden of writing "bridges" from R and Python into ML.NET. It also has
          advantages in situations where you need to send a sequence of commands "over
          the wire" in some complex fashion. While a few types would need to be handled
          (e.g., standard numeric types, IDataView, IFileHandle, and some others),
          so long as the entry-points used only those supported types, composing an
          experiment in those non-.NET environments would be possible.

          Possible Alternate State

          Instead of working indirectly with ML.NET components through the entry-point
          abstraction, you could just instantiate and use the existing classes directly.
          That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
          so forth would be instantiated and operated on directly.

          While entry-points would still be necessary for any components we wished to
          expose through R or Python, we would constrain our usage to those applications
          where the added level of abstraction served some purpose.

          This alternate pattern of usage is already well tested, as it actually
          reflects how ML.NET itself is written.

          Changes for ML.NET

          In order to move towards this state, a few high level adjustments will be
          necessary.

          • Low level API is based direct instantiations of IDataViews/ITrainer and
            other fundamental types and utilities already used within ML.NET code.
          • We will work to actively identify and improve that low level API from the
            point of view of usage. See the sequel for more in depth discussion of this
            point.
          • Writing higher level abstractions to make things easier should be
            encouraged, however always with the aim of making them non-opaque. That is,
            in edge cases when the abstraction fails, integrating what can be done
            with the abstraction with the lower level explicit API should be possible.
            Generally: Easy things should be easy and hard things should be possible.
          • To clarify: We are not getting rid of entry-points, because it remains the
            mechanism by which interop from non-.NET programming environments into TLC
            will continue to happen, and is therefore important. The shift is: the lower
            level C# API will not use entry-points. For the purpose of servicing
            GUI/Python/non-.NET bindings, we will continue in our own code to provide
            entry points, while allowing user code to work by implementing the core
            interfaces directly.

          Examples of Potential Improvements in "Direct Access" API

          We give the following concrete examples of areas that probably need
          improvement. The examples are meant to be illustrative only. That is: the list
          is not exhaustive, nor are specific "solutions" to problems meant to convey
          that something must be done in a particular way.

          • Instantiation of late binding components was previously always done via
            dependency injection. Therefore, all components have constructors or static
            create methods that have had identical signatures (e.g., for transforms,
            IHostEnvironment env, Arguments args, IDataView input). Direct
            instantiation by the user could use that, but would doubtless be better
            served by a more contextually appropriate constructor that reflects common
            use-cases. For example, this:

            IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

            may become this:

            IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

            This can work both ways: if these objects are directly instantiated, the
            objects could provide richer information than merely being an
            IDataTransform, or what have you. Due to working via the command line,
            entry-points, or a GUI, it is considered almost useless for a component to
            have any purely programmatic access. So for example: we could have had the
            AffineNormalizer expose its slope and intercept, but we instead expose it
            by metadata instead. A direct accessor in ML.NET may be appropriate if we
            directly use these components.

          • Creating a transform and loader feels similar. However, creating a trainer,
            using it to provide a predictor, and then ultimately parameterizing a scorer
            transform with that predictor. Where possible we can try to harmonize the
            interfaces to make them seem more consistent. (Obviously not always possible
            since the underlying abstraction may in fact be genuinely different.)

          • Some parts of the current library introduce needless complexity: Train
            method on trainer is void, always followed by CreatePredictor. Other
            incidents of needless complexity may be less easy to resolve.

          • Some parts of the current library introduce needful complexity, but could
            probably be improved somehow. RoleMappedData creation and usage, while
            providing an essential service ("use this column for this purpose"), is
            incredibly difficult to use. When it was just an "internal" structure we
            just sort of dealt with it, but we would like to improve it. (In some cases
            we can hide its creation into auxillary helper methods, for example.)

          • Simple things like improving naming of things may just help a lot. For
            example: ScoreUtils.GetScorer returns a transform with the predictor's
            scores applied to data. ScoreUtils.GetScoredData or something may be a
            better name.

          • Our so-called "internal" methods do not always direct people towards pits of
            success. For example: some pipeline components should probably apply only
            during training (e.g., filtering, sampling, caching). Some distinction or
            other engineering nicety (e.g., have the utilities for saving models throw
            by default) may help warn people off this common misuse case.

          • Components of the existing API that deal with
            late-binding/dependency-injection stuff could potentially use delegates or
            something like entry-point style factory interfaces instead. This means
            among other things lifting out things like SubComponent from most code.
            Whether these delegates happen to be composed from the command line parser
            calling SubComponent.CreateInstance, or some entry-point "subgraph"
            generating a delegate out of its own graph, is the business of the command
            line parser and entry-point engine, not the component code itself. (Maybe
            the delegate just calls Run graph or something then binds the values.)

            So for example what is currently this:

            newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

            might become this:

            newOva(env, host =>newSdcaRegression(host));
          • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

            Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

            This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

          Note that many of these enhancements will serve not only users, but component
          authors (including us), and so improve the whole platform.

          Miscellaneous Details

          Note that C# code generation from entry-point graphs will still be possible:
          all entry-point invocations come down to (1) defining input objects, (2)
          calling a static method and (3) doing something with the output object.
          However it will probably not be possible to make it seem "natural" any more
          than an attempt to do code-generation from a mml command line would seem
          "natural."

          When we decided to make the public facing API entry-points based, this
          necessarily required shifting related infrastructure (e.g., GraphRunner,
          JsonManifestUtils) into more central assemblies. Once that "idiom" is
          deconstructed, this infrastructure should resume its prior state of being in
          an isolated assembly.

          Along similar lines of isolation, once we shift the components to not use
          SubComponent directly, we can "uplift" what is currently the command line
          parsing code out into a separate assembly.

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            APIIssues pertaining the friendly API

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Proposal for Major Change in API #371

              Description

              @TomFinley

              In this issue we describe a proposal to change the API. The core of the
              proposal is, instead of working via the entry-point runtime abstraction lying
              on top of the implementing code, we encourage people to use the implementing
              code directly.

              Current State

              Within ML.NET, for a component to be exposed in the "public" API, a component
              author follows the following steps (from an extremely high level):

              1. The author writes a component, implementing some sort of central interface.
                Often this is something like IDataLoader, IDataTransform, ITrainer,
                or some other such type of object.
              2. An "entry-point" wrapping object is created for that component. This is a
                purely functional view of components as having inputs (as fields in some
                sort of input class) and outputs (as fields in some sort of output class).
                This is decorated with attributes, to allow the dependency injection
                framework to do its work.
              3. A JSON "manifest" describing all such components is created, through some
                process involving a scan of all .dlls and the aforementioned attributes.
              4. Some other code reads this JSON "manifest" and out of it generates a number
                of C# classes. (This process being the code in CSharpApiGenerator.cs, the
                artifact of which is described in CSharpApi.cs.)

              A user then works with this component in the following fashion.

              1. The user constructs a LearningPipeline object.
              2. They adds implementations of ILearningPipelineItem, which are sort of
                configuration objects. (These are some of the objects that were code
                generated.)
              3. Through some process that is probably too complex to describe here, these
                ILearningPipelineItem are transmuted into a sort of abstract "graph"
                structure comprised of inputs and outputs. (This is an "entry-point"
                experiment graph.)
              4. This graph structure is then serialized to JSON, de-serialized back out of
                JSON, then the actual underlying code that implements the operations is
                loaded using dependency injection.
              5. Once loaded, the associated "settings" objects (which are actual types
                explicitly written in ML.NET) have their fields populated from values in
                this JSON.
              6. There is some higher level runtime coordinating this process of graph nodes
                (the entry-point graph runner). This is a sort of runtime for the nodes,
                and handles job scheduling, variable setting, and whatnot.

              The way this process works is via something called entry-points. Entry-points
              were conceived as a mechanism to enable a "regular" way to invoke ML.NET
              components from native code, that was more expressive and powerful than the
              command line. Essentially: they are a command-line on steroids, that instead
              of inventing a new DSL utilizes JSON. This is effective at alleviating the
              burden of writing "bridges" from R and Python into ML.NET. It also has
              advantages in situations where you need to send a sequence of commands "over
              the wire" in some complex fashion. While a few types would need to be handled
              (e.g., standard numeric types, IDataView, IFileHandle, and some others),
              so long as the entry-points used only those supported types, composing an
              experiment in those non-.NET environments would be possible.

              Possible Alternate State

              Instead of working indirectly with ML.NET components through the entry-point
              abstraction, you could just instantiate and use the existing classes directly.
              That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
              so forth would be instantiated and operated on directly.

              While entry-points would still be necessary for any components we wished to
              expose through R or Python, we would constrain our usage to those applications
              where the added level of abstraction served some purpose.

              This alternate pattern of usage is already well tested, as it actually
              reflects how ML.NET itself is written.

              Changes for ML.NET

              In order to move towards this state, a few high level adjustments will be
              necessary.

              • Low level API is based direct instantiations of IDataViews/ITrainer and
                other fundamental types and utilities already used within ML.NET code.
              • We will work to actively identify and improve that low level API from the
                point of view of usage. See the sequel for more in depth discussion of this
                point.
              • Writing higher level abstractions to make things easier should be
                encouraged, however always with the aim of making them non-opaque. That is,
                in edge cases when the abstraction fails, integrating what can be done
                with the abstraction with the lower level explicit API should be possible.
                Generally: Easy things should be easy and hard things should be possible.
              • To clarify: We are not getting rid of entry-points, because it remains the
                mechanism by which interop from non-.NET programming environments into TLC
                will continue to happen, and is therefore important. The shift is: the lower
                level C# API will not use entry-points. For the purpose of servicing
                GUI/Python/non-.NET bindings, we will continue in our own code to provide
                entry points, while allowing user code to work by implementing the core
                interfaces directly.

              Examples of Potential Improvements in "Direct Access" API

              We give the following concrete examples of areas that probably need
              improvement. The examples are meant to be illustrative only. That is: the list
              is not exhaustive, nor are specific "solutions" to problems meant to convey
              that something must be done in a particular way.

              • Instantiation of late binding components was previously always done via
                dependency injection. Therefore, all components have constructors or static
                create methods that have had identical signatures (e.g., for transforms,
                IHostEnvironment env, Arguments args, IDataView input). Direct
                instantiation by the user could use that, but would doubtless be better
                served by a more contextually appropriate constructor that reflects common
                use-cases. For example, this:

                IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

                may become this:

                IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

                This can work both ways: if these objects are directly instantiated, the
                objects could provide richer information than merely being an
                IDataTransform, or what have you. Due to working via the command line,
                entry-points, or a GUI, it is considered almost useless for a component to
                have any purely programmatic access. So for example: we could have had the
                AffineNormalizer expose its slope and intercept, but we instead expose it
                by metadata instead. A direct accessor in ML.NET may be appropriate if we
                directly use these components.

              • Creating a transform and loader feels similar. However, creating a trainer,
                using it to provide a predictor, and then ultimately parameterizing a scorer
                transform with that predictor. Where possible we can try to harmonize the
                interfaces to make them seem more consistent. (Obviously not always possible
                since the underlying abstraction may in fact be genuinely different.)

              • Some parts of the current library introduce needless complexity: Train
                method on trainer is void, always followed by CreatePredictor. Other
                incidents of needless complexity may be less easy to resolve.

              • Some parts of the current library introduce needful complexity, but could
                probably be improved somehow. RoleMappedData creation and usage, while
                providing an essential service ("use this column for this purpose"), is
                incredibly difficult to use. When it was just an "internal" structure we
                just sort of dealt with it, but we would like to improve it. (In some cases
                we can hide its creation into auxillary helper methods, for example.)

              • Simple things like improving naming of things may just help a lot. For
                example: ScoreUtils.GetScorer returns a transform with the predictor's
                scores applied to data. ScoreUtils.GetScoredData or something may be a
                better name.

              • Our so-called "internal" methods do not always direct people towards pits of
                success. For example: some pipeline components should probably apply only
                during training (e.g., filtering, sampling, caching). Some distinction or
                other engineering nicety (e.g., have the utilities for saving models throw
                by default) may help warn people off this common misuse case.

              • Components of the existing API that deal with
                late-binding/dependency-injection stuff could potentially use delegates or
                something like entry-point style factory interfaces instead. This means
                among other things lifting out things like SubComponent from most code.
                Whether these delegates happen to be composed from the command line parser
                calling SubComponent.CreateInstance, or some entry-point "subgraph"
                generating a delegate out of its own graph, is the business of the command
                line parser and entry-point engine, not the component code itself. (Maybe
                the delegate just calls Run graph or something then binds the values.)

                So for example what is currently this:

                newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

                might become this:

                newOva(env, host =>newSdcaRegression(host));
              • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

                Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

                This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

              Note that many of these enhancements will serve not only users, but component
              authors (including us), and so improve the whole platform.

              Miscellaneous Details

              Note that C# code generation from entry-point graphs will still be possible:
              all entry-point invocations come down to (1) defining input objects, (2)
              calling a static method and (3) doing something with the output object.
              However it will probably not be possible to make it seem "natural" any more
              than an attempt to do code-generation from a mml command line would seem
              "natural."

              When we decided to make the public facing API entry-points based, this
              necessarily required shifting related infrastructure (e.g., GraphRunner,
              JsonManifestUtils) into more central assemblies. Once that "idiom" is
              deconstructed, this infrastructure should resume its prior state of being in
              an isolated assembly.

              Along similar lines of isolation, once we shift the components to not use
              SubComponent directly, we can "uplift" what is currently the command line
              parsing code out into a separate assembly.

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                APIIssues pertaining the friendly API

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Proposal for Major Change in API #371

                  Description

                  @TomFinley

                  In this issue we describe a proposal to change the API. The core of the
                  proposal is, instead of working via the entry-point runtime abstraction lying
                  on top of the implementing code, we encourage people to use the implementing
                  code directly.

                  Current State

                  Within ML.NET, for a component to be exposed in the "public" API, a component
                  author follows the following steps (from an extremely high level):

                  1. The author writes a component, implementing some sort of central interface.
                    Often this is something like IDataLoader, IDataTransform, ITrainer,
                    or some other such type of object.
                  2. An "entry-point" wrapping object is created for that component. This is a
                    purely functional view of components as having inputs (as fields in some
                    sort of input class) and outputs (as fields in some sort of output class).
                    This is decorated with attributes, to allow the dependency injection
                    framework to do its work.
                  3. A JSON "manifest" describing all such components is created, through some
                    process involving a scan of all .dlls and the aforementioned attributes.
                  4. Some other code reads this JSON "manifest" and out of it generates a number
                    of C# classes. (This process being the code in CSharpApiGenerator.cs, the
                    artifact of which is described in CSharpApi.cs.)

                  A user then works with this component in the following fashion.

                  1. The user constructs a LearningPipeline object.
                  2. They adds implementations of ILearningPipelineItem, which are sort of
                    configuration objects. (These are some of the objects that were code
                    generated.)
                  3. Through some process that is probably too complex to describe here, these
                    ILearningPipelineItem are transmuted into a sort of abstract "graph"
                    structure comprised of inputs and outputs. (This is an "entry-point"
                    experiment graph.)
                  4. This graph structure is then serialized to JSON, de-serialized back out of
                    JSON, then the actual underlying code that implements the operations is
                    loaded using dependency injection.
                  5. Once loaded, the associated "settings" objects (which are actual types
                    explicitly written in ML.NET) have their fields populated from values in
                    this JSON.
                  6. There is some higher level runtime coordinating this process of graph nodes
                    (the entry-point graph runner). This is a sort of runtime for the nodes,
                    and handles job scheduling, variable setting, and whatnot.

                  The way this process works is via something called entry-points. Entry-points
                  were conceived as a mechanism to enable a "regular" way to invoke ML.NET
                  components from native code, that was more expressive and powerful than the
                  command line. Essentially: they are a command-line on steroids, that instead
                  of inventing a new DSL utilizes JSON. This is effective at alleviating the
                  burden of writing "bridges" from R and Python into ML.NET. It also has
                  advantages in situations where you need to send a sequence of commands "over
                  the wire" in some complex fashion. While a few types would need to be handled
                  (e.g., standard numeric types, IDataView, IFileHandle, and some others),
                  so long as the entry-points used only those supported types, composing an
                  experiment in those non-.NET environments would be possible.

                  Possible Alternate State

                  Instead of working indirectly with ML.NET components through the entry-point
                  abstraction, you could just instantiate and use the existing classes directly.
                  That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
                  so forth would be instantiated and operated on directly.

                  While entry-points would still be necessary for any components we wished to
                  expose through R or Python, we would constrain our usage to those applications
                  where the added level of abstraction served some purpose.

                  This alternate pattern of usage is already well tested, as it actually
                  reflects how ML.NET itself is written.

                  Changes for ML.NET

                  In order to move towards this state, a few high level adjustments will be
                  necessary.

                  • Low level API is based direct instantiations of IDataViews/ITrainer and
                    other fundamental types and utilities already used within ML.NET code.
                  • We will work to actively identify and improve that low level API from the
                    point of view of usage. See the sequel for more in depth discussion of this
                    point.
                  • Writing higher level abstractions to make things easier should be
                    encouraged, however always with the aim of making them non-opaque. That is,
                    in edge cases when the abstraction fails, integrating what can be done
                    with the abstraction with the lower level explicit API should be possible.
                    Generally: Easy things should be easy and hard things should be possible.
                  • To clarify: We are not getting rid of entry-points, because it remains the
                    mechanism by which interop from non-.NET programming environments into TLC
                    will continue to happen, and is therefore important. The shift is: the lower
                    level C# API will not use entry-points. For the purpose of servicing
                    GUI/Python/non-.NET bindings, we will continue in our own code to provide
                    entry points, while allowing user code to work by implementing the core
                    interfaces directly.

                  Examples of Potential Improvements in "Direct Access" API

                  We give the following concrete examples of areas that probably need
                  improvement. The examples are meant to be illustrative only. That is: the list
                  is not exhaustive, nor are specific "solutions" to problems meant to convey
                  that something must be done in a particular way.

                  • Instantiation of late binding components was previously always done via
                    dependency injection. Therefore, all components have constructors or static
                    create methods that have had identical signatures (e.g., for transforms,
                    IHostEnvironment env, Arguments args, IDataView input). Direct
                    instantiation by the user could use that, but would doubtless be better
                    served by a more contextually appropriate constructor that reflects common
                    use-cases. For example, this:

                    IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

                    may become this:

                    IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

                    This can work both ways: if these objects are directly instantiated, the
                    objects could provide richer information than merely being an
                    IDataTransform, or what have you. Due to working via the command line,
                    entry-points, or a GUI, it is considered almost useless for a component to
                    have any purely programmatic access. So for example: we could have had the
                    AffineNormalizer expose its slope and intercept, but we instead expose it
                    by metadata instead. A direct accessor in ML.NET may be appropriate if we
                    directly use these components.

                  • Creating a transform and loader feels similar. However, creating a trainer,
                    using it to provide a predictor, and then ultimately parameterizing a scorer
                    transform with that predictor. Where possible we can try to harmonize the
                    interfaces to make them seem more consistent. (Obviously not always possible
                    since the underlying abstraction may in fact be genuinely different.)

                  • Some parts of the current library introduce needless complexity: Train
                    method on trainer is void, always followed by CreatePredictor. Other
                    incidents of needless complexity may be less easy to resolve.

                  • Some parts of the current library introduce needful complexity, but could
                    probably be improved somehow. RoleMappedData creation and usage, while
                    providing an essential service ("use this column for this purpose"), is
                    incredibly difficult to use. When it was just an "internal" structure we
                    just sort of dealt with it, but we would like to improve it. (In some cases
                    we can hide its creation into auxillary helper methods, for example.)

                  • Simple things like improving naming of things may just help a lot. For
                    example: ScoreUtils.GetScorer returns a transform with the predictor's
                    scores applied to data. ScoreUtils.GetScoredData or something may be a
                    better name.

                  • Our so-called "internal" methods do not always direct people towards pits of
                    success. For example: some pipeline components should probably apply only
                    during training (e.g., filtering, sampling, caching). Some distinction or
                    other engineering nicety (e.g., have the utilities for saving models throw
                    by default) may help warn people off this common misuse case.

                  • Components of the existing API that deal with
                    late-binding/dependency-injection stuff could potentially use delegates or
                    something like entry-point style factory interfaces instead. This means
                    among other things lifting out things like SubComponent from most code.
                    Whether these delegates happen to be composed from the command line parser
                    calling SubComponent.CreateInstance, or some entry-point "subgraph"
                    generating a delegate out of its own graph, is the business of the command
                    line parser and entry-point engine, not the component code itself. (Maybe
                    the delegate just calls Run graph or something then binds the values.)

                    So for example what is currently this:

                    newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

                    might become this:

                    newOva(env, host =>newSdcaRegression(host));
                  • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

                    Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

                    This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

                  Note that many of these enhancements will serve not only users, but component
                  authors (including us), and so improve the whole platform.

                  Miscellaneous Details

                  Note that C# code generation from entry-point graphs will still be possible:
                  all entry-point invocations come down to (1) defining input objects, (2)
                  calling a static method and (3) doing something with the output object.
                  However it will probably not be possible to make it seem "natural" any more
                  than an attempt to do code-generation from a mml command line would seem
                  "natural."

                  When we decided to make the public facing API entry-points based, this
                  necessarily required shifting related infrastructure (e.g., GraphRunner,
                  JsonManifestUtils) into more central assemblies. Once that "idiom" is
                  deconstructed, this infrastructure should resume its prior state of being in
                  an isolated assembly.

                  Along similar lines of isolation, once we shift the components to not use
                  SubComponent directly, we can "uplift" what is currently the command line
                  parsing code out into a separate assembly.

                  Activity

                  Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    APIIssues pertaining the friendly API

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Proposal for Major Change in API #371

                      Description

                      @TomFinley

                      In this issue we describe a proposal to change the API. The core of the
                      proposal is, instead of working via the entry-point runtime abstraction lying
                      on top of the implementing code, we encourage people to use the implementing
                      code directly.

                      Current State

                      Within ML.NET, for a component to be exposed in the "public" API, a component
                      author follows the following steps (from an extremely high level):

                      1. The author writes a component, implementing some sort of central interface.
                        Often this is something like IDataLoader, IDataTransform, ITrainer,
                        or some other such type of object.
                      2. An "entry-point" wrapping object is created for that component. This is a
                        purely functional view of components as having inputs (as fields in some
                        sort of input class) and outputs (as fields in some sort of output class).
                        This is decorated with attributes, to allow the dependency injection
                        framework to do its work.
                      3. A JSON "manifest" describing all such components is created, through some
                        process involving a scan of all .dlls and the aforementioned attributes.
                      4. Some other code reads this JSON "manifest" and out of it generates a number
                        of C# classes. (This process being the code in CSharpApiGenerator.cs, the
                        artifact of which is described in CSharpApi.cs.)

                      A user then works with this component in the following fashion.

                      1. The user constructs a LearningPipeline object.
                      2. They adds implementations of ILearningPipelineItem, which are sort of
                        configuration objects. (These are some of the objects that were code
                        generated.)
                      3. Through some process that is probably too complex to describe here, these
                        ILearningPipelineItem are transmuted into a sort of abstract "graph"
                        structure comprised of inputs and outputs. (This is an "entry-point"
                        experiment graph.)
                      4. This graph structure is then serialized to JSON, de-serialized back out of
                        JSON, then the actual underlying code that implements the operations is
                        loaded using dependency injection.
                      5. Once loaded, the associated "settings" objects (which are actual types
                        explicitly written in ML.NET) have their fields populated from values in
                        this JSON.
                      6. There is some higher level runtime coordinating this process of graph nodes
                        (the entry-point graph runner). This is a sort of runtime for the nodes,
                        and handles job scheduling, variable setting, and whatnot.

                      The way this process works is via something called entry-points. Entry-points
                      were conceived as a mechanism to enable a "regular" way to invoke ML.NET
                      components from native code, that was more expressive and powerful than the
                      command line. Essentially: they are a command-line on steroids, that instead
                      of inventing a new DSL utilizes JSON. This is effective at alleviating the
                      burden of writing "bridges" from R and Python into ML.NET. It also has
                      advantages in situations where you need to send a sequence of commands "over
                      the wire" in some complex fashion. While a few types would need to be handled
                      (e.g., standard numeric types, IDataView, IFileHandle, and some others),
                      so long as the entry-points used only those supported types, composing an
                      experiment in those non-.NET environments would be possible.

                      Possible Alternate State

                      Instead of working indirectly with ML.NET components through the entry-point
                      abstraction, you could just instantiate and use the existing classes directly.
                      That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
                      so forth would be instantiated and operated on directly.

                      While entry-points would still be necessary for any components we wished to
                      expose through R or Python, we would constrain our usage to those applications
                      where the added level of abstraction served some purpose.

                      This alternate pattern of usage is already well tested, as it actually
                      reflects how ML.NET itself is written.

                      Changes for ML.NET

                      In order to move towards this state, a few high level adjustments will be
                      necessary.

                      • Low level API is based direct instantiations of IDataViews/ITrainer and
                        other fundamental types and utilities already used within ML.NET code.
                      • We will work to actively identify and improve that low level API from the
                        point of view of usage. See the sequel for more in depth discussion of this
                        point.
                      • Writing higher level abstractions to make things easier should be
                        encouraged, however always with the aim of making them non-opaque. That is,
                        in edge cases when the abstraction fails, integrating what can be done
                        with the abstraction with the lower level explicit API should be possible.
                        Generally: Easy things should be easy and hard things should be possible.
                      • To clarify: We are not getting rid of entry-points, because it remains the
                        mechanism by which interop from non-.NET programming environments into TLC
                        will continue to happen, and is therefore important. The shift is: the lower
                        level C# API will not use entry-points. For the purpose of servicing
                        GUI/Python/non-.NET bindings, we will continue in our own code to provide
                        entry points, while allowing user code to work by implementing the core
                        interfaces directly.

                      Examples of Potential Improvements in "Direct Access" API

                      We give the following concrete examples of areas that probably need
                      improvement. The examples are meant to be illustrative only. That is: the list
                      is not exhaustive, nor are specific "solutions" to problems meant to convey
                      that something must be done in a particular way.

                      • Instantiation of late binding components was previously always done via
                        dependency injection. Therefore, all components have constructors or static
                        create methods that have had identical signatures (e.g., for transforms,
                        IHostEnvironment env, Arguments args, IDataView input). Direct
                        instantiation by the user could use that, but would doubtless be better
                        served by a more contextually appropriate constructor that reflects common
                        use-cases. For example, this:

                        IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

                        may become this:

                        IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

                        This can work both ways: if these objects are directly instantiated, the
                        objects could provide richer information than merely being an
                        IDataTransform, or what have you. Due to working via the command line,
                        entry-points, or a GUI, it is considered almost useless for a component to
                        have any purely programmatic access. So for example: we could have had the
                        AffineNormalizer expose its slope and intercept, but we instead expose it
                        by metadata instead. A direct accessor in ML.NET may be appropriate if we
                        directly use these components.

                      • Creating a transform and loader feels similar. However, creating a trainer,
                        using it to provide a predictor, and then ultimately parameterizing a scorer
                        transform with that predictor. Where possible we can try to harmonize the
                        interfaces to make them seem more consistent. (Obviously not always possible
                        since the underlying abstraction may in fact be genuinely different.)

                      • Some parts of the current library introduce needless complexity: Train
                        method on trainer is void, always followed by CreatePredictor. Other
                        incidents of needless complexity may be less easy to resolve.

                      • Some parts of the current library introduce needful complexity, but could
                        probably be improved somehow. RoleMappedData creation and usage, while
                        providing an essential service ("use this column for this purpose"), is
                        incredibly difficult to use. When it was just an "internal" structure we
                        just sort of dealt with it, but we would like to improve it. (In some cases
                        we can hide its creation into auxillary helper methods, for example.)

                      • Simple things like improving naming of things may just help a lot. For
                        example: ScoreUtils.GetScorer returns a transform with the predictor's
                        scores applied to data. ScoreUtils.GetScoredData or something may be a
                        better name.

                      • Our so-called "internal" methods do not always direct people towards pits of
                        success. For example: some pipeline components should probably apply only
                        during training (e.g., filtering, sampling, caching). Some distinction or
                        other engineering nicety (e.g., have the utilities for saving models throw
                        by default) may help warn people off this common misuse case.

                      • Components of the existing API that deal with
                        late-binding/dependency-injection stuff could potentially use delegates or
                        something like entry-point style factory interfaces instead. This means
                        among other things lifting out things like SubComponent from most code.
                        Whether these delegates happen to be composed from the command line parser
                        calling SubComponent.CreateInstance, or some entry-point "subgraph"
                        generating a delegate out of its own graph, is the business of the command
                        line parser and entry-point engine, not the component code itself. (Maybe
                        the delegate just calls Run graph or something then binds the values.)

                        So for example what is currently this:

                        newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

                        might become this:

                        newOva(env, host =>newSdcaRegression(host));
                      • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

                        Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

                        This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

                      Note that many of these enhancements will serve not only users, but component
                      authors (including us), and so improve the whole platform.

                      Miscellaneous Details

                      Note that C# code generation from entry-point graphs will still be possible:
                      all entry-point invocations come down to (1) defining input objects, (2)
                      calling a static method and (3) doing something with the output object.
                      However it will probably not be possible to make it seem "natural" any more
                      than an attempt to do code-generation from a mml command line would seem
                      "natural."

                      When we decided to make the public facing API entry-points based, this
                      necessarily required shifting related infrastructure (e.g., GraphRunner,
                      JsonManifestUtils) into more central assemblies. Once that "idiom" is
                      deconstructed, this infrastructure should resume its prior state of being in
                      an isolated assembly.

                      Along similar lines of isolation, once we shift the components to not use
                      SubComponent directly, we can "uplift" what is currently the command line
                      parsing code out into a separate assembly.

                      Activity

                      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        APIIssues pertaining the friendly API

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Proposal for Major Change in API #371

                          Description

                          @TomFinley

                          In this issue we describe a proposal to change the API. The core of the
                          proposal is, instead of working via the entry-point runtime abstraction lying
                          on top of the implementing code, we encourage people to use the implementing
                          code directly.

                          Current State

                          Within ML.NET, for a component to be exposed in the "public" API, a component
                          author follows the following steps (from an extremely high level):

                          1. The author writes a component, implementing some sort of central interface.
                            Often this is something like IDataLoader, IDataTransform, ITrainer,
                            or some other such type of object.
                          2. An "entry-point" wrapping object is created for that component. This is a
                            purely functional view of components as having inputs (as fields in some
                            sort of input class) and outputs (as fields in some sort of output class).
                            This is decorated with attributes, to allow the dependency injection
                            framework to do its work.
                          3. A JSON "manifest" describing all such components is created, through some
                            process involving a scan of all .dlls and the aforementioned attributes.
                          4. Some other code reads this JSON "manifest" and out of it generates a number
                            of C# classes. (This process being the code in CSharpApiGenerator.cs, the
                            artifact of which is described in CSharpApi.cs.)

                          A user then works with this component in the following fashion.

                          1. The user constructs a LearningPipeline object.
                          2. They adds implementations of ILearningPipelineItem, which are sort of
                            configuration objects. (These are some of the objects that were code
                            generated.)
                          3. Through some process that is probably too complex to describe here, these
                            ILearningPipelineItem are transmuted into a sort of abstract "graph"
                            structure comprised of inputs and outputs. (This is an "entry-point"
                            experiment graph.)
                          4. This graph structure is then serialized to JSON, de-serialized back out of
                            JSON, then the actual underlying code that implements the operations is
                            loaded using dependency injection.
                          5. Once loaded, the associated "settings" objects (which are actual types
                            explicitly written in ML.NET) have their fields populated from values in
                            this JSON.
                          6. There is some higher level runtime coordinating this process of graph nodes
                            (the entry-point graph runner). This is a sort of runtime for the nodes,
                            and handles job scheduling, variable setting, and whatnot.

                          The way this process works is via something called entry-points. Entry-points
                          were conceived as a mechanism to enable a "regular" way to invoke ML.NET
                          components from native code, that was more expressive and powerful than the
                          command line. Essentially: they are a command-line on steroids, that instead
                          of inventing a new DSL utilizes JSON. This is effective at alleviating the
                          burden of writing "bridges" from R and Python into ML.NET. It also has
                          advantages in situations where you need to send a sequence of commands "over
                          the wire" in some complex fashion. While a few types would need to be handled
                          (e.g., standard numeric types, IDataView, IFileHandle, and some others),
                          so long as the entry-points used only those supported types, composing an
                          experiment in those non-.NET environments would be possible.

                          Possible Alternate State

                          Instead of working indirectly with ML.NET components through the entry-point
                          abstraction, you could just instantiate and use the existing classes directly.
                          That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
                          so forth would be instantiated and operated on directly.

                          While entry-points would still be necessary for any components we wished to
                          expose through R or Python, we would constrain our usage to those applications
                          where the added level of abstraction served some purpose.

                          This alternate pattern of usage is already well tested, as it actually
                          reflects how ML.NET itself is written.

                          Changes for ML.NET

                          In order to move towards this state, a few high level adjustments will be
                          necessary.

                          • Low level API is based direct instantiations of IDataViews/ITrainer and
                            other fundamental types and utilities already used within ML.NET code.
                          • We will work to actively identify and improve that low level API from the
                            point of view of usage. See the sequel for more in depth discussion of this
                            point.
                          • Writing higher level abstractions to make things easier should be
                            encouraged, however always with the aim of making them non-opaque. That is,
                            in edge cases when the abstraction fails, integrating what can be done
                            with the abstraction with the lower level explicit API should be possible.
                            Generally: Easy things should be easy and hard things should be possible.
                          • To clarify: We are not getting rid of entry-points, because it remains the
                            mechanism by which interop from non-.NET programming environments into TLC
                            will continue to happen, and is therefore important. The shift is: the lower
                            level C# API will not use entry-points. For the purpose of servicing
                            GUI/Python/non-.NET bindings, we will continue in our own code to provide
                            entry points, while allowing user code to work by implementing the core
                            interfaces directly.

                          Examples of Potential Improvements in "Direct Access" API

                          We give the following concrete examples of areas that probably need
                          improvement. The examples are meant to be illustrative only. That is: the list
                          is not exhaustive, nor are specific "solutions" to problems meant to convey
                          that something must be done in a particular way.

                          • Instantiation of late binding components was previously always done via
                            dependency injection. Therefore, all components have constructors or static
                            create methods that have had identical signatures (e.g., for transforms,
                            IHostEnvironment env, Arguments args, IDataView input). Direct
                            instantiation by the user could use that, but would doubtless be better
                            served by a more contextually appropriate constructor that reflects common
                            use-cases. For example, this:

                            IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

                            may become this:

                            IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

                            This can work both ways: if these objects are directly instantiated, the
                            objects could provide richer information than merely being an
                            IDataTransform, or what have you. Due to working via the command line,
                            entry-points, or a GUI, it is considered almost useless for a component to
                            have any purely programmatic access. So for example: we could have had the
                            AffineNormalizer expose its slope and intercept, but we instead expose it
                            by metadata instead. A direct accessor in ML.NET may be appropriate if we
                            directly use these components.

                          • Creating a transform and loader feels similar. However, creating a trainer,
                            using it to provide a predictor, and then ultimately parameterizing a scorer
                            transform with that predictor. Where possible we can try to harmonize the
                            interfaces to make them seem more consistent. (Obviously not always possible
                            since the underlying abstraction may in fact be genuinely different.)

                          • Some parts of the current library introduce needless complexity: Train
                            method on trainer is void, always followed by CreatePredictor. Other
                            incidents of needless complexity may be less easy to resolve.

                          • Some parts of the current library introduce needful complexity, but could
                            probably be improved somehow. RoleMappedData creation and usage, while
                            providing an essential service ("use this column for this purpose"), is
                            incredibly difficult to use. When it was just an "internal" structure we
                            just sort of dealt with it, but we would like to improve it. (In some cases
                            we can hide its creation into auxillary helper methods, for example.)

                          • Simple things like improving naming of things may just help a lot. For
                            example: ScoreUtils.GetScorer returns a transform with the predictor's
                            scores applied to data. ScoreUtils.GetScoredData or something may be a
                            better name.

                          • Our so-called "internal" methods do not always direct people towards pits of
                            success. For example: some pipeline components should probably apply only
                            during training (e.g., filtering, sampling, caching). Some distinction or
                            other engineering nicety (e.g., have the utilities for saving models throw
                            by default) may help warn people off this common misuse case.

                          • Components of the existing API that deal with
                            late-binding/dependency-injection stuff could potentially use delegates or
                            something like entry-point style factory interfaces instead. This means
                            among other things lifting out things like SubComponent from most code.
                            Whether these delegates happen to be composed from the command line parser
                            calling SubComponent.CreateInstance, or some entry-point "subgraph"
                            generating a delegate out of its own graph, is the business of the command
                            line parser and entry-point engine, not the component code itself. (Maybe
                            the delegate just calls Run graph or something then binds the values.)

                            So for example what is currently this:

                            newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

                            might become this:

                            newOva(env, host =>newSdcaRegression(host));
                          • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

                            Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

                            This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

                          Note that many of these enhancements will serve not only users, but component
                          authors (including us), and so improve the whole platform.

                          Miscellaneous Details

                          Note that C# code generation from entry-point graphs will still be possible:
                          all entry-point invocations come down to (1) defining input objects, (2)
                          calling a static method and (3) doing something with the output object.
                          However it will probably not be possible to make it seem "natural" any more
                          than an attempt to do code-generation from a mml command line would seem
                          "natural."

                          When we decided to make the public facing API entry-points based, this
                          necessarily required shifting related infrastructure (e.g., GraphRunner,
                          JsonManifestUtils) into more central assemblies. Once that "idiom" is
                          deconstructed, this infrastructure should resume its prior state of being in
                          an isolated assembly.

                          Along similar lines of isolation, once we shift the components to not use
                          SubComponent directly, we can "uplift" what is currently the command line
                          parsing code out into a separate assembly.

                          Activity

                          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            APIIssues pertaining the friendly API

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Proposal for Major Change in API #371

                              Description

                              @TomFinley

                              In this issue we describe a proposal to change the API. The core of the
                              proposal is, instead of working via the entry-point runtime abstraction lying
                              on top of the implementing code, we encourage people to use the implementing
                              code directly.

                              Current State

                              Within ML.NET, for a component to be exposed in the "public" API, a component
                              author follows the following steps (from an extremely high level):

                              1. The author writes a component, implementing some sort of central interface.
                                Often this is something like IDataLoader, IDataTransform, ITrainer,
                                or some other such type of object.
                              2. An "entry-point" wrapping object is created for that component. This is a
                                purely functional view of components as having inputs (as fields in some
                                sort of input class) and outputs (as fields in some sort of output class).
                                This is decorated with attributes, to allow the dependency injection
                                framework to do its work.
                              3. A JSON "manifest" describing all such components is created, through some
                                process involving a scan of all .dlls and the aforementioned attributes.
                              4. Some other code reads this JSON "manifest" and out of it generates a number
                                of C# classes. (This process being the code in CSharpApiGenerator.cs, the
                                artifact of which is described in CSharpApi.cs.)

                              A user then works with this component in the following fashion.

                              1. The user constructs a LearningPipeline object.
                              2. They adds implementations of ILearningPipelineItem, which are sort of
                                configuration objects. (These are some of the objects that were code
                                generated.)
                              3. Through some process that is probably too complex to describe here, these
                                ILearningPipelineItem are transmuted into a sort of abstract "graph"
                                structure comprised of inputs and outputs. (This is an "entry-point"
                                experiment graph.)
                              4. This graph structure is then serialized to JSON, de-serialized back out of
                                JSON, then the actual underlying code that implements the operations is
                                loaded using dependency injection.
                              5. Once loaded, the associated "settings" objects (which are actual types
                                explicitly written in ML.NET) have their fields populated from values in
                                this JSON.
                              6. There is some higher level runtime coordinating this process of graph nodes
                                (the entry-point graph runner). This is a sort of runtime for the nodes,
                                and handles job scheduling, variable setting, and whatnot.

                              The way this process works is via something called entry-points. Entry-points
                              were conceived as a mechanism to enable a "regular" way to invoke ML.NET
                              components from native code, that was more expressive and powerful than the
                              command line. Essentially: they are a command-line on steroids, that instead
                              of inventing a new DSL utilizes JSON. This is effective at alleviating the
                              burden of writing "bridges" from R and Python into ML.NET. It also has
                              advantages in situations where you need to send a sequence of commands "over
                              the wire" in some complex fashion. While a few types would need to be handled
                              (e.g., standard numeric types, IDataView, IFileHandle, and some others),
                              so long as the entry-points used only those supported types, composing an
                              experiment in those non-.NET environments would be possible.

                              Possible Alternate State

                              Instead of working indirectly with ML.NET components through the entry-point
                              abstraction, you could just instantiate and use the existing classes directly.
                              That is, the aforementioned IDataLoader, IDataTransform, ITrainer, and
                              so forth would be instantiated and operated on directly.

                              While entry-points would still be necessary for any components we wished to
                              expose through R or Python, we would constrain our usage to those applications
                              where the added level of abstraction served some purpose.

                              This alternate pattern of usage is already well tested, as it actually
                              reflects how ML.NET itself is written.

                              Changes for ML.NET

                              In order to move towards this state, a few high level adjustments will be
                              necessary.

                              • Low level API is based direct instantiations of IDataViews/ITrainer and
                                other fundamental types and utilities already used within ML.NET code.
                              • We will work to actively identify and improve that low level API from the
                                point of view of usage. See the sequel for more in depth discussion of this
                                point.
                              • Writing higher level abstractions to make things easier should be
                                encouraged, however always with the aim of making them non-opaque. That is,
                                in edge cases when the abstraction fails, integrating what can be done
                                with the abstraction with the lower level explicit API should be possible.
                                Generally: Easy things should be easy and hard things should be possible.
                              • To clarify: We are not getting rid of entry-points, because it remains the
                                mechanism by which interop from non-.NET programming environments into TLC
                                will continue to happen, and is therefore important. The shift is: the lower
                                level C# API will not use entry-points. For the purpose of servicing
                                GUI/Python/non-.NET bindings, we will continue in our own code to provide
                                entry points, while allowing user code to work by implementing the core
                                interfaces directly.

                              Examples of Potential Improvements in "Direct Access" API

                              We give the following concrete examples of areas that probably need
                              improvement. The examples are meant to be illustrative only. That is: the list
                              is not exhaustive, nor are specific "solutions" to problems meant to convey
                              that something must be done in a particular way.

                              • Instantiation of late binding components was previously always done via
                                dependency injection. Therefore, all components have constructors or static
                                create methods that have had identical signatures (e.g., for transforms,
                                IHostEnvironment env, Arguments args, IDataView input). Direct
                                instantiation by the user could use that, but would doubtless be better
                                served by a more contextually appropriate constructor that reflects common
                                use-cases. For example, this:

                                IDataTransformtrans=newConcatTransform(env,newConcatTransform.Arguments(){Column=new[]{newConcatTransform.Column(){Name="NumericalFeatures",Source=new[]{"SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15"}}}},loader);

                                may become this:

                                IDataTransformtrans=newConcatTransform(env,loader,"NumericalFeatures","SqftLiving","SqftLot","SqftAbove","SqftBasement","Lat","Long","SqftLiving15","SqftLot15");

                                This can work both ways: if these objects are directly instantiated, the
                                objects could provide richer information than merely being an
                                IDataTransform, or what have you. Due to working via the command line,
                                entry-points, or a GUI, it is considered almost useless for a component to
                                have any purely programmatic access. So for example: we could have had the
                                AffineNormalizer expose its slope and intercept, but we instead expose it
                                by metadata instead. A direct accessor in ML.NET may be appropriate if we
                                directly use these components.

                              • Creating a transform and loader feels similar. However, creating a trainer,
                                using it to provide a predictor, and then ultimately parameterizing a scorer
                                transform with that predictor. Where possible we can try to harmonize the
                                interfaces to make them seem more consistent. (Obviously not always possible
                                since the underlying abstraction may in fact be genuinely different.)

                              • Some parts of the current library introduce needless complexity: Train
                                method on trainer is void, always followed by CreatePredictor. Other
                                incidents of needless complexity may be less easy to resolve.

                              • Some parts of the current library introduce needful complexity, but could
                                probably be improved somehow. RoleMappedData creation and usage, while
                                providing an essential service ("use this column for this purpose"), is
                                incredibly difficult to use. When it was just an "internal" structure we
                                just sort of dealt with it, but we would like to improve it. (In some cases
                                we can hide its creation into auxillary helper methods, for example.)

                              • Simple things like improving naming of things may just help a lot. For
                                example: ScoreUtils.GetScorer returns a transform with the predictor's
                                scores applied to data. ScoreUtils.GetScoredData or something may be a
                                better name.

                              • Our so-called "internal" methods do not always direct people towards pits of
                                success. For example: some pipeline components should probably apply only
                                during training (e.g., filtering, sampling, caching). Some distinction or
                                other engineering nicety (e.g., have the utilities for saving models throw
                                by default) may help warn people off this common misuse case.

                              • Components of the existing API that deal with
                                late-binding/dependency-injection stuff could potentially use delegates or
                                something like entry-point style factory interfaces instead. This means
                                among other things lifting out things like SubComponent from most code.
                                Whether these delegates happen to be composed from the command line parser
                                calling SubComponent.CreateInstance, or some entry-point "subgraph"
                                generating a delegate out of its own graph, is the business of the command
                                line parser and entry-point engine, not the component code itself. (Maybe
                                the delegate just calls Run graph or something then binds the values.)

                                So for example what is currently this:

                                newOva(env,newOva.Argumnets(){Trainer=newSubComponent("sdcaR"));

                                might become this:

                                newOva(env, host =>newSdcaRegression(host));
                              • When we think about transform chains and pipelines, both the existing and suggested systems have a need for an intermediate object capable of representing a pipeline before it is instantiated. That intermediate form must be something you can reason over, both to pre-verify pipelines, as well as for certain applications like suggested transforms/auto-ML. One example is issue Do a column validation during pipeline construction #267.

                                Entry-points were an intermediate object, but being logically only JObjects you could not get rich information about what or how they would operate. (Given a pipeline in entry-points you could tell that something might be outputting aIDataView, for example, but have no information about what columns were actually in that output.)

                                This suggests that the API will want something likeLearningPipeline, though I am quite confident LearningPipeline is an incorrect level of abstraction. (See the previous point about opaque abstractions, among other points.)

                              Note that many of these enhancements will serve not only users, but component
                              authors (including us), and so improve the whole platform.

                              Miscellaneous Details

                              Note that C# code generation from entry-point graphs will still be possible:
                              all entry-point invocations come down to (1) defining input objects, (2)
                              calling a static method and (3) doing something with the output object.
                              However it will probably not be possible to make it seem "natural" any more
                              than an attempt to do code-generation from a mml command line would seem
                              "natural."

                              When we decided to make the public facing API entry-points based, this
                              necessarily required shifting related infrastructure (e.g., GraphRunner,
                              JsonManifestUtils) into more central assemblies. Once that "idiom" is
                              deconstructed, this infrastructure should resume its prior state of being in
                              an isolated assembly.

                              Along similar lines of isolation, once we shift the components to not use
                              SubComponent directly, we can "uplift" what is currently the command line
                              parsing code out into a separate assembly.

                              Activity

                              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                APIIssues pertaining the friendly API

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions