TheAlgorithms/C++ 1.0.0
All the algorithms implemented in C++
Loading...
Searching...
No Matches
kohonen_som_topology.cpp
Go to the documentation of this file.
1
26#define _USE_MATH_DEFINES //< required for MS Visual C++
27#include <algorithm>
28#include <array>
29#include <cerrno>
30#include <cmath>
31#include <cstdlib>
32#include <cstring>
33#include <ctime>
34#include <fstream>
35#include <iostream>
36#include <valarray>
37#include <vector>
38#ifdef _OPENMP // check if OpenMP based parallellization is available
39#include <omp.h>
40#endif
41
53double _random(double a, double b) {
54 return ((b - a) * (std::rand() % 100) / 100.f) + a;
55}
56
65int save_2d_data(const char *fname,
66 const std::vector<std::valarray<double>> &X) {
67 size_t num_points = X.size(); // number of rows
68 size_t num_features = X[0].size(); // number of columns
69
70 std::ofstream fp;
71 fp.open(fname);
72 if (!fp.is_open()) {
73 // error with opening file to write
74 std::cerr << "Error opening file " << fname << ": "
75 << std::strerror(errno) << "\n";
76 return -1;
77 }
78
79 // for each point in the array
80 for (int i = 0; i < num_points; i++) {
81 // for each feature in the array
82 for (int j = 0; j < num_features; j++) {
83 fp << X[i][j]; // print the feature value
84 if (j < num_features - 1) { // if not the last feature
85 fp << ","; // suffix comma
86 }
87 }
88 if (i < num_points - 1) { // if not the last row
89 fp << "\n"; // start a new line
90 }
91 }
92
93 fp.close();
94 return 0;
95}
96
105void get_min_2d(const std::vector<std::valarray<double>> &X, double *val,
106 int *x_idx, int *y_idx) {
107 val[0] = INFINITY; // initial min value
108 size_t N = X.size();
109
110 for (int i = 0; i < N; i++) { // traverse each x-index
111 auto result = std::min_element(std::begin(X[i]), std::end(X[i]));
112 double d_min = *result;
113 std::ptrdiff_t j = std::distance(std::begin(X[i]), result);
114
115 if (d_min < val[0]) { // if a lower value is found
116 // save the value and its index
117 x_idx[0] = i;
118 y_idx[0] = j;
119 val[0] = d_min;
120 }
121 }
122}
123
127namespace machine_learning {
129constexpr double MIN_DISTANCE = 1e-4;
130
142int save_u_matrix(const char *fname,
143 const std::vector<std::vector<std::valarray<double>>> &W) {
144 std::ofstream fp(fname);
145 if (!fp) { // error with fopen
146 std::cerr << "File error (" << fname << "): " << std::strerror(errno)
147 << std::endl;
148 return -1;
149 }
150
151 // neighborhood range
152 unsigned int R = 1;
153
154 for (int i = 0; i < W.size(); i++) { // for each x
155 for (int j = 0; j < W[0].size(); j++) { // for each y
156 double distance = 0.f;
157
158 int from_x = std::max<int>(0, i - R);
159 int to_x = std::min<int>(W.size(), i + R + 1);
160 int from_y = std::max<int>(0, j - R);
161 int to_y = std::min<int>(W[0].size(), j + R + 1);
162 int l = 0, m = 0;
163#ifdef _OPENMP
164#pragma omp parallel for reduction(+ : distance)
165#endif
166 for (l = from_x; l < to_x; l++) { // scan neighborhoor in x
167 for (m = from_y; m < to_y; m++) { // scan neighborhood in y
168 auto d = W[i][j] - W[l][m];
169 double d2 = std::pow(d, 2).sum();
170 distance += std::sqrt(d2);
171 // distance += d2;
172 }
173 }
174
175 distance /= R * R; // mean distance from neighbors
176 fp << distance; // print the mean separation
177 if (j < W[0].size() - 1) { // if not the last column
178 fp << ','; // suffix comma
179 }
180 }
181 if (i < W.size() - 1) { // if not the last row
182 fp << '\n'; // start a new line
183 }
184 }
185
186 fp.close();
187 return 0;
188}
189
200double update_weights(const std::valarray<double> &X,
201 std::vector<std::vector<std::valarray<double>>> *W,
202 std::vector<std::valarray<double>> *D, double alpha,
203 int R) {
204 int x = 0, y = 0;
205 int num_out_x = static_cast<int>(W->size()); // output nodes - in X
206 int num_out_y = static_cast<int>(W[0][0].size()); // output nodes - in Y
207 // int num_features = static_cast<int>(W[0][0][0].size()); // features =
208 // in Z
209 double d_min = 0.f;
210
211#ifdef _OPENMP
212#pragma omp for
213#endif
214 // step 1: for each output point
215 for (x = 0; x < num_out_x; x++) {
216 for (y = 0; y < num_out_y; y++) {
217 (*D)[x][y] = 0.f;
218 // compute Euclidian distance of each output
219 // point from the current sample
220 auto d = ((*W)[x][y] - X);
221 (*D)[x][y] = (d * d).sum();
222 (*D)[x][y] = std::sqrt((*D)[x][y]);
223 }
224 }
225
226 // step 2: get closest node i.e., node with snallest Euclidian distance
227 // to the current pattern
228 int d_min_x = 0, d_min_y = 0;
229 get_min_2d(*D, &d_min, &d_min_x, &d_min_y);
230
231 // step 3a: get the neighborhood range
232 int from_x = std::max(0, d_min_x - R);
233 int to_x = std::min(num_out_x, d_min_x + R + 1);
234 int from_y = std::max(0, d_min_y - R);
235 int to_y = std::min(num_out_y, d_min_y + R + 1);
236
237 // step 3b: update the weights of nodes in the
238 // neighborhood
239#ifdef _OPENMP
240#pragma omp for
241#endif
242 for (x = from_x; x < to_x; x++) {
243 for (y = from_y; y < to_y; y++) {
244 /* you can enable the following normalization if needed.
245 personally, I found it detrimental to convergence */
246 // const double s2pi = sqrt(2.f * M_PI);
247 // double normalize = 1.f / (alpha * s2pi);
248
249 /* apply scaling inversely proportional to distance from the
250 current node */
251 double d2 =
252 (d_min_x - x) * (d_min_x - x) + (d_min_y - y) * (d_min_y - y);
253 double scale_factor = std::exp(-d2 / (2.f * alpha * alpha));
254
255 (*W)[x][y] += (X - (*W)[x][y]) * alpha * scale_factor;
256 }
257 }
258 return d_min;
259}
260
269void kohonen_som(const std::vector<std::valarray<double>> &X,
270 std::vector<std::vector<std::valarray<double>>> *W,
271 double alpha_min) {
272 size_t num_samples = X.size(); // number of rows
273 // size_t num_features = X[0].size(); // number of columns
274 size_t num_out = W->size(); // output matrix size
275 size_t R = num_out >> 2, iter = 0;
276 double alpha = 1.f;
277
278 std::vector<std::valarray<double>> D(num_out);
279 for (int i = 0; i < num_out; i++) D[i] = std::valarray<double>(num_out);
280
281 double dmin = 1.f; // average minimum distance of all samples
282 double past_dmin = 1.f; // average minimum distance of all samples
283 double dmin_ratio = 1.f; // change per step
284
285 // Loop alpha from 1 to slpha_min
286 for (; alpha > 0 && dmin_ratio > 1e-5; alpha -= 1e-4, iter++) {
287 // Loop for each sample pattern in the data set
288 for (int sample = 0; sample < num_samples; sample++) {
289 // update weights for the current input pattern sample
290 dmin += update_weights(X[sample], W, &D, alpha, R);
291 }
292
293 // every 100th iteration, reduce the neighborhood range
294 if (iter % 300 == 0 && R > 1) {
295 R--;
296 }
297
298 dmin /= num_samples;
299
300 // termination condition variable -> % change in minimum distance
301 dmin_ratio = (past_dmin - dmin) / past_dmin;
302 if (dmin_ratio < 0) {
303 dmin_ratio = 1.f;
304 }
305 past_dmin = dmin;
306
307 std::cout << "iter: " << iter << "\t alpha: " << alpha << "\t R: " << R
308 << "\t d_min: " << dmin_ratio << "\r";
309 }
310
311 std::cout << "\n";
312}
313
314} // namespace machine_learning
315
318
330void test_2d_classes(std::vector<std::valarray<double>> *data) {
331 const int N = data->size();
332 const double R = 0.3; // radius of cluster
333 int i = 0;
334 const int num_classes = 4;
335 std::array<std::array<double, 2>, num_classes> centres = {
336 // centres of each class cluster
337 std::array<double, 2>({.5, .5}), // centre of class 1
338 std::array<double, 2>({.5, -.5}), // centre of class 2
339 std::array<double, 2>({-.5, .5}), // centre of class 3
340 std::array<double, 2>({-.5, -.5}) // centre of class 4
341 };
342
343#ifdef _OPENMP
344#pragma omp for
345#endif
346 for (i = 0; i < N; i++) {
347 // select a random class for the point
348 int cls = std::rand() % num_classes;
349
350 // create random coordinates (x,y,z) around the centre of the class
351 data[0][i][0] = _random(centres[cls][0] - R, centres[cls][0] + R);
352 data[0][i][1] = _random(centres[cls][1] - R, centres[cls][1] + R);
353
354 /* The follosing can also be used
355 for (int j = 0; j < 2; j++)
356 data[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
357 */
358 }
359}
360
369void test1() {
370 int j = 0, N = 300;
371 int features = 2;
372 int num_out = 30;
373 std::vector<std::valarray<double>> X(N);
374 std::vector<std::vector<std::valarray<double>>> W(num_out);
375 for (int i = 0; i < std::max(num_out, N); i++) {
376 // loop till max(N, num_out)
377 if (i < N) { // only add new arrays if i < N
378 X[i] = std::valarray<double>(features);
379 }
380 if (i < num_out) { // only add new arrays if i < num_out
381 W[i] = std::vector<std::valarray<double>>(num_out);
382 for (int k = 0; k < num_out; k++) {
383 W[i][k] = std::valarray<double>(features);
384#ifdef _OPENMP
385#pragma omp for
386#endif
387 for (j = 0; j < features; j++) {
388 // preallocate with random initial weights
389 W[i][k][j] = _random(-10, 10);
390 }
391 }
392 }
393 }
394
395 test_2d_classes(&X); // create test data around circumference of a circle
396 save_2d_data("test1.csv", X); // save test data points
397 save_u_matrix("w11.csv", W); // save initial random weights
398 kohonen_som(X, &W, 1e-4); // train the SOM
399 save_u_matrix("w12.csv", W); // save the resultant weights
400}
401
411void test_3d_classes1(std::vector<std::valarray<double>> *data) {
412 const size_t N = data->size();
413 const double R = 0.3; // radius of cluster
414 int i = 0;
415 const int num_classes = 4;
416 const std::array<std::array<double, 3>, num_classes> centres = {
417 // centres of each class cluster
418 std::array<double, 3>({.5, .5, .5}), // centre of class 1
419 std::array<double, 3>({.5, -.5, -.5}), // centre of class 2
420 std::array<double, 3>({-.5, .5, .5}), // centre of class 3
421 std::array<double, 3>({-.5, -.5 - .5}) // centre of class 4
422 };
423
424#ifdef _OPENMP
425#pragma omp for
426#endif
427 for (i = 0; i < N; i++) {
428 // select a random class for the point
429 int cls = std::rand() % num_classes;
430
431 // create random coordinates (x,y,z) around the centre of the class
432 data[0][i][0] = _random(centres[cls][0] - R, centres[cls][0] + R);
433 data[0][i][1] = _random(centres[cls][1] - R, centres[cls][1] + R);
434 data[0][i][2] = _random(centres[cls][2] - R, centres[cls][2] + R);
435
436 /* The follosing can also be used
437 for (int j = 0; j < 3; j++)
438 data[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
439 */
440 }
441}
442
451void test2() {
452 int j = 0, N = 300;
453 int features = 3;
454 int num_out = 30;
455 std::vector<std::valarray<double>> X(N);
456 std::vector<std::vector<std::valarray<double>>> W(num_out);
457 for (int i = 0; i < std::max(num_out, N); i++) {
458 // loop till max(N, num_out)
459 if (i < N) { // only add new arrays if i < N
460 X[i] = std::valarray<double>(features);
461 }
462 if (i < num_out) { // only add new arrays if i < num_out
463 W[i] = std::vector<std::valarray<double>>(num_out);
464 for (int k = 0; k < num_out; k++) {
465 W[i][k] = std::valarray<double>(features);
466#ifdef _OPENMP
467#pragma omp for
468#endif
469 for (j = 0; j < features; j++) {
470 // preallocate with random initial weights
471 W[i][k][j] = _random(-10, 10);
472 }
473 }
474 }
475 }
476
477 test_3d_classes1(&X); // create test data around circumference of a circle
478 save_2d_data("test2.csv", X); // save test data points
479 save_u_matrix("w21.csv", W); // save initial random weights
480 kohonen_som(X, &W, 1e-4); // train the SOM
481 save_u_matrix("w22.csv", W); // save the resultant weights
482}
483
493void test_3d_classes2(std::vector<std::valarray<double>> *data) {
494 const size_t N = data->size();
495 const double R = 0.2; // radius of cluster
496 int i = 0;
497 const int num_classes = 8;
498 const std::array<std::array<double, 3>, num_classes> centres = {
499 // centres of each class cluster
500 std::array<double, 3>({.5, .5, .5}), // centre of class 1
501 std::array<double, 3>({.5, .5, -.5}), // centre of class 2
502 std::array<double, 3>({.5, -.5, .5}), // centre of class 3
503 std::array<double, 3>({.5, -.5, -.5}), // centre of class 4
504 std::array<double, 3>({-.5, .5, .5}), // centre of class 5
505 std::array<double, 3>({-.5, .5, -.5}), // centre of class 6
506 std::array<double, 3>({-.5, -.5, .5}), // centre of class 7
507 std::array<double, 3>({-.5, -.5, -.5}) // centre of class 8
508 };
509
510#ifdef _OPENMP
511#pragma omp for
512#endif
513 for (i = 0; i < N; i++) {
514 // select a random class for the point
515 int cls = std::rand() % num_classes;
516
517 // create random coordinates (x,y,z) around the centre of the class
518 data[0][i][0] = _random(centres[cls][0] - R, centres[cls][0] + R);
519 data[0][i][1] = _random(centres[cls][1] - R, centres[cls][1] + R);
520 data[0][i][2] = _random(centres[cls][2] - R, centres[cls][2] + R);
521
522 /* The follosing can also be used
523 for (int j = 0; j < 3; j++)
524 data[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
525 */
526 }
527}
528
537void test3() {
538 int j = 0, N = 500;
539 int features = 3;
540 int num_out = 30;
541 std::vector<std::valarray<double>> X(N);
542 std::vector<std::vector<std::valarray<double>>> W(num_out);
543 for (int i = 0; i < std::max(num_out, N); i++) {
544 // loop till max(N, num_out)
545 if (i < N) { // only add new arrays if i < N
546 X[i] = std::valarray<double>(features);
547 }
548 if (i < num_out) { // only add new arrays if i < num_out
549 W[i] = std::vector<std::valarray<double>>(num_out);
550 for (int k = 0; k < num_out; k++) {
551 W[i][k] = std::valarray<double>(features);
552#ifdef _OPENMP
553#pragma omp for
554#endif
555 for (j = 0; j < features; j++) {
556 // preallocate with random initial weights
557 W[i][k][j] = _random(-10, 10);
558 }
559 }
560 }
561 }
562
563 test_3d_classes2(&X); // create test data around circumference of a circle
564 save_2d_data("test3.csv", X); // save test data points
565 save_u_matrix("w31.csv", W); // save initial random weights
566 kohonen_som(X, &W, 1e-4); // train the SOM
567 save_u_matrix("w32.csv", W); // save the resultant weights
568}
569
577double get_clock_diff(clock_t start_t, clock_t end_t) {
578 return static_cast<double>(end_t - start_t) / CLOCKS_PER_SEC;
579}
580
582int main(int argc, char **argv) {
583#ifdef _OPENMP
584 std::cout << "Using OpenMP based parallelization\n";
585#else
586 std::cout << "NOT using OpenMP based parallelization\n";
587#endif
588
589 std::srand(std::time(nullptr));
590
591 std::clock_t start_clk = std::clock();
592 test1();
593 auto end_clk = std::clock();
594 std::cout << "Test 1 completed in " << get_clock_diff(start_clk, end_clk)
595 << " sec\n";
596
597 start_clk = std::clock();
598 test2();
599 end_clk = std::clock();
600 std::cout << "Test 2 completed in " << get_clock_diff(start_clk, end_clk)
601 << " sec\n";
602
603 start_clk = std::clock();
604 test3();
605 end_clk = std::clock();
606 std::cout << "Test 3 completed in " << get_clock_diff(start_clk, end_clk)
607 << " sec\n";
608
609 std::cout
610 << "(Note: Calculated times include: creating test sets, training "
611 "model and writing files to disk.)\n\n";
612 return 0;
613}
int main()
Main function.
void get_min_2d(const std::vector< std::valarray< double > > &X, double *val, int *x_idx, int *y_idx)
int save_2d_data(const char *fname, const std::vector< std::valarray< double > > &X)
double _random(double a, double b)
int data[MAX]
test data
void test2()
void test_3d_classes1(std::vector< std::valarray< double > > *data)
void test1()
double get_clock_diff(clock_t start_t, clock_t end_t)
void test_2d_classes(std::vector< std::valarray< double > > *data)
void test_3d_classes2(std::vector< std::valarray< double > > *data)
void test3()
A* search algorithm
T sum(const std::vector< std::valarray< T > > &A)
constexpr double MIN_DISTANCE
int save_u_matrix(const char *fname, const std::vector< std::vector< std::valarray< double > > > &W)
void kohonen_som(const std::vector< std::valarray< double > > &X, std::vector< std::vector< std::valarray< double > > > *W, double alpha_min)
double update_weights(const std::valarray< double > &X, std::vector< std::vector< std::valarray< double > > > *W, std::vector< std::valarray< double > > *D, double alpha, int R)