diff --git a/CMakeLists.txt b/CMakeLists.txt index f689a49..e04a639 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.18) +project(COSC-4P80-Assignment-3 VERSION 0.0.19) include(FetchContent) option(ENABLE_ADDRSAN "Enable the address sanitizer" OFF) diff --git a/include/assign3/array.h b/include/assign3/array.h index b1e1479..b3bb69a 100644 --- a/include/assign3/array.h +++ b/include/assign3/array.h @@ -29,12 +29,24 @@ namespace assign3 class array_t { public: - explicit array_t(blt::size_t dimensions, blt::size_t width, blt::size_t height): + explicit array_t(blt::size_t dimensions, blt::size_t width, blt::size_t height, shape_t shape): width(static_cast(width)), height(static_cast(height)) { - for (blt::size_t i = 0; i < width; i++) - for (blt::size_t j = 0; j < height; j++) - map.emplace_back(dimensions, (j % 2 == 0 ? static_cast(i) : static_cast(i) + 0.5f), j); + switch (shape) + { + case shape_t::GRID: + case shape_t::GRID_WRAP: + for (blt::size_t j = 0; j < height; j++) + for (blt::size_t i = 0; i < width; i++) + map.emplace_back(dimensions, i, j); + break; + case shape_t::GRID_OFFSET: + 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); + break; + } } [[nodiscard]] blt::vec2ul from_index(blt::size_t index) const diff --git a/include/assign3/fwdecl.h b/include/assign3/fwdecl.h index f389c7d..395b93a 100644 --- a/include/assign3/fwdecl.h +++ b/include/assign3/fwdecl.h @@ -30,15 +30,19 @@ namespace assign3 inline constexpr blt::i32 RENDER_2D = 0x0; inline constexpr blt::i32 RENDER_3D = 0x1; - enum class shape : blt::i32 + enum class shape_t : blt::i32 { - GRID = RENDER_2D, - GRID_WRAP = RENDER_2D, - GRID_OFFSET = RENDER_2D, - GRID_OFFSET_WRAP = RENDER_2D, - GAUSSIAN_DIST = RENDER_2D, - TOROIDAL = RENDER_3D, - CYLINDER = RENDER_3D + GRID, + GRID_WRAP, + GRID_OFFSET, + GRID_OFFSET_WRAP + }; + + inline std::array shape_names{ + "Grid", + "Edge Wrapped Grid", + "Honey Comb Grid", + "Edge Wrapped Honey Comb" }; enum class debug_type @@ -47,7 +51,7 @@ namespace assign3 DISTANCE }; - inline std::array debug_names { + inline std::array debug_names{ "Distance to Datapoint", "Distance to Neighbours" }; diff --git a/include/assign3/manager.h b/include/assign3/manager.h index a912a09..94ba30d 100644 --- a/include/assign3/manager.h +++ b/include/assign3/manager.h @@ -92,31 +92,43 @@ namespace assign3 void render(); - void update_graphics(); - void regenerate_network() { - som = std::make_unique(motor_data.files[currently_selected_network].normalize(), som_width, som_height, max_epochs); + switch (static_cast(selected_som_mode)) + { + case shape_t::GRID: + case shape_t::GRID_OFFSET: + distance_function = std::make_unique(); + break; + case shape_t::GRID_OFFSET_WRAP: + case shape_t::GRID_WRAP: + distance_function = std::make_unique(som_width, som_height); + break; + } + 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)); + error_plotting.push_back(som->topological_error(motor_data.files[currently_selected_network])); } private: motor_data_t& motor_data; std::unique_ptr som; std::unique_ptr topology_function; + std::unique_ptr distance_function; + + std::vector error_plotting; blt::gfx::font_renderer_t fr2d{}; blt::gfx::batch_renderer_2d br2d; - float draw_width = 0; - float draw_height = 0; - float neuron_scale = 35; - blt::i32 som_width = 5; blt::i32 som_height = 5; - blt::i32 max_epochs = 100; - Scalar initial_learn_rate = 0.1; + blt::i32 max_epochs = 2000; + Scalar initial_learn_rate = 1; int currently_selected_network = 0; + int selected_som_mode = 0; bool debug_mode = false; bool draw_colors = true; bool draw_data_lines = false; diff --git a/include/assign3/neuron.h b/include/assign3/neuron.h index 81b7afc..29c2d80 100644 --- a/include/assign3/neuron.h +++ b/include/assign3/neuron.h @@ -42,17 +42,30 @@ namespace assign3 [[nodiscard]] Scalar dist(const std::vector& X) const; - [[nodiscard]] inline const std::vector& get_data() const + neuron_t& set_activation(Scalar act) + { + activation = act; + return *this; + } + + void activate(Scalar act) + { activation += act; } + + [[nodiscard]] const std::vector& get_data() const { return data; } - [[nodiscard]] inline Scalar get_x() const + [[nodiscard]] Scalar get_x() const { return x_pos; } - [[nodiscard]] inline Scalar get_y() const + [[nodiscard]] Scalar get_y() const { return y_pos; } + [[nodiscard]] Scalar get_activation() const + { return activation; } + private: Scalar x_pos, y_pos; + Scalar activation = 0; std::vector data; }; diff --git a/include/assign3/som.h b/include/assign3/som.h index 30ef605..b1b4fbc 100644 --- a/include/assign3/som.h +++ b/include/assign3/som.h @@ -29,7 +29,7 @@ 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); + 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); blt::size_t get_closest_neuron(const std::vector& data); @@ -39,6 +39,8 @@ namespace assign3 blt::vec2 get_topological_position(const std::vector& data); + Scalar topological_error(const data_file_t& data); + [[nodiscard]] const array_t& get_array() const { return array; } @@ -53,6 +55,7 @@ namespace assign3 data_file_t file; blt::size_t current_epoch = 0; blt::size_t max_epochs; + distance_function_t* dist_func; }; } diff --git a/src/manager.cpp b/src/manager.cpp index 4fe0a4e..8fa8bff 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -97,7 +97,6 @@ namespace assign3 topology_function = std::make_unique(); regenerate_network(); - update_graphics(); } void renderer_t::cleanup() @@ -142,8 +141,13 @@ namespace assign3 ImGui::Checkbox("Run to completion", &running); ImGui::Text("Epoch %ld / %ld", som->get_current_epoch(), som->get_max_epochs()); } + ImGui::SetNextItemOpen(true, ImGuiCond_Appearing); if (ImGui::CollapsingHeader("SOM Settings")) { + ImGui::Text("Network Shape"); + if (ImGui::ListBox("##NetworkShape", &selected_som_mode, get_selection_string, shape_names.data(), + static_cast(shape_names.size()))) + regenerate_network(); if (ImGui::InputInt("SOM Width", &som_width) || ImGui::InputInt("SOM Height", &som_height) || ImGui::InputInt("Max Epochs", &max_epochs)) regenerate_network(); @@ -195,13 +199,22 @@ namespace assign3 ImPlotPoint(som_width, som_height), ImPlotHeatmapFlags_ColMajor); ImPlot::EndPlot(); } + ImPlot::SetNextAxesLimits(0, max_epochs, 0, 1, ImPlotCond_Once); + if (ImPlot::BeginPlot("Error")) + { + ImPlot::PlotLine("##error", error_plotting.data(), static_cast(error_plotting.size())); + ImPlot::EndPlot(); + } } ImGui::End(); if (running) { if (som->get_current_epoch() < som->get_max_epochs()) + { som->train_epoch(initial_learn_rate, topology_function.get()); + error_plotting.push_back(som->topological_error(current_data_file)); + } } @@ -220,24 +233,6 @@ namespace assign3 fr2d.render(); } - void renderer_t::update_graphics() - { - // find the min x / y for the currently drawn som as positions may depend on type. - const auto x_comparator = [](const auto& a, const auto& b) { - return a.get_x() < b.get_x(); - }; - const auto y_comparator = [](const auto& a, const auto& b) { - return a.get_y() < b.get_y(); - }; - const auto& som_neurons = som->get_array().get_map(); - auto min_x = std::min_element(som_neurons.begin(), som_neurons.end(), x_comparator)->get_x(); - auto max_x = std::max_element(som_neurons.begin(), som_neurons.end(), x_comparator)->get_x(); - auto min_y = std::min_element(som_neurons.begin(), som_neurons.end(), y_comparator)->get_y(); - auto max_y = std::max_element(som_neurons.begin(), som_neurons.end(), y_comparator)->get_y(); - draw_width = (max_x - min_x); - draw_height = (max_y - min_y); - } - std::vector renderer_t::get_neuron_activations(const data_file_t& file) { static std::vector closest_type; @@ -248,6 +243,9 @@ namespace assign3 for (auto [i, v] : blt::enumerate(som->get_array().get_map())) { auto half = som->find_closest_neighbour_distance(i) / at_distance_measurement; +// auto sigma = std::sqrt(-(half * half) / (2 * std::log(requested_activation))); +// auto r = 1 / (2 * sigma * sigma); +// auto scale = topology_function->scale(half, requested_activation); for (const auto& data : file.data_points) { diff --git a/src/som.cpp b/src/som.cpp index 558ff16..f2381ae 100644 --- a/src/som.cpp +++ b/src/som.cpp @@ -26,8 +26,9 @@ namespace assign3 { - som_t::som_t(const data_file_t& file, blt::size_t width, blt::size_t height, blt::size_t max_epochs): - array(file.data_points.begin()->bins.size(), width, height), file(file), max_epochs(max_epochs) + 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): + 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{}()); @@ -57,8 +58,7 @@ namespace assign3 { if (i == v0_idx) continue; - toroidal_euclidean_distance_function_t dist_func{static_cast(array.get_width()), static_cast(array.get_height())}; - auto dist = basis_func->call(neuron_t::distance(&dist_func, v0, n), time_ratio * scale); + auto dist = basis_func->call(neuron_t::distance(dist_func, v0, n), time_ratio * scale); n.update(current_data.bins, dist, eta); } } @@ -84,12 +84,11 @@ namespace assign3 Scalar som_t::find_closest_neighbour_distance(blt::size_t v0) { - toroidal_euclidean_distance_function_t dist_func{static_cast(array.get_width()), static_cast(array.get_height())}; Scalar distance_min = std::numeric_limits::max(); for (const auto& [i, n] : blt::enumerate(array.get_map())) { if (i != v0) - distance_min = std::min(distance_min, neuron_t::distance(&dist_func, array.get_map()[v0], n)); + distance_min = std::min(distance_min, neuron_t::distance(dist_func, array.get_map()[v0], n)); } return distance_min; } @@ -132,7 +131,7 @@ namespace assign3 auto n_1 = array.get_map()[ni_1]; auto n_2 = array.get_map()[ni_2]; auto n_3 = array.get_map()[ni_3]; - + auto p_1 = blt::vec2{n_1.get_x(), n_1.get_y()}; auto p_2 = blt::vec2{n_2.get_x(), n_2.get_y()}; auto p_3 = blt::vec2{n_3.get_x(), n_3.get_y()}; @@ -140,5 +139,40 @@ namespace assign3 return (dp1 * p_1) + (dp2 * p_2) + (dp3 * p_3); } + Scalar som_t::topological_error(const data_file_t& data) + { + Scalar total = 0; + std::vector> distances; + + for (const auto& x : data.data_points) + { + distances.clear(); + for (const auto& [i, n] : blt::enumerate(array.get_map())) + distances.emplace_back(i, n.dist(x.bins)); + + std::pair min1 = {0, std::numeric_limits::max()}; + std::pair min2 = {0, std::numeric_limits::max()}; + + for (const auto& elem : distances) + { + if (elem.second < min1.second) + { + min2 = min1; + min1 = elem; + } else if (elem.second < min2.second) + min2 = elem; + } + + // we can assert the neurons are neighbours if the distance between the BMUs and the nearest neighbour are equal. + auto min_distances = neuron_t::distance(dist_func, array.get_map()[min1.first], array.get_map()[min2.first]); + auto neighbour_distances = find_closest_neighbour_distance(min1.first); + + if (!blt::f_equal(min_distances, neighbour_distances)) + total += 1; + } + + return total / static_cast(data.data_points.size()); + } + } \ No newline at end of file