Skip to content

Commit b8645d3

Browse files
committed
Add keras.layers.CategoryEncoding.
1 parent 2ee08e8 commit b8645d3

8 files changed

Lines changed: 189 additions & 2 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Newtonsoft.Json;
2+
using Tensorflow.NumPy;
3+
4+
namespace Tensorflow.Keras.ArgsDefinition
5+
{
6+
public class CategoryEncodingArgs : AutoSerializeLayerArgs
7+
{
8+
[JsonProperty("num_tokens")]
9+
public int NumTokens { get; set; }
10+
[JsonProperty("output_mode")]
11+
public string OutputMode { get; set; }
12+
[JsonProperty("sparse")]
13+
public bool Sparse { get; set; }
14+
public NDArray CountWeights { get; set; }
15+
}
16+
}

src/TensorFlowNET.Core/Keras/Layers/ILayersApi.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using Tensorflow.NumPy;
23
using static Google.Protobuf.Reflection.FieldDescriptorProto.Types;
34

45
namespace Tensorflow.Keras.Layers
@@ -28,6 +29,17 @@ public ILayer BatchNormalization(int axis = -1,
2829
bool renorm = false,
2930
float renorm_momentum = 0.99f);
3031

32+
/// <summary>
33+
/// A preprocessing layer which encodes integer features.
34+
/// </summary>
35+
/// <param name="num_tokens">The total number of tokens the layer should support.</param>
36+
/// <param name="output_mode">Specification for the output of the layer.</param>
37+
/// <returns></returns>
38+
public ILayer CategoryEncoding(int num_tokens,
39+
string output_mode = "one_hot",
40+
bool sparse = false,
41+
NDArray count_weights = null);
42+
3143
public ILayer Conv1D(int filters,
3244
Shape kernel_size,
3345
int strides = 1,

src/TensorFlowNET.Core/Operations/math_ops.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -839,10 +839,24 @@ public static Tensor bincount(Tensor arr, Tensor weights = null,
839839
output_size = math_ops.maximum(minlength, output_size);
840840
if (maxlength != null)
841841
output_size = math_ops.minimum(maxlength, output_size);
842-
var weights = constant_op.constant(new long[0], dtype: dtype);
842+
weights = weights ?? constant_op.constant(new int[0], dtype: dtype);
843843
return tf.Context.ExecuteOp("Bincount", name, new ExecuteOpArgs(arr, output_size, weights));
844844
}
845+
else
846+
{
847+
var array_is_nonempty = math_ops.reduce_prod(array_ops.shape(arr)) > 0;
848+
var output_size = math_ops.cast(array_is_nonempty, arr.dtype) * (math_ops.reduce_max(arr) + 1);
849+
if (minlength != null)
850+
output_size = math_ops.maximum(minlength, output_size);
851+
if (maxlength != null)
852+
output_size = math_ops.minimum(maxlength, output_size);
853+
weights = weights ?? array_ops.constant(new int[0], dtype: dtype);
845854

855+
return tf.Context.ExecuteOp("DenseBincount", name,
856+
new ExecuteOpArgs(arr, output_size, weights, binary_output)
857+
.SetAttributes(new { binary_output }));
858+
}
859+
846860
throw new NotImplementedException("");
847861
});
848862

src/TensorFlowNET.Core/Tensors/constant_op.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ static Tensor convert_to_eager_tensor(object value,
153153
bool allow_broadcast)
154154
{
155155
var t = convert_to_eager_tensor(value, tf.Context, dtype: dtype);
156+
if (dtype != TF_DataType.DtInvalid && dtype != t.dtype)
157+
{
158+
t = math_ops.cast(t, dtype);
159+
}
156160
if (shape is null || shape.IsNull)
157161
return t;
158162

src/TensorFlowNET.Keras/Layers/LayersApi.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Tensorflow.Keras.ArgsDefinition.Rnn;
55
using Tensorflow.Keras.Engine;
66
using Tensorflow.Keras.Layers.Rnn;
7+
using Tensorflow.NumPy;
78
using static Tensorflow.Binding;
89
using static Tensorflow.KerasApi;
910

@@ -829,5 +830,14 @@ IInitializer GetInitializerByName(string name)
829830
"orthogonal" => tf.orthogonal_initializer,
830831
_ => tf.glorot_uniform_initializer
831832
};
833+
834+
public ILayer CategoryEncoding(int num_tokens, string output_mode = "one_hot", bool sparse = false, NDArray count_weights = null)
835+
=> new CategoryEncoding(new CategoryEncodingArgs
836+
{
837+
NumTokens = num_tokens,
838+
OutputMode = output_mode,
839+
Sparse = sparse,
840+
CountWeights = count_weights
841+
});
832842
}
833843
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using Tensorflow.Keras.ArgsDefinition;
2+
using Tensorflow.Keras.Engine;
3+
4+
namespace Tensorflow.Keras.Layers
5+
{
6+
/// <summary>
7+
/// This layer provides options for condensing data into a categorical encoding when the total number of tokens are known in advance.
8+
/// </summary>
9+
public class CategoryEncoding : Layer
10+
{
11+
CategoryEncodingArgs args;
12+
13+
public CategoryEncoding(CategoryEncodingArgs args) : base(args)
14+
{
15+
this.args = args;
16+
}
17+
18+
protected override Tensors Call(Tensors inputs, Tensor state = null, bool? training = null)
19+
{
20+
var depth = args.NumTokens;
21+
var max_value = tf.reduce_max(inputs);
22+
var min_value = tf.reduce_min(inputs);
23+
24+
/*var condition = tf.logical_and(tf.greater(tf.cast(constant_op.constant(depth), max_value.dtype), max_value),
25+
tf.greater_equal(min_value, tf.cast(constant_op.constant(0), min_value.dtype)));*/
26+
27+
var bincounts = encode_categorical_inputs(inputs, args.OutputMode, depth, args.DType,
28+
sparse: args.Sparse,
29+
count_weights: args.CountWeights);
30+
31+
if(args.OutputMode != "tf_idf")
32+
{
33+
return bincounts;
34+
}
35+
36+
return inputs;
37+
}
38+
39+
public override Shape ComputeOutputShape(Shape input_shape)
40+
{
41+
return input_shape;
42+
}
43+
44+
Tensors encode_categorical_inputs(Tensor inputs, string output_mode, int depth,
45+
TF_DataType dtype = TF_DataType.TF_FLOAT,
46+
bool sparse = false,
47+
Tensor count_weights = null)
48+
{
49+
bool binary_output = false;
50+
if (output_mode == "one_hot")
51+
{
52+
binary_output = true;
53+
if (inputs.shape[-1] != 1)
54+
{
55+
inputs = tf.expand_dims(inputs, -1);
56+
}
57+
}
58+
else if (output_mode == "multi_hot")
59+
{
60+
binary_output = true;
61+
}
62+
63+
var depth_tensor = constant_op.constant(depth);
64+
var result = tf.math.bincount(inputs,
65+
weights: count_weights,
66+
minlength: depth_tensor,
67+
maxlength: depth_tensor,
68+
dtype: dtype,
69+
axis: -1,
70+
binary_output: binary_output);
71+
72+
return result;
73+
}
74+
}
75+
}

test/TensorFlowNET.Keras.UnitTest/Layers/LayersTest.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,5 +177,60 @@ public void LayerNormalization()
177177
Assert.AreEqual((5, 2), output.shape);
178178
Assert.IsTrue(output[0].numpy().Equals(new[] { -0.99998f, 0.99998f }));
179179
}
180+
181+
/// <summary>
182+
/// https://www.tensorflow.org/api_docs/python/tf/keras/layers/CategoryEncoding
183+
/// </summary>
184+
[TestMethod]
185+
public void CategoryEncoding()
186+
{
187+
// one-hot
188+
var inputs = np.array(new[] { 3, 2, 0, 1 });
189+
var layer = tf.keras.layers.CategoryEncoding(4);
190+
191+
Tensor output = layer.Apply(inputs);
192+
Assert.AreEqual((4, 4), output.shape);
193+
Assert.IsTrue(output[0].numpy().Equals(new[] { 0, 0, 0, 1f }));
194+
Assert.IsTrue(output[1].numpy().Equals(new[] { 0, 0, 1, 0f }));
195+
Assert.IsTrue(output[2].numpy().Equals(new[] { 1, 0, 0, 0f }));
196+
Assert.IsTrue(output[3].numpy().Equals(new[] { 0, 1, 0, 0f }));
197+
198+
// multi-hot
199+
inputs = np.array(new[,]
200+
{
201+
{ 0, 1 },
202+
{ 0, 0 },
203+
{ 1, 2 },
204+
{ 3, 1 }
205+
});
206+
layer = tf.keras.layers.CategoryEncoding(4, output_mode: "multi_hot");
207+
output = layer.Apply(inputs);
208+
Assert.IsTrue(output[0].numpy().Equals(new[] { 1, 1, 0, 0f }));
209+
Assert.IsTrue(output[1].numpy().Equals(new[] { 1, 0, 0, 0f }));
210+
Assert.IsTrue(output[2].numpy().Equals(new[] { 0, 1, 1, 0f }));
211+
Assert.IsTrue(output[3].numpy().Equals(new[] { 0, 1, 0, 1f }));
212+
213+
// using weighted inputs in "count" mode
214+
inputs = np.array(new[,]
215+
{
216+
{ 0, 1 },
217+
{ 0, 0 },
218+
{ 1, 2 },
219+
{ 3, 1 }
220+
});
221+
var weights = np.array(new[,]
222+
{
223+
{ 0.1f, 0.2f },
224+
{ 0.1f, 0.1f },
225+
{ 0.2f, 0.3f },
226+
{ 0.4f, 0.2f }
227+
});
228+
layer = tf.keras.layers.CategoryEncoding(4, output_mode: "count", count_weights: weights);
229+
output = layer.Apply(inputs);
230+
Assert.IsTrue(output[0].numpy().Equals(new[] { 0.1f, 0.2f, 0f, 0f }));
231+
Assert.IsTrue(output[1].numpy().Equals(new[] { 0.2f, 0f, 0f, 0f }));
232+
Assert.IsTrue(output[2].numpy().Equals(new[] { 0f, 0.2f, 0.3f, 0f }));
233+
Assert.IsTrue(output[3].numpy().Equals(new[] { 0f, 0.2f, 0f, 0.4f }));
234+
}
180235
}
181236
}

test/TensorFlowNET.Keras.UnitTest/Losses/LossesTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
using System.Linq;
55
using System.Text;
66
using System.Threading.Tasks;
7+
using Tensorflow;
78
using TensorFlowNET.Keras.UnitTest;
89
using static Tensorflow.Binding;
910
using static Tensorflow.KerasApi;
1011

11-
namespace Tensorflow.Keras.UnitTest.Losses;
12+
namespace TensorFlowNET.Keras.UnitTest;
1213

1314
[TestClass]
1415
public class LossesTest : EagerModeTestBase

0 commit comments

Comments
 (0)