Smart Domains

Smart Domains

Domain Layer Design, Event Sourcing & AI

Matteo è Senior Software Developer, aspirante Yogi e Runner. Nell’articolo di oggi descriverà come ingegnerizzare l’enterprise con l’intelligenza artificiale, portando l’esempio di un’azienda (import/export) che ha una flotta di mezzi da gestire.

Introduzione

Il trend crescente delle tecnologie 4.0 [1], fornisce una nuova modalità per trasformare dati prodotti da vecchi e nuovi sistemi informatici in nuovo valore aggiunto.

Grazie alla quantità, varietà e disponibilità di questi, specialmente la famiglia degli algoritmi di machine learning (ML) e data mining sono in grado di sfruttare al meglio e trasformare ogni dato prodotto in nuove informazioni utili. È certo che molte aziende che investiranno e sapranno sfruttare queste nuove tecnologie ne avranno un notevole beneficio nel prossimo futuro.

Mentre gli investimenti in IA crescono a buon ritmo, la richiesta di sviluppatori esperti nel campo non fa altrettanto. Infatti, la progressiva adozione di queste nuove tecnologie sta dividendo nettamente le categorie di sviluppatori creando nuove mansioni altamente specializzate.

Molte aziende scelgono infatti i grandi fornitori di servizi come Amazon e Google per soddisfare la crescente richiesta tecnologica relativa al campo dell’IA.

Ma è sempre la scelta più conveniente?

Penso sia ragionevole pensare che “non sempre” sia la risposta giusta.

Ci sono molti casi in cui lo sviluppo custom di una soluzione che sfrutta o addirittura incorpora l’IA può apportare grandi benefici, specialmente quando il settore tecnologico è situato in una “nicchia” di mercato.

Gli strumenti e librerie disponibili già realizzati non sempre sono adattabili ad ogni esigenza e spesso esiste una migliore implementazione più aderente alla necessità.

Credo che in tutti quei casi in cui l’intelligenza (artificiale) rappresenta uno degli aspetti basilari e principali di un prodotto software o una delle sue più utili peculiarità, lo sviluppo custom di queste debba essere preso in considerazione.

Ci sono scenari d’uso ove questo è già realtà da tempo, robot per la pulizia casalinga, robot operatori nelle catene di montaggio e molte altre applicazioni nel campo della logistica.

Ad ogni modo penso che nel prossimo futuro molti avranno la possibilità di aggiungere alle loro soluzioni l’intelligenza artificiale come parte stessa (by design) del proprio business.

Ma come e perché farlo?

L’orientamento agli eventi piuttosto che alle sole entità (quando possibile) è divenuto piuttosto diffuso ed è ideale per incorporare l’IA direttamente a livello di design.

In questo articolo, cercherò di illustrare come approcciare un caso tipico utilizzando principalmente due tecnologie, Event Sourcing e Intelligenza artificiale a livello di modellazione degli oggetti del dominio [2].

Gli eventi che descrivono gli oggetti possono essere utilizzati per alimentare algoritmi intelligenti (classificatori) e di tipo statistico. Questi permettono così di aggiungere alcune proprietà, che chiameremo “smart”, agli oggetti stessi.

Un mezzo di trasporto ad esempio, oltre le proprietà che lo descrivono potrà esibire anche delle grandezze che esprimono una probabilità o una previsione.

È possibile far interagire queste proprietà direttamente con le regole di business: real-time senza una elaborazione a posteriori.

Questo colloca intelligenza e analisi dei dati direttamente nel dominio, contrariamente a ciò che molto spesso viene fatto oggi in molte implementazioni ove l’analisi tramite ML avviene in un secondo momento da file di log o metriche.

Tutti i concetti illustrati si basano su codice consultabile al seguente url: https://github.com/sandhaka/SmartDomainsCaseStudy

Un’azienda di import/export deve gestire una flotta di mezzi di trasporto. Nell’esempio è stata utilizzata una mappa semplificata per rendere più leggibile e comprensibile l’intero progetto.

Aggregati “Event sourced”

Capture all changes to an application state as a sequence of events.
[M. Fowler]

Se non lo conoscete già, Event Sourcing significa riferirsi a un oggetto non unicamente con il suo stato corrente, ma attraverso la serie di eventi verificatosi elaborati lungo tutta la sua vita [3]. Solo questi eventi saranno memorizzati, lo stato al momento desiderato sarà calcolato Runtime.

Come vale per un conto bancario, che non è una semplice tabella con l’importo del saldo all’ora di lettura corrente, così ogni camion della flotta può essere visto come un elenco di eventi relativi a tutte le sue partenze e arrivi.

Assieme a questi eventi, le metriche riportano tutti gli effetti del viaggio sul mezzo di trasporto e autista.

Semplified UML used

L’oggetto “truck” ha alcune proprietà come il codice modello, la posizione corrente e alcune metriche sullo stato corrente. Il valore decimale: AverageFatigueLastWeek descrive la mediana dello stress subito dal conducente nell’ultima settimana.

L’implementazione proposta sotto, elabora tutti gli eventi e calcola l’aggregato al momento di lettura.

				
					...
protected override void When(DomainEvent @event)
{
      DomainLog?.Invoke(".", false);
     
      var latestChanges = LastChanges
            .Cast<RecordData>()
            .ToList();
      switch (@event)
      {
            // Handle arrival event stats
            case TransportArrival arrivalEvent:
            {
                  Location = arrivalEvent.Location;
                  CumulativeDelay += arrivalEvent.Delay;
                  AverageDelayLastWeek = TimeSpan.FromMinutes(
                        latestChanges
                              .FilterCast<TransportArrival>()
                              .Take(7)
                              .Average(evt => evt.Delay.TotalMinutes));
                  AverageFatigueLastWeek = latestChanges
                        .FilterCast<TransportArrival>()
                        .Take(7)
                        .Average(d => d.Fatigue);
                  AverageFatigueLast30Days = latestChanges
                        .FilterCast<TransportArrival>()
                        .Take(30)
                        .Average(d => d.Fatigue);
                  TotalAccidents = latestChanges
                        .FilterCast<TransportArrival>()
                        .Count(evt => evt.HadAccident);
                 
                  break;
            }
            case TransportDeparture departureEvent:
            {
                  Location = "-";
                  break;
            }
      }
}
...
				
			

Alla partenza non viene aggiornato nulla, perché le metriche vengono raccolte alla fine del viaggio.

L’aggregato può essere salvato o riletto come flusso di eventi (implementato nel codebase con un “in-memory” store).

Tutti gli eventi di dominio che rappresentano l’oggetto camion, poco sorprendentemente possono alimentare anche un algoritmo ML. Questo può imparare da tutte queste metriche circa il comportamento di ogni coppia camion-guidatore.

Un classificatore Naive Bayes stima la probabilità di avere un incidente durante il prossimo viaggio deducendolo appunto da questa sua storia.

Smart Properties

I classificatori Naive Bayes [4] sono una famiglia di semplici “classificatori probabilistici” basati sull’applicazione del teorema di Bayes.

L’implementazione dell’interfaccia IWiseActor del livello di dominio ci permette di sfruttare queste capacità e di prevedere un’incidenza probabilistica futura di un incidente in base alle attuali metriche di fatica del conducente, alle condizioni meteorologiche, alle statistiche storiche e così via.

Wise Actor infrastructure implementation UML

Il metodo Update permette di aggiungere o aggiornare il modello statistico del camion.

Mentre la variabile learner è un’implementazione di un classificatore “Naive Bayes” di terze parti.

				
					...
public void Update(TransportTruck transportTruck)
{
      var translatedData = Parse(transportTruck.Changes.Cast<RecordData>());
     
      var itemModel = _learner.Learn(translatedData.x, translatedData.y);
     
      if (!_bayesianModel.ContainsKey(transportTruck.Id))
      {
            _bayesianModel.Add(transportTruck.Id, itemModel);
            return;
      }
      _bayesianModel[transportTruck.Id] = itemModel;
}
...
				
			

In realtà, la base di codice proposta è già piuttosto corposa per essere usata come esempio, tuttavia anche se il dominio è solo un giocattolo, tutte le strutture necessarie per implementare una versione di aggregato Event sourcing sono molte.

Dopo la creazione della flotta (una raccolta di oggetti TransportTruck), una routine popola la lista degli eventi di ogni camion con alcuni dati precedentemente definiti. Queste metriche descrivono tutti i viaggi passati.

				
					[
  [
    {
      "DepartureLocation": "VERONA",
      "ArrivalLocation": "MONZA",
      "DepartureTime": "2020-09-11T11:27:31.0923247Z",
      "ArrivalTime": "2020-09-11T15:15:26.6063433Z",
      "Delay": "01:44:00",
      "WeatherCode": "Wet",
      "FatigueScore": 0.0386380793706691,
      "Accident": false
    },
    {
      "DepartureLocation": "MONZA",
      "ArrivalLocation": "MANTUA",
      "DepartureTime": "2020-09-12T08:15:26.6063433Z",
      "ArrivalTime": "2020-09-12T10:42:17.132659Z",
      "Delay": "00:00:00",
      "WeatherCode": "Foggy",
      "FatigueScore": 1.5447707002773186,
      "Accident": false
    },
    {
      ...
				
			

Ho usato xUnit per eseguire facilmente gli esempi, nel progetto di unit-test: TransportFleet.UseCase, il metodo ChooseTruckCandidateWith-LowerAccidentProbability fa il necessario e dimostra quanto sopra.

La parte interessante è la seguente, il metodo PredictAccident è incorporato nell’aggregato del dominio come un qualsiasi altro metodo di classe nonostante nasconda il necessario per ottenere i risultati ottenuti dal ML.

				
					...
_fleet.ForEach(truck =>
{
      // Trigger internal model updating
      truck.UpdateStats();
     
      var prediction = truck.PredictAccident(goodWeather);
      var prob = prediction.Probabilities.ToList();
     
      DemoLogger.InfLog($"Truck {truck.ModelCode}: {prob[0].Label}-> {prob[0].ProbabilityScore:P}, " +
                                $"{prob[1].Label}-> {prob[1].ProbabilityScore:P}");
     
      rank.Add(truck.Id, prob[1].ProbabilityScore);
});
var candidateId = rank.OrderBy(c => c.Value).First().Key;
var candidate = _fleet.Find(f => f.Id.Equals(candidateId));
...
				
			

È possibile eseguire questo test dalla cartella del progetto:

				
					dotnet test --filter TransportFleet.UseCase.TestCase.ChooseTruckCandidateWithLowerAccidentProbability

				
			

Ottenendo l’output seguente:

				
					[APP] Create fleet ...
[APP] Fleet with 3 units created
[APP] Adding historical data to fleet...
..............................................................................................................................................................................................................................................................................................................................................................................................
[APP] Fleet historical data initialization done
[APP] Estimate the probability to have an accident:
[APP] Truck COJJAU: No-> 89,53%, Yes-> 10,47%
[APP] Truck CVOCJX: No-> 100,00%, Yes-> 0,00%
[APP] Truck QLVQQG: No-> 97,82%, Yes-> 2,18%
[APP] Best truck: Code= CVOCJX Current Location= TREVIGLIO AverageLastWeekFatigue= 0,23488760270335357 AverageLastWeekDelay= 00:00:00 TotalAccidents= 0 Events= 100
				
			

Il modello del dominio è piuttosto semplificato e la previsione è banale usando solo alcune delle molte metriche ipotizzabili per una previsione di questo tipo.

Tuttavia, il codice è completo e utile per costruire casi d’uso più complessi.

Più AI, Constraint satisfaction

Il caso d’uso ML basato su Naive Bayes mostrato sopra è uno scenario molto comune.

Constraint satisfaction problem (CSP) è un altro ben noto modello matematico usato in intelligenza artificiale e ricerca operativa.

L’implementazione proposta è basata sul concetto di “backtracking”, in cui un algoritmo progressivamente abbandona dei valori candidati definiti a priori quando questi non sono in grado di soddisfare i vincoli del problema in questione.

Nell’esempio, dobbiamo organizzare tre viaggi per ogni giorno della settimana (dal lunedì al sabato) automaticamente utilizzando una flotta di sei camion.

Questo esempio è più elaborato del precedente. Prima viene creata una rappresentazione degli oggetti del dominio e poi viene elaborata direttamente da un generico algoritmo di risoluzione.

Il punto più interessante è la possibilità di inserire uno o più vincoli senza preoccuparsi troppo circa l’esistenza di una soluzione. L’algoritmo troverà quella che soddisfa la richiesta o una delle configurazioni più vicine possibili.

				
					_fleet = DemoFleetDataFactory.CreateRandomFleet(6);
// Create Csp data problem from current fleet status
var cspData = FleetCsp.FleetToCspModel(DemoFleetDataFactory.CreateCspProblemVariables().ToList(), _fleet);
// Instantiate Csp-Model
var fleetCsp = CspFactory.Create(
      new Dictionary<string, IEnumerable<TruckCspValue>>(cspData.Select(v => new KeyValuePair<string, IEnumerable<TruckCspValue>>(v.Key, v.Value.Domains))),
      new Dictionary<string, IEnumerable<string>>(cspData.Select(v => new KeyValuePair<string, IEnumerable<string>>(v.Key, v.Value.Relations))),
      new Func<string, TruckCspValue, string, TruckCspValue, bool>[]
      {
            // Constraint: Do not overlap on the same day
            (variableA, transportTruckA, variableB, transportTruckB) =>
            {
                  var dayA = variableA.Split('.').First();
                  var dayB = variableB.Split('.').First();
                  return dayA != dayB || transportTruckA != transportTruckB;
            }
      });
// Resolve the given problem
var solved = fleetCsp.UseBackTrackingSearchResolver(
      SelectUnassignedVariableStrategyTypes<TruckCspValue>.FirstUnassignedVariable,
        DomainValuesOrderingStrategyTypes<TruckCspValue>.DomainCustomOrder)
      .Resolve(()=>
      {
            PrintPlan(fleetCsp);
      });
...
				
			

Per comprendere completamente il codice di cui sopra, probabilmente è necessario guardare l’implementazione del risolutore CSP utilizzata nell’esempio.

In ogni caso, anche questo è un altro buon candidato da includere direttamente a livello di business layer quando gli aggregati devono essere in grado di risolvere dei problemi di ottimizzazione combinatoria come quello qui descritto.

				
					dotnet test --filter TransportFleet.UseCase.TestCase.OptimizeAndAssignTrips
				
			
				
					[APP] Create fleet ...
[APP]
 --------------------------------------------------------------------
 | Week Day | Travel 1        | Travel 2         | Travel 3         |
 --------------------------------------------------------------------
 | MON      | Mon.Tr0: LHEDEM | Mon.Tr7: IRKYAC  | Mon.Tr14: ZYVFNZ |
 --------------------------------------------------------------------
 | TUE      | Tue.Tr1: IRKYAC | Tue.Tr8: LHEDEM  | Tue.Tr15: INFQXW |
 --------------------------------------------------------------------
 | WEN      | Wen.Tr2: ZYVFNZ | Wen.Tr9: INFQXW  | Wen.Tr16: LHEDEM |
 --------------------------------------------------------------------
 | THU      | Thu.Tr3: INFQXW | Thu.Tr10: ZYVFNZ | Thu.Tr17: IRKYAC |
 --------------------------------------------------------------------
 | FRI      | Fri.Tr4: NRDMOR | Fri.Tr11: IUVKFI | Fri.Tr18: LHEDEM |
 --------------------------------------------------------------------
 | SAT      | Sat.Tr5: IUVKFI | Sat.Tr12: NRDMOR | Sat.Tr19: IRKYAC |
 --------------------------------------------------------------------
Truck: LHEDEM, Total travels: 4
Truck: IRKYAC, Total travels: 4
Truck: ZYVFNZ, Total travels: 3
Truck: INFQXW, Total travels: 3
Truck: NRDMOR, Total travels: 2
Truck: IUVKFI, Total travels: 2
				
			

La pianificazione viene effettuata senza sovrapposizioni tra i giorni.

Se si vuole aggiungere un nuovo vincolo è molto semplice. Voglio assicurarmi che nessun camion faccia due viaggi consecutivi il lunedì e il martedì, per assurdo una regola di business stabilisce ad esempio che è troppo stressante.

				
					...
(variableA, transportTruckA, variableB, transportTruckB) =>
{
      var dayA = variableA.Split('.').First();
      var dayB = variableB.Split('.').First();
      if (
            string.Compare(dayA, "mon", StringComparison.OrdinalIgnoreCase) == 0 &&
            string.Compare(dayB, "tue", StringComparison.OrdinalIgnoreCase) == 0 ||
            string.Compare(dayA, "tue", StringComparison.OrdinalIgnoreCase) == 0 &&
            string.Compare(dayB, "mon", StringComparison.OrdinalIgnoreCase) == 0
      )
      {
            return transportTruckA != transportTruckB;
      }
      return true;
}
...
				
			

Il risultato cambierà automaticamente in questo modo:

				
					[APP] Create fleet ...
[APP]
 --------------------------------------------------------------------
 | Week Day | Travel 1        | Travel 2         | Travel 3         |
 --------------------------------------------------------------------
 | MON      | Mon.Tr0: SAHWPW | Mon.Tr7: OCKRCS  | Mon.Tr14: NIMBOC |
 --------------------------------------------------------------------
 | TUE      | Tue.Tr1: YQRCQG | Tue.Tr8: WVBFOQ  | Tue.Tr15: RBOZJD |
 --------------------------------------------------------------------
 | WEN      | Wen.Tr2: OCKRCS | Wen.Tr9: SAHWPW  | Wen.Tr16: YQRCQG |
 --------------------------------------------------------------------
 | THU      | Thu.Tr3: WVBFOQ | Thu.Tr10: YQRCQG | Thu.Tr17: SAHWPW |
 --------------------------------------------------------------------
 | FRI      | Fri.Tr4: NIMBOC | Fri.Tr11: RBOZJD | Fri.Tr18: OCKRCS |
 --------------------------------------------------------------------
 | SAT      | Sat.Tr5: RBOZJD | Sat.Tr12: NIMBOC | Sat.Tr19: WVBFOQ |
 --------------------------------------------------------------------
Truck: SAHWPW, Total travels: 3
Truck: YQRCQG, Total travels: 3
Truck: OCKRCS, Total travels: 3
Truck: WVBFOQ, Total travels: 3
Truck: NIMBOC, Total travels: 3
Truck: RBOZJD, Total travels: 3
				
			

Si noti che nessun camion che viaggia il lunedì lo farà il martedì e viceversa.

È un sistema veramente utile per cambiare dinamicamente uno scenario complesso quando ho bisogno di ottimizzare l’assegnazione delle risorse in base a proprietà, vincoli, etc.

Leave a comment

ItalyEnglishFrenchGerman