Codeunit 2003 ML Prediction Management
- App
- Base Application
- Namespace
- System.AI
- Versions
- 17-28
Procedures, 26Events, 3Obsolete, 2
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/System/AI/MLPredictionManagement.Codeunit.al536 lines, Copyright (c) Microsoft Corporation. MIT
namespace System.AI;
using System;
using System.IO;
using System.Reflection;
using System.Text;
using System.Utilities;
codeunit 2003 "ML Prediction Management"
{
trigger OnRun()
begin
end;
var
AzureMLConnector: Codeunit "Azure ML Connector";
NotInitializedErr: Label 'The request has not been properly initialized.';
RecordVar: Variant;
LabelNo: Integer;
ConfidenceNo: Integer;
FeatureNumbers: array[25] of Integer;
LastFeatureIndex: Integer;
UsingKeyvaultCredentials: Boolean;
NotRecordVariantErr: Label 'The variant must be a record variant.';
FieldDoesNotExistErr: Label 'A field with the ID %1 does not exist.', Comment = '%1 = field ID';
TooManyFeaturesErr: Label 'Cannot train or predict because you have added more than %1 features to the model.', Comment = '%1 = max number of features';
LabelCannotBeFeatureErr: Label 'You have used the same field as the feature and as the label. A field can be either the label or feature, but not both.';
FeatureRepeatedErr: Label 'You can add a field as a feature only one time.';
TrainingPercent: Decimal;
ApiUri: Text[250];
ApiKey: SecretText;
ApiTimeout: Integer;
TrainingPercentageErr: Label 'The training percentage must be a decimal number between 0 and 1.';
SomethingWentWrongErr: Label 'Oops, something went wrong when connecting to the Azure Machine Learning endpoint. Please contact your system administrator. %1.', Comment = '%1 = detailed error';
DetailedErrorErr: Label 'Details: ';
ErrorResponseTxt: Label 'Error code: ', Locked = true;
AzureMachineLearningLimitReachedErr: Label 'The Microsoft Azure Machine Learning limit has been reached. Please contact your system administrator.';
DownloadModelPlotLbl: Label 'Download model visualization in pdf format.';
MachineLearningSecretNameTxt: Label 'machinelearning', Locked = true;
NoCredentialsInKeyVaultErr: Label 'There were no machine learning credentials in the key vault or the credentials were corrupted.';
procedure DefaultInitialize()
begin
LabelNo := 0;
ConfidenceNo := 0;
LastFeatureIndex := 0;
TrainingPercent := 0.8;
end;
procedure Initialize(Uri: Text[250]; "Key": SecretText; TimeOutSeconds: Integer)
begin
ApiUri := Uri;
ApiKey := Key;
ApiTimeout := TimeOutSeconds;
UsingKeyvaultCredentials := false;
DefaultInitialize();
end;
[NonDebuggable]
procedure InitializeWithKeyVaultCredentials(TimeOutSeconds: Integer)
var
AzureAIUsage: Codeunit "Azure AI Usage";
AzureAIService: Enum "Azure AI Service";
LimitType: Option;
LimitValue: Decimal;
begin
AzureAIService := AzureAIService::"Machine Learning";
if AzureAIUsage.GetTotalProcessingTime(AzureAIService) > AzureAIUsage.GetResourceLimit(AzureAIService) then
Error(AzureMachineLearningLimitReachedErr);
GetMachineLearningCredentials(ApiUri, ApiKey, LimitType, LimitValue);
ApiTimeout := TimeOutSeconds;
UsingKeyvaultCredentials := true;
DefaultInitialize();
end;
[Scope('OnPrem')]
procedure SetMessageHandler(MessageHandler: DotNet HttpMessageHandler)
begin
AzureMLConnector.SetMessageHandler(MessageHandler);
end;
procedure SetRecord(RecordVariant: Variant)
var
DataTypeManagement: Codeunit "Data Type Management";
RecRef: RecordRef;
begin
if not DataTypeManagement.GetRecordRef(RecordVariant, RecRef) then
Error(NotRecordVariantErr);
RecordVar := RecordVariant;
end;
procedure GetRecord(var RecordVariant: Variant)
begin
RecordVariant := RecordVar;
end;
procedure AddFeature(FeatureFieldNo: Integer)
var
FeatureIndex: Integer;
begin
if LastFeatureIndex >= MaxNoFeatures() then
Error(TooManyFeaturesErr, MaxNoFeatures());
if FeatureFieldNo = LabelNo then
Error(LabelCannotBeFeatureErr);
TestFieldNumber(FeatureFieldNo);
for FeatureIndex := 1 to LastFeatureIndex do
if FeatureNumbers[FeatureIndex] = FeatureFieldNo then
Error(FeatureRepeatedErr);
LastFeatureIndex := LastFeatureIndex + 1;
FeatureNumbers[LastFeatureIndex] := FeatureFieldNo;
end;
procedure SetLabel(LabelFieldNo: Integer)
var
DataTypeManagement: Codeunit "Data Type Management";
RecRef: RecordRef;
begin
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
TestFieldNumber(LabelFieldNo);
LabelNo := LabelFieldNo;
end;
procedure SetConfidence(ConfidenceFieldNo: Integer)
var
DataTypeManagement: Codeunit "Data Type Management";
RecRef: RecordRef;
begin
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
TestFieldNumber(ConfidenceFieldNo);
ConfidenceNo := ConfidenceFieldNo;
end;
procedure SetTrainingPercent(TrainingPercentValue: Decimal)
begin
if (TrainingPercentValue <= 0) or (TrainingPercentValue >= 1) then
Error(TrainingPercentageErr);
TrainingPercent := TrainingPercentValue;
end;
procedure GetTrainingPercent(): Decimal
begin
exit(TrainingPercent);
end;
procedure Train(var Model: Text; var Quality: Decimal)
var
OutputValue: Text;
CallAzureEndPoint: Boolean;
begin
CallAzureEndPoint := true;
OnBeforeTrain(Model, Quality, CallAzureEndPoint);
if not CallAzureEndPoint then
exit;
InitializeAzureMLConnector();
TestFeatureLabelInitialized();
AzureMLConnector.AddParameter('method', 'train');
AzureMLConnector.AddParameter('train_percent', Format(TrainingPercent, 0, 9));
CreateInput();
if not AzureMLConnector.SendToAzureMLInternal(UsingKeyvaultCredentials) then
Error(SomethingWentWrongErr, GetLastDetailedError());
AzureMLConnector.GetOutput(1, 1, OutputValue);
Model := OutputValue;
AzureMLConnector.GetOutput(1, 2, OutputValue);
SYSTEM.Evaluate(Quality, OutputValue, 9);
end;
procedure Predict(Model: Text)
var
CallAzureEndPoint: Boolean;
begin
CallAzureEndPoint := true;
OnBeforePredict(RecordVar, CallAzureEndPoint);
if not CallAzureEndPoint then
exit;
InitializeAzureMLConnector();
TestFeatureLabelInitialized();
AzureMLConnector.AddParameter('method', 'predict');
AzureMLConnector.AddParameter('model', Model);
CreateInput();
if not AzureMLConnector.SendToAzureMLInternal(UsingKeyvaultCredentials) then
Error(SomethingWentWrongErr, GetLastDetailedError());
LoadPrediction();
end;
procedure Evaluate(Model: Text; var Quality: Decimal)
var
OutputValue: Text;
CallAzureEndPoint: Boolean;
begin
CallAzureEndPoint := true;
OnBeforeEvaluate(Model, Quality, RecordVar, CallAzureEndPoint);
if not CallAzureEndPoint then
exit;
InitializeAzureMLConnector();
TestFeatureLabelInitialized();
AzureMLConnector.AddParameter('method', 'evaluate');
AzureMLConnector.AddParameter('model', Model);
if CreateInput() then begin
if not AzureMLConnector.SendToAzureMLInternal(UsingKeyvaultCredentials) then
Error(SomethingWentWrongErr, GetLastDetailedError());
AzureMLConnector.GetOutput(1, 1, OutputValue);
SYSTEM.Evaluate(Quality, OutputValue);
end;
end;
procedure PlotModel(Model: Text; Features: Text; Labels: Text): Text
var
Result: Text;
begin
InitializeAzureMLConnector();
AzureMLConnector.AddParameter('method', 'plotmodel');
AzureMLConnector.AddParameter('model', Model);
if Features <> '' then
AzureMLConnector.AddParameter('captions', StrSubstNo('"%1"', Features));
if Labels <> '' then
AzureMLConnector.AddParameter('labels', StrSubstNo('"%1"', Labels));
CreateDummyInput();
if not AzureMLConnector.SendToAzureMLInternal(UsingKeyvaultCredentials) then
Error(SomethingWentWrongErr, GetLastDetailedError());
AzureMLConnector.GetOutput(1, 1, Result);
exit(Result);
end;
procedure DownloadPlot(PdfDataBase64: Text; ModelName: Text)
var
Base64Convert: Codeunit "Base64 Convert";
TempBlob: Codeunit "Temp Blob";
OutStream: OutStream;
InStr: InStream;
begin
TempBlob.CreateOutStream(OutStream);
Base64Convert.FromBase64(PdfDataBase64, OutStream);
TempBlob.CreateInStream(InStr);
ModelName := StrSubstNo('%1.pdf', ModelName);
DownloadFromStream(InStr, DownloadModelPlotLbl, '', '*.pdf', ModelName);
end;
procedure IsDataSufficientForClassification(): Boolean
var
TypeHelper: Codeunit "Type Helper";
RecRef: RecordRef;
FieldRef: FieldRef;
LabelDict: DotNet GenericDictionary2;
TotalRecordCount: Integer;
LabelDictKey: Text;
LabelDictValue: Integer;
Element: Integer;
MinLabelCount: Integer;
MinLabelCountInitialized: Boolean;
begin
if not IsDataSufficientBase(RecRef) then
exit(false);
CreateLabelDictionary(LabelDict);
repeat
FieldRef := RecRef.Field(LabelNo);
LabelDictKey := Format(FieldRef.Value, 0, 9);
if LabelDict.TryGetValue(LabelDictKey, LabelDictValue) then begin
LabelDict.Remove(LabelDictKey);
LabelDict.Add(LabelDictKey, LabelDictValue + 1);
end else
LabelDict.Add(LabelDictKey, 1);
TotalRecordCount += 1;
until RecRef.Next() = 0;
MinLabelCountInitialized := false;
foreach Element in LabelDict.Values do
if not MinLabelCountInitialized then begin
MinLabelCount := Element;
MinLabelCountInitialized := true;
end else
if Element < MinLabelCount then
MinLabelCount := Element;
if MinLabelCount = TotalRecordCount then
exit(false); // there is only one label in the dataset
// The certainity needed for at least one of the labels to be in the training set should be 99%
exit(TotalRecordCount >= (TypeHelper.CalculateLog(1 - 0.99) /
TypeHelper.CalculateLog(1 - (MinLabelCount / TotalRecordCount)) /
TrainingPercent));
end;
procedure IsDataSufficientForRegression(): Boolean
var
RecRef: RecordRef;
begin
exit(IsDataSufficientBase(RecRef));
end;
procedure IsDataSufficientBase(var RecRef: RecordRef): Boolean
var
DataTypeManagement: Codeunit "Data Type Management";
begin
TestFeatureLabelInitialized();
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
if not RecRef.FindSet() then
exit(false);
// 20 is the minimum number of data points for which the decision
// tree algorithm we are using will create a model with multiple nodes
exit(TrainingPercent * RecRef.Count >= 20);
end;
[NonDebuggable]
local procedure InitializeAzureMLConnector()
begin
if not AzureMLConnector.Initialize(ApiKey, ApiUri, ApiTimeout) then
Error(NotInitializedErr, GetLastDetailedError());
end;
local procedure CreateLabelDictionary(var LabelDict: DotNet GenericDictionary2)
var
Type: DotNet Type;
Activator: DotNet Activator;
Arr: DotNet Array;
DummyString: DotNet String;
DummyInt: Integer;
begin
// A new object of type dictionary<String, int> is created. This object holds the Label as the key and the
// value as the number of records in the dataset with that label.
Arr := Arr.CreateInstance(GetDotNetType(Type), 2);
Arr.SetValue(GetDotNetType(DummyString), 0);
Arr.SetValue(GetDotNetType(DummyInt), 1);
Type := GetDotNetType(LabelDict);
Type := Type.MakeGenericType(Arr);
LabelDict := Activator.CreateInstance(Type);
end;
local procedure TestFieldNumber(FieldNumber: Integer)
var
DataTypeManagement: Codeunit "Data Type Management";
RecRef: RecordRef;
begin
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
if not RecRef.FieldExist(FieldNumber) then
Error(FieldDoesNotExistErr, FieldNumber);
end;
local procedure TestFeatureLabelInitialized()
var
Initialized: Boolean;
begin
Initialized := true;
if Format(RecordVar) = '' then
Initialized := false;
if LabelNo = 0 then
Initialized := false;
if LastFeatureIndex = 0 then
Initialized := false;
if not Initialized then
Error(NotInitializedErr);
end;
local procedure CreateInput(): Boolean
var
DataTypeManagement: Codeunit "Data Type Management";
RecRef: RecordRef;
FieldRef: FieldRef;
ColumnNo: Integer;
begin
for ColumnNo := 1 to MaxNoFeatures() do
AzureMLConnector.AddInputColumnName(StrSubstNo('feature%1', ColumnNo));
AzureMLConnector.AddInputColumnName('label');
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
if not RecRef.FindSet() then
exit(false);
repeat
AzureMLConnector.AddInputRow();
for ColumnNo := 1 to MaxNoFeatures() do
if ColumnNo <= LastFeatureIndex then begin
FieldRef := RecRef.Field(FeatureNumbers[ColumnNo]);
if FieldRef.Class = FieldClass::FlowField then
FieldRef.CalcField();
AzureMLConnector.AddInputValue(Format(FieldRef.Value, 0, 9));
AzureMLConnector.AddParameter(StrSubstNo('featuretype%1', ColumnNo), Format(FieldRef.Type));
end else
AzureMLConnector.AddInputValue('');
FieldRef := RecRef.Field(LabelNo);
if FieldRef.Class = FieldClass::FlowField then
FieldRef.CalcField();
AzureMLConnector.AddInputValue(Format(FieldRef.Value, 0, 9));
AzureMLConnector.AddParameter('labeltype', Format(FieldRef.Type));
until RecRef.Next() = 0;
exit(true);
end;
local procedure CreateDummyInput()
var
ColumnNo: Integer;
begin
for ColumnNo := 1 to MaxNoFeatures() do
AzureMLConnector.AddInputColumnName(StrSubstNo('feature%1', ColumnNo));
AzureMLConnector.AddInputColumnName('label');
AzureMLConnector.AddInputRow();
for ColumnNo := 1 to MaxNoFeatures() + 1 do
AzureMLConnector.AddInputValue('0');
end;
procedure GetParameter(Name: Text): Text
var
ParameterValue: Text;
begin
AzureMLConnector.GetParameter(Name, ParameterValue);
exit(ParameterValue);
end;
procedure GetInput(RowNo: Integer; ColumnNo: Integer): Text
var
InputValue: Text;
begin
AzureMLConnector.GetInput(RowNo, ColumnNo, InputValue);
exit(InputValue);
end;
procedure GetInputLength(): Integer
var
Length: Integer;
begin
AzureMLConnector.GetInputLength(Length);
exit(Length);
end;
local procedure GetLastDetailedError(): Text
var
DetailedErrorTxt: Text;
DetailedErrorStart: Integer;
begin
DetailedErrorStart := StrPos(GetLastErrorText, ErrorResponseTxt);
if DetailedErrorStart > 0 then begin
DetailedErrorTxt := CopyStr(GetLastErrorText, StrPos(GetLastErrorText, ErrorResponseTxt));
DetailedErrorTxt := CopyStr(DetailedErrorTxt, 1, StrPos(DetailedErrorTxt, '\') - 1);
exit('\' + DetailedErrorErr + DetailedErrorTxt);
end;
exit('\' + DetailedErrorErr + GetLastErrorText);
end;
local procedure LoadPrediction()
var
DataTypeManagement: Codeunit "Data Type Management";
ConfigValidateManagement: Codeunit "Config. Validate Management";
RecRef: RecordRef;
FieldRef: FieldRef;
LabelColumnNumber: Integer;
ConfidenceColumnNumber: Integer;
RowNumber: Integer;
Label: Text[250];
Confidence: Text[250];
OutputValue: Text;
begin
RowNumber := 1;
LabelColumnNumber := MaxNoFeatures() + 1;
ConfidenceColumnNumber := LabelColumnNumber + 1;
DataTypeManagement.GetRecordRef(RecordVar, RecRef);
if RecRef.FindSet() then
repeat
if ConfidenceNo <> 0 then begin
AzureMLConnector.GetOutput(RowNumber, ConfidenceColumnNumber, OutputValue);
Confidence := CopyStr(OutputValue, 1, MaxStrLen(Confidence));
FieldRef := RecRef.Field(ConfidenceNo);
ConfigValidateManagement.EvaluateValueWithValidate(FieldRef, Confidence, true);
end;
AzureMLConnector.GetOutput(RowNumber, LabelColumnNumber, OutputValue);
Label := CopyStr(OutputValue, 1, MaxStrLen(Label));
FieldRef := RecRef.Field(LabelNo);
ConfigValidateManagement.EvaluateValueWithValidate(FieldRef, Label, true);
RecRef.Modify(true);
RowNumber := RowNumber + 1;
until RecRef.Next() = 0;
if RecRef.FindSet() then
RecRef.SetTable(RecordVar);
end;
procedure MaxNoFeatures(): Integer
begin
exit(ArrayLen(FeatureNumbers));
end;
[TryFunction]
[Scope('OnPrem')]
procedure GetMachineLearningCredentials(var MLApiUri: Text[250]; var MLApiKey: SecretText; var LimitType: Option; var Limit: Decimal)
var
MachineLearningKeyVaultMgmt: Codeunit "Machine Learning KeyVaultMgmt.";
begin
MachineLearningKeyVaultMgmt.GetMachineLearningCredentials(MachineLearningSecretNameTxt, MLApiUri, MLApiKey, LimitType, Limit);
if MLApiUri = '' then
Error(NoCredentialsInKeyVaultErr);
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeTrain(var Model: Text; var Quality: Decimal; var CallAzureEndPoint: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforePredict(var RecordVariant: Variant; var CallAzureEndPoint: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnBeforeEvaluate(Model: Text; var Quality: Decimal; var RecordVariant: Variant; var CallAzureEndPoint: Boolean)
begin
end;
}