diff --git a/CMakeLists.txt b/CMakeLists.txt index e04a639..a7b9ac7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.25) -project(COSC-4P80-Assignment-3 VERSION 0.0.19) +project(COSC-4P80-Assignment-3 VERSION 0.0.20) include(FetchContent) option(ENABLE_ADDRSAN "Enable the address sanitizer" OFF) diff --git a/build_emscripten.sh b/build_emscripten.sh new file mode 100755 index 0000000..917691a --- /dev/null +++ b/build_emscripten.sh @@ -0,0 +1,5 @@ +#!bash +mkdir cmake-build-emscript +emcmake cmake -DCMAKE_BUILD_TYPE=Release -S ./ -B ./cmake-build-emscript +emmake cmake --build ./cmake-build-emscript -j 32 +scp ./cmake-build-emscript/COSC-4P80-Assignment-3.* administrator@supercomputer:/var/www/tpgc.me/playground/self_organizing_maps/motors_galore/ diff --git a/include/assign3/array.h b/include/assign3/array.h index b3bb69a..63cfaed 100644 --- a/include/assign3/array.h +++ b/include/assign3/array.h @@ -44,7 +44,8 @@ namespace assign3 case shape_t::GRID_OFFSET_WRAP: for (blt::size_t j = 0; j < height; j++) for (blt::size_t i = 0; i < width; i++) - map.emplace_back(dimensions, (j % 2 == 0 ? static_cast(i) : static_cast(i) + 0.5f), j); + map.emplace_back(dimensions, (j % 2 == 0 ? static_cast(i) : static_cast(i) + 0.5f), + static_cast(static_cast(j) * (std::sqrt(3) / 2.0))); break; } } diff --git a/include/assign3/fwdecl.h b/include/assign3/fwdecl.h index 395b93a..ee1e48f 100644 --- a/include/assign3/fwdecl.h +++ b/include/assign3/fwdecl.h @@ -39,13 +39,13 @@ namespace assign3 }; inline std::array shape_names{ - "Grid", - "Edge Wrapped Grid", - "Honey Comb Grid", - "Edge Wrapped Honey Comb" + "Grid", + "Edge Wrapped Grid", + "Honey Comb Grid", + "Edge Wrapped Honey Comb" }; - enum class debug_type + enum class debug_t { DATA_POINT, DISTANCE @@ -55,6 +55,25 @@ namespace assign3 "Distance to Datapoint", "Distance to Neighbours" }; + + enum class init_t + { + COMPLETELY_RANDOM, + RANDOM_DATA, + SAMPLED_DATA + }; + + inline std::array init_names{ + "Completely Random", + "Random Data Based", + "Sample Based" + }; + + inline std::array init_helps{ + "Initializes weights randomly between -1 and 1", + "Find min and max of each data element, then initialize weights between that range", + "Initialize weights based on the input data" + }; } #endif //COSC_4P80_ASSIGNMENT_3_FWDECL_H diff --git a/include/assign3/manager.h b/include/assign3/manager.h index 94ba30d..176c9bc 100644 --- a/include/assign3/manager.h +++ b/include/assign3/manager.h @@ -107,7 +107,8 @@ namespace assign3 } error_plotting.clear(); som = std::make_unique(motor_data.files[currently_selected_network], som_width, som_height, max_epochs, - distance_function.get(), static_cast(selected_som_mode)); + distance_function.get(), static_cast(selected_som_mode), + static_cast(selected_init_type), normalize_init); error_plotting.push_back(som->topological_error(motor_data.files[currently_selected_network])); } @@ -129,12 +130,15 @@ namespace assign3 int currently_selected_network = 0; int selected_som_mode = 0; + int selected_init_type = 0; + bool normalize_init = false; bool debug_mode = false; bool draw_colors = true; bool draw_data_lines = false; bool running = false; int debug_state = 0; int selected_data_point = 0; + int selected_neuron = 0; float requested_activation = 0.5; float at_distance_measurement = 2; diff --git a/include/assign3/neuron.h b/include/assign3/neuron.h index 29c2d80..ef5ab05 100644 --- a/include/assign3/neuron.h +++ b/include/assign3/neuron.h @@ -23,6 +23,7 @@ #include #include "blt/std/types.h" #include +#include namespace assign3 { @@ -34,7 +35,7 @@ namespace assign3 data.resize(dimensions); } - neuron_t& randomize(blt::size_t seed); + neuron_t& randomize(blt::size_t seed, init_t init, bool normalize, const data_file_t& file); neuron_t& update(const std::vector& new_data, Scalar dist, Scalar eta); diff --git a/include/assign3/som.h b/include/assign3/som.h index b1b4fbc..909957b 100644 --- a/include/assign3/som.h +++ b/include/assign3/som.h @@ -29,7 +29,8 @@ namespace assign3 class som_t { public: - som_t(const data_file_t& file, blt::size_t width, blt::size_t height, blt::size_t max_epochs, distance_function_t* dist_func, shape_t shape); + som_t(const data_file_t& file, blt::size_t width, blt::size_t height, blt::size_t max_epochs, distance_function_t* dist_func, + shape_t shape, init_t init, bool normalize); blt::size_t get_closest_neuron(const std::vector& data); diff --git a/src/functions.cpp b/src/functions.cpp index 4e5ae2b..90a592c 100644 --- a/src/functions.cpp +++ b/src/functions.cpp @@ -48,8 +48,8 @@ namespace assign3 Scalar toroidal_euclidean_distance_function_t::distance(blt::span x, blt::span y) const { BLT_ASSERT(x.size() == 2 && y.size() == 2); - Scalar x_diff = x[0] - y[0]; - Scalar y_diff = x[1] - y[1]; + Scalar x_diff = std::abs(x[0] - y[0]); + Scalar y_diff = std::abs(x[1] - y[1]); Scalar x_min = std::min(x_diff, static_cast(width) - x_diff); Scalar y_min = std::min(y_diff, static_cast(height) - y_diff); return std::sqrt(x_min * x_min + y_min * y_min); diff --git a/src/manager.cpp b/src/manager.cpp index 8fa8bff..3848d01 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -144,10 +144,16 @@ namespace assign3 ImGui::SetNextItemOpen(true, ImGuiCond_Appearing); if (ImGui::CollapsingHeader("SOM Settings")) { - ImGui::Text("Network Shape"); + ImGui::SeparatorText("Network Shape"); if (ImGui::ListBox("##NetworkShape", &selected_som_mode, get_selection_string, shape_names.data(), static_cast(shape_names.size()))) regenerate_network(); + ImGui::SeparatorText("Init Type"); + if (ImGui::ListBox("##InitType", &selected_init_type, get_selection_string, init_names.data(), static_cast(init_names.size()))) + regenerate_network(); + ImGui::TextWrapped("Help: %s", init_helps[selected_init_type].c_str()); + ImGui::Separator(); + ImGui::Checkbox("Normalize Init Data", &normalize_init); if (ImGui::InputInt("SOM Width", &som_width) || ImGui::InputInt("SOM Height", &som_height) || ImGui::InputInt("Max Epochs", &max_epochs)) regenerate_network(); @@ -160,14 +166,15 @@ namespace assign3 if (debug_mode) { ImGui::ListBox("##DebugStateSelect", &debug_state, get_selection_string, debug_names.data(), debug_names.size()); - switch (static_cast(debug_state)) + switch (static_cast(debug_state)) { - case debug_type::DATA_POINT: + case debug_t::DATA_POINT: { ImGui::Checkbox("Data Type Color", &draw_colors); ImGui::Checkbox("Data Lines", &draw_data_lines); auto current_data_file = motor_data.files[currently_selected_network]; - std::vector names; + static std::vector names; + names.clear(); for (const auto& [i, v] : blt::enumerate(current_data_file.data_points)) names.push_back("#" + std::to_string(i) + " (" + (v.is_bad ? "Bad)" : "Good)")); ImGui::Text("Select Data Point"); @@ -175,8 +182,15 @@ namespace assign3 static_cast(names.size())); } break; - case debug_type::DISTANCE: - + case debug_t::DISTANCE: + { + static std::vector names; + names.clear(); + for (blt::size_t i = 0; i < som->get_array().get_map().size(); i++) + names.push_back("Neuron " + std::to_string(i)); + ImGui::Text("Select Neuron"); + ImGui::ListBox("##SelectNeuron", &selected_neuron, get_selection_string, names.data(), static_cast(names.size())); + } break; } } @@ -262,9 +276,9 @@ namespace assign3 void renderer_t::draw_debug(const data_file_t& file) { - switch (static_cast(debug_state)) + switch (static_cast(debug_state)) { - case debug_type::DATA_POINT: + case debug_t::DATA_POINT: { std::vector data_positions; std::vector neuron_positions; @@ -300,7 +314,8 @@ namespace assign3 auto scale = topology_function->scale(half, requested_activation); auto ds = topology_function->call(context.neuron.dist(data_point.bins), scale); - neuron_positions.push_back(context.neuron_padded); + if (draw_data_lines) + neuron_positions.push_back(context.neuron_padded); auto& text = fr2d.render_text(std::to_string(ds), 18).setColor(0.2, 0.2, 0.8); auto text_width = text.getAssociatedText().getTextWidth(); auto text_height = text.getAssociatedText().getTextHeight(); @@ -317,9 +332,34 @@ namespace assign3 } } break; - case debug_type::DISTANCE: + case debug_t::DISTANCE: { - + auto closest_type = normalize_data(get_neuron_activations(file)); + auto& selected_neuron_ref = som->get_array().get_map()[selected_neuron]; + static std::vector distances_2d; + static std::vector distances_nd; + distances_2d.clear(); + distances_nd.clear(); + + for (const auto& n : som->get_array().get_map()) + { + distances_2d.push_back(neuron_t::distance(distance_function.get(), selected_neuron_ref, n)); + distances_nd.push_back(selected_neuron_ref.dist(n.get_data())); + } + + draw_som(neuron_render_info_t{}.set_base_pos({370, 145}).set_neuron_scale(120).set_neuron_padding({0, 0}), + [this, &closest_type](render_data_t context) { + auto& text = fr2d.render_text( + "2D: " + std::to_string(distances_2d[context.index]) + "\nND: " + + std::to_string(distances_nd[context.index]), 18).setColor(0.2, 0.2, 0.8); + auto text_width = text.getAssociatedText().getTextWidth(); + text.setPosition(context.neuron_padded - blt::vec2{text_width / 2.0f, 0}).setZIndex(1); + + if (static_cast(selected_neuron) == context.index) + return blt::make_color(0, 0, 1); + auto type = closest_type[context.index]; + return type >= 0 ? blt::make_color(0, type, 0) : blt::make_color(-type, 0, 0); + }); } break; } diff --git a/src/neuron.cpp b/src/neuron.cpp index 661f97b..67c5cf2 100644 --- a/src/neuron.cpp +++ b/src/neuron.cpp @@ -23,12 +23,59 @@ namespace assign3 { - neuron_t& neuron_t::randomize(blt::size_t seed) + neuron_t& neuron_t::randomize(blt::size_t seed, init_t init, bool normalize, const data_file_t& file) { blt::random::random_t rand{seed}; + switch (init) + { + case init_t::COMPLETELY_RANDOM: + for (auto& v : data) + v = static_cast(rand.get_double(-1, 1)); + break; + case init_t::RANDOM_DATA: + { + static std::vector min_values, max_values; + min_values.clear(); + max_values.clear(); + + min_values.resize(data.size()); + max_values.resize(data.size()); + + for (const auto& [min, max] : blt::in_pairs(min_values, max_values)) + { + min = std::numeric_limits::max(); + max = std::numeric_limits::min(); + } + + for (const auto& point : file.data_points) + { + for (const auto& [i, bin] : blt::enumerate(point.bins)) + { + min_values[i] = std::min(min_values[i], bin); + max_values[i] = std::max(max_values[i], bin); + } + } + for (const auto& [i, v] : blt::enumerate(data)) + v = static_cast(rand.get_double(min_values[i], max_values[i])); + } + break; + case init_t::SAMPLED_DATA: + { + auto selected = rand.select(file.data_points); + std::memcpy(data.data(), selected.bins.data(), data.size() * sizeof(Scalar)); + } + break; + } - for (auto& v : data) - v = static_cast(rand.get_double(-1, 1)); + if (normalize) + { + Scalar total = 0; + for (auto v : data) + total += v * v; + Scalar mag = std::sqrt(total); + for (auto& v : data) + v /= mag; + } return *this; } @@ -36,14 +83,14 @@ namespace assign3 // apply the distance based on the update neuron function neuron_t& neuron_t::update(const std::vector& new_data, Scalar dist, Scalar eta) { - static thread_local std::vector diff; - diff.clear(); +// static thread_local std::vector diff; +// diff.clear(); + +// for (auto [v, x] : blt::in_pairs(data, new_data)) +// diff.push_back(x - v); - for (auto [v, x] : blt::in_pairs(data, new_data)) - diff.push_back(x - v); - - for (auto [v, d] : blt::in_pairs(data, diff)) - v += eta * dist * d; + for (auto [v, d] : blt::in_pairs(data, new_data)) + v += eta * dist * (d - v); return *this; } diff --git a/src/som.cpp b/src/som.cpp index f2381ae..b8b0ec4 100644 --- a/src/som.cpp +++ b/src/som.cpp @@ -27,11 +27,11 @@ namespace assign3 { som_t::som_t(const data_file_t& file, blt::size_t width, blt::size_t height, blt::size_t max_epochs, distance_function_t* dist_func, - shape_t shape): + shape_t shape, init_t init, bool normalize): array(file.data_points.begin()->bins.size(), width, height, shape), file(file), max_epochs(max_epochs), dist_func(dist_func) { for (auto& v : array.get_map()) - v.randomize(std::random_device{}()); + v.randomize(std::random_device{}(), init, normalize, file); } void som_t::train_epoch(Scalar initial_learn_rate, topology_function_t* basis_func)