using System;
using System.Collections.Generic;
using System.Linq;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Policies;
using Unity.Barracuda;
namespace Unity.MLAgents.Inference
{
///
/// Prepares the Tensors for the Learning Brain and exposes a list of failed checks if Model
/// and BrainParameters are incompatible.
///
internal class BarracudaModelParamLoader
{
enum ModelActionType
{
Unknown,
Discrete,
Continuous
}
const long k_ApiVersion = 2;
///
/// Generates the Tensor inputs that are expected to be present in the Model.
///
///
/// The Barracuda engine model for loading static parameters.
///
/// TensorProxy IEnumerable with the expected Tensor inputs.
public static IReadOnlyList GetInputTensors(Model model)
{
var tensors = new List();
if (model == null)
return tensors;
foreach (var input in model.inputs)
{
tensors.Add(new TensorProxy
{
name = input.name,
valueType = TensorProxy.TensorType.FloatingPoint,
data = null,
shape = input.shape.Select(i => (long)i).ToArray()
});
}
foreach (var mem in model.memories)
{
tensors.Add(new TensorProxy
{
name = mem.input,
valueType = TensorProxy.TensorType.FloatingPoint,
data = null,
shape = TensorUtils.TensorShapeFromBarracuda(mem.shape)
});
}
tensors.Sort((el1, el2) => el1.name.CompareTo(el2.name));
return tensors;
}
public static int GetNumVisualInputs(Model model)
{
var count = 0;
if (model == null)
return count;
foreach (var input in model.inputs)
{
if (input.shape.Length == 4)
{
if (input.name.StartsWith(TensorNames.VisualObservationPlaceholderPrefix))
{
count++;
}
}
}
return count;
}
///
/// Generates the Tensor outputs that are expected to be present in the Model.
///
///
/// The Barracuda engine model for loading static parameters
///
/// TensorProxy IEnumerable with the expected Tensor outputs
public static string[] GetOutputNames(Model model)
{
var names = new List();
if (model == null)
{
return names.ToArray();
}
names.Add(TensorNames.ActionOutput);
var memory = (int)model.GetTensorByName(TensorNames.MemorySize)[0];
if (memory > 0)
{
foreach (var mem in model.memories)
{
names.Add(mem.output);
}
}
names.Sort();
return names.ToArray();
}
///
/// Factory for the ModelParamLoader : Creates a ModelParamLoader and runs the checks
/// on it.
///
///
/// The Barracuda engine model for loading static parameters
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// Attached sensor components
/// BehaviorType or the Agent to check.
/// The list the error messages of the checks that failed
public static IEnumerable CheckModel(Model model, BrainParameters brainParameters,
SensorComponent[] sensorComponents, BehaviorType behaviorType = BehaviorType.Default)
{
List failedModelChecks = new List();
if (model == null)
{
var errorMsg = "There is no model for this Brain; cannot run inference. ";
if (behaviorType == BehaviorType.InferenceOnly)
{
errorMsg += "Either assign a model, or change to a different Behavior Type.";
}
else
{
errorMsg += "(But can still train)";
}
failedModelChecks.Add(errorMsg);
return failedModelChecks;
}
var modelApiVersion = (int)model.GetTensorByName(TensorNames.VersionNumber)[0];
if (modelApiVersion == -1)
{
failedModelChecks.Add(
"Model was not trained using the right version of ML-Agents. " +
"Cannot use this model.");
return failedModelChecks;
}
if (modelApiVersion < k_ApiVersion)
{
failedModelChecks.Add(
$"Version of the trainer the model was trained with ({modelApiVersion}) " +
$"is not compatible with the Brain's version ({k_ApiVersion}).");
return failedModelChecks;
}
if (modelApiVersion > k_ApiVersion)
{
var minTrainerVersion = PythonTrainerVersions.s_MinSupportedVersion;
var maxTrainerVersion = PythonTrainerVersions.s_MaxSupportedVersion;
failedModelChecks.Add(
"Model was trained with a newer version of the trainer than is supported. " +
$"Either retrain with an older trainer (between versions {minTrainerVersion} and {maxTrainerVersion}), " +
$"or update to a newer version of com.unity.ml-agents.\n" +
$"Model version: {modelApiVersion} Supported version: {k_ApiVersion}"
);
return failedModelChecks;
}
var memorySize = (int)model.GetTensorByName(TensorNames.MemorySize)[0];
var isContinuousInt = (int)model.GetTensorByName(TensorNames.IsContinuousControl)[0];
var isContinuous = GetActionType(isContinuousInt);
var actionSize = (int)model.GetTensorByName(TensorNames.ActionOutputShape)[0];
failedModelChecks.AddRange(
CheckIntScalarPresenceHelper(new Dictionary()
{
{TensorNames.MemorySize, memorySize},
{TensorNames.IsContinuousControl, isContinuousInt},
{TensorNames.ActionOutputShape, actionSize}
})
);
failedModelChecks.AddRange(
CheckInputTensorPresence(model, brainParameters, memorySize, isContinuous, sensorComponents)
);
failedModelChecks.AddRange(
CheckOutputTensorPresence(model, memorySize))
;
failedModelChecks.AddRange(
CheckInputTensorShape(model, brainParameters, sensorComponents)
);
failedModelChecks.AddRange(
CheckOutputTensorShape(model, brainParameters, isContinuous, actionSize)
);
return failedModelChecks;
}
///
/// Converts the integer value in the model corresponding to the type of control to a
/// ModelActionType.
///
///
/// The integer value in the model indicating the type of control
///
/// The equivalent ModelActionType
static ModelActionType GetActionType(int isContinuousInt)
{
ModelActionType isContinuous;
switch (isContinuousInt)
{
case 0:
isContinuous = ModelActionType.Discrete;
break;
case 1:
isContinuous = ModelActionType.Continuous;
break;
default:
isContinuous = ModelActionType.Unknown;
break;
}
return isContinuous;
}
///
/// Given a Dictionary of node names to int values, create checks if the values have the
/// invalid value of -1.
///
/// Mapping from node names to int values
/// The list the error messages of the checks that failed
static IEnumerable CheckIntScalarPresenceHelper(
Dictionary requiredScalarFields)
{
var failedModelChecks = new List();
foreach (var field in requiredScalarFields)
{
if (field.Value == -1)
{
failedModelChecks.Add($"Missing node in the model provided : {field.Key}");
}
}
return failedModelChecks;
}
///
/// Generates failed checks that correspond to inputs expected by the model that are not
/// present in the BrainParameters.
///
///
/// The Barracuda engine model for loading static parameters
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
///
/// The memory size that the model is expecting.
///
///
/// Whether the model is expecting continuous or discrete control.
///
/// Array of attached sensor components
///
/// A IEnumerable of string corresponding to the failed input presence checks.
///
static IEnumerable CheckInputTensorPresence(
Model model,
BrainParameters brainParameters,
int memory,
ModelActionType isContinuous,
SensorComponent[] sensorComponents
)
{
var failedModelChecks = new List();
var tensorsNames = GetInputTensors(model).Select(x => x.name).ToList();
// If there is no Vector Observation Input but the Brain Parameters expect one.
if ((brainParameters.VectorObservationSize != 0) &&
(!tensorsNames.Contains(TensorNames.VectorObservationPlaceholder)))
{
failedModelChecks.Add(
"The model does not contain a Vector Observation Placeholder Input. " +
"You must set the Vector Observation Space Size to 0.");
}
// If there are not enough Visual Observation Input compared to what the
// sensors expect.
var visObsIndex = 0;
for (var sensorIndex = 0; sensorIndex < sensorComponents.Length; sensorIndex++)
{
var sensor = sensorComponents[sensorIndex];
if (!sensor.IsVisual())
{
continue;
}
if (!tensorsNames.Contains(
TensorNames.VisualObservationPlaceholderPrefix + visObsIndex))
{
failedModelChecks.Add(
"The model does not contain a Visual Observation Placeholder Input " +
$"for sensor component {visObsIndex} ({sensor.GetType().Name}).");
}
visObsIndex++;
}
var expectedVisualObs = GetNumVisualInputs(model);
// Check if there's not enough visual sensors (too many would be handled above)
if (expectedVisualObs > visObsIndex)
{
failedModelChecks.Add(
$"The model expects {expectedVisualObs} visual inputs," +
$" but only found {visObsIndex} visual sensors."
);
}
// If the model has a non-negative memory size but requires a recurrent input
if (memory > 0)
{
if (!tensorsNames.Any(x => x.EndsWith("_h")) ||
!tensorsNames.Any(x => x.EndsWith("_c")))
{
failedModelChecks.Add(
"The model does not contain a Recurrent Input Node but has memory_size.");
}
}
// If the model uses discrete control but does not have an input for action masks
if (isContinuous == ModelActionType.Discrete)
{
if (!tensorsNames.Contains(TensorNames.ActionMaskPlaceholder))
{
failedModelChecks.Add(
"The model does not contain an Action Mask but is using Discrete Control.");
}
}
return failedModelChecks;
}
///
/// Generates failed checks that correspond to outputs expected by the model that are not
/// present in the BrainParameters.
///
///
/// The Barracuda engine model for loading static parameters
///
/// The memory size that the model is expecting/
///
/// A IEnumerable of string corresponding to the failed output presence checks.
///
static IEnumerable CheckOutputTensorPresence(Model model, int memory)
{
var failedModelChecks = new List();
// If there is no Action Output.
if (!model.outputs.Contains(TensorNames.ActionOutput))
{
failedModelChecks.Add("The model does not contain an Action Output Node.");
}
// If there is no Recurrent Output but the model is Recurrent.
if (memory > 0)
{
var memOutputs = model.memories.Select(x => x.output).ToList();
if (!memOutputs.Any(x => x.EndsWith("_h")) ||
!memOutputs.Any(x => x.EndsWith("_c")))
{
failedModelChecks.Add(
"The model does not contain a Recurrent Output Node but has memory_size.");
}
}
return failedModelChecks;
}
///
/// Checks that the shape of the visual observation input placeholder is the same as the corresponding sensor.
///
/// The tensor that is expected by the model
/// The sensor that produces the visual observation.
///
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
///
static string CheckVisualObsShape(
TensorProxy tensorProxy, SensorComponent sensorComponent)
{
var shape = sensorComponent.GetObservationShape();
var heightBp = shape[0];
var widthBp = shape[1];
var pixelBp = shape[2];
var heightT = tensorProxy.Height;
var widthT = tensorProxy.Width;
var pixelT = tensorProxy.Channels;
if ((widthBp != widthT) || (heightBp != heightT) || (pixelBp != pixelT))
{
return $"The visual Observation of the model does not match. " +
$"Received TensorProxy of shape [?x{widthBp}x{heightBp}x{pixelBp}] but " +
$"was expecting [?x{widthT}x{heightT}x{pixelT}].";
}
return null;
}
///
/// Generates failed checks that correspond to inputs shapes incompatibilities between
/// the model and the BrainParameters.
///
///
/// The Barracuda engine model for loading static parameters
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// Attached sensors
/// The list the error messages of the checks that failed
static IEnumerable CheckInputTensorShape(
Model model, BrainParameters brainParameters, SensorComponent[] sensorComponents)
{
var failedModelChecks = new List();
var tensorTester =
new Dictionary>()
{
{TensorNames.VectorObservationPlaceholder, CheckVectorObsShape},
{TensorNames.PreviousActionPlaceholder, CheckPreviousActionShape},
{TensorNames.RandomNormalEpsilonPlaceholder, ((bp, tensor, scs) => null)},
{TensorNames.ActionMaskPlaceholder, ((bp, tensor, scs) => null)},
{TensorNames.SequenceLengthPlaceholder, ((bp, tensor, scs) => null)},
{TensorNames.RecurrentInPlaceholder, ((bp, tensor, scs) => null)},
};
foreach (var mem in model.memories)
{
tensorTester[mem.input] = ((bp, tensor, scs) => null);
}
var visObsIndex = 0;
for (var sensorIndex = 0; sensorIndex < sensorComponents.Length; sensorIndex++)
{
var sensorComponent = sensorComponents[sensorIndex];
if (!sensorComponent.IsVisual())
{
continue;
}
tensorTester[TensorNames.VisualObservationPlaceholderPrefix + visObsIndex] =
(bp, tensor, scs) => CheckVisualObsShape(tensor, sensorComponent);
visObsIndex++;
}
// If the model expects an input but it is not in this list
foreach (var tensor in GetInputTensors(model))
{
if (!tensorTester.ContainsKey(tensor.name))
{
if (!tensor.name.Contains("visual_observation"))
{
failedModelChecks.Add(
"Model requires an unknown input named : " + tensor.name);
}
}
else
{
var tester = tensorTester[tensor.name];
var error = tester.Invoke(brainParameters, tensor, sensorComponents);
if (error != null)
{
failedModelChecks.Add(error);
}
}
}
return failedModelChecks;
}
///
/// Checks that the shape of the Vector Observation input placeholder is the same in the
/// model and in the Brain Parameters.
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// The tensor that is expected by the model
/// Array of attached sensor components
///
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
///
static string CheckVectorObsShape(
BrainParameters brainParameters, TensorProxy tensorProxy, SensorComponent[] sensorComponents)
{
var vecObsSizeBp = brainParameters.VectorObservationSize;
var numStackedVector = brainParameters.NumStackedVectorObservations;
var totalVecObsSizeT = tensorProxy.shape[tensorProxy.shape.Length - 1];
var totalVectorSensorSize = 0;
foreach (var sensorComp in sensorComponents)
{
if (sensorComp.IsVector())
{
totalVectorSensorSize += sensorComp.GetObservationShape()[0];
}
}
if (vecObsSizeBp * numStackedVector + totalVectorSensorSize != totalVecObsSizeT)
{
var sensorSizes = "";
foreach (var sensorComp in sensorComponents)
{
if (sensorComp.IsVector())
{
var vecSize = sensorComp.GetObservationShape()[0];
if (sensorSizes.Length == 0)
{
sensorSizes = $"[{vecSize}";
}
else
{
sensorSizes += $", {vecSize}";
}
}
}
sensorSizes += "]";
return $"Vector Observation Size of the model does not match. Was expecting {totalVecObsSizeT} " +
$"but received {vecObsSizeBp} x {numStackedVector} vector observations and " +
$"SensorComponent sizes: {sensorSizes}.";
}
return null;
}
///
/// Checks that the shape of the Previous Vector Action input placeholder is the same in the
/// model and in the Brain Parameters.
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// The tensor that is expected by the model
/// Array of attached sensor components
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
static string CheckPreviousActionShape(
BrainParameters brainParameters, TensorProxy tensorProxy, SensorComponent[] sensorComponents)
{
var numberActionsBp = brainParameters.VectorActionSize.Length;
var numberActionsT = tensorProxy.shape[tensorProxy.shape.Length - 1];
if (numberActionsBp != numberActionsT)
{
return "Previous Action Size of the model does not match. " +
$"Received {numberActionsBp} but was expecting {numberActionsT}.";
}
return null;
}
///
/// Generates failed checks that correspond to output shapes incompatibilities between
/// the model and the BrainParameters.
///
///
/// The Barracuda engine model for loading static parameters
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
///
/// Whether the model is expecting continuous or discrete control.
///
///
/// The size of the action output that is expected by the model.
///
///
/// A IEnumerable of string corresponding to the incompatible shapes between model
/// and BrainParameters.
///
static IEnumerable CheckOutputTensorShape(
Model model,
BrainParameters brainParameters,
ModelActionType isContinuous,
int modelActionSize)
{
var failedModelChecks = new List();
if (isContinuous == ModelActionType.Unknown)
{
failedModelChecks.Add("Cannot infer type of Control from the provided model.");
return failedModelChecks;
}
if (isContinuous == ModelActionType.Continuous &&
brainParameters.VectorActionSpaceType != SpaceType.Continuous)
{
failedModelChecks.Add(
"Model has been trained using Continuous Control but the Brain Parameters " +
"suggest Discrete Control.");
return failedModelChecks;
}
if (isContinuous == ModelActionType.Discrete &&
brainParameters.VectorActionSpaceType != SpaceType.Discrete)
{
failedModelChecks.Add(
"Model has been trained using Discrete Control but the Brain Parameters " +
"suggest Continuous Control.");
return failedModelChecks;
}
var tensorTester = new Dictionary>();
if (brainParameters.VectorActionSpaceType == SpaceType.Continuous)
{
tensorTester[TensorNames.ActionOutput] = CheckContinuousActionOutputShape;
}
else
{
tensorTester[TensorNames.ActionOutput] = CheckDiscreteActionOutputShape;
}
// If the model expects an output but it is not in this list
foreach (var name in model.outputs)
{
if (tensorTester.ContainsKey(name))
{
Func tester = tensorTester[name];
var error = tester.Invoke(brainParameters, model.GetShapeByName(name), modelActionSize);
if (error != null)
{
failedModelChecks.Add(error);
}
}
}
return failedModelChecks;
}
///
/// Checks that the shape of the discrete action output is the same in the
/// model and in the Brain Parameters.
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// The tensor shape that is expected by the model
///
/// The size of the action output that is expected by the model.
///
///
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
///
static string CheckDiscreteActionOutputShape(
BrainParameters brainParameters, TensorShape? shape, int modelActionSize)
{
var bpActionSize = brainParameters.VectorActionSize.Sum();
if (modelActionSize != bpActionSize)
{
return "Action Size of the model does not match. The BrainParameters expect " +
$"{bpActionSize} but the model contains {modelActionSize}.";
}
return null;
}
///
/// Checks that the shape of the continuous action output is the same in the
/// model and in the Brain Parameters.
///
///
/// The BrainParameters that are used verify the compatibility with the InferenceEngine
///
/// The tensor shape that is expected by the model
///
/// The size of the action output that is expected by the model.
///
/// If the Check failed, returns a string containing information about why the
/// check failed. If the check passed, returns null.
static string CheckContinuousActionOutputShape(
BrainParameters brainParameters, TensorShape? shape, int modelActionSize)
{
var bpActionSize = brainParameters.VectorActionSize[0];
if (modelActionSize != bpActionSize)
{
return "Action Size of the model does not match. The BrainParameters expect " +
$"{bpActionSize} but the model contains {modelActionSize}.";
}
return null;
}
}
}