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
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:
@@ -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} )
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
* ```
|
||||
* 
|
||||
*/
|
||||
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"
|
||||
* ```
|
||||
* 
|
||||
*/
|
||||
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"
|
||||
* ```
|
||||
* 
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user