diff --git a/include/layers/ConcatLayer.hpp b/include/layers/ConcatLayer.hpp new file mode 100644 index 00000000..d76f0ee2 --- /dev/null +++ b/include/layers/ConcatLayer.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include +#include + +#include "layers/Layer.hpp" +#include "layers/Tensor.hpp" + +namespace it_lab_ai { + +class ConcatLayer : public Layer { + public: + explicit ConcatLayer(int64_t axis = 0) : axis_(axis) {} + + void run(const Tensor& input, Tensor& output) override; + void run(const std::vector& inputs, Tensor& output); + + static std::string get_name() { return "ConcatLayer"; } + + private: + int64_t axis_; + + void validate_inputs(const std::vector& inputs) const; + int64_t normalize_axis(size_t rank) const; + Shape calculate_output_shape(const std::vector& inputs) const; + + template + void concatenate(const std::vector& inputs, Tensor& output) const; +}; + +} // namespace it_lab_ai \ No newline at end of file diff --git a/src/layers/ConcatLayer.cpp b/src/layers/ConcatLayer.cpp new file mode 100644 index 00000000..fc20269e --- /dev/null +++ b/src/layers/ConcatLayer.cpp @@ -0,0 +1,150 @@ +#include "layers/ConcatLayer.hpp" + +namespace it_lab_ai { + +void ConcatLayer::run(const Tensor& input, Tensor& output) { output = input; } + +void ConcatLayer::run(const std::vector& inputs, Tensor& output) { + if (inputs.empty()) { + throw std::runtime_error("ConcatLayer: No input tensors provided"); + } + + validate_inputs(inputs); + + switch (inputs[0].get_type()) { + case Type::kFloat: + concatenate(inputs, output); + break; + case Type::kInt: + concatenate(inputs, output); + break; + default: + throw std::runtime_error("ConcatLayer: Unsupported input tensor type"); + } +} + +void ConcatLayer::validate_inputs(const std::vector& inputs) const { + if (inputs.empty()) return; + + const Shape& first_shape = inputs[0].get_shape(); + Type first_type = inputs[0].get_type(); + const int64_t normalized_axis = normalize_axis(first_shape.dims()); + + for (size_t i = 1; i < inputs.size(); ++i) { + const Shape& shape = inputs[i].get_shape(); + if (shape.dims() != first_shape.dims()) { + throw std::runtime_error( + "ConcatLayer: All input tensors must have the same rank"); + } + + if (inputs[i].get_type() != first_type) { + throw std::runtime_error( + "ConcatLayer: All input tensors must have the same type"); + } + + for (size_t dim = 0; dim < shape.dims(); ++dim) { + if (dim != static_cast(normalized_axis) && + shape[dim] != first_shape[dim]) { + throw std::runtime_error( + "ConcatLayer: All input tensors must have the same shape except " + "for the concatenation axis"); + } + } + } +} + +int64_t ConcatLayer::normalize_axis(size_t rank) const { + if (rank == 0) { + throw std::runtime_error("ConcatLayer: Cannot concatenate scalar tensors"); + } + + int64_t axis = axis_; + + if (axis < 0) { + axis += static_cast(rank); + } + + if (axis < 0 || axis >= static_cast(rank)) { + throw std::runtime_error("ConcatLayer: Axis " + std::to_string(axis_) + + " out of range for tensor rank " + + std::to_string(rank)); + } + + return axis; +} + +Shape ConcatLayer::calculate_output_shape( + const std::vector& inputs) const { + if (inputs.empty()) return Shape({}); + + const Shape& first_shape = inputs[0].get_shape(); + std::vector output_dims(first_shape.dims()); + for (size_t i = 0; i < first_shape.dims(); ++i) { + output_dims[i] = first_shape[i]; + } + + const int64_t normalized_axis = normalize_axis(first_shape.dims()); + output_dims[normalized_axis] = 0; + for (const auto& input : inputs) { + output_dims[normalized_axis] += input.get_shape()[normalized_axis]; + } + + return Shape(output_dims); +} + +template +void ConcatLayer::concatenate(const std::vector& inputs, + Tensor& output) const { + Shape output_shape = calculate_output_shape(inputs); + std::vector output_data(output_shape.count(), 0); + + const int64_t axis = normalize_axis(inputs[0].get_shape().dims()); + const size_t outer_size = [&]() { + size_t size = 1; + for (int64_t i = 0; i < axis; ++i) { + size *= output_shape[i]; + } + return size; + }(); + + const size_t inner_size = [&]() { + size_t size = 1; + for (size_t i = axis + 1; i < output_shape.dims(); ++i) { + size *= output_shape[i]; + } + return size; + }(); + + size_t output_offset = 0; + + for (const auto& input : inputs) { + const auto& input_data = *input.as(); + const Shape& input_shape = input.get_shape(); + const size_t input_axis_size = input_shape[axis]; + + for (size_t outer = 0; outer < outer_size; ++outer) { + for (size_t a = 0; a < input_axis_size; ++a) { + for (size_t inner = 0; inner < inner_size; ++inner) { + size_t input_pos = + outer * input_axis_size * inner_size + a * inner_size + inner; + + size_t output_pos = outer * output_shape[axis] * inner_size + + (output_offset + a) * inner_size + inner; + + output_data[output_pos] = input_data[input_pos]; + } + } + } + + output_offset += input_axis_size; + } + + output = make_tensor(output_data, output_shape); +} + +template void ConcatLayer::concatenate(const std::vector&, + Tensor&) const; +template void ConcatLayer::concatenate(const std::vector&, + Tensor&) const; + +} // namespace it_lab_ai \ No newline at end of file diff --git a/test/single_layer/test_concatlayer.cpp b/test/single_layer/test_concatlayer.cpp new file mode 100644 index 00000000..4c144cc9 --- /dev/null +++ b/test/single_layer/test_concatlayer.cpp @@ -0,0 +1,198 @@ +#include + +#include "gtest/gtest.h" +#include "layers/ConcatLayer.hpp" +#include "layers/Tensor.hpp" + +using namespace it_lab_ai; + +TEST(ConcatLayerTests, ConcatEmptyTensors) { + ConcatLayer layer(0); + + Tensor empty1 = make_tensor({}, {0}); + Tensor empty2 = make_tensor({}, {2, 0, 3}); + + Tensor output; + + EXPECT_THROW(layer.run({empty1, empty2}, output), std::runtime_error); +} + +TEST(ConcatLayerTests, ConcatSingleElementTensors) { + ConcatLayer layer(0); + + Tensor single1 = make_tensor({42.0f}, {1}); + Tensor single2 = make_tensor({99.0f}, {1}); + + Tensor output; + + layer.run({single1, single2}, output); + + ASSERT_EQ(output.get_shape(), Shape({2})); + EXPECT_FLOAT_EQ(output.get({0}), 42.0f); + EXPECT_FLOAT_EQ(output.get({1}), 99.0f); +} + +TEST(ConcatLayerTests, ConcatAlongAxisWithSize1) { + ConcatLayer layer(0); + + Tensor input1 = make_tensor({1, 2, 3, 4, 5, 6}, {1, 3, 2}); + Tensor input2 = make_tensor({7, 8, 9, 10, 11, 12}, {1, 3, 2}); + + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({2, 3, 2})); + + EXPECT_FLOAT_EQ(output.get({0, 0, 0}), 1.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 1}), 2.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 0}), 3.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 1}), 4.0f); + EXPECT_FLOAT_EQ(output.get({0, 2, 0}), 5.0f); + EXPECT_FLOAT_EQ(output.get({0, 2, 1}), 6.0f); + + EXPECT_FLOAT_EQ(output.get({1, 0, 0}), 7.0f); + EXPECT_FLOAT_EQ(output.get({1, 0, 1}), 8.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 0}), 9.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 1}), 10.0f); + EXPECT_FLOAT_EQ(output.get({1, 2, 0}), 11.0f); + EXPECT_FLOAT_EQ(output.get({1, 2, 1}), 12.0f); +} + +TEST(ConcatLayerTests, ConcatScalars) { + ConcatLayer layer(0); + + Tensor scalar1 = make_tensor({42.0f}, {}); + Tensor scalar2 = make_tensor({99.0f}, {}); + + Tensor output; + + EXPECT_THROW(layer.run({scalar1, scalar2}, output), std::runtime_error); +} + +TEST(ConcatLayerTests, ConcatSameShapeFloatAxis0) { + ConcatLayer layer; + Tensor input1 = make_tensor({1.0f, 2.0f, 3.0f, 4.0f}, {2, 2}); + Tensor input2 = make_tensor({5.0f, 6.0f, 7.0f, 8.0f}, {2, 2}); + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({4, 2})); + + EXPECT_FLOAT_EQ(output.get({0, 0}), 1.0f); + EXPECT_FLOAT_EQ(output.get({0, 1}), 2.0f); + EXPECT_FLOAT_EQ(output.get({1, 0}), 3.0f); + EXPECT_FLOAT_EQ(output.get({1, 1}), 4.0f); + + EXPECT_FLOAT_EQ(output.get({2, 0}), 5.0f); + EXPECT_FLOAT_EQ(output.get({2, 1}), 6.0f); + EXPECT_FLOAT_EQ(output.get({3, 0}), 7.0f); + EXPECT_FLOAT_EQ(output.get({3, 1}), 8.0f); +} + +TEST(ConcatLayerTests, ConcatSameShapeIntAxis1) { + ConcatLayer layer(1); + Tensor input1 = make_tensor({1, 2, 3, 4}, {2, 2}); + Tensor input2 = make_tensor({1, 2, 3, 4}, {2, 2}); + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({2, 4})); + + EXPECT_EQ(output.get({0, 0}), 1); + EXPECT_EQ(output.get({0, 1}), 2); + EXPECT_EQ(output.get({0, 2}), 1); + EXPECT_EQ(output.get({0, 3}), 2); + + EXPECT_EQ(output.get({1, 0}), 3); + EXPECT_EQ(output.get({1, 1}), 4); + EXPECT_EQ(output.get({1, 2}), 3); + EXPECT_EQ(output.get({1, 3}), 4); +} + +TEST(ConcatLayerTests, Concat3DTensorsAxis2) { + ConcatLayer layer(2); + Tensor input1 = make_tensor({1, 2, 3, 4, 5, 6, 7, 8}, {2, 2, 2}); + Tensor input2 = + make_tensor({9, 10, 11, 12, 13, 14, 15, 16}, {2, 2, 2}); + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({2, 2, 4})); + + EXPECT_FLOAT_EQ(output.get({0, 0, 0}), 1.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 1}), 2.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 0}), 3.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 1}), 4.0f); + + EXPECT_FLOAT_EQ(output.get({0, 0, 2}), 9.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 3}), 10.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 2}), 11.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 3}), 12.0f); + + EXPECT_FLOAT_EQ(output.get({1, 0, 0}), 5.0f); + EXPECT_FLOAT_EQ(output.get({1, 0, 1}), 6.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 0}), 7.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 1}), 8.0f); + + EXPECT_FLOAT_EQ(output.get({1, 0, 2}), 13.0f); + EXPECT_FLOAT_EQ(output.get({1, 0, 3}), 14.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 2}), 15.0f); + EXPECT_FLOAT_EQ(output.get({1, 1, 3}), 16.0f); +} + +TEST(ConcatLayerTests, NegativeAxis) { + ConcatLayer layer(-1); + Tensor input1 = make_tensor({1.0f, 2.0f, 3.0f, 4.0f}, {2, 2}); + Tensor input2 = make_tensor({5.0f, 6.0f, 7.0f, 8.0f}, {2, 2}); + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({2, 4})); + + EXPECT_FLOAT_EQ(output.get({0, 0}), 1.0f); + EXPECT_FLOAT_EQ(output.get({0, 1}), 2.0f); + EXPECT_FLOAT_EQ(output.get({0, 2}), 5.0f); + EXPECT_FLOAT_EQ(output.get({0, 3}), 6.0f); + + EXPECT_FLOAT_EQ(output.get({1, 0}), 3.0f); + EXPECT_FLOAT_EQ(output.get({1, 1}), 4.0f); + EXPECT_FLOAT_EQ(output.get({1, 2}), 7.0f); + EXPECT_FLOAT_EQ(output.get({1, 3}), 8.0f); +} + +TEST(ConcatLayerTests, ConcatResNetStyle) { + ConcatLayer layer(1); + Tensor input1 = make_tensor({1, 2, 3, 4, 5, 6, 7, 8}, {1, 2, 2, 2}); + Tensor input2 = + make_tensor({9, 10, 11, 12, 13, 14, 15, 16}, {1, 2, 2, 2}); + Tensor output; + + layer.run({input1, input2}, output); + + ASSERT_EQ(output.get_shape(), Shape({1, 4, 2, 2})); + + EXPECT_FLOAT_EQ(output.get({0, 0, 0, 0}), 1.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 0, 1}), 2.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 1, 0}), 3.0f); + EXPECT_FLOAT_EQ(output.get({0, 0, 1, 1}), 4.0f); + + EXPECT_FLOAT_EQ(output.get({0, 1, 0, 0}), 5.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 0, 1}), 6.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 1, 0}), 7.0f); + EXPECT_FLOAT_EQ(output.get({0, 1, 1, 1}), 8.0f); + + EXPECT_FLOAT_EQ(output.get({0, 2, 0, 0}), 9.0f); + EXPECT_FLOAT_EQ(output.get({0, 2, 0, 1}), 10.0f); + EXPECT_FLOAT_EQ(output.get({0, 2, 1, 0}), 11.0f); + EXPECT_FLOAT_EQ(output.get({0, 2, 1, 1}), 12.0f); + + EXPECT_FLOAT_EQ(output.get({0, 3, 0, 0}), 13.0f); + EXPECT_FLOAT_EQ(output.get({0, 3, 0, 1}), 14.0f); + EXPECT_FLOAT_EQ(output.get({0, 3, 1, 0}), 15.0f); + EXPECT_FLOAT_EQ(output.get({0, 3, 1, 1}), 16.0f); +} \ No newline at end of file