chore: import upstream snapshot with attribution
Awesome CI Workflow / Code Formatter (push) Waiting to run
Awesome CI Workflow / Compile checks (macOS-latest) (push) Blocked by required conditions
Awesome CI Workflow / Compile checks (ubuntu-latest) (push) Blocked by required conditions
Awesome CI Workflow / Compile checks (windows-latest) (push) Blocked by required conditions

This commit is contained in:
wehub-resource-sync
2026-07-13 12:37:28 +08:00
commit 29cfe479ab
432 changed files with 68491 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
# If necessary, use the RELATIVE flag, otherwise each source file may be listed
# with full pathname. RELATIVE may makes it easier to extract an executable name
# automatically.
file( GLOB APP_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.cpp )
# file( GLOB APP_SOURCES ${CMAKE_SOURCE_DIR}/*.c )
# AUX_SOURCE_DIRECTORY(${CMAKE_CURRENT_SOURCE_DIR} APP_SOURCES)
foreach( testsourcefile ${APP_SOURCES} )
# I used a simple string replace, to cut off .cpp.
string( REPLACE ".cpp" "" testname ${testsourcefile} )
add_executable( ${testname} ${testsourcefile} )
set_target_properties(${testname} PROPERTIES LINKER_LANGUAGE CXX)
if(OpenMP_CXX_FOUND)
target_link_libraries(${testname} OpenMP::OpenMP_CXX)
endif()
install(TARGETS ${testname} DESTINATION "bin/machine_learning")
endforeach( testsourcefile ${APP_SOURCES} )
+712
View File
@@ -0,0 +1,712 @@
/**
* @brief
* [A* search algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm)
* @details
* A* is an informed search algorithm, or a best-first search, meaning that it
* is formulated in terms of weighted graphs: starting from a specific starting
* node of a graph (initial state), it aims to find a path to the given goal
* node having the smallest cost (least distance travelled, shortest time,
* etc.). It evaluates by maintaining a tree of paths originating at the start
* node and extending those paths one edge at a time until it reaches the final
* state.
* The weighted edges (or cost) is evaluated on two factors, G score
* (cost required from starting node or initial state to current state) and H
* score (cost required from current state to final state). The F(state), then
* is evaluated as:
* F(state) = G(state) + H(state).
*
* To solve the given search with shortest cost or path possible is to inspect
* values having minimum F(state).
* @author [Ashish Daulatabad](https://github.com/AshishYUO)
*/
#include <algorithm> /// for `std::reverse` function
#include <array> /// for `std::array`, representing `EightPuzzle` board
#include <cassert> /// for `assert`
#include <cstdint> /// for `std::uint32_t`
#include <functional> /// for `std::function` STL
#include <iostream> /// for IO operations
#include <map> /// for `std::map` STL
#include <memory> /// for `std::shared_ptr`
#include <set> /// for `std::set` STL
#include <vector> /// for `std::vector` STL
/**
* @namespace machine_learning
* @brief Machine learning algorithms
*/
namespace machine_learning {
/**
* @namespace aystar_search
* @brief Functions for [A*
* Search](https://en.wikipedia.org/wiki/A*_search_algorithm) implementation.
*/
namespace aystar_search {
/**
* @class EightPuzzle
* @brief A class defining [EightPuzzle/15-Puzzle
* game](https://en.wikipedia.org/wiki/15_puzzle).
* @details
* A well known 3 x 3 puzzle of the form
* `
* 1 2 3
* 4 5 6
* 7 8 0
* `
* where `0` represents an empty space in the puzzle
* Given any random state, the goal is to achieve the above configuration
* (or any other configuration if possible)
* @tparam N size of the square Puzzle, default is set to 3 (since it is
* EightPuzzle)
*/
template <size_t N = 3>
class EightPuzzle {
std::array<std::array<uint32_t, N>, N>
board; /// N x N array to store the current state of the Puzzle.
std::vector<std::pair<int8_t, int8_t>> moves = {
{0, 1},
{1, 0},
{0, -1},
{-1,
0}}; /// A helper array to evaluate the next state from current state;
/**
* @brief Finds an empty space in puzzle (in this case; a zero)
* @returns a pair indicating integer distances from top and right
* respectively, else returns -1, -1
*/
std::pair<uint32_t, uint32_t> find_zero() {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
if (!board[i][j]) {
return {i, j};
}
}
}
return {-1, -1};
}
/**
* @brief check whether the index value is bounded within the puzzle area
* @param value index for the current board
* @returns `true` if index is within the board, else `false`
*/
inline bool in_range(const uint32_t value) const { return value < N; }
public:
/**
* @brief get the value from i units from right and j units from left side
* of the board
* @param i integer denoting ith row
* @param j integer denoting column
* @returns non-negative integer denoting the value at ith row and jth
* column
* @returns -1 if invalid i or j position
*/
uint32_t get(size_t i, size_t j) const {
if (in_range(i) && in_range(j)) {
return board[i][j];
}
return -1;
}
/**
* @brief Returns the current state of the board
*/
std::array<std::array<uint32_t, N>, N> get_state() { return board; }
/**
* @brief returns the size of the EightPuzzle (number of row / column)
* @return N, the size of the puzzle.
*/
inline size_t get_size() const { return N; }
/**
* @brief Default constructor for EightPuzzle
*/
EightPuzzle() {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
board[i][j] = ((i * 3 + j + 1) % (N * N));
}
}
}
/**
* @brief Parameterized Constructor for EightPuzzle
* @param init a 2-dimensional array denoting a puzzle configuration
*/
explicit EightPuzzle(const std::array<std::array<uint32_t, N>, N> &init)
: board(init) {}
/**
* @brief Copy constructor
* @param A a reference of an EightPuzzle
*/
EightPuzzle(const EightPuzzle<N> &A) : board(A.board) {}
/**
* @brief Move constructor
* @param A a reference of an EightPuzzle
*/
EightPuzzle(const EightPuzzle<N> &&A) noexcept
: board(std::move(A.board)) {}
/**
* @brief Destructor of EightPuzzle
*/
~EightPuzzle() = default;
/**
* @brief Copy assignment operator
* @param A a reference of an EightPuzzle
*/
EightPuzzle &operator=(const EightPuzzle &A) {
board = A.board;
return *this;
}
/**
* @brief Move assignment operator
* @param A a reference of an EightPuzzle
*/
EightPuzzle &operator=(EightPuzzle &&A) noexcept {
board = std::move(A.board);
return *this;
}
/**
* @brief Find all possible states after processing all possible
* moves, given the current state of the puzzle
* @returns list of vector containing all possible next moves
* @note the implementation is compulsory to create A* search
*/
std::vector<EightPuzzle<N>> generate_possible_moves() {
auto zero_pos = find_zero();
// vector which will contain all possible state from current state
std::vector<EightPuzzle<N>> NewStates;
for (auto &move : moves) {
if (in_range(zero_pos.first + move.first) &&
in_range(zero_pos.second + move.second)) {
// swap with the possible moves
std::array<std::array<uint32_t, N>, N> new_config = board;
std::swap(new_config[zero_pos.first][zero_pos.second],
new_config[zero_pos.first + move.first]
[zero_pos.second + move.second]);
EightPuzzle<N> new_state(new_config);
// Store new state and calculate heuristic value, and depth
NewStates.emplace_back(new_state);
}
}
return NewStates;
}
/**
* @brief check whether two boards are equal
* @returns `true` if check.state is equal to `this->state`, else
* `false`
*/
bool operator==(const EightPuzzle<N> &check) const {
if (check.get_size() != N) {
return false;
}
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
if (board[i][j] != check.board[i][j]) {
return false;
}
}
}
return true;
}
/**
* @brief check whether one board is lexicographically smaller
* @returns `true` if this->state is lexicographically smaller than
* `check.state`, else `false`
*/
bool operator<(const EightPuzzle<N> &check) const {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
if (board[i][j] != check.board[i][j]) {
return board[i][j] < check.board[i][j];
}
}
}
return false;
}
/**
* @brief check whether one board is lexicographically smaller or equal
* @returns `true` if this->state is lexicographically smaller than
* `check.state` or same, else `false`
*/
bool operator<=(const EightPuzzle<N> &check) const {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
if (board[i][j] != check.board[i][j]) {
return board[i][j] < check.board[i][j];
}
}
}
return true;
}
/**
* @brief friend operator to display EightPuzzle<>
* @param op ostream object
* @param SomeState a certain state.
* @returns ostream operator op
*/
friend std::ostream &operator<<(std::ostream &op,
const EightPuzzle<N> &SomeState) {
for (size_t i = 0; i < N; ++i) {
for (size_t j = 0; j < N; ++j) {
op << SomeState.board[i][j] << " ";
}
op << "\n";
}
return op;
}
};
/**
* @class AyStarSearch
* @brief A class defining [A* search
* algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm). for some
* initial state and final state
* @details AyStarSearch class is defined as the informed search algorithm
* that is formulated in terms of weighted graphs: starting from a specific
* starting node of a graph (initial state), it aims to find a path to the given
* goal node having the smallest cost (least distance travelled, shortest time,
* etc.)
* The weighted edges (or cost) is evaluated on two factors, G score
* (cost required from starting node or initial state to current state) and H
* score (cost required from current state to final state). The `F(state)`, then
* is evaluated as:
* `F(state) = G(state) + H(state)`.
* The best search would be the final state having minimum `F(state)` value
* @tparam Puzzle denotes the puzzle or problem involving initial state and
* final state to be solved by A* search.
* @note 1. The algorithm is referred from pesudocode from
* [Wikipedia page](https://en.wikipedia.org/wiki/A*_search_algorithm)
* as is.
* 2. For `AyStarSearch` to work, the definitions for template Puzzle is
* compulsory.
* a. Comparison operator for template Puzzle (`<`, `==`, and `<=`)
* b. `generate_possible_moves()`
*/
template <typename Puzzle>
class AyStarSearch {
/**
* @brief Struct that handles all the information related to the current
* state.
*/
typedef struct Info {
std::shared_ptr<Puzzle> state; /// Holds the current state.
size_t heuristic_value = 0; /// stores h score
size_t depth = 0; /// stores g score
/**
* @brief Default constructor
*/
Info() = default;
/**
* @brief constructor having Puzzle as parameter
* @param A a puzzle object
*/
explicit Info(const Puzzle &A) : state(std::make_shared<Puzzle>(A)) {}
/**
* @brief constructor having three parameters
* @param A a puzzle object
* @param h_value heuristic value of this puzzle object
* @param depth the depth at which this node was found during traversal
*/
Info(const Puzzle &A, size_t h_value, size_t d)
: state(std::make_shared<Puzzle>(A)),
heuristic_value(h_value),
depth(d) {}
/**
* @brief Copy constructor
* @param A Info object reference
*/
Info(const Info &A)
: state(std::make_shared<Puzzle>(A.state)),
heuristic_value(A.heuristic_value),
depth(A.depth) {}
/**
* @brief Move constructor
* @param A Info object reference
*/
Info(const Info &&A) noexcept
: state(std::make_shared<Puzzle>(std::move(A.state))),
heuristic_value(std::move(A.heuristic_value)),
depth(std::move(A.depth)) {}
/**
* @brief copy assignment operator
* @param A Info object reference
*/
Info &operator=(const Info &A) {
state = A.state;
heuristic_value = A.heuristic_value;
depth = A.depth;
return *this;
}
/**
* @brief move assignment operator
* @param A Info object reference
*/
Info &operator=(Info &&A) noexcept {
state = std::move(A.state);
heuristic_value = std::move(A.heuristic_value);
depth = std::move(A.depth);
return *this;
}
/**
* @brief Destructor for Info
*/
~Info() = default;
} Info;
std::shared_ptr<Info> Initial; // Initial state of the AyStarSearch
std::shared_ptr<Info> Final; // Final state of the AyStarSearch
/**
* @brief Custom comparator for open_list
*/
struct comparison_operator {
bool operator()(const std::shared_ptr<Info> &a,
const std::shared_ptr<Info> &b) const {
return *(a->state) < *(b->state);
}
};
public:
using MapOfPuzzleInfoWithPuzzleInfo =
std::map<std::shared_ptr<Info>, std::shared_ptr<Info>,
comparison_operator>;
using MapOfPuzzleInfoWithInteger =
std::map<std::shared_ptr<Info>, uint32_t, comparison_operator>;
using SetOfPuzzleInfo =
std::set<std::shared_ptr<Info>, comparison_operator>;
/**
* @brief Parameterized constructor for AyStarSearch
* @param initial denoting initial state of the puzzle
* @param final denoting final state of the puzzle
*/
AyStarSearch(const Puzzle &initial, const Puzzle &final) {
Initial = std::make_shared<Info>(initial);
Final = std::make_shared<Info>(final);
}
/**
* @brief A helper solution: launches when a solution for AyStarSearch
* is found
* @param FinalState the pointer to the obtained final state
* @param parent_of the list of all parents of nodes stored during A*
* search
* @returns the list of moves denoting moves from final state to initial
* state (in reverse)
*/
std::vector<Puzzle> Solution(
std::shared_ptr<Info> FinalState,
const MapOfPuzzleInfoWithPuzzleInfo &parent_of) {
// Useful for traversing from final state to current state.
auto current_state = FinalState;
/*
* For storing the solution tree starting from initial state to
* final state
*/
std::vector<Puzzle> answer;
while (current_state != nullptr) {
answer.emplace_back(*current_state->state);
current_state = parent_of.find(current_state)->second;
}
return answer;
}
/**
* Main algorithm for finding `FinalState`, given the `InitialState`
* @param dist the heuristic finction, defined by the user
* @param permissible_depth the depth at which the A* search discards
* searching for solution
* @returns List of moves from Final state to initial state, if
* evaluated, else returns an empty array
*/
std::vector<Puzzle> a_star_search(
const std::function<uint32_t(const Puzzle &, const Puzzle &)> &dist,
const uint32_t permissible_depth = 30) {
MapOfPuzzleInfoWithPuzzleInfo
parent_of; /// Stores the parent of the states
MapOfPuzzleInfoWithInteger g_score; /// Stores the g_score
SetOfPuzzleInfo open_list; /// Stores the list to explore
SetOfPuzzleInfo closed_list; /// Stores the list that are explored
// Before starting the AyStartSearch, initialize the set and maps
open_list.emplace(Initial);
parent_of[Initial] = nullptr;
g_score[Initial] = 0;
while (!open_list.empty()) {
// Iterator for state having having lowest f_score.
typename SetOfPuzzleInfo::iterator it_low_f_score;
uint32_t min_f_score = 1e9;
for (auto iter = open_list.begin(); iter != open_list.end();
++iter) {
// f score here is evaluated by g score (depth) and h score
// (distance between current state and final state)
uint32_t f_score = (*iter)->heuristic_value + (*iter)->depth;
if (f_score < min_f_score) {
min_f_score = f_score;
it_low_f_score = iter;
}
}
// current_state, stores lowest f score so far for this state.
std::shared_ptr<Info> current_state = *it_low_f_score;
// if this current state is equal to final, return
if (*(current_state->state) == *(Final->state)) {
return Solution(current_state, parent_of);
}
// else remove from open list as visited.
open_list.erase(it_low_f_score);
// if current_state has exceeded the allowed depth, skip
// neighbor checking
if (current_state->depth >= permissible_depth) {
continue;
}
// Generate all possible moves (neighbors) given the current
// state
std::vector<Puzzle> total_possible_moves =
current_state->state->generate_possible_moves();
for (Puzzle &neighbor : total_possible_moves) {
// calculate score of neighbors with respect to
// current_state
std::shared_ptr<Info> Neighbor = std::make_shared<Info>(
neighbor, dist(neighbor, *(Final->state)),
current_state->depth + 1U);
uint32_t temp_g_score = Neighbor->depth;
// Check whether this state is explored.
// If this state is discovered at greater depth, then discard,
// else remove from closed list and explore the node
auto closed_list_iter = closed_list.find(Neighbor);
if (closed_list_iter != closed_list.end()) {
// 1. If state in closed list has higher depth, then remove
// from list since we have found better option,
// 2. Else don't explore this state.
if (Neighbor->depth < (*closed_list_iter)->depth) {
closed_list.erase(closed_list_iter);
} else {
continue;
}
}
auto neighbor_g_score_iter = g_score.find(Neighbor);
// if the neighbor is already created and has minimum
// g_score, then update g_score and f_score else insert new
if (neighbor_g_score_iter != g_score.end()) {
if (neighbor_g_score_iter->second > temp_g_score) {
neighbor_g_score_iter->second = temp_g_score;
parent_of[Neighbor] = current_state;
}
} else {
g_score[Neighbor] = temp_g_score;
parent_of[Neighbor] = current_state;
}
// If this is a new state, insert into open_list
// else update if the this state has better g score than
// existing one.
auto iter = open_list.find(Neighbor);
if (iter == open_list.end()) {
open_list.emplace(Neighbor);
} else if ((*iter)->depth > Neighbor->depth) {
(*iter)->depth = Neighbor->depth;
}
}
closed_list.emplace(current_state);
}
// Cannot find the solution, return empty vector
return std::vector<Puzzle>(0);
}
};
} // namespace aystar_search
} // namespace machine_learning
/**
* @brief Self test-implementations
* @returns void
*/
static void test() {
// Renaming for simplicity
using matrix3 = std::array<std::array<uint32_t, 3>, 3>;
using row3 = std::array<uint32_t, 3>;
using matrix4 = std::array<std::array<uint32_t, 4>, 4>;
using row4 = std::array<uint32_t, 4>;
// 1st test: A* search for simple EightPuzzle problem
matrix3 puzzle;
puzzle[0] = row3({0, 2, 3});
puzzle[1] = row3({1, 5, 6});
puzzle[2] = row3({4, 7, 8});
matrix3 ideal;
ideal[0] = row3({1, 2, 3});
ideal[1] = row3({4, 5, 6});
ideal[2] = row3({7, 8, 0});
/*
* Heuristic function: Manhattan distance
*/
auto manhattan_distance =
[](const machine_learning::aystar_search::EightPuzzle<> &first,
const machine_learning::aystar_search::EightPuzzle<> &second) {
uint32_t ret = 0;
for (size_t i = 0; i < first.get_size(); ++i) {
for (size_t j = 0; j < first.get_size(); ++j) {
uint32_t find = first.get(i, j);
size_t m = first.get_size(), n = first.get_size();
for (size_t k = 0; k < second.get_size(); ++k) {
for (size_t l = 0; l < second.get_size(); ++l) {
if (find == second.get(k, l)) {
std::tie(m, n) = std::make_pair(k, l);
break;
}
}
if (m != first.get_size()) {
break;
}
}
if (m != first.get_size()) {
ret += (std::max(m, i) - std::min(m, i)) +
(std::max(n, j) - std::min(n, j));
}
}
}
return ret;
};
machine_learning::aystar_search::EightPuzzle<> Puzzle(puzzle);
machine_learning::aystar_search::EightPuzzle<> Ideal(ideal);
machine_learning::aystar_search::AyStarSearch<
machine_learning::aystar_search::EightPuzzle<3>>
search(Puzzle, Ideal); /// Search object
std::vector<matrix3> answer; /// Array that validates the answer
answer.push_back(
matrix3({row3({0, 2, 3}), row3({1, 5, 6}), row3({4, 7, 8})}));
answer.push_back(
matrix3({row3({1, 2, 3}), row3({0, 5, 6}), row3({4, 7, 8})}));
answer.push_back(
matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({0, 7, 8})}));
answer.push_back(
matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({7, 0, 8})}));
answer.push_back(
matrix3({row3({1, 2, 3}), row3({4, 5, 6}), row3({7, 8, 0})}));
auto Solution = search.a_star_search(manhattan_distance);
std::cout << Solution.size() << std::endl;
assert(Solution.size() == answer.size());
uint32_t i = 0;
for (auto it = Solution.rbegin(); it != Solution.rend(); ++it) {
assert(it->get_state() == answer[i]);
++i;
}
// 2nd test: A* search for complicated EightPuzzle problem
// Initial state
puzzle[0] = row3({5, 7, 3});
puzzle[1] = row3({2, 0, 6});
puzzle[2] = row3({1, 4, 8});
// Final state
ideal[0] = row3({1, 2, 3});
ideal[1] = row3({4, 5, 6});
ideal[2] = row3({7, 8, 0});
Puzzle = machine_learning::aystar_search::EightPuzzle<>(puzzle);
Ideal = machine_learning::aystar_search::EightPuzzle<>(ideal);
// Initialize the search object
search = machine_learning::aystar_search::AyStarSearch<
machine_learning::aystar_search::EightPuzzle<3>>(Puzzle, Ideal);
Solution = search.a_star_search(manhattan_distance);
std::cout << Solution.size() << std::endl;
// Static assertion due to large solution
assert(13 == Solution.size());
// Check whether the final state is equal to expected one
assert(Solution[0].get_state() == ideal);
for (auto it = Solution.rbegin(); it != Solution.rend(); ++it) {
std::cout << *it << std::endl;
}
// 3rd test: A* search for 15-Puzzle
// Initial State of the puzzle
matrix4 puzzle2;
puzzle2[0] = row4({10, 1, 6, 2});
puzzle2[1] = row4({5, 8, 4, 3});
puzzle2[2] = row4({13, 0, 7, 11});
puzzle2[3] = row4({14, 9, 15, 12});
// Final state of the puzzle
matrix4 ideal2;
ideal2[0] = row4({1, 2, 3, 4});
ideal2[1] = row4({5, 6, 7, 8});
ideal2[2] = row4({9, 10, 11, 12});
ideal2[3] = row4({13, 14, 15, 0});
// Instantiate states for a*, initial state and final states
machine_learning::aystar_search::EightPuzzle<4> Puzzle2(puzzle2),
Ideal2(ideal2);
// Initialize the search object
machine_learning::aystar_search::AyStarSearch<
machine_learning::aystar_search::EightPuzzle<4>>
search2(Puzzle2, Ideal2);
/**
* Heuristic function: Manhattan distance
*/
auto manhattan_distance2 =
[](const machine_learning::aystar_search::EightPuzzle<4> &first,
const machine_learning::aystar_search::EightPuzzle<4> &second) {
uint32_t ret = 0;
for (size_t i = 0; i < first.get_size(); ++i) {
for (size_t j = 0; j < first.get_size(); ++j) {
uint32_t find = first.get(i, j);
size_t m = first.get_size(), n = first.get_size();
for (size_t k = 0; k < second.get_size(); ++k) {
for (size_t l = 0; l < second.get_size(); ++l) {
if (find == second.get(k, l)) {
std::tie(m, n) = std::make_pair(k, l);
break;
}
}
if (m != first.get_size()) {
break;
}
}
if (m != first.get_size()) {
ret += (std::max(m, i) - std::min(m, i)) +
(std::max(n, j) - std::min(n, j));
}
}
}
return ret;
};
auto sol2 = search2.a_star_search(manhattan_distance2);
std::cout << sol2.size() << std::endl;
// Static assertion due to large solution
assert(24 == sol2.size());
// Check whether the final state is equal to expected one
assert(sol2[0].get_state() == ideal2);
for (auto it = sol2.rbegin(); it != sol2.rend(); ++it) {
std::cout << *it << std::endl;
}
}
/**
* @brief Main function
* @returns 0 on exit
*/
int main() {
test(); // run self-test implementations
return 0;
}
+378
View File
@@ -0,0 +1,378 @@
/**
* \addtogroup machine_learning Machine Learning Algorithms
* @{
* \file
* \brief [Adaptive Linear Neuron
* (ADALINE)](https://en.wikipedia.org/wiki/ADALINE) implementation
*
* \author [Krishna Vedala](https://github.com/kvedala)
*
* \details
* <a href="https://commons.wikimedia.org/wiki/File:Adaline_flow_chart.gif"><img
* src="https://upload.wikimedia.org/wikipedia/commons/b/be/Adaline_flow_chart.gif"
* alt="Structure of an ADALINE network. Source: Wikipedia"
* style="width:200px; float:right;"></a>
*
* ADALINE is one of the first and simplest single layer artificial neural
* network. The algorithm essentially implements a linear function
* \f[ f\left(x_0,x_1,x_2,\ldots\right) =
* \sum_j x_jw_j+\theta
* \f]
* where \f$x_j\f$ are the input features of a sample, \f$w_j\f$ are the
* coefficients of the linear function and \f$\theta\f$ is a constant. If we
* know the \f$w_j\f$, then for any given set of features, \f$y\f$ can be
* computed. Computing the \f$w_j\f$ is a supervised learning algorithm wherein
* a set of features and their corresponding outputs are given and weights are
* computed using stochastic gradient descent method.
*/
#include <array>
#include <cassert>
#include <climits>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <numeric>
#include <vector>
/** Maximum number of iterations to learn */
constexpr int MAX_ITER = 500; // INT_MAX
/** \namespace machine_learning
* \brief Machine learning algorithms
*/
namespace machine_learning {
class adaline {
public:
/**
* Default constructor
* \param[in] num_features number of features present
* \param[in] eta learning rate (optional, default=0.1)
* \param[in] convergence accuracy (optional,
* default=\f$1\times10^{-5}\f$)
*/
explicit adaline(int num_features, const double eta = 0.01f,
const double accuracy = 1e-5)
: eta(eta), accuracy(accuracy) {
if (eta <= 0) {
std::cerr << "learning rate should be positive and nonzero"
<< std::endl;
std::exit(EXIT_FAILURE);
}
weights = std::vector<double>(
num_features +
1); // additional weight is for the constant bias term
// initialize with random weights in the range [-50, 49]
for (double &weight : weights) weight = 1.f;
// weights[i] = (static_cast<double>(std::rand() % 100) - 50);
}
/**
* Operator to print the weights of the model
*/
friend std::ostream &operator<<(std::ostream &out, const adaline &ada) {
out << "<";
for (int i = 0; i < ada.weights.size(); i++) {
out << ada.weights[i];
if (i < ada.weights.size() - 1) {
out << ", ";
}
}
out << ">";
return out;
}
/**
* predict the output of the model for given set of features
* \param[in] x input vector
* \param[out] out optional argument to return neuron output before
* applying activation function (optional, `nullptr` to ignore) \returns
* model prediction output
*/
int predict(const std::vector<double> &x, double *out = nullptr) {
if (!check_size_match(x)) {
return 0;
}
double y = weights.back(); // assign bias value
// for (int i = 0; i < x.size(); i++) y += x[i] * weights[i];
y = std::inner_product(x.begin(), x.end(), weights.begin(), y);
if (out != nullptr) { // if out variable is provided
*out = y;
}
return activation(y); // quantizer: apply ADALINE threshold function
}
/**
* Update the weights of the model using supervised learning for one
* feature vector
* \param[in] x feature vector
* \param[in] y known output value
* \returns correction factor
*/
double fit(const std::vector<double> &x, const int &y) {
if (!check_size_match(x)) {
return 0;
}
/* output of the model with current weights */
int p = predict(x);
int prediction_error = y - p; // error in estimation
double correction_factor = eta * prediction_error;
/* update each weight, the last weight is the bias term */
for (int i = 0; i < x.size(); i++) {
weights[i] += correction_factor * x[i];
}
weights[x.size()] += correction_factor; // update bias
return correction_factor;
}
/**
* Update the weights of the model using supervised learning for an
* array of vectors.
* \param[in] X array of feature vector
* \param[in] y known output value for each feature vector
*/
template <size_t N>
void fit(std::array<std::vector<double>, N> const &X,
std::array<int, N> const &Y) {
double avg_pred_error = 1.f;
int iter = 0;
for (iter = 0; (iter < MAX_ITER) && (avg_pred_error > accuracy);
iter++) {
avg_pred_error = 0.f;
// perform fit for each sample
for (int i = 0; i < N; i++) {
double err = fit(X[i], Y[i]);
avg_pred_error += std::abs(err);
}
avg_pred_error /= N;
// Print updates every 200th iteration
// if (iter % 100 == 0)
std::cout << "\tIter " << iter << ": Training weights: " << *this
<< "\tAvg error: " << avg_pred_error << std::endl;
}
if (iter < MAX_ITER) {
std::cout << "Converged after " << iter << " iterations."
<< std::endl;
} else {
std::cout << "Did not converge after " << iter << " iterations."
<< std::endl;
}
}
/** Defines activation function as Heaviside's step function.
* \f[
* f(x) = \begin{cases}
* -1 & \forall x \le 0\\
* 1 & \forall x > 0
* \end{cases}
* \f]
* @param x input value to apply activation on
* @return activation output
*/
int activation(double x) { return x > 0 ? 1 : -1; }
private:
/**
* convenient function to check if input feature vector size matches the
* model weights size
* \param[in] x fecture vector to check
* \returns `true` size matches
* \returns `false` size does not match
*/
bool check_size_match(const std::vector<double> &x) {
if (x.size() != (weights.size() - 1)) {
std::cerr << __func__ << ": "
<< "Number of features in x does not match the feature "
"dimension in model!"
<< std::endl;
return false;
}
return true;
}
const double eta; ///< learning rate of the algorithm
const double accuracy; ///< model fit convergence accuracy
std::vector<double> weights; ///< weights of the neural network
};
} // namespace machine_learning
using machine_learning::adaline;
/** @} */
/**
* test function to predict points in a 2D coordinate system above the line
* \f$x=y\f$ as +1 and others as -1.
* Note that each point is defined by 2 values or 2 features.
* \param[in] eta learning rate (optional, default=0.01)
*/
void test1(double eta = 0.01) {
adaline ada(2, eta); // 2 features
const int N = 10; // number of sample points
std::array<std::vector<double>, N> X = {
std::vector<double>({0, 1}), std::vector<double>({1, -2}),
std::vector<double>({2, 3}), std::vector<double>({3, -1}),
std::vector<double>({4, 1}), std::vector<double>({6, -5}),
std::vector<double>({-7, -3}), std::vector<double>({-8, 5}),
std::vector<double>({-9, 2}), std::vector<double>({-10, -15})};
std::array<int, N> y = {1, -1, 1, -1, -1,
-1, 1, 1, 1, -1}; // corresponding y-values
std::cout << "------- Test 1 -------" << std::endl;
std::cout << "Model before fit: " << ada << std::endl;
ada.fit<N>(X, y);
std::cout << "Model after fit: " << ada << std::endl;
int predict = ada.predict({5, -3});
std::cout << "Predict for x=(5,-3): " << predict;
assert(predict == -1);
std::cout << " ...passed" << std::endl;
predict = ada.predict({5, 8});
std::cout << "Predict for x=(5,8): " << predict;
assert(predict == 1);
std::cout << " ...passed" << std::endl;
}
/**
* test function to predict points in a 2D coordinate system above the line
* \f$x+3y=-1\f$ as +1 and others as -1.
* Note that each point is defined by 2 values or 2 features.
* The function will create random sample points for training and test purposes.
* \param[in] eta learning rate (optional, default=0.01)
*/
void test2(double eta = 0.01) {
adaline ada(2, eta); // 2 features
const int N = 50; // number of sample points
std::array<std::vector<double>, N> X;
std::array<int, N> Y{}; // corresponding y-values
// generate sample points in the interval
// [-range2/100 , (range2-1)/100]
int range = 500; // sample points full-range
int range2 = range >> 1; // sample points half-range
for (int i = 0; i < N; i++) {
double x0 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x1 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
X[i] = std::vector<double>({x0, x1});
Y[i] = (x0 + 3. * x1) > -1 ? 1 : -1;
}
std::cout << "------- Test 2 -------" << std::endl;
std::cout << "Model before fit: " << ada << std::endl;
ada.fit(X, Y);
std::cout << "Model after fit: " << ada << std::endl;
int N_test_cases = 5;
for (int i = 0; i < N_test_cases; i++) {
double x0 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x1 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
int predict = ada.predict({x0, x1});
std::cout << "Predict for x=(" << x0 << "," << x1 << "): " << predict;
int expected_val = (x0 + 3. * x1) > -1 ? 1 : -1;
assert(predict == expected_val);
std::cout << " ...passed" << std::endl;
}
}
/**
* test function to predict points in a 3D coordinate system lying within the
* sphere of radius 1 and centre at origin as +1 and others as -1. Note that
* each point is defined by 3 values but we use 6 features. The function will
* create random sample points for training and test purposes.
* The sphere centred at origin and radius 1 is defined as:
* \f$x^2+y^2+z^2=r^2=1\f$ and if the \f$r^2<1\f$, point lies within the sphere
* else, outside.
*
* \param[in] eta learning rate (optional, default=0.01)
*/
void test3(double eta = 0.01) {
adaline ada(6, eta); // 2 features
const int N = 100; // number of sample points
std::array<std::vector<double>, N> X;
std::array<int, N> Y{}; // corresponding y-values
// generate sample points in the interval
// [-range2/100 , (range2-1)/100]
int range = 200; // sample points full-range
int range2 = range >> 1; // sample points half-range
for (int i = 0; i < N; i++) {
double x0 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x1 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x2 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
X[i] = std::vector<double>({x0, x1, x2, x0 * x0, x1 * x1, x2 * x2});
Y[i] = ((x0 * x0) + (x1 * x1) + (x2 * x2)) <= 1.f ? 1 : -1;
}
std::cout << "------- Test 3 -------" << std::endl;
std::cout << "Model before fit: " << ada << std::endl;
ada.fit(X, Y);
std::cout << "Model after fit: " << ada << std::endl;
int N_test_cases = 5;
for (int i = 0; i < N_test_cases; i++) {
double x0 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x1 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
double x2 = (static_cast<double>(std::rand() % range) - range2) / 100.f;
int predict = ada.predict({x0, x1, x2, x0 * x0, x1 * x1, x2 * x2});
std::cout << "Predict for x=(" << x0 << "," << x1 << "," << x2
<< "): " << predict;
int expected_val = ((x0 * x0) + (x1 * x1) + (x2 * x2)) <= 1.f ? 1 : -1;
assert(predict == expected_val);
std::cout << " ...passed" << std::endl;
}
}
/** Main function */
int main(int argc, char **argv) {
std::srand(std::time(nullptr)); // initialize random number generator
double eta = 0.1; // default value of eta
if (argc == 2) { // read eta value from commandline argument if present
eta = strtof(argv[1], nullptr);
}
test1(eta);
std::cout << "Press ENTER to continue..." << std::endl;
std::cin.get();
test2(eta);
std::cout << "Press ENTER to continue..." << std::endl;
std::cin.get();
test3(eta);
return 0;
}
+152
View File
@@ -0,0 +1,152 @@
https://archive.ics.uci.edu/ml/datasets/iris
sepal length in cm,sepal width in cm,petal length in cm,petal width in cm
5.1,3.5,1.4,.2,0
4.9,3,1.4,.2,0
4.7,3.2,1.3,.2,0
4.6,3.1,1.5,.2,0
5,3.6,1.4,.2,0
5.4,3.9,1.7,.4,0
4.6,3.4,1.4,.3,0
5,3.4,1.5,.2,0
4.4,2.9,1.4,.2,0
4.9,3.1,1.5,.1,0
5.4,3.7,1.5,.2,0
4.8,3.4,1.6,.2,0
4.8,3,1.4,.1,0
4.3,3,1.1,.1,0
5.8,4,1.2,.2,0
5.7,4.4,1.5,.4,0
5.4,3.9,1.3,.4,0
5.1,3.5,1.4,.3,0
5.7,3.8,1.7,.3,0
5.1,3.8,1.5,.3,0
5.4,3.4,1.7,.2,0
5.1,3.7,1.5,.4,0
4.6,3.6,1,.2,0
5.1,3.3,1.7,.5,0
4.8,3.4,1.9,.2,0
5,3,1.6,.2,0
5,3.4,1.6,.4,0
5.2,3.5,1.5,.2,0
5.2,3.4,1.4,.2,0
4.7,3.2,1.6,.2,0
4.8,3.1,1.6,.2,0
5.4,3.4,1.5,.4,0
5.2,4.1,1.5,.1,0
5.5,4.2,1.4,.2,0
4.9,3.1,1.5,.2,0
5,3.2,1.2,.2,0
5.5,3.5,1.3,.2,0
4.9,3.6,1.4,.1,0
4.4,3,1.3,.2,0
5.1,3.4,1.5,.2,0
5,3.5,1.3,.3,0
4.5,2.3,1.3,.3,0
4.4,3.2,1.3,.2,0
5,3.5,1.6,.6,0
5.1,3.8,1.9,.4,0
4.8,3,1.4,.3,0
5.1,3.8,1.6,.2,0
4.6,3.2,1.4,.2,0
5.3,3.7,1.5,.2,0
5,3.3,1.4,.2,0
7,3.2,4.7,1.4,1
6.4,3.2,4.5,1.5,1
6.9,3.1,4.9,1.5,1
5.5,2.3,4,1.3,1
6.5,2.8,4.6,1.5,1
5.7,2.8,4.5,1.3,1
6.3,3.3,4.7,1.6,1
4.9,2.4,3.3,1,1
6.6,2.9,4.6,1.3,1
5.2,2.7,3.9,1.4,1
5,2,3.5,1,1
5.9,3,4.2,1.5,1
6,2.2,4,1,1
6.1,2.9,4.7,1.4,1
5.6,2.9,3.6,1.3,1
6.7,3.1,4.4,1.4,1
5.6,3,4.5,1.5,1
5.8,2.7,4.1,1,1
6.2,2.2,4.5,1.5,1
5.6,2.5,3.9,1.1,1
5.9,3.2,4.8,1.8,1
6.1,2.8,4,1.3,1
6.3,2.5,4.9,1.5,1
6.1,2.8,4.7,1.2,1
6.4,2.9,4.3,1.3,1
6.6,3,4.4,1.4,1
6.8,2.8,4.8,1.4,1
6.7,3,5,1.7,1
6,2.9,4.5,1.5,1
5.7,2.6,3.5,1,1
5.5,2.4,3.8,1.1,1
5.5,2.4,3.7,1,1
5.8,2.7,3.9,1.2,1
6,2.7,5.1,1.6,1
5.4,3,4.5,1.5,1
6,3.4,4.5,1.6,1
6.7,3.1,4.7,1.5,1
6.3,2.3,4.4,1.3,1
5.6,3,4.1,1.3,1
5.5,2.5,4,1.3,1
5.5,2.6,4.4,1.2,1
6.1,3,4.6,1.4,1
5.8,2.6,4,1.2,1
5,2.3,3.3,1,1
5.6,2.7,4.2,1.3,1
5.7,3,4.2,1.2,1
5.7,2.9,4.2,1.3,1
6.2,2.9,4.3,1.3,1
5.1,2.5,3,1.1,1
5.7,2.8,4.1,1.3,1
6.3,3.3,6,2.5,2
5.8,2.7,5.1,1.9,2
7.1,3,5.9,2.1,2
6.3,2.9,5.6,1.8,2
6.5,3,5.8,2.2,2
7.6,3,6.6,2.1,2
4.9,2.5,4.5,1.7,2
7.3,2.9,6.3,1.8,2
6.7,2.5,5.8,1.8,2
7.2,3.6,6.1,2.5,2
6.5,3.2,5.1,2,2
6.4,2.7,5.3,1.9,2
6.8,3,5.5,2.1,2
5.7,2.5,5,2,2
5.8,2.8,5.1,2.4,2
6.4,3.2,5.3,2.3,2
6.5,3,5.5,1.8,2
7.7,3.8,6.7,2.2,2
7.7,2.6,6.9,2.3,2
6,2.2,5,1.5,2
6.9,3.2,5.7,2.3,2
5.6,2.8,4.9,2,2
7.7,2.8,6.7,2,2
6.3,2.7,4.9,1.8,2
6.7,3.3,5.7,2.1,2
7.2,3.2,6,1.8,2
6.2,2.8,4.8,1.8,2
6.1,3,4.9,1.8,2
6.4,2.8,5.6,2.1,2
7.2,3,5.8,1.6,2
7.4,2.8,6.1,1.9,2
7.9,3.8,6.4,2,2
6.4,2.8,5.6,2.2,2
6.3,2.8,5.1,1.5,2
6.1,2.6,5.6,1.4,2
7.7,3,6.1,2.3,2
6.3,3.4,5.6,2.4,2
6.4,3.1,5.5,1.8,2
6,3,4.8,1.8,2
6.9,3.1,5.4,2.1,2
6.7,3.1,5.6,2.4,2
6.9,3.1,5.1,2.3,2
5.8,2.7,5.1,1.9,2
6.8,3.2,5.9,2.3,2
6.7,3.3,5.7,2.5,2
6.7,3,5.2,2.3,2
6.3,2.5,5,1.9,2
6.5,3,5.2,2,2
6.2,3.4,5.4,2.3,2
5.9,3,5.1,1.8,2
1 https://archive.ics.uci.edu/ml/datasets/iris
2 sepal length in cm,sepal width in cm,petal length in cm,petal width in cm
3 5.1,3.5,1.4,.2,0
4 4.9,3,1.4,.2,0
5 4.7,3.2,1.3,.2,0
6 4.6,3.1,1.5,.2,0
7 5,3.6,1.4,.2,0
8 5.4,3.9,1.7,.4,0
9 4.6,3.4,1.4,.3,0
10 5,3.4,1.5,.2,0
11 4.4,2.9,1.4,.2,0
12 4.9,3.1,1.5,.1,0
13 5.4,3.7,1.5,.2,0
14 4.8,3.4,1.6,.2,0
15 4.8,3,1.4,.1,0
16 4.3,3,1.1,.1,0
17 5.8,4,1.2,.2,0
18 5.7,4.4,1.5,.4,0
19 5.4,3.9,1.3,.4,0
20 5.1,3.5,1.4,.3,0
21 5.7,3.8,1.7,.3,0
22 5.1,3.8,1.5,.3,0
23 5.4,3.4,1.7,.2,0
24 5.1,3.7,1.5,.4,0
25 4.6,3.6,1,.2,0
26 5.1,3.3,1.7,.5,0
27 4.8,3.4,1.9,.2,0
28 5,3,1.6,.2,0
29 5,3.4,1.6,.4,0
30 5.2,3.5,1.5,.2,0
31 5.2,3.4,1.4,.2,0
32 4.7,3.2,1.6,.2,0
33 4.8,3.1,1.6,.2,0
34 5.4,3.4,1.5,.4,0
35 5.2,4.1,1.5,.1,0
36 5.5,4.2,1.4,.2,0
37 4.9,3.1,1.5,.2,0
38 5,3.2,1.2,.2,0
39 5.5,3.5,1.3,.2,0
40 4.9,3.6,1.4,.1,0
41 4.4,3,1.3,.2,0
42 5.1,3.4,1.5,.2,0
43 5,3.5,1.3,.3,0
44 4.5,2.3,1.3,.3,0
45 4.4,3.2,1.3,.2,0
46 5,3.5,1.6,.6,0
47 5.1,3.8,1.9,.4,0
48 4.8,3,1.4,.3,0
49 5.1,3.8,1.6,.2,0
50 4.6,3.2,1.4,.2,0
51 5.3,3.7,1.5,.2,0
52 5,3.3,1.4,.2,0
53 7,3.2,4.7,1.4,1
54 6.4,3.2,4.5,1.5,1
55 6.9,3.1,4.9,1.5,1
56 5.5,2.3,4,1.3,1
57 6.5,2.8,4.6,1.5,1
58 5.7,2.8,4.5,1.3,1
59 6.3,3.3,4.7,1.6,1
60 4.9,2.4,3.3,1,1
61 6.6,2.9,4.6,1.3,1
62 5.2,2.7,3.9,1.4,1
63 5,2,3.5,1,1
64 5.9,3,4.2,1.5,1
65 6,2.2,4,1,1
66 6.1,2.9,4.7,1.4,1
67 5.6,2.9,3.6,1.3,1
68 6.7,3.1,4.4,1.4,1
69 5.6,3,4.5,1.5,1
70 5.8,2.7,4.1,1,1
71 6.2,2.2,4.5,1.5,1
72 5.6,2.5,3.9,1.1,1
73 5.9,3.2,4.8,1.8,1
74 6.1,2.8,4,1.3,1
75 6.3,2.5,4.9,1.5,1
76 6.1,2.8,4.7,1.2,1
77 6.4,2.9,4.3,1.3,1
78 6.6,3,4.4,1.4,1
79 6.8,2.8,4.8,1.4,1
80 6.7,3,5,1.7,1
81 6,2.9,4.5,1.5,1
82 5.7,2.6,3.5,1,1
83 5.5,2.4,3.8,1.1,1
84 5.5,2.4,3.7,1,1
85 5.8,2.7,3.9,1.2,1
86 6,2.7,5.1,1.6,1
87 5.4,3,4.5,1.5,1
88 6,3.4,4.5,1.6,1
89 6.7,3.1,4.7,1.5,1
90 6.3,2.3,4.4,1.3,1
91 5.6,3,4.1,1.3,1
92 5.5,2.5,4,1.3,1
93 5.5,2.6,4.4,1.2,1
94 6.1,3,4.6,1.4,1
95 5.8,2.6,4,1.2,1
96 5,2.3,3.3,1,1
97 5.6,2.7,4.2,1.3,1
98 5.7,3,4.2,1.2,1
99 5.7,2.9,4.2,1.3,1
100 6.2,2.9,4.3,1.3,1
101 5.1,2.5,3,1.1,1
102 5.7,2.8,4.1,1.3,1
103 6.3,3.3,6,2.5,2
104 5.8,2.7,5.1,1.9,2
105 7.1,3,5.9,2.1,2
106 6.3,2.9,5.6,1.8,2
107 6.5,3,5.8,2.2,2
108 7.6,3,6.6,2.1,2
109 4.9,2.5,4.5,1.7,2
110 7.3,2.9,6.3,1.8,2
111 6.7,2.5,5.8,1.8,2
112 7.2,3.6,6.1,2.5,2
113 6.5,3.2,5.1,2,2
114 6.4,2.7,5.3,1.9,2
115 6.8,3,5.5,2.1,2
116 5.7,2.5,5,2,2
117 5.8,2.8,5.1,2.4,2
118 6.4,3.2,5.3,2.3,2
119 6.5,3,5.5,1.8,2
120 7.7,3.8,6.7,2.2,2
121 7.7,2.6,6.9,2.3,2
122 6,2.2,5,1.5,2
123 6.9,3.2,5.7,2.3,2
124 5.6,2.8,4.9,2,2
125 7.7,2.8,6.7,2,2
126 6.3,2.7,4.9,1.8,2
127 6.7,3.3,5.7,2.1,2
128 7.2,3.2,6,1.8,2
129 6.2,2.8,4.8,1.8,2
130 6.1,3,4.9,1.8,2
131 6.4,2.8,5.6,2.1,2
132 7.2,3,5.8,1.6,2
133 7.4,2.8,6.1,1.9,2
134 7.9,3.8,6.4,2,2
135 6.4,2.8,5.6,2.2,2
136 6.3,2.8,5.1,1.5,2
137 6.1,2.6,5.6,1.4,2
138 7.7,3,6.1,2.3,2
139 6.3,3.4,5.6,2.4,2
140 6.4,3.1,5.5,1.8,2
141 6,3,4.8,1.8,2
142 6.9,3.1,5.4,2.1,2
143 6.7,3.1,5.6,2.4,2
144 6.9,3.1,5.1,2.3,2
145 5.8,2.7,5.1,1.9,2
146 6.8,3.2,5.9,2.3,2
147 6.7,3.3,5.7,2.5,2
148 6.7,3,5.2,2.3,2
149 6.3,2.5,5,1.9,2
150 6.5,3,5.2,2,2
151 6.2,3.4,5.4,2.3,2
152 5.9,3,5.1,1.8,2
+194
View File
@@ -0,0 +1,194 @@
/**
* @file
* @brief Implementation of [K-Nearest Neighbors algorithm]
* (https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm).
* @author [Luiz Carlos Cosmi Filho](https://github.com/luizcarloscf)
* @details K-nearest neighbors algorithm, also known as KNN or k-NN, is a
* supervised learning classifier, which uses proximity to make classifications.
* This implementantion uses the Euclidean Distance as distance metric to find
* the K-nearest neighbors.
*/
#include <algorithm> /// for std::transform and std::sort
#include <cassert> /// for assert
#include <cmath> /// for std::pow and std::sqrt
#include <iostream> /// for std::cout
#include <numeric> /// for std::accumulate
#include <unordered_map> /// for std::unordered_map
#include <vector> /// for std::vector
/**
* @namespace machine_learning
* @brief Machine learning algorithms
*/
namespace machine_learning {
/**
* @namespace k_nearest_neighbors
* @brief Functions for the [K-Nearest Neighbors algorithm]
* (https://en.wikipedia.org/wiki/K-nearest_neighbors_algorithm) implementation
*/
namespace k_nearest_neighbors {
/**
* @brief Compute the Euclidean distance between two vectors.
*
* @tparam T typename of the vector
* @param a first unidimentional vector
* @param b second unidimentional vector
* @return double scalar representing the Euclidean distance between provided
* vectors
*/
template <typename T>
double euclidean_distance(const std::vector<T>& a, const std::vector<T>& b) {
std::vector<double> aux;
std::transform(a.begin(), a.end(), b.begin(), std::back_inserter(aux),
[](T x1, T x2) { return std::pow((x1 - x2), 2); });
aux.shrink_to_fit();
return std::sqrt(std::accumulate(aux.begin(), aux.end(), 0.0));
}
/**
* @brief K-Nearest Neighbors (Knn) class using Euclidean distance as
* distance metric.
*/
class Knn {
private:
std::vector<std::vector<double>> X_{}; ///< attributes vector
std::vector<int> Y_{}; ///< labels vector
public:
/**
* @brief Construct a new Knn object.
* @details Using lazy-learning approch, just holds in memory the dataset.
* @param X attributes vector
* @param Y labels vector
*/
explicit Knn(std::vector<std::vector<double>>& X, std::vector<int>& Y)
: X_(X), Y_(Y){};
/**
* Copy Constructor for class Knn.
*
* @param model instance of class to be copied
*/
Knn(const Knn& model) = default;
/**
* Copy assignment operator for class Knn
*/
Knn& operator=(const Knn& model) = default;
/**
* Move constructor for class Knn
*/
Knn(Knn&&) = default;
/**
* Move assignment operator for class Knn
*/
Knn& operator=(Knn&&) = default;
/**
* @brief Destroy the Knn object
*/
~Knn() = default;
/**
* @brief Classify sample.
* @param sample sample
* @param k number of neighbors
* @return int label of most frequent neighbors
*/
int predict(std::vector<double>& sample, int k) {
std::vector<int> neighbors;
std::vector<std::pair<double, int>> distances;
for (size_t i = 0; i < this->X_.size(); ++i) {
auto current = this->X_.at(i);
auto label = this->Y_.at(i);
auto distance = euclidean_distance(current, sample);
distances.emplace_back(distance, label);
}
std::sort(distances.begin(), distances.end());
for (int i = 0; i < k; i++) {
auto label = distances.at(i).second;
neighbors.push_back(label);
}
std::unordered_map<int, int> frequency;
for (auto neighbor : neighbors) {
++frequency[neighbor];
}
std::pair<int, int> predicted;
predicted.first = -1;
predicted.second = -1;
for (auto& kv : frequency) {
if (kv.second > predicted.second) {
predicted.second = kv.second;
predicted.first = kv.first;
}
}
return predicted.first;
}
};
} // namespace k_nearest_neighbors
} // namespace machine_learning
/**
* @brief Self-test implementations
* @returns void
*/
static void test() {
std::cout << "------- Test 1 -------" << std::endl;
std::vector<std::vector<double>> X1 = {{0.0, 0.0}, {0.25, 0.25},
{0.0, 0.5}, {0.5, 0.5},
{1.0, 0.5}, {1.0, 1.0}};
std::vector<int> Y1 = {1, 1, 1, 1, 2, 2};
auto model1 = machine_learning::k_nearest_neighbors::Knn(X1, Y1);
std::vector<double> sample1 = {1.2, 1.2};
std::vector<double> sample2 = {0.1, 0.1};
std::vector<double> sample3 = {0.1, 0.5};
std::vector<double> sample4 = {1.0, 0.75};
assert(model1.predict(sample1, 2) == 2);
assert(model1.predict(sample2, 2) == 1);
assert(model1.predict(sample3, 2) == 1);
assert(model1.predict(sample4, 2) == 2);
std::cout << "... Passed" << std::endl;
std::cout << "------- Test 2 -------" << std::endl;
std::vector<std::vector<double>> X2 = {
{0.0, 0.0, 0.0}, {0.25, 0.25, 0.0}, {0.0, 0.5, 0.0}, {0.5, 0.5, 0.0},
{1.0, 0.5, 0.0}, {1.0, 1.0, 0.0}, {1.0, 1.0, 1.0}, {1.5, 1.5, 1.0}};
std::vector<int> Y2 = {1, 1, 1, 1, 2, 2, 3, 3};
auto model2 = machine_learning::k_nearest_neighbors::Knn(X2, Y2);
std::vector<double> sample5 = {1.2, 1.2, 0.0};
std::vector<double> sample6 = {0.1, 0.1, 0.0};
std::vector<double> sample7 = {0.1, 0.5, 0.0};
std::vector<double> sample8 = {1.0, 0.75, 1.0};
assert(model2.predict(sample5, 2) == 2);
assert(model2.predict(sample6, 2) == 1);
assert(model2.predict(sample7, 2) == 1);
assert(model2.predict(sample8, 2) == 3);
std::cout << "... Passed" << std::endl;
std::cout << "------- Test 3 -------" << std::endl;
std::vector<std::vector<double>> X3 = {{0.0}, {1.0}, {2.0}, {3.0},
{4.0}, {5.0}, {6.0}, {7.0}};
std::vector<int> Y3 = {1, 1, 1, 1, 2, 2, 2, 2};
auto model3 = machine_learning::k_nearest_neighbors::Knn(X3, Y3);
std::vector<double> sample9 = {0.5};
std::vector<double> sample10 = {2.9};
std::vector<double> sample11 = {5.5};
std::vector<double> sample12 = {7.5};
assert(model3.predict(sample9, 3) == 1);
assert(model3.predict(sample10, 3) == 1);
assert(model3.predict(sample11, 3) == 2);
assert(model3.predict(sample12, 3) == 2);
std::cout << "... Passed" << std::endl;
}
/**
* @brief Main function
* @return int 0 on exit
*/
int main() {
test(); // run self-test implementations
return 0;
}
+613
View File
@@ -0,0 +1,613 @@
/**
* \addtogroup machine_learning Machine Learning Algorithms
* @{
* \file
* \author [Krishna Vedala](https://github.com/kvedala)
*
* \brief [Kohonen self organizing
* map](https://en.wikipedia.org/wiki/Self-organizing_map) (topological map)
*
* \details
* This example implements a powerful unsupervised learning algorithm called as
* a self organizing map. The algorithm creates a connected network of weights
* that closely follows the given data points. This thus creates a topological
* map of the given data i.e., it maintains the relationship between varipus
* data points in a much higher dimesional space by creating an equivalent in a
* 2-dimensional space.
* <img alt="Trained topological maps for the test cases in the program"
* src="https://raw.githubusercontent.com/TheAlgorithms/C-Plus-Plus/docs/images/machine_learning/2D_Kohonen_SOM.svg"
* />
* \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_trace.cpp
*/
#define _USE_MATH_DEFINES //< required for MS Visual C++
#include <algorithm>
#include <array>
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <cstring>
#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_2d_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 << ": "
<< std::strerror(errno) << "\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;
}
/**
* Get minimum value and index of the value in a matrix
* \param[in] X matrix to search
* \param[in] N number of points in the vector
* \param[out] val minimum value found
* \param[out] idx_x x-index where minimum value was found
* \param[out] idx_y y-index where minimum value was found
*/
void get_min_2d(const std::vector<std::valarray<double>> &X, double *val,
int *x_idx, int *y_idx) {
val[0] = INFINITY; // initial min value
size_t N = X.size();
for (int i = 0; i < N; i++) { // traverse each x-index
auto result = std::min_element(std::begin(X[i]), std::end(X[i]));
double d_min = *result;
std::ptrdiff_t j = std::distance(std::begin(X[i]), result);
if (d_min < val[0]) { // if a lower value is found
// save the value and its index
x_idx[0] = i;
y_idx[0] = j;
val[0] = d_min;
}
}
}
/** \namespace machine_learning
* \brief Machine learning algorithms
*/
namespace machine_learning {
/** Minimum average distance of image nodes */
constexpr double MIN_DISTANCE = 1e-4;
/**
* Create the distance matrix or
* [U-matrix](https://en.wikipedia.org/wiki/U-matrix) from the trained
* 3D weiths matrix and save to disk.
*
* \param [in] fname filename to save in (gets overwriten without
* confirmation)
* \param [in] W model matrix to save
* \returns 0 if all ok
* \returns -1 if file creation failed
*/
int save_u_matrix(const char *fname,
const std::vector<std::vector<std::valarray<double>>> &W) {
std::ofstream fp(fname);
if (!fp) { // error with fopen
std::cerr << "File error (" << fname << "): " << std::strerror(errno)
<< std::endl;
return -1;
}
// neighborhood range
unsigned int R = 1;
for (int i = 0; i < W.size(); i++) { // for each x
for (int j = 0; j < W[0].size(); j++) { // for each y
double distance = 0.f;
int from_x = std::max<int>(0, i - R);
int to_x = std::min<int>(W.size(), i + R + 1);
int from_y = std::max<int>(0, j - R);
int to_y = std::min<int>(W[0].size(), j + R + 1);
int l = 0, m = 0;
#ifdef _OPENMP
#pragma omp parallel for reduction(+ : distance)
#endif
for (l = from_x; l < to_x; l++) { // scan neighborhoor in x
for (m = from_y; m < to_y; m++) { // scan neighborhood in y
auto d = W[i][j] - W[l][m];
double d2 = std::pow(d, 2).sum();
distance += std::sqrt(d2);
// distance += d2;
}
}
distance /= R * R; // mean distance from neighbors
fp << distance; // print the mean separation
if (j < W[0].size() - 1) { // if not the last column
fp << ','; // suffix comma
}
}
if (i < W.size() - 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 - N features
* \param[in,out] W weights matrix - PxQxN
* \param[in,out] D temporary vector to store distances PxQ
* \param[in] alpha learning rate \f$0<\alpha\le1\f$
* \param[in] R neighborhood range
* \returns minimum distance of sample and trained weights
*/
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) {
int x = 0, y = 0;
int num_out_x = static_cast<int>(W->size()); // output nodes - in X
int num_out_y = static_cast<int>(W[0][0].size()); // output nodes - in Y
// int num_features = static_cast<int>(W[0][0][0].size()); // features =
// in Z
double d_min = 0.f;
#ifdef _OPENMP
#pragma omp for
#endif
// step 1: for each output point
for (x = 0; x < num_out_x; x++) {
for (y = 0; y < num_out_y; y++) {
(*D)[x][y] = 0.f;
// compute Euclidian distance of each output
// point from the current sample
auto d = ((*W)[x][y] - X);
(*D)[x][y] = (d * d).sum();
(*D)[x][y] = std::sqrt((*D)[x][y]);
}
}
// step 2: get closest node i.e., node with snallest Euclidian distance
// to the current pattern
int d_min_x = 0, d_min_y = 0;
get_min_2d(*D, &d_min, &d_min_x, &d_min_y);
// step 3a: get the neighborhood range
int from_x = std::max(0, d_min_x - R);
int to_x = std::min(num_out_x, d_min_x + R + 1);
int from_y = std::max(0, d_min_y - R);
int to_y = std::min(num_out_y, d_min_y + R + 1);
// step 3b: update the weights of nodes in the
// neighborhood
#ifdef _OPENMP
#pragma omp for
#endif
for (x = from_x; x < to_x; x++) {
for (y = from_y; y < to_y; y++) {
/* you can enable the following normalization if needed.
personally, I found it detrimental to convergence */
// const double s2pi = sqrt(2.f * M_PI);
// double normalize = 1.f / (alpha * s2pi);
/* apply scaling inversely proportional to distance from the
current node */
double d2 =
(d_min_x - x) * (d_min_x - x) + (d_min_y - y) * (d_min_y - y);
double scale_factor = std::exp(-d2 / (2.f * alpha * alpha));
(*W)[x][y] += (X - (*W)[x][y]) * alpha * scale_factor;
}
}
return d_min;
}
/**
* 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(const std::vector<std::valarray<double>> &X,
std::vector<std::vector<std::valarray<double>>> *W,
double alpha_min) {
size_t num_samples = X.size(); // number of rows
// size_t num_features = X[0].size(); // number of columns
size_t num_out = W->size(); // output matrix size
size_t R = num_out >> 2, iter = 0;
double alpha = 1.f;
std::vector<std::valarray<double>> D(num_out);
for (int i = 0; i < num_out; i++) D[i] = std::valarray<double>(num_out);
double dmin = 1.f; // average minimum distance of all samples
double past_dmin = 1.f; // average minimum distance of all samples
double dmin_ratio = 1.f; // change per step
// Loop alpha from 1 to slpha_min
for (; alpha > 0 && dmin_ratio > 1e-5; alpha -= 1e-4, 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
dmin += update_weights(X[sample], W, &D, alpha, R);
}
// every 100th iteration, reduce the neighborhood range
if (iter % 300 == 0 && R > 1) {
R--;
}
dmin /= num_samples;
// termination condition variable -> % change in minimum distance
dmin_ratio = (past_dmin - dmin) / past_dmin;
if (dmin_ratio < 0) {
dmin_ratio = 1.f;
}
past_dmin = dmin;
std::cout << "iter: " << iter << "\t alpha: " << alpha << "\t R: " << R
<< "\t d_min: " << dmin_ratio << "\r";
}
std::cout << "\n";
}
} // namespace machine_learning
using machine_learning::kohonen_som;
using machine_learning::save_u_matrix;
/** @} */
/** Creates a random set of points distributed in four 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$
*
* \param[out] data matrix to store data in
*/
void test_2d_classes(std::vector<std::valarray<double>> *data) {
const int N = data->size();
const double R = 0.3; // radius of cluster
int i = 0;
const int num_classes = 4;
std::array<std::array<double, 2>, num_classes> centres = {
// centres of each class cluster
std::array<double, 2>({.5, .5}), // centre of class 1
std::array<double, 2>({.5, -.5}), // centre of class 2
std::array<double, 2>({-.5, .5}), // centre of class 3
std::array<double, 2>({-.5, -.5}) // centre of class 4
};
#ifdef _OPENMP
#pragma omp for
#endif
for (i = 0; i < N; i++) {
// select a random class for the point
int cls = std::rand() % num_classes;
// 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);
/* The follosing can also be used
for (int j = 0; j < 2; j++)
data[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
*/
}
}
/** Test that creates a random set of points distributed in four clusters in
* 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
*/
void test1() {
int j = 0, N = 300;
int features = 2;
int num_out = 30;
std::vector<std::valarray<double>> X(N);
std::vector<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::vector<std::valarray<double>>(num_out);
for (int k = 0; k < num_out; k++) {
W[i][k] = std::valarray<double>(features);
#ifdef _OPENMP
#pragma omp for
#endif
for (j = 0; j < features; j++) {
// preallocate with random initial weights
W[i][k][j] = _random(-10, 10);
}
}
}
}
test_2d_classes(&X); // create test data around circumference of a circle
save_2d_data("test1.csv", X); // save test data points
save_u_matrix("w11.csv", W); // save initial random weights
kohonen_som(X, &W, 1e-4); // train the SOM
save_u_matrix("w12.csv", W); // save the resultant weights
}
/** Creates a random set of points distributed in four 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$
*
* \param[out] data matrix to store data in
*/
void test_3d_classes1(std::vector<std::valarray<double>> *data) {
const size_t N = data->size();
const double R = 0.3; // radius of cluster
int i = 0;
const int num_classes = 4;
const std::array<std::array<double, 3>, num_classes> centres = {
// centres of each class cluster
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
};
#ifdef _OPENMP
#pragma omp for
#endif
for (i = 0; i < N; i++) {
// select a random class for the point
int cls = std::rand() % num_classes;
// 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[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
*/
}
}
/** Test that creates a random set of points distributed in 4 clusters in
* 3D space and trains an SOM that finds the topological 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
*/
void test2() {
int j = 0, N = 300;
int features = 3;
int num_out = 30;
std::vector<std::valarray<double>> X(N);
std::vector<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::vector<std::valarray<double>>(num_out);
for (int k = 0; k < num_out; k++) {
W[i][k] = std::valarray<double>(features);
#ifdef _OPENMP
#pragma omp for
#endif
for (j = 0; j < features; j++) {
// preallocate with random initial weights
W[i][k][j] = _random(-10, 10);
}
}
}
}
test_3d_classes1(&X); // create test data around circumference of a circle
save_2d_data("test2.csv", X); // save test data points
save_u_matrix("w21.csv", W); // save initial random weights
kohonen_som(X, &W, 1e-4); // train the SOM
save_u_matrix("w22.csv", W); // save the resultant weights
}
/** Creates a random set of points distributed in four 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$
*
* \param[out] data matrix to store data in
*/
void test_3d_classes2(std::vector<std::valarray<double>> *data) {
const size_t N = data->size();
const double R = 0.2; // radius of cluster
int i = 0;
const int num_classes = 8;
const std::array<std::array<double, 3>, num_classes> centres = {
// centres of each class cluster
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
std::array<double, 3>({-.5, -.5, -.5}) // centre of class 8
};
#ifdef _OPENMP
#pragma omp for
#endif
for (i = 0; i < N; i++) {
// select a random class for the point
int cls = std::rand() % num_classes;
// 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[i][j] = _random(centres[class][j] - R, centres[class][j] + R);
*/
}
}
/** Test that creates a random set of points distributed in eight clusters in
* 3D space and trains an SOM that finds the topological pattern. 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
*/
void test3() {
int j = 0, N = 500;
int features = 3;
int num_out = 30;
std::vector<std::valarray<double>> X(N);
std::vector<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::vector<std::valarray<double>>(num_out);
for (int k = 0; k < num_out; k++) {
W[i][k] = std::valarray<double>(features);
#ifdef _OPENMP
#pragma omp for
#endif
for (j = 0; j < features; j++) {
// preallocate with random initial weights
W[i][k][j] = _random(-10, 10);
}
}
}
}
test_3d_classes2(&X); // create test data around circumference of a circle
save_2d_data("test3.csv", X); // save test data points
save_u_matrix("w31.csv", W); // save initial random weights
kohonen_som(X, &W, 1e-4); // train the SOM
save_u_matrix("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() {
#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;
}
+488
View File
@@ -0,0 +1,488 @@
/**
* \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;
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() {
#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;
}
+837
View File
@@ -0,0 +1,837 @@
/**
* @file
* @author [Deep Raval](https://github.com/imdeep2905)
*
* @brief Implementation of [Multilayer Perceptron]
* (https://en.wikipedia.org/wiki/Multilayer_perceptron).
*
* @details
* A multilayer perceptron (MLP) is a class of feedforward artificial neural
* network (ANN). The term MLP is used ambiguously, sometimes loosely to any
* feedforward ANN, sometimes strictly to refer to networks composed of multiple
* layers of perceptrons (with threshold activation). Multilayer perceptrons are
* sometimes colloquially referred to as "vanilla" neural networks, especially
* when they have a single hidden layer.
*
* An MLP consists of at least three layers of nodes: an input layer, a hidden
* layer and an output layer. Except for the input nodes, each node is a neuron
* that uses a nonlinear activation function. MLP utilizes a supervised learning
* technique called backpropagation for training. Its multiple layers and
* non-linear activation distinguish MLP from a linear perceptron. It can
* distinguish data that is not linearly separable.
*
* See [Backpropagation](https://en.wikipedia.org/wiki/Backpropagation) for
* training algorithm.
*
* \note This implementation uses mini-batch gradient descent as optimizer and
* MSE as loss function. Bias is also not included.
*/
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <valarray>
#include <vector>
#include "vector_ops.hpp" // Custom header file for vector operations
/** \namespace machine_learning
* \brief Machine learning algorithms
*/
namespace machine_learning {
/** \namespace neural_network
* \brief Neural Network or Multilayer Perceptron
*/
namespace neural_network {
/** \namespace activations
* \brief Various activation functions used in Neural network
*/
namespace activations {
/**
* Sigmoid function
* @param X Value
* @return Returns sigmoid(x)
*/
double sigmoid(const double &x) { return 1.0 / (1.0 + std::exp(-x)); }
/**
* Derivative of sigmoid function
* @param X Value
* @return Returns derivative of sigmoid(x)
*/
double dsigmoid(const double &x) { return x * (1 - x); }
/**
* Relu function
* @param X Value
* @returns relu(x)
*/
double relu(const double &x) { return std::max(0.0, x); }
/**
* Derivative of relu function
* @param X Value
* @returns derivative of relu(x)
*/
double drelu(const double &x) { return x >= 0.0 ? 1.0 : 0.0; }
/**
* Tanh function
* @param X Value
* @return Returns tanh(x)
*/
double tanh(const double &x) { return 2 / (1 + std::exp(-2 * x)) - 1; }
/**
* Derivative of Sigmoid function
* @param X Value
* @return Returns derivative of tanh(x)
*/
double dtanh(const double &x) { return 1 - x * x; }
} // namespace activations
/** \namespace util_functions
* \brief Various utility functions used in Neural network
*/
namespace util_functions {
/**
* Square function
* @param X Value
* @return Returns x * x
*/
double square(const double &x) { return x * x; }
/**
* Identity function
* @param X Value
* @return Returns x
*/
double identity_function(const double &x) { return x; }
} // namespace util_functions
/** \namespace layers
* \brief This namespace contains layers used
* in MLP.
*/
namespace layers {
/**
* neural_network::layers::DenseLayer class is used to store all necessary
* information about the layers (i.e. neurons, activation and kernel). This
* class is used by NeuralNetwork class to store layers.
*
*/
class DenseLayer {
public:
// To store activation function and it's derivative
double (*activation_function)(const double &);
double (*dactivation_function)(const double &);
int neurons; // To store number of neurons (used in summary)
std::string activation; // To store activation name (used in summary)
std::vector<std::valarray<double>> kernel; // To store kernel (aka weights)
/**
* Constructor for neural_network::layers::DenseLayer class
* @param neurons number of neurons
* @param activation activation function for layer
* @param kernel_shape shape of kernel
* @param random_kernel flag for whether to initialize kernel randomly
*/
DenseLayer(const int &neurons, const std::string &activation,
const std::pair<size_t, size_t> &kernel_shape,
const bool &random_kernel) {
// Choosing activation (and it's derivative)
if (activation == "sigmoid") {
activation_function = neural_network::activations::sigmoid;
dactivation_function = neural_network::activations::sigmoid;
} else if (activation == "relu") {
activation_function = neural_network::activations::relu;
dactivation_function = neural_network::activations::drelu;
} else if (activation == "tanh") {
activation_function = neural_network::activations::tanh;
dactivation_function = neural_network::activations::dtanh;
} else if (activation == "none") {
// Set identity function in casse of none is supplied
activation_function =
neural_network::util_functions::identity_function;
dactivation_function =
neural_network::util_functions::identity_function;
} else {
// If supplied activation is invalid
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Invalid argument. Expected {none, sigmoid, relu, "
"tanh} got ";
std::cerr << activation << std::endl;
std::exit(EXIT_FAILURE);
}
this->activation = activation; // Setting activation name
this->neurons = neurons; // Setting number of neurons
// Initialize kernel according to flag
if (random_kernel) {
uniform_random_initialization(kernel, kernel_shape, -1.0, 1.0);
} else {
unit_matrix_initialization(kernel, kernel_shape);
}
}
/**
* Constructor for neural_network::layers::DenseLayer class
* @param neurons number of neurons
* @param activation activation function for layer
* @param kernel values of kernel (useful in loading model)
*/
DenseLayer(const int &neurons, const std::string &activation,
const std::vector<std::valarray<double>> &kernel) {
// Choosing activation (and it's derivative)
if (activation == "sigmoid") {
activation_function = neural_network::activations::sigmoid;
dactivation_function = neural_network::activations::sigmoid;
} else if (activation == "relu") {
activation_function = neural_network::activations::relu;
dactivation_function = neural_network::activations::drelu;
} else if (activation == "tanh") {
activation_function = neural_network::activations::tanh;
dactivation_function = neural_network::activations::dtanh;
} else if (activation == "none") {
// Set identity function in casse of none is supplied
activation_function =
neural_network::util_functions::identity_function;
dactivation_function =
neural_network::util_functions::identity_function;
} else {
// If supplied activation is invalid
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Invalid argument. Expected {none, sigmoid, relu, "
"tanh} got ";
std::cerr << activation << std::endl;
std::exit(EXIT_FAILURE);
}
this->activation = activation; // Setting activation name
this->neurons = neurons; // Setting number of neurons
this->kernel = kernel; // Setting supplied kernel values
}
/**
* Copy Constructor for class DenseLayer.
*
* @param model instance of class to be copied.
*/
DenseLayer(const DenseLayer &layer) = default;
/**
* Destructor for class DenseLayer.
*/
~DenseLayer() = default;
/**
* Copy assignment operator for class DenseLayer
*/
DenseLayer &operator=(const DenseLayer &layer) = default;
/**
* Move constructor for class DenseLayer
*/
DenseLayer(DenseLayer &&) = default;
/**
* Move assignment operator for class DenseLayer
*/
DenseLayer &operator=(DenseLayer &&) = default;
};
} // namespace layers
/**
* NeuralNetwork class is implements MLP. This class is
* used by actual user to create and train networks.
*
*/
class NeuralNetwork {
private:
std::vector<neural_network::layers::DenseLayer> layers; // To store layers
/**
* Private Constructor for class NeuralNetwork. This constructor
* is used internally to load model.
* @param config vector containing pair (neurons, activation)
* @param kernels vector containing all pretrained kernels
*/
NeuralNetwork(
const std::vector<std::pair<int, std::string>> &config,
const std::vector<std::vector<std::valarray<double>>> &kernels) {
// First layer should not have activation
if (config.begin()->second != "none") {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr
<< "First layer can't have activation other than none got "
<< config.begin()->second;
std::cerr << std::endl;
std::exit(EXIT_FAILURE);
}
// Network should have atleast two layers
if (config.size() <= 1) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Invalid size of network, ";
std::cerr << "Atleast two layers are required";
std::exit(EXIT_FAILURE);
}
// Reconstructing all pretrained layers
for (size_t i = 0; i < config.size(); i++) {
layers.emplace_back(neural_network::layers::DenseLayer(
config[i].first, config[i].second, kernels[i]));
}
std::cout << "INFO: Network constructed successfully" << std::endl;
}
/**
* Private function to get detailed predictions (i.e.
* activated neuron values). This function is used in
* backpropagation, single predict and batch predict.
* @param X input vector
*/
std::vector<std::vector<std::valarray<double>>>
__detailed_single_prediction(const std::vector<std::valarray<double>> &X) {
std::vector<std::vector<std::valarray<double>>> details;
std::vector<std::valarray<double>> current_pass = X;
details.emplace_back(X);
for (const auto &l : layers) {
current_pass = multiply(current_pass, l.kernel);
current_pass = apply_function(current_pass, l.activation_function);
details.emplace_back(current_pass);
}
return details;
}
public:
/**
* Default Constructor for class NeuralNetwork. This constructor
* is used to create empty variable of type NeuralNetwork class.
*/
NeuralNetwork() = default;
/**
* Constructor for class NeuralNetwork. This constructor
* is used by user.
* @param config vector containing pair (neurons, activation)
*/
explicit NeuralNetwork(
const std::vector<std::pair<int, std::string>> &config) {
// First layer should not have activation
if (config.begin()->second != "none") {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr
<< "First layer can't have activation other than none got "
<< config.begin()->second;
std::cerr << std::endl;
std::exit(EXIT_FAILURE);
}
// Network should have atleast two layers
if (config.size() <= 1) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Invalid size of network, ";
std::cerr << "Atleast two layers are required";
std::exit(EXIT_FAILURE);
}
// Separately creating first layer so it can have unit matrix
// as kernel.
layers.push_back(neural_network::layers::DenseLayer(
config[0].first, config[0].second,
{config[0].first, config[0].first}, false));
// Creating remaining layers
for (size_t i = 1; i < config.size(); i++) {
layers.push_back(neural_network::layers::DenseLayer(
config[i].first, config[i].second,
{config[i - 1].first, config[i].first}, true));
}
std::cout << "INFO: Network constructed successfully" << std::endl;
}
/**
* Copy Constructor for class NeuralNetwork.
*
* @param model instance of class to be copied.
*/
NeuralNetwork(const NeuralNetwork &model) = default;
/**
* Destructor for class NeuralNetwork.
*/
~NeuralNetwork() = default;
/**
* Copy assignment operator for class NeuralNetwork
*/
NeuralNetwork &operator=(const NeuralNetwork &model) = default;
/**
* Move constructor for class NeuralNetwork
*/
NeuralNetwork(NeuralNetwork &&) = default;
/**
* Move assignment operator for class NeuralNetwork
*/
NeuralNetwork &operator=(NeuralNetwork &&) = default;
/**
* Function to get X and Y from csv file (where X = data, Y = label)
* @param file_name csv file name
* @param last_label flag for whether label is in first or last column
* @param normalize flag for whether to normalize data
* @param slip_lines number of lines to skip
* @return returns pair of X and Y
*/
std::pair<std::vector<std::vector<std::valarray<double>>>,
std::vector<std::vector<std::valarray<double>>>>
get_XY_from_csv(const std::string &file_name, const bool &last_label,
const bool &normalize, const int &slip_lines = 1) {
std::ifstream in_file; // Ifstream to read file
in_file.open(file_name.c_str(), std::ios::in); // Open file
// If there is any problem in opening file
if (!in_file.is_open()) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Unable to open file: " << file_name << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::vector<std::valarray<double>>> X,
Y; // To store X and Y
std::string line; // To store each line
// Skip lines
for (int i = 0; i < slip_lines; i++) {
std::getline(in_file, line, '\n'); // Ignore line
}
// While file has information
while (!in_file.eof() && std::getline(in_file, line, '\n')) {
std::valarray<double> x_data,
y_data; // To store single sample and label
std::stringstream ss(line); // Constructing stringstream from line
std::string token; // To store each token in line (seprated by ',')
while (std::getline(ss, token, ',')) { // For each token
// Insert numerical value of token in x_data
x_data = insert_element(x_data, std::stod(token));
}
// If label is in last column
if (last_label) {
y_data.resize(this->layers.back().neurons);
// If task is classification
if (y_data.size() > 1) {
y_data[x_data[x_data.size() - 1]] = 1;
}
// If task is regrssion (of single value)
else {
y_data[0] = x_data[x_data.size() - 1];
}
x_data = pop_back(x_data); // Remove label from x_data
} else {
y_data.resize(this->layers.back().neurons);
// If task is classification
if (y_data.size() > 1) {
y_data[x_data[x_data.size() - 1]] = 1;
}
// If task is regrssion (of single value)
else {
y_data[0] = x_data[x_data.size() - 1];
}
x_data = pop_front(x_data); // Remove label from x_data
}
// Push collected X_data and y_data in X and Y
X.push_back({x_data});
Y.push_back({y_data});
}
// Normalize training data if flag is set
if (normalize) {
// Scale data between 0 and 1 using min-max scaler
X = minmax_scaler(X, 0.01, 1.0);
}
in_file.close(); // Closing file
return make_pair(X, Y); // Return pair of X and Y
}
/**
* Function to get prediction of model on single sample.
* @param X array of feature vectors
* @return returns predictions as vector
*/
std::vector<std::valarray<double>> single_predict(
const std::vector<std::valarray<double>> &X) {
// Get activations of all layers
auto activations = this->__detailed_single_prediction(X);
// Return activations of last layer (actual predicted values)
return activations.back();
}
/**
* Function to get prediction of model on batch
* @param X array of feature vectors
* @return returns predicted values as vector
*/
std::vector<std::vector<std::valarray<double>>> batch_predict(
const std::vector<std::vector<std::valarray<double>>> &X) {
// Store predicted values
std::vector<std::vector<std::valarray<double>>> predicted_batch(
X.size());
for (size_t i = 0; i < X.size(); i++) { // For every sample
// Push predicted values
predicted_batch[i] = this->single_predict(X[i]);
}
return predicted_batch; // Return predicted values
}
/**
* Function to fit model on supplied data
* @param X array of feature vectors
* @param Y array of target values
* @param epochs number of epochs (default = 100)
* @param learning_rate learning rate (default = 0.01)
* @param batch_size batch size for gradient descent (default = 32)
* @param shuffle flag for whether to shuffle data (default = true)
*/
void fit(const std::vector<std::vector<std::valarray<double>>> &X_,
const std::vector<std::vector<std::valarray<double>>> &Y_,
const int &epochs = 100, const double &learning_rate = 0.01,
const size_t &batch_size = 32, const bool &shuffle = true) {
std::vector<std::vector<std::valarray<double>>> X = X_, Y = Y_;
// Both label and input data should have same size
if (X.size() != Y.size()) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "X and Y in fit have different sizes" << std::endl;
std::exit(EXIT_FAILURE);
}
std::cout << "INFO: Training Started" << std::endl;
for (int epoch = 1; epoch <= epochs; epoch++) { // For every epoch
// Shuffle X and Y if flag is set
if (shuffle) {
equal_shuffle(X, Y);
}
auto start =
std::chrono::high_resolution_clock::now(); // Start clock
double loss = 0,
acc = 0; // Initialize performance metrics with zero
// For each starting index of batch
for (size_t batch_start = 0; batch_start < X.size();
batch_start += batch_size) {
for (size_t i = batch_start;
i < std::min(X.size(), batch_start + batch_size); i++) {
std::vector<std::valarray<double>> grad, cur_error,
predicted;
auto activations = this->__detailed_single_prediction(X[i]);
// Gradients vector to store gradients for all layers
// They will be averaged and applied to kernel
std::vector<std::vector<std::valarray<double>>> gradients;
gradients.resize(this->layers.size());
// First initialize gradients to zero
for (size_t i = 0; i < gradients.size(); i++) {
zeroes_initialization(
gradients[i], get_shape(this->layers[i].kernel));
}
predicted = activations.back(); // Predicted vector
cur_error = predicted - Y[i]; // Absoulute error
// Calculating loss with MSE
loss += sum(apply_function(
cur_error, neural_network::util_functions::square));
// If prediction is correct
if (argmax(predicted) == argmax(Y[i])) {
acc += 1;
}
// For every layer (except first) starting from last one
for (size_t j = this->layers.size() - 1; j >= 1; j--) {
// Backpropogating errors
cur_error = hadamard_product(
cur_error,
apply_function(
activations[j + 1],
this->layers[j].dactivation_function));
// Calculating gradient for current layer
grad = multiply(transpose(activations[j]), cur_error);
// Change error according to current kernel values
cur_error = multiply(cur_error,
transpose(this->layers[j].kernel));
// Adding gradient values to collection of gradients
gradients[j] = gradients[j] + grad / double(batch_size);
}
// Applying gradients
for (size_t j = this->layers.size() - 1; j >= 1; j--) {
// Updating kernel (aka weights)
this->layers[j].kernel = this->layers[j].kernel -
gradients[j] * learning_rate;
}
}
}
auto stop =
std::chrono::high_resolution_clock::now(); // Stoping the clock
// Calculate time taken by epoch
auto duration =
std::chrono::duration_cast<std::chrono::microseconds>(stop -
start);
loss /= X.size(); // Averaging loss
acc /= X.size(); // Averaging accuracy
std::cout.precision(4); // set output precision to 4
// Printing training stats
std::cout << "Training: Epoch " << epoch << '/' << epochs;
std::cout << ", Loss: " << loss;
std::cout << ", Accuracy: " << acc;
std::cout << ", Taken time: " << duration.count() / 1e6
<< " seconds";
std::cout << std::endl;
}
return;
}
/**
* Function to fit model on data stored in csv file
* @param file_name csv file name
* @param last_label flag for whether label is in first or last column
* @param epochs number of epochs
* @param learning_rate learning rate
* @param normalize flag for whether to normalize data
* @param slip_lines number of lines to skip
* @param batch_size batch size for gradient descent (default = 32)
* @param shuffle flag for whether to shuffle data (default = true)
*/
void fit_from_csv(const std::string &file_name, const bool &last_label,
const int &epochs, const double &learning_rate,
const bool &normalize, const int &slip_lines = 1,
const size_t &batch_size = 32,
const bool &shuffle = true) {
// Getting training data from csv file
auto data =
this->get_XY_from_csv(file_name, last_label, normalize, slip_lines);
// Fit the model on training data
this->fit(data.first, data.second, epochs, learning_rate, batch_size,
shuffle);
return;
}
/**
* Function to evaluate model on supplied data
* @param X array of feature vectors (input data)
* @param Y array of target values (label)
*/
void evaluate(const std::vector<std::vector<std::valarray<double>>> &X,
const std::vector<std::vector<std::valarray<double>>> &Y) {
std::cout << "INFO: Evaluation Started" << std::endl;
double acc = 0, loss = 0; // initialize performance metrics with zero
for (size_t i = 0; i < X.size(); i++) { // For every sample in input
// Get predictions
std::vector<std::valarray<double>> pred =
this->single_predict(X[i]);
// If predicted class is correct
if (argmax(pred) == argmax(Y[i])) {
acc += 1; // Increment accuracy
}
// Calculating loss - Mean Squared Error
loss += sum(apply_function((Y[i] - pred),
neural_network::util_functions::square) *
0.5);
}
acc /= X.size(); // Averaging accuracy
loss /= X.size(); // Averaging loss
// Prinitng performance of the model
std::cout << "Evaluation: Loss: " << loss;
std::cout << ", Accuracy: " << acc << std::endl;
return;
}
/**
* Function to evaluate model on data stored in csv file
* @param file_name csv file name
* @param last_label flag for whether label is in first or last column
* @param normalize flag for whether to normalize data
* @param slip_lines number of lines to skip
*/
void evaluate_from_csv(const std::string &file_name, const bool &last_label,
const bool &normalize, const int &slip_lines = 1) {
// Getting training data from csv file
auto data =
this->get_XY_from_csv(file_name, last_label, normalize, slip_lines);
// Evaluating model
this->evaluate(data.first, data.second);
return;
}
/**
* Function to save current model.
* @param file_name file name to save model (*.model)
*/
void save_model(const std::string &_file_name) {
std::string file_name = _file_name;
// Adding ".model" extension if it is not already there in name
if (file_name.find(".model") == file_name.npos) {
file_name += ".model";
}
std::ofstream out_file; // Ofstream to write in file
// Open file in out|trunc mode
out_file.open(file_name.c_str(),
std::ofstream::out | std::ofstream::trunc);
// If there is any problem in opening file
if (!out_file.is_open()) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Unable to open file: " << file_name << std::endl;
std::exit(EXIT_FAILURE);
}
/**
Format in which model is saved:
total_layers
neurons(1st neural_network::layers::DenseLayer) activation_name(1st
neural_network::layers::DenseLayer) kernel_shape(1st
neural_network::layers::DenseLayer) kernel_values
.
.
.
neurons(Nth neural_network::layers::DenseLayer) activation_name(Nth
neural_network::layers::DenseLayer) kernel_shape(Nth
neural_network::layers::DenseLayer) kernel_value
For Example, pretrained model with 3 layers:
<pre>
3
4 none
4 4
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
6 relu
4 6
-1.88963 -3.61165 1.30757 -0.443906 -2.41039 -2.69653
-0.684753 0.0891452 0.795294 -2.39619 2.73377 0.318202
-2.91451 -4.43249 -0.804187 2.51995 -6.97524 -1.07049
-0.571531 -1.81689 -1.24485 1.92264 -2.81322 1.01741
3 sigmoid
6 3
0.390267 -0.391703 -0.0989607
0.499234 -0.564539 -0.28097
0.553386 -0.153974 -1.92493
-2.01336 -0.0219682 1.44145
1.72853 -0.465264 -0.705373
-0.908409 -0.740547 0.376416
</pre>
*/
// Saving model in the same format
out_file << layers.size();
out_file << std::endl;
for (const auto &layer : this->layers) {
out_file << layer.neurons << ' ' << layer.activation << std::endl;
const auto shape = get_shape(layer.kernel);
out_file << shape.first << ' ' << shape.second << std::endl;
for (const auto &row : layer.kernel) {
for (const auto &val : row) {
out_file << val << ' ';
}
out_file << std::endl;
}
}
std::cout << "INFO: Model saved successfully with name : ";
std::cout << file_name << std::endl;
out_file.close(); // Closing file
return;
}
/**
* Function to load earlier saved model.
* @param file_name file from which model will be loaded (*.model)
* @return instance of NeuralNetwork class with pretrained weights
*/
NeuralNetwork load_model(const std::string &file_name) {
std::ifstream in_file; // Ifstream to read file
in_file.open(file_name.c_str()); // Openinig file
// If there is any problem in opening file
if (!in_file.is_open()) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Unable to open file: " << file_name << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::pair<int, std::string>> config; // To store config
std::vector<std::vector<std::valarray<double>>>
kernels; // To store pretrained kernels
// Loading model from saved file format
size_t total_layers = 0;
in_file >> total_layers;
for (size_t i = 0; i < total_layers; i++) {
int neurons = 0;
std::string activation;
size_t shape_a = 0, shape_b = 0;
std::vector<std::valarray<double>> kernel;
in_file >> neurons >> activation >> shape_a >> shape_b;
for (size_t r = 0; r < shape_a; r++) {
std::valarray<double> row(shape_b);
for (size_t c = 0; c < shape_b; c++) {
in_file >> row[c];
}
kernel.push_back(row);
}
config.emplace_back(make_pair(neurons, activation));
;
kernels.emplace_back(kernel);
}
std::cout << "INFO: Model loaded successfully" << std::endl;
in_file.close(); // Closing file
return NeuralNetwork(
config, kernels); // Return instance of NeuralNetwork class
}
/**
* Function to print summary of the network.
*/
void summary() {
// Printing Summary
std::cout
<< "==============================================================="
<< std::endl;
std::cout << "\t\t+ MODEL SUMMARY +\t\t\n";
std::cout
<< "==============================================================="
<< std::endl;
for (size_t i = 1; i <= layers.size(); i++) { // For every layer
std::cout << i << ")";
std::cout << " Neurons : "
<< layers[i - 1].neurons; // number of neurons
std::cout << ", Activation : "
<< layers[i - 1].activation; // activation
std::cout << ", kernel Shape : "
<< get_shape(layers[i - 1].kernel); // kernel shape
std::cout << std::endl;
}
std::cout
<< "==============================================================="
<< std::endl;
return;
}
};
} // namespace neural_network
} // namespace machine_learning
/**
* Function to test neural network
* @returns none
*/
static void test() {
// Creating network with 3 layers for "iris.csv"
machine_learning::neural_network::NeuralNetwork myNN =
machine_learning::neural_network::NeuralNetwork({
{4, "none"}, // First layer with 3 neurons and "none" as activation
{6,
"relu"}, // Second layer with 6 neurons and "relu" as activation
{3, "sigmoid"} // Third layer with 3 neurons and "sigmoid" as
// activation
});
// Printing summary of model
myNN.summary();
// Training Model
myNN.fit_from_csv("iris.csv", true, 100, 0.3, false, 2, 32, true);
// Testing predictions of model
assert(machine_learning::argmax(
myNN.single_predict({{5, 3.4, 1.6, 0.4}})) == 0);
assert(machine_learning::argmax(
myNN.single_predict({{6.4, 2.9, 4.3, 1.3}})) == 1);
assert(machine_learning::argmax(
myNN.single_predict({{6.2, 3.4, 5.4, 2.3}})) == 2);
return;
}
/**
* @brief Main function
* @returns 0 on exit
*/
int main() {
// Testing
test();
return 0;
}
@@ -0,0 +1,473 @@
/**
* @file
* \brief Linear regression example using [Ordinary least
* squares](https://en.wikipedia.org/wiki/Ordinary_least_squares)
*
* Program that gets the number of data samples and number of features per
* sample along with output per sample. It applies OLS regression to compute
* the regression output for additional test data samples.
*
* \author [Krishna Vedala](https://github.com/kvedala)
*/
#include <cassert>
#include <cmath> // for std::abs
#include <iomanip> // for print formatting
#include <iostream>
#include <vector>
/**
* operator to print a matrix
*/
template <typename T>
std::ostream &operator<<(std::ostream &out,
std::vector<std::vector<T>> const &v) {
const int width = 10;
const char separator = ' ';
for (size_t row = 0; row < v.size(); row++) {
for (size_t col = 0; col < v[row].size(); col++) {
out << std::left << std::setw(width) << std::setfill(separator)
<< v[row][col];
}
out << std::endl;
}
return out;
}
/**
* operator to print a vector
*/
template <typename T>
std::ostream &operator<<(std::ostream &out, std::vector<T> const &v) {
const int width = 15;
const char separator = ' ';
for (size_t row = 0; row < v.size(); row++) {
out << std::left << std::setw(width) << std::setfill(separator)
<< v[row];
}
return out;
}
/**
* function to check if given matrix is a square matrix
* \returns 1 if true, 0 if false
*/
template <typename T>
inline bool is_square(std::vector<std::vector<T>> const &A) {
// Assuming A is square matrix
size_t N = A.size();
for (size_t i = 0; i < N; i++) {
if (A[i].size() != N) {
return false;
}
}
return true;
}
/**
* Matrix multiplication such that if A is size (mxn) and
* B is of size (pxq) then the multiplication is defined
* only when n = p and the resultant matrix is of size (mxq)
*
* \returns resultant matrix
**/
template <typename T>
std::vector<std::vector<T>> operator*(std::vector<std::vector<T>> const &A,
std::vector<std::vector<T>> const &B) {
// Number of rows in A
size_t N_A = A.size();
// Number of columns in B
size_t N_B = B[0].size();
std::vector<std::vector<T>> result(N_A);
if (A[0].size() != B.size()) {
std::cerr << "Number of columns in A != Number of rows in B ("
<< A[0].size() << ", " << B.size() << ")" << std::endl;
return result;
}
for (size_t row = 0; row < N_A; row++) {
std::vector<T> v(N_B);
for (size_t col = 0; col < N_B; col++) {
v[col] = static_cast<T>(0);
for (size_t j = 0; j < B.size(); j++) {
v[col] += A[row][j] * B[j][col];
}
}
result[row] = v;
}
return result;
}
/**
* multiplication of a matrix with a column vector
* \returns resultant vector
*/
template <typename T>
std::vector<T> operator*(std::vector<std::vector<T>> const &A,
std::vector<T> const &B) {
// Number of rows in A
size_t N_A = A.size();
std::vector<T> result(N_A);
if (A[0].size() != B.size()) {
std::cerr << "Number of columns in A != Number of rows in B ("
<< A[0].size() << ", " << B.size() << ")" << std::endl;
return result;
}
for (size_t row = 0; row < N_A; row++) {
result[row] = static_cast<T>(0);
for (size_t j = 0; j < B.size(); j++) result[row] += A[row][j] * B[j];
}
return result;
}
/**
* pre-multiplication of a vector by a scalar
* \returns resultant vector
*/
template <typename T>
std::vector<float> operator*(float const scalar, std::vector<T> const &A) {
// Number of rows in A
size_t N_A = A.size();
std::vector<float> result(N_A);
for (size_t row = 0; row < N_A; row++) {
result[row] += A[row] * static_cast<float>(scalar);
}
return result;
}
/**
* post-multiplication of a vector by a scalar
* \returns resultant vector
*/
template <typename T>
std::vector<float> operator*(std::vector<T> const &A, float const scalar) {
// Number of rows in A
size_t N_A = A.size();
std::vector<float> result(N_A);
for (size_t row = 0; row < N_A; row++) {
result[row] = A[row] * static_cast<float>(scalar);
}
return result;
}
/**
* division of a vector by a scalar
* \returns resultant vector
*/
template <typename T>
std::vector<float> operator/(std::vector<T> const &A, float const scalar) {
return (1.f / scalar) * A;
}
/**
* subtraction of two vectors of identical lengths
* \returns resultant vector
*/
template <typename T>
std::vector<T> operator-(std::vector<T> const &A, std::vector<T> const &B) {
// Number of rows in A
size_t N = A.size();
std::vector<T> result(N);
if (B.size() != N) {
std::cerr << "Vector dimensions shouldbe identical!" << std::endl;
return A;
}
for (size_t row = 0; row < N; row++) result[row] = A[row] - B[row];
return result;
}
/**
* addition of two vectors of identical lengths
* \returns resultant vector
*/
template <typename T>
std::vector<T> operator+(std::vector<T> const &A, std::vector<T> const &B) {
// Number of rows in A
size_t N = A.size();
std::vector<T> result(N);
if (B.size() != N) {
std::cerr << "Vector dimensions shouldbe identical!" << std::endl;
return A;
}
for (size_t row = 0; row < N; row++) result[row] = A[row] + B[row];
return result;
}
/**
* Get matrix inverse using Row-trasnformations. Given matrix must
* be a square and non-singular.
* \returns inverse matrix
**/
template <typename T>
std::vector<std::vector<float>> get_inverse(
std::vector<std::vector<T>> const &A) {
// Assuming A is square matrix
size_t N = A.size();
std::vector<std::vector<float>> inverse(N);
for (size_t row = 0; row < N; row++) {
// preallocatae a resultant identity matrix
inverse[row] = std::vector<float>(N);
for (size_t col = 0; col < N; col++) {
inverse[row][col] = (row == col) ? 1.f : 0.f;
}
}
if (!is_square(A)) {
std::cerr << "A must be a square matrix!" << std::endl;
return inverse;
}
// preallocatae a temporary matrix identical to A
std::vector<std::vector<float>> temp(N);
for (size_t row = 0; row < N; row++) {
std::vector<float> v(N);
for (size_t col = 0; col < N; col++) {
v[col] = static_cast<float>(A[row][col]);
}
temp[row] = v;
}
// start transformations
for (size_t row = 0; row < N; row++) {
for (size_t row2 = row; row2 < N && temp[row][row] == 0; row2++) {
// this to ensure diagonal elements are not 0
temp[row] = temp[row] + temp[row2];
inverse[row] = inverse[row] + inverse[row2];
}
for (size_t col2 = row; col2 < N && temp[row][row] == 0; col2++) {
// this to further ensure diagonal elements are not 0
for (size_t row2 = 0; row2 < N; row2++) {
temp[row2][row] = temp[row2][row] + temp[row2][col2];
inverse[row2][row] = inverse[row2][row] + inverse[row2][col2];
}
}
if (temp[row][row] == 0) {
// Probably a low-rank matrix and hence singular
std::cerr << "Low-rank matrix, no inverse!" << std::endl;
return inverse;
}
// set diagonal to 1
auto divisor = static_cast<float>(temp[row][row]);
temp[row] = temp[row] / divisor;
inverse[row] = inverse[row] / divisor;
// Row transformations
for (size_t row2 = 0; row2 < N; row2++) {
if (row2 == row) {
continue;
}
float factor = temp[row2][row];
temp[row2] = temp[row2] - factor * temp[row];
inverse[row2] = inverse[row2] - factor * inverse[row];
}
}
return inverse;
}
/**
* matrix transpose
* \returns resultant matrix
**/
template <typename T>
std::vector<std::vector<T>> get_transpose(
std::vector<std::vector<T>> const &A) {
std::vector<std::vector<T>> result(A[0].size());
for (size_t row = 0; row < A[0].size(); row++) {
std::vector<T> v(A.size());
for (size_t col = 0; col < A.size(); col++) v[col] = A[col][row];
result[row] = v;
}
return result;
}
/**
* Perform Ordinary Least Squares curve fit. This operation is defined as
* \f[\beta = \left(X^TXX^T\right)Y\f]
* \param X feature matrix with rows representing sample vector of features
* \param Y known regression value for each sample
* \returns fitted regression model polynomial coefficients
*/
template <typename T>
std::vector<float> fit_OLS_regressor(std::vector<std::vector<T>> const &X,
std::vector<T> const &Y) {
// NxF
std::vector<std::vector<T>> X2 = X;
for (size_t i = 0; i < X2.size(); i++) {
// add Y-intercept -> Nx(F+1)
X2[i].push_back(1);
}
// (F+1)xN
std::vector<std::vector<T>> Xt = get_transpose(X2);
// (F+1)x(F+1)
std::vector<std::vector<T>> tmp = get_inverse(Xt * X2);
// (F+1)xN
std::vector<std::vector<float>> out = tmp * Xt;
// cout << endl
// << "Projection matrix: " << X2 * out << endl;
// Fx1,1 -> (F+1)^th element is the independent constant
return out * Y;
}
/**
* Given data and OLS model coeffficients, predict
* regression estimates. This operation is defined as
* \f[y_{\text{row}=i} = \sum_{j=\text{columns}}\beta_j\cdot X_{i,j}\f]
*
* \param X feature matrix with rows representing sample vector of features
* \param beta fitted regression model
* \return vector with regression values for each sample
**/
template <typename T>
std::vector<float> predict_OLS_regressor(std::vector<std::vector<T>> const &X,
std::vector<float> const &beta /**< */
) {
std::vector<float> result(X.size());
for (size_t rows = 0; rows < X.size(); rows++) {
// -> start with constant term
result[rows] = beta[X[0].size()];
for (size_t cols = 0; cols < X[0].size(); cols++) {
result[rows] += beta[cols] * X[rows][cols];
}
}
// Nx1
return result;
}
/** Self test checks */
void ols_test() {
/* test function = x^2 -5 */
std::cout << "Test 1 (quadratic function)....";
// create training data set with features = x, x^2, x^3
std::vector<std::vector<float>> data1(
{{-5, 25, -125}, {-1, 1, -1}, {0, 0, 0}, {1, 1, 1}, {6, 36, 216}});
// create corresponding outputs
std::vector<float> Y1({20, -4, -5, -4, 31});
// perform regression modelling
std::vector<float> beta1 = fit_OLS_regressor(data1, Y1);
// create test data set with same features = x, x^2, x^3
std::vector<std::vector<float>> test_data1(
{{-2, 4, -8}, {2, 4, 8}, {-10, 100, -1000}, {10, 100, 1000}});
// expected regression outputs
std::vector<float> expected1({-1, -1, 95, 95});
// predicted regression outputs
std::vector<float> out1 = predict_OLS_regressor(test_data1, beta1);
// compare predicted results are within +-0.01 limit of expected
for (size_t rows = 0; rows < out1.size(); rows++) {
assert(std::abs(out1[rows] - expected1[rows]) < 0.01);
}
std::cout << "passed\n";
/* test function = x^3 + x^2 - 100 */
std::cout << "Test 2 (cubic function)....";
// create training data set with features = x, x^2, x^3
std::vector<std::vector<float>> data2(
{{-5, 25, -125}, {-1, 1, -1}, {0, 0, 0}, {1, 1, 1}, {6, 36, 216}});
// create corresponding outputs
std::vector<float> Y2({-200, -100, -100, 98, 152});
// perform regression modelling
std::vector<float> beta2 = fit_OLS_regressor(data2, Y2);
// create test data set with same features = x, x^2, x^3
std::vector<std::vector<float>> test_data2(
{{-2, 4, -8}, {2, 4, 8}, {-10, 100, -1000}, {10, 100, 1000}});
// expected regression outputs
std::vector<float> expected2({-104, -88, -1000, 1000});
// predicted regression outputs
std::vector<float> out2 = predict_OLS_regressor(test_data2, beta2);
// compare predicted results are within +-0.01 limit of expected
for (size_t rows = 0; rows < out2.size(); rows++) {
assert(std::abs(out2[rows] - expected2[rows]) < 0.01);
}
std::cout << "passed\n";
std::cout << std::endl; // ensure test results are displayed on screen
// (flush stdout)
}
/**
* main function
*/
int main() {
ols_test();
size_t N = 0, F = 0;
std::cout << "Enter number of features: ";
// number of features = columns
std::cin >> F;
std::cout << "Enter number of samples: ";
// number of samples = rows
std::cin >> N;
std::vector<std::vector<float>> data(N);
std::vector<float> Y(N);
std::cout
<< "Enter training data. Per sample, provide features and one output."
<< std::endl;
for (size_t rows = 0; rows < N; rows++) {
std::vector<float> v(F);
std::cout << "Sample# " << rows + 1 << ": ";
for (size_t cols = 0; cols < F; cols++) {
// get the F features
std::cin >> v[cols];
}
data[rows] = v;
// get the corresponding output
std::cin >> Y[rows];
}
std::vector<float> beta = fit_OLS_regressor(data, Y);
std::cout << std::endl << std::endl << "beta:" << beta << std::endl;
size_t T = 0;
std::cout << "Enter number of test samples: ";
// number of test sample inputs
std::cin >> T;
std::vector<std::vector<float>> data2(T);
// vector<float> Y2(T);
for (size_t rows = 0; rows < T; rows++) {
std::cout << "Sample# " << rows + 1 << ": ";
std::vector<float> v(F);
for (size_t cols = 0; cols < F; cols++) std::cin >> v[cols];
data2[rows] = v;
}
std::vector<float> out = predict_OLS_regressor(data2, beta);
for (size_t rows = 0; rows < T; rows++) std::cout << out[rows] << std::endl;
return 0;
}
+514
View File
@@ -0,0 +1,514 @@
/**
* @file vector_ops.hpp
* @author [Deep Raval](https://github.com/imdeep2905)
*
* @brief Various functions for vectors associated with [NeuralNetwork (aka
* Multilayer Perceptron)]
* (https://en.wikipedia.org/wiki/Multilayer_perceptron).
*
*/
#ifndef VECTOR_OPS_FOR_NN
#define VECTOR_OPS_FOR_NN
#include <algorithm>
#include <chrono>
#include <iostream>
#include <random>
#include <valarray>
#include <vector>
/**
* @namespace machine_learning
* @brief Machine Learning algorithms
*/
namespace machine_learning {
/**
* Overloaded operator "<<" to print 2D vector
* @tparam T typename of the vector
* @param out std::ostream to output
* @param A 2D vector to be printed
*/
template <typename T>
std::ostream &operator<<(std::ostream &out,
std::vector<std::valarray<T>> const &A) {
// Setting output precision to 4 in case of floating point numbers
out.precision(4);
for (const auto &a : A) { // For each row in A
for (const auto &x : a) { // For each element in row
std::cout << x << ' '; // print element
}
std::cout << std::endl;
}
return out;
}
/**
* Overloaded operator "<<" to print a pair
* @tparam T typename of the pair
* @param out std::ostream to output
* @param A Pair to be printed
*/
template <typename T>
std::ostream &operator<<(std::ostream &out, const std::pair<T, T> &A) {
// Setting output precision to 4 in case of floating point numbers
out.precision(4);
// printing pair in the form (p, q)
std::cout << "(" << A.first << ", " << A.second << ")";
return out;
}
/**
* Overloaded operator "<<" to print a 1D vector
* @tparam T typename of the vector
* @param out std::ostream to output
* @param A 1D vector to be printed
*/
template <typename T>
std::ostream &operator<<(std::ostream &out, const std::valarray<T> &A) {
// Setting output precision to 4 in case of floating point numbers
out.precision(4);
for (const auto &a : A) { // For every element in the vector.
std::cout << a << ' '; // Print element
}
std::cout << std::endl;
return out;
}
/**
* Function to insert element into 1D vector
* @tparam T typename of the 1D vector and the element
* @param A 1D vector in which element will to be inserted
* @param ele element to be inserted
* @return new resultant vector
*/
template <typename T>
std::valarray<T> insert_element(const std::valarray<T> &A, const T &ele) {
std::valarray<T> B; // New 1D vector to store resultant vector
B.resize(A.size() + 1); // Resizing it accordingly
for (size_t i = 0; i < A.size(); i++) { // For every element in A
B[i] = A[i]; // Copy element in B
}
B[B.size() - 1] = ele; // Inserting new element in last position
return B; // Return resultant vector
}
/**
* Function to remove first element from 1D vector
* @tparam T typename of the vector
* @param A 1D vector from which first element will be removed
* @return new resultant vector
*/
template <typename T>
std::valarray<T> pop_front(const std::valarray<T> &A) {
std::valarray<T> B; // New 1D vector to store resultant vector
B.resize(A.size() - 1); // Resizing it accordingly
for (size_t i = 1; i < A.size();
i++) { // // For every (except first) element in A
B[i - 1] = A[i]; // Copy element in B with left shifted position
}
return B; // Return resultant vector
}
/**
* Function to remove last element from 1D vector
* @tparam T typename of the vector
* @param A 1D vector from which last element will be removed
* @return new resultant vector
*/
template <typename T>
std::valarray<T> pop_back(const std::valarray<T> &A) {
std::valarray<T> B; // New 1D vector to store resultant vector
B.resize(A.size() - 1); // Resizing it accordingly
for (size_t i = 0; i < A.size() - 1;
i++) { // For every (except last) element in A
B[i] = A[i]; // Copy element in B
}
return B; // Return resultant vector
}
/**
* Function to equally shuffle two 3D vectors (used for shuffling training data)
* @tparam T typename of the vector
* @param A First 3D vector
* @param B Second 3D vector
*/
template <typename T>
void equal_shuffle(std::vector<std::vector<std::valarray<T>>> &A,
std::vector<std::vector<std::valarray<T>>> &B) {
// If two vectors have different sizes
if (A.size() != B.size()) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr
<< "Can not equally shuffle two vectors with different sizes: ";
std::cerr << A.size() << " and " << B.size() << std::endl;
std::exit(EXIT_FAILURE);
}
for (size_t i = 0; i < A.size(); i++) { // For every element in A and B
// Genrating random index < size of A and B
std::srand(std::chrono::system_clock::now().time_since_epoch().count());
size_t random_index = std::rand() % A.size();
// Swap elements in both A and B with same random index
std::swap(A[i], A[random_index]);
std::swap(B[i], B[random_index]);
}
return;
}
/**
* Function to initialize given 2D vector using uniform random initialization
* @tparam T typename of the vector
* @param A 2D vector to be initialized
* @param shape required shape
* @param low lower limit on value
* @param high upper limit on value
*/
template <typename T>
void uniform_random_initialization(std::vector<std::valarray<T>> &A,
const std::pair<size_t, size_t> &shape,
const T &low, const T &high) {
A.clear(); // Making A empty
// Uniform distribution in range [low, high]
std::default_random_engine generator(
std::chrono::system_clock::now().time_since_epoch().count());
std::uniform_real_distribution<T> distribution(low, high);
for (size_t i = 0; i < shape.first; i++) { // For every row
std::valarray<T>
row; // Making empty row which will be inserted in vector
row.resize(shape.second);
for (auto &r : row) { // For every element in row
r = distribution(generator); // copy random number
}
A.push_back(row); // Insert new row in vector
}
return;
}
/**
* Function to Intialize 2D vector as unit matrix
* @tparam T typename of the vector
* @param A 2D vector to be initialized
* @param shape required shape
*/
template <typename T>
void unit_matrix_initialization(std::vector<std::valarray<T>> &A,
const std::pair<size_t, size_t> &shape) {
A.clear(); // Making A empty
for (size_t i = 0; i < shape.first; i++) {
std::valarray<T>
row; // Making empty row which will be inserted in vector
row.resize(shape.second);
row[i] = T(1); // Insert 1 at ith position
A.push_back(row); // Insert new row in vector
}
return;
}
/**
* Function to Intialize 2D vector as zeroes
* @tparam T typename of the vector
* @param A 2D vector to be initialized
* @param shape required shape
*/
template <typename T>
void zeroes_initialization(std::vector<std::valarray<T>> &A,
const std::pair<size_t, size_t> &shape) {
A.clear(); // Making A empty
for (size_t i = 0; i < shape.first; i++) {
std::valarray<T>
row; // Making empty row which will be inserted in vector
row.resize(shape.second); // By default all elements are zero
A.push_back(row); // Insert new row in vector
}
return;
}
/**
* Function to get sum of all elements in 2D vector
* @tparam T typename of the vector
* @param A 2D vector for which sum is required
* @return returns sum of all elements of 2D vector
*/
template <typename T>
T sum(const std::vector<std::valarray<T>> &A) {
T cur_sum = 0; // Initially sum is zero
for (const auto &a : A) { // For every row in A
cur_sum += a.sum(); // Add sum of that row to current sum
}
return cur_sum; // Return sum
}
/**
* Function to get shape of given 2D vector
* @tparam T typename of the vector
* @param A 2D vector for which shape is required
* @return shape as pair
*/
template <typename T>
std::pair<size_t, size_t> get_shape(const std::vector<std::valarray<T>> &A) {
const size_t sub_size = (*A.begin()).size();
for (const auto &a : A) {
// If supplied vector don't have same shape in all rows
if (a.size() != sub_size) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Supplied vector is not 2D Matrix" << std::endl;
std::exit(EXIT_FAILURE);
}
}
return std::make_pair(A.size(), sub_size); // Return shape as pair
}
/**
* Function to scale given 3D vector using min-max scaler
* @tparam T typename of the vector
* @param A 3D vector which will be scaled
* @param low new minimum value
* @param high new maximum value
* @return new scaled 3D vector
*/
template <typename T>
std::vector<std::vector<std::valarray<T>>> minmax_scaler(
const std::vector<std::vector<std::valarray<T>>> &A, const T &low,
const T &high) {
std::vector<std::vector<std::valarray<T>>> B =
A; // Copying into new vector B
const auto shape = get_shape(B[0]); // Storing shape of B's every element
// As this function is used for scaling training data vector should be of
// shape (1, X)
if (shape.first != 1) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr
<< "Supplied vector is not supported for minmax scaling, shape: ";
std::cerr << shape << std::endl;
std::exit(EXIT_FAILURE);
}
for (size_t i = 0; i < shape.second; i++) {
T min = B[0][0][i], max = B[0][0][i];
for (size_t j = 0; j < B.size(); j++) {
// Updating minimum and maximum values
min = std::min(min, B[j][0][i]);
max = std::max(max, B[j][0][i]);
}
for (size_t j = 0; j < B.size(); j++) {
// Applying min-max scaler formula
B[j][0][i] =
((B[j][0][i] - min) / (max - min)) * (high - low) + low;
}
}
return B; // Return new resultant 3D vector
}
/**
* Function to get index of maximum element in 2D vector
* @tparam T typename of the vector
* @param A 2D vector for which maximum index is required
* @return index of maximum element
*/
template <typename T>
size_t argmax(const std::vector<std::valarray<T>> &A) {
const auto shape = get_shape(A);
// As this function is used on predicted (or target) vector, shape should be
// (1, X)
if (shape.first != 1) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Supplied vector is ineligible for argmax" << std::endl;
std::exit(EXIT_FAILURE);
}
// Return distance of max element from first element (i.e. index)
return std::distance(std::begin(A[0]),
std::max_element(std::begin(A[0]), std::end(A[0])));
}
/**
* Function which applys supplied function to every element of 2D vector
* @tparam T typename of the vector
* @param A 2D vector on which function will be applied
* @param func Function to be applied
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> apply_function(
const std::vector<std::valarray<T>> &A, T (*func)(const T &)) {
std::vector<std::valarray<double>> B =
A; // New vector to store resultant vector
for (auto &b : B) { // For every row in vector
b = b.apply(func); // Apply function to that row
}
return B; // Return new resultant 2D vector
}
/**
* Overloaded operator "*" to multiply given 2D vector with scaler
* @tparam T typename of both vector and the scaler
* @param A 2D vector to which scaler will be multiplied
* @param val Scaler value which will be multiplied
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> operator*(const std::vector<std::valarray<T>> &A,
const T &val) {
std::vector<std::valarray<double>> B =
A; // New vector to store resultant vector
for (auto &b : B) { // For every row in vector
b = b * val; // Multiply row with scaler
}
return B; // Return new resultant 2D vector
}
/**
* Overloaded operator "/" to divide given 2D vector with scaler
* @tparam T typename of the vector and the scaler
* @param A 2D vector to which scaler will be divided
* @param val Scaler value which will be divided
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> operator/(const std::vector<std::valarray<T>> &A,
const T &val) {
std::vector<std::valarray<double>> B =
A; // New vector to store resultant vector
for (auto &b : B) { // For every row in vector
b = b / val; // Divide row with scaler
}
return B; // Return new resultant 2D vector
}
/**
* Function to get transpose of 2D vector
* @tparam T typename of the vector
* @param A 2D vector which will be transposed
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> transpose(
const std::vector<std::valarray<T>> &A) {
const auto shape = get_shape(A); // Current shape of vector
std::vector<std::valarray<T>> B; // New vector to store result
// Storing transpose values of A in B
for (size_t j = 0; j < shape.second; j++) {
std::valarray<T> row;
row.resize(shape.first);
for (size_t i = 0; i < shape.first; i++) {
row[i] = A[i][j];
}
B.push_back(row);
}
return B; // Return new resultant 2D vector
}
/**
* Overloaded operator "+" to add two 2D vectors
* @tparam T typename of the vector
* @param A First 2D vector
* @param B Second 2D vector
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> operator+(
const std::vector<std::valarray<T>> &A,
const std::vector<std::valarray<T>> &B) {
const auto shape_a = get_shape(A);
const auto shape_b = get_shape(B);
// If vectors don't have equal shape
if (shape_a.first != shape_b.first || shape_a.second != shape_b.second) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Supplied vectors have different shapes ";
std::cerr << shape_a << " and " << shape_b << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::valarray<T>> C;
for (size_t i = 0; i < A.size(); i++) { // For every row
C.push_back(A[i] + B[i]); // Elementwise addition
}
return C; // Return new resultant 2D vector
}
/**
* Overloaded operator "-" to add subtract 2D vectors
* @tparam T typename of the vector
* @param A First 2D vector
* @param B Second 2D vector
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> operator-(
const std::vector<std::valarray<T>> &A,
const std::vector<std::valarray<T>> &B) {
const auto shape_a = get_shape(A);
const auto shape_b = get_shape(B);
// If vectors don't have equal shape
if (shape_a.first != shape_b.first || shape_a.second != shape_b.second) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Supplied vectors have different shapes ";
std::cerr << shape_a << " and " << shape_b << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::valarray<T>> C; // Vector to store result
for (size_t i = 0; i < A.size(); i++) { // For every row
C.push_back(A[i] - B[i]); // Elementwise substraction
}
return C; // Return new resultant 2D vector
}
/**
* Function to multiply two 2D vectors
* @tparam T typename of the vector
* @param A First 2D vector
* @param B Second 2D vector
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> multiply(const std::vector<std::valarray<T>> &A,
const std::vector<std::valarray<T>> &B) {
const auto shape_a = get_shape(A);
const auto shape_b = get_shape(B);
// If vectors are not eligible for multiplication
if (shape_a.second != shape_b.first) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Vectors are not eligible for multiplication ";
std::cerr << shape_a << " and " << shape_b << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::valarray<T>> C; // Vector to store result
// Normal matrix multiplication
for (size_t i = 0; i < shape_a.first; i++) {
std::valarray<T> row;
row.resize(shape_b.second);
for (size_t j = 0; j < shape_b.second; j++) {
for (size_t k = 0; k < shape_a.second; k++) {
row[j] += A[i][k] * B[k][j];
}
}
C.push_back(row);
}
return C; // Return new resultant 2D vector
}
/**
* Function to get hadamard product of two 2D vectors
* @tparam T typename of the vector
* @param A First 2D vector
* @param B Second 2D vector
* @return new resultant vector
*/
template <typename T>
std::vector<std::valarray<T>> hadamard_product(
const std::vector<std::valarray<T>> &A,
const std::vector<std::valarray<T>> &B) {
const auto shape_a = get_shape(A);
const auto shape_b = get_shape(B);
// If vectors are not eligible for hadamard product
if (shape_a.first != shape_b.first || shape_a.second != shape_b.second) {
std::cerr << "ERROR (" << __func__ << ") : ";
std::cerr << "Vectors have different shapes ";
std::cerr << shape_a << " and " << shape_b << std::endl;
std::exit(EXIT_FAILURE);
}
std::vector<std::valarray<T>> C; // Vector to store result
for (size_t i = 0; i < A.size(); i++) {
C.push_back(A[i] * B[i]); // Elementwise multiplication
}
return C; // Return new resultant 2D vector
}
} // namespace machine_learning
#endif