diff --git a/CMakeLists.txt b/CMakeLists.txt index 19f59c7..37d8dc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.9) project(NeuralNet) +add_compile_options(-Wall -Wextra -Wpedantic) + set(CMAKE_CXX_STANDARD 17) set(SOURCE_FILES main.cpp) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c20dfa5..ac2abd4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,17 @@ project(${CMAKE_PROJECT_NAME}_lib) set(HEADER_FILES - activation_function.hpp - neural_net.hpp + ./activation_function.hpp + ./neural_net.hpp + ./utility.hpp ) set(SOURCE_FILES + ./neural_net.cpp ) -if (EXISTS ${SOURCE_FILES}) +# Check if any source files exist +list(LENGTH SOURCE_FILES SOURCE_FILES_LENGTH) +if (SOURCE_FILES_LENGTH GREATER 0) # The library contains header and source files. add_library(${CMAKE_PROJECT_NAME}_lib STATIC ${SOURCE_FILES} diff --git a/src/activation_function.hpp b/src/activation_function.hpp index fd20741..34269cf 100644 --- a/src/activation_function.hpp +++ b/src/activation_function.hpp @@ -3,7 +3,6 @@ #include #include -#include #include /** * Functor to set the activation function as a Sigmoid function diff --git a/src/neural_net.cpp b/src/neural_net.cpp index c2ba86c..c4a302d 100644 --- a/src/neural_net.cpp +++ b/src/neural_net.cpp @@ -1,4 +1,5 @@ #include "neural_net.hpp" +#include "utility.hpp" #include #include #include @@ -28,3 +29,37 @@ NeuralNet::NeuralNet(std::vector &layer_sizes) start_idx += size; } } + +/** Pass input vector through the neural network. + * This is a fully connected neural network geometry. + * @param x Input vector + * @return output of feed forward phase + */ +template +std::vector +NeuralNet::feed_forward(std::vector &x) { + std::vector A = x; + int start_idx = 0; + + // Feed each layer forward except the last layer using the user specified + // activation function + for (auto size = m_sizes.begin(); size < m_sizes.end() - 1; size++) { + // Get the iterator range for the current layer + auto layer_start = m_weights.begin() + start_idx; + auto layer_end = m_weights.end() + start_idx + *size; + + std::vector Anew = Utilities::feed_layer( + layer_start, layer_end, &A, m_activation_func); + if (Anew.size() > A.capacity()) { + A.reserve(Anew.size()); + } + std::move(Anew.begin(), Anew.end(), A.begin()); + start_idx += *size; + } + + // Always use soft max for the final layer + auto last_layer_start = m_weights.begin() + start_idx; + auto output = Utilities::feed_layer(last_layer_start, + m_weights.end(), A, m_soft_max); + return output; +} diff --git a/src/neural_net.hpp b/src/neural_net.hpp index 4e8cf34..820799d 100644 --- a/src/neural_net.hpp +++ b/src/neural_net.hpp @@ -1,6 +1,7 @@ #ifndef NEURAL_NET_H #define NEURAL_NET_H +#include "activation_function.hpp" #include template class NeuralNet { public: @@ -8,8 +9,11 @@ public: private: ActivationFunction m_activation_func; + SoftMax m_soft_max; std::vector m_sizes; - std::vector m_weights; - std::vector feed_forward(std::vector x); + std::vector m_weights; + std::vector feed_forward(std::vector &x); + std::vector feed_layer_forward(size_t layer_start_idx, size_t size, + std::vector &A); }; #endif diff --git a/src/utility.hpp b/src/utility.hpp new file mode 100644 index 0000000..e29900f --- /dev/null +++ b/src/utility.hpp @@ -0,0 +1,32 @@ +#ifndef UTILITY_H +#define UTILITY_H + +#include +#include +#include +#include + +namespace Utilities { + +template +std::vector feed_layer(std::vector::iterator weight_start, + std::vector::iterator weight_end, + std::vector &A, + ActivationFunction activation_func) { + // Calculate the new A vector from the current weights + std::vector Anew; + Anew.reserve(std::distance(weight_start, weight_end)); + std::transform(weight_start, weight_end, Anew.begin(), + [&A, &activation_func](float weight) { + float summed_weight = std::accumulate( + A.begin(), A.end(), 0.0f, [&weight](float acc, float a) { + return acc + a * weight; + }); + return summed_weight; + }); + activation_func(Anew); + return Anew; +}; + +} // namespace Utilities +#endif diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 7ba1712..8f1605f 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -2,6 +2,7 @@ include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) add_executable(Unit_Tests_run test_activation_functions.cpp + test_utility.cpp ) target_link_libraries(Unit_Tests_run gtest gtest_main) diff --git a/tests/unit_tests/test_activation_functions.cpp b/tests/unit_tests/test_activation_functions.cpp index 2e47fa7..82b00a0 100644 --- a/tests/unit_tests/test_activation_functions.cpp +++ b/tests/unit_tests/test_activation_functions.cpp @@ -1,75 +1,71 @@ -#include #include "../../src/activation_function.hpp" #include +#include #include TEST(ActivationFunctionTest, SigmoidTest) { - Sigmoid sigmoid; - std::vector input = {0.0, 10.0, -10.0, 1.0, -1.0}; - std::vector expected = { - 0.5, - 0.9999546, - 0.0000454, - 1.0 / (1.0 + exp(-1.0)), - 1.0 / (1.0 + exp(1.0)) - }; - - std::vector test = input; - sigmoid(test); - - ASSERT_EQ(test.size(), expected.size()); - for (size_t i = 0; i < test.size(); i++) { - EXPECT_NEAR(test[i], expected[i], 1e-6); - } - - // Test initialization standard deviation - EXPECT_NEAR(sigmoid.init_stddev(100), sqrt(1.0/100), 1e-6); + Sigmoid sigmoid; + std::vector input = {0.0, 10.0, -10.0, 1.0, -1.0}; + std::vector expected = {0.5, 0.9999546, 0.0000454, + static_cast(1.0 / (1.0 + exp(-1.0))), + static_cast(1.0 / (1.0 + exp(1.0)))}; + + std::vector test = input; + sigmoid(test); + + ASSERT_EQ(test.size(), expected.size()); + for (size_t i = 0; i < test.size(); i++) { + EXPECT_NEAR(test[i], expected[i], 1e-6); + } + + // Test initialization standard deviation + EXPECT_NEAR(sigmoid.init_stddev(100), sqrt(1.0 / 100), 1e-6); } TEST(ActivationFunctionTest, ReLUTest) { - ReLU relu; - std::vector input = {0.0, 5.0, -5.0, 0.0001, -0.0001}; - std::vector expected = {0.0, 5.0, 0.0, 0.0001, 0.0}; - - std::vector test = input; - relu(test); - - ASSERT_EQ(test.size(), expected.size()); - for (size_t i = 0; i < test.size(); i++) { - EXPECT_FLOAT_EQ(test[i], expected[i]); - } - - // Test initialization standard deviation - EXPECT_NEAR(relu.init_stddev(100), sqrt(2.0/100), 1e-6); + ReLU relu; + std::vector input = {0.0, 5.0, -5.0, 0.0001, -0.0001}; + std::vector expected = {0.0, 5.0, 0.0, 0.0001, 0.0}; + + std::vector test = input; + relu(test); + + ASSERT_EQ(test.size(), expected.size()); + for (size_t i = 0; i < test.size(); i++) { + EXPECT_FLOAT_EQ(test[i], expected[i]); + } + + // Test initialization standard deviation + EXPECT_NEAR(relu.init_stddev(100), sqrt(2.0 / 100), 1e-6); } TEST(ActivationFunctionTest, SoftMaxTest) { - SoftMax softmax; - std::vector input = {1.0, 2.0, 3.0, 4.0, 1.0}; - std::vector test = input; - - softmax(test); - - // Test properties of softmax - ASSERT_EQ(test.size(), input.size()); - - // Sum should be approximately 1 - float sum = 0.0; - for (float val : test) { - sum += val; - // All values should be between 0 and 1 - EXPECT_GE(val, 0.0); - EXPECT_LE(val, 1.0); + SoftMax softmax; + std::vector input = {1.0, 2.0, 3.0, 4.0, 1.0}; + std::vector test = input; + + softmax(test); + + // Test properties of softmax + ASSERT_EQ(test.size(), input.size()); + + // Sum should be approximately 1 + float sum = 0.0; + for (float val : test) { + sum += val; + // All values should be between 0 and 1 + EXPECT_GE(val, 0.0); + EXPECT_LE(val, 1.0); + } + EXPECT_NEAR(sum, 1.0, 1e-6); + + // Higher input should lead to higher output + for (size_t i = 0; i < test.size() - 1; i++) { + if (input[i] < input[i + 1]) { + EXPECT_LT(test[i], test[i + 1]); } - EXPECT_NEAR(sum, 1.0, 1e-6); - - // Higher input should lead to higher output - for (size_t i = 0; i < test.size() - 1; i++) { - if (input[i] < input[i + 1]) { - EXPECT_LT(test[i], test[i + 1]); - } - } - - // Test initialization standard deviation - EXPECT_NEAR(softmax.init_stddev(100), sqrt(1.0/100), 1e-6); + } + + // Test initialization standard deviation + EXPECT_NEAR(softmax.init_stddev(100), sqrt(1.0 / 100), 1e-6); } diff --git a/tests/unit_tests/test_utility.cpp b/tests/unit_tests/test_utility.cpp new file mode 100644 index 0000000..15b2bfe --- /dev/null +++ b/tests/unit_tests/test_utility.cpp @@ -0,0 +1,80 @@ +#include "activation_function.hpp" +#include "utility.hpp" +#include +#include + +// Simple identity activation function for testing +struct Identity { + void operator()(std::vector& x) const { + // Identity function - no change to values + } +}; + +TEST(UtilityTest, FeedLayerIdentityTest) { + // Test with identity activation function for simple verification + // Input: [1, 2] + // Weights: [0.5, -0.5, 1.0, -1.0] + // Expected: [0.5, -1.0] (manually calculated) + // First output: 1.0 * 0.5 + 2.0 * -0.5 = 0.5 + // Second output: 1.0 * 1.0 + 2.0 * -1.0 = -1.0 + + std::vector weights = {0.5, -0.5, 1.0, -1.0}; + std::vector input = {1.0, 2.0}; + Identity identity; + + auto output = Utilities::feed_layer(weights.begin(), weights.end(), + input, identity); + + ASSERT_EQ(output.size(), 2); + EXPECT_NEAR(output[0], 0.5f, 1e-5); // 1.0 * 0.5 + 2.0 * -0.5 + EXPECT_NEAR(output[1], -1.0f, 1e-5); // 1.0 * 1.0 + 2.0 * -1.0 +} + +TEST(UtilityTest, FeedLayerSigmoidTest) { + // Test with sigmoid activation + // Input: [1] + // Weights: [2, -2] + std::vector weights = {2.0, -2.0}; + std::vector input = {1.0}; + Sigmoid sigmoid; + + auto output = Utilities::feed_layer(weights.begin(), weights.end(), + input, sigmoid); + + ASSERT_EQ(output.size(), 2); + // Note: Sigmoid is applied to the whole vector after matrix multiplication + float expected0 = 2.0; // 1.0 * 2.0 + float expected1 = -2.0; // 1.0 * -2.0 + EXPECT_NEAR(output[0], 1.0 / (1.0 + std::exp(-expected0)), 1e-5); + EXPECT_NEAR(output[1], 1.0 / (1.0 + std::exp(-expected1)), 1e-5); +} + +TEST(UtilityTest, FeedLayerSoftMaxTest) { + // Test with softmax activation + // Input: [1] + // Weights: [2, 2] + std::vector weights = {2.0, 2.0}; + std::vector input = {1.0}; + SoftMax softmax; + + auto output = Utilities::feed_layer(weights.begin(), weights.end(), + input, softmax); + + ASSERT_EQ(output.size(), 2); + // Both outputs should be 0.5 since inputs to softmax are equal (both 2.0) + EXPECT_NEAR(output[0], 0.5, 1e-5); + EXPECT_NEAR(output[1], 0.5, 1e-5); +} + +TEST(UtilityTest, FeedLayerEmptyInput) { + std::vector weights = {1.0, 1.0}; + std::vector input = {}; + Identity identity; + + auto output = Utilities::feed_layer(weights.begin(), weights.end(), + input, identity); + + ASSERT_EQ(output.size(), 2); + EXPECT_NEAR(output[0], 0.0f, 1e-5); + EXPECT_NEAR(output[1], 0.0f, 1e-5); +}