diff --git a/README.md b/README.md index faef39ca..5e3184de 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Would you like to build/train a model using Keras/Python? And would you like to * `AveragePooling1D/2D/3D`, `GlobalAveragePooling1D/2D/3D` * `TimeDistributed` * `Conv1D/2D/3D`, `SeparableConv2D`, `DepthwiseConv2D` -* `Conv1DTranspose`, `Conv2DTranspose` +* `Conv1DTranspose`, `Conv2DTranspose`, `Conv3DTranspose` * `Cropping1D/2D/3D`, `ZeroPadding1D/2D/3D`, `CenterCrop` * `BatchNormalization`, `Dense`, `Flatten`, `Normalization` * `Dropout`, `AlphaDropout`, `GaussianDropout`, `GaussianNoise` @@ -52,7 +52,7 @@ Would you like to build/train a model using Keras/Python? And would you like to * `RandomContrast`, `RandomFlip`, `RandomHeight` * `RandomRotation`, `RandomTranslation`, `RandomWidth`, `RandomZoom` * `MaxPooling1D/2D/3D`, `GlobalMaxPooling1D/2D/3D` -* `UpSampling1D/2D`, `Resizing`, `Rescaling` +* `UpSampling1D/2D/3D`, `Resizing`, `Rescaling` * `Reshape`, `Permute`, `RepeatVector` * `Embedding`, `CategoryEncoding` * `Attention`, `AdditiveAttention`, `MultiHeadAttention` @@ -79,7 +79,7 @@ Would you like to build/train a model using Keras/Python? And would you like to `RepeatVector`, `RNN`, `SimpleRNN`, `SimpleRNNCell`, `StackedRNNCells`, `StringLookup`, `TextVectorization`, `Bidirectional`, `GRU`, `LSTM`, `CuDNNGRU`, `CuDNNLSTM`, -`ThresholdedReLU`, `Upsampling3D`, `temporal` models +`ThresholdedReLU`, `temporal` models Usage ----- diff --git a/include/fdeep/convolution3d.hpp b/include/fdeep/convolution3d.hpp index 6f546908..f16c63cc 100644 --- a/include/fdeep/convolution3d.hpp +++ b/include/fdeep/convolution3d.hpp @@ -38,7 +38,8 @@ namespace internal { padding pad_type, std::size_t input_shape_size_d4, std::size_t input_shape_height, - std::size_t input_shape_width) + std::size_t input_shape_width, + bool transposed = false) { const int filter_size_d4 = static_cast(filter_shape.size_dim_4_); const int filter_height = static_cast(filter_shape.height_); @@ -59,9 +60,15 @@ namespace internal { out_height = fplus::ceil(static_cast(in_height) / static_cast(strides_y) - 0.001); out_width = fplus::ceil(static_cast(in_width) / static_cast(strides_x) - 0.001); } else { - out_size_d4 = fplus::ceil(static_cast(in_size_d4 - filter_size_d4 + 1) / static_cast(strides_d4) - 0.001); - out_height = fplus::ceil(static_cast(in_height - filter_height + 1) / static_cast(strides_y) - 0.001); - out_width = fplus::ceil(static_cast(in_width - filter_width + 1) / static_cast(strides_x) - 0.001); + if (transposed) { + out_size_d4 = fplus::ceil(static_cast(in_size_d4 + filter_size_d4 - 1) / static_cast(strides_d4) - 0.001); + out_height = fplus::ceil(static_cast(in_height + filter_height - 1) / static_cast(strides_y) - 0.001); + out_width = fplus::ceil(static_cast(in_width + filter_width - 1) / static_cast(strides_x) - 0.001); + } else { + out_size_d4 = fplus::ceil(static_cast(in_size_d4 - filter_size_d4 + 1) / static_cast(strides_d4) - 0.001); + out_height = fplus::ceil(static_cast(in_height - filter_height + 1) / static_cast(strides_y) - 0.001); + out_width = fplus::ceil(static_cast(in_width - filter_width + 1) / static_cast(strides_x) - 0.001); + } } int pad_front = 0; @@ -71,6 +78,15 @@ namespace internal { int pad_left = 0; int pad_right = 0; + if (transposed) { + pad_front = filter_size_d4 - 1; + pad_back = filter_size_d4 - 1; + pad_top = filter_height - 1; + pad_bottom = filter_height - 1; + pad_left = filter_width - 1; + pad_right = filter_width - 1; + } + if (pad_type == padding::same) { int pad_along_d4 = 0; int pad_along_height = 0; @@ -125,7 +141,8 @@ namespace internal { tensor filter_mats_; }; - inline tensor dilate_tensor_3d(const shape3& dilation_rate, const tensor& in) + inline tensor dilate_tensor_3d(const shape3& dilation_rate, const tensor& in, + bool trailing_zeros = false) { if (dilation_rate == shape3(1, 1, 1)) { return in; @@ -133,20 +150,26 @@ namespace internal { assertion(in.shape().rank() == 4, "Invalid rank for 3d dilation"); const auto in_shape = in.shape(); + const std::size_t expansion_d4 = trailing_zeros ? (dilation_rate.size_dim_4_ - 1) : 0; + const std::size_t expansion_y = trailing_zeros ? (dilation_rate.height_ - 1) : 0; + const std::size_t expansion_x = trailing_zeros ? (dilation_rate.width_ - 1) : 0; const tensor_shape dilated_shape( - (in_shape.size_dim_4_ - 1) * dilation_rate.size_dim_4_ + 1, - (in_shape.height_ - 1) * dilation_rate.height_ + 1, - (in_shape.width_ - 1) * dilation_rate.width_ + 1, + (in_shape.size_dim_4_ - 1) * dilation_rate.size_dim_4_ + 1 + expansion_d4, + (in_shape.height_ - 1) * dilation_rate.height_ + 1 + expansion_y, + (in_shape.width_ - 1) * dilation_rate.width_ + 1 + expansion_x, in_shape.depth_); + const std::size_t offset_d4 = expansion_d4 - expansion_d4 / 2; + const std::size_t offset_y = expansion_y - expansion_y / 2; + const std::size_t offset_x = expansion_x - expansion_x / 2; tensor result(dilated_shape, static_cast(0)); for (std::size_t d4 = 0; d4 < in_shape.size_dim_4_; ++d4) { for (std::size_t y = 0; y < in_shape.height_; ++y) { for (std::size_t x = 0; x < in_shape.width_; ++x) { for (std::size_t z = 0; z < in_shape.depth_; ++z) { result.set_ignore_rank(tensor_pos( - d4 * dilation_rate.size_dim_4_, - y * dilation_rate.height_, - x * dilation_rate.width_, + d4 * dilation_rate.size_dim_4_ + offset_d4, + y * dilation_rate.height_ + offset_y, + x * dilation_rate.width_ + offset_x, z), in.get_ignore_rank(tensor_pos(d4, y, x, z))); } @@ -156,6 +179,16 @@ namespace internal { return result; } + inline tensor reverse_size_dim_4_dimension(const tensor& in) + { + tensor out = tensor(in.shape(), static_cast(0)); + loop_over_all_dims(in.shape(), [&in, &out](std::size_t dim5, std::size_t dim4, std::size_t y, std::size_t x, std::size_t z) { + out.set_ignore_rank(tensor_pos(dim5, in.shape().size_dim_4_ - dim4 - 1, y, x, z), + in.get_ignore_rank(tensor_pos(dim5, dim4, y, x, z))); + }); + return out; + } + inline filter dilate_filter_3d(const shape3& dilation_rate, const filter& undilated) { return filter(dilate_tensor_3d(dilation_rate, undilated.get_tensor()), @@ -165,7 +198,8 @@ namespace internal { inline filter_vec generate_filters_3d( const shape3& dilation_rate, const tensor_shape& filter_shape, std::size_t k, - const float_vec& weights, const float_vec& bias) + const float_vec& weights, const float_vec& bias, + bool transpose = false) { filter_vec filters(k, filter(tensor(filter_shape, 0), 0)); @@ -186,6 +220,11 @@ namespace internal { for (auto& filt : filters) { filt.set_params(*it_filter_val, *it_filter_bias); filt = dilate_filter_3d(dilation_rate, filt); + if (transpose) { + filt = filter(reverse_size_dim_4_dimension(filt.get_tensor()), filt.get_bias()); + filt = filter(reverse_height_dimension(filt.get_tensor()), filt.get_bias()); + filt = filter(reverse_width_dimension(filt.get_tensor()), filt.get_bias()); + } ++it_filter_val; ++it_filter_bias; } @@ -276,6 +315,73 @@ namespace internal { Eigen::OuterStride<>(static_cast(f_depth * strides_x))); } + // Special version for convolution with strides 1×1×1. + // Uses fewer but larger GEMMs by collapsing the spatial output loops into one big input mapping. + inline tensor convolve_accumulative_s1x1x1_3d( + std::size_t out_size_d4, + std::size_t out_height, + std::size_t out_width, + const convolution3d_filter_matrices& filter_mat, + const tensor& in) + { + const tensor& filter_mats = filter_mat.filter_mats_; + const auto f_size_d4 = filter_mat.filter_shape_.size_dim_4_; + const auto f_height = filter_mat.filter_shape_.height_; + const auto f_width = filter_mat.filter_shape_.width_; + const auto f_depth = filter_mat.filter_shape_.depth_; + const auto out_depth = filter_mat.filter_count_; + + assertion(f_depth == in.shape().depth_, "filter depth does not match input"); + assertion(out_height == (in.shape().height_ - f_height) + 1, "output height does not match"); + assertion(out_width == (in.shape().width_ - f_width) + 1, "output width does not match"); + assertion(out_size_d4 == (in.shape().size_dim_4_ - f_size_d4) + 1, "output d4 size does not match"); + assertion(out_depth == filter_mat.biases_.size(), "invalid bias count"); + + tensor output = init_conv_output_tensor_3d(out_size_d4, out_height, out_width, out_depth, in.shape().rank(), filter_mat); + + const std::size_t out_height_temp = out_height + f_height - 1; + const std::size_t out_width_temp = out_width + f_width - 1; + tensor output_temp(tensor_shape_with_changed_rank( + tensor_shape(out_size_d4, out_height_temp, out_width_temp, out_depth), + in.shape().rank()), + static_cast(0)); + + const auto mapping_width = (out_size_d4 - 1) * out_height_temp * out_width_temp + + (out_height - 1) * out_width_temp + + out_width; + + for (std::size_t d4_filt = 0; d4_filt < f_size_d4; ++d4_filt) { + for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) { + const Eigen::Map + filter(const_cast(&filter_mats.get_ref_ignore_rank(tensor_pos(d4_filt, y_filt, 0, 0, 0))), + static_cast(out_depth), + static_cast(f_width * f_depth)); + + const auto input = get_im2col_mapping_3d(in, f_width, f_depth, 1, mapping_width, 0, 0, d4_filt, y_filt); + + Eigen::Map + output_temp_map(&output_temp.get_ref_ignore_rank(tensor_pos(0, 0, 0, 0, 0)), + static_cast(out_depth), + static_cast(mapping_width)); + + output_temp_map.noalias() += filter * input; + } + } + + for (std::size_t d4_out = 0; d4_out < out_size_d4; ++d4_out) { + for (std::size_t y_out = 0; y_out < out_height; ++y_out) { + for (std::size_t x_out = 0; x_out < out_width; ++x_out) { + for (std::size_t z_out = 0; z_out < out_depth; ++z_out) { + output.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, x_out, z_out)) + += output_temp.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, x_out, z_out)); + } + } + } + } + + return output; + } + inline tensor convolve_accumulative_3d( std::size_t out_size_d4, std::size_t out_height, @@ -299,6 +405,10 @@ namespace internal { assertion(out_width == (in.shape().width_ - f_width) / strides_x + 1, "output width does not match"); assertion(out_depth == filter_mat.biases_.size(), "invalid bias count"); + if (strides_d4 == 1 && strides_y == 1 && strides_x == 1) { + return convolve_accumulative_s1x1x1_3d(out_size_d4, out_height, out_width, filter_mat, in); + } + tensor output = init_conv_output_tensor_3d(out_size_d4, out_height, out_width, out_depth, in.shape().rank(), filter_mat); for (std::size_t d4_filt = 0; d4_filt < f_size_d4; ++d4_filt) { @@ -358,5 +468,42 @@ namespace internal { in_padded); } + inline tensor convolve_transposed_3d( + const shape3& strides, + const padding& pad_type, + const convolution3d_filter_matrices& filter_mat, + const tensor& input) + { + assertion(filter_mat.filter_shape_.depth_ == input.shape().depth_, + "invalid filter depth"); + + const auto input_dilated = dilate_tensor_3d(strides, input, pad_type == padding::same); + + const shape3 filter_spatial_shape( + filter_mat.filter_shape_.size_dim_4_, + filter_mat.filter_shape_.height_, + filter_mat.filter_shape_.width_); + + const auto conv_cfg = preprocess_convolution_3d( + filter_spatial_shape, + shape3(1, 1, 1), pad_type, + input_dilated.shape().size_dim_4_, + input_dilated.shape().height_, + input_dilated.shape().width_, + true); + + const auto in_padded = pad_tensor(0, + conv_cfg.pad_front_, conv_cfg.pad_back_, + conv_cfg.pad_top_, conv_cfg.pad_bottom_, + conv_cfg.pad_left_, conv_cfg.pad_right_, + input_dilated); + + return convolve_accumulative_3d( + conv_cfg.out_size_d4_, conv_cfg.out_height_, conv_cfg.out_width_, + 1, 1, 1, + filter_mat, + in_padded); + } + } } diff --git a/include/fdeep/import_model.hpp b/include/fdeep/import_model.hpp index 0b46e393..b8ed3d8a 100644 --- a/include/fdeep/import_model.hpp +++ b/include/fdeep/import_model.hpp @@ -40,6 +40,7 @@ #include "fdeep/layers/conv_2d_layer.hpp" #include "fdeep/layers/conv_2d_transpose_layer.hpp" #include "fdeep/layers/conv_3d_layer.hpp" +#include "fdeep/layers/conv_3d_transpose_layer.hpp" #include "fdeep/layers/cropping_3d_layer.hpp" #include "fdeep/layers/dense_layer.hpp" #include "fdeep/layers/depthwise_conv_2d_layer.hpp" @@ -94,6 +95,7 @@ #include "fdeep/layers/unit_normalization_layer.hpp" #include "fdeep/layers/upsampling_1d_layer.hpp" #include "fdeep/layers/upsampling_2d_layer.hpp" +#include "fdeep/layers/upsampling_3d_layer.hpp" #include "fdeep/layers/zero_padding_3d_layer.hpp" #include "fdeep/tensor.hpp" #include "fdeep/tensor_shape.hpp" @@ -447,6 +449,36 @@ namespace internal { dilation_rate, weights, bias); } + inline layer_ptr create_conv_3d_transpose_layer(const get_param_f& get_param, + const nlohmann::json& data, + const std::string& name) + { + const std::string padding_str = data["config"]["padding"]; + const auto pad_type = create_padding(padding_str); + + const shape3 strides = create_shape3(data["config"]["strides"]); + const shape3 dilation_rate = create_shape3(data["config"]["dilation_rate"]); + + const auto filter_count = create_size_t(data["config"]["filters"]); + float_vec bias(filter_count, 0); + const bool use_bias = data["config"]["use_bias"]; + if (use_bias) + bias = decode_floats(get_param(name, "bias")); + assertion(bias.size() == filter_count, "size of bias does not match"); + + const float_vec weights = decode_floats(get_param(name, "weights")); + const shape3 kernel_size = create_shape3(data["config"]["kernel_size"]); + assertion(weights.size() % kernel_size.volume() == 0, + "invalid number of weights"); + const std::size_t filter_depths = weights.size() / (kernel_size.volume() * filter_count); + const tensor_shape filter_shape( + kernel_size.size_dim_4_, kernel_size.height_, kernel_size.width_, filter_depths); + + return std::make_shared(name, + filter_shape, filter_count, strides, pad_type, + dilation_rate, weights, bias); + } + inline layer_ptr create_conv_2d_transpose_layer(const get_param_f& get_param, const nlohmann::json& data, const std::string& name) @@ -660,6 +692,15 @@ namespace internal { name, scale_factor, interpolation); } + inline layer_ptr create_upsampling_3d_layer( + const get_param_f&, const nlohmann::json& data, + const std::string& name) + { + const auto scale_factor = create_shape3(data["config"]["size"]); + return std::make_shared( + name, scale_factor); + } + inline layer_ptr create_dense_layer(const get_param_f& get_param, const nlohmann::json& data, const std::string& name) { @@ -1301,6 +1342,7 @@ namespace internal { { "Conv3D", create_conv_3d_layer }, { "Conv1DTranspose", create_conv_2d_transpose_layer }, { "Conv2DTranspose", create_conv_2d_transpose_layer }, + { "Conv3DTranspose", create_conv_3d_transpose_layer }, { "SeparableConv1D", create_separable_conv_2D_layer }, { "SeparableConv2D", create_separable_conv_2D_layer }, { "DepthwiseConv2D", create_depthwise_conv_2D_layer }, @@ -1365,6 +1407,7 @@ namespace internal { { "GlobalAveragePooling3D", create_global_average_pooling_3d_layer }, { "UpSampling1D", create_upsampling_1d_layer }, { "UpSampling2D", create_upsampling_2d_layer }, + { "UpSampling3D", create_upsampling_3d_layer }, { "Dense", create_dense_layer }, { "Add", create_add_layer }, { "Maximum", create_maximum_layer }, diff --git a/include/fdeep/layers/conv_3d_transpose_layer.hpp b/include/fdeep/layers/conv_3d_transpose_layer.hpp new file mode 100644 index 00000000..65ebb28f --- /dev/null +++ b/include/fdeep/layers/conv_3d_transpose_layer.hpp @@ -0,0 +1,56 @@ +// Copyright 2016, Tobias Hermann. +// https://github.com/Dobiasd/frugally-deep +// Distributed under the MIT License. +// (See accompanying LICENSE file or at +// https://opensource.org/licenses/MIT) + +#pragma once + +#include "fdeep/convolution3d.hpp" +#include "fdeep/filter.hpp" +#include "fdeep/layers/layer.hpp" +#include "fdeep/shape3.hpp" +#include "fdeep/tensor_shape.hpp" + +#include + +#include +#include +#include + +namespace fdeep { +namespace internal { + + class conv_3d_transpose_layer : public layer { + public: + explicit conv_3d_transpose_layer( + const std::string& name, const tensor_shape& filter_shape, + std::size_t k, const shape3& strides, padding p, + const shape3& dilation_rate, + const float_vec& weights, const float_vec& bias) + : layer(name) + , filters_(generate_im2col_filter_matrix_3d( + generate_filters_3d(dilation_rate, filter_shape, k, weights, bias, true))) + , dilation_rate_(dilation_rate) + , strides_(strides) + , padding_(p) + { + assertion(k > 0, "needs at least one filter"); + assertion(filter_shape.volume() > 0, "filter must have volume"); + assertion(strides.volume() > 0, "invalid strides"); + } + + protected: + tensors apply_impl(const tensors& inputs) const override + { + const auto& input = single_tensor_from_tensors(inputs); + return { convolve_transposed_3d(strides_, padding_, filters_, input) }; + } + convolution3d_filter_matrices filters_; + shape3 dilation_rate_; + shape3 strides_; + padding padding_; + }; + +} +} diff --git a/include/fdeep/layers/upsampling_3d_layer.hpp b/include/fdeep/layers/upsampling_3d_layer.hpp new file mode 100644 index 00000000..1235e72a --- /dev/null +++ b/include/fdeep/layers/upsampling_3d_layer.hpp @@ -0,0 +1,45 @@ +// Copyright 2016, Tobias Hermann. +// https://github.com/Dobiasd/frugally-deep +// Distributed under the MIT License. +// (See accompanying LICENSE file or at +// https://opensource.org/licenses/MIT) + +#pragma once + +#include "fdeep/layers/layer.hpp" + +#include + +#include +#include +#include +#include + +namespace fdeep { +namespace internal { + + class upsampling_3d_layer : public layer { + public: + explicit upsampling_3d_layer(const std::string& name, + const shape3& scale_factor) + : layer(name) + , scale_factor_(scale_factor) + { + } + + protected: + tensors apply_impl(const tensors& inputs) const override final + { + const auto& input = single_tensor_from_tensors(inputs); + return { resize3d_nearest( + input, + shape3( + scale_factor_.size_dim_4_ * input.shape().size_dim_4_, + scale_factor_.height_ * input.shape().height_, + scale_factor_.width_ * input.shape().width_)) }; + } + shape3 scale_factor_; + }; + +} +} diff --git a/include/fdeep/tensor.hpp b/include/fdeep/tensor.hpp index 5c736dc7..9a31d35c 100644 --- a/include/fdeep/tensor.hpp +++ b/include/fdeep/tensor.hpp @@ -956,6 +956,27 @@ namespace internal { return out_vol; } + inline tensor resize3d_nearest(const tensor& in_vol, const shape3& target_size) + { + tensor out_vol(tensor_shape(target_size.size_dim_4_, target_size.height_, target_size.width_, in_vol.shape().depth_), 0); + const float_type scale_d4 = static_cast(target_size.size_dim_4_) / static_cast(in_vol.shape().size_dim_4_); + const float_type scale_y = static_cast(target_size.height_) / static_cast(in_vol.shape().height_); + const float_type scale_x = static_cast(target_size.width_) / static_cast(in_vol.shape().width_); + for (std::size_t d4 = 0; d4 < out_vol.shape().size_dim_4_; ++d4) { + const std::size_t d4_in = fplus::round((static_cast(d4) + 0.5f) / scale_d4 - 0.5f); + for (std::size_t y = 0; y < out_vol.shape().height_; ++y) { + const std::size_t y_in = fplus::round((static_cast(y) + 0.5f) / scale_y - 0.5f); + for (std::size_t x = 0; x < out_vol.shape().width_; ++x) { + const std::size_t x_in = fplus::round((static_cast(x) + 0.5f) / scale_x - 0.5f); + for (std::size_t z = 0; z < in_vol.shape().depth_; ++z) { + out_vol.set_ignore_rank(tensor_pos(d4, y, x, z), in_vol.get_ignore_rank(tensor_pos(d4_in, y_in, x_in, z))); + } + } + } + } + return out_vol; + } + inline float_type interpolate_2d_value_bilinearly(const tensor& t, float_type y, float_type x, std::size_t z) { y = fplus::max(0, y); diff --git a/include/fdeep/tensor_shape.hpp b/include/fdeep/tensor_shape.hpp index 5b180f2a..f4125253 100644 --- a/include/fdeep/tensor_shape.hpp +++ b/include/fdeep/tensor_shape.hpp @@ -9,6 +9,7 @@ #include "fdeep/common.hpp" #include "fdeep/shape2.hpp" +#include "fdeep/shape3.hpp" #include "fdeep/tensor_shape_variable.hpp" #include diff --git a/keras_export/convert_model.py b/keras_export/convert_model.py index 3aa58fff..a389bef2 100755 --- a/keras_export/convert_model.py +++ b/keras_export/convert_model.py @@ -228,6 +228,12 @@ def prepare_filter_weights_conv_3d(weights: NDFloat32Array) -> NDFloat32Array: return np.moveaxis(weights, [0, 1, 2, 3, 4], [1, 2, 3, 4, 0]).flatten() +def prepare_filter_weights_conv_3d_transpose(weights: NDFloat32Array) -> NDFloat32Array: + """Change dimension order of 3d transpose filter weights to the one used in fdeep""" + assert len(weights.shape) == 5 + return np.moveaxis(weights, [0, 1, 2, 3, 4], [1, 2, 3, 0, 4]).flatten() + + def show_conv_1d_layer(layer: Layer) -> Mapping[str, list[str]]: """Serialize Conv1D layer to dict""" weights = layer.get_weights() @@ -285,6 +291,27 @@ def show_conv_3d_layer(layer: Layer) -> Mapping[str, list[str]]: return result +def show_conv_3d_transpose_layer(layer: Layer) -> Mapping[str, list[str]]: + """Serialize Conv3DTranspose layer to dict""" + weights = layer.get_weights() + assert len(weights) == 1 or len(weights) == 2 + assert len(weights[0].shape) == 5 + weights_flat = prepare_filter_weights_conv_3d_transpose(weights[0]) + assert layer.padding in ['valid', 'same'] + assert layer.strides[0] <= layer.kernel_size[0] + assert layer.strides[1] <= layer.kernel_size[1] + assert layer.strides[2] <= layer.kernel_size[2] + assert len(get_layer_input_shape(layer)) == 5 + assert get_layer_input_shape(layer)[0] in {None, 1} + result = { + 'weights': encode_floats(weights_flat) + } + if len(weights) == 2: + bias = weights[1] + result['bias'] = encode_floats(bias) + return result + + def show_separable_conv_2d_layer(layer: Layer) -> Mapping[str, list[str]]: """Serialize SeparableConv2D layer to dict""" weights = layer.get_weights() @@ -475,6 +502,11 @@ def show_upsampling2d_layer(layer: Layer) -> None: assert layer.interpolation in ['nearest', 'bilinear'] +def show_upsampling3d_layer(layer: Layer) -> None: + """Validate UpSampling3D layer config (size is taken from layer config)""" + assert len(layer.size) == 3 + + def show_resizing_layer(layer: Layer) -> None: """Serialize Resizing layer to dict""" assert layer.interpolation in ['nearest', 'bilinear', 'area'] @@ -526,6 +558,7 @@ def get_layer_functions_dict() -> Mapping[str, Callable[[Layer], LayerConfig]]: 'Conv3D': show_conv_3d_layer, 'Conv1DTranspose': show_conv_1d_transpose_layer, 'Conv2DTranspose': show_conv_2d_transpose_layer, + 'Conv3DTranspose': show_conv_3d_transpose_layer, 'SeparableConv2D': show_separable_conv_2d_layer, 'DepthwiseConv2D': show_depthwise_conv_2d_layer, 'BatchNormalization': show_batch_normalization_layer, @@ -539,6 +572,7 @@ def get_layer_functions_dict() -> Mapping[str, Callable[[Layer], LayerConfig]]: 'Softmax': show_softmax_layer, 'Normalization': show_normalization_layer, 'UpSampling2D': show_upsampling2d_layer, + 'UpSampling3D': show_upsampling3d_layer, 'Resizing': show_resizing_layer, 'Rescaling': show_rescaling_layer, 'CategoryEncoding': show_category_encoding_layer, diff --git a/keras_export/generate_test_models.py b/keras_export/generate_test_models.py index b2b9522c..e7d707cd 100644 --- a/keras_export/generate_test_models.py +++ b/keras_export/generate_test_models.py @@ -18,7 +18,8 @@ from keras.layers import GlobalAveragePooling1D, GlobalMaxPooling1D from keras.layers import GlobalAveragePooling2D, GlobalMaxPooling2D from keras.layers import GlobalAveragePooling3D, GlobalMaxPooling3D -from keras.layers import Identity, Conv2DTranspose, Conv1DTranspose +from keras.layers import Identity, Conv2DTranspose, Conv1DTranspose, Conv3DTranspose +from keras.layers import UpSampling3D from keras.layers import Input, Dense, Dropout, Flatten, Activation from keras.layers import LeakyReLU, ELU, PReLU, ReLU, Softmax from keras.layers import MaxPooling1D, AveragePooling1D, UpSampling1D @@ -236,6 +237,12 @@ def get_test_model_exhaustive() -> Model: outputs.append(Conv3D(4, (1, 2, 4), strides=(2, 2, 3), padding='same')(inputs[2])) outputs.append(Conv3D(4, (1, 2, 4), padding='same', dilation_rate=(2, 2, 3))(inputs[2])) + outputs.append(Conv3DTranspose(4, (3, 3, 3))(inputs[2])) + outputs.append(Conv3DTranspose(4, (3, 3, 3), use_bias=False, padding='valid')(inputs[2])) + outputs.append(Conv3DTranspose(4, (3, 3, 3), padding='same')(inputs[2])) + outputs.append(Conv3DTranspose(4, (2, 2, 2), strides=(2, 2, 2), padding='valid')(inputs[2])) + outputs.append(Conv3DTranspose(4, (2, 2, 2), strides=(2, 2, 2), padding='same')(inputs[2])) + outputs.append(SeparableConv2D(3, (3, 3))(inputs[4])) outputs.append(DepthwiseConv2D((3, 3))(inputs[4])) outputs.append(DepthwiseConv2D((1, 2))(inputs[4])) @@ -267,6 +274,10 @@ def get_test_model_exhaustive() -> Model: outputs.append(UpSampling2D(size=(1, 2), interpolation='bilinear')(inputs[4])) outputs.append(UpSampling2D(size=(5, 3), interpolation='bilinear')(inputs[4])) + outputs.append(UpSampling3D(size=(2, 2, 2))(inputs[2])) + outputs.append(UpSampling3D(size=(1, 3, 5))(inputs[2])) + outputs.append(UpSampling3D(size=(2, 1, 1))(inputs[2])) + outputs.append(Resizing(4, 5)(inputs[4])) outputs.append(Resizing(5, 6)(inputs[4])) outputs.append(Resizing(19, 53, interpolation="bilinear")(inputs[23]))