Skip to content
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

Add transpose convolution layers #315

Merged
merged 8 commits into from
Feb 18, 2022

Conversation

juliabeliaeva
Copy link
Contributor

This PR adds implementations for Conv1DTranspose, Conv2DTranspose and Conv3DTranspose layers.

Caveats:

  1. Dilation values greater than 1 are not supported in tensorflow on cpu: CPU support for dilation rates larger than 1 tensorflow/tensorflow#28264. Therefore, I have not added any tests for dilations as they do not work.
  2. Output padding is not supported in org.tensorflow.op.NnOps#conv3dBackpropInput. Conv3DTranspose still has the parameter, it just does not do anything. I'm not sure if this parameter is needed or if it is, where to add a warning about its usage.

Fixes #124

@zaleslaw
Copy link
Collaborator

zaleslaw commented Dec 27, 2021

I suggest to be an honest library for a user and remove unsupported parameters by adding to the KDoc the NOTE about this removal (with a link to the known bug).

But it could be a problem during the model convertation Keras->KotlinDL and KotlinDL->Keras and in the case
Keras->KotlinDL we could just skip values and the case KotlinDL->Keras write some default values from Keras layers contructors

@zaleslaw
Copy link
Collaborator

zaleslaw commented Dec 27, 2021

@juliabeliaeva I made an integration example with auto-encoder based on the following article https://www.machinecurve.com/index.php/2019/12/10/conv2dtranspose-using-2d-transposed-convolutions-with-keras/

The Colab is available here, you could a copy of it and download the JSON config and h5 weights put it to examples/resources, and run the following example

fun loadAutoencoderModelWithWeightsAndEvaluate() {
    val (_, test) = fashionMnist()

    val jsonConfigFile = getAutoencoderJSONConfigFile()
    val model = Sequential.loadModelConfiguration(jsonConfigFile)

    model.use {
        for(layer in it.layers) {
            layer.isTrainable = false
        }
        it.compile(
            optimizer = Adam(),
            loss = Losses.MAE,
            metric = Metrics.ACCURACY
        )

        it.logSummary()

        val hdfFile = getAutoencoderWeightsFile()
        it.loadWeights(hdfFile)

        val result = it.predict(test.x[0])
    }
}

/** */
fun main(): Unit = loadAutoencoderModelWithWeightsAndEvaluate()

/** Returns JSON file with model configuration, saved from Keras 2.x. */
fun getAutoencoderJSONConfigFile(): File {
    val pathToConfig = "models/mnist/autoencoder/modelConfig.json"
    val realPathToConfig = OnHeapDataset::class.java.classLoader.getResource(pathToConfig).path.toString()

    return File(realPathToConfig)
}

/** Returns .h5 file with model weights, saved from Keras 2.x. */
fun getAutoencoderWeightsFile(): HdfFile {
    val pathToWeights = "models/mnist/autoencoder/weights.h5"
    val realPathToWeights = OnHeapDataset::class.java.classLoader.getResource(pathToWeights).path.toString()
    val file = File(realPathToWeights)
    return HdfFile(file)
}

It fails with the following error message

Exception in thread "main" java.lang.IllegalStateException: Attempting to use uninitialized value Exception in thread "main" java.lang.IllegalStateException: Attempting to use uninitialized value conv2d_transpose_6_conv2d_transpose_kernel
	 [[{{node Conv2dBackpropInput}}]]

Looks like something was missed during the loading of the weights or initialization of the variables.

To reproduce the bug, download the following archive (json + h5)
weights.zip

@juliabeliaeva
Copy link
Contributor Author

@zaleslaw thank you, I forgot about loading weights from h5 files completely. I'll implement it.

@juliabeliaeva
Copy link
Contributor Author

But it could be a problem during the model convertation Keras->KotlinDL and KotlinDL->Keras and in the case
Keras->KotlinDL we could just skip values and the case KotlinDL->Keras write some default values from Keras layers contructors

That was my concern: if someone loads a model from keras and saves it back, they will loose their parameters in the process.

@juliabeliaeva
Copy link
Contributor Author

@zaleslaw I just pushed a new version of this PR. Apart from the added loading weights for transposed convolutions, there are some other notable changes:

  1. I moved expansion of kernel, strides and dilations directly to org.jetbrains.kotlinx.dl.api.core.layer.convolutional.Conv1D#convImplementation instead of doing it in constructor. One of the side effects of this change is that kernel variable has a correct shape now and weights could be loaded for it without problems.
  2. The changes in Conv1D allowed to get rid of the duplicated sets of properties ("internal" properties in AbstractConv vs non-internal properties in the implementations). Having two sets of properties was really confusing, it was hard to figure out which to use as they were sometimes the same and sometimes different. This change makes convolutional layer hierarchy much simpler.
  3. I removed outputPadding from Conv3DTranspose as suggested. I kept the dilations since they should on gpu.
  4. It turned out that conv2dBackpropInput/conv3dBackpropInput need to have a specific batch size, so I had to add a hack for it (I would appreciate any suggestions on how to make this simpler). See: https://github.com/JetBrains/KotlinDL/blob/1d5492e5ef51b2d7eb7066e35d8a82934ca390e2/api/src/main/kotlin/org/jetbrains/kotlinx/dl/api/core/layer/convolutional/ConvTranspose.kt#L86
  5. After these changes the "Autoencoder" example still does not work completely: org.jetbrains.kotlinx.dl.api.core.GraphTrainableModel#internalPredict function does not expect to have an image as a prediction. I'm going to send a separate PR with the fix.

@zaleslaw
Copy link
Collaborator

@juliabeliaeva Is this the final version of this PR or is it required any PR should be merged before?

@juliabeliaeva
Copy link
Contributor Author

@juliabeliaeva Is this the final version of this PR or is it required any PR should be merged before?

It's the final version, can be merged right now.

Copy link
Collaborator

@zaleslaw zaleslaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please have a look at the left comments and add the general example related to the autoencoder based on the Colab shared earlier in the issue comments.
No big changes, just a few minor things for future developers.

At this moment #328 is merged to the master, you could merge in this PR and finish the Autoencoder example

@@ -57,42 +56,27 @@ private const val EXTRA_DIM = 1L
* @since 0.3
*/
public class Conv1D(
public val filters: Int = 32,
public val kernelSize: Int = 3,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like kernel size is a stable term in Deep Learning for CNN

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use kernelSize here since kernelSize is an array and we want to allow passing a single integer here.

}

override fun toString(): String =
"Conv1D(filters=$filters, kernelSize=$kernelSize, strides=${strides.contentToString()}, " +
"dilation=${dilations.contentToString()}, activation=$activation, kernelInitializer=$kernelInitializer, " +
"biasInitializer=$biasInitializer, kernelShape=${kernel.shape}, biasShape=${bias?.shape}, padding=$padding, " +
"biasRegularizer=$biasRegularizer, kernelRegularizer=$kernelRegularizer, activityRegularizer=$activityRegularizer)"

internal companion object {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these functions be companion functions or could be just private functions in the Conv1D?

Copy link
Contributor Author

@juliabeliaeva juliabeliaeva Feb 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are used in both Conv1D and Conv1DTranspose since 1D convolutions are implemented with 2D convolutions. They can't be private, but they are only needed for convolutions implementation, so they are internal.

return kernel.withAdded(EXTRA_DIM - 1, 1)
}

internal fun Ops.expandKernel(kernel: Operand<Float>): Operand<Float> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it Conv1D specific functions, could they be a part of our local helper framework over the TensorFlow Java API

Copy link
Contributor Author

@juliabeliaeva juliabeliaeva Feb 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are specific for Conv1D/Conv1DTranspose, I don't think they are needed outside of these operations.

*/
public class Conv1DTranspose(
public override val filters: Int = 3,
public val kernelLength: Int = 3,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kernel Size is better

override fun convImplementation(tf: Ops, input: Operand<Float>): Operand<Float> {
return tf.withExpandedDimensions(input) { expandedInput ->
val expandedOutputPadding = outputPadding?.withAdded(EXTRA_DIM * 2, listOf(0, 0))
return@withExpandedDimensions tf.nn.conv2dBackpropInput(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very complex function call, could we extract some logic to the separate variables and leave some comments for the future (why, for example, we did tf.shapeWithDynamicBatchSize(outputShape.expand(), input) for the specific parameter and so on) At this moment this knowledge is hidden in the issue comments.
I suggest doing this, because it's enough far from the initial Keras code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why, for example, we did tf.shapeWithDynamicBatchSize(outputShape.expand(), input)

shapeWithDynamicBatchSize has a javadoc with explanation and links to the relevant issues, I'll add some comments to this method as well.

)
}

internal companion object {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methods of this companion are used in the both, Conv1DTranspose and Conv2DTranspose, probably need we need a separate place with top-level functions a la Util class.

* C - number of channels
* ```
*
* Note: providing explicit output padding is currently not supported.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we throw an exception on that or add a contract to init section?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no outputPadding parameter, so there is no situation where we could throw an exception. The note is here since keras does have this parameter (see https://keras.io/api/layers/convolution_layers/convolution3d_transpose/).

}

internal companion object {
/**
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move here other companion functions mentioned earlier?

@@ -41,7 +41,7 @@ internal fun soundBlock(filters: Int, kernelSize: Int, poolStride: Int): Array<L
arrayOf(
Conv1D(
filters = filters,
kernelSize = kernelSize,
kernelLength = kernelSize,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kernelSize!

@juliabeliaeva
Copy link
Contributor Author

juliabeliaeva commented Feb 17, 2022

Pushed a new version of this PR.

  1. Rebased on master, resolved conflicts.
  2. Rewritten Conv1DTranspose implementation to be more clear.
  3. Moved helper methods.
  4. Discovered that my implementation computes the output size a bit differently than keras, so I rewritten it to match (see the last commit in the branch).

I think it might be more convenient to implement the example as another PR.

@zaleslaw zaleslaw added LGTM PR reviewed and is ready to merge and removed Review This PR is under review labels Feb 18, 2022
@zaleslaw zaleslaw merged commit 016450c into Kotlin:master Feb 18, 2022
@juliabeliaeva juliabeliaeva deleted the conv-transpose branch April 15, 2022 23:53
@juliabeliaeva juliabeliaeva mentioned this pull request Jan 10, 2023
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LGTM PR reviewed and is ready to merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Conv1DTranspose, Conv2DTranspose, Conv3DTranspose layers
2 participants