From c476105881708d3fc90024c3386700ba7f66f89f Mon Sep 17 00:00:00 2001 From: Krishna Vedala <7001608+kvedala@users.noreply.github.com> Date: Wed, 3 Jun 2020 21:40:28 -0400 Subject: [PATCH 1/6] find openMP before adding subdirectories --- CMakeLists.txt | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 46a033649..d304c776f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.3) +cmake_minimum_required(VERSION 3.9) project(Algorithms_in_C++ LANGUAGES CXX VERSION 1.0.0 @@ -53,6 +53,15 @@ if(DOXYGEN_FOUND) ) endif() +if(USE_OPENMP) + find_package(OpenMP) + if (OpenMP_CXX_FOUND) + message(STATUS "Building with OpenMP Multithreading.") + else() + message(STATUS "No OpenMP found, no multithreading.") + endif() +endif() + add_subdirectory(math) add_subdirectory(others) add_subdirectory(search) @@ -63,15 +72,6 @@ add_subdirectory(probability) add_subdirectory(machine_learning) add_subdirectory(computer_oriented_statistical_methods) -if(USE_OPENMP) - find_package(OpenMP) - if (OpenMP_CXX_FOUND) - message(STATUS "Building with OpenMP Multithreading.") - else() - message(STATUS "No OpenMP found, no multithreading.") - endif() -endif() - set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_VERSION ${PROJECT_VERSION}) include(CPack) From 65def005b8a33d9366515a121532fedbe3b1825d Mon Sep 17 00:00:00 2001 From: Krishna Vedala <7001608+kvedala@users.noreply.github.com> Date: Wed, 3 Jun 2020 21:59:18 -0400 Subject: [PATCH 2/6] added kohonen self organizing map --- machine_learning/kohonen_som_trace.cpp | 428 +++++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 machine_learning/kohonen_som_trace.cpp diff --git a/machine_learning/kohonen_som_trace.cpp b/machine_learning/kohonen_som_trace.cpp new file mode 100644 index 000000000..fd8c503cd --- /dev/null +++ b/machine_learning/kohonen_som_trace.cpp @@ -0,0 +1,428 @@ +/** + * \file + * \brief [Kohonen self organizing + * map](https://en.wikipedia.org/wiki/Self-organizing_map) (data tracing) + * + * This example implements a powerful self organizing map algorithm. + * The algorithm creates a connected network of weights that closely + * follows the given data points. This this creates a chain of nodes that + * resembles the given input shape. + * + * \note This C++ version of the program is considerable slower than its [C + * counterpart](https://github.com/kvedala/C/blob/master/machine_learning/kohonen_som_trace.c) + * \note The compiled code is much slower when compiled with MS Visual C++ 2019 + * than with GCC on windows + */ +#define _USE_MATH_DEFINES // required for MS Visual C++ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _OPENMP // check if OpenMP based parallellization is available +#include +#endif + +/** + * Helper function to generate a random number in a given interval. + * \n Steps: + * 1. `r1 = rand() % 100` gets a random number between 0 and 99 + * 2. `r2 = r1 / 100` converts random number to be between 0 and 0.99 + * 3. scale and offset the random number to given range of \f$[a,b]\f$ + * + * \param[in] a lower limit + * \param[in] b upper limit + * \returns random number in the range \f$[a,b]\f$ + */ +double _random(double a, double b) { + return ((b - a) * (std::rand() % 100) / 100.f) + a; +} + +/** + * Save a given n-dimensional data martix to file. + * + * \param[in] fname filename to save in (gets overwriten without confirmation) + * \param[in] X matrix to save + * \returns 0 if all ok + * \returns -1 if file creation failed + */ +int save_nd_data(const char *fname, + const std::vector> &X) { + size_t num_points = X.size(); // number of rows + size_t num_features = X[0].size(); // number of columns + + std::ofstream fp; + fp.open(fname); + if (!fp.is_open()) { + // error with opening file to write + std::cerr << "Error opening file " << fname << "\n"; + return -1; + } + + // for each point in the array + for (int i = 0; i < num_points; i++) { + // for each feature in the array + for (int j = 0; j < num_features; j++) { + fp << X[i][j]; // print the feature value + if (j < num_features - 1) // if not the last feature + fp << ","; // suffix comma + } + if (i < num_points - 1) // if not the last row + fp << "\n"; // start a new line + } + + fp.close(); + return 0; +} + +/** + * Update weights of the SOM using Kohonen algorithm + * + * \param[in] X data point + * \param[in,out] W weights matrix + * \param[in,out] D temporary vector to store distances + * \param[in] alpha learning rate \f$0<\alpha\le1\f$ + * \param[in] R neighborhood range + */ +void update_weights(const std::valarray &x, + std::vector> *W, + std::valarray *D, double alpha, int R) { + int j, k; + int num_out = W->size(); // number of SOM output nodes + int num_features = x.size(); // number of data features + +#ifdef _OPENMP +#pragma omp for +#endif + // step 1: for each output point + for (j = 0; j < num_out; j++) { + // compute Euclidian distance of each output + // point from the current sample + (*D)[j] = (((*W)[j] - x) * ((*W)[j] - x)).sum(); + } + + // step 2: get closest node i.e., node with snallest Euclidian distance to + // the current pattern + auto result = std::min_element(std::begin(*D), std::end(*D)); + double d_min = *result; + int d_min_idx = std::distance(std::begin(*D), result); + + // step 3a: get the neighborhood range + int from_node = std::max(0, d_min_idx - R); + int to_node = std::min(num_out, d_min_idx + R + 1); + + // step 3b: update the weights of nodes in the + // neighborhood +#ifdef _OPENMP +#pragma omp for +#endif + for (j = from_node; j < to_node; j++) + // update weights of nodes in the neighborhood + (*W)[j] += alpha * (x - (*W)[j]); +} + +/** + * Apply incremental algorithm with updating neighborhood and learning rates + * on all samples in the given datset. + * + * \param[in] X data set + * \param[in,out] W weights matrix + * \param[in] D temporary vector to store distances + * \param[in] alpha_min terminal value of alpha + */ +void kohonen_som_tracer(const std::vector> &X, + std::vector> *W, + double alpha_min) { + int num_samples = X.size(); // number of rows + int num_features = X[0].size(); // number of columns + int num_out = W->size(); // number of rows + int R = num_out >> 2, iter = 0; + double alpha = 1.f; + + std::valarray D(num_out); + + // Loop alpha from 1 to slpha_min + for (; alpha > alpha_min; alpha -= 0.01, iter++) { + // Loop for each sample pattern in the data set + for (int sample = 0; sample < num_samples; sample++) { + // update weights for the current input pattern sample + update_weights(X[sample], W, &D, alpha, R); + } + + // every 10th iteration, reduce the neighborhood range + if (iter % 10 == 0 && R > 1) + R--; + } +} + +/** Creates a random set of points distributed *near* the circumference + * of a circle and trains an SOM that finds that circular pattern. The + * generating function is + * \f{eqnarray*}{ \f} + * + * \param[out] data matrix to store data in + */ +void test_circle(std::vector> *data) { + const int N = data->size(); + const double R = 0.75, dr = 0.3; + double a_t = 0., b_t = 2.f * M_PI; // theta random between 0 and 2*pi + double a_r = R - dr, b_r = R + dr; // radius random between R-dr and R+dr + int i; + +#ifdef _OPENMP +#pragma omp for +#endif + for (i = 0; i < N; i++) { + double r = _random(a_r, b_r); // random radius + double theta = _random(a_t, b_t); // random theta + data[0][i][0] = r * cos(theta); // convert from polar to cartesian + data[0][i][1] = r * sin(theta); + } +} + +/** Test that creates a random set of points distributed *near* the + * circumference of a circle and trains an SOM that finds that circular pattern. + * The following [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) + * files are created to validate the execution: + * * `test1.csv`: random test samples points with a circular pattern + * * `w11.csv`: initial random map + * * `w12.csv`: trained SOM map + * + * The outputs can be readily plotted in [gnuplot](https:://gnuplot.info) using + * the following snippet + * ```gnuplot + * set datafile separator ',' + * plot "test1.csv" title "original", \ + * "w11.csv" title "w1", \ + * "w12.csv" title "w2" + * ``` + * ![Sample execution + * output](https://raw.githubusercontent.com/kvedala/C/docs/images/machine_learning/kohonen/test1.svg) + */ +void test1() { + int j, N = 500; + int features = 2; + int num_out = 50; + std::vector> X(N); + std::vector> W(num_out); + for (int i = 0; i < std::max(num_out, N); i++) { + // loop till max(N, num_out) + if (i < N) // only add new arrays if i < N + X[i] = std::valarray(features); + if (i < num_out) { // only add new arrays if i < num_out + W[i] = std::valarray(features); + +#ifdef _OPENMP +#pragma omp for +#endif + for (j = 0; j < features; j++) + // preallocate with random initial weights + W[i][j] = _random(-1, 1); + } + } + + test_circle(&X); // create test data around circumference of a circle + save_nd_data("test1.csv", X); // save test data points + save_nd_data("w11.csv", W); // save initial random weights + kohonen_som_tracer(X, &W, 0.1); // train the SOM + save_nd_data("w12.csv", W); // save the resultant weights +} + +/** Creates a random set of points distributed *near* the locus + * of the [Lamniscate of + * Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono) and trains an SOM + * that finds that circular pattern. + * \param[out] data matrix to store data in + */ +void test_lamniscate(std::vector> *data) { + const int N = data->size(); + const double dr = 0.2; + int i; + +#ifdef _OPENMP +#pragma omp for +#endif + for (i = 0; i < N; i++) { + double dx = _random(-dr, dr); // random change in x + double dy = _random(-dr, dr); // random change in y + double theta = _random(0, M_PI); // random theta + data[0][i][0] = dx + cos(theta); // convert from polar to cartesian + data[0][i][1] = dy + sin(2. * theta) / 2.f; + } +} + +/** Test that creates a random set of points distributed *near* the locus + * of the [Lamniscate of + * Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono) and trains an SOM + * that finds that circular pattern. The following + * [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files are created + * to validate the execution: + * * `test2.csv`: random test samples points with a lamniscate pattern + * * `w21.csv`: initial random map + * * `w22.csv`: trained SOM map + * + * The outputs can be readily plotted in [gnuplot](https:://gnuplot.info) using + * the following snippet + * ```gnuplot + * set datafile separator ',' + * plot "test2.csv" title "original", \ + * "w21.csv" title "w1", \ + * "w22.csv" title "w2" + * ``` + * ![Sample execution + * output](https://raw.githubusercontent.com/kvedala/C/docs/images/machine_learning/kohonen/test2.svg) + */ +void test2() { + int j, N = 500; + int features = 2; + int num_out = 20; + std::vector> X(N); + std::vector> W(num_out); + for (int i = 0; i < std::max(num_out, N); i++) { + // loop till max(N, num_out) + if (i < N) // only add new arrays if i < N + X[i] = std::valarray(features); + if (i < num_out) { // only add new arrays if i < num_out + W[i] = std::valarray(features); + +#ifdef _OPENMP +#pragma omp for +#endif + for (j = 0; j < features; j++) + // preallocate with random initial weights + W[i][j] = _random(-1, 1); + } + } + + test_lamniscate(&X); // create test data around the lamniscate + save_nd_data("test2.csv", X); // save test data points + save_nd_data("w21.csv", W); // save initial random weights + kohonen_som_tracer(X, &W, 0.01); // train the SOM + save_nd_data("w22.csv", W); // save the resultant weights +} + +/** Creates a random set of points distributed *near* the locus + * of the [Lamniscate of + * Gerono](https://en.wikipedia.org/wiki/Lemniscate_of_Gerono) and trains an SOM + * that finds that circular pattern. + * \param[out] data matrix to store data in + */ +void test_3d_classes(std::vector> *data) { + const int N = data->size(); + const double R = 0.1; // radius of cluster + int i; + const int num_classes = 8; + const double centres[][3] = { + // centres of each class cluster + {.5, .5, .5}, // centre of class 0 + {.5, .5, -.5}, // centre of class 1 + {.5, -.5, .5}, // centre of class 2 + {.5, -.5, -.5}, // centre of class 3 + {-.5, .5, .5}, // centre of class 4 + {-.5, .5, -.5}, // centre of class 5 + {-.5, -.5, .5}, // centre of class 6 + {-.5, -.5, -.5} // centre of class 7 + }; + +#ifdef _OPENMP +#pragma omp for +#endif + for (i = 0; i < N; i++) { + int cls = + std::rand() % num_classes; // select a random class for the point + + // create random coordinates (x,y,z) around the centre of the class + data[0][i][0] = _random(centres[cls][0] - R, centres[cls][0] + R); + data[0][i][1] = _random(centres[cls][1] - R, centres[cls][1] + R); + data[0][i][2] = _random(centres[cls][2] - R, centres[cls][2] + R); + + /* The follosing can also be used + for (int j = 0; j < 3; j++) + data[0][i][j] = _random(centres[cls][j] - R, centres[cls][j] + R); + */ + } +} + +/** Test that creates a random set of points distributed in six clusters in + * 3D space. The following + * [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) files are created + * to validate the execution: + * * `test3.csv`: random test samples points with a circular pattern + * * `w31.csv`: initial random map + * * `w32.csv`: trained SOM map + * + * The outputs can be readily plotted in [gnuplot](https:://gnuplot.info) using + * the following snippet + * ```gnuplot + * set datafile separator ',' + * plot "test3.csv" title "original", \ + * "w31.csv" title "w1", \ + * "w32.csv" title "w2" + * ``` + * ![Sample execution + * output](https://raw.githubusercontent.com/kvedala/C/docs/images/machine_learning/kohonen/test3.svg) + */ +void test3() { + int j, N = 200; + int features = 3; + int num_out = 20; + std::vector> X(N); + std::vector> W(num_out); + for (int i = 0; i < std::max(num_out, N); i++) { + // loop till max(N, num_out) + if (i < N) // only add new arrays if i < N + X[i] = std::valarray(features); + if (i < num_out) { // only add new arrays if i < num_out + W[i] = std::valarray(features); + +#ifdef _OPENMP +#pragma omp for +#endif + for (j = 0; j < features; j++) + // preallocate with random initial weights + W[i][j] = _random(-1, 1); + } + } + + test_3d_classes(&X); // create test data around the lamniscate + save_nd_data("test3.csv", X); // save test data points + save_nd_data("w31.csv", W); // save initial random weights + kohonen_som_tracer(X, &W, 0.01); // train the SOM + save_nd_data("w32.csv", W); // save the resultant weights +} + +/** Main function */ +int main(int argc, char **argv) { +#ifdef _OPENMP + std::cout << "Using OpenMP based parallelization\n"; +#else + std::cout << "NOT using OpenMP based parallelization\n"; +#endif + + auto start_clk = std::chrono::steady_clock::now(); + test1(); + auto end_clk = std::chrono::steady_clock::now(); + std::chrono::duration time_diff = end_clk - start_clk; + std::cout << "Test 1 completed in " << time_diff.count() << " sec\n"; + + start_clk = std::chrono::steady_clock::now(); + test2(); + end_clk = std::chrono::steady_clock::now(); + time_diff = end_clk - start_clk; + std::cout << "Test 2 completed in " << time_diff.count() << " sec\n"; + + start_clk = std::chrono::steady_clock::now(); + test3(); + end_clk = std::chrono::steady_clock::now(); + time_diff = end_clk - start_clk; + std::cout << "Test 3 completed in " << time_diff.count() << " sec\n"; + + std::cout + << "(Note: Calculated times include: creating test sets, training " + "model and writing files to disk.)\n\n"; + return 0; +} From c6f12f290fea02f0332e7fcc430279d3d1e10748 Mon Sep 17 00:00:00 2001 From: github-actions <${GITHUB_ACTOR}@users.noreply.github.com> Date: Thu, 4 Jun 2020 02:01:35 +0000 Subject: [PATCH 3/6] updating DIRECTORY.md --- DIRECTORY.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DIRECTORY.md b/DIRECTORY.md index 31146ab3d..5507717ef 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -106,6 +106,7 @@ ## Machine Learning * [Adaline Learning](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/adaline_learning.cpp) + * [Kohonen Som Trace](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/machine_learning/kohonen_som_trace.cpp) ## Math * [Binary Exponent](https://github.com/TheAlgorithms/C-Plus-Plus/blob/master/math/binary_exponent.cpp) From aea585284abfc089394f371e070480cbdc1e273e Mon Sep 17 00:00:00 2001 From: Krishna Vedala <7001608+kvedala@users.noreply.github.com> Date: Wed, 3 Jun 2020 22:02:20 -0400 Subject: [PATCH 4/6] remove older files and folders from gh-pages before adding new files --- .github/workflows/gh-pages.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 700d1a3f7..881ea1c33 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,6 +1,6 @@ name: Doxygen CI -on: +on: push: branches: [master] @@ -25,10 +25,12 @@ jobs: clean: false - name: Move & Commit files run: | - cp -rp ./build/html/* . && rm -rf ./build && ls -lah git config --global user.name github-actions git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY - git add * + rm -rf d* && rm *.html && rm *.svg && rm *.map && rm *.md5 && rm *.png && rm *.js && rm *.css + git add . + cp -rp ./build/html/* . && rm -rf ./build && ls -lah + git add . git commit -m "Documentation for $GITHUB_SHA" || true git push --force || true From 1085bf43012ab269fb29141f2b7afbbbe274e81e Mon Sep 17 00:00:00 2001 From: Krishna Vedala <7001608+kvedala@users.noreply.github.com> Date: Wed, 3 Jun 2020 22:13:46 -0400 Subject: [PATCH 5/6] remove chronos library due to inacceptability by cpplint --- machine_learning/kohonen_som_trace.cpp | 36 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/machine_learning/kohonen_som_trace.cpp b/machine_learning/kohonen_som_trace.cpp index fd8c503cd..1c45e7b3f 100644 --- a/machine_learning/kohonen_som_trace.cpp +++ b/machine_learning/kohonen_som_trace.cpp @@ -15,7 +15,6 @@ */ #define _USE_MATH_DEFINES // required for MS Visual C++ #include -#include #include #include #include @@ -395,6 +394,17 @@ void test3() { save_nd_data("w32.csv", W); // save the resultant weights } +/** + * Convert clock cycle difference to time in seconds + * + * \param[in] start_t start clock + * \param[in] end_t end clock + * \returns time difference in seconds + */ +double get_clock_diff(clock_t start_t, clock_t end_t) { + return (double)(end_t - start_t) / (double)CLOCKS_PER_SEC; +} + /** Main function */ int main(int argc, char **argv) { #ifdef _OPENMP @@ -403,23 +413,23 @@ int main(int argc, char **argv) { std::cout << "NOT using OpenMP based parallelization\n"; #endif - auto start_clk = std::chrono::steady_clock::now(); + std::clock_t start_clk = std::clock(); test1(); - auto end_clk = std::chrono::steady_clock::now(); - std::chrono::duration time_diff = end_clk - start_clk; - std::cout << "Test 1 completed in " << time_diff.count() << " sec\n"; + auto end_clk = std::clock(); + std::cout << "Test 1 completed in " << get_clock_diff(start_clk, end_clk) + << " sec\n"; - start_clk = std::chrono::steady_clock::now(); + start_clk = std::clock(); test2(); - end_clk = std::chrono::steady_clock::now(); - time_diff = end_clk - start_clk; - std::cout << "Test 2 completed in " << time_diff.count() << " sec\n"; + end_clk = std::clock(); + std::cout << "Test 2 completed in " << get_clock_diff(start_clk, end_clk) + << " sec\n"; - start_clk = std::chrono::steady_clock::now(); + start_clk = std::clock(); test3(); - end_clk = std::chrono::steady_clock::now(); - time_diff = end_clk - start_clk; - std::cout << "Test 3 completed in " << time_diff.count() << " sec\n"; + end_clk = std::clock(); + std::cout << "Test 3 completed in " << get_clock_diff(start_clk, end_clk) + << " sec\n"; std::cout << "(Note: Calculated times include: creating test sets, training " From c8a2b1d5b20300e34686a65ffd70919bc660cc5f Mon Sep 17 00:00:00 2001 From: Krishna Vedala <7001608+kvedala@users.noreply.github.com> Date: Wed, 3 Jun 2020 22:16:14 -0400 Subject: [PATCH 6/6] use c++ specific static_cast instead --- machine_learning/kohonen_som_trace.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/machine_learning/kohonen_som_trace.cpp b/machine_learning/kohonen_som_trace.cpp index 1c45e7b3f..61d3245ec 100644 --- a/machine_learning/kohonen_som_trace.cpp +++ b/machine_learning/kohonen_som_trace.cpp @@ -402,7 +402,7 @@ void test3() { * \returns time difference in seconds */ double get_clock_diff(clock_t start_t, clock_t end_t) { - return (double)(end_t - start_t) / (double)CLOCKS_PER_SEC; + return static_cast(end_t - start_t) / CLOCKS_PER_SEC; } /** Main function */