Skip to content

Commit ecfa571

Browse files
Dobiasdclaude
andauthored
Add Conv3D layer support, closes #137 (#455)
Implements the Conv3D layer using the same im2col + Eigen GEMM strategy as Conv2D, with two outer filter-offset loops for the d4 axis. Supports valid/same/causal padding, strides, and dilation. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 24d9c9d commit ecfa571

6 files changed

Lines changed: 363 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Would you like to build/train a model using Keras/Python? And would you like to
4242
* `Add`, `Concatenate`, `Subtract`, `Multiply`, `Average`, `Maximum`, `Minimum`, `Dot`
4343
* `AveragePooling1D/2D/3D`, `GlobalAveragePooling1D/2D/3D`
4444
* `TimeDistributed`
45-
* `Conv1D/2D`, `SeparableConv2D`, `DepthwiseConv2D`
45+
* `Conv1D/2D/3D`, `SeparableConv2D`, `DepthwiseConv2D`
4646
* `Conv1DTranspose`, `Conv2DTranspose`
4747
* `Cropping1D/2D/3D`, `ZeroPadding1D/2D/3D`, `CenterCrop`
4848
* `BatchNormalization`, `Dense`, `Flatten`, `Normalization`
@@ -71,7 +71,7 @@ Would you like to build/train a model using Keras/Python? And would you like to
7171
### Currently not supported are the following:
7272

7373
`Lambda` ([why](FAQ.md#why-are-lambda-layers-not-supported)),
74-
`Conv3D`, `ConvLSTM1D`, `ConvLSTM2D`, `Discretization`,
74+
`ConvLSTM1D`, `ConvLSTM2D`, `Discretization`,
7575
`GRUCell`, `Hashing`,
7676
`IntegerLookup`,
7777
`LocallyConnected1D`, `LocallyConnected2D`,

include/fdeep/convolution3d.hpp

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
#include "fdeep/common.hpp"
1010

11+
#include "fdeep/convolution.hpp"
1112
#include "fdeep/filter.hpp"
13+
#include "fdeep/shape3.hpp"
1214

1315
#include <algorithm>
1416
#include <cassert>
@@ -115,5 +117,246 @@ namespace internal {
115117
out_size_d4_size_t, out_height_size_t, out_width_size_t };
116118
}
117119

120+
struct convolution3d_filter_matrices {
121+
tensor_shape filter_shape_;
122+
std::size_t filter_count_;
123+
float_vec biases_;
124+
bool use_bias_;
125+
tensor filter_mats_;
126+
};
127+
128+
inline tensor dilate_tensor_3d(const shape3& dilation_rate, const tensor& in)
129+
{
130+
if (dilation_rate == shape3(1, 1, 1)) {
131+
return in;
132+
}
133+
assertion(in.shape().rank() == 4, "Invalid rank for 3d dilation");
134+
135+
const auto in_shape = in.shape();
136+
const tensor_shape dilated_shape(
137+
(in_shape.size_dim_4_ - 1) * dilation_rate.size_dim_4_ + 1,
138+
(in_shape.height_ - 1) * dilation_rate.height_ + 1,
139+
(in_shape.width_ - 1) * dilation_rate.width_ + 1,
140+
in_shape.depth_);
141+
tensor result(dilated_shape, static_cast<float_type>(0));
142+
for (std::size_t d4 = 0; d4 < in_shape.size_dim_4_; ++d4) {
143+
for (std::size_t y = 0; y < in_shape.height_; ++y) {
144+
for (std::size_t x = 0; x < in_shape.width_; ++x) {
145+
for (std::size_t z = 0; z < in_shape.depth_; ++z) {
146+
result.set_ignore_rank(tensor_pos(
147+
d4 * dilation_rate.size_dim_4_,
148+
y * dilation_rate.height_,
149+
x * dilation_rate.width_,
150+
z),
151+
in.get_ignore_rank(tensor_pos(d4, y, x, z)));
152+
}
153+
}
154+
}
155+
}
156+
return result;
157+
}
158+
159+
inline filter dilate_filter_3d(const shape3& dilation_rate, const filter& undilated)
160+
{
161+
return filter(dilate_tensor_3d(dilation_rate, undilated.get_tensor()),
162+
undilated.get_bias());
163+
}
164+
165+
inline filter_vec generate_filters_3d(
166+
const shape3& dilation_rate,
167+
const tensor_shape& filter_shape, std::size_t k,
168+
const float_vec& weights, const float_vec& bias)
169+
{
170+
filter_vec filters(k, filter(tensor(filter_shape, 0), 0));
171+
172+
assertion(!filters.empty(), "at least one filter needed");
173+
const std::size_t param_count = fplus::sum(fplus::transform(
174+
fplus_c_mem_fn_t(filter, volume, std::size_t), filters));
175+
176+
assertion(static_cast<std::size_t>(weights.size()) == param_count,
177+
"invalid weight size");
178+
const auto filter_param_cnt = filters.front().shape().volume();
179+
180+
auto filter_weights = fplus::split_every(filter_param_cnt, weights);
181+
assertion(filter_weights.size() == filters.size(),
182+
"invalid size of filter weights");
183+
assertion(bias.size() == filters.size(), "invalid bias size");
184+
auto it_filter_val = std::begin(filter_weights);
185+
auto it_filter_bias = std::begin(bias);
186+
for (auto& filt : filters) {
187+
filt.set_params(*it_filter_val, *it_filter_bias);
188+
filt = dilate_filter_3d(dilation_rate, filt);
189+
++it_filter_val;
190+
++it_filter_bias;
191+
}
192+
193+
return filters;
194+
}
195+
196+
inline convolution3d_filter_matrices generate_im2col_filter_matrix_3d(
197+
const std::vector<filter>& filters)
198+
{
199+
assertion(fplus::all_the_same_on(
200+
fplus_c_mem_fn_t(filter, shape, tensor_shape), filters),
201+
"all filters must have the same shape");
202+
203+
const auto biases = fplus::transform_convert<float_vec>(
204+
fplus_c_mem_fn_t(filter, get_bias, float_type),
205+
filters);
206+
207+
const bool use_bias = fplus::sum(biases) != static_cast<float_type>(0) || !fplus::all_the_same(biases);
208+
209+
const auto shape = filters.front().shape();
210+
211+
tensor filter_mats = tensor(
212+
tensor_shape(shape.size_dim_4_, shape.height_, shape.width_, shape.depth_, filters.size()),
213+
static_cast<float_type>(0));
214+
215+
for (std::size_t d4 = 0; d4 < shape.size_dim_4_; ++d4) {
216+
for (std::size_t y = 0; y < shape.height_; ++y) {
217+
for (std::size_t n = 0; n < filters.size(); ++n) {
218+
for (std::size_t x = 0; x < shape.width_; ++x) {
219+
for (std::size_t z = 0; z < shape.depth_; ++z) {
220+
filter_mats.set(tensor_pos(d4, y, x, z, n),
221+
filters[n].get(tensor_pos(d4, y, x, z)));
222+
}
223+
}
224+
}
225+
}
226+
}
227+
228+
return { shape, filters.size(), biases, use_bias, filter_mats };
229+
}
230+
231+
inline tensor init_conv_output_tensor_3d(
232+
std::size_t out_size_d4,
233+
std::size_t out_height,
234+
std::size_t out_width,
235+
std::size_t out_depth,
236+
std::size_t rank,
237+
const convolution3d_filter_matrices& filter_mat)
238+
{
239+
tensor output(tensor_shape_with_changed_rank(
240+
tensor_shape(out_size_d4, out_height, out_width, out_depth),
241+
rank),
242+
static_cast<float_type>(0));
243+
if (filter_mat.use_bias_) {
244+
const auto bias_ptr = &filter_mat.biases_.front();
245+
const auto bias_ptr_end = bias_ptr + out_depth;
246+
for (std::size_t d4_out = 0; d4_out < out_size_d4; ++d4_out) {
247+
for (std::size_t y_out = 0; y_out < out_height; ++y_out) {
248+
for (std::size_t x_out = 0; x_out < out_width; ++x_out) {
249+
auto output_ptr = &output.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, x_out, 0));
250+
std::copy(bias_ptr, bias_ptr_end, output_ptr);
251+
}
252+
}
253+
}
254+
}
255+
return output;
256+
}
257+
258+
inline Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned, Eigen::OuterStride<>> get_im2col_mapping_3d(
259+
const tensor& in,
260+
std::size_t f_width,
261+
std::size_t f_depth,
262+
std::size_t strides_x,
263+
std::size_t out_width,
264+
std::size_t d4,
265+
std::size_t y,
266+
std::size_t d4_filt,
267+
std::size_t y_filt)
268+
{
269+
// Same trick as in the 2D case: avoid materializing the im2col matrix
270+
// by using an outer stride smaller than the row count, so adjacent
271+
// columns share data along the receptive field.
272+
return Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned, Eigen::OuterStride<>>(
273+
const_cast<float_type*>(&in.get_ref_ignore_rank(tensor_pos(0, d4 + d4_filt, y + y_filt, 0, 0))),
274+
static_cast<EigenIndex>(f_width * f_depth),
275+
static_cast<EigenIndex>(out_width),
276+
Eigen::OuterStride<>(static_cast<EigenIndex>(f_depth * strides_x)));
277+
}
278+
279+
inline tensor convolve_accumulative_3d(
280+
std::size_t out_size_d4,
281+
std::size_t out_height,
282+
std::size_t out_width,
283+
std::size_t strides_d4,
284+
std::size_t strides_y,
285+
std::size_t strides_x,
286+
const convolution3d_filter_matrices& filter_mat,
287+
const tensor& in)
288+
{
289+
const tensor& filter_mats = filter_mat.filter_mats_;
290+
const auto f_size_d4 = filter_mat.filter_shape_.size_dim_4_;
291+
const auto f_height = filter_mat.filter_shape_.height_;
292+
const auto f_width = filter_mat.filter_shape_.width_;
293+
const auto f_depth = filter_mat.filter_shape_.depth_;
294+
const auto out_depth = filter_mat.filter_count_;
295+
296+
assertion(f_depth == in.shape().depth_, "filter depth does not match input");
297+
assertion(filter_mats.shape().size_dim_5_ == f_size_d4, "incorrect number of filter levels in d4 direction");
298+
assertion(filter_mats.shape().size_dim_4_ == f_height, "incorrect number of filter levels in y direction");
299+
assertion(out_width == (in.shape().width_ - f_width) / strides_x + 1, "output width does not match");
300+
assertion(out_depth == filter_mat.biases_.size(), "invalid bias count");
301+
302+
tensor output = init_conv_output_tensor_3d(out_size_d4, out_height, out_width, out_depth, in.shape().rank(), filter_mat);
303+
304+
for (std::size_t d4_filt = 0; d4_filt < f_size_d4; ++d4_filt) {
305+
for (std::size_t y_filt = 0; y_filt < f_height; ++y_filt) {
306+
const Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
307+
filter(const_cast<float_type*>(&filter_mats.get_ref_ignore_rank(tensor_pos(d4_filt, y_filt, 0, 0, 0))),
308+
static_cast<EigenIndex>(out_depth),
309+
static_cast<EigenIndex>(f_width * f_depth));
310+
for (std::size_t d4 = 0, d4_out = 0; d4 < in.shape().size_dim_4_ + 1 - f_size_d4; d4 += strides_d4, ++d4_out) {
311+
for (std::size_t y = 0, y_out = 0; y < in.shape().height_ + 1 - f_height; y += strides_y, ++y_out) {
312+
const auto input = get_im2col_mapping_3d(in, f_width, f_depth, strides_x, out_width, d4, y, d4_filt, y_filt);
313+
Eigen::Map<ColMajorMatrixXf, Eigen::Unaligned>
314+
output_map(&output.get_ref_ignore_rank(tensor_pos(0, d4_out, y_out, 0, 0)),
315+
static_cast<EigenIndex>(out_depth),
316+
static_cast<EigenIndex>(out_width));
317+
318+
output_map.noalias() += filter * input;
319+
}
320+
}
321+
}
322+
}
323+
324+
return output;
325+
}
326+
327+
inline tensor convolve_3d(
328+
const shape3& strides,
329+
const padding& pad_type,
330+
const convolution3d_filter_matrices& filter_mat,
331+
const tensor& input)
332+
{
333+
assertion(filter_mat.filter_shape_.depth_ == input.shape().depth_,
334+
"invalid filter depth");
335+
336+
const shape3 filter_spatial_shape(
337+
filter_mat.filter_shape_.size_dim_4_,
338+
filter_mat.filter_shape_.height_,
339+
filter_mat.filter_shape_.width_);
340+
341+
const auto conv_cfg = preprocess_convolution_3d(
342+
filter_spatial_shape,
343+
strides, pad_type,
344+
input.shape().size_dim_4_,
345+
input.shape().height_,
346+
input.shape().width_);
347+
348+
const auto in_padded = pad_tensor(0,
349+
conv_cfg.pad_front_, conv_cfg.pad_back_,
350+
conv_cfg.pad_top_, conv_cfg.pad_bottom_,
351+
conv_cfg.pad_left_, conv_cfg.pad_right_,
352+
input);
353+
354+
return convolve_accumulative_3d(
355+
conv_cfg.out_size_d4_, conv_cfg.out_height_, conv_cfg.out_width_,
356+
strides.size_dim_4_, strides.height_, strides.width_,
357+
filter_mat,
358+
in_padded);
359+
}
360+
118361
}
119362
}

include/fdeep/import_model.hpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include "fdeep/layers/concatenate_layer.hpp"
4040
#include "fdeep/layers/conv_2d_layer.hpp"
4141
#include "fdeep/layers/conv_2d_transpose_layer.hpp"
42+
#include "fdeep/layers/conv_3d_layer.hpp"
4243
#include "fdeep/layers/cropping_3d_layer.hpp"
4344
#include "fdeep/layers/dense_layer.hpp"
4445
#include "fdeep/layers/depthwise_conv_2d_layer.hpp"
@@ -416,6 +417,36 @@ namespace internal {
416417
dilation_rate, weights, bias);
417418
}
418419

420+
inline layer_ptr create_conv_3d_layer(const get_param_f& get_param,
421+
const nlohmann::json& data,
422+
const std::string& name)
423+
{
424+
const std::string padding_str = data["config"]["padding"];
425+
const auto pad_type = create_padding(padding_str);
426+
427+
const shape3 strides = create_shape3(data["config"]["strides"]);
428+
const shape3 dilation_rate = create_shape3(data["config"]["dilation_rate"]);
429+
430+
const auto filter_count = create_size_t(data["config"]["filters"]);
431+
float_vec bias(filter_count, 0);
432+
const bool use_bias = data["config"]["use_bias"];
433+
if (use_bias)
434+
bias = decode_floats(get_param(name, "bias"));
435+
assertion(bias.size() == filter_count, "size of bias does not match");
436+
437+
const float_vec weights = decode_floats(get_param(name, "weights"));
438+
const shape3 kernel_size = create_shape3(data["config"]["kernel_size"]);
439+
assertion(weights.size() % kernel_size.volume() == 0,
440+
"invalid number of weights");
441+
const std::size_t filter_depths = weights.size() / (kernel_size.volume() * filter_count);
442+
const tensor_shape filter_shape(
443+
kernel_size.size_dim_4_, kernel_size.height_, kernel_size.width_, filter_depths);
444+
445+
return std::make_shared<conv_3d_layer>(name,
446+
filter_shape, filter_count, strides, pad_type,
447+
dilation_rate, weights, bias);
448+
}
449+
419450
inline layer_ptr create_conv_2d_transpose_layer(const get_param_f& get_param,
420451
const nlohmann::json& data,
421452
const std::string& name)
@@ -1267,6 +1298,7 @@ namespace internal {
12671298
{ "Identity", create_identity_layer },
12681299
{ "Conv1D", create_conv_2d_layer },
12691300
{ "Conv2D", create_conv_2d_layer },
1301+
{ "Conv3D", create_conv_3d_layer },
12701302
{ "Conv1DTranspose", create_conv_2d_transpose_layer },
12711303
{ "Conv2DTranspose", create_conv_2d_transpose_layer },
12721304
{ "SeparableConv1D", create_separable_conv_2D_layer },
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2016, Tobias Hermann.
2+
// https://github.com/Dobiasd/frugally-deep
3+
// Distributed under the MIT License.
4+
// (See accompanying LICENSE file or at
5+
// https://opensource.org/licenses/MIT)
6+
7+
#pragma once
8+
9+
#include "fdeep/convolution3d.hpp"
10+
#include "fdeep/filter.hpp"
11+
#include "fdeep/layers/layer.hpp"
12+
#include "fdeep/shape3.hpp"
13+
#include "fdeep/tensor_shape.hpp"
14+
15+
#include <fplus/fplus.hpp>
16+
17+
#include <cstddef>
18+
#include <string>
19+
#include <vector>
20+
21+
namespace fdeep {
22+
namespace internal {
23+
24+
class conv_3d_layer : public layer {
25+
public:
26+
explicit conv_3d_layer(
27+
const std::string& name, const tensor_shape& filter_shape,
28+
std::size_t k, const shape3& strides, padding p,
29+
const shape3& dilation_rate,
30+
const float_vec& weights, const float_vec& bias)
31+
: layer(name)
32+
, filters_(generate_im2col_filter_matrix_3d(
33+
generate_filters_3d(dilation_rate, filter_shape, k, weights, bias)))
34+
, strides_(strides)
35+
, padding_(p)
36+
{
37+
assertion(k > 0, "needs at least one filter");
38+
assertion(filter_shape.volume() > 0, "filter must have volume");
39+
assertion(strides.volume() > 0, "invalid strides");
40+
}
41+
42+
protected:
43+
tensors apply_impl(const tensors& inputs) const override
44+
{
45+
const auto& input = single_tensor_from_tensors(inputs);
46+
return { convolve_3d(strides_, padding_, filters_, input) };
47+
}
48+
convolution3d_filter_matrices filters_;
49+
shape3 strides_;
50+
padding padding_;
51+
};
52+
53+
}
54+
}

0 commit comments

Comments
 (0)