-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Add stacking to multi-dimensional and compressed observations #4476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
} | ||
|
||
// Construct compression mapping | ||
var compressibleSensor = m_WrappedSensor as ICompressibleSensor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to handle the case where the wrapped sensor isn't actually an ICompressibleSensor, just an ISensor. I think in that case, you should either leave the mapping empty, or the "identity" mapping (mapping[i] == i
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is inside the else statement of checking m_WrappedSensor.GetCompressionType()
which should have a compression type other than None. My intention is to make all sensors that can have compressed observation being ICompressibleSensor, so it should always be ICompressibleSensor here.
Or should I check and raise an exception here if the sensor has m_WrappedSensor.GetCompressionType() !=SensorCompressionType.None
but its' not an ICompressibleSensor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't assume in general that m_WrappedSensor.GetCompressionType() !=SensorCompressionType.None
means that it's an ICompressibleSensor
. I think maybe a better name would change how we think about this - how about something like ISparseChannelSensor
?
My intention is to make all sensors that can have compressed observation being ICompressibleSensor, so it should always be ICompressibleSensor here.
I actually think this might make things harder for backwards compatibility reasons. I'll start another thread on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still needs to be addressed - if wrapping a non-ICompressibleSensor, assume an identity mapping.
offset = wrappedNumChannel * i; | ||
for (var j = 0; j < wrappedMapLength; j++) | ||
{ | ||
m_CompressionMapping[j + wrappedMapLength * i] = wrappedMapping[j] > 0 ? wrappedMapping[j] + offset : 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use -1 if the wrappedMapping is negative?
protobuf-definitions/proto/mlagents_envs/communicator_objects/observation.proto
Outdated
Show resolved
Hide resolved
@@ -85,6 +85,7 @@ def generate_compressed_data(in_array: np.ndarray) -> bytes: | |||
def generate_compressed_proto_obs(in_array: np.ndarray) -> ObservationProto: | |||
obs_proto = ObservationProto() | |||
obs_proto.compressed_data = generate_compressed_data(in_array) | |||
obs_proto.mapping.extend(list(range(len(in_array)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs some more unit tests. I think you should cover
- 1 channel, no mapping (old case)
- 3 channels, no mapping (old case)
-
3 channels, no mapping (old case)
- 1 channel, mapping
- 3 channel, mapping
-
3 channels with a non-trivial mapping (e.g. 4 channels and stacking=2)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure I'll add those.
But are we allowing compressed observation with 1 channel? I thought it should always be uncompressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compressed observation with 1 channel
Grayscale visual observations, and those can be stacked.
You're also going to need to add some safeguards to make sure that different versions of C# and python can work OK with each other, or give an error early when then can't. To do this, you'll need to increase the communication protocol to 1.2.0 and add a flag in the Capabilities proto message - see #4462 for an example. There's no problem using a new version of python with old C#, but if you have the latest C# using a non-trivial mapping and it's connect to an old version of the python code, you'll need to raise a warning and fall back to uncompressed observations, since the python code won't be able to decompress them correctly. Because of that, you might want consider not using the ICompressibleSensor for Camera and RenderTexture sensor, since it will be harder to detect when it's OK to use compression. |
com.unity.ml-agents/Runtime/Sensors/RenderTextureSensorComponent.cs
Outdated
Show resolved
Hide resolved
} | ||
|
||
public int[] ConstructStackedCompressedChannelMapping(ISensor wrappedSenesor) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method does a lot of things. I think I know what it does, but you should add more comments here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some comments. Let me know if anything still seems unclear.
if actual_channels > expected_channels: | ||
img = img[..., 0:expected_channels] | ||
if mappings is not None and len(mappings) > 0: | ||
image_arrays = np.concatenate(image_arrays, axis=2).transpose((2, 0, 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a comment here. This method is getting pretty long, maybe adding some helper methods will help compartmentalize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you do something like
mappings = list(range(expected_channels)) if expected_channels > 0 else [0, 0, 0]
so that the "old case" is handled by the new code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't that get into the same performance debate for C# that a mapping has to be created every time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point :) I'm not sure if memory allocation/GC in python is better or worse than in C#, but it's at least tolerated in python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you could use range(expected_channels)
(although you'd have to make the mypy types a bit more generic) which should allocate less, and you could reuse (0, 0, 0)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've split them into two functions now so it looks clear how it process with mapping or with expected_channels. If unifying them still looks better than this I can change it.
Co-authored-by: Vincent-Pierre BERGES <[email protected]>
Co-authored-by: Chris Elion <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a few small changes for what's there. But you're missing some unit test coverage on the new functionality. You can see what's covered here (this is from the latest CI run):
https://yamato-artifactviewer.prd.cds.internal.unity3d.com/10cef3c6-1a04-4bc2-9e8c-9ee059861eef%2Flogs%2Fupm-ci~%2Ftest-results%2Fisolation-3638200-com.unity.ml-agents%2FReport/index.htm
In particular, I think you need to:
- Make a test that creates a dummy sensor that writes compressed observations, put a stacking sensor around that, and make sure the output bytes are expected. The dummy sensor doesn't need to output valid PNGs, you can just return {i} for the i'th step.
- Make a test that has a non-multiple-of-3 number of channels, so that the -1 padding code will be tested. Again make a dummy sensor class that returns something like [3, 2, 1, 0] for the mapping and make sure the StackingSensor mapping is what you expect.
- Add some test cases for IsTrivialMapping from GrpcExtensions
{ | ||
if (mapping[i] != i) | ||
{ | ||
isIdentityMapping = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return false here? Probably not worth optimizing, but might be clearer.
@@ -75,6 +75,11 @@ public bool Grayscale | |||
set { m_Grayscale = value; } | |||
} | |||
|
|||
[HideInInspector, SerializeField] | |||
[Range(1, 50)] | |||
[Tooltip("Number of camera frames that will be stacked before being fed to the neural network.")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Neat, I figured this would get ignored and you had to do it in the custom editor, but looks like it just works.
if actual_channels > expected_channels: | ||
img = img[..., 0:expected_channels] | ||
if mappings is not None and len(mappings) > 0: | ||
return process_images_mapping(image_arrays, mappings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: leading _
for these functions?
|
||
# Old API without mapping provided. Use the first n channel, n=expected_channels. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to docstring instead
@@ -85,11 +99,32 @@ public int Write(ObservationWriter writer) | |||
|
|||
// Now write the saved observations (oldest first) | |||
var numWritten = 0; | |||
for (var i = 0; i < m_NumStackedObservations; i++) | |||
if (wrappedShape.Length == 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Found this bug that I didn't change the Write method properly for uncompressed obs and it's still stacking on the first dimension. Here changed it to the last dimension for 2D observations so that inference can work properly.
edit: this will not affect 1D case.
namespace Unity.MLAgents.Sensors | ||
{ | ||
/// <summary> | ||
/// Sensor interface for generating observations. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you update the description here? I think it's copied from ISensor
// Test mapping with dummy layers that should be dropped | ||
var dummySensor = new Dummy3DSensor(); | ||
dummySensor.Shape = new int[] { 2, 2, 4 }; | ||
dummySensor.Mapping = new int[] { 0, 1, 2, 3, -1, -1 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrapped sensor shouldn't be adding -1's to its mapping, right? I think we decided this was the responsibility of the StackingSensor. The assert below can stay the same, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I guess you should try both - once where -1 is in the wrapped sensor's mapping to make sure they're preserved, and another to make sure they're added if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes I agree both cases can happen, added test for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Proposed change(s)
Add observations stacking to multi-dimensional and compressed observations.
This allows stacking for visual observations and other multi-dimensional observations.
Useful links (Github issues, JIRA tickets, ML-Agents forum threads etc.)
Types of change(s)
Checklist
Other comments