ABONAMENTE VIDEO REDACȚIA
RO
EN
×
▼ LISTĂ EDIȚII ▼
Numărul 108
Abonament PDF

Roslyn Source Generators

Daniel Costea
Senior Software Developer @ EU Agency
PROGRAMARE

Pentru machine learning punctul central este modelul. Fie vă antrenați propriul model de machine learning, fie aveți unul de consumat în codul de producție, trebuie să cunoașteți câteva informații despre modul în care a fost instruit, cum ar fi eticheta (în engleza Label sau caracteristica țintă), modelele de date (de intrare și ieșire) și scenariul care a fost folosit pentru antrenare. Împreună cu aceste detalii, este foarte important să cunoașteți acuratețea modelului dvs. de machine learning. În unele cazuri ați putea avea instrumente precum MLOps pentru a avea grijă de aceste detalii, dar poate că nu aveți.

Ori s-ar putea să vă gândiți la un alt scenariu pentru a îmbunătăți funcționalitatea MLOps prin generarea de modele de date de intrare și ieșire (care sunt puternic tastate clase C# cu proprietăți și adnotări de date specifice de machine learning). Sau puteți genera un cod boilerplate pentru validare sau consumare a modelului de machine learning pentru scenarii mai complexe, cum ar fi serviciile web, blazor și aplicațiile consolă.

Dacă sunteți Data Scientist, poate doriți să începeți antrenarea modelului de machine learning de la zero. Dacă sunteți un programator software puteți prefera Model Builder, care este un instrument vizual excelent pentru a vă ajuta să creați un model ML.NET pornind de la date. Împreună cu modelul de machine learning, sunt generate modelele de date de intrare și de ieșire și chiar cod boilerplate pentru a antrena și consuma modelul.

În caz că intenționați să vă antrenați modelul ML.NET din linia de comandă, puteți valorifica instrumentul ML.NET CLI care acoperă aproape toate scenariile pe care le puteți găsi în Model Builder.

Instalați în global tools după cum urmează:

dotnet tool install -g mlnet

Prin urmare, de ce avem nevoie de o altă abordare pentru a obține modelele de date de intrare și ieșire sau un cod boilerplate pentru a antrena și consuma un model de machine learning?

Suntem pe cale să aflăm mai târziu în articol, dar permiteți-mi să vă prezint mai întâi Source Generators.

Introducere în Roslyn Source Generators

Roslyn este un set de compilatoare open source și API de analiză cod pentru .NET, iar generatoarele sursă Roslyn (disponibile cu C # 9) permit metaprogramarea la compilare. Aceasta înseamnă cod care poate fi creat la compilare și adăugat la rezultatul compilării.

Prin definiție, metaprogramarea este o tehnică de programare în care programele de calculator au capacitatea de a trata alte programe ca propriile lor date. Așadar,un program poate fi conceput pentru a citi / genera / analiza / sau transforma alte programe și chiar să se modifice în timp ce rulează.

Din perspectiva C #, generatoarele de surse ne permit:

Din perspectiva performanței, acesta este cel mai important lucru, deoarece aceasta este metaprogramarea la compilare. Cu siguranță, sunteți familiarizați și cu tipul de metaprogramare reflection, doar că acesta se desfășoară în runtime.

În rândurile de mai jos expunem operațiile desfășurate în compilator (dintr-o perspectivă high-level):

  1. Se citește fișierul cod sursă C # de pe disc.

  2. Se analizează textul din fișierul C # și se transformă într-un model de obiect.

  3. Se construiește arborele de sintaxă concret din modelul obiectului (vă rugăm să rețineți aici, modelul conține totul, cuvinte cheie, spații albe, astfel încât să puteți reveni de la un arbore de sintaxă înapoi la codul sursă).

  4. Arborele de sintaxă este trimis în faza de compilare.

  5. Rezultatul compilării are acum arborele dvs. de sintaxă și conține informațiile simbolice.

  6. Arborele de sintaxă și informațiile simbolice sunt trimise către un generator sursă.

  7. Generatorul sursă emite codul.

  8. Codul sursă generat este adăugat la rezultatul compilării proiectului principal.

Vă rog să rețineți că nu există niciun mecanism pentru a șterge sau înlocui codul sursă existent sau deja generat.

Puteți inspecta arborele de sintaxă într-un mod programatic, din clasa Source Generator, dar dacă doriți să vedeți cum arată, puteți verifica la adresa web https://sharplab.io/ sau chiar puteți construi un arbore, utilizând Roslyn quoter.

Să ne imaginăm următorul scenariu

Ești gata să integrezi și să consumi modelul de machine learning în codul tău.

Dar, ghinion, aveți doar modelul ML.NET (care este de fapt un fișier zip) și nu aveți nici codul sursa asociat (sau generat, pentru acesta). Este greu să găsești totul atunci când acestea sunt răspândite în mai multe foldere și proiecte. Prin urmare, ce poți face?

Desigur, poți arunca o privire în fișierul zip al modelului ML.NET pentru fișierul schemă. Schema are în interior toate numele și tipurile caracteristicilor (features) și puteți utiliza aceste informații pentru a scrie manual clasele C# pentru modelele de date de intrare și de ieșire.

Dar pentru un model nativ ML.NET, fișierul schemă este un fișier binar. Cum nu este prea elegant a extrage ceea ce aveți nevoie dintr-un fișier binar, este posibil să preferați o abordare programatică pentru a face acești pași. De aceea, în acest articol voi exemplifica un caz de utilizare pragmatic, începând de la fișierul zip al modelului ML.NET pentru a genera părțile lipsă, modelele de date de intrare și de ieșire și un cod boilerplate cu scopul de a antrena și consuma modelul de machine learning în diferite moduri.

După cum am menționat anterior, modelul ML.NET este un fișier zip care conține fișiere cu date despre weights și biases rezultate în urma antrenamentului. Pe lângă acestea, este prezent fișierul binar al schemei.

Vrem să înțelegem antetul schemei pentru a extrage numele și tipul caracteristicilor și adnotărilor asociate, dacă acestea există.

Următorul tabel descrie structura fișierului schemă.

Offsets Type Name and Description
0 ulong Signature: The magic number of this
file.
8 ulong Version: Indicates the version of the
data file.
16 ulong CompatibleVersion: Indicates the minimum
reader version that can interpret this
file, possibly with some data loss.
24 long TableOfContentsOffset: The offset to the
column table of contents structure.
32 long TailOffset: The eight-byte tail
signature starts at this offset. So, the
entire dataset stream should be
considered to have byte length of eight
plus this value.
40 long RowCount: The number of rows in this
data file.
48 int ColumnCount: The number of columns in
this data file.

Acest lucru este util în generarea modelelor de date de intrare și de ieșire. Cu toate acestea, trebuie să extragem scenariul care a fost utilizat pentru antrenarea modelului. Pentru a distinge dacă este vorba despre o clasificare binară, o clasificare multiplă, o regresie sau altele, avem nevoie să găsim care este eticheta (Label sau caracteristica țintă).

Din cauză că nu am reușit să găsesc informații despre scenariu și despre eticheta din modelul ML.NET, am adăugat în setarea AdditionalFiles din fișierul appsettings.csproj câteva atribute personalizate, numite Scenario și Label.

Să scriem niște cod

În mod ideal, schema poate fi citită dintr-un model ML.NET prin următoarea bucată de cod:

ITransformer model = mlContext.Model
  .Load("C:/Temp/MLModel0.zip", out var modelSchema);

Din păcate, acest lucru nu a avut prea mult succes, efect al dependențelor bibliotecii Microsoft.ML care nu se încarcă corect. Așa că a trebuit să urmez abordarea descrisă de acest articol citind schema model din fișierul zip.

Un generator sursă, fie un proiect, un dll sau un pachet nuget, este o bibliotecă netstandard 2.0 cu dependențe de bibliotecile Microsoft.CodeAnalysis.Analyzers și Microsoft.CodeAnalysis.CSharp. Cum limbajul C# utilizat pentru un generator sursă este C# 9, trebuie să specificați acest lucru în atributul LangVersion prin preview" sau "9.0".

Din perspectiva codării, un generator sursă este o clasă adnotată cu atributul Generator și care implementează interfața ISourceGenerator pentru metodele Initialize și Execute.

[Generator]
public class DataModelsGenerator : ISourceGenerator
{
  public void Initialize(
   GeneratorInitializationContext context)
    {
    }

  public void Execute(
   GeneratorExecutionContext context)
    {
    }
}

Proiectul care folosește generatoarele sursă are o referință la biblioteca generatoare sursă. Referința include atributele OutputItemType="Analyzer" și ReferenceOutputAssembly="false", ceea ce înseamnă că dll-ul generat nu este adăugat la biblioteca construită și funcționează ca un analyzer. Dacă sunteți familiarizați cu Roslyn Analyzers, știți exact ce înseamnă asta.

În mod implicit, clasele C# generate nu sunt emise ca fișiere fizice, dar putem rezolva acest lucru setând câteva proprietăți, inclusiv calea de ieșire pentru fișierele emise, după cum urmează:

<LangVersion>preview</LangVersion>

<EmitCompilerGeneratedFiles>
  true
</EmitCompilerGeneratedFiles>

<CompilerGeneratedFilesOutputPath>
 $(BaseIntermediateOutputPath)GeneratedMLNETFiles
</CompilerGeneratedFilesOutputPath>

Până în acest moment, avem scheletul unui proiect folosind un generator sursă. Următorul pa este să trecem la adăugarea funcționalității care citește schema modelului de machine learning din fișierul zip ML.NET și generează modelele de date de intrare și de ieșire C# pe baza schemei precum numele și tipurile de caracteristicilor (features).

[Generator]
public class DataModelsGenerator : ISourceGenerator
{
  const string ModelInput = nameof(ModelInput);
  const string ModelOutput = nameof(ModelOutput);
  const string Predictor = nameof(Predictor);
  const string Program = nameof(Program);

  public void Initialize(
  GeneratorInitializationContext context) { }

  public void Execute(
  GeneratorExecutionContext context)
  {
    (Scenario? scenario, _) = 
      GetAdditionalFileOptions(context);

    var zipFiles = context.AdditionalFiles
     .Where(f => Path.GetExtension(f.Path)
     .Equals(„.zip”, 
        StringComparison.OrdinalIgnoreCase));

     var zipFile = zipFiles.ToArray()[0].Path;

     Stream zip = null;

     zip = StreamHelper.GetZipFileStream(zipFile);
     using var reader = new BinaryReader(zip, 
       Encoding.UTF8);

     var features = StreamHelper
       .ExtractFeatures(reader);

     StringBuilder modelInputBuilder = SyntaxHelper
       .ModelInputBuilder(features, ModelInput);

     SourceText sourceText1 = SourceText
       .From(modelInputBuilder.ToString(), 
        Encoding.UTF8);

     context.AddSource($”{ModelInput}.cs”, 
     sourceText1);

     StringBuilder modelOutputBuilder = SyntaxHelper
    .ModelOutputBuilder(ModelOutput, scenario.Value);

     SourceText sourceText2 = SourceText
    .From(modelOutputBuilder.ToString(), 
       Encoding.UTF8);

     context.AddSource($”{ModelOutput}.cs”, 
     sourceText2);

     StringBuilder clientBuilder = SyntaxHelper
     .PredictorBuilder(Predictor, zipFile);

     SourceText sourceText3 = SourceText
     .From(clientBuilder.ToString(), Encoding.UTF8);

     context.AddSource($”{Predictor}.cs”, 
       sourceText3);

     StringBuilder webapiBuilder = SyntaxHelper
      .ProgramBuilder(Program, zipFile);

     SourceText sourceText4 = SourceText
      .From(webapiBuilder.ToString(), Encoding.UTF8);

     context.AddSource($”{Program}.cs”, sourceText4);
   }

  private (Scenario?, AdditionalText) 
  GetAdditionalFileOptions(
    GeneratorExecutionContext context)
   {
    var file = context.AdditionalFiles.First();
    if (Path.GetExtension(file.Path).Equals(„.zip”,
    StringComparison.OrdinalIgnoreCase))

     {
      context.AnalyzerConfigOptions.GetOptions(file)
      .TryGetValue(„build_metadata.additionalfiles
      .Scenario”, out string scenarioValue);

      Enum.TryParse(scenarioValue, true, 
      out Scenario scenario);

         return (scenario, file);
     }

     return (null, null);
  }
}

Metodele SyntaxHelper. Nu sunt incluse în codul de mai sus, dar puteți găsi întreaga soluție pe github. Acum avem clasa generatorului sursă, o putem referi și folosi în proiectul nostru. Acesta este un cod C# proaspăt generat care folosește informațiile din fișierul binar care conține schema modelul zip ML.NET.

using System;
using Microsoft.ML.Data;

namespace GeneratedDataModels
{
  public class ModelInput
  {
    [LoadColumn(0)]
    public float Temperature { get; set; }

    [LoadColumn(1)]
    public float Luminosity { get; set; }

    [LoadColumn(2)]
    public float Infrared { get; set; }

    [LoadColumn(3)]
    public float Distance { get; set; }

    [LoadColumn(4)]
    public string CreatedAt { get; set; }

    [LoadColumn(5)]
    public string Label { get; set; }

  }
}

using System;
using Microsoft.ML.Data;

namespace GeneratedDataModels
{
  public class ModelOutput
  {
    [ColumnName("PredictedLabel")]
    public string Prediction { get; set; }

    public float[] Score { get; set; }
  }
}

De aici este ușor să generăm un cod boilerplate pentru a consuma modelul ML.NET începând (doar) de la fișierul zip ML.NET.

using System;
using Microsoft.ML;

namespace GeneratedDataModels
{
  public class Predictor
  {
    public static ModelOutput Predict(ModelInput
     sampleData)
    {
      MLContext mlContext = new MLContext(seed: 1);
      ITransformer model = mlContext.Model
        .Load(„C:/Temp/MLModel1.zip”, 
         out var modelSchema);

      var predictor = mlContext.Model
      .CreatePredictionEngine<ModelInput, 
       ModelOutput>(model);

      var predicted = predictor.Predict(sampleData);
      return predicted;
    }
  }
}

Sau poate vă place un serviciu webapi:

using Microsoft.Extensions.ML;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json;
using System.Threading.Tasks;

namespace GeneratedDataModels
{
  class Program
  {
    static void Main(string[] args)
    {
      WebHost.CreateDefaultBuilder()
        .ConfigureServices(services => {
          services.AddPredictionEnginePool
            <ModelInput,ModelOutput>()
            .FromFile(„C:/Temp/MLModel1.zip”);
        })
        .Configure(app => {
          app.UseHttpsRedirection();
          app.UseRouting();
          app.UseEndpoints(routes => {
            routes.MapPost(„/predict”, 
            PredictHandler);
          });
        })
        .Build()
        .Run();
    }

    static async Task PredictHandler(HttpContext http)
    {
      var predEngine = http.RequestServices
      .GetRequiredService<PredictionEnginePool
      <ModelInput,ModelOutput>>();

      var input = await JsonSerializer
        .DeserializeAsync<ModelInput>
        (http.Request.Body);

      var prediction = predEngine.Predict(input);
      await http.Response.WriteAsJsonAsync(prediction);
    }
  }
}

Cantitatea de cod de mai sus este pentru a vă demonstra că generatoarele sursă sunt de mare ajutor pentru generarea codului redundant și plictisitor, totul realizându-se la momentul compilării! Într-adevăr, vă puteți gândi la source generators ca la un analizor de cod.

Dar nu trebuie să ne oprim aici, putem genera cod pentru a valida modelul ML.NET, pentru a măsura calitatea modelului sau pentru a sugera pipelines de antrenare a modelului.

Un alt caz de utilizare poate fi crearea propriului constructor de modele pornind de la antetul setului de date. Singura limită este propria imaginație.

Resources

  1. Roslyn Source Generators:

  2. ML.NET:

LANSAREA NUMĂRULUI 112

Prezentări articole și
Panel: DevOps

Joi, 28 Octombrie, ora 18:30

Înregistrează-te

Facebook Meetup StreamEvent YouTube

Conferință

VIDEO: NUMĂRULUI 111

Sponsori

  • Accenture
  • Bosch
  • ntt data
  • Betfair
  • FlowTraders
  • MHP
  • Connatix
  • Cognizant Softvision
  • BoatyardX
  • Colors in projects