diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index f4ae043..c1b396a 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -1,37 +1,37 @@ -name: CMake - -on: - push: - branches: [ development ] - pull_request: - branches: [ development ] - -env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) - BUILD_TYPE: Release - -jobs: - build: - # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. - # You can convert this to a matrix build if you need cross-platform coverage. - # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type - run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} - - - name: Build - # Build your program with the given configuration - run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - - name: Test - working-directory: ${{github.workspace}}/build - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest -C ${{env.BUILD_TYPE}} - +name: CMake + +on: + push: + branches: [ development ] + pull_request: + branches: [ development ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + + - name: Build + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Test + working-directory: ${{github.workspace}}/build + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest -C ${{env.BUILD_TYPE}} + diff --git a/.gitignore b/.gitignore index aeb4402..2674946 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ -*.DS_Store -cmake-build-debug -cmake-build-release -AdversarialSearch/Minimax/Main -AdversarialSearch/Minimax/Main.dSYM/ -build -*.asta -*.asta.lock -*.jude -*.idea +*.DS_Store +cmake-build-debug +cmake-build-release +AdversarialSearch/Minimax/Main +AdversarialSearch/Minimax/Main.dSYM/ +build +*.asta +*.asta.lock +*.jude +*.idea +cmake-build-release \ No newline at end of file diff --git a/AdversarialSearch/Minimax/Main.cpp b/AdversarialSearch/Minimax/Main.cpp deleted file mode 100644 index 448fe0b..0000000 --- a/AdversarialSearch/Minimax/Main.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "MinimaxTester.h" - -#define RUN_MINIMAX_TESTS 0 - -int main(int argc, char *argv[]) -{ - if (argc > 1) - { - int boardState = atoi(argv[1]); - TicTacToeMinimax::benchmarkMinimax(boardState, true); - } - else - { - if (RUN_MINIMAX_TESTS) - { - TicTacToeMinimax::RunMinimaxTests(); - } - else - { - TicTacToeMinimax::benchmarkMinimaxVsMinimax(0, true); - } - } - return 0; -} diff --git a/AdversarialSearch/Minimax/MinimaxTester.h b/AdversarialSearch/Minimax/MinimaxTester.h deleted file mode 100644 index f1d3e15..0000000 --- a/AdversarialSearch/Minimax/MinimaxTester.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include -#include "TicTacToeMinimax.h" - -using namespace std; - -namespace TicTacToeMinimax -{ - namespace - { - string getStateText(int state) - { - if (state == TicTacToeMinimax::PLAYING) - return "Playing"; - else if (state == TicTacToeMinimax::DRAW) - return "Draw"; - else if (state == TicTacToeMinimax::CROSS_WINS) - return "CrossWins"; - return "CircleWins"; - } - } - - void RunMinimaxTests(); - void benchmarkMinimax(int board, bool isMaxTurn); - void benchmarkMinimaxVsMinimax(int board, bool isMaxTurn); - void benchmarkEvaluate(int board, bool isMaxTurn); - void benchmarkEvaluateAll(int board, bool isMaxTurn); - void printBoard(int board); - void printState(int state); -} diff --git a/AdversarialSearch/Minimax/CMakeLists.txt b/AdversarialSearch/Minimax/Tests/CMakeLists.txt similarity index 51% rename from AdversarialSearch/Minimax/CMakeLists.txt rename to AdversarialSearch/Minimax/Tests/CMakeLists.txt index ad474b9..e8b1214 100644 --- a/AdversarialSearch/Minimax/CMakeLists.txt +++ b/AdversarialSearch/Minimax/Tests/CMakeLists.txt @@ -1,6 +1,6 @@ -set(FILES - TicTacToeMinimax.cpp - MinimaxTester.cpp - Main.cpp -) -add_executable(Minimax ${FILES}) +set(FILES + ../TicTacToeMinimax.cpp + MinimaxTester.cpp + MinimaxTestMain.cpp +) +add_executable(Minimax ${FILES}) diff --git a/AdversarialSearch/Minimax/Tests/MinimaxTestMain.cpp b/AdversarialSearch/Minimax/Tests/MinimaxTestMain.cpp new file mode 100644 index 0000000..7514c90 --- /dev/null +++ b/AdversarialSearch/Minimax/Tests/MinimaxTestMain.cpp @@ -0,0 +1,16 @@ +#include "MinimaxTester.h" + +int main(int argc, char *argv[]) +{ + if (argc > 1) + { + int boardState = atoi(argv[1]); + TicTacToeMinimax::BenchmarkMinimax(boardState, true); + } + else + { + TicTacToeMinimax::RunMinimaxTests(); + TicTacToeMinimax::BenchmarkMinimaxVsMinimax(0, true); + } + return 0; +} diff --git a/AdversarialSearch/Minimax/MinimaxTester.cpp b/AdversarialSearch/Minimax/Tests/MinimaxTester.cpp similarity index 70% rename from AdversarialSearch/Minimax/MinimaxTester.cpp rename to AdversarialSearch/Minimax/Tests/MinimaxTester.cpp index 5386983..23a33ff 100644 --- a/AdversarialSearch/Minimax/MinimaxTester.cpp +++ b/AdversarialSearch/Minimax/Tests/MinimaxTester.cpp @@ -1,149 +1,170 @@ -#include "MinimaxTester.h" - -struct MinimaxTestCase -{ -public: - const int CurrentBoard; - const vector ExpectedBoards; - const bool IsMaxTurn; - MinimaxTestCase(int currentBoard, bool isMaxTurn, vector expectedNextBoard) - : CurrentBoard(currentBoard), ExpectedBoards(expectedNextBoard), IsMaxTurn(isMaxTurn) - { - // - } -}; - -void TicTacToeMinimax::RunMinimaxTests() -{ - vector testCases = { - MinimaxTestCase(0b10'10'00'00'11'00'00'00'00, true, {0b10'10'11'00'11'00'00'00'00}), - MinimaxTestCase(0b11'11'00'00'10'00'00'00'00, false, {0b11'11'10'00'10'00'00'00'00}), - MinimaxTestCase(0b00'00'00'00'11'00'00'10'10, true, {0b00'00'00'00'11'00'11'10'10}), - MinimaxTestCase(0b00'00'00'00'10'00'00'11'11, false, {0b00'00'00'00'10'00'10'11'11}), - MinimaxTestCase(0b00'10'10'00'11'11'00'00'00, true, {0b00'10'10'11'11'11'00'00'00}), - MinimaxTestCase(0b00'11'11'00'10'10'00'00'00, false, {0b00'11'11'10'10'10'00'00'00}), - MinimaxTestCase(0b00'11'00'11'10'11'10'00'10, true, {0b11'11'00'11'10'11'10'00'10, - 0b00'11'11'11'10'11'10'00'10, - 0b00'11'00'11'10'11'10'11'10}), - MinimaxTestCase(0b00'10'00'10'11'10'11'00'11, false, {0b10'10'00'10'11'10'11'00'11, - 0b00'10'10'10'11'10'11'00'11, - 0b00'10'00'10'11'10'11'10'11}), - }; - - for(unsigned int i = 0; i < testCases.size(); i++) - { - std::cout << "Test Case " << (i + 1) << " ("; - MinimaxTestCase testCase = testCases[i]; - std::cout << std::boolalpha; - std::cout << "IsMaxTurn=" << testCase.IsMaxTurn << " / "; - std::cout << "Board=" << testCase.CurrentBoard << " / "; - std::cout << "Expected="; - bool isResultExpected = false; - int result = TicTacToeMinimax::predict(testCase.CurrentBoard, testCase.IsMaxTurn, 6); - for(unsigned int x = 0; x < testCase.ExpectedBoards.size(); x++) - { - if (i > 0U && i < testCase.ExpectedBoards.size() - 1U) - { - std::cout << ";"; - } - int expected = testCase.ExpectedBoards[x]; - std::cout << expected; - isResultExpected |= result == expected; - } - std::cout << ") "; - if (isResultExpected) - { - std::cout << "[PASSED]"; - } - else - { - std::cout << "[FAILED] Result was=" << result; - std::cout << "CurrentBoard:" << std::endl; - printBoard(testCase.CurrentBoard); - std::cout << "Predicted board state:" << std::endl; - printBoard(result); - } - std::cout << std::endl; - } -} - -void TicTacToeMinimax::benchmarkMinimax(int board, bool isMaxTurn) -{ - long long int totalDuration = 0ll; - printBoard(board); - int state = TicTacToeMinimax::getState(board); - if (state == PLAYING) - { - auto begin = std::chrono::steady_clock::now(); - board = TicTacToeMinimax::predict(board, isMaxTurn, 6); - auto end = std::chrono::steady_clock::now(); - totalDuration += (end - begin).count(); - } - printBoard(board); - printState(state); - cout << "benchmarkMinimax: " << totalDuration << " ns"; - cout << endl << endl; -} - -void TicTacToeMinimax::benchmarkMinimaxVsMinimax(int board, bool isMaxTurn) -{ - std::vector durationByMove; - long long int totalDuration = 0ll; - int firstBoard = board; - int currentBoard = firstBoard; - int state = TicTacToeMinimax::getState(currentBoard); - while (state == PLAYING) - { - auto begin = std::chrono::steady_clock::now(); - int bestMove = TicTacToeMinimax::predict(currentBoard, isMaxTurn, 6); - auto end = std::chrono::steady_clock::now(); - long long int nanos = (end - begin).count(); - totalDuration += nanos; - durationByMove.emplace_back(nanos); - isMaxTurn = !isMaxTurn; - currentBoard = bestMove; - state = TicTacToeMinimax::getState(currentBoard); - if (state != PLAYING && state != DRAW) - { - std::cerr << "'" << getStateText(state) << "' state must never happen during a Minimax vs Minimax battle!" << std::endl; - return; - } - } - printBoard(firstBoard); - printBoard(currentBoard); - printState(state); - for (int i = 0; i < durationByMove.size(); ++i) - { - long long int nanoseconds = durationByMove[i]; - printf("Move %i took %.6f ms\n", (i + 1), (nanoseconds / 1'000'000.0)); - } - printf("Total duration: %.6f ms\n", (totalDuration / 1'000'000.0)); - cout << endl << endl; -} - -void TicTacToeMinimax::printBoard(int board) -{ - int crossMask = 3; - cout << endl; - for (int x = 0; x < 9; x++) - { - if (x > 0 && x % 3 == 0) cout << endl; - if ((board & crossMask) == 0) - { - cout << "[ ]"; - } - else - { - if ((board & crossMask) == crossMask) cout << "[X]"; - else cout << "[O]"; - } - crossMask <<= 2; - } - cout << endl; -} - -void TicTacToeMinimax::printState(int state) -{ - string stateText = getStateText(state); - cout << stateText << endl; -} +#include "MinimaxTester.h" + +/// @brief Get a string representation for a given 'TicTacToeMinimax' flag. +/// @param state A 'TicTacToeMinimax' flag. +/// @return An string representing the 'state' parameter. +string GetStateText(int state) +{ + if (state == TicTacToeMinimax::PLAYING) + return "Playing"; + else if (state == TicTacToeMinimax::DRAW) + return "Draw"; + else if (state == TicTacToeMinimax::CROSS_WINS) + return "CrossWins"; + return "CircleWins"; +} + +struct MinimaxTestCase +{ +public: + const int CurrentBoard; + const vector ExpectedBoards; + const bool IsMaxTurn; + MinimaxTestCase(int currentBoard, bool isMaxTurn, vector expectedNextBoard) + : CurrentBoard(currentBoard), ExpectedBoards(expectedNextBoard), IsMaxTurn(isMaxTurn) + { + // + } +}; + +void TicTacToeMinimax::RunMinimaxTests() +{ + const vector testCases = { + MinimaxTestCase(0b10'10'00'00'11'00'00'00'00, true, {0b10'10'11'00'11'00'00'00'00}), + MinimaxTestCase(0b11'11'00'00'10'00'00'00'00, false, {0b11'11'10'00'10'00'00'00'00}), + MinimaxTestCase(0b00'00'00'00'11'00'00'10'10, true, {0b00'00'00'00'11'00'11'10'10}), + MinimaxTestCase(0b00'00'00'00'10'00'00'11'11, false, {0b00'00'00'00'10'00'10'11'11}), + MinimaxTestCase(0b00'10'10'00'11'11'00'00'00, true, {0b00'10'10'11'11'11'00'00'00}), + MinimaxTestCase(0b00'11'11'00'10'10'00'00'00, false, {0b00'11'11'10'10'10'00'00'00}), + MinimaxTestCase(0b00'11'00'11'10'11'10'00'10, true, {0b11'11'00'11'10'11'10'00'10, + 0b00'11'11'11'10'11'10'00'10, + 0b00'11'00'11'10'11'10'11'10}), + MinimaxTestCase(0b00'10'00'10'11'10'11'00'11, false, {0b10'10'00'10'11'10'11'00'11, + 0b00'10'10'10'11'10'11'00'11, + 0b00'10'00'10'11'10'11'10'11}), + }; + + for(unsigned int i = 0; i < testCases.size(); i++) + { + std::cout << "Test Case " << (i + 1) << " ("; + const MinimaxTestCase testCase = testCases[i]; + std::cout << std::boolalpha; + std::cout << "IsMaxTurn=" << testCase.IsMaxTurn << " / "; + std::cout << "Board=" << testCase.CurrentBoard << " / "; + std::cout << "Expected="; + bool isResultExpected = false; + int result = TicTacToeMinimax::Predict(testCase.CurrentBoard, testCase.IsMaxTurn, 6); + for(unsigned int x = 0; x < testCase.ExpectedBoards.size(); x++) + { + if (i > 0U && i < testCase.ExpectedBoards.size() - 1U) + { + std::cout << ";"; + } + int expected = testCase.ExpectedBoards[x]; + std::cout << expected; + isResultExpected |= result == expected; + } + std::cout << ") "; + if (isResultExpected) + { + std::cout << "[PASSED]"; + } + else + { + std::cout << "[FAILED] Result was=" << result; + std::cout << "CurrentBoard:" << std::endl; + PrintBoard(testCase.CurrentBoard); + std::cout << "Predicted board state:" << std::endl; + PrintBoard(result); + } + std::cout << std::endl; + } +} + +void TicTacToeMinimax::BenchmarkMinimax(int board, bool isMaxTurn) +{ + long long int totalDuration = 0ll; + PrintBoard(board); + int state = TicTacToeMinimax::GetState(board); + if (state == PLAYING) + { + auto begin = std::chrono::steady_clock::now(); + board = TicTacToeMinimax::Predict(board, isMaxTurn, 6); + auto end = std::chrono::steady_clock::now(); + totalDuration += (end - begin).count(); + } + PrintBoard(board); + PrintState(state); + cout << "benchmarkMinimax: " << totalDuration << " ns"; + cout << endl << endl; +} + +void TicTacToeMinimax::BenchmarkMinimaxVsMinimax(int board, bool isMaxTurn) +{ + std::vector durationByMove; + long long int totalDuration = 0ll; + int firstBoard = board; + int currentBoard = firstBoard; + int state = TicTacToeMinimax::GetState(currentBoard); + while (state == PLAYING) + { + auto begin = std::chrono::steady_clock::now(); + + // actually run the minimax algorithm + int bestMove = TicTacToeMinimax::Predict(currentBoard, isMaxTurn, 6); + + auto end = std::chrono::steady_clock::now(); + // calculate the algorithm's duration in nanoseconds + long long int nanos = (end - begin).count(); + totalDuration += nanos; + // store the duration + durationByMove.emplace_back(nanos); + + isMaxTurn = !isMaxTurn; + currentBoard = bestMove; + state = TicTacToeMinimax::GetState(currentBoard); + // print an error in case the algorithm ends up doing something completely wrong + if (state != PLAYING && state != DRAW) + { + std::cerr << "'" << GetStateText(state) << "' state must never happen during a Minimax vs Minimax battle!" << std::endl; + return; + } + } + PrintBoard(firstBoard); + PrintBoard(currentBoard); + PrintState(state); + for (int i = 0; i < durationByMove.size(); ++i) + { + long long int nanoseconds = durationByMove[i]; + printf("Move %i took %.6f ms\n", (i + 1), (nanoseconds / 1'000'000.0)); + } + printf("Total duration: %.6f ms\n", (totalDuration / 1'000'000.0)); + cout << endl << endl; +} + +void TicTacToeMinimax::PrintBoard(int board) +{ + int crossMask = 3; + cout << endl; + for (int x = 0; x < 9; x++) + { + if (x > 0 && x % 3 == 0) cout << endl; + if ((board & crossMask) == 0) + { + cout << "[ ]"; + } + else + { + if ((board & crossMask) == crossMask) cout << "[X]"; + else cout << "[O]"; + } + crossMask <<= 2; + } + cout << endl; +} + +void TicTacToeMinimax::PrintState(int state) +{ + string stateText = GetStateText(state); + cout << stateText << endl; +} diff --git a/AdversarialSearch/Minimax/Tests/MinimaxTester.h b/AdversarialSearch/Minimax/Tests/MinimaxTester.h new file mode 100644 index 0000000..edee851 --- /dev/null +++ b/AdversarialSearch/Minimax/Tests/MinimaxTester.h @@ -0,0 +1,25 @@ +#pragma once + +#include "../TicTacToeMinimax.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace TicTacToeMinimax +{ + void RunMinimaxTests(); + void BenchmarkMinimax(int board, bool isMaxTurn); + void BenchmarkMinimaxVsMinimax(int board, bool isMaxTurn); + void BenchmarkEvaluate(int board, bool isMaxTurn); + void BenchmarkEvaluateAll(int board, bool isMaxTurn); + void PrintBoard(int board); + void PrintState(int state); +} diff --git a/AdversarialSearch/Minimax/TicTacToeMinimax.cpp b/AdversarialSearch/Minimax/TicTacToeMinimax.cpp index 0a9c49d..99b66ba 100644 --- a/AdversarialSearch/Minimax/TicTacToeMinimax.cpp +++ b/AdversarialSearch/Minimax/TicTacToeMinimax.cpp @@ -1,214 +1,218 @@ -#include "TicTacToeMinimax.h" - -#define MAXIMUM_MASK_VALUE 0b1'00'00'00'00'00'00'00'00'00 -#define DRAW_BOARD_STATE 0b10'10'10'10'10'10'10'10'10 -const int winnerMasks[8] = -{ - 0b10'10'10'00'00'00'00'00'00, - 0b10'00'00'10'00'00'10'00'00, - 0b00'00'00'00'00'00'10'10'10, - 0b00'00'10'00'00'10'00'00'10, - 0b10'00'00'00'10'00'00'00'10, - 0b00'10'00'00'10'00'00'10'00, - 0b00'00'00'10'10'10'00'00'00, - 0b00'00'10'00'10'00'10'00'00, -}; -int _maxDepth; - -namespace TicTacToeMinimax -{ - int predictMinTreeScore(const int board, int alpha, int beta, int depth); - - int predictMaxTreeScore(const int board, int alpha, int beta, int depth) - { - const int state = getState(board); - if (state != PLAYING || depth >= _maxDepth) - { - return state; - } - int bestScore = INT_MIN; - int mask = 0b11; - depth++; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = board | mask; - const int score = predictMinTreeScore(newBoard, alpha, beta, depth); - if (score >= beta) - return score; - if (score > alpha) - alpha = score; - if (score > bestScore) - bestScore = score; - } - mask <<= 2; - } - return bestScore; - } - - int predictMinTreeScore(int board, int alpha, int beta, int depth) - { - const int state = getState(board); - if (state != PLAYING || depth >= _maxDepth) - { - return state; - } - int bestScore = INT_MAX; - int mask = 0b10; - depth++; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = board | mask; - const int score = predictMaxTreeScore(newBoard, alpha, beta, depth); - if (score <= alpha) - return score; - if (score < beta) - beta = score; - if (score < bestScore) - bestScore = score; - } - mask <<= 2; - } - return bestScore; - } - - int predictMaxDecision(int board) - { - int bestMove = 0; - int bestMoveScore = INT_MIN; - int mask = 0b11; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = board | mask; - const int score = predictMinTreeScore(newBoard, INT_MIN, INT_MAX, 1); - if (score > bestMoveScore) - { - bestMoveScore = score; - bestMove = newBoard; - } - } - mask <<= 2; - } - return bestMove; - } - - int predictMinDecision(int board) - { - int bestMove = 0; - int bestMoveScore = INT_MAX; - int mask = 0b10; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = board | mask; - const int score = predictMaxTreeScore(newBoard, INT_MIN, INT_MAX, 1); - if (score < bestMoveScore) - { - bestMoveScore = score; - bestMove = newBoard; - } - } - mask <<= 2; - } - return bestMove; - } - - std::vector predictMaxDecisions(int board) - { - std::vector bestMoves; - int bestMoveScore = INT_MIN; - int mask = 0b11; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = (board | mask); - const int score = predictMinTreeScore(newBoard, INT_MIN, INT_MAX, 1); - if (score > bestMoveScore) - { - bestMoveScore = score; - bestMoves.clear(); - bestMoves.push_back(newBoard); - } - else if (score == bestMoveScore) - { - bestMoves.push_back(newBoard); - } - } - mask <<= 2; - } - return bestMoves; - } - - std::vector predictMinDecisions(int board) - { - std::vector bestMoves; - int bestMoveScore = INT_MAX; - int mask = 0b10; - while (mask < MAXIMUM_MASK_VALUE) - { - if ((board & mask) == 0) - { - const int newBoard = (board | mask); - const int score = predictMaxTreeScore(newBoard, INT_MIN, INT_MAX, 1); - if (score < bestMoveScore) - { - bestMoveScore = score; - bestMoves.clear(); - bestMoves.push_back(newBoard); - } - else if (score == bestMoveScore) - { - bestMoves.push_back(newBoard); - } - } - mask <<= 2; - } - return bestMoves; - } - - int predict(int board, bool isMaxTurn, int maxDepth) - { - _maxDepth = maxDepth; - if (isMaxTurn) - { - return predictMaxDecision(board); - } - return predictMinDecision(board); - } - - std::vector predictAll(int board, bool isMaxTurn, int maxDepth) - { - _maxDepth = maxDepth; - if (isMaxTurn) - { - return predictMaxDecisions(board); - } - return predictMinDecisions(board); - } - - int getState(int board) - { - for(int winnerMask : winnerMasks) - { - if ((board & winnerMask) == winnerMask) - { - winnerMask = winnerMask >> 1; - int piecesXor = (winnerMask ^ board) & winnerMask; - if (piecesXor == 0) - return CROSS_WINS; - if (piecesXor == winnerMask) - return CIRCLE_WINS; - } - } - if ((board & DRAW_BOARD_STATE) == DRAW_BOARD_STATE) - return DRAW; - return PLAYING; - } -} +#include "TicTacToeMinimax.h" + +#define MAXIMUM_MASK_VALUE 0b1'00'00'00'00'00'00'00'00'00 +#define DRAW_BOARD_STATE 0b10'10'10'10'10'10'10'10'10 +const int winnerMasks[8] = +{ + 0b10'10'10'00'00'00'00'00'00, + 0b10'00'00'10'00'00'10'00'00, + 0b00'00'00'00'00'00'10'10'10, + 0b00'00'10'00'00'10'00'00'10, + 0b10'00'00'00'10'00'00'00'10, + 0b00'10'00'00'10'00'00'10'00, + 0b00'00'00'10'10'10'00'00'00, + 0b00'00'10'00'10'00'10'00'00, +}; +int _maxDepth; + +namespace TicTacToeMinimax +{ + int PredictMinTreeScore(const int board, int alpha, int beta, int depth); + + int PredictMaxTreeScore(const int board, int alpha, int beta, int depth) + { + const int state = GetState(board); + if (state != PLAYING || depth >= _maxDepth) + { + return state; + } + int bestScore = INT_MIN; + int mask = 0b11; + depth++; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = board | mask; + const int score = PredictMinTreeScore(newBoard, alpha, beta, depth); + if (score >= beta) + return score; + if (score > alpha) + alpha = score; + if (score > bestScore) + bestScore = score; + } + mask <<= 2; + } + return bestScore; + } + + int PredictMinTreeScore(int board, int alpha, int beta, int depth) + { + const int state = GetState(board); + if (state != PLAYING || depth >= _maxDepth) + { + return state; + } + int bestScore = INT_MAX; + int mask = 0b10; + depth++; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = board | mask; + const int score = PredictMaxTreeScore(newBoard, alpha, beta, depth); + if (score <= alpha) + return score; + if (score < beta) + beta = score; + if (score < bestScore) + bestScore = score; + } + mask <<= 2; + } + return bestScore; + } + + int PredictMaxDecision(int board) + { + int bestMove = 0; + int bestMoveScore = INT_MIN; + int mask = 0b11; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = board | mask; + const int score = PredictMinTreeScore(newBoard, INT_MIN, INT_MAX, 1); + if (score > bestMoveScore) + { + bestMoveScore = score; + bestMove = newBoard; + } + } + mask <<= 2; + } + return bestMove; + } + + int PredictMinDecision(int board) + { + int bestMove = 0; + int bestMoveScore = INT_MAX; + int mask = 0b10; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = board | mask; + const int score = PredictMaxTreeScore(newBoard, INT_MIN, INT_MAX, 1); + if (score < bestMoveScore) + { + bestMoveScore = score; + bestMove = newBoard; + } + } + mask <<= 2; + } + return bestMove; + } + + std::vector PredictMaxDecisions(int board) + { + std::vector bestMoves; + int bestMoveScore = INT_MIN; + int mask = 0b11; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = (board | mask); + const int score = PredictMinTreeScore(newBoard, INT_MIN, INT_MAX, 1); + if (score > bestMoveScore) + { + bestMoveScore = score; + bestMoves.clear(); + bestMoves.push_back(newBoard); + } + else if (score == bestMoveScore) + { + bestMoves.push_back(newBoard); + } + } + mask <<= 2; + } + return bestMoves; + } + + std::vector PredictMinDecisions(int board) + { + std::vector bestMoves; + int bestMoveScore = INT_MAX; + int mask = 0b10; + while (mask < MAXIMUM_MASK_VALUE) + { + if ((board & mask) == 0) + { + const int newBoard = (board | mask); + const int score = PredictMaxTreeScore(newBoard, INT_MIN, INT_MAX, 1); + if (score < bestMoveScore) + { + bestMoveScore = score; + bestMoves.clear(); + bestMoves.push_back(newBoard); + } + else if (score == bestMoveScore) + { + bestMoves.push_back(newBoard); + } + } + mask <<= 2; + } + return bestMoves; + } + + int Predict(int board, bool isMaxTurn, int maxDepth) + { + _maxDepth = maxDepth; + if (isMaxTurn) + { + return PredictMaxDecision(board); + } + return PredictMinDecision(board); + } + + std::vector PredictAll(int board, bool isMaxTurn, int maxDepth) + { + _maxDepth = maxDepth; + if (isMaxTurn) + { + return PredictMaxDecisions(board); + } + return PredictMinDecisions(board); + } + + int GetState(int board) + { + // Find out if there is a winner at this board state + for(int winnerMask : winnerMasks) + { + if ((board & winnerMask) == winnerMask) + { + // Find out who is the winner + winnerMask = winnerMask >> 1; + int piecesXor = (winnerMask ^ board) & winnerMask; + if (piecesXor == 0) + return CROSS_WINS; + if (piecesXor == winnerMask) + return CIRCLE_WINS; + } + } + // + if ((board & DRAW_BOARD_STATE) == DRAW_BOARD_STATE) + return DRAW; + // If there is not WINNER or + return PLAYING; + } +} diff --git a/AdversarialSearch/Minimax/TicTacToeMinimax.h b/AdversarialSearch/Minimax/TicTacToeMinimax.h index ca78c17..b0cb253 100644 --- a/AdversarialSearch/Minimax/TicTacToeMinimax.h +++ b/AdversarialSearch/Minimax/TicTacToeMinimax.h @@ -1,16 +1,16 @@ -#pragma once - -#include -#include - -namespace TicTacToeMinimax -{ - const int DRAW = 0; - const int CROSS_WINS = 1; - const int CIRCLE_WINS = -1; - const int PLAYING = 2; - - int predict(int board, bool isMaxTurn, int maxDepth); - std::vector predictAll(int board, bool isMaxTurn, int maxDepth); - int getState(int board); -} +#pragma once + +#include +#include + +namespace TicTacToeMinimax +{ + const int DRAW = 0; + const int CROSS_WINS = 1; + const int CIRCLE_WINS = -1; + const int PLAYING = 2; + + int Predict(int board, bool isMaxTurn, int maxDepth); + std::vector PredictAll(int board, bool isMaxTurn, int maxDepth); + int GetState(int board); +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c27675..7f08ec6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,21 @@ -cmake_minimum_required(VERSION 3.14.5) -project(AIEngine) -set(CMAKE_CXX_STANDARD 14) -set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/build) -add_subdirectory(AdversarialSearch/Minimax) -add_subdirectory(Miscellaneous) -add_subdirectory(NeuralNetwork/Perceptron) -add_subdirectory(NeuralNetwork/MLP) +# set up cmake version +cmake_minimum_required(VERSION 3.14.5) + +# create the project +project(AIEngine) + +# set project properties +set(CMAKE_CXX_STANDARD 14) +set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/build) +set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/Libs) + +# include tests from subdirectories +add_subdirectory(AdversarialSearch/Minimax/Tests) +add_subdirectory(Miscellaneous/Tests) +add_subdirectory(NeuralNetwork/Perceptron/Tests) +add_subdirectory(NeuralNetwork/MLP/Tests) + +# include examples from subdirectories +add_subdirectory(Examples/MathLibrary) +# include MathLibrary tests +add_subdirectory(Examples/MathLibrary/Tests) diff --git a/Examples/MathLibrary/ApiExports.h b/Examples/MathLibrary/ApiExports.h new file mode 100644 index 0000000..45a6fd5 --- /dev/null +++ b/Examples/MathLibrary/ApiExports.h @@ -0,0 +1,11 @@ +#pragma once + +#if defined(_WIN32) || defined(WIN32) +# ifdef MATHLIBRARY_EXPORTS +# define MATHLIBRARY_API __declspec(dllexport) +# else +# define MATHLIBRARY_API __declspec(dllimport) +# endif +#else +# define MATHLIBRARY_API +#endif diff --git a/Examples/MathLibrary/CMakeLists.txt b/Examples/MathLibrary/CMakeLists.txt new file mode 100644 index 0000000..2f7ab0a --- /dev/null +++ b/Examples/MathLibrary/CMakeLists.txt @@ -0,0 +1,14 @@ +add_definitions(-D MATHLIBRARY_EXPORTS) + +# list of source files +set(FILES + MathLibrary.cpp + ) + +# shared and static libraries built from the same object files +add_library(MathLibraryShared SHARED ${FILES}) +add_library(MathLibraryStatic STATIC ${FILES}) + +# rename output name of DLL and LIB files +set_target_properties(MathLibraryShared PROPERTIES OUTPUT_NAME MathLibrary) +set_target_properties(MathLibraryStatic PROPERTIES OUTPUT_NAME MathLibrary) diff --git a/Examples/MathLibrary/MathLibrary.cpp b/Examples/MathLibrary/MathLibrary.cpp new file mode 100644 index 0000000..8ef07a7 --- /dev/null +++ b/Examples/MathLibrary/MathLibrary.cpp @@ -0,0 +1,21 @@ +#include "MathLibrary.h" + +int Sum(int lhs, int rhs) +{ + return lhs + rhs; +} + +int GetOne() +{ + return 1; +} + +int MathFacade::Sum(int lhs, int rhs) +{ + return ::Sum(lhs, rhs); +} + +int MathFacade::GetOne() +{ + return ::GetOne(); +} diff --git a/Examples/MathLibrary/MathLibrary.h b/Examples/MathLibrary/MathLibrary.h new file mode 100644 index 0000000..080d83c --- /dev/null +++ b/Examples/MathLibrary/MathLibrary.h @@ -0,0 +1,15 @@ +// MathLibrary.h - Contains declarations of math functions +#pragma once + +#include "ApiExports.h" + +MATHLIBRARY_API class MathFacade { +public: + static int Sum(int lhs, int rhs); + static int GetOne(); +}; + +extern "C" { + MATHLIBRARY_API int Sum(int lhs, int rhs); + MATHLIBRARY_API int GetOne(); +} diff --git a/Examples/MathLibrary/Tests/CMakeLists.txt b/Examples/MathLibrary/Tests/CMakeLists.txt new file mode 100644 index 0000000..6601cd5 --- /dev/null +++ b/Examples/MathLibrary/Tests/CMakeLists.txt @@ -0,0 +1,9 @@ +set(FILES + MathLibraryTestMain.cpp +) +add_executable(MathLibrary ${FILES}) + +# link static library into executable +add_library(MathLibraryLib STATIC IMPORTED) +set_target_properties(MathLibraryLib PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/build/Libs/libMathLibrary.a) +target_link_libraries(MathLibrary PRIVATE MathLibraryLib) diff --git a/Examples/MathLibrary/Tests/MathLibraryTestMain.cpp b/Examples/MathLibrary/Tests/MathLibraryTestMain.cpp new file mode 100644 index 0000000..828a1a0 --- /dev/null +++ b/Examples/MathLibrary/Tests/MathLibraryTestMain.cpp @@ -0,0 +1,22 @@ +#include + +#include "../MathLibrary.h" + +using namespace std; + +int main() +{ + cout << "MathFacade::Sum" << endl; + cout << " (1, 3): " << MathFacade::Sum(1, 3) << " " << endl; + + cout << "::Sum" << endl; + cout << " (1, 3): " << Sum(1, 3) << " " << endl; + + cout << "MathFacade::GetOne" << endl; + cout << " (): " << MathFacade::GetOne() << " " << endl; + + cout << "::GetOne" << endl; + cout << " (): " << GetOne() << " " << endl; + + return 0; +} diff --git a/LICENSE b/LICENSE index fd8d425..8b66f26 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2019 andreirs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2019 Ândrei Schuch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/AdversarialSearch/Minimax/BitMath.h b/Miscellaneous/BitMath.cpp similarity index 92% rename from AdversarialSearch/Minimax/BitMath.h rename to Miscellaneous/BitMath.cpp index e2680ac..dcae02f 100644 --- a/AdversarialSearch/Minimax/BitMath.h +++ b/Miscellaneous/BitMath.cpp @@ -2,7 +2,7 @@ namespace BitMath { - int reverseBits(const int value, int maxBits) + int ReverseBits(const int value, int maxBits) { int reversedBits = 0; maxBits = (maxBits % 2 == 0) ? maxBits : maxBits - 1; diff --git a/Miscellaneous/BitMath.h b/Miscellaneous/BitMath.h new file mode 100644 index 0000000..b5caa97 --- /dev/null +++ b/Miscellaneous/BitMath.h @@ -0,0 +1,10 @@ +#include + +namespace BitMath +{ + /// @brief Reverse the bist of an integer value. + /// @param value The value to be reversed. + /// @param maxBits The maximum number of bits to revert. + /// @return The reversed value. + int ReverseBits(const int value, int maxBits); +} diff --git a/Miscellaneous/CMakeLists.txt b/Miscellaneous/CMakeLists.txt deleted file mode 100644 index 02f5806..0000000 --- a/Miscellaneous/CMakeLists.txt +++ /dev/null @@ -1,5 +0,0 @@ -set(FILES - Matrix.cpp - MatrixTests.cpp -) -add_executable(Matrix ${FILES}) diff --git a/Miscellaneous/ISerializable.cpp b/Miscellaneous/ISerializable.cpp index 5cf4280..1a67b96 100644 --- a/Miscellaneous/ISerializable.cpp +++ b/Miscellaneous/ISerializable.cpp @@ -1,26 +1,26 @@ -#include "ISerializable.h" - -namespace serialization -{ - void loadWeightsFromFile(const char* filePath, ISerializable& target) - { - std::fstream stream; - stream.open (filePath, std::fstream::in | std::fstream::binary); - if (stream.good()) - { - target.deserialize(stream); - } - stream.close(); - } - - void serializeToFile(const char* filePath, const ISerializable& target) - { - std::fstream stream; - stream.open (filePath, std::fstream::out | std::fstream::binary | std::fstream::trunc); - if (stream.good()) - { - target.serialize(stream); - } - stream.close(); - } -} +#include "ISerializable.h" + +namespace Serialization +{ + void LoadWeightsFromFile(const char* filePath, ISerializable& target) + { + std::fstream stream; + stream.open (filePath, std::fstream::in | std::fstream::binary); + if (stream.good()) + { + target.Deserialize(stream); + } + stream.close(); + } + + void SerializeToFile(const char* filePath, const ISerializable& target) + { + std::fstream stream; + stream.open (filePath, std::fstream::out | std::fstream::binary | std::fstream::trunc); + if (stream.good()) + { + target.Serialize(stream); + } + stream.close(); + } +} diff --git a/Miscellaneous/ISerializable.h b/Miscellaneous/ISerializable.h index 41e1f86..8518a6c 100644 --- a/Miscellaneous/ISerializable.h +++ b/Miscellaneous/ISerializable.h @@ -1,17 +1,17 @@ -#pragma once - -#include -#include - -namespace serialization -{ - class ISerializable - { - public: - virtual void serialize(std::ostream& stream) const = 0; - virtual void deserialize(std::istream& stream) = 0; - }; - - void loadWeightsFromFile(const char* filePath, ISerializable& target); - void serializeToFile(const char* filePath, const ISerializable& target); -} +#pragma once + +#include +#include + +namespace Serialization +{ + class ISerializable + { + public: + virtual void Serialize(std::ostream& stream) const = 0; + virtual void Deserialize(std::istream& stream) = 0; + }; + + void LoadWeightsFromFile(const char* filePath, ISerializable& target); + void SerializeToFile(const char* filePath, const ISerializable& target); +} diff --git a/Miscellaneous/Matrix.cpp b/Miscellaneous/Matrix.cpp index cc79e01..9218ad6 100644 --- a/Miscellaneous/Matrix.cpp +++ b/Miscellaneous/Matrix.cpp @@ -1,147 +1,147 @@ -#include "Matrix.h" - -Matrix::Matrix(int rows, int columns) : _rows(rows), - _columns(columns) -{ - assert(columns >= 1 && rows >= 1); - int size = columns * rows; - _values = std::vector(size); -} - -Matrix::Matrix(const Matrix& source) -{ - _values = source._values; - _rows = source._rows; - _columns = source._columns; -} - -double Matrix::get(const int row, const int column) const -{ - int index = row * _columns + column; - assert(index < (_rows * _columns)); - double value = _values[index]; - return value; -} - -void Matrix::set(const int row, const int column, const double value) -{ - assert((row >= 0 && row < _rows) && (column >= 0 && column < _columns)); - const int index = row * _columns + column; - _values[index] = value; -} - -int Matrix::getRows() const -{ - return _rows; -} - -int Matrix::getColumns() const -{ - return _columns; -} - -void Matrix::print(std::ostream& stream) const -{ - print(stream, 3); -} - -void Matrix::print(std::ostream& stream, int decimalPlace) const -{ - int decimalFactor = static_cast(pow(10, decimalPlace)); - assert(decimalFactor > 0); - for (int r = 0; r < _rows; r++) - { - for (int c = 0; c < _columns; c++) - { - double value = get(r, c); - double truncatedValue = floor(value * decimalFactor) / decimalFactor; - stream << "[" << truncatedValue << "] "; - } - stream << std::endl; - } -} - -//STATIC FUNCTIONS -std::unique_ptr Matrix::multiply(const Matrix &left, const Matrix &right) -{ - std::unique_ptr result = std::make_unique(left.getRows(),right.getColumns()); - multiply(left, right, *result); - return result; -} - -void Matrix::multiply(const Matrix& left, const Matrix& right, Matrix& target) -{ - assert(left.getColumns() == right.getRows()); - for(int row = 0; row < target.getRows(); ++row) - { - for(int column = 0; column < target.getColumns(); ++column) - { - double sum = 0.0; - for(int leftColumn = 0; leftColumn < left.getColumns(); ++leftColumn) - { - sum += left.get(row, leftColumn) * right.get(leftColumn, column); - } - target.set(row, column, sum); - } - } -} - -void Matrix::multiply(Matrix& target, const double scalar) -{ - for (int r = 0; r < target.getRows(); ++r) - { - for (int c = 0; c < target.getColumns(); ++c) - { - double value = target.get(r,c) * scalar; - target.set(r,c,value); - } - } -} - -void Matrix::add(Matrix& target, const double value) -{ - for (int r = 0; r < target.getRows(); ++r) - { - for (int c = 0; c < target.getColumns(); ++c) - { - double sum = target.get(r, c) + value; - target.set(r, c, sum); - } - } -} - -std::unique_ptr Matrix::fromVectorRows(const std::vector& vector) -{ - int size = vector.size(); - std::unique_ptr matrix = std::make_unique(size, 1); - for (int i = 0; i < size; ++i) { - const double number = vector[i]; - matrix->set(i, 0, number); - } - return matrix; -} - -std::unique_ptr Matrix::fromVectorColumns(const std::vector& vector) -{ - int size = vector.size(); - std::unique_ptr matrix = std::make_unique(1, size); - for (int i = 0; i < size; ++i) { - const double number = vector[i]; - matrix->set(0, i, number); - } - return matrix; -} - -void Matrix::copy(const Matrix& source, Matrix& target) -{ - assert(source.getColumns() == target.getColumns()); - assert(source.getRows() == target.getRows()); - for (int r = 0; r < source.getRows(); ++r) - { - for (int c = 0; c < source.getRows(); ++c) - { - double value = source.get(r, c); - target.set(r, c, value); - } - } -} +#include "Matrix.h" + +Matrix::Matrix(int rows, int columns) : _rows(rows), + _columns(columns) +{ + assert(columns >= 1 && rows >= 1); + int size = columns * rows; + _values = std::vector(size); +} + +Matrix::Matrix(const Matrix& source) +{ + _values = source._values; + _rows = source._rows; + _columns = source._columns; +} + +double Matrix::Get(const int row, const int column) const +{ + int index = row * _columns + column; + assert(index < (_rows * _columns)); + double value = _values[index]; + return value; +} + +void Matrix::Set(const int row, const int column, const double value) +{ + assert((row >= 0 && row < _rows) && (column >= 0 && column < _columns)); + const int index = row * _columns + column; + _values[index] = value; +} + +int Matrix::GetRows() const +{ + return _rows; +} + +int Matrix::GetColumns() const +{ + return _columns; +} + +void Matrix::Print(std::ostream& stream) const +{ + Print(stream, 3); +} + +void Matrix::Print(std::ostream& stream, int decimalPlace) const +{ + int decimalFactor = static_cast(pow(10, decimalPlace)); + assert(decimalFactor > 0); + for (int r = 0; r < _rows; r++) + { + for (int c = 0; c < _columns; c++) + { + double value = Get(r, c); + double truncatedValue = floor(value * decimalFactor) / decimalFactor; + stream << "[" << truncatedValue << "] "; + } + stream << std::endl; + } +} + +//STATIC FUNCTIONS +std::unique_ptr Matrix::Multiply(const Matrix &left, const Matrix &right) +{ + std::unique_ptr result = std::make_unique(left.GetRows(),right.GetColumns()); + Multiply(left, right, *result); + return result; +} + +void Matrix::Multiply(const Matrix& left, const Matrix& right, Matrix& target) +{ + assert(left.GetColumns() == right.GetRows()); + for(int row = 0; row < target.GetRows(); ++row) + { + for(int column = 0; column < target.GetColumns(); ++column) + { + double sum = 0.0; + for(int leftColumn = 0; leftColumn < left.GetColumns(); ++leftColumn) + { + sum += left.Get(row, leftColumn) * right.Get(leftColumn, column); + } + target.Set(row, column, sum); + } + } +} + +void Matrix::Multiply(Matrix& target, const double scalar) +{ + for (int r = 0; r < target.GetRows(); ++r) + { + for (int c = 0; c < target.GetColumns(); ++c) + { + double value = target.Get(r,c) * scalar; + target.Set(r,c,value); + } + } +} + +void Matrix::Add(Matrix& target, const double value) +{ + for (int r = 0; r < target.GetRows(); ++r) + { + for (int c = 0; c < target.GetColumns(); ++c) + { + double sum = target.Get(r, c) + value; + target.Set(r, c, sum); + } + } +} + +std::unique_ptr Matrix::FromVectorRows(const std::vector& vector) +{ + int size = vector.size(); + std::unique_ptr matrix = std::make_unique(size, 1); + for (int i = 0; i < size; ++i) { + const double number = vector[i]; + matrix->Set(i, 0, number); + } + return matrix; +} + +std::unique_ptr Matrix::FromVectorColumns(const std::vector& vector) +{ + int size = vector.size(); + std::unique_ptr matrix = std::make_unique(1, size); + for (int i = 0; i < size; ++i) { + const double number = vector[i]; + matrix->Set(0, i, number); + } + return matrix; +} + +void Matrix::Copy(const Matrix& source, Matrix& target) +{ + assert(source.GetColumns() == target.GetColumns()); + assert(source.GetRows() == target.GetRows()); + for (int r = 0; r < source.GetRows(); ++r) + { + for (int c = 0; c < source.GetRows(); ++c) + { + double value = source.Get(r, c); + target.Set(r, c, value); + } + } +} diff --git a/Miscellaneous/Matrix.h b/Miscellaneous/Matrix.h index 786ea5f..2b8658b 100644 --- a/Miscellaneous/Matrix.h +++ b/Miscellaneous/Matrix.h @@ -1,34 +1,34 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -class Matrix -{ -private: - std::vector _values; - int _rows; - int _columns; - -public: - Matrix(int rows, int columns); - Matrix(const Matrix& source); - double get(int row, int column) const; - void set(int row, int column, double value); - int getRows() const; - int getColumns() const; - void print(std::ostream& stream) const; - void print(std::ostream& stream, int decimalPlace) const; - - static std::unique_ptr multiply(const Matrix& left, const Matrix& right); - static void multiply(const Matrix& left, const Matrix& right, Matrix& target); - static void multiply(Matrix& target, double scalar); - static void add(Matrix& target, double value); - static std::unique_ptr fromVectorRows(const std::vector& vector); - static std::unique_ptr fromVectorColumns(const std::vector& vector); - static void copy(const Matrix& source, Matrix& target); -}; +#pragma once +#include +#include +#include +#include +#include +#include +#include + +class Matrix +{ +private: + std::vector _values; + int _rows; + int _columns; + +public: + Matrix(int rows, int columns); + Matrix(const Matrix& source); + double Get(int row, int column) const; + void Set(int row, int column, double value); + int GetRows() const; + int GetColumns() const; + void Print(std::ostream& stream) const; + void Print(std::ostream& stream, int decimalPlace) const; + + static std::unique_ptr Multiply(const Matrix& left, const Matrix& right); + static void Multiply(const Matrix& left, const Matrix& right, Matrix& target); + static void Multiply(Matrix& target, double scalar); + static void Add(Matrix& target, double value); + static std::unique_ptr FromVectorRows(const std::vector& vector); + static std::unique_ptr FromVectorColumns(const std::vector& vector); + static void Copy(const Matrix& source, Matrix& target); +}; diff --git a/Miscellaneous/MatrixTests.cpp b/Miscellaneous/MatrixTests.cpp deleted file mode 100644 index fb8ffc4..0000000 --- a/Miscellaneous/MatrixTests.cpp +++ /dev/null @@ -1,76 +0,0 @@ -#include "Matrix.h" -#include - -using namespace std; - -int main() -{ - cout << "Matrix * Matrix" << endl; - auto left = make_unique(1,3); - left->set(0,0,1); - left->set(0,1,2); - left->set(0,2,3); - auto right = make_unique(3,1); - right->set(0,0,4); - right->set(1,0,5); - right->set(2,0,6); - auto dotProduct = Matrix::multiply(*left, *right); - dotProduct->print(cout); - - cout << endl; - left = make_unique(3,1); - left->set(0,0,4); - left->set(1,0,5); - left->set(2,0,6); - right = make_unique(1,3); - right->set(0,0,1); - right->set(0,1,2); - right->set(0,2,3); - dotProduct = Matrix::multiply(*left, *right); - dotProduct->print(cout); - - cout << endl; - left = make_unique(1,3); - left->set(0,0,3); - left->set(0,1,4); - left->set(0,2,2); - right = make_unique(3,4); - right->set(0,0,13); - right->set(0,1,9); - right->set(0,2,7); - right->set(0,3,15); - right->set(1,0,8); - right->set(1,1,7); - right->set(1,2,4); - right->set(1,3,6); - right->set(2,0,6); - right->set(2,1,4); - right->set(2,2,0); - right->set(2,3,3); - dotProduct = Matrix::multiply(*left, *right); - dotProduct->print(cout); - - cout << endl; - left = make_unique(3,3); - left->set(0,0,0.9); - left->set(0,1,0.3); - left->set(0,2,0.4); - left->set(1,0,0.2); - left->set(1,1,0.8); - left->set(1,2,0.2); - left->set(2,0,0.1); - left->set(2,1,0.5); - left->set(2,2,0.6); - right = make_unique(3, 1); - right->set(0,0,0.9); - right->set(1,0,0.1); - right->set(2,0,0.8); - dotProduct = Matrix::multiply(*left, *right); - dotProduct->print(cout); - - cout << endl << "Matrix * Scalar" << endl; - Matrix::multiply(*dotProduct, 2.0); - dotProduct->print(cout); - - return 0; -} diff --git a/Miscellaneous/Random.cpp b/Miscellaneous/Random.cpp index c814a63..5b20628 100644 --- a/Miscellaneous/Random.cpp +++ b/Miscellaneous/Random.cpp @@ -1,15 +1,49 @@ -#include "Random.h" - -void RandomUtils::initRandomSeed() -{ - auto seed = static_cast(time(nullptr)); - srand(seed); -} - -double RandomUtils::range(double min, double max) -{ - double randomValue = ((double) std::rand()) / (double) RAND_MAX; - double difference = max - min; - double result = randomValue * difference; - return min + result; -} +#include "Random.h" + +#include +#include + +std::mt19937& GetEngine() +{ + static std::random_device randomDevice; + static std::mt19937 engine(randomDevice()); + return engine; +} + +void RandomUtils::SetRandomSeed() +{ + SetSeed(std::time(nullptr)); +} + +void RandomUtils::SetSeed(long long seed) +{ + GetEngine().seed(seed); +} + +int RandomUtils::Range(int min, int max) +{ + std::uniform_int_distribution<> intDistribution(min, max); + int value = intDistribution(GetEngine()); + return value; +} + +double RandomUtils::Range(double min, double max) +{ + std::uniform_real_distribution<> realDistribution(min, max); + double value = realDistribution(GetEngine()); + return value; +} + +int RandomUtils::Int() +{ + std::uniform_int_distribution<> realDistribution; + int value = realDistribution(GetEngine()); + return value; +} + +double RandomUtils::Double() +{ + std::uniform_real_distribution<> realDistribution; + double value = realDistribution(GetEngine()); + return value; +} diff --git a/Miscellaneous/Random.h b/Miscellaneous/Random.h index 65a140e..672a7ac 100644 --- a/Miscellaneous/Random.h +++ b/Miscellaneous/Random.h @@ -1,11 +1,16 @@ -#pragma once - -#include -#include -#include - -namespace RandomUtils -{ - void initRandomSeed(); - double range(double a, double b); -} +#pragma once + +namespace RandomUtils +{ + // seed + void SetRandomSeed(); + void SetSeed(long long seed); + + // random fixed and floating point values + int Int(); + double Double(); + + // range of values + int Range(int min, int max); + double Range(double min, double max); +} diff --git a/Miscellaneous/Tests/CMakeLists.txt b/Miscellaneous/Tests/CMakeLists.txt new file mode 100644 index 0000000..01ac72e --- /dev/null +++ b/Miscellaneous/Tests/CMakeLists.txt @@ -0,0 +1,5 @@ +set(FILES + ../Matrix.cpp + MatrixTestMain.cpp +) +add_executable(Matrix ${FILES}) diff --git a/Miscellaneous/Tests/MatrixTestMain.cpp b/Miscellaneous/Tests/MatrixTestMain.cpp new file mode 100644 index 0000000..8148a5a --- /dev/null +++ b/Miscellaneous/Tests/MatrixTestMain.cpp @@ -0,0 +1,76 @@ +#include "../Matrix.h" +#include + +using namespace std; + +int main() +{ + cout << "Matrix * Matrix" << endl; + auto left = make_unique(1,3); + left->Set(0,0,1); + left->Set(0,1,2); + left->Set(0,2,3); + auto right = make_unique(3,1); + right->Set(0,0,4); + right->Set(1,0,5); + right->Set(2,0,6); + auto dotProduct = Matrix::Multiply(*left, *right); + dotProduct->Print(cout); + + cout << endl; + left = make_unique(3,1); + left->Set(0,0,4); + left->Set(1,0,5); + left->Set(2,0,6); + right = make_unique(1,3); + right->Set(0,0,1); + right->Set(0,1,2); + right->Set(0,2,3); + dotProduct = Matrix::Multiply(*left, *right); + dotProduct->Print(cout); + + cout << endl; + left = make_unique(1,3); + left->Set(0,0,3); + left->Set(0,1,4); + left->Set(0,2,2); + right = make_unique(3,4); + right->Set(0,0,13); + right->Set(0,1,9); + right->Set(0,2,7); + right->Set(0,3,15); + right->Set(1,0,8); + right->Set(1,1,7); + right->Set(1,2,4); + right->Set(1,3,6); + right->Set(2,0,6); + right->Set(2,1,4); + right->Set(2,2,0); + right->Set(2,3,3); + dotProduct = Matrix::Multiply(*left, *right); + dotProduct->Print(cout); + + cout << endl; + left = make_unique(3,3); + left->Set(0,0,0.9); + left->Set(0,1,0.3); + left->Set(0,2,0.4); + left->Set(1,0,0.2); + left->Set(1,1,0.8); + left->Set(1,2,0.2); + left->Set(2,0,0.1); + left->Set(2,1,0.5); + left->Set(2,2,0.6); + right = make_unique(3, 1); + right->Set(0,0,0.9); + right->Set(1,0,0.1); + right->Set(2,0,0.8); + dotProduct = Matrix::Multiply(*left, *right); + dotProduct->Print(cout); + + cout << endl << "Matrix * Scalar" << endl; + Matrix::Multiply(*dotProduct, 2.0); + dotProduct->Print(cout); + + return 0; +} diff --git a/NeuralNetwork/Common/ActivationFunctions.cpp b/NeuralNetwork/Common/ActivationFunctions.cpp index 48b896a..c31c6aa 100644 --- a/NeuralNetwork/Common/ActivationFunctions.cpp +++ b/NeuralNetwork/Common/ActivationFunctions.cpp @@ -1,54 +1,102 @@ -#include "ActivationFunctions.h" - -namespace NeuralNetwork -{ - namespace activation - { - double thresholdValue = 0.5; - - double sigmoid(const double& value) - { - double result = 1.0 / (1.0 + std::exp(-value)); - return result; - } - - void sigmoid(std::vector& vector) - { - for (double& i : vector) - { - i = sigmoid(i); - } - } - - double sign(const double& value) - { - if (value > 0.0) - { - return 1.0; - } - return -1.0; - } - - double threshold(const double& value) - { - if (value >= thresholdValue) - { - return 1.0; - } - return 0.0; - } - - void threshold(std::vector& vector) - { - for (double& i : vector) - { - i = threshold(i); - } - } - - void setThreshold(const double&& threshold) - { - thresholdValue = threshold; - } - } -} +#include "ActivationFunctions.h" + +namespace NeuralNetwork +{ + namespace Activation + { + double thresholdValue = 0.5; + + double Sigmoid(const double& value) + { + double result = 1.0 / (1.0 + std::exp(-value)); + return result; + } + + void Sigmoid(std::vector& vector) + { + for (double& i : vector) + { + i = Sigmoid(i); + } + } + + double Sign(const double& value) + { + if (value > 0.0) + { + return 1.0; + } + return -1.0; + } + + void Sign(std::vector& vector) + { + for (double& i : vector) + { + i = Sign(i); + } + } + + double Threshold(const double& value) + { + if (value >= thresholdValue) + { + return 1.0; + } + return 0.0; + } + + void Threshold(std::vector& vector) + { + for (double& i : vector) + { + i = Threshold(i); + } + } + + void SetThreshold(const double&& threshold) + { + thresholdValue = threshold; + } + + void Apply(const EActivationFunctionType& activationFunctionType, double& value) + { + switch(activationFunctionType) + { + case EActivationFunctionType::None: break; + + case EActivationFunctionType::Sigmoid: + value = Sigmoid(value); + break; + + case EActivationFunctionType::Sign: + value = Sign(value); + break; + + case EActivationFunctionType::Threshold: + value = Threshold(value); + break; + } + } + + void Apply(const EActivationFunctionType &activationFunctionType, std::vector &vector) + { + switch(activationFunctionType) + { + case EActivationFunctionType::None: break; + + case EActivationFunctionType::Sigmoid: + Sigmoid(vector); + break; + + case EActivationFunctionType::Sign: + Sign(vector); + break; + + case EActivationFunctionType::Threshold: + Threshold(vector); + break; + } + } + } +} diff --git a/NeuralNetwork/Common/ActivationFunctions.h b/NeuralNetwork/Common/ActivationFunctions.h index 5c4b5ae..3befdb4 100644 --- a/NeuralNetwork/Common/ActivationFunctions.h +++ b/NeuralNetwork/Common/ActivationFunctions.h @@ -1,19 +1,31 @@ -#pragma once - -#include -#include - -namespace NeuralNetwork -{ - namespace activation - { - typedef double (*function)(const double&); - typedef void (*vector_function)(std::vector&); - double sigmoid(const double&); - void sigmoid(std::vector&); - double sign(const double&); - double threshold(const double&); - void threshold(std::vector&); - void setThreshold(const double&&); - } -} +#pragma once + +#include +#include + +namespace NeuralNetwork +{ + namespace Activation + { + double Sigmoid(const double&); + void Sigmoid(std::vector&); + + double Sign(const double&); + void Sign(std::vector&); + + double Threshold(const double&); + void Threshold(std::vector&); + void SetThreshold(const double&&); + + enum class EActivationFunctionType + { + None, + Sigmoid, + Sign, + Threshold, + }; + + void Apply(const EActivationFunctionType& activationFunctionType, double& value); + void Apply(const EActivationFunctionType& activationFunctionType, std::vector& vector); + } +} diff --git a/NeuralNetwork/Common/Neuron.cpp b/NeuralNetwork/Common/Neuron.cpp index 151f852..0ffe4f9 100644 --- a/NeuralNetwork/Common/Neuron.cpp +++ b/NeuralNetwork/Common/Neuron.cpp @@ -1,74 +1,67 @@ -#include -#include "Neuron.h" - -using namespace NeuralNetwork; - -Neuron::Neuron(int weights) : _activationFunction(nullptr) -{ - _weights = std::vector(weights); -} - -double Neuron::feedforward(const std::vector& inputs, double bias) const -{ - // calculate Neuron's output - double output = bias; - for (int i = 0; i < _weights.size(); ++i) - { - output += _weights[i] * inputs[i]; - } - - // apply the activation function (if existing) - if (_activationFunction != nullptr) - { - output = _activationFunction(output); - } - - return output; -} - -void Neuron::randomizeWeights() -{ - for (double& weight : _weights) - { - weight = RandomUtils::range(-1.0, 1.0); - - // fix absolute zero weights - if (weight == 0.0) - { - weight = 0.000001; - } - } -} - -void Neuron::serialize(std::ostream &stream) const -{ - stream << std::setprecision(18) << std::hex; - for (const double& _weight : _weights) - { - stream << _weight << std::endl; - } -} - -void Neuron::deserialize(std::istream& stream) -{ - stream >> std::setprecision(18) >> std::hex; - for (double& _weight : _weights) - { - stream >> _weight; - } -} - -void Neuron::setActivationFunction(activation::function activationFunction) -{ - _activationFunction = activationFunction; -} - -const std::vector& Neuron::getWeights() const -{ - return _weights; -} - -void Neuron::setWeights(const std::vector& weights) -{ - _weights = weights; -} +#include "Neuron.h" + +#include + +using namespace NeuralNetwork; + +Neuron::Neuron(int weights) +{ + _weights = std::vector(weights); +} + +double Neuron::Feedforward(const std::vector& inputs, double bias) const +{ + // calculate Neuron's output + double output = bias; + for (int i = 0; i < _weights.size(); ++i) + { + output += _weights[i] * inputs[i]; + } + + // apply the Activation function (if existing) + Activation::Apply(ActivationFunction, output); + + return output; +} + +void Neuron::RandomizeWeights() +{ + for (double& weight : _weights) + { + weight = RandomUtils::Range(-1.0, 1.0); + + // fix absolute zero weights + if (weight == 0.0) + { + weight = 0.000001; + } + } +} + +void Neuron::Serialize(std::ostream &stream) const +{ + stream << std::setprecision(18) << std::hex; + for (const double& _weight : _weights) + { + stream << _weight << std::endl; + } +} + +void Neuron::Deserialize(std::istream& stream) +{ + stream >> std::setprecision(18) >> std::hex; + for (double& _weight : _weights) + { + stream >> _weight; + } +} + +const std::vector& Neuron::GetWeights() const +{ + return _weights; +} + +void Neuron::SetWeights(const std::vector& weights) +{ + _weights = weights; +} diff --git a/NeuralNetwork/Common/Neuron.h b/NeuralNetwork/Common/Neuron.h index df2fcf1..ab1c0fe 100644 --- a/NeuralNetwork/Common/Neuron.h +++ b/NeuralNetwork/Common/Neuron.h @@ -1,29 +1,29 @@ -#pragma once - -#include "../../Miscellaneous/Matrix.h" -#include "../../Miscellaneous/Random.h" -#include "ActivationFunctions.h" -#include "../../Miscellaneous/ISerializable.h" - -namespace NeuralNetwork -{ - class Neuron final : public serialization::ISerializable - { - private: - std::vector _weights; - activation::function _activationFunction; - - public: - explicit Neuron(int weightsLength); - void randomizeWeights(); - - double feedforward(const std::vector& inputs, double bias) const; - void setActivationFunction(activation::function activationFunction); - void setWeights(const std::vector& weights); - const std::vector& getWeights() const; - - // serialization - void serialize(std::ostream &stream) const override; - void deserialize(std::istream &stream) override; - }; -} +#pragma once + +#include "../../Miscellaneous/Matrix.h" +#include "../../Miscellaneous/Random.h" +#include "../../Miscellaneous/ISerializable.h" +#include "ActivationFunctions.h" + +namespace NeuralNetwork +{ + class Neuron final : public Serialization::ISerializable + { + private: + std::vector _weights; + + public: + Activation::EActivationFunctionType ActivationFunction = Activation::EActivationFunctionType::None; + + explicit Neuron(int weightsLength); + void RandomizeWeights(); + + double Feedforward(const std::vector& inputs, double bias) const; + void SetWeights(const std::vector& weights); + const std::vector& GetWeights() const; + + // serialization + void Serialize(std::ostream &stream) const override; + void Deserialize(std::istream &stream) override; + }; +} diff --git a/NeuralNetwork/MLP/CMakeLists.txt b/NeuralNetwork/MLP/CMakeLists.txt deleted file mode 100644 index b86cc2e..0000000 --- a/NeuralNetwork/MLP/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -set(FILES - ../../Miscellaneous/Random.cpp - ../../Miscellaneous/ISerializable.cpp - ../Common/ActivationFunctions.cpp - ../Common/Neuron.cpp - Layer.cpp - MLP.cpp - MLPMain.cpp -) -add_executable(MultiLayerPerceptron ${FILES}) diff --git a/NeuralNetwork/MLP/Layer.cpp b/NeuralNetwork/MLP/Layer.cpp index 4025653..f86e6a8 100644 --- a/NeuralNetwork/MLP/Layer.cpp +++ b/NeuralNetwork/MLP/Layer.cpp @@ -1,55 +1,55 @@ -#include "Layer.h" - -using namespace NeuralNetwork; - -Layer::Layer(int neurons, int weights) -{ - for(int i = 0; i < neurons; i++) - { - std::unique_ptr neuron = std::make_unique(weights); - neuron->setActivationFunction(activation::sigmoid); - neuron->randomizeWeights(); - _neurons.push_back(std::move(neuron)); - } -} - -void Layer::serialize(std::ostream& stream) const -{ - for(unsigned int i = 0; i < _neurons.size(); i++) - { - Neuron& neuron = *_neurons[i]; - neuron.serialize(stream); - } -} - -void Layer::deserialize(std::istream &stream) -{ - for(unsigned int i = 0; i < _neurons.size(); i++) - { - Neuron& neuron = *_neurons[i]; - neuron.deserialize(stream); - } -} - -std::vector Layer::feedforward(const std::vector& inputs) -{ - std::vector outputs(_neurons.size()); - for(unsigned int i = 0; i < _neurons.size(); i++) - { - Neuron& neuron = *_neurons[i]; - outputs[i] = neuron.feedforward(inputs, 0.0); - } - return outputs; -} - -int Layer::getNeuronsLength() const -{ - return _neurons.size(); -} - -Neuron& Layer::getNeuron(int index) const -{ - assert(index < _neurons.size()); - Neuron& neuron = *_neurons[index]; - return neuron; -} +#include "Layer.h" + +using namespace NeuralNetwork; + +Layer::Layer(size_t neurons, size_t weights) +{ + for(size_t i = 0; i < neurons; i++) + { + std::unique_ptr neuron = std::make_unique(weights); + neuron->ActivationFunction = Activation::EActivationFunctionType::Sigmoid; + neuron->RandomizeWeights(); + _neurons.push_back(std::move(neuron)); + } +} + +void Layer::Serialize(std::ostream& stream) const +{ + for(const auto& _neuron : _neurons) + { + Neuron& neuron = *_neuron; + neuron.Serialize(stream); + } +} + +void Layer::Deserialize(std::istream &stream) +{ + for(auto& _neuron : _neurons) + { + Neuron& neuron = *_neuron; + neuron.Deserialize(stream); + } +} + +std::vector Layer::Feedforward(const std::vector& inputs) +{ + std::vector outputs(_neurons.size()); + for(size_t n = 0; n < _neurons.size(); n++) + { + Neuron& neuron = *_neurons[n]; + outputs[n] = neuron.Feedforward(inputs, 0.0); + } + return outputs; +} + +size_t Layer::GetNeuronsLength() const +{ + return _neurons.size(); +} + +Neuron& Layer::GetNeuron(int index) const +{ + assert(index < _neurons.size()); + Neuron& neuron = *_neurons[index]; + return neuron; +} diff --git a/NeuralNetwork/MLP/Layer.h b/NeuralNetwork/MLP/Layer.h index 95e7243..cdd4b5d 100644 --- a/NeuralNetwork/MLP/Layer.h +++ b/NeuralNetwork/MLP/Layer.h @@ -1,23 +1,23 @@ -#pragma once - -#include -#include "../../Miscellaneous/ISerializable.h" -#include "../Common/ActivationFunctions.h" -#include "../Common/Neuron.h" - -namespace NeuralNetwork -{ - class Layer final : public serialization::ISerializable - { - private: - std::vector> _neurons; - - public: - Layer(int neurons, int weights); - std::vector feedforward(const std::vector& inputs); - void serialize(std::ostream &stream) const override; - void deserialize(std::istream &stream) override; - Neuron& getNeuron(int index) const; - int getNeuronsLength() const; - }; -} +#pragma once + +#include +#include "../../Miscellaneous/ISerializable.h" +#include "../Common/ActivationFunctions.h" +#include "../Common/Neuron.h" + +namespace NeuralNetwork +{ + class Layer final : public Serialization::ISerializable + { + private: + std::vector> _neurons; + + public: + Layer(size_t neurons, size_t weights); + std::vector Feedforward(const std::vector& inputs); + void Serialize(std::ostream &stream) const override; + void Deserialize(std::istream &stream) override; + Neuron& GetNeuron(int index) const; + size_t GetNeuronsLength() const; + }; +} diff --git a/NeuralNetwork/MLP/MLP.cpp b/NeuralNetwork/MLP/MLP.cpp index ea41449..9fe9f6b 100644 --- a/NeuralNetwork/MLP/MLP.cpp +++ b/NeuralNetwork/MLP/MLP.cpp @@ -1,153 +1,143 @@ -#include "MLP.h" - -using namespace NeuralNetwork; - -MultiLayerPerceptron::MultiLayerPerceptron(int inputsLength, const std::vector &neuronsByLayerArr) -{ - int layersLength = neuronsByLayerArr.size(); - _inputsByLayer = std::vector>(layersLength); - int previousLayerLength = inputsLength + 1; // insert bias neuron - for (int l = 0; l < layersLength; l++) - { - _inputsByLayer[l] = std::vector(previousLayerLength); - int weightsLength = neuronsByLayerArr[l]; - if (l < (layersLength - 1)) - { - weightsLength++; // insert bias neuron - } - std::unique_ptr layer = std::make_unique(weightsLength, previousLayerLength); - _layers.push_back(std::move(layer)); - previousLayerLength = weightsLength; - } -} - -std::vector MultiLayerPerceptron::feedforward(const std::vector inputs) -{ - std::vector outputs(inputs); - for (int l = 0; l < _layers.size(); l++) - { - const std::unique_ptr &layer = _layers[l]; - outputs.emplace_back(1.0); // insert bias - _inputsByLayer[l] = outputs; - outputs = layer->feedforward(outputs); - } - if (_outputActivationFunction != nullptr) - { - _outputActivationFunction(outputs); - } - return outputs; -} - -void getPreviousLayerErrors(const Neuron &neuron, double error, std::vector &previousLayerErrors) -{ - const std::vector weights = neuron.getWeights(); - if (previousLayerErrors.size() != weights.size()) - { - previousLayerErrors = std::vector(weights.size()); - } - double weightsSum = 0.0; - for (int w = 0; w < weights.size(); ++w) - { - weightsSum += weights[w]; - } - - for (int w = 0; w < weights.size(); ++w) - { - double percentage = weights[w] / weightsSum; - previousLayerErrors[w] += (error * percentage); - } -} - -void updateNeuronWeights(Neuron &neuron, const std::vector &inputs, const double output, const double error, const double learningRate) -{ - std::vector weights = neuron.getWeights(); - for (int w = 0; w < weights.size(); ++w) - { - double weightDifference = -(error * (output * (1.0 - output)) * inputs[w]); - weightDifference = -(learningRate * weightDifference); - weights[w] += weightDifference; - } - neuron.setWeights(weights); -} - -void MultiLayerPerceptron::backpropagate(const std::vector inputs, const std::vector targets, const double learningRate) -{ - std::vector outputs = feedforward(inputs); - std::vector errors(outputs.size()); - for (int o = 0; o < outputs.size(); ++o) - { - errors[o] = targets[o] - outputs[o]; - } - // backpropagate errors - for (int l = (_layers.size() - 1); l >= 0; --l) - { - // reset previous layer errors - int size = inputs.size(); - if (l > 0) - { - const std::unique_ptr &layer = _layers[l - 1]; - size = layer->getNeuronsLength(); - } - std::vector previousLayerErrors(size); - - //backpropagate errors - Layer &layer = getLayer(l); - std::vector layerInputs = _inputsByLayer[l]; - std::vector layerOutputs = layer.feedforward(layerInputs); - for (int n = 0; n < layer.getNeuronsLength(); ++n) - { - Neuron &neuron = layer.getNeuron(n); - getPreviousLayerErrors(neuron, errors[n], previousLayerErrors); - updateNeuronWeights(neuron, layerInputs, layerOutputs[n], errors[n], learningRate); - } - errors = previousLayerErrors; - } -} - -int MultiLayerPerceptron::getLayersLength() const -{ - return _layers.size(); -} - -Layer &MultiLayerPerceptron::getLayer(int index) const -{ - assert(index < _layers.size()); - const std::unique_ptr &layer = _layers[index]; - return *layer; -} - -void MultiLayerPerceptron::serialize(std::ostream &stream) const -{ - for (int i = 0; i < _layers.size(); i++) - { - const std::unique_ptr &layer = _layers[i]; - layer->serialize(stream); - } -} - -void MultiLayerPerceptron::deserialize(std::istream &stream) -{ - for (int i = 0; i < _layers.size(); i++) - { - const std::unique_ptr &layer = _layers[i]; - layer->deserialize(stream); - } -} - -void MultiLayerPerceptron::setActivationFunction(activation::vector_function activationFunction) -{ - _outputActivationFunction = activationFunction; -} - -void MultiLayerPerceptron::randomizeWeights(MultiLayerPerceptron &mlp) const -{ - for (int l = 0; l < mlp.getLayersLength(); ++l) - { - Layer &layer = mlp.getLayer(l); - for (int n = 0; n < layer.getNeuronsLength(); ++n) - { - Neuron &neuron = layer.getNeuron(n); - neuron.randomizeWeights(); - } - } -} +#include "MLP.h" + +using namespace NeuralNetwork; + +MultiLayerPerceptron::MultiLayerPerceptron(int inputsLength, const std::vector &neuronsByLayerArr) +{ + int layersLength = neuronsByLayerArr.size(); + _inputsByLayer = std::vector>(layersLength); + int previousLayerLength = inputsLength + 1; // insert bias neuron + for (int l = 0; l < layersLength; l++) + { + _inputsByLayer[l] = std::vector(previousLayerLength); + int weightsLength = neuronsByLayerArr[l]; + if (l < (layersLength - 1)) + { + weightsLength++; // insert bias neuron + } + std::unique_ptr layer = std::make_unique(weightsLength, previousLayerLength); + _layers.push_back(std::move(layer)); + previousLayerLength = weightsLength; + } +} + +std::vector MultiLayerPerceptron::Feedforward(const std::vector& inputs) +{ + std::vector outputs(inputs); + for (int l = 0; l < _layers.size(); l++) + { + const std::unique_ptr &layer = _layers[l]; + outputs.emplace_back(1.0); // insert bias + _inputsByLayer[l] = outputs; + outputs = layer->Feedforward(outputs); + } + + // apply activation function + Activation::Apply(ActivationFunction, outputs); + + return outputs; +} + +void GetPreviousLayerErrors(const Neuron& neuron, double error, std::vector& previousLayerErrors) +{ + const std::vector& weights = neuron.GetWeights(); + previousLayerErrors.resize(weights.size()); + + double weightsSum = 0.0; + for (double weight : weights) + { + weightsSum += weight; + } + + for (int w = 0; w < weights.size(); ++w) + { + double percentage = weights[w] / weightsSum; + previousLayerErrors[w] += (error * percentage); + } +} + +void UpdateNeuronWeights(Neuron &neuron, const std::vector &inputs, const double output, const double error, const double learningRate) +{ + auto& weights = const_cast&>(neuron.GetWeights()); + for (size_t w = 0; w < weights.size(); ++w) + { + double weightDifference = -(error * (output * (1.0 - output)) * inputs[w]); + weightDifference = -(learningRate * weightDifference); + weights[w] += weightDifference; + } +} + +void MultiLayerPerceptron::BackPropagate(const std::vector& inputs, const std::vector& targets, const double learningRate) +{ + std::vector outputs = Feedforward(inputs); + std::vector errors(outputs.size()); + for (int o = 0; o < outputs.size(); ++o) + { + errors[o] = targets[o] - outputs[o]; + } + // BackPropagate errors + for (int l = (_layers.size() - 1); l >= 0; --l) + { + // reset previous layer errors + size_t size = inputs.size(); + if (l > 0) + { + const std::unique_ptr &layer = _layers[l - 1]; + size = layer->GetNeuronsLength(); + } + std::vector previousLayerErrors(size); + + //BackPropagate errors + Layer &layer = GetLayer(l); + std::vector layerInputs = _inputsByLayer[l]; + std::vector layerOutputs = layer.Feedforward(layerInputs); + for (int n = 0; n < layer.GetNeuronsLength(); ++n) + { + Neuron &neuron = layer.GetNeuron(n); + GetPreviousLayerErrors(neuron, errors[n], previousLayerErrors); + UpdateNeuronWeights(neuron, layerInputs, layerOutputs[n], errors[n], learningRate); + } + errors = previousLayerErrors; + } +} + +size_t MultiLayerPerceptron::GetLayersLength() const +{ + return _layers.size(); +} + +Layer &MultiLayerPerceptron::GetLayer(const size_t index) const +{ + assert(index < _layers.size()); + const std::unique_ptr &layer = _layers[index]; + return *layer; +} + +void MultiLayerPerceptron::Serialize(std::ostream &stream) const +{ + for (const auto& layer : _layers) + { + layer->Serialize(stream); + } +} + +void MultiLayerPerceptron::Deserialize(std::istream &stream) +{ + for (auto& layer : _layers) + { + layer->Deserialize(stream); + } +} + +void MultiLayerPerceptron::RandomizeWeights() const +{ + for (int l = 0; l < GetLayersLength(); ++l) + { + Layer &layer = GetLayer(l); + for (int n = 0; n < layer.GetNeuronsLength(); ++n) + { + Neuron &neuron = layer.GetNeuron(n); + neuron.RandomizeWeights(); + } + } +} diff --git a/NeuralNetwork/MLP/MLP.h b/NeuralNetwork/MLP/MLP.h index c659ec9..25eda6f 100644 --- a/NeuralNetwork/MLP/MLP.h +++ b/NeuralNetwork/MLP/MLP.h @@ -1,26 +1,26 @@ -#pragma once - -#include "../Common/ActivationFunctions.h" -#include "Layer.h" - -namespace NeuralNetwork -{ - class MultiLayerPerceptron final : public serialization::ISerializable - { - private: - std::vector> _layers; - std::vector> _inputsByLayer; - activation::vector_function _outputActivationFunction; - - public: - MultiLayerPerceptron(int inputsLength, const std::vector &neuronsByLayerArr); - std::vector feedforward(const std::vector inputs); - void backpropagate(const std::vector inputs, const std::vector targets, const double learningRate); - void serialize(std::ostream &stream) const override; - void deserialize(std::istream &stream) override; - void setActivationFunction(activation::vector_function activationFunction); - int getLayersLength() const; - Layer& getLayer(int index) const; - void randomizeWeights(MultiLayerPerceptron &mlp) const; - }; -} +#pragma once + +#include "../Common/ActivationFunctions.h" +#include "Layer.h" + +namespace NeuralNetwork +{ + class MultiLayerPerceptron final : public Serialization::ISerializable + { + private: + std::vector> _layers; + std::vector> _inputsByLayer; + + public: + Activation::EActivationFunctionType ActivationFunction = Activation::EActivationFunctionType::None; + + MultiLayerPerceptron(int inputsLength, const std::vector &neuronsByLayerArr); + std::vector Feedforward(const std::vector& inputs); + void BackPropagate(const std::vector& inputs, const std::vector& targets, const double learningRate); + void Serialize(std::ostream& stream) const override; + void Deserialize(std::istream& stream) override; + size_t GetLayersLength() const; + Layer& GetLayer(size_t index) const; + void RandomizeWeights() const; + }; +} diff --git a/NeuralNetwork/MLP/MLPMain.cpp b/NeuralNetwork/MLP/MLPMain.cpp deleted file mode 100644 index 4323ec7..0000000 --- a/NeuralNetwork/MLP/MLPMain.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include -#include "MLP.h" - -using namespace NeuralNetwork; - -bool equals(const std::vector& lhs, const std::vector& rhs) -{ - for (int i = 0; i < lhs.size(); ++i) - { - if (lhs[i] != rhs[i]) - return false; - } - return true; -} - -void TEST_MLP_XOR_2_2_1() -{ - std::cout << __FUNCTION__ << "... "; - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - activation::setThreshold(0.7); - RandomUtils::initRandomSeed(); - std::vector neuronsByLayer({2,1}); - MultiLayerPerceptron mlp(2, neuronsByLayer); - mlp.setActivationFunction(activation::threshold); - mlp.randomizeWeights(mlp); - loadWeightsFromFile(weightsFilePath.c_str(), mlp); - std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; - std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; - long iteration = 0L; - size_t trainingIndex = 0; - size_t rightGuesses = 0; - while (rightGuesses < trainingInputs.size()) - { - std::vector& inputs = trainingInputs[trainingIndex]; - std::vector& expectedOutputs = trainingOutputs[trainingIndex]; - std::vector outputs = mlp.feedforward(inputs); - if (equals(outputs, expectedOutputs)) - { - rightGuesses++; - } - else - { - iteration++; - mlp.backpropagate(inputs, expectedOutputs, 0.01); - rightGuesses = 0; - } - trainingIndex++; - trainingIndex %= trainingInputs.size(); - } - printf("The network has been trained in '%ld' iterations.\n", iteration); - serializeToFile(weightsFilePath.c_str(), mlp); -} - -void TEST_MLP_XOR_2_3_1() -{ - std::cout << __FUNCTION__ << "... "; - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - activation::setThreshold(0.7); - RandomUtils::initRandomSeed(); - std::vector neuronsByLayer({3,1}); - MultiLayerPerceptron mlp(2, neuronsByLayer); - mlp.setActivationFunction(activation::threshold); - mlp.randomizeWeights(mlp); - loadWeightsFromFile(weightsFilePath.c_str(), mlp); - std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; - std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; - long iteration = 0L; - size_t trainingIndex = 0; - size_t rightGuesses = 0; - while (rightGuesses < trainingInputs.size()) - { - std::vector& inputs = trainingInputs[trainingIndex]; - std::vector& expectedOutputs = trainingOutputs[trainingIndex]; - std::vector outputs = mlp.feedforward(inputs); - if (equals(outputs, expectedOutputs)) - { - rightGuesses++; - } - else - { - iteration++; - mlp.backpropagate(inputs, expectedOutputs, 0.01); - rightGuesses = 0; - } - trainingIndex++; - trainingIndex %= trainingInputs.size(); - } - printf("The network has been trained in '%ld' iterations.\n", iteration); - serializeToFile(weightsFilePath.c_str(), mlp); -} - -void TEST_MLP_XOR_2_3_3_3_1() -{ - std::cout << __FUNCTION__ << "... "; - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - activation::setThreshold(0.7); - RandomUtils::initRandomSeed(); - std::vector neuronsByLayer({3, 3, 3, 1}); - MultiLayerPerceptron mlp(2, neuronsByLayer); - mlp.setActivationFunction(activation::threshold); - mlp.randomizeWeights(mlp); - loadWeightsFromFile(weightsFilePath.c_str(), mlp); - std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; - std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; - long iteration = 0L; - size_t trainingIndex = 0; - size_t rightGuesses = 0; - while (rightGuesses < trainingInputs.size()) - { - std::vector& inputs = trainingInputs[trainingIndex]; - std::vector expectedOutputs = trainingOutputs[trainingIndex]; - std::vector outputs = mlp.feedforward(inputs); - if (equals(outputs, expectedOutputs)) - { - rightGuesses++; - } - else - { - iteration++; - mlp.backpropagate(inputs, expectedOutputs, 0.01); - rightGuesses = 0; - } - trainingIndex++; - trainingIndex %= trainingInputs.size(); - } - printf("The network has been trained in '%ld' iterations.\n", iteration); - serializeToFile(weightsFilePath.c_str(), mlp); -} - -void TEST_MLP_XOR_2_3_2() -{ - std::cout << __FUNCTION__ << "... "; - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - activation::setThreshold(0.7); - RandomUtils::initRandomSeed(); - std::vector neuronsByLayer({3, 2}); - MultiLayerPerceptron mlp(2, neuronsByLayer); - mlp.setActivationFunction(activation::threshold); - mlp.randomizeWeights(mlp); - loadWeightsFromFile(weightsFilePath.c_str(), mlp); - std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; - std::vector> trainingOutputs = { {1.0, 0.0}, {0.0, 1.0}, {0.0, 1.0}, {1.0, 0.0} }; - long iteration = 0L; - size_t trainingIndex = 0; - size_t rightGuesses = 0; - while (rightGuesses < trainingInputs.size()) - { - std::vector& inputs = trainingInputs[trainingIndex]; - std::vector& expectedOutputs = trainingOutputs[trainingIndex]; - std::vector outputs = mlp.feedforward(inputs); - if (equals(outputs, expectedOutputs)) - { - rightGuesses++; - } - else - { - iteration++; - mlp.backpropagate(inputs, expectedOutputs, 0.01); - rightGuesses = 0; - } - trainingIndex++; - trainingIndex %= trainingInputs.size(); - } - printf("The network has been trained in '%ld' iterations.\n", iteration); - serializeToFile(weightsFilePath.c_str(), mlp); -} - -void TEST_MLP_number_recognition_digital_clock_0_to_9() -{ - std::cout << __FUNCTION__ << "... "; - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - activation::setThreshold(0.7); - RandomUtils::initRandomSeed(); - std::vector neuronsByLayer({10, 10}); - MultiLayerPerceptron mlp(7, neuronsByLayer); - mlp.setActivationFunction(activation::threshold); - mlp.randomizeWeights(mlp); - loadWeightsFromFile(weightsFilePath.c_str(), mlp); - std::vector> trainingInputs = - { - { 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0 }, //0 - { 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0 }, //1 - { 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0 }, //2 - { 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0 }, //3 - { 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0 }, //4 - { 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0 }, //5 - { 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0 }, //6 - { 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0 }, //7 - { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }, //8 - { 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0 }, //9 - }; - std::vector> trainingOutputs = - { - { 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 }, - { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 }, - }; - long iteration = 0L; - size_t trainingIndex = 0; - size_t rightGuesses = 0; - do - { - std::vector& inputs = trainingInputs[trainingIndex]; - std::vector& expectedOutputs = trainingOutputs[trainingIndex]; - std::vector outputs = mlp.feedforward(inputs); - if (equals(outputs, expectedOutputs)) - { - rightGuesses++; - } - else - { - iteration++; - rightGuesses = 0; - mlp.backpropagate(inputs, expectedOutputs, 0.01); - } - trainingIndex++; - trainingIndex %= trainingInputs.size(); - } - while (rightGuesses < trainingInputs.size()); - printf("The network has been trained in '%ld' iterations.\n", iteration); - serializeToFile(weightsFilePath.c_str(), mlp); -} - -int main() -{ - TEST_MLP_XOR_2_2_1(); - TEST_MLP_XOR_2_3_1(); - TEST_MLP_XOR_2_3_3_3_1(); - TEST_MLP_XOR_2_3_2(); - TEST_MLP_number_recognition_digital_clock_0_to_9(); - return 0; -} diff --git a/NeuralNetwork/MLP/Tests/CMakeLists.txt b/NeuralNetwork/MLP/Tests/CMakeLists.txt new file mode 100644 index 0000000..7d1b8e7 --- /dev/null +++ b/NeuralNetwork/MLP/Tests/CMakeLists.txt @@ -0,0 +1,13 @@ +# uncomment to persist weights from MLP tests +#add_definitions(-DPERSIST_WEIGHTS) + +set(FILES + ../../../Miscellaneous/Random.cpp + ../../../Miscellaneous/ISerializable.cpp + ../../Common/ActivationFunctions.cpp + ../../Common/Neuron.cpp + ../Layer.cpp + ../MLP.cpp + MLPTestMain.cpp +) +add_executable(MultiLayerPerceptron ${FILES}) diff --git a/NeuralNetwork/MLP/Tests/MLPTestMain.cpp b/NeuralNetwork/MLP/Tests/MLPTestMain.cpp new file mode 100644 index 0000000..bd24e9d --- /dev/null +++ b/NeuralNetwork/MLP/Tests/MLPTestMain.cpp @@ -0,0 +1,198 @@ +#include "../MLP.h" + +#include + +using namespace NeuralNetwork; + +void ApplySupervisedLearning(MultiLayerPerceptron& mlp, + // training inputs and outputs + const std::vector>& trainingInputs, + const std::vector>& trainingOutputs) +{ + size_t totalIterations = 0L; + size_t iteration = 0L; + size_t trainingIndex = 0; + size_t rightGuesses = 0; + + while (rightGuesses < trainingInputs.size()) + { + const std::vector& inputs = trainingInputs[trainingIndex]; + const std::vector& expectedOutputs = trainingOutputs[trainingIndex]; + const std::vector outputs = mlp.Feedforward(inputs); + + if (outputs == expectedOutputs) + { + rightGuesses++; + } + else + { + iteration++; + totalIterations++; + mlp.BackPropagate(inputs, expectedOutputs, 0.1); + rightGuesses = 0; + + // if number of iteration exploits a threshold + if (iteration >= 100000ll) + { + iteration = 0l; + mlp.RandomizeWeights(); + } + } + trainingIndex++; + trainingIndex %= trainingInputs.size(); + } + printf("The network has been trained in '%zu' iterations.\n", totalIterations); +} + +void TEST_MLP_XOR_2_2_1() +{ + std::cout << __FUNCTION__ << "... "; + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Activation::SetThreshold(0.7); + RandomUtils::SetRandomSeed(); + std::vector neuronsByLayer({2,1}); + MultiLayerPerceptron mlp(2, neuronsByLayer); + mlp.ActivationFunction = Activation::EActivationFunctionType::Threshold; + mlp.RandomizeWeights(); +#ifdef PERSIST_WEIGHTS + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), mlp); +#endif + + std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; + std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; + ApplySupervisedLearning(mlp, trainingInputs, trainingOutputs); + +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), mlp); +#endif +} + +void TEST_MLP_XOR_2_3_1() +{ + std::cout << __FUNCTION__ << "... "; + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Activation::SetThreshold(0.7); + RandomUtils::SetRandomSeed(); + std::vector neuronsByLayer({3,1}); + MultiLayerPerceptron mlp(2, neuronsByLayer); + mlp.ActivationFunction = Activation::EActivationFunctionType::Threshold; + mlp.RandomizeWeights(); + +#ifdef PERSIST_WEIGHTS + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), mlp); +#endif + + std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; + std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; + ApplySupervisedLearning(mlp, trainingInputs, trainingOutputs); + +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), mlp); +#endif +} + +void TEST_MLP_XOR_2_3_3_3_1() +{ + std::cout << __FUNCTION__ << "... "; + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Activation::SetThreshold(0.7); + RandomUtils::SetRandomSeed(); + std::vector neuronsByLayer({3, 3, 3, 1}); + MultiLayerPerceptron mlp(2, neuronsByLayer); + mlp.ActivationFunction = Activation::EActivationFunctionType::Threshold; + mlp.RandomizeWeights(); + +#ifdef PERSIST_WEIGHTS + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), mlp); +#endif + + std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; + std::vector> trainingOutputs = { {0.0}, {1.0}, {1.0}, {0.0} }; + ApplySupervisedLearning(mlp, trainingInputs, trainingOutputs); + +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), mlp); +#endif +} + +void TEST_MLP_XOR_2_3_2() +{ + std::cout << __FUNCTION__ << "... "; + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Activation::SetThreshold(0.7); + RandomUtils::SetRandomSeed(); + std::vector neuronsByLayer({3, 2}); + MultiLayerPerceptron mlp(2, neuronsByLayer); + mlp.ActivationFunction = Activation::EActivationFunctionType::Threshold; + mlp.RandomizeWeights(); + +#ifdef PERSIST_WEIGHTS + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), mlp); +#endif + + std::vector> trainingInputs = { { 0.0, 0.0 }, { 0.0, 1.0 }, { 1.0, 0.0 }, { 1.0, 1.0 } }; + std::vector> trainingOutputs = { {1.0, 0.0}, {0.0, 1.0}, {0.0, 1.0}, {1.0, 0.0} }; + ApplySupervisedLearning(mlp, trainingInputs, trainingOutputs); + +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), mlp); +#endif +} + +void TEST_MLP_number_recognition_digital_clock_0_to_9() +{ + std::cout << __FUNCTION__ << "... "; + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Activation::SetThreshold(0.7); + RandomUtils::SetRandomSeed(); + std::vector neuronsByLayer({10, 10}); + MultiLayerPerceptron mlp(7, neuronsByLayer); + mlp.ActivationFunction = Activation::EActivationFunctionType::Threshold; + mlp.RandomizeWeights(); + +#ifdef PERSIST_WEIGHTS + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), mlp); +#endif + + std::vector> trainingInputs = + { + { 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0 }, //0 + { 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0 }, //1 + { 1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0 }, //2 + { 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0 }, //3 + { 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 0.0 }, //4 + { 1.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0 }, //5 + { 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0 }, //6 + { 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0 }, //7 + { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 }, //8 + { 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0 }, //9 + }; + std::vector> trainingOutputs = + { + { 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 }, + { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 }, + }; + ApplySupervisedLearning(mlp, trainingInputs, trainingOutputs); + +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), mlp); +#endif +} + +int main() +{ + TEST_MLP_XOR_2_2_1(); + TEST_MLP_XOR_2_3_1(); + TEST_MLP_XOR_2_3_3_3_1(); + TEST_MLP_XOR_2_3_2(); + TEST_MLP_number_recognition_digital_clock_0_to_9(); + return 0; +} diff --git a/NeuralNetwork/Perceptron/CMakeLists.txt b/NeuralNetwork/Perceptron/CMakeLists.txt deleted file mode 100644 index 31e0973..0000000 --- a/NeuralNetwork/Perceptron/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -set(FILES - ../../Miscellaneous/Random.cpp - ../../Miscellaneous/ISerializable.cpp - ../Common/ActivationFunctions.cpp - ../Common/Neuron.cpp - Perceptron.cpp - PerceptronMain.cpp -) -add_executable(Perceptron ${FILES}) diff --git a/NeuralNetwork/Perceptron/Perceptron.cpp b/NeuralNetwork/Perceptron/Perceptron.cpp index aac2222..fadc55b 100644 --- a/NeuralNetwork/Perceptron/Perceptron.cpp +++ b/NeuralNetwork/Perceptron/Perceptron.cpp @@ -1,51 +1,51 @@ -#include "Perceptron.h" - -using namespace NeuralNetwork; - -Perceptron::Perceptron(int weights) : _bias(0.0) -{ - _neuron = std::make_unique(weights); -} - -double Perceptron::feedforward(const std::vector& inputs) -{ - double output = _neuron->feedforward(inputs, _bias); - return output; -} - -void Perceptron::train(const std::vector& inputs, double target, double learningRate) -{ - double output = feedforward(inputs); - double error = (target - output); - std::vector weights = _neuron->getWeights(); - for (int c = 0; c < weights.size(); ++c) - { - weights[c] += inputs[c] * error * learningRate; - } - _neuron->setWeights(weights); - _bias += error; -} - -void Perceptron::serialize(std::ostream &stream) const -{ - stream << std::hex; - stream << _bias << std::endl; - _neuron->serialize(stream); -} - -void Perceptron::deserialize(std::istream &stream) -{ - stream >> _bias; - _neuron->deserialize(stream); -} - -void Perceptron::setActivationFunction(activation::function activationFunction) -{ - _neuron->setActivationFunction(activationFunction); -} - -void Perceptron::randomizeWeights() -{ - _neuron->randomizeWeights(); - _bias = RandomUtils::range(-0.1, 0.1); -} +#include "Perceptron.h" + +using namespace NeuralNetwork; + +Perceptron::Perceptron(int weights) : _bias(0.0) +{ + _neuron = std::make_unique(weights); +} + +double Perceptron::Feedforward(const std::vector& inputs) +{ + double output = _neuron->Feedforward(inputs, _bias); + return output; +} + +void Perceptron::Train(const std::vector& inputs, double target, double learningRate) +{ + double output = Feedforward(inputs); + double error = (target - output); + std::vector weights = _neuron->GetWeights(); + for (int c = 0; c < weights.size(); ++c) + { + weights[c] += inputs[c] * error * learningRate; + } + _neuron->SetWeights(weights); + _bias += error; +} + +void Perceptron::Serialize(std::ostream &stream) const +{ + stream << std::hex; + stream << _bias << std::endl; + _neuron->Serialize(stream); +} + +void Perceptron::Deserialize(std::istream &stream) +{ + stream >> _bias; + _neuron->Deserialize(stream); +} + +void Perceptron::RandomizeWeights() +{ + _neuron->RandomizeWeights(); + _bias = RandomUtils::Range(-0.1, 0.1); +} + +Neuron *Perceptron::GetNeuron() +{ + return _neuron.get(); +} diff --git a/NeuralNetwork/Perceptron/Perceptron.h b/NeuralNetwork/Perceptron/Perceptron.h index 1e03c3c..b135b9b 100644 --- a/NeuralNetwork/Perceptron/Perceptron.h +++ b/NeuralNetwork/Perceptron/Perceptron.h @@ -1,24 +1,24 @@ -#pragma once - -#include -#include "../../Miscellaneous/ISerializable.h" -#include "../Common/Neuron.h" - -namespace NeuralNetwork -{ - class Perceptron final : public serialization::ISerializable - { - private: - double _bias; - std::unique_ptr _neuron; - - public: - explicit Perceptron(int weights); - double feedforward(const std::vector& inputs); - void train(const std::vector& inputs, double target, double learningRate); - void setActivationFunction(activation::function activationFunction); - void randomizeWeights(); - void serialize(std::ostream &stream) const override; - void deserialize(std::istream &stream) override; - }; -} +#pragma once + +#include +#include "../../Miscellaneous/ISerializable.h" +#include "../Common/Neuron.h" + +namespace NeuralNetwork +{ + class Perceptron final : public Serialization::ISerializable + { + private: + double _bias; + std::unique_ptr _neuron; + + public: + explicit Perceptron(int weights); + Neuron* GetNeuron(); + double Feedforward(const std::vector& inputs); + void Train(const std::vector& inputs, double target, double learningRate); + void RandomizeWeights(); + void Serialize(std::ostream &stream) const override; + void Deserialize(std::istream &stream) override; + }; +} diff --git a/NeuralNetwork/Perceptron/Tests/CMakeLists.txt b/NeuralNetwork/Perceptron/Tests/CMakeLists.txt new file mode 100644 index 0000000..9944f74 --- /dev/null +++ b/NeuralNetwork/Perceptron/Tests/CMakeLists.txt @@ -0,0 +1,12 @@ +# uncomment to persist weights from MLP tests +#add_definitions(-DPERSIST_WEIGHTS) + +set(FILES + ../../../Miscellaneous/Random.cpp + ../../../Miscellaneous/ISerializable.cpp + ../../Common/ActivationFunctions.cpp + ../../Common/Neuron.cpp + ../Perceptron.cpp + PerceptronTestMain.cpp +) +add_executable(Perceptron ${FILES}) diff --git a/NeuralNetwork/Perceptron/PerceptronMain.cpp b/NeuralNetwork/Perceptron/Tests/PerceptronTestMain.cpp similarity index 61% rename from NeuralNetwork/Perceptron/PerceptronMain.cpp rename to NeuralNetwork/Perceptron/Tests/PerceptronTestMain.cpp index e7941ac..6d89d07 100644 --- a/NeuralNetwork/Perceptron/PerceptronMain.cpp +++ b/NeuralNetwork/Perceptron/Tests/PerceptronTestMain.cpp @@ -1,96 +1,103 @@ -#include "Perceptron.h" - -#include - -using namespace NeuralNetwork; - -void Test_Neuron_W1_greater_than_W2() -{ - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - RandomUtils::initRandomSeed(); - std::unique_ptr neuron = std::make_unique(2); - neuron->setActivationFunction(activation::sign); - neuron->randomizeWeights(); - loadWeightsFromFile(weightsFilePath.c_str(), *neuron); - double learningRate = 0.1; - int iterations = 0; - std::vector> inputs = - { - std::vector {1.0, 0.5}, // 1.0 - std::vector {0.5, 1.0}, // -1.0 - std::vector {10.0, 0.0}, // 1.0 - std::vector {0.0, 10.0}, // -1.0 - std::vector {0.0, -10.0}, // 1.0 - std::vector {-10.0, 0.0}, // -1.0 - std::vector {10.0, -10.0}, // 1.0 - std::vector {-10.0, 10.0}, // -1.0 - std::vector {1000.0, -1000.0}, // 1.0 - std::vector {-1000.0, 1000.0}, // -1.0 - std::vector {351.0, -179.0}, // 1.0 - std::vector {-499.8, 732.0}, // -1.0 - }; - std::vector expectedOutputs = {1.0, -1.0, 1.0, -1.0, 1.0, -1.0, - 1.0, -1.0, 1.0, -1.0, 1.0, -1.0}; - for (int i = 0; i < inputs.size(); ++i) - { - double guess = neuron->feedforward(inputs[i]); - if (guess != expectedOutputs[i]) - { - iterations++; - neuron->train(inputs[i], expectedOutputs[i], learningRate); - i = -1; - } - } - std::cout << "The network has been trained! (iterations: " << iterations << ")" << std::endl; - serializeToFile(weightsFilePath.c_str(), *neuron); -} - -void Test_Neuron_W2_greater_than_W1() -{ - std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); - RandomUtils::initRandomSeed(); - std::unique_ptr perceptron = std::make_unique(2); - perceptron->setActivationFunction(activation::sign); - perceptron->randomizeWeights(); - loadWeightsFromFile(weightsFilePath.c_str(), *perceptron); - double learningRate = 0.1; - int iterations = 0; - std::vector> inputs = - { - std::vector {1.0, 0.5}, // -1.0 - std::vector {0.5, 1.0}, // 1.0 - std::vector {10.0, 0.0}, // -1.0 - std::vector {0.0, 10.0}, // 1.0 - std::vector {0.0, -10.0}, // -1.0 - std::vector {-10.0, 0.0}, // 1.0 - std::vector {10.0, -10.0}, // -1.0 - std::vector {-10.0, 10.0}, // 1.0 - }; - std::vector expectedOutputs = {-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0}; - - // apply sigmoid on inputs - for (std::vector& input : inputs) - { - activation::sigmoid(input); - } - - for (int i = 0; i < inputs.size(); ++i) - { - double guess = perceptron->feedforward(inputs[i]); - if (guess != expectedOutputs[i]) - { - iterations++; - perceptron->train(inputs[i], expectedOutputs[i], learningRate); - i = -1; - } - } - std::cout << "The network has been trained! (iterations: " << iterations << ")" << std::endl; - serializeToFile(weightsFilePath.c_str(), *perceptron); -} - -int main() -{ - Test_Neuron_W1_greater_than_W2(); - Test_Neuron_W2_greater_than_W1(); - return 0; -} +#include "../Perceptron.h" + +#include + +void Test_Neuron_W1_greater_than_W2() +{ + std::cout << __FUNCTION__ << "... "; + RandomUtils::SetRandomSeed(); + std::unique_ptr perceptron = std::make_unique(2); + perceptron->GetNeuron()->ActivationFunction = NeuralNetwork::Activation::EActivationFunctionType::Sign; + perceptron->RandomizeWeights(); +#ifdef PERSIST_WEIGHTS + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), *perceptron); +#endif + + int iterations = 0; + std::vector> inputs = + { + std::vector {1.0, 0.5}, // 1.0 + std::vector {0.5, 1.0}, // -1.0 + std::vector {10.0, 0.0}, // 1.0 + std::vector {0.0, 10.0}, // -1.0 + std::vector {0.0, -10.0}, // 1.0 + std::vector {-10.0, 0.0}, // -1.0 + std::vector {10.0, -10.0}, // 1.0 + std::vector {-10.0, 10.0}, // -1.0 + std::vector {1000.0, -1000.0}, // 1.0 + std::vector {-1000.0, 1000.0}, // -1.0 + std::vector {351.0, -179.0}, // 1.0 + std::vector {-499.8, 732.0}, // -1.0 + }; + std::vector expectedOutputs = {1.0, -1.0, 1.0, -1.0, 1.0, -1.0, + 1.0, -1.0, 1.0, -1.0, 1.0, -1.0}; + for (int i = 0; i < inputs.size(); ++i) + { + double guess = perceptron->Feedforward(inputs[i]); + if (guess != expectedOutputs[i]) + { + iterations++; + perceptron->Train(inputs[i], expectedOutputs[i], 0.1); + i = -1; + } + } + std::cout << "The network has been trained! (iterations: " << iterations << ")" << std::endl; +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), *perceptron); +#endif +} + +void Test_Neuron_W2_greater_than_W1() +{ + std::cout << __FUNCTION__ << "... "; + RandomUtils::SetRandomSeed(); + std::unique_ptr perceptron = std::make_unique(2); + perceptron->GetNeuron()->ActivationFunction = NeuralNetwork::Activation::EActivationFunctionType::Sign; + perceptron->RandomizeWeights(); +#ifdef PERSIST_WEIGHTS + std::string weightsFilePath = ".\\" + std::string(__FUNCTION__); + Serialization::LoadWeightsFromFile(weightsFilePath.c_str(), *perceptron); +#endif + int iterations = 0; + std::vector> inputs = + { + std::vector {1.0, 0.5}, // -1.0 + std::vector {0.5, 1.0}, // 1.0 + std::vector {10.0, 0.0}, // -1.0 + std::vector {0.0, 10.0}, // 1.0 + std::vector {0.0, -10.0}, // -1.0 + std::vector {-10.0, 0.0}, // 1.0 + std::vector {10.0, -10.0}, // -1.0 + std::vector {-10.0, 10.0}, // 1.0 + }; + std::vector expectedOutputs = {-1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0}; + + // apply sigmoid on inputs + for (std::vector& input : inputs) + { + NeuralNetwork::Activation::Sigmoid(input); + } + + for (int i = 0; i < inputs.size(); ++i) + { + double guess = perceptron->Feedforward(inputs[i]); + if (guess != expectedOutputs[i]) + { + iterations++; + perceptron->Train(inputs[i], expectedOutputs[i], 0.1); + i = -1; + } + } + std::cout << "The network has been trained! (iterations: " << iterations << ")" << std::endl; +#ifdef PERSIST_WEIGHTS + Serialization::SerializeToFile(weightsFilePath.c_str(), *perceptron); +#endif +} + +int main() +{ + Test_Neuron_W1_greater_than_W2(); + Test_Neuron_W2_greater_than_W1(); + return 0; +} diff --git a/README.md b/README.md index 833754c..f3bb82f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# AIEngine -Artificial Intelligence API meant to be used in game development. It contains algorithms and techniques used in game development. - -API Inteligência Artificial para ser usada no desenvolvimento de jogos digitais. A API contém algoritmos e técnicas usadas no desenvolvimento de jogos. - -

- -

+# AIEngine +Artificial Intelligence API meant to be used in game development. It contains algorithms and techniques used in game development. + +API Inteligência Artificial para ser usada no desenvolvimento de jogos digitais. A API contém algoritmos e técnicas usadas no desenvolvimento de jogos. + +

+ +