mirror of
https://hub.njuu.cf/TheAlgorithms/C-Plus-Plus.git
synced 2023-10-11 13:05:55 +08:00
8ab9a2ae93
* tidied up code based on error reports by clang-tidy * added doc for activation function
489 lines
16 KiB
C++
489 lines
16 KiB
C++
/**
|
|
* \addtogroup machine_learning Machine Learning Algorithms
|
|
* @{
|
|
* \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.
|
|
*
|
|
* \author [Krishna Vedala](https://github.com/kvedala)
|
|
*
|
|
* \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
|
|
* \see kohonen_som_topology.cpp
|
|
*/
|
|
#define _USE_MATH_DEFINES // required for MS Visual C++
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <valarray>
|
|
#include <vector>
|
|
#ifdef _OPENMP // check if OpenMP based parallellization is available
|
|
#include <omp.h>
|
|
#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<std::valarray<double>> &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;
|
|
}
|
|
|
|
/** \namespace machine_learning
|
|
* \brief Machine learning algorithms
|
|
*/
|
|
namespace machine_learning {
|
|
|
|
/**
|
|
* 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<double> &x,
|
|
std::vector<std::valarray<double>> *W,
|
|
std::valarray<double> *D, double alpha, int R) {
|
|
int j = 0, k = 0;
|
|
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] alpha_min terminal value of alpha
|
|
*/
|
|
void kohonen_som_tracer(const std::vector<std::valarray<double>> &X,
|
|
std::vector<std::valarray<double>> *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<double> D(num_out);
|
|
|
|
// Loop alpha from 1 to slpha_min
|
|
do {
|
|
// 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--;
|
|
}
|
|
|
|
alpha -= 0.01;
|
|
iter++;
|
|
} while (alpha > alpha_min);
|
|
}
|
|
|
|
} // namespace machine_learning
|
|
|
|
/** @} */
|
|
|
|
using machine_learning::kohonen_som_tracer;
|
|
|
|
/** 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*}{
|
|
* r &\in& [1-\delta r, 1+\delta r)\\
|
|
* \theta &\in& [0, 2\pi)\\
|
|
* x &=& r\cos\theta\\
|
|
* y &=& r\sin\theta
|
|
* \f}
|
|
*
|
|
* \param[out] data matrix to store data in
|
|
*/
|
|
void test_circle(std::vector<std::valarray<double>> *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 = 0;
|
|
|
|
#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/TheAlgorithms/C-Plus-Plus/docs/images/machine_learning/kohonen/test1.svg)
|
|
*/
|
|
void test1() {
|
|
int j = 0, N = 500;
|
|
int features = 2;
|
|
int num_out = 50;
|
|
std::vector<std::valarray<double>> X(N);
|
|
std::vector<std::valarray<double>> 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<double>(features);
|
|
}
|
|
if (i < num_out) { // only add new arrays if i < num_out
|
|
W[i] = std::valarray<double>(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).
|
|
* \f{eqnarray*}{
|
|
* \delta r &=& 0.2\\
|
|
* \delta x &\in& [-\delta r, \delta r)\\
|
|
* \delta y &\in& [-\delta r, \delta r)\\
|
|
* \theta &\in& [0, \pi)\\
|
|
* x &=& \delta x + \cos\theta\\
|
|
* y &=& \delta y + \frac{\sin(2\theta)}{2}
|
|
* \f}
|
|
* \param[out] data matrix to store data in
|
|
*/
|
|
void test_lamniscate(std::vector<std::valarray<double>> *data) {
|
|
const int N = data->size();
|
|
const double dr = 0.2;
|
|
int i = 0;
|
|
|
|
#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/TheAlgorithms/C-Plus-Plus/docs/images/machine_learning/kohonen/test2.svg)
|
|
*/
|
|
void test2() {
|
|
int j = 0, N = 500;
|
|
int features = 2;
|
|
int num_out = 20;
|
|
std::vector<std::valarray<double>> X(N);
|
|
std::vector<std::valarray<double>> 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<double>(features);
|
|
}
|
|
if (i < num_out) { // only add new arrays if i < num_out
|
|
W[i] = std::valarray<double>(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 in six clusters in
|
|
* 3D space with centroids at the points
|
|
* * \f${0.5, 0.5, 0.5}\f$
|
|
* * \f${0.5, 0.5, -0.5}\f$
|
|
* * \f${0.5, -0.5, 0.5}\f$
|
|
* * \f${0.5, -0.5, -0.5}\f$
|
|
* * \f${-0.5, 0.5, 0.5}\f$
|
|
* * \f${-0.5, 0.5, -0.5}\f$
|
|
* * \f${-0.5, -0.5, 0.5}\f$
|
|
* * \f${-0.5, -0.5, -0.5}\f$
|
|
*
|
|
* \param[out] data matrix to store data in
|
|
*/
|
|
void test_3d_classes(std::vector<std::valarray<double>> *data) {
|
|
const int N = data->size();
|
|
const double R = 0.1; // radius of cluster
|
|
int i = 0;
|
|
const int num_classes = 8;
|
|
const std::array<const std::array<double, 3>, num_classes> centres = {
|
|
// centres of each class cluster
|
|
std::array<double, 3>({.5, .5, .5}), // centre of class 0
|
|
std::array<double, 3>({.5, .5, -.5}), // centre of class 1
|
|
std::array<double, 3>({.5, -.5, .5}), // centre of class 2
|
|
std::array<double, 3>({.5, -.5, -.5}), // centre of class 3
|
|
std::array<double, 3>({-.5, .5, .5}), // centre of class 4
|
|
std::array<double, 3>({-.5, .5, -.5}), // centre of class 5
|
|
std::array<double, 3>({-.5, -.5, .5}), // centre of class 6
|
|
std::array<double, 3>({-.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/TheAlgorithms/C-Plus-Plus/docs/images/machine_learning/kohonen/test3.svg)
|
|
*/
|
|
void test3() {
|
|
int j = 0, N = 200;
|
|
int features = 3;
|
|
int num_out = 20;
|
|
std::vector<std::valarray<double>> X(N);
|
|
std::vector<std::valarray<double>> 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<double>(features);
|
|
}
|
|
if (i < num_out) { // only add new arrays if i < num_out
|
|
W[i] = std::valarray<double>(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
|
|
}
|
|
|
|
/**
|
|
* 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 static_cast<double>(end_t - start_t) / CLOCKS_PER_SEC;
|
|
}
|
|
|
|
/** 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
|
|
|
|
std::srand(std::time(nullptr));
|
|
|
|
std::clock_t start_clk = std::clock();
|
|
test1();
|
|
auto end_clk = std::clock();
|
|
std::cout << "Test 1 completed in " << get_clock_diff(start_clk, end_clk)
|
|
<< " sec\n";
|
|
|
|
start_clk = std::clock();
|
|
test2();
|
|
end_clk = std::clock();
|
|
std::cout << "Test 2 completed in " << get_clock_diff(start_clk, end_clk)
|
|
<< " sec\n";
|
|
|
|
start_clk = std::clock();
|
|
test3();
|
|
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 "
|
|
"model and writing files to disk.)\n\n";
|
|
return 0;
|
|
}
|