diff --git a/libraries/kotlintest-core-jvm-4.0.2631-SNAPSHOT.jar b/libraries/kotlintest-core-jvm-4.0.2631-SNAPSHOT.jar index 49cf4e7b..b0b47ef7 100644 Binary files a/libraries/kotlintest-core-jvm-4.0.2631-SNAPSHOT.jar and b/libraries/kotlintest-core-jvm-4.0.2631-SNAPSHOT.jar differ diff --git a/test-util/test-util.gradle.kts b/test-util/test-util.gradle.kts index ddb8afef..59452b10 100644 --- a/test-util/test-util.gradle.kts +++ b/test-util/test-util.gradle.kts @@ -43,6 +43,9 @@ dependencies { } }) + // Needed by kotlintest + api(group = "com.github.wumpz", name = "diffutils", version = "2.2") + api( group = "org.junit.jupiter", name = "junit-jupiter", diff --git a/tf-data-code/src/main/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCode.kt b/tf-data-code/src/main/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCode.kt index 3c5be518..ba16dd95 100644 --- a/tf-data-code/src/main/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCode.kt +++ b/tf-data-code/src/main/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCode.kt @@ -77,6 +77,18 @@ class DefaultLayerToCode : LayerToCode, KoinComponent { ) } + is Layer.AveragePooling2D -> makeLayerCode( + "tf.keras.layers.AvgPool2D", + listOf(), + listOf( + "pool_size" to layer.poolSize, + "strides" to layer.strides, + "padding" to layer.padding.value, + "data_format" to layer.dataFormat?.value, + "name" to layer.name + ) + ).right() + is Layer.Dense -> makeLayerCode( "tf.keras.layers.Dense", listOf(), @@ -106,6 +118,24 @@ class DefaultLayerToCode : LayerToCode, KoinComponent { ) ).right() + is Layer.GlobalAveragePooling2D -> makeLayerCode( + "tf.keras.layers.GlobalAveragePooling2D", + listOf(), + listOf( + "data_format" to layer.dataFormat?.value, + "name" to layer.name + ) + ).right() + + is Layer.GlobalMaxPooling2D -> makeLayerCode( + "tf.keras.layers.GlobalMaxPooling2D", + listOf(), + listOf( + "data_format" to layer.dataFormat?.value, + "name" to layer.name + ) + ).right() + is Layer.MaxPooling2D -> makeLayerCode( "tf.keras.layers.MaxPooling2D", listOf(), @@ -118,6 +148,26 @@ class DefaultLayerToCode : LayerToCode, KoinComponent { ) ).right() + is Layer.SpatialDropout2D -> makeLayerCode( + "tf.keras.layers.SpatialDropout2D", + listOf(layer.rate.toString()), + listOf( + "data_format" to layer.dataFormat?.value, + "name" to layer.name + ) + ).right() + + is Layer.UpSampling2D -> makeLayerCode( + "tf.keras.layers.UpSampling2D", + listOf(), + listOf( + "size" to layer.size, + "data_format" to layer.dataFormat?.value, + "interpolation" to layer.interpolation.value, + "name" to layer.name + ) + ).right() + // TODO: Remove this else -> "Cannot construct an unknown layer: $layer".left() } diff --git a/tf-data-code/src/test/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCodeTest.kt b/tf-data-code/src/test/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCodeTest.kt index 387303f4..65e787ba 100644 --- a/tf-data-code/src/test/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCodeTest.kt +++ b/tf-data-code/src/test/kotlin/edu/wpi/axon/tfdata/code/layer/DefaultLayerToCodeTest.kt @@ -13,9 +13,10 @@ import edu.wpi.axon.tfdata.layer.Activation import edu.wpi.axon.tfdata.layer.Constraint import edu.wpi.axon.tfdata.layer.DataFormat import edu.wpi.axon.tfdata.layer.Initializer +import edu.wpi.axon.tfdata.layer.Interpolation +import edu.wpi.axon.tfdata.layer.Layer import edu.wpi.axon.tfdata.layer.PoolingPadding import edu.wpi.axon.tfdata.layer.Regularizer -import edu.wpi.axon.tfdata.layer.Layer import io.kotlintest.shouldBe import io.mockk.every import io.mockk.mockk @@ -183,6 +184,57 @@ internal class DefaultLayerToCodeTest : KoinTestFixture() { ), ("""tf.keras.layers.Flatten(data_format="channels_first", name="name")""").right(), null + ), + Arguments.of( + Layer.AveragePooling2D( + "name", + None, + Right(Tuple2(2, 2)), + Left(3), + PoolingPadding.Valid, + DataFormat.ChannelsLast + ), + Right("""tf.keras.layers.AvgPool2D(pool_size=(2, 2), strides=3, padding="valid", data_format="channels_last", name="name")"""), + null + ), + Arguments.of( + Layer.GlobalMaxPooling2D( + "name", + None, + DataFormat.ChannelsFirst + ), + Right("""tf.keras.layers.GlobalMaxPooling2D(data_format="channels_first", name="name")"""), + null + ), + Arguments.of( + Layer.SpatialDropout2D( + "name", + None, + 0.2, + null + ), + Right("""tf.keras.layers.SpatialDropout2D(0.2, data_format=None, name="name")"""), + null + ), + Arguments.of( + Layer.UpSampling2D( + "name", + None, + Right(Tuple2(2, 2)), + null, + Interpolation.Nearest + ), + Right("""tf.keras.layers.UpSampling2D(size=(2, 2), data_format=None, interpolation="nearest", name="name")"""), + null + ), + Arguments.of( + Layer.GlobalAveragePooling2D( + "name", + None, + DataFormat.ChannelsLast + ), + Right("""tf.keras.layers.GlobalAveragePooling2D(data_format="channels_last", name="name")"""), + null ) ) diff --git a/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Interpolation.kt b/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Interpolation.kt new file mode 100644 index 00000000..ef9a1369 --- /dev/null +++ b/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Interpolation.kt @@ -0,0 +1,8 @@ +package edu.wpi.axon.tfdata.layer + +/** + * Values for the `interpolation` parameter for sampling-type layers. + */ +enum class Interpolation(val value: String) { + Nearest("nearest"), Bilinear("bilinear") +} diff --git a/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Layer.kt b/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Layer.kt index 45abacf9..bfcb1249 100644 --- a/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Layer.kt +++ b/tf-data/src/main/kotlin/edu/wpi/axon/tfdata/layer/Layer.kt @@ -83,6 +83,17 @@ sealed class Layer { override val inputs: Option> ) : Layer() + /** + * A layer that contains an entire model inside it. + * + * @param model The model that acts as this layer. + */ + data class ModelLayer( + override val name: String, + override val inputs: Option>, + val model: Model + ) : Layer() + /** * A layer that accepts input data and has no parameters. * @@ -141,6 +152,18 @@ sealed class Layer { val virtualBatchSize: Int? = null ) : Layer() + /** + * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/AveragePooling2D + */ + data class AveragePooling2D( + override val name: String, + override val inputs: Option>, + val poolSize: Either> = Right(Tuple2(2, 2)), + val strides: Either>? = null, + val padding: PoolingPadding = PoolingPadding.Valid, + val dataFormat: DataFormat? = null + ) : Layer() + /** * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/Conv2D */ @@ -197,6 +220,24 @@ sealed class Layer { val dataFormat: DataFormat? = null ) : Layer() + /** + * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/GlobalAveragePooling2D + */ + data class GlobalAveragePooling2D( + override val name: String, + override val inputs: Option>, + val dataFormat: DataFormat? + ) : Layer() + + /** + * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/GlobalMaxPool2D + */ + data class GlobalMaxPooling2D( + override val name: String, + override val inputs: Option>, + val dataFormat: DataFormat? = null + ) : Layer() + /** * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/MaxPool2D */ @@ -208,4 +249,34 @@ sealed class Layer { val padding: PoolingPadding = PoolingPadding.Valid, val dataFormat: DataFormat? = null ) : Layer() + + /** + * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/SpatialDropout2D + */ + data class SpatialDropout2D( + override val name: String, + override val inputs: Option>, + val rate: Double, + val dataFormat: DataFormat? = null + ) : Layer() { + + init { + require(rate in 0.0..1.0) { + "rate ($rate) was outside the allowed range of [0, 1]." + } + } + } + + /** + * https://www.tensorflow.org/versions/r1.14/api_docs/python/tf/keras/layers/UpSampling2D + * + * Bug: TF does not export a value for [interpolation]. + */ + data class UpSampling2D( + override val name: String, + override val inputs: Option>, + val size: Either> = Right(Tuple2(2, 2)), + val dataFormat: DataFormat? = null, + val interpolation: Interpolation = Interpolation.Nearest + ) : Layer() } diff --git a/tf-data/src/test/kotlin/edu/wpi/axon/tfdata/layer/LayerTest.kt b/tf-data/src/test/kotlin/edu/wpi/axon/tfdata/layer/LayerTest.kt index 9b1c7558..61cc6002 100644 --- a/tf-data/src/test/kotlin/edu/wpi/axon/tfdata/layer/LayerTest.kt +++ b/tf-data/src/test/kotlin/edu/wpi/axon/tfdata/layer/LayerTest.kt @@ -14,6 +14,18 @@ internal class LayerTest { @Test fun `dropout with invalid rate`() { + shouldThrow { Layer.Dropout("", None, -0.1) } shouldThrow { Layer.Dropout("", None, 1.2) } } + + @Test + fun `spatialdropout2d with valid rate`() { + shouldNotThrow { Layer.SpatialDropout2D("", None, 0.5) } + } + + @Test + fun `spatialdropout2d with invalid rate`() { + shouldThrow { Layer.SpatialDropout2D("", None, -0.1) } + shouldThrow { Layer.SpatialDropout2D("", None, 1.2) } + } } diff --git a/tf-layer-loader/src/main/kotlin/edu/wpi/axon/tflayerloader/LoadLayersFromHDF5.kt b/tf-layer-loader/src/main/kotlin/edu/wpi/axon/tflayerloader/LoadLayersFromHDF5.kt index b007805d..494f2525 100644 --- a/tf-layer-loader/src/main/kotlin/edu/wpi/axon/tflayerloader/LoadLayersFromHDF5.kt +++ b/tf-layer-loader/src/main/kotlin/edu/wpi/axon/tflayerloader/LoadLayersFromHDF5.kt @@ -18,6 +18,7 @@ import edu.wpi.axon.tfdata.layer.Activation import edu.wpi.axon.tfdata.layer.Constraint import edu.wpi.axon.tfdata.layer.DataFormat import edu.wpi.axon.tfdata.layer.Initializer +import edu.wpi.axon.tfdata.layer.Interpolation import edu.wpi.axon.tfdata.layer.Layer import edu.wpi.axon.tfdata.layer.PoolingPadding import edu.wpi.axon.tfdata.layer.Regularizer @@ -129,6 +130,8 @@ class LoadLayersFromHDF5( val json = data["config"] as JsonObject val name = json["name"] as String return when (className) { + "Sequential", "Model" -> Layer.ModelLayer(name, data.inboundNodes(), parseModel(data)) + "InputLayer" -> Layer.InputLayer( name, (json["batch_input_shape"] as JsonArray).toList().let { @@ -166,8 +169,16 @@ class LoadLayersFromHDF5( json["virtual_batch_size"] as Int? ) - "Conv2D" - -> Layer.Conv2D( + "AvgPool2D", "AveragePooling2D" -> Layer.AveragePooling2D( + name, + data.inboundNodes(), + json["pool_size"].tuple2OrInt(), + json["strides"].tuple2OrIntOrNull(), + json["padding"].poolingPadding(), + json["data_format"].dataFormatOrNull() + ) + + "Conv2D" -> Layer.Conv2D( name, data.inboundNodes(), json["filters"] as Int, @@ -208,6 +219,18 @@ class LoadLayersFromHDF5( json["data_format"].dataFormatOrNull() ) + "GlobalAveragePooling2D", "GlobalAvgPool2D" -> Layer.GlobalAveragePooling2D( + name, + data.inboundNodes(), + json["data_format"].dataFormatOrNull() + ) + + "GlobalMaxPooling2D", "GlobalMaxPool2D" -> Layer.GlobalMaxPooling2D( + name, + data.inboundNodes(), + json["data_format"].dataFormatOrNull() + ) + "MaxPool2D", "MaxPooling2D" -> Layer.MaxPooling2D( name, data.inboundNodes(), @@ -217,6 +240,21 @@ class LoadLayersFromHDF5( json["data_format"].dataFormatOrNull() ) + "SpatialDropout2D" -> Layer.SpatialDropout2D( + name, + data.inboundNodes(), + json["rate"].double(), + json["data_format"].dataFormatOrNull() + ) + + "UpSampling2D" -> Layer.UpSampling2D( + name, + data.inboundNodes(), + json["size"].tuple2OrInt(), + json["data_format"].dataFormatOrNull(), + json["interpolation"].interpolation() + ) + else -> Layer.UnknownLayer( name, data.inboundNodes() @@ -363,6 +401,13 @@ private fun Any?.dataFormatOrNull(): DataFormat? = when (this as? String) { else -> throw IllegalArgumentException("Not convertible: $this") } +private fun Any?.interpolation(): Interpolation = when (this as? String) { + // Null in versions < v1.15.0 (TF bug). Use nearest as the default + null, "nearest" -> Interpolation.Nearest + "bilinear" -> Interpolation.Bilinear + else -> throw IllegalArgumentException("Not convertible: $this") +} + private fun Any?.tuple2OrInt(): Either> = when { this is Int -> Left(this) diff --git a/tf-layer-loader/src/test/kotlin/edu/wpi/axon/tflayerloader/LoadSpecificLayerTypesIntegrationTest.kt b/tf-layer-loader/src/test/kotlin/edu/wpi/axon/tflayerloader/LoadSpecificLayerTypesIntegrationTest.kt new file mode 100644 index 00000000..ef52d874 --- /dev/null +++ b/tf-layer-loader/src/test/kotlin/edu/wpi/axon/tflayerloader/LoadSpecificLayerTypesIntegrationTest.kt @@ -0,0 +1,102 @@ +@file:SuppressWarnings("LongMethod", "LargeClass") +@file:Suppress("UnstableApiUsage") + +package edu.wpi.axon.tflayerloader + +import arrow.core.None +import arrow.core.Right +import arrow.core.Tuple2 +import edu.wpi.axon.tfdata.Model +import edu.wpi.axon.tfdata.layer.DataFormat +import edu.wpi.axon.tfdata.layer.Interpolation +import edu.wpi.axon.tfdata.layer.Layer +import edu.wpi.axon.tfdata.layer.PoolingPadding +import io.kotlintest.matchers.collections.shouldContainExactly +import io.kotlintest.shouldBe +import org.junit.jupiter.api.Test + +internal class LoadSpecificLayerTypesIntegrationTest { + + @Test + fun `load AvgPool2D`() { + loadModel("sequential_with_avgpool2d.h5") { + it.name shouldBe "sequential_7" + it.batchInputShape shouldBe listOf(null, null, 2, 2) + it.layers.shouldContainExactly( + Layer.AveragePooling2D( + "average_pooling2d_7", + None, + Right(Tuple2(2, 2)), + Right(Tuple2(2, 2)), + PoolingPadding.Valid, + DataFormat.ChannelsLast + ).trainable() + ) + } + } + + @Test + fun `load GlobalMaxPooling2D`() { + loadModel("sequential_with_globalmaxpooling2d.h5") { + it.name shouldBe "sequential_8" + it.batchInputShape shouldBe listOf(null, null, 2, 2) + it.layers.shouldContainExactly( + Layer.GlobalMaxPooling2D( + "global_max_pooling2d", + None, + DataFormat.ChannelsLast + ).trainable() + ) + } + } + + @Test + fun `load SpatialDropout2D`() { + loadModel("sequential_with_spatialdropout2d.h5") { + it.name shouldBe "sequential_9" + it.batchInputShape shouldBe listOf(null, null, 2, 2) + it.layers.shouldContainExactly( + Layer.SpatialDropout2D( + "spatial_dropout2d", + None, + 0.2, + null + ).trainable() + ) + } + } + + @Test + fun `load UpSampling2D`() { + loadModel("sequential_with_upsampling2d_nearest.h5") { + it.name shouldBe "sequential_3" + it.batchInputShape shouldBe listOf(null, null, 2, 2) + it.layers.shouldContainExactly( + Layer.UpSampling2D( + "up_sampling2d_3", + None, + Right(Tuple2(2, 2)), + DataFormat.ChannelsLast, + Interpolation.Nearest + ).trainable() + ) + } + } + + @Test + fun `load UpSampling2D bilinear`() { + loadModel("sequential_with_upsampling2d_bilinear.h5") { + it.name shouldBe "sequential_2" + it.batchInputShape shouldBe listOf(null, null, 2, 2) + it.layers.shouldContainExactly( + Layer.UpSampling2D( + "up_sampling2d_2", + None, + Right(Tuple2(2, 2)), + DataFormat.ChannelsLast, + Interpolation.Bilinear + ).trainable() + ) + } + } +} diff --git a/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_avgpool2d.h5 b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_avgpool2d.h5 new file mode 100644 index 00000000..72d637cc Binary files /dev/null and b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_avgpool2d.h5 differ diff --git a/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_globalmaxpooling2d.h5 b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_globalmaxpooling2d.h5 new file mode 100644 index 00000000..8fc6e130 Binary files /dev/null and b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_globalmaxpooling2d.h5 differ diff --git a/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_spatialdropout2d.h5 b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_spatialdropout2d.h5 new file mode 100644 index 00000000..101162fb Binary files /dev/null and b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_spatialdropout2d.h5 differ diff --git a/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_bilinear.h5 b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_bilinear.h5 new file mode 100644 index 00000000..a3850f95 Binary files /dev/null and b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_bilinear.h5 differ diff --git a/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_nearest.h5 b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_nearest.h5 new file mode 100644 index 00000000..797c7d15 Binary files /dev/null and b/tf-layer-loader/src/test/resources/edu/wpi/axon/tflayerloader/sequential_with_upsampling2d_nearest.h5 differ diff --git a/training/src/test/kotlin/edu/wpi/axon/training/MobilenetIntegrationTest.kt b/training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-14-IntegrationTest.kt similarity index 99% rename from training/src/test/kotlin/edu/wpi/axon/training/MobilenetIntegrationTest.kt rename to training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-14-IntegrationTest.kt index c1430d11..cac778ff 100644 --- a/training/src/test/kotlin/edu/wpi/axon/training/MobilenetIntegrationTest.kt +++ b/training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-14-IntegrationTest.kt @@ -14,7 +14,7 @@ import io.kotlintest.shouldBe import org.junit.jupiter.api.Test import org.koin.core.context.startKoin -internal class MobilenetIntegrationTest : KoinTestFixture() { +internal class `Mobilenet-v-1-14-IntegrationTest` : KoinTestFixture() { @Test fun `test with mobilenet`() { diff --git a/training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-15-IntegrationTest.kt b/training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-15-IntegrationTest.kt new file mode 100644 index 00000000..7a51fd76 --- /dev/null +++ b/training/src/test/kotlin/edu/wpi/axon/training/Mobilenet-v-1-15-IntegrationTest.kt @@ -0,0 +1,54 @@ +@file:SuppressWarnings("LongMethod", "LargeClass") + +package edu.wpi.axon.training + +import arrow.core.None +import edu.wpi.axon.dsl.defaultModule +import edu.wpi.axon.testutil.KoinTestFixture +import edu.wpi.axon.tfdata.Model +import edu.wpi.axon.tfdata.layer.Activation +import edu.wpi.axon.tfdata.layer.DataFormat +import edu.wpi.axon.tfdata.layer.Layer +import io.kotlintest.matchers.collections.shouldHaveSize +import io.kotlintest.matchers.types.shouldBeInstanceOf +import io.kotlintest.shouldBe +import org.junit.jupiter.api.Test +import org.koin.core.context.startKoin + +internal class `Mobilenet-v-1-15-IntegrationTest` : KoinTestFixture() { + + @Test + fun `test with mobilenet`() { + startKoin { + modules(defaultModule()) + } + + val modelName = "mobilenet_tf_1_15_0.h5" + val (model, path) = loadModel(modelName) + model.shouldBeInstanceOf { + it.layers.shouldHaveSize(3) + it.layers.toList().let { + it[0].shouldBeInstanceOf { + it.layer.shouldBeInstanceOf { + it.model.shouldBeInstanceOf { + it.layers.nodes().shouldHaveSize(155) + } + } + } + + it[1] shouldBe Layer.GlobalAveragePooling2D( + "global_average_pooling2d", + None, + DataFormat.ChannelsLast + ).trainable() + + it[2] shouldBe Layer.Dense( + "dense", + None, + 10, + activation = Activation.SoftMax + ).trainable() + } + } + } +} diff --git a/training/src/test/resources/edu/wpi/axon/training/mobilenet_tf_1_15_0.h5 b/training/src/test/resources/edu/wpi/axon/training/mobilenet_tf_1_15_0.h5 new file mode 100644 index 00000000..6c921aa1 Binary files /dev/null and b/training/src/test/resources/edu/wpi/axon/training/mobilenet_tf_1_15_0.h5 differ