From 54cd45c2119d2b36bb42e76ea74b15cbb5450bd6 Mon Sep 17 00:00:00 2001 From: Maciej Procyk Date: Wed, 2 Jun 2021 15:10:09 +0200 Subject: [PATCH] Add Conv1D impementation (#67) * fix conv2d layer test * create conv1d with code refactor * naming fixes * add docs version * fixes for PR requests * lint fixes * add model loader and saver from keras for conv1d --- .../dl/api/core/layer/convolutional/Conv1D.kt | 96 ++++++++++ .../dl/api/core/layer/convolutional/Conv2D.kt | 160 ++++++++++------ .../core/layer/convolutional/ConvPadding.kt | 10 +- .../layer/convolutional/DepthwiseConv2D.kt | 15 +- .../layer/convolutional/SeparableConv2D.kt | 19 +- .../kotlinx/dl/api/core/layer/core/Dense.kt | 5 +- .../api/core/layer/normalization/BatchNorm.kt | 10 +- .../dl/api/core/layer/pooling/AvgPool2D.kt | 8 +- .../dl/api/core/layer/pooling/MaxPool2D.kt | 8 +- .../dl/api/core/shape/ShapeFunctions.kt | 72 ++++++- .../dl/api/inference/keras/KerasConstants.kt | 1 + .../dl/api/inference/keras/ModelLoader.kt | 32 ++++ .../dl/api/inference/keras/ModelSaver.kt | 18 ++ .../kotlinx/dl/api/core/layer/Conv1DTest.kt | 49 +++++ .../kotlinx/dl/api/core/layer/Conv2DTest.kt | 75 ++++---- .../dl/api/core/layer/ConvLayerTest.kt | 178 ++++++++++-------- 16 files changed, 523 insertions(+), 233 deletions(-) create mode 100644 api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv1D.kt create mode 100644 api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv1DTest.kt diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv1D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv1D.kt new file mode 100644 index 000000000..665b33ec2 --- /dev/null +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv1D.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2021 JetBrains s.r.o. and Kotlin Deep Learning project contributors. All Rights Reserved. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.dl.api.core.layer.convolutional + +import org.jetbrains.kotlinx.dl.api.core.activation.Activations +import org.jetbrains.kotlinx.dl.api.core.initializer.HeNormal +import org.jetbrains.kotlinx.dl.api.core.initializer.HeUniform +import org.jetbrains.kotlinx.dl.api.core.initializer.Initializer +import org.tensorflow.Operand +import org.tensorflow.op.Ops +import org.tensorflow.op.core.Squeeze + +private const val KERNEL_VARIABLE_NAME = "conv1d_kernel" + +private const val BIAS_VARIABLE_NAME = "conv1d_bias" + +private const val EXTRA_DIM = 1L + +/** + * 1D convolution layer (e.g. convolution over audio data). + * + * This layer creates a convolution kernel that is convolved (actually cross-correlated) + * with the layer input to produce a tensor of outputs. + * Finally, the `activation` is applied to the outputs as well. + * + * It expects input data of size `(N, L, C)` where + * ``` + * N - batch size + * L - length of signal sequence + * C - number of channels + * ``` + * + * @property [filters] The dimensionality of the output space (i.e. the number of filters in the convolution). + * @property [kernelSize] Long number, specifying the width of the 1D convolution window. + * @property [strides] Three numbers specifying stride of the pooling + * operation for each dimension of input tensor. + * NOTE: Specifying stride value != 1 is incompatible with specifying `dilation` value != 1. + * @property [dilations] Three numbers specifying the dilation rate to use for + * dilated convolution sequence dimensions of input tensor. + * @property [activation] Activation function. + * @property [kernelInitializer] An initializer for the convolution kernel + * @property [biasInitializer] An initializer for the bias vector. + * @property [padding] The padding method, either 'valid' or 'same' or 'full'. + * @property [name] Custom layer name. + * @property [useBias] If true the layer uses a bias vector. + * @constructor Creates [Conv1D] object. + * + * @since 0.3 + */ +public class Conv1D( + public val filters: Long = 32, + public val kernelSize: Long = 3, + public val strides: LongArray = longArrayOf(1, 1, 1), + public val dilations: LongArray = longArrayOf(1, 1, 1), + public val activation: Activations = Activations.Relu, + public val kernelInitializer: Initializer = HeNormal(), + public val biasInitializer: Initializer = HeUniform(), + public val padding: ConvPadding = ConvPadding.SAME, + public val useBias: Boolean = true, + name: String = "", +) : Conv2DImpl( + filtersInternal = filters, + kernelSizeInternal = longArrayOf(1, kernelSize), + stridesInternal = longArrayOf(strides[0], 1, strides[1], strides[2]), + dilationsInternal = longArrayOf(dilations[0], 1, dilations[1], dilations[2]), + activationInternal = activation, + kernelInitializerInternal = kernelInitializer, + biasInitializerInternal = biasInitializer, + paddingInternal = padding, + useBiasInternal = useBias, + kernelVariableName = KERNEL_VARIABLE_NAME, + biasVariableName = BIAS_VARIABLE_NAME, + name = name +) { + private val squeezeAxis = Squeeze.axis(listOf(EXTRA_DIM)) + + override fun forward( + tf: Ops, + input: Operand, + isTraining: Operand, + numberOfLosses: Operand? + ): Operand { + val reshapedInput = tf.expandDims(input, tf.constant(EXTRA_DIM)) + val result = super.forward(tf, reshapedInput, isTraining, numberOfLosses) + return tf.squeeze(result, squeezeAxis) + } + + override fun toString(): String { + return "Conv2D(filters=$filters, kernelSize=$kernelSize, strides=$strides, " + + "dilation=$dilations, activation=$activation, kernelInitializer=$kernelInitializer, " + + "biasInitializer=$biasInitializer, kernelShape=$kernelShape, biasShape=$biasShape, padding=$padding)" + } +} diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv2D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv2D.kt index 9951646a3..a22eff3bb 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv2D.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/Conv2D.kt @@ -21,9 +21,11 @@ import org.tensorflow.op.Ops import org.tensorflow.op.core.Variable import org.tensorflow.op.nn.Conv2d import org.tensorflow.op.nn.Conv2d.dilations +import java.lang.IllegalArgumentException import kotlin.math.roundToInt private const val KERNEL_VARIABLE_NAME = "conv2d_kernel" + private const val BIAS_VARIABLE_NAME = "conv2d_bias" /** @@ -31,12 +33,20 @@ private const val BIAS_VARIABLE_NAME = "conv2d_bias" * * This layer creates a convolution kernel that is convolved (actually cross-correlated) * with the layer input to produce a tensor of outputs. - * Finally, if `activation` is applied to the outputs as well. + * Finally, the `activation` is applied to the outputs as well. + * + * It expects input data of size `(N, H, W, C)` where + * ``` + * N - batch size + * H - height + * W - width + * C - number of channels + * ``` * * @property [filters] The dimensionality of the output space (i.e. the number of filters in the convolution). * @property [kernelSize] Two long numbers, specifying the height and width of the 2D convolution window. * @property [strides] Strides of the pooling operation for each dimension of input tensor. - * NOTE: Specifying any stride value != 1 is incompatible with specifying any `dilation_rate` value != 1. + * NOTE: Specifying any stride value != 1 is incompatible with specifying any `dilations` value != 1. * @property [dilations] Four numbers, specifying the dilation rate to use for dilated convolution for each dimension of input tensor. * @property [activation] Activation function. * @property [kernelInitializer] An initializer for the convolution kernel @@ -57,69 +67,89 @@ public class Conv2D( public val padding: ConvPadding = ConvPadding.SAME, public val useBias: Boolean = true, name: String = "" +) : Conv2DImpl( + filtersInternal = filters, + kernelSizeInternal = kernelSize, + stridesInternal = strides, + dilationsInternal = dilations, + activationInternal = activation, + kernelInitializerInternal = kernelInitializer, + biasInitializerInternal = biasInitializer, + paddingInternal = padding, + useBiasInternal = useBias, + kernelVariableName = KERNEL_VARIABLE_NAME, + biasVariableName = BIAS_VARIABLE_NAME, + name = name +) { + init { + assertArraySize(kernelSize, 2, "kernelSize") + assertArraySize(strides, 4, "strides") + assertArraySize(dilations, 4, "dilations") + } + + override fun toString(): String { + return "Conv2D(filters=$filters, kernelSize=${kernelSize.contentToString()}, strides=${strides.contentToString()}, " + + "dilations=${dilations.contentToString()}, activation=$activation, kernelInitializer=$kernelInitializer, " + + "biasInitializer=$biasInitializer, kernelShape=$kernelShape, biasShape=$biasShape, padding=$padding)" + } +} + +public abstract class Conv2DImpl( + private val filtersInternal: Long, + private val kernelSizeInternal: LongArray, + private val stridesInternal: LongArray, + private val dilationsInternal: LongArray, + private val activationInternal: Activations, + private val kernelInitializerInternal: Initializer, + private val biasInitializerInternal: Initializer, + private val paddingInternal: ConvPadding, + private val useBiasInternal: Boolean, + private val kernelVariableName: String, + private val biasVariableName: String, + name: String = "" ) : Layer(name) { // weight tensors private lateinit var kernel: Variable private var bias: Variable? = null // weight tensor shapes - private lateinit var biasShape: Shape - private lateinit var kernelShape: Shape + protected lateinit var kernelShape: Shape + protected lateinit var biasShape: Shape override fun build(tf: Ops, kGraph: KGraph, inputShape: Shape) { // Amount of channels should be the last value in the inputShape (make warning here) val lastElement = inputShape.size(inputShape.numDimensions() - 1) // Compute shapes of kernel and bias matrices - kernelShape = shapeFromDims(*kernelSize, lastElement, filters) - biasShape = Shape.make(filters) + kernelShape = shapeFromDims(*kernelSizeInternal, lastElement, filtersInternal) + biasShape = Shape.make(filtersInternal) - // should be calculated before addWeight because it's used in calculation, need to rewrite addWEight to avoid strange behaviour - // calculate fanIn, fanOut + // should be calculated before addWeight because it's used in calculation, + // need to rewrite addWeight to avoid strange behaviour calculate fanIn, fanOut val inputDepth = lastElement // amount of channels - val outputDepth = filters // amount of channels for the next layer + val outputDepth = filtersInternal // amount of channels for the next layer - fanIn = (inputDepth * kernelSize[0] * kernelSize[1]).toInt() - fanOut = ((outputDepth * kernelSize[0] * kernelSize[1] / (strides[0].toDouble() * strides[1])).roundToInt()) + fanIn = (inputDepth * kernelSizeInternal[0] * kernelSizeInternal[1]).toInt() + fanOut = ((outputDepth * kernelSizeInternal[0] * kernelSizeInternal[1] / + (stridesInternal[0].toDouble() * stridesInternal[1])).roundToInt()) val (kernelVariableName, biasVariableName) = defineVariableNames() createConv2DVariables(tf, kernelVariableName, biasVariableName, kGraph) } - private fun defineVariableNames(): Pair { - return if (name.isNotEmpty()) { - Pair(conv2dKernelVarName(name), conv2dBiasVarName(name)) - } else { - Pair(KERNEL_VARIABLE_NAME, BIAS_VARIABLE_NAME) - } - } - - private fun createConv2DVariables( - tf: Ops, - kernelVariableName: String, - biasVariableName: String, - kGraph: KGraph - ) { - kernel = tf.withName(kernelVariableName).variable(kernelShape, getDType()) - if (useBias) bias = tf.withName(biasVariableName).variable(biasShape, getDType()) - - kernel = addWeight(tf, kGraph, kernelVariableName, kernel, kernelInitializer) - if (useBias) bias = addWeight(tf, kGraph, biasVariableName, bias!!, biasInitializer) - } - override fun computeOutputShape(inputShape: Shape): Shape { var rows = inputShape.size(1) var cols = inputShape.size(2) rows = convOutputLength( - rows, kernelSize[0].toInt(), padding, - strides[1].toInt(), dilations[1].toInt() + rows, kernelSizeInternal[0].toInt(), paddingInternal, + stridesInternal[1].toInt(), dilationsInternal[1].toInt() ) cols = convOutputLength( - cols, kernelSize[1].toInt(), padding, - strides[2].toInt(), dilations[2].toInt() + cols, kernelSizeInternal[1].toInt(), paddingInternal, + stridesInternal[2].toInt(), dilationsInternal[2].toInt() ) - val shape = Shape.make(inputShape.size(0), rows, cols, filters) + val shape = Shape.make(inputShape.size(0), rows, cols, filtersInternal) outputShape = TensorShape(shape) return shape } @@ -130,26 +160,15 @@ public class Conv2D( isTraining: Operand, numberOfLosses: Operand? ): Operand { - val tfPadding = when (padding) { - ConvPadding.SAME -> "SAME" - ConvPadding.VALID -> "VALID" - ConvPadding.FULL -> "FULL" - } + val paddingName = paddingInternal.paddingName + val options: Conv2d.Options = dilations(dilationsInternal.toList()).dataFormat("NHWC") + var output: Operand = tf.nn.conv2d(input, kernel, stridesInternal.toMutableList(), paddingName, options) - val options: Conv2d.Options = dilations(dilations.toList()).dataFormat("NHWC") - var output: Operand = tf.nn.conv2d(input, kernel, strides.toMutableList(), tfPadding, options) - - if (useBias) { + if (useBiasInternal) { output = tf.nn.biasAdd(output, bias) } - return Activations.convert(activation).apply(tf, output, name) - } - - override val weights: Map> get() = extractConv2DWeights() - - private fun extractConv2DWeights(): Map> { - return extractWeights(defineVariableNames().toList()) + return Activations.convert(activationInternal).apply(tf, output, name) } /** Returns the shape of kernel weights. */ @@ -158,12 +177,41 @@ public class Conv2D( /** Returns the shape of bias weights. */ public val biasShapeArray: LongArray get() = TensorShape(biasShape).dims() + override val weights: Map> get() = extractConv2DWeights() + override val hasActivation: Boolean get() = true override val paramCount: Int - get() = (numElementsInShape(shapeToLongArray(kernelShape)) + numElementsInShape(shapeToLongArray(biasShape))).toInt() + get() = (kernelShape.numElements() + biasShape.numElements()).toInt() - override fun toString(): String { - return "Conv2D(filters=$filters, kernelSize=${kernelSize.contentToString()}, strides=${strides.contentToString()}, dilations=${dilations.contentToString()}, activation=$activation, kernelInitializer=$kernelInitializer, biasInitializer=$biasInitializer, kernelShape=$kernelShape, padding=$padding)" + private fun extractConv2DWeights(): Map> { + return extractWeights(defineVariableNames().toList()) + } + + private fun defineVariableNames(): Pair { + return if (name.isNotEmpty()) { + Pair(conv2dKernelVarName(name), conv2dBiasVarName(name)) + } else { + Pair(kernelVariableName, biasVariableName) + } + } + + private fun createConv2DVariables( + tf: Ops, + kernelVariableName: String, + biasVariableName: String, + kGraph: KGraph + ) { + kernel = tf.withName(kernelVariableName).variable(kernelShape, getDType()) + if (useBiasInternal) bias = tf.withName(biasVariableName).variable(biasShape, getDType()) + + kernel = addWeight(tf, kGraph, kernelVariableName, kernel, kernelInitializerInternal) + if (useBiasInternal) bias = addWeight(tf, kGraph, biasVariableName, bias!!, biasInitializerInternal) + } +} + +private fun assertArraySize(array: LongArray, size: Int, name: String) { + if (array.size != size) { + throw IllegalArgumentException("$name is expected to have size equal $size") } } diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/ConvPadding.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/ConvPadding.kt index 699369f83..964544e7b 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/ConvPadding.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/ConvPadding.kt @@ -8,16 +8,16 @@ package org.jetbrains.kotlinx.dl.api.core.layer.convolutional /** * Type of padding. */ -public enum class ConvPadding { +public enum class ConvPadding(internal val paddingName: String) { /** * Results in padding evenly to the left/right or up/down of the input such that output has the same * height/width dimension as the input. */ - SAME, + SAME("SAME"), - /** No padding. */ - VALID, + /** No padding. Results in smaller output size for `kernelSize > 1` */ + VALID("VALID"), /** Full padding. For Keras compatibility goals. */ - FULL + FULL("FULL"); } diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/DepthwiseConv2D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/DepthwiseConv2D.kt index 510376809..bad0fc345 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/DepthwiseConv2D.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/DepthwiseConv2D.kt @@ -137,19 +137,14 @@ public class DepthwiseConv2D( isTraining: Operand, numberOfLosses: Operand? ): Operand { - val tfPadding = when (padding) { - ConvPadding.SAME -> "SAME" - ConvPadding.VALID -> "VALID" - ConvPadding.FULL -> "FULL" - } - + val paddingName = padding.paddingName val options: DepthwiseConv2dNative.Options = dilations(dilations.toList()).dataFormat("NHWC") var output: Operand = tf.nn.depthwiseConv2dNative( input, depthwiseKernel, strides.toMutableList(), - tfPadding, + paddingName, options ) @@ -175,11 +170,7 @@ public class DepthwiseConv2D( override val hasActivation: Boolean get() = true override val paramCount: Int - get() = (numElementsInShape(shapeToLongArray(depthwiseKernelShape)) + numElementsInShape( - shapeToLongArray( - biasShape - ) - )).toInt() + get() = (depthwiseKernelShape.numElements() + biasShape.numElements()).toInt() override fun toString(): String { return "DepthwiseConv2D(kernelSize=${kernelSize.contentToString()}, strides=${strides.contentToString()}, dilations=${dilations.contentToString()}, activation=$activation, depthMultiplier=$depthMultiplier, depthwiseInitializer=$depthwiseInitializer, biasInitializer=$biasInitializer, padding=$padding, useBias=$useBias, depthwiseKernel=$depthwiseKernel, bias=$bias, biasShape=$biasShape, depthwiseKernelShape=$depthwiseKernelShape)" diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/SeparableConv2D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/SeparableConv2D.kt index 8a4b53f98..3ed6bfcfd 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/SeparableConv2D.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/SeparableConv2D.kt @@ -165,12 +165,7 @@ public class SeparableConv2D( isTraining: Operand, numberOfLosses: Operand? ): Operand { - val tfPadding = when (padding) { - ConvPadding.SAME -> "SAME" - ConvPadding.VALID -> "VALID" - ConvPadding.FULL -> "FULL" - } - + val paddingName = padding.paddingName val depthwiseConv2DOptions: DepthwiseConv2dNative.Options = dilations(dilations.toList()).dataFormat("NHWC") val depthwiseOutput: Operand = @@ -178,7 +173,7 @@ public class SeparableConv2D( input, depthwiseKernel, strides.toMutableList(), - tfPadding, + paddingName, depthwiseConv2DOptions ) @@ -213,15 +208,7 @@ public class SeparableConv2D( override val hasActivation: Boolean get() = true override val paramCount: Int - get() = (numElementsInShape(shapeToLongArray(depthwiseKernelShape)) + numElementsInShape( - shapeToLongArray( - pointwiseKernelShape - ) - ) + numElementsInShape( - shapeToLongArray( - biasShape - ) - )).toInt() + get() = (depthwiseKernelShape.numElements() + pointwiseKernelShape.numElements() + biasShape.numElements()).toInt() override fun toString(): String { return "SeparableConv2D(kernelSize=${kernelSize.contentToString()}, strides=${strides.contentToString()}, dilations=${dilations.contentToString()}, activation=$activation, depthwiseInitializer=$depthwiseInitializer, biasInitializer=$biasInitializer, kernelShape=$depthwiseKernelShape, padding=$padding)" diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/core/Dense.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/core/Dense.kt index 0d3304d10..2d62a464f 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/core/Dense.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/core/Dense.kt @@ -12,8 +12,7 @@ import org.jetbrains.kotlinx.dl.api.core.initializer.HeUniform import org.jetbrains.kotlinx.dl.api.core.initializer.Initializer import org.jetbrains.kotlinx.dl.api.core.layer.Layer import org.jetbrains.kotlinx.dl.api.core.shape.TensorShape -import org.jetbrains.kotlinx.dl.api.core.shape.numElementsInShape -import org.jetbrains.kotlinx.dl.api.core.shape.shapeToLongArray +import org.jetbrains.kotlinx.dl.api.core.shape.numElements import org.jetbrains.kotlinx.dl.api.core.util.denseBiasVarName import org.jetbrains.kotlinx.dl.api.core.util.denseKernelVarName import org.jetbrains.kotlinx.dl.api.core.util.getDType @@ -115,7 +114,7 @@ public class Dense( override val hasActivation: Boolean get() = true override val paramCount: Int - get() = (numElementsInShape(shapeToLongArray(kernelShape)) + numElementsInShape(shapeToLongArray(biasShape))).toInt() + get() = (kernelShape.numElements() + biasShape.numElements()).toInt() /** Returns the shape of kernel weights. */ public val kernelShapeArray: LongArray get() = TensorShape(kernelShape).dims() diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/normalization/BatchNorm.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/normalization/BatchNorm.kt index 0354786af..523fc0f02 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/normalization/BatchNorm.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/normalization/BatchNorm.kt @@ -12,8 +12,7 @@ import org.jetbrains.kotlinx.dl.api.core.initializer.Zeros import org.jetbrains.kotlinx.dl.api.core.layer.Layer import org.jetbrains.kotlinx.dl.api.core.layer.NoGradients import org.jetbrains.kotlinx.dl.api.core.shape.TensorShape -import org.jetbrains.kotlinx.dl.api.core.shape.numElementsInShape -import org.jetbrains.kotlinx.dl.api.core.shape.shapeToLongArray +import org.jetbrains.kotlinx.dl.api.core.shape.numElements import org.jetbrains.kotlinx.dl.api.core.util.* import org.tensorflow.Operand import org.tensorflow.Shape @@ -149,10 +148,9 @@ public class BatchNorm( override val paramCount: Int get() { - var paramCount = - numElementsInShape(shapeToLongArray(weightShape)) + numElementsInShape(shapeToLongArray(weightShape)) - if (scale) paramCount += numElementsInShape(shapeToLongArray(weightShape)) - if (center) paramCount += numElementsInShape(shapeToLongArray(weightShape)) + var paramCount = weightShape.numElements() + weightShape.numElements() + if (scale) paramCount += weightShape.numElements() + if (center) paramCount += weightShape.numElements() return paramCount.toInt() } diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/AvgPool2D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/AvgPool2D.kt index d73620753..6a404d684 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/AvgPool2D.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/AvgPool2D.kt @@ -65,17 +65,13 @@ public class AvgPool2D( stridesLongList.add(it.toLong()) } - val tfPadding = when (padding) { - ConvPadding.SAME -> "SAME" - ConvPadding.VALID -> "VALID" - ConvPadding.FULL -> "FULL" - } + val paddingName = padding.paddingName return tf.nn.avgPool( input, poolSizeLongList, stridesLongList, - tfPadding + paddingName ) } diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/MaxPool2D.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/MaxPool2D.kt index 55a7d4442..3d503fa24 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/MaxPool2D.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/pooling/MaxPool2D.kt @@ -54,17 +54,13 @@ public class MaxPool2D( isTraining: Operand, numberOfLosses: Operand? ): Operand { - val tfPadding = when (padding) { - ConvPadding.SAME -> "SAME" - ConvPadding.VALID -> "VALID" - ConvPadding.FULL -> "FULL" - } + val paddingName = padding.paddingName return tf.nn.maxPool( input, tf.constant(poolSize), tf.constant(strides), - tfPadding + paddingName ) } diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/shape/ShapeFunctions.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/shape/ShapeFunctions.kt index 415291dae..179d0756c 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/shape/ShapeFunctions.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/shape/ShapeFunctions.kt @@ -27,19 +27,19 @@ internal fun shapeOperand(tf: Ops, shape: Shape): Operand { } /** Extracts dimensions as [IntArray] from [Shape]. */ -internal fun shapeToIntArray(shape: Shape): IntArray { - val shapeArray = IntArray(shape.numDimensions()) +internal fun Shape.toIntArray(): IntArray { + val shapeArray = IntArray(numDimensions()) for (i in shapeArray.indices) { - shapeArray[i] = shape.size(i).toInt() + shapeArray[i] = size(i).toInt() } return shapeArray } /** Extracts dimensions as [LongArray] from [Shape]. */ -internal fun shapeToLongArray(shape: Shape): LongArray { - val shapeArray = LongArray(shape.numDimensions()) +internal fun Shape.toLongArray(): LongArray { + val shapeArray = LongArray(numDimensions()) for (i in shapeArray.indices) { - shapeArray[i] = shape.size(i) + shapeArray[i] = size(i) } return shapeArray } @@ -86,6 +86,9 @@ internal fun numElementsInShape(shape: LongArray): Long { return prod } +/** Returns amount of elements in [Shape]. */ +internal fun Shape.numElements(): Long = numElementsInShape(toLongArray()) + /** Reshapes 2D array of floats to 1D array of floats. */ internal fun reshape2DTo1D(dst: Array, size: Int): FloatArray { val result = FloatArray(size) { 0.0f } @@ -136,3 +139,60 @@ internal fun reshape4DTo1D(dst: Array>>, size: Int): Flo } return result } + +/** + * Get shape of array of arrays (of arrays...) of Array of elems of any type. + * If the most inner array does not have any elements its size is missed in result */ +internal fun getShapeOfArray(data: Array<*>): Shape { + fun appendPrimitiveArraySize(size: Int, acc: MutableList): LongArray { + acc += size.toLong() + return acc.toLongArray() + } + tailrec fun collectDims(data: Array<*>, acc: MutableList): LongArray { + val firstElem = data[0] ?: return acc.toLongArray() + acc += data.size.toLong() + return when(firstElem) { + is Array<*> -> collectDims(firstElem, acc) + is BooleanArray -> appendPrimitiveArraySize(firstElem.size, acc) + is ByteArray -> appendPrimitiveArraySize(firstElem.size, acc) + is CharArray -> appendPrimitiveArraySize(firstElem.size, acc) + is ShortArray -> appendPrimitiveArraySize(firstElem.size, acc) + is IntArray -> appendPrimitiveArraySize(firstElem.size, acc) + is LongArray -> appendPrimitiveArraySize(firstElem.size, acc) + is FloatArray -> appendPrimitiveArraySize(firstElem.size, acc) + is DoubleArray -> appendPrimitiveArraySize(firstElem.size, acc) + else -> acc.toLongArray() + } + } + return shapeFromDims(*collectDims(data, mutableListOf())) +} + +/** + * Create an array of arrays (of arrays...) of Floats with specified [shape] and + * initialized with given [initValue]. When the number of dimensions in result tensor + * is bigger than 1, the last dimension array is FloatArray (instead of Array). + */ +internal fun getFloatArrayOfShape(shape: Shape, initValue: Float = 0.0f): Array<*> { + fun getFloatArrayOfShape(shape: Shape, dimIndex: Int): Any = if (shape.numDimensions() - 1 == dimIndex) { + FloatArray(shape.size(dimIndex).toInt()) { initValue } + } else { + Array(shape.size(dimIndex).toInt()) { getFloatArrayOfShape(shape, dimIndex + 1) } + } + return if (shape.numDimensions() == 1) { + Array(shape.size(0).toInt()) { initValue } + } + else { + getFloatArrayOfShape(shape, 0) as Array<*> + } +} + +internal fun Any?.castArrayDim(): Array<*> = this as Array<*> + +/** Cast Array<*> to Array when sure about its dimensions */ +internal fun Array<*>.cast2DArray(): Array = this.map { it as FloatArray }.toTypedArray() + +/** Cast Array<*> to Array> when sure about its dimensions */ +internal fun Array<*>.cast3DArray(): Array> = this.map { it.castArrayDim().cast2DArray() }.toTypedArray() + +/** Cast Array<*> to Array>> when sure about its dimensions */ +internal fun Array<*>.cast4DArray(): Array>> = this.map { it.castArrayDim().cast3DArray() }.toTypedArray() diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/KerasConstants.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/KerasConstants.kt index c30f045cd..ae9484dd2 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/KerasConstants.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/KerasConstants.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlinx.dl.api.inference.keras // Keras layers +internal const val LAYER_CONV1D: String = "Conv1D" internal const val LAYER_CONV2D: String = "Conv2D" internal const val LAYER_DEPTHWISE_CONV2D: String = "DepthwiseConv2D" internal const val LAYER_SEPARABLE_CONV2D: String = "SeparableConv2D" diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelLoader.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelLoader.kt index f60c27039..9abbc64b1 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelLoader.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelLoader.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlinx.dl.api.core.layer.activation.ELU import org.jetbrains.kotlinx.dl.api.core.layer.activation.LeakyReLU import org.jetbrains.kotlinx.dl.api.core.layer.activation.ReLU import org.jetbrains.kotlinx.dl.api.core.layer.activation.ThresholdedReLU +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv1D import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv2D import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.ConvPadding import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.DepthwiseConv2D @@ -130,6 +131,7 @@ private fun convertToSequentialLayer( kerasLayer: KerasLayer ): Layer { return when (kerasLayer.class_name) { + LAYER_CONV1D -> createConv1D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_CONV2D -> createConv2D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_DEPTHWISE_CONV2D -> createDepthwiseConv2D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_SEPARABLE_CONV2D -> createSeparableConv2D(kerasLayer.config!!, kerasLayer.config.name!!) @@ -264,6 +266,7 @@ private fun convertToLayer( layersByName: MutableMap ): Layer { val layer = when (kerasLayer.class_name) { + LAYER_CONV1D -> createConv1D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_CONV2D -> createConv2D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_DEPTHWISE_CONV2D -> createDepthwiseConv2D(kerasLayer.config!!, kerasLayer.config.name!!) LAYER_SEPARABLE_CONV2D -> createSeparableConv2D(kerasLayer.config!!, kerasLayer.config.name!!) @@ -647,6 +650,35 @@ private fun createReshape(config: LayerConfig, name: String): Reshape { return Reshape(name = name, targetShape = config.target_shape!!) } +private fun createConv1D(config: LayerConfig, name: String): Conv1D { + val kernelSize = config.kernel_size!!.map { it.toLong() }[0] + val strides = config.strides!!.map { it.toLong() }.toLongArray() + + val addedOnesStrides = LongArray(3) + addedOnesStrides[0] = 1 + addedOnesStrides[1] = strides[0] + addedOnesStrides[2] = 1 + + val dilation = config.dilation_rate!!.map { it.toLong() }.toLongArray() + val addedOnesDilation = LongArray(3) + addedOnesDilation[0] = 1 + addedOnesDilation[1] = dilation[0] + addedOnesDilation[2] = 1 + + return Conv1D( + filters = config.filters!!.toLong(), + kernelSize = kernelSize, + strides = addedOnesStrides, + dilations = addedOnesDilation, + activation = convertToActivation(config.activation!!), + kernelInitializer = convertToInitializer(config.kernel_initializer!!), + biasInitializer = convertToInitializer(config.bias_initializer!!), + padding = convertPadding(config.padding!!), + useBias = config.use_bias!!, + name = name + ) +} + private fun createConv2D(config: LayerConfig, name: String): Conv2D { val kernelSize = config.kernel_size!!.map { it.toLong() }.toLongArray() val strides = config.strides!!.map { it.toLong() }.toLongArray() diff --git a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelSaver.kt b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelSaver.kt index 6a2977e8a..e9cac6945 100644 --- a/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelSaver.kt +++ b/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/inference/keras/ModelSaver.kt @@ -14,6 +14,7 @@ import org.jetbrains.kotlinx.dl.api.core.initializer.* import org.jetbrains.kotlinx.dl.api.core.layer.Layer import org.jetbrains.kotlinx.dl.api.core.layer.activation.LeakyReLU import org.jetbrains.kotlinx.dl.api.core.layer.activation.ThresholdedReLU +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv1D import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv2D import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.ConvPadding import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.DepthwiseConv2D @@ -71,6 +72,7 @@ public fun GraphTrainableModel.saveModelConfiguration(jsonConfigFile: File, isKe private fun convertToKerasLayer(layer: Layer, isKerasFullyCompatible: Boolean, isFunctional: Boolean): KerasLayer { val kerasLayer = when (layer::class) { + Conv1D::class -> createKerasConv1D(layer as Conv1D, isKerasFullyCompatible) Conv2D::class -> createKerasConv2D(layer as Conv2D, isKerasFullyCompatible) Flatten::class -> createKerasFlatten(layer as Flatten) MaxPool2D::class -> createKerasMaxPooling2D(layer as MaxPool2D) @@ -358,6 +360,22 @@ private fun createKerasConcatenate(layer: Concatenate): KerasLayer { return KerasLayer(class_name = LAYER_CONCATENATE, config = configX) } +private fun createKerasConv1D(layer: Conv1D, isKerasFullyCompatible: Boolean): KerasLayer { + val configX = LayerConfig( + filters = layer.filters.toInt(), + kernel_size = listOf(layer.kernelSize.toInt()), + strides = listOf(layer.strides[1].toInt()), + dilation_rate = listOf(layer.dilations[1].toInt()), + activation = convertToKerasActivation(layer.activation), + kernel_initializer = convertToKerasInitializer(layer.kernelInitializer, isKerasFullyCompatible), + bias_initializer = convertToKerasInitializer(layer.biasInitializer, isKerasFullyCompatible), + padding = convertPadding(layer.padding), + name = layer.name, + use_bias = layer.useBias + ) + return KerasLayer(class_name = LAYER_CONV1D, config = configX) +} + private fun createKerasConv2D(layer: Conv2D, isKerasFullyCompatible: Boolean): KerasLayer { val kernelSize = layer.kernelSize.map { it.toInt() }.toList() val configX = LayerConfig( diff --git a/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv1DTest.kt b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv1DTest.kt new file mode 100644 index 000000000..2f1ba7bf5 --- /dev/null +++ b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv1DTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2020 JetBrains s.r.o. and Kotlin Deep Learning project contributors. All Rights Reserved. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.kotlinx.dl.api.core.layer + +import org.jetbrains.kotlinx.dl.api.core.initializer.Constant +import org.jetbrains.kotlinx.dl.api.core.initializer.Zeros +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv1D +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.ConvPadding +import org.junit.jupiter.api.Test + +internal class Conv1DTest : ConvLayerTest() { + + @Test + fun zeroedInputTensorWithDefaultValues() { + val input = createFloatConv1DTensor(batchSize = 1, size = 3, channels = 1, initValue = 0.0f) + val expected = createFloatConv1DTensor(batchSize = 1, size = 3, channels = 32, initValue = 0.0f) + + assertFloatConv1DTensorsEquals( + Conv1D( + name = "TestConv1D_1", + biasInitializer = Zeros() + ), + input, + expected + ) + } + + @Test + fun constantInputTensorWithValidPadding() { + val input = createFloatConv1DTensor(batchSize = 1, size = 3,channels = 1, initValue = 1.0f) + val expected = createFloatConv1DTensor(batchSize = 1, size = 2, channels = 16, initValue = 2.0f) + + assertFloatConv1DTensorsEquals( + Conv1D( + name = "TestConv1D_2", + filters = 16, + kernelInitializer = Constant(1.0f), + biasInitializer = Zeros(), + kernelSize = 2, + padding = ConvPadding.VALID + ), + input, + expected + ) + } +} diff --git a/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv2DTest.kt b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv2DTest.kt index 7b7c94a97..2d16a8f2e 100644 --- a/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv2DTest.kt +++ b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/Conv2DTest.kt @@ -5,54 +5,45 @@ package org.jetbrains.kotlinx.dl.api.core.layer +import org.jetbrains.kotlinx.dl.api.core.initializer.Constant +import org.jetbrains.kotlinx.dl.api.core.initializer.Zeros +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv2D +import org.jetbrains.kotlinx.dl.api.core.layer.convolutional.ConvPadding import org.junit.jupiter.api.Test internal class Conv2DTest : ConvLayerTest() { - @Test - fun conv2d() { - /*val input = Array(1) { - Array(2) { - Array(2) { - FloatArray(1) - } - } - } - - for (i in 0..1) - for (j in 0..1) - input[0][i][j][0] = 1.0f - - - val expected = Array(1) { - Array(2) { - Array(2) { - FloatArray(1) - } - } - } - - for (i in 0..1) - for (j in 0..1) - expected[0][i][j][0] = 1.0f - - val actual = Array(1) { - Array(2) { - Array(2) { - FloatArray(1) - } - } - } - - for (i in 0..1) - for (j in 0..1) - actual[0][i][j][0] = 1.0f + @Test + fun zeroedInputTensorWithDefaultValues() { + val input = createFloatConv2DTensor(batchSize = 1, height = 3, width = 3, channels = 1, initValue = 0.0f) + val expected = createFloatConv2DTensor(batchSize = 1, height = 3, width = 3, channels = 32, initValue = 0.0f) + + assertFloatConv2DTensorsEquals( + Conv2D( + name = "TestConv2D_1", + biasInitializer = Zeros() + ), + input, + expected + ) + } - assertActivationFunction( - Conv2D(name = "TestConv2D_1", filters = 1, kernelInitializer = HeNormal(12L), biasInitializer = Zeros()), + @Test + fun constantInputTensorWithValidPadding() { + val input = createFloatConv2DTensor(batchSize = 1, height = 3, width = 3, channels = 1, initValue = 1.0f) + val expected = createFloatConv2DTensor(batchSize = 1, height = 2, width = 2, channels = 16, initValue = 4.0f) + + assertFloatConv2DTensorsEquals( + Conv2D( + name = "TestConv2D_2", + filters = 16, + kernelInitializer = Constant(1.0f), + biasInitializer = Zeros(), + kernelSize = longArrayOf(2, 2), + padding = ConvPadding.VALID + ), input, - actual, expected - )*/ + ) } } diff --git a/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/ConvLayerTest.kt b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/ConvLayerTest.kt index c7eada8fd..338e4c38f 100644 --- a/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/ConvLayerTest.kt +++ b/api/src/test/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/ConvLayerTest.kt @@ -7,96 +7,124 @@ package org.jetbrains.kotlinx.dl.api.core.layer import org.jetbrains.kotlinx.dl.api.core.KGraph import org.jetbrains.kotlinx.dl.api.core.activation.EPS -import org.jetbrains.kotlinx.dl.api.core.shape.shapeFromDims -import org.junit.jupiter.api.Assertions +import org.jetbrains.kotlinx.dl.api.core.shape.* +import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals -import org.tensorflow.Graph -import org.tensorflow.Session -import org.tensorflow.Shape +import org.tensorflow.* import org.tensorflow.op.Ops +import org.tensorflow.op.core.Constant -// TODO: tests should start from variable initialization in Graph mode only. Looks like eager mode does not support variables -// We could tests without variables only -open class ConvLayerTest { - protected fun assertActivationFunction( - layer: Layer, - input: Array>>, - actual: Array>>, - expected: Array>> - ) { - val inputSize = input.size - val inputShape = Shape.make(inputSize.toLong()) - - Graph().use { g -> - Session(g).use { session -> - val tf = Ops.create(g) - val kGraph = KGraph(g.toGraphDef()) - val inputOp = tf.constant(input) +internal typealias FloatConv1DTensor = Array> +internal typealias FloatConv2DTensor = Array>> - layer.build(tf, kGraph, inputShape) +internal typealias AnyDTensor = Array<*> - val isTraining = tf.constant(true) - val numberOfLosses = tf.constant(1.0f) - val output = layer.forward(tf, inputOp, isTraining, numberOfLosses).asOutput().tensor() - - val expectedShape = Shape.make( - inputSize.toLong() - ) - - val actualShape = shapeFromDims(*output.shape()) - assertEquals(expectedShape, actualShape) +open class ConvLayerTest { - output.copyTo(actual) + protected fun assertFloatConv1DTensorsEquals( + layer: Layer, + input: FloatConv1DTensor, + expected: FloatConv1DTensor + ) { + val actual = expected.copyZeroed() + assertTensorsEquals(layer, input, expected, actual, + ::assertFloatConv1DTensorsEquals) { tf, tensor -> tf.constant(tensor.cast3DArray()) } + } - for (i in expected.indices) { - for (j in expected[i].indices) { - for (k in expected[i][j].indices) { - Assertions.assertArrayEquals( - expected[i][j][k], - actual[i][j][k], - EPS - ) - } + protected fun assertFloatConv2DTensorsEquals( + layer: Layer, + input: FloatConv2DTensor, + expected: FloatConv2DTensor + ) { + val actual = expected.copyZeroed() + assertTensorsEquals(layer, input, expected, actual, + ::assertFloatConv2DTensorsEquals) { tf, tensor -> tf.constant(tensor.cast4DArray()) } + } - } + protected fun createFloatConv1DTensor( + batchSize: Long, + size: Long, + channels: Long, + initValue: Float + ): FloatConv1DTensor = + getFloatArrayOfShape(Shape.make(batchSize, size, channels), initValue).cast3DArray() + + protected fun createFloatConv2DTensor( + batchSize: Long, + height: Long, + width: Long, + channels: Long, + initValue: Float + ): FloatConv2DTensor = + getFloatArrayOfShape(Shape.make(batchSize, height, width, channels), initValue).cast4DArray() + + private fun FloatConv1DTensor.copyZeroed(): FloatConv1DTensor = + getFloatArrayOfShape(getShapeOfArray(this)).cast3DArray() + + private fun FloatConv2DTensor.copyZeroed(): FloatConv2DTensor = + getFloatArrayOfShape(getShapeOfArray(this)).cast4DArray() + + private fun assertTensorsEquals( + layer: Layer, + input: AnyDTensor, + expected: AnyDTensor, + actual: AnyDTensor, + assertEquals: (AnyDTensor, AnyDTensor) -> Unit, + constProducer: (Ops, AnyDTensor) -> Constant + ) { + Graph().use { graph -> + Session(graph).use { session -> + val tf = Ops.create(graph) + KGraph(graph.toGraphDef()).use { kGraph -> + + val inputOp = constProducer(tf, input) + val isTraining = tf.constant(true) + val numberOfLosses = tf.constant(1.0f) + + layer.build(tf, kGraph, getShapeOfArray(input)) + val output = layer.forward(tf, inputOp, isTraining, numberOfLosses).asOutput() + kGraph.initializeGraphVariables(session) + val outputTensor = session.runner().fetch(output).run().first() + val outputTensorShape = shapeFromDims(*outputTensor.shape()) + outputTensor.copyTo(actual) + + assertEquals(getShapeOfArray(expected), outputTensorShape) + assertEquals(expected, actual) } - } } + } + private fun assertFloatConv1DTensorsEquals( + expected: AnyDTensor, + actual: AnyDTensor + ) { + val expectedTensor = expected.cast3DArray() + val actualTensor = actual.cast3DArray() + val msg = "Expected ${expectedTensor.contentDeepToString()} " + + "to equal ${actualTensor.contentDeepToString()}" + for (i in expectedTensor.indices) { + for (j in expectedTensor[i].indices) { + assertArrayEquals(expectedTensor[i][j], actualTensor[i][j], EPS, msg) + } + } + } - - /* EagerSession.create().use { - val tf = Ops.create(it) - val inputOp = tf.constant(input) - layer.build(tf, KGraph(Graph().toGraphDef()), inputShape) - val isTraining = tf.constant(true) - val numberOfLosses = tf.constant(1.0f) - val output = layer.forward(tf, inputOp, isTraining, numberOfLosses).asOutput().tensor() - - val expectedShape = Shape.make( - inputSize.toLong() - ) - - val actualShape = shapeFromDims(*output.shape()) - assertEquals(expectedShape, actualShape) - - output.copyTo(actual) - - for (i in expected.indices) { - for (j in expected[i].indices) { - for (k in expected[i][j].indices) { - Assertions.assertArrayEquals( - expected[i][j][k], - actual[i][j][k], - EPS - ) - } - + private fun assertFloatConv2DTensorsEquals( + expected: AnyDTensor, + actual: AnyDTensor + ) { + val expectedTensor = expected.cast4DArray() + val actualTensor = actual.cast4DArray() + val msg = "Expected ${expectedTensor.contentDeepToString()} " + + "to equal ${actualTensor.contentDeepToString()}" + for (i in expectedTensor.indices) { + for (j in expectedTensor[i].indices) { + for (k in expectedTensor[i][j].indices) { + assertArrayEquals(expectedTensor[i][j][k], actualTensor[i][j][k], EPS, msg) } } - }*/ + } } } -