Compare commits
34 Commits
5f70449091
...
aftermath
Author | SHA1 | Date | |
---|---|---|---|
4ebc9c2013
|
|||
b882569169
|
|||
8b2ed26ab7
|
|||
5efa3beaee
|
|||
9a0449c12d
|
|||
7222119dfb
|
|||
cb54f61a69
|
|||
07d572a98c
|
|||
c4f3e6f19a
|
|||
adc0ca238f
|
|||
b9e76becce
|
|||
85cb447283
|
|||
b03e84044a
|
|||
7f7ddad36a
|
|||
3d8fea7a37
|
|||
bc214a496c
|
|||
3e954ba841
|
|||
6f7fb290b0
|
|||
49a49a9dcd
|
|||
af7a1d2b40
|
|||
4a54bd42a2
|
|||
099b4bea09
|
|||
be06e475f0
|
|||
c10ebca0e0
|
|||
0c226371cc
|
|||
644b6c9be0
|
|||
9981ad1811
|
|||
41cceece20
|
|||
f6e154bc6e
|
|||
a2622a4fb6
|
|||
d8218f9713
|
|||
48bfa02e1d
|
|||
f519003766
|
|||
8ddfd58a50
|
17
.clang-tidy
Normal file
17
.clang-tidy
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
Checks: '-*,
|
||||||
|
clang-*,
|
||||||
|
bugprone-*,
|
||||||
|
cppcoreguidelines-*,
|
||||||
|
modernize-*,
|
||||||
|
performance-*,
|
||||||
|
-cppcoreguidelines-pro-type-vararg,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-bugprone-exception-escape'
|
||||||
|
|
||||||
|
HeaderFilterRegex: 'src/*'
|
||||||
|
AnalyzeTemporaryDtors: false
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
FormatStyle: file
|
||||||
|
FormatStyleOptions: ''
|
||||||
|
...
|
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[submodule "lib/mdlp"]
|
||||||
|
path = lib/mdlp
|
||||||
|
url = https://github.com/rmontanana/mdlp
|
||||||
|
[submodule "lib/catch2"]
|
||||||
|
path = lib/catch2
|
||||||
|
url = https://github.com/catchorg/Catch2.git
|
||||||
|
[submodule "lib/argparse"]
|
||||||
|
path = lib/argparse
|
||||||
|
url = https://github.com/p-ranav/argparse
|
||||||
|
[submodule "lib/json"]
|
||||||
|
path = lib/json
|
||||||
|
url = https://github.com/nlohmann/json.git
|
35
.vscode/launch.json
vendored
35
.vscode/launch.json
vendored
@@ -4,22 +4,37 @@
|
|||||||
{
|
{
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "bayesnet",
|
"name": "sample",
|
||||||
"program": "${workspaceFolder}/build/sample/main",
|
"program": "${workspaceFolder}/build/sample/BayesNetSample",
|
||||||
"args": [
|
"args": [
|
||||||
"-f",
|
"-d",
|
||||||
"iris"
|
"iris",
|
||||||
|
"-m",
|
||||||
|
"TAN",
|
||||||
|
"-p",
|
||||||
|
"../../data/",
|
||||||
|
"--tensors"
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}/build/sample/",
|
||||||
"preLaunchTask": "CMake: build"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "lldb",
|
"type": "lldb",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "aout",
|
"name": "experiment",
|
||||||
"program": "${workspaceFolder}/a.out",
|
"program": "${workspaceFolder}/build/src/Platform/main",
|
||||||
"args": [],
|
"args": [
|
||||||
"cwd": "${workspaceFolder}"
|
"-m",
|
||||||
|
"TAN",
|
||||||
|
"-p",
|
||||||
|
"/Users/rmontanana/Code/discretizbench/datasets",
|
||||||
|
"--discretize",
|
||||||
|
"--stratified",
|
||||||
|
"--title",
|
||||||
|
"Debug test",
|
||||||
|
"-d",
|
||||||
|
"ionosphere"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}/build/src/Platform",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Build & debug active file",
|
"name": "Build & debug active file",
|
||||||
|
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -97,7 +97,12 @@
|
|||||||
"future": "cpp",
|
"future": "cpp",
|
||||||
"queue": "cpp",
|
"queue": "cpp",
|
||||||
"typeindex": "cpp",
|
"typeindex": "cpp",
|
||||||
"shared_mutex": "cpp"
|
"shared_mutex": "cpp",
|
||||||
|
"*.ipp": "cpp",
|
||||||
|
"cassert": "cpp",
|
||||||
|
"charconv": "cpp",
|
||||||
|
"source_location": "cpp",
|
||||||
|
"ranges": "cpp"
|
||||||
},
|
},
|
||||||
"cmake.configureOnOpen": false,
|
"cmake.configureOnOpen": false,
|
||||||
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
|
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools"
|
||||||
|
@@ -33,12 +33,24 @@ set(CMAKE_BUILD_TYPE "Debug")
|
|||||||
# --------------
|
# --------------
|
||||||
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH})
|
set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH})
|
||||||
|
|
||||||
|
include(AddGitSubmodule)
|
||||||
|
include(StaticAnalyzers) # clang-tidy
|
||||||
|
include(CodeCoverage)
|
||||||
|
|
||||||
|
# External libraries - dependencies of BayesNet
|
||||||
|
# ---------------------------------------------
|
||||||
|
# include(FetchContent)
|
||||||
|
add_git_submodule("lib/mdlp")
|
||||||
|
add_git_submodule("lib/catch2")
|
||||||
|
add_git_submodule("lib/argparse")
|
||||||
|
add_git_submodule("lib/json")
|
||||||
|
|
||||||
# Subdirectories
|
# Subdirectories
|
||||||
# --------------
|
# --------------
|
||||||
add_subdirectory(config)
|
add_subdirectory(config)
|
||||||
add_subdirectory(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
add_subdirectory(lib/Files)
|
||||||
add_subdirectory(${BayesNet_SOURCE_DIR}/src/Platform)
|
add_subdirectory(src/BayesNet)
|
||||||
|
add_subdirectory(src/Platform)
|
||||||
add_subdirectory(sample)
|
add_subdirectory(sample)
|
||||||
|
|
||||||
file(GLOB BayesNet_HEADERS CONFIGURE_DEPENDS ${BayesNet_SOURCE_DIR}/src/BayesNet/*.h ${BayesNet_SOURCE_DIR}/BayesNet/*.hpp)
|
file(GLOB BayesNet_HEADERS CONFIGURE_DEPENDS ${BayesNet_SOURCE_DIR}/src/BayesNet/*.h ${BayesNet_SOURCE_DIR}/BayesNet/*.hpp)
|
||||||
@@ -51,14 +63,14 @@ if (ENABLE_TESTING)
|
|||||||
MESSAGE("Testing enabled")
|
MESSAGE("Testing enabled")
|
||||||
enable_testing()
|
enable_testing()
|
||||||
if (CODE_COVERAGE)
|
if (CODE_COVERAGE)
|
||||||
include(CodeCoverage)
|
#include(CodeCoverage)
|
||||||
MESSAGE("Code coverage enabled")
|
MESSAGE("Code coverage enabled")
|
||||||
set(CMAKE_C_FLAGS " ${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
|
set(CMAKE_C_FLAGS " ${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
|
||||||
set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
|
set(CMAKE_CXX_FLAGS " ${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage")
|
||||||
SET(GCC_COVERAGE_LINK_FLAGS " ${GCC_COVERAGE_LINK_FLAGS} -lgcov --coverage")
|
SET(GCC_COVERAGE_LINK_FLAGS " ${GCC_COVERAGE_LINK_FLAGS} -lgcov --coverage")
|
||||||
endif (CODE_COVERAGE)
|
endif (CODE_COVERAGE)
|
||||||
find_package(Catch2 3 REQUIRED)
|
#find_package(Catch2 3 REQUIRED)
|
||||||
include(CTest)
|
include(CTest)
|
||||||
include(Catch)
|
#include(Catch)
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
endif (ENABLE_TESTING)
|
endif (ENABLE_TESTING)
|
12
cmake/modules/AddGitSubmodule.cmake
Normal file
12
cmake/modules/AddGitSubmodule.cmake
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
function(add_git_submodule dir)
|
||||||
|
find_package(Git REQUIRED)
|
||||||
|
|
||||||
|
if(NOT EXISTS ${dir}/CMakeLists.txt)
|
||||||
|
message(STATUS "🚨 Adding git submodule => ${dir}")
|
||||||
|
execute_process(COMMAND ${GIT_EXECUTABLE}
|
||||||
|
submodule update --init --recursive -- ${dir}
|
||||||
|
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
|
||||||
|
endif()
|
||||||
|
add_subdirectory(${dir})
|
||||||
|
endfunction(add_git_submodule)
|
22
cmake/modules/StaticAnalyzers.cmake
Normal file
22
cmake/modules/StaticAnalyzers.cmake
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
if(ENABLE_CLANG_TIDY)
|
||||||
|
find_program(CLANG_TIDY_COMMAND NAMES clang-tidy)
|
||||||
|
|
||||||
|
if(NOT CLANG_TIDY_COMMAND)
|
||||||
|
message(WARNING "🔴 CMake_RUN_CLANG_TIDY is ON but clang-tidy is not found!")
|
||||||
|
set(CMAKE_CXX_CLANG_TIDY "" CACHE STRING "" FORCE)
|
||||||
|
else()
|
||||||
|
|
||||||
|
message(STATUS "🟢 CMake_RUN_CLANG_TIDY is ON")
|
||||||
|
set(CLANGTIDY_EXTRA_ARGS
|
||||||
|
"-extra-arg=-Wno-unknown-warning-option"
|
||||||
|
)
|
||||||
|
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_COMMAND};-p=${CMAKE_BINARY_DIR};${CLANGTIDY_EXTRA_ARGS}" CACHE STRING "" FORCE)
|
||||||
|
|
||||||
|
add_custom_target(clang-tidy
|
||||||
|
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target ${CMAKE_PROJECT_NAME}
|
||||||
|
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR} --target clang-tidy
|
||||||
|
COMMENT "Running clang-tidy..."
|
||||||
|
)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
endif()
|
||||||
|
endif(ENABLE_CLANG_TIDY)
|
1
data/_TAN_cpp_accuracy__.json
Normal file
1
data/_TAN_cpp_accuracy__.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
null
|
@@ -2,6 +2,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ vector<int>& ArffFiles::getY()
|
|||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArffFiles::load(const string& fileName, bool classLast)
|
void ArffFiles::loadCommon(string fileName)
|
||||||
{
|
{
|
||||||
ifstream file(fileName);
|
ifstream file(fileName);
|
||||||
if (!file.is_open()) {
|
if (!file.is_open()) {
|
||||||
@@ -74,24 +75,51 @@ void ArffFiles::load(const string& fileName, bool classLast)
|
|||||||
file.close();
|
file.close();
|
||||||
if (attributes.empty())
|
if (attributes.empty())
|
||||||
throw invalid_argument("No attributes found");
|
throw invalid_argument("No attributes found");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArffFiles::load(const string& fileName, bool classLast)
|
||||||
|
{
|
||||||
|
int labelIndex;
|
||||||
|
loadCommon(fileName);
|
||||||
if (classLast) {
|
if (classLast) {
|
||||||
className = get<0>(attributes.back());
|
className = get<0>(attributes.back());
|
||||||
classType = get<1>(attributes.back());
|
classType = get<1>(attributes.back());
|
||||||
attributes.pop_back();
|
attributes.pop_back();
|
||||||
|
labelIndex = static_cast<int>(attributes.size());
|
||||||
} else {
|
} else {
|
||||||
className = get<0>(attributes.front());
|
className = get<0>(attributes.front());
|
||||||
classType = get<1>(attributes.front());
|
classType = get<1>(attributes.front());
|
||||||
attributes.erase(attributes.begin());
|
attributes.erase(attributes.begin());
|
||||||
|
labelIndex = 0;
|
||||||
}
|
}
|
||||||
generateDataset(classLast);
|
generateDataset(labelIndex);
|
||||||
|
}
|
||||||
|
void ArffFiles::load(const string& fileName, const string& name)
|
||||||
|
{
|
||||||
|
int labelIndex;
|
||||||
|
loadCommon(fileName);
|
||||||
|
bool found = false;
|
||||||
|
for (int i = 0; i < attributes.size(); ++i) {
|
||||||
|
if (attributes[i].first == name) {
|
||||||
|
className = get<0>(attributes[i]);
|
||||||
|
classType = get<1>(attributes[i]);
|
||||||
|
attributes.erase(attributes.begin() + i);
|
||||||
|
labelIndex = i;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
throw invalid_argument("Class name not found");
|
||||||
|
}
|
||||||
|
generateDataset(labelIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArffFiles::generateDataset(bool classLast)
|
void ArffFiles::generateDataset(int labelIndex)
|
||||||
{
|
{
|
||||||
X = vector<vector<float>>(attributes.size(), vector<float>(lines.size()));
|
X = vector<vector<float>>(attributes.size(), vector<float>(lines.size()));
|
||||||
auto yy = vector<string>(lines.size(), "");
|
auto yy = vector<string>(lines.size(), "");
|
||||||
int labelIndex = classLast ? static_cast<int>(attributes.size()) : 0;
|
auto removeLines = vector<int>(); // Lines with missing values
|
||||||
for (size_t i = 0; i < lines.size(); i++) {
|
for (size_t i = 0; i < lines.size(); i++) {
|
||||||
stringstream ss(lines[i]);
|
stringstream ss(lines[i]);
|
||||||
string value;
|
string value;
|
||||||
@@ -101,10 +129,20 @@ void ArffFiles::generateDataset(bool classLast)
|
|||||||
if (pos++ == labelIndex) {
|
if (pos++ == labelIndex) {
|
||||||
yy[i] = value;
|
yy[i] = value;
|
||||||
} else {
|
} else {
|
||||||
|
if (value == "?") {
|
||||||
|
X[xIndex++][i] = -1;
|
||||||
|
removeLines.push_back(i);
|
||||||
|
} else
|
||||||
X[xIndex++][i] = stof(value);
|
X[xIndex++][i] = stof(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (auto i : removeLines) {
|
||||||
|
yy.erase(yy.begin() + i);
|
||||||
|
for (auto& x : X) {
|
||||||
|
x.erase(x.begin() + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
y = factorize(yy);
|
y = factorize(yy);
|
||||||
}
|
}
|
||||||
|
|
@@ -14,12 +14,12 @@ private:
|
|||||||
string classType;
|
string classType;
|
||||||
vector<vector<float>> X;
|
vector<vector<float>> X;
|
||||||
vector<int> y;
|
vector<int> y;
|
||||||
|
void generateDataset(int);
|
||||||
void generateDataset(bool);
|
void loadCommon(string);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ArffFiles();
|
ArffFiles();
|
||||||
void load(const string&, bool = true);
|
void load(const string&, bool = true);
|
||||||
|
void load(const string&, const string&);
|
||||||
vector<string> getLines() const;
|
vector<string> getLines() const;
|
||||||
unsigned long int getSize() const;
|
unsigned long int getSize() const;
|
||||||
string getClassName() const;
|
string getClassName() const;
|
2
lib/Files/CMakeLists.txt
Normal file
2
lib/Files/CMakeLists.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
add_library(ArffFiles ArffFiles.cc)
|
||||||
|
#target_link_libraries(BayesNet "${TORCH_LIBRARIES}")
|
1
lib/argparse
Submodule
1
lib/argparse
Submodule
Submodule lib/argparse added at b0930ab028
1
lib/catch2
Submodule
1
lib/catch2
Submodule
Submodule lib/catch2 added at 4acc51828f
1
lib/json
Submodule
1
lib/json
Submodule
Submodule lib/json added at 5d2754306d
1
lib/mdlp
Submodule
1
lib/mdlp
Submodule
Submodule lib/mdlp added at fbffc3a9c4
@@ -1,4 +1,7 @@
|
|||||||
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
||||||
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
||||||
add_executable(sample sample.cc ${BayesNet_SOURCE_DIR}/src/Platform/ArffFiles.cc ${BayesNet_SOURCE_DIR}/src/Platform/CPPFImdlp.cpp ${BayesNet_SOURCE_DIR}/src/Platform/Metrics.cpp ${BayesNet_SOURCE_DIR}/src/Platform/typesFImdlp.h ${BayesNet_HEADERS})
|
include_directories(${BayesNet_SOURCE_DIR}/lib/Files)
|
||||||
target_link_libraries(sample BayesNet "${TORCH_LIBRARIES}")
|
include_directories(${BayesNet_SOURCE_DIR}/lib/mdlp)
|
||||||
|
include_directories(${BayesNet_SOURCE_DIR}/lib/argparse/include)
|
||||||
|
add_executable(BayesNetSample sample.cc ${BayesNet_SOURCE_DIR}/src/Platform/Folding.cc ${BayesNet_SOURCE_DIR}/src/Platform/Models.cc)
|
||||||
|
target_link_libraries(BayesNetSample BayesNet ArffFiles mdlp "${TORCH_LIBRARIES}")
|
294
sample/sample.cc
294
sample/sample.cc
@@ -1,96 +1,20 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
|
||||||
#include <torch/torch.h>
|
#include <torch/torch.h>
|
||||||
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <getopt.h>
|
#include <map>
|
||||||
|
#include <argparse/argparse.hpp>
|
||||||
#include "ArffFiles.h"
|
#include "ArffFiles.h"
|
||||||
#include "Network.h"
|
#include "BayesMetrics.h"
|
||||||
#include "Metrics.hpp"
|
|
||||||
#include "CPPFImdlp.h"
|
#include "CPPFImdlp.h"
|
||||||
#include "KDB.h"
|
#include "Folding.h"
|
||||||
#include "SPODE.h"
|
#include "Models.h"
|
||||||
#include "AODE.h"
|
#include "modelRegister.h"
|
||||||
#include "TAN.h"
|
|
||||||
|
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
|
|
||||||
const string PATH = "data/";
|
const string PATH = "../../data/";
|
||||||
|
|
||||||
/* print a description of all supported options */
|
|
||||||
void usage(const char* path)
|
|
||||||
{
|
|
||||||
/* take only the last portion of the path */
|
|
||||||
const char* basename = strrchr(path, '/');
|
|
||||||
basename = basename ? basename + 1 : path;
|
|
||||||
|
|
||||||
cout << "usage: " << basename << "[OPTION]" << endl;
|
|
||||||
cout << " -h, --help\t\t Print this help and exit." << endl;
|
|
||||||
cout
|
|
||||||
<< " -f, --file[=FILENAME]\t {diabetes, glass, iris, kdd_JapaneseVowels, letter, liver-disorders, mfeat-factors}."
|
|
||||||
<< endl;
|
|
||||||
cout << " -p, --path[=FILENAME]\t folder where the data files are located, default " << PATH << endl;
|
|
||||||
cout << " -m, --model={AODE, KDB, SPODE, TAN}\t " << endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
tuple<string, string, string> parse_arguments(int argc, char** argv)
|
|
||||||
{
|
|
||||||
string file_name;
|
|
||||||
string model_name;
|
|
||||||
string path = PATH;
|
|
||||||
const vector<struct option> long_options = {
|
|
||||||
{"help", no_argument, nullptr, 'h'},
|
|
||||||
{"file", required_argument, nullptr, 'f'},
|
|
||||||
{"path", required_argument, nullptr, 'p'},
|
|
||||||
{"model", required_argument, nullptr, 'm'},
|
|
||||||
{nullptr, no_argument, nullptr, 0}
|
|
||||||
};
|
|
||||||
while (true) {
|
|
||||||
const auto c = getopt_long(argc, argv, "hf:p:m:", long_options.data(), nullptr);
|
|
||||||
if (c == -1)
|
|
||||||
break;
|
|
||||||
switch (c) {
|
|
||||||
case 'h':
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(0);
|
|
||||||
case 'f':
|
|
||||||
file_name = string(optarg);
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
model_name = string(optarg);
|
|
||||||
break;
|
|
||||||
case 'p':
|
|
||||||
path = optarg;
|
|
||||||
if (path.back() != '/')
|
|
||||||
path += '/';
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
default:
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (file_name.empty()) {
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
return make_tuple(file_name, path, model_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline constexpr auto hash_conv(const std::string_view sv)
|
|
||||||
{
|
|
||||||
unsigned long hash{ 5381 };
|
|
||||||
for (unsigned char c : sv) {
|
|
||||||
hash = ((hash << 5) + hash) ^ c;
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline constexpr auto operator"" _sh(const char* str, size_t len)
|
|
||||||
{
|
|
||||||
return hash_conv(std::string_view{ str, len });
|
|
||||||
}
|
|
||||||
|
|
||||||
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features)
|
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features)
|
||||||
{
|
{
|
||||||
@@ -116,8 +40,23 @@ bool file_exists(const std::string& name)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pair<vector<vector<int>>, vector<int>> extract_indices(vector<int> indices, vector<vector<int>> X, vector<int> y)
|
||||||
|
{
|
||||||
|
vector<vector<int>> Xr;
|
||||||
|
vector<int> yr;
|
||||||
|
for (int col = 0; col < X.size(); ++col) {
|
||||||
|
Xr.push_back(vector<int>());
|
||||||
|
}
|
||||||
|
for (auto index : indices) {
|
||||||
|
for (int col = 0; col < X.size(); ++col) {
|
||||||
|
Xr[col].push_back(X[col][index]);
|
||||||
|
}
|
||||||
|
yr.push_back(y[index]);
|
||||||
|
}
|
||||||
|
return { Xr, yr };
|
||||||
|
}
|
||||||
|
|
||||||
tuple<string, string, string> get_options(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
{
|
{
|
||||||
map<string, bool> datasets = {
|
map<string, bool> datasets = {
|
||||||
{"diabetes", true},
|
{"diabetes", true},
|
||||||
@@ -129,88 +68,102 @@ tuple<string, string, string> get_options(int argc, char** argv)
|
|||||||
{"liver-disorders", true},
|
{"liver-disorders", true},
|
||||||
{"mfeat-factors", true},
|
{"mfeat-factors", true},
|
||||||
};
|
};
|
||||||
vector <string> models = { "AODE", "KDB", "SPODE", "TAN" };
|
auto valid_datasets = vector<string>();
|
||||||
string file_name;
|
transform(datasets.begin(), datasets.end(), back_inserter(valid_datasets),
|
||||||
string path;
|
[](const pair<string, bool>& pair) { return pair.first; });
|
||||||
string model_name;
|
argparse::ArgumentParser program("BayesNetSample");
|
||||||
tie(file_name, path, model_name) = parse_arguments(argc, argv);
|
program.add_argument("-d", "--dataset")
|
||||||
if (datasets.find(file_name) == datasets.end()) {
|
.help("Dataset file name")
|
||||||
cout << "Invalid file name: " << file_name << endl;
|
.action([valid_datasets](const std::string& value) {
|
||||||
usage(argv[0]);
|
if (find(valid_datasets.begin(), valid_datasets.end(), value) != valid_datasets.end()) {
|
||||||
exit(1);
|
return value;
|
||||||
}
|
}
|
||||||
if (!file_exists(path + file_name + ".arff")) {
|
throw runtime_error("file must be one of {diabetes, ecoli, glass, iris, kdd_JapaneseVowels, letter, liver-disorders, mfeat-factors}");
|
||||||
cout << "Data File " << path + file_name + ".arff" << " does not exist" << endl;
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
if (find(models.begin(), models.end(), model_name) == models.end()) {
|
);
|
||||||
cout << "Invalid model name: " << model_name << endl;
|
program.add_argument("-p", "--path")
|
||||||
usage(argv[0]);
|
.help(" folder where the data files are located, default")
|
||||||
exit(1);
|
.default_value(string{ PATH }
|
||||||
|
);
|
||||||
|
program.add_argument("-m", "--model")
|
||||||
|
.help("Model to use " + platform::Models::instance()->toString())
|
||||||
|
.action([](const std::string& value) {
|
||||||
|
static const vector<string> choices = platform::Models::instance()->getNames();
|
||||||
|
if (find(choices.begin(), choices.end(), value) != choices.end()) {
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
return { file_name, path, model_name };
|
throw runtime_error("Model must be one of " + platform::Models::instance()->toString());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
program.add_argument("--discretize").help("Discretize input dataset").default_value(false).implicit_value(true);
|
||||||
|
program.add_argument("--stratified").help("If Stratified KFold is to be done").default_value(false).implicit_value(true);
|
||||||
|
program.add_argument("--tensors").help("Use tensors to store samples").default_value(false).implicit_value(true);
|
||||||
|
program.add_argument("-f", "--folds").help("Number of folds").default_value(5).scan<'i', int>().action([](const string& value) {
|
||||||
|
try {
|
||||||
|
auto k = stoi(value);
|
||||||
|
if (k < 2) {
|
||||||
|
throw runtime_error("Number of folds must be greater than 1");
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
catch (const runtime_error& err) {
|
||||||
|
throw runtime_error(err.what());
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
throw runtime_error("Number of folds must be an integer");
|
||||||
|
}});
|
||||||
|
program.add_argument("-s", "--seed").help("Random seed").default_value(-1).scan<'i', int>();
|
||||||
|
bool class_last, stratified, tensors;
|
||||||
|
string model_name, file_name, path, complete_file_name;
|
||||||
|
int nFolds, seed;
|
||||||
|
try {
|
||||||
|
program.parse_args(argc, argv);
|
||||||
|
file_name = program.get<string>("dataset");
|
||||||
|
path = program.get<string>("path");
|
||||||
|
model_name = program.get<string>("model");
|
||||||
|
complete_file_name = path + file_name + ".arff";
|
||||||
|
stratified = program.get<bool>("stratified");
|
||||||
|
tensors = program.get<bool>("tensors");
|
||||||
|
nFolds = program.get<int>("folds");
|
||||||
|
seed = program.get<int>("seed");
|
||||||
|
class_last = datasets[file_name];
|
||||||
|
if (!file_exists(complete_file_name)) {
|
||||||
|
throw runtime_error("Data File " + path + file_name + ".arff" + " does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const exception& err) {
|
||||||
|
cerr << err.what() << endl;
|
||||||
|
cerr << program;
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
/*
|
||||||
{
|
* Begin Processing
|
||||||
string file_name, path, model_name;
|
*/
|
||||||
tie(file_name, path, model_name) = get_options(argc, argv);
|
|
||||||
auto handler = ArffFiles();
|
auto handler = ArffFiles();
|
||||||
handler.load(path + file_name + ".arff");
|
handler.load(complete_file_name, class_last);
|
||||||
// Get Dataset X, y
|
// Get Dataset X, y
|
||||||
vector<mdlp::samples_t>& X = handler.getX();
|
vector<mdlp::samples_t>& X = handler.getX();
|
||||||
mdlp::labels_t& y = handler.getY();
|
mdlp::labels_t& y = handler.getY();
|
||||||
// Get className & Features
|
// Get className & Features
|
||||||
auto className = handler.getClassName();
|
auto className = handler.getClassName();
|
||||||
vector<string> features;
|
vector<string> features;
|
||||||
for (auto feature : handler.getAttributes()) {
|
auto attributes = handler.getAttributes();
|
||||||
features.push_back(feature.first);
|
transform(attributes.begin(), attributes.end(), back_inserter(features),
|
||||||
}
|
[](const pair<string, string>& item) { return item.first; });
|
||||||
// Discretize Dataset
|
// Discretize Dataset
|
||||||
vector<mdlp::labels_t> Xd;
|
auto [Xd, maxes] = discretize(X, y, features);
|
||||||
map<string, int> maxes;
|
|
||||||
tie(Xd, maxes) = discretize(X, y, features);
|
|
||||||
maxes[className] = *max_element(y.begin(), y.end()) + 1;
|
maxes[className] = *max_element(y.begin(), y.end()) + 1;
|
||||||
map<string, vector<int>> states;
|
map<string, vector<int>> states;
|
||||||
for (auto feature : features) {
|
for (auto feature : features) {
|
||||||
states[feature] = vector<int>(maxes[feature]);
|
states[feature] = vector<int>(maxes[feature]);
|
||||||
}
|
}
|
||||||
states[className] = vector<int>(
|
states[className] = vector<int>(maxes[className]);
|
||||||
maxes[className]);
|
auto clf = platform::Models::instance()->create(model_name);
|
||||||
double score;
|
clf->fit(Xd, y, features, className, states);
|
||||||
vector<string> lines;
|
auto score = clf->score(Xd, y);
|
||||||
vector<string> graph;
|
auto lines = clf->show();
|
||||||
auto kdb = bayesnet::KDB(2);
|
auto graph = clf->graph();
|
||||||
auto aode = bayesnet::AODE();
|
|
||||||
auto spode = bayesnet::SPODE(2);
|
|
||||||
auto tan = bayesnet::TAN();
|
|
||||||
switch (hash_conv(model_name)) {
|
|
||||||
case "AODE"_sh:
|
|
||||||
aode.fit(Xd, y, features, className, states);
|
|
||||||
lines = aode.show();
|
|
||||||
score = aode.score(Xd, y);
|
|
||||||
graph = aode.graph();
|
|
||||||
break;
|
|
||||||
case "KDB"_sh:
|
|
||||||
kdb.fit(Xd, y, features, className, states);
|
|
||||||
lines = kdb.show();
|
|
||||||
score = kdb.score(Xd, y);
|
|
||||||
graph = kdb.graph();
|
|
||||||
break;
|
|
||||||
case "SPODE"_sh:
|
|
||||||
spode.fit(Xd, y, features, className, states);
|
|
||||||
lines = spode.show();
|
|
||||||
score = spode.score(Xd, y);
|
|
||||||
graph = spode.graph();
|
|
||||||
break;
|
|
||||||
case "TAN"_sh:
|
|
||||||
tan.fit(Xd, y, features, className, states);
|
|
||||||
lines = tan.show();
|
|
||||||
score = tan.score(Xd, y);
|
|
||||||
graph = tan.graph();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for (auto line : lines) {
|
for (auto line : lines) {
|
||||||
cout << line << endl;
|
cout << line << endl;
|
||||||
}
|
}
|
||||||
@@ -221,5 +174,48 @@ int main(int argc, char** argv)
|
|||||||
file.close();
|
file.close();
|
||||||
cout << "Graph saved in " << model_name << "_" << file_name << ".dot" << endl;
|
cout << "Graph saved in " << model_name << "_" << file_name << ".dot" << endl;
|
||||||
cout << "dot -Tpng -o " + dot_file + ".png " + dot_file + ".dot " << endl;
|
cout << "dot -Tpng -o " + dot_file + ".png " + dot_file + ".dot " << endl;
|
||||||
|
string stratified_string = stratified ? " Stratified" : "";
|
||||||
|
cout << nFolds << " Folds" << stratified_string << " Cross validation" << endl;
|
||||||
|
cout << "==========================================" << endl;
|
||||||
|
torch::Tensor Xt = torch::zeros({ static_cast<int>(Xd.size()), static_cast<int>(Xd[0].size()) }, torch::kInt32);
|
||||||
|
torch::Tensor yt = torch::tensor(y, torch::kInt32);
|
||||||
|
for (int i = 0; i < features.size(); ++i) {
|
||||||
|
Xt.index_put_({ i, "..." }, torch::tensor(Xd[i], torch::kInt32));
|
||||||
|
}
|
||||||
|
float total_score = 0, total_score_train = 0, score_train, score_test;
|
||||||
|
Fold* fold;
|
||||||
|
if (stratified)
|
||||||
|
fold = new StratifiedKFold(nFolds, y, seed);
|
||||||
|
else
|
||||||
|
fold = new KFold(nFolds, y.size(), seed);
|
||||||
|
for (auto i = 0; i < nFolds; ++i) {
|
||||||
|
auto [train, test] = fold->getFold(i);
|
||||||
|
cout << "Fold: " << i + 1 << endl;
|
||||||
|
if (tensors) {
|
||||||
|
auto ttrain = torch::tensor(train, torch::kInt64);
|
||||||
|
auto ttest = torch::tensor(test, torch::kInt64);
|
||||||
|
torch::Tensor Xtraint = torch::index_select(Xt, 1, ttrain);
|
||||||
|
torch::Tensor ytraint = yt.index({ ttrain });
|
||||||
|
torch::Tensor Xtestt = torch::index_select(Xt, 1, ttest);
|
||||||
|
torch::Tensor ytestt = yt.index({ ttest });
|
||||||
|
clf->fit(Xtraint, ytraint, features, className, states);
|
||||||
|
score_train = clf->score(Xtraint, ytraint);
|
||||||
|
score_test = clf->score(Xtestt, ytestt);
|
||||||
|
} else {
|
||||||
|
auto [Xtrain, ytrain] = extract_indices(train, Xd, y);
|
||||||
|
auto [Xtest, ytest] = extract_indices(test, Xd, y);
|
||||||
|
clf->fit(Xtrain, ytrain, features, className, states);
|
||||||
|
score_train = clf->score(Xtrain, ytrain);
|
||||||
|
score_test = clf->score(Xtest, ytest);
|
||||||
|
}
|
||||||
|
total_score_train += score_train;
|
||||||
|
total_score += score_test;
|
||||||
|
cout << "Score Train: " << score_train << endl;
|
||||||
|
cout << "Score Test : " << score_test << endl;
|
||||||
|
cout << "-------------------------------------------------------------------------------" << endl;
|
||||||
|
}
|
||||||
|
cout << "**********************************************************************************" << endl;
|
||||||
|
cout << "Average Score Train: " << total_score_train / nFolds << endl;
|
||||||
|
cout << "Average Score Test : " << total_score / nFolds << endl;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
@@ -8,7 +8,8 @@ namespace bayesnet {
|
|||||||
void train() override;
|
void train() override;
|
||||||
public:
|
public:
|
||||||
AODE();
|
AODE();
|
||||||
vector<string> graph(string title = "AODE");
|
virtual ~AODE() {};
|
||||||
|
vector<string> graph(string title = "AODE") override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
@@ -1,48 +1,23 @@
|
|||||||
#ifndef CLASSIFIERS_H
|
#ifndef BASE_H
|
||||||
#define CLASSIFIERS_H
|
#define BASE_H
|
||||||
#include <torch/torch.h>
|
#include <torch/torch.h>
|
||||||
#include "Network.h"
|
#include <vector>
|
||||||
#include "Metrics.hpp"
|
|
||||||
using namespace std;
|
|
||||||
using namespace torch;
|
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
|
using namespace std;
|
||||||
class BaseClassifier {
|
class BaseClassifier {
|
||||||
private:
|
|
||||||
bool fitted;
|
|
||||||
BaseClassifier& build(vector<string>& features, string className, map<string, vector<int>>& states);
|
|
||||||
protected:
|
|
||||||
Network model;
|
|
||||||
int m, n; // m: number of samples, n: number of features
|
|
||||||
Tensor X;
|
|
||||||
vector<vector<int>> Xv;
|
|
||||||
Tensor y;
|
|
||||||
vector<int> yv;
|
|
||||||
Tensor dataset;
|
|
||||||
Metrics metrics;
|
|
||||||
vector<string> features;
|
|
||||||
string className;
|
|
||||||
map<string, vector<int>> states;
|
|
||||||
void checkFitParameters();
|
|
||||||
virtual void train() = 0;
|
|
||||||
public:
|
public:
|
||||||
BaseClassifier(Network model);
|
virtual BaseClassifier& fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states) = 0;
|
||||||
|
virtual BaseClassifier& fit(torch::Tensor& X, torch::Tensor& y, vector<string>& features, string className, map<string, vector<int>>& states) = 0;
|
||||||
|
vector<int> virtual predict(vector<vector<int>>& X) = 0;
|
||||||
|
float virtual score(vector<vector<int>>& X, vector<int>& y) = 0;
|
||||||
|
float virtual score(torch::Tensor& X, torch::Tensor& y) = 0;
|
||||||
|
int virtual getNumberOfNodes() = 0;
|
||||||
|
int virtual getNumberOfEdges() = 0;
|
||||||
|
int virtual getNumberOfStates() = 0;
|
||||||
|
vector<string> virtual show() = 0;
|
||||||
|
vector<string> virtual graph(string title = "") = 0;
|
||||||
virtual ~BaseClassifier() = default;
|
virtual ~BaseClassifier() = default;
|
||||||
BaseClassifier& fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states);
|
const string inline getVersion() const { return "0.1.0"; };
|
||||||
void addNodes();
|
|
||||||
int getNumberOfNodes();
|
|
||||||
int getNumberOfEdges();
|
|
||||||
Tensor predict(Tensor& X);
|
|
||||||
vector<int> predict(vector<vector<int>>& X);
|
|
||||||
float score(Tensor& X, Tensor& y);
|
|
||||||
float score(vector<vector<int>>& X, vector<int>& y);
|
|
||||||
vector<string> show();
|
|
||||||
virtual vector<string> graph(string title) = 0;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
#include "Metrics.hpp"
|
#include "BayesMetrics.h"
|
||||||
#include "Mst.h"
|
#include "Mst.h"
|
||||||
using namespace std;
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
Metrics::Metrics(torch::Tensor& samples, vector<string>& features, string& className, int classNumStates)
|
Metrics::Metrics(torch::Tensor& samples, vector<string>& features, string& className, int classNumStates)
|
||||||
: samples(samples)
|
: samples(samples)
|
||||||
@@ -13,12 +12,12 @@ namespace bayesnet {
|
|||||||
: features(features)
|
: features(features)
|
||||||
, className(className)
|
, className(className)
|
||||||
, classNumStates(classNumStates)
|
, classNumStates(classNumStates)
|
||||||
|
, samples(torch::zeros({ static_cast<int>(vsamples[0].size()), static_cast<int>(vsamples.size() + 1) }, torch::kInt32))
|
||||||
{
|
{
|
||||||
samples = torch::zeros({ static_cast<int64_t>(vsamples[0].size()), static_cast<int64_t>(vsamples.size() + 1) }, torch::kInt64);
|
|
||||||
for (int i = 0; i < vsamples.size(); ++i) {
|
for (int i = 0; i < vsamples.size(); ++i) {
|
||||||
samples.index_put_({ "...", i }, torch::tensor(vsamples[i], torch::kInt64));
|
samples.index_put_({ "...", i }, torch::tensor(vsamples[i], torch::kInt32));
|
||||||
}
|
}
|
||||||
samples.index_put_({ "...", -1 }, torch::tensor(labels, torch::kInt64));
|
samples.index_put_({ "...", -1 }, torch::tensor(labels, torch::kInt32));
|
||||||
}
|
}
|
||||||
vector<pair<string, string>> Metrics::doCombinations(const vector<string>& source)
|
vector<pair<string, string>> Metrics::doCombinations(const vector<string>& source)
|
||||||
{
|
{
|
||||||
@@ -44,8 +43,8 @@ namespace bayesnet {
|
|||||||
margin[value] = mask.sum().item<float>() / samples.sizes()[0];
|
margin[value] = mask.sum().item<float>() / samples.sizes()[0];
|
||||||
}
|
}
|
||||||
for (auto [first, second] : combinations) {
|
for (auto [first, second] : combinations) {
|
||||||
int64_t index_first = find(features.begin(), features.end(), first) - features.begin();
|
int index_first = find(features.begin(), features.end(), first) - features.begin();
|
||||||
int64_t index_second = find(features.begin(), features.end(), second) - features.begin();
|
int index_second = find(features.begin(), features.end(), second) - features.begin();
|
||||||
double accumulated = 0;
|
double accumulated = 0;
|
||||||
for (int value = 0; value < classNumStates; ++value) {
|
for (int value = 0; value < classNumStates; ++value) {
|
||||||
auto mask = samples.index({ "...", -1 }) == value;
|
auto mask = samples.index({ "...", -1 }) == value;
|
||||||
@@ -95,7 +94,7 @@ namespace bayesnet {
|
|||||||
totalWeight += 1;
|
totalWeight += 1;
|
||||||
}
|
}
|
||||||
if (totalWeight == 0)
|
if (totalWeight == 0)
|
||||||
throw invalid_argument("Total weight should not be zero");
|
return 0;
|
||||||
double entropyValue = 0;
|
double entropyValue = 0;
|
||||||
for (int value = 0; value < featureCounts.sizes()[0]; ++value) {
|
for (int value = 0; value < featureCounts.sizes()[0]; ++value) {
|
||||||
double p_f = featureCounts[value].item<double>() / totalWeight;
|
double p_f = featureCounts[value].item<double>() / totalWeight;
|
||||||
@@ -124,7 +123,6 @@ namespace bayesnet {
|
|||||||
*/
|
*/
|
||||||
vector<pair<int, int>> Metrics::maximumSpanningTree(vector<string> features, Tensor& weights, int root)
|
vector<pair<int, int>> Metrics::maximumSpanningTree(vector<string> features, Tensor& weights, int root)
|
||||||
{
|
{
|
||||||
auto result = vector<pair<int, int>>();
|
|
||||||
auto mst = MST(features, weights, root);
|
auto mst = MST(features, weights, root);
|
||||||
return mst.maximumSpanningTree();
|
return mst.maximumSpanningTree();
|
||||||
}
|
}
|
@@ -11,7 +11,7 @@ namespace bayesnet {
|
|||||||
Tensor samples;
|
Tensor samples;
|
||||||
vector<string> features;
|
vector<string> features;
|
||||||
string className;
|
string className;
|
||||||
int classNumStates;
|
int classNumStates = 0;
|
||||||
public:
|
public:
|
||||||
Metrics() = default;
|
Metrics() = default;
|
||||||
Metrics(Tensor&, vector<string>&, string&, int);
|
Metrics(Tensor&, vector<string>&, string&, int);
|
@@ -1,2 +1,2 @@
|
|||||||
add_library(BayesNet bayesnetUtils.cc Network.cc Node.cc Metrics.cc BaseClassifier.cc KDB.cc TAN.cc SPODE.cc Ensemble.cc AODE.cc Mst.cc)
|
add_library(BayesNet bayesnetUtils.cc Network.cc Node.cc BayesMetrics.cc Classifier.cc KDB.cc TAN.cc SPODE.cc Ensemble.cc AODE.cc Mst.cc)
|
||||||
target_link_libraries(BayesNet "${TORCH_LIBRARIES}")
|
target_link_libraries(BayesNet "${TORCH_LIBRARIES}")
|
@@ -1,12 +1,11 @@
|
|||||||
#include "BaseClassifier.h"
|
#include "Classifier.h"
|
||||||
#include "bayesnetUtils.h"
|
#include "bayesnetUtils.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
|
|
||||||
BaseClassifier::BaseClassifier(Network model) : model(model), m(0), n(0), metrics(Metrics()), fitted(false) {}
|
Classifier::Classifier(Network model) : model(model), m(0), n(0), metrics(Metrics()), fitted(false) {}
|
||||||
BaseClassifier& BaseClassifier::build(vector<string>& features, string className, map<string, vector<int>>& states)
|
Classifier& Classifier::build(vector<string>& features, string className, map<string, vector<int>>& states)
|
||||||
{
|
{
|
||||||
dataset = torch::cat({ X, y.view({y.size(0), 1}) }, 1);
|
dataset = torch::cat({ X, y.view({y.size(0), 1}) }, 1);
|
||||||
this->features = features;
|
this->features = features;
|
||||||
@@ -16,22 +15,37 @@ namespace bayesnet {
|
|||||||
auto n_classes = states[className].size();
|
auto n_classes = states[className].size();
|
||||||
metrics = Metrics(dataset, features, className, n_classes);
|
metrics = Metrics(dataset, features, className, n_classes);
|
||||||
train();
|
train();
|
||||||
|
if (Xv == vector<vector<int>>()) {
|
||||||
|
// fit with tensors
|
||||||
|
model.fit(X, y, features, className);
|
||||||
|
} else {
|
||||||
|
// fit with vectors
|
||||||
model.fit(Xv, yv, features, className);
|
model.fit(Xv, yv, features, className);
|
||||||
|
}
|
||||||
fitted = true;
|
fitted = true;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
BaseClassifier& BaseClassifier::fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
Classifier& Classifier::fit(torch::Tensor& X, torch::Tensor& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
||||||
{
|
{
|
||||||
this->X = torch::zeros({ static_cast<int64_t>(X[0].size()), static_cast<int64_t>(X.size()) }, kInt64);
|
this->X = torch::transpose(X, 0, 1);
|
||||||
|
this->y = y;
|
||||||
|
Xv = vector<vector<int>>();
|
||||||
|
yv = vector<int>(y.data_ptr<int>(), y.data_ptr<int>() + y.size(0));
|
||||||
|
return build(features, className, states);
|
||||||
|
}
|
||||||
|
|
||||||
|
Classifier& Classifier::fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
||||||
|
{
|
||||||
|
this->X = torch::zeros({ static_cast<int>(X[0].size()), static_cast<int>(X.size()) }, kInt32);
|
||||||
Xv = X;
|
Xv = X;
|
||||||
for (int i = 0; i < X.size(); ++i) {
|
for (int i = 0; i < X.size(); ++i) {
|
||||||
this->X.index_put_({ "...", i }, torch::tensor(X[i], kInt64));
|
this->X.index_put_({ "...", i }, torch::tensor(X[i], kInt32));
|
||||||
}
|
}
|
||||||
this->y = torch::tensor(y, kInt64);
|
this->y = torch::tensor(y, kInt32);
|
||||||
yv = y;
|
yv = y;
|
||||||
return build(features, className, states);
|
return build(features, className, states);
|
||||||
}
|
}
|
||||||
void BaseClassifier::checkFitParameters()
|
void Classifier::checkFitParameters()
|
||||||
{
|
{
|
||||||
auto sizes = X.sizes();
|
auto sizes = X.sizes();
|
||||||
m = sizes[0];
|
m = sizes[0];
|
||||||
@@ -52,23 +66,24 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Tensor BaseClassifier::predict(Tensor& X)
|
Tensor Classifier::predict(Tensor& X)
|
||||||
{
|
{
|
||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
throw logic_error("Classifier has not been fitted");
|
throw logic_error("Classifier has not been fitted");
|
||||||
}
|
}
|
||||||
auto m_ = X.size(0);
|
auto m_ = X.size(0);
|
||||||
auto n_ = X.size(1);
|
auto n_ = X.size(1);
|
||||||
|
//auto Xt = torch::transpose(X, 0, 1);
|
||||||
vector<vector<int>> Xd(n_, vector<int>(m_, 0));
|
vector<vector<int>> Xd(n_, vector<int>(m_, 0));
|
||||||
for (auto i = 0; i < n_; i++) {
|
for (auto i = 0; i < n_; i++) {
|
||||||
auto temp = X.index({ "...", i });
|
auto temp = X.index({ "...", i });
|
||||||
Xd[i] = vector<int>(temp.data_ptr<int>(), temp.data_ptr<int>() + m_);
|
Xd[i] = vector<int>(temp.data_ptr<int>(), temp.data_ptr<int>() + temp.numel());
|
||||||
}
|
}
|
||||||
auto yp = model.predict(Xd);
|
auto yp = model.predict(Xd);
|
||||||
auto ypred = torch::tensor(yp, torch::kInt64);
|
auto ypred = torch::tensor(yp, torch::kInt32);
|
||||||
return ypred;
|
return ypred;
|
||||||
}
|
}
|
||||||
vector<int> BaseClassifier::predict(vector<vector<int>>& X)
|
vector<int> Classifier::predict(vector<vector<int>>& X)
|
||||||
{
|
{
|
||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
throw logic_error("Classifier has not been fitted");
|
throw logic_error("Classifier has not been fitted");
|
||||||
@@ -82,15 +97,16 @@ namespace bayesnet {
|
|||||||
auto yp = model.predict(Xd);
|
auto yp = model.predict(Xd);
|
||||||
return yp;
|
return yp;
|
||||||
}
|
}
|
||||||
float BaseClassifier::score(Tensor& X, Tensor& y)
|
float Classifier::score(Tensor& X, Tensor& y)
|
||||||
{
|
{
|
||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
throw logic_error("Classifier has not been fitted");
|
throw logic_error("Classifier has not been fitted");
|
||||||
}
|
}
|
||||||
Tensor y_pred = predict(X);
|
auto Xt = torch::transpose(X, 0, 1);
|
||||||
|
Tensor y_pred = predict(Xt);
|
||||||
return (y_pred == y).sum().item<float>() / y.size(0);
|
return (y_pred == y).sum().item<float>() / y.size(0);
|
||||||
}
|
}
|
||||||
float BaseClassifier::score(vector<vector<int>>& X, vector<int>& y)
|
float Classifier::score(vector<vector<int>>& X, vector<int>& y)
|
||||||
{
|
{
|
||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
throw logic_error("Classifier has not been fitted");
|
throw logic_error("Classifier has not been fitted");
|
||||||
@@ -103,11 +119,11 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
return model.score(Xd, y);
|
return model.score(Xd, y);
|
||||||
}
|
}
|
||||||
vector<string> BaseClassifier::show()
|
vector<string> Classifier::show()
|
||||||
{
|
{
|
||||||
return model.show();
|
return model.show();
|
||||||
}
|
}
|
||||||
void BaseClassifier::addNodes()
|
void Classifier::addNodes()
|
||||||
{
|
{
|
||||||
// Add all nodes to the network
|
// Add all nodes to the network
|
||||||
for (auto feature : features) {
|
for (auto feature : features) {
|
||||||
@@ -115,13 +131,17 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
model.addNode(className, states[className].size());
|
model.addNode(className, states[className].size());
|
||||||
}
|
}
|
||||||
int BaseClassifier::getNumberOfNodes()
|
int Classifier::getNumberOfNodes()
|
||||||
{
|
{
|
||||||
// Features does not include class
|
// Features does not include class
|
||||||
return fitted ? model.getFeatures().size() + 1 : 0;
|
return fitted ? model.getFeatures().size() + 1 : 0;
|
||||||
}
|
}
|
||||||
int BaseClassifier::getNumberOfEdges()
|
int Classifier::getNumberOfEdges()
|
||||||
{
|
{
|
||||||
return fitted ? model.getEdges().size() : 0;
|
return fitted ? model.getEdges().size() : 0;
|
||||||
}
|
}
|
||||||
|
int Classifier::getNumberOfStates()
|
||||||
|
{
|
||||||
|
return fitted ? model.getStates() : 0;
|
||||||
|
}
|
||||||
}
|
}
|
50
src/BayesNet/Classifier.h
Normal file
50
src/BayesNet/Classifier.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#ifndef CLASSIFIER_H
|
||||||
|
#define CLASSIFIER_H
|
||||||
|
#include <torch/torch.h>
|
||||||
|
#include "BaseClassifier.h"
|
||||||
|
#include "Network.h"
|
||||||
|
#include "BayesMetrics.h"
|
||||||
|
using namespace std;
|
||||||
|
using namespace torch;
|
||||||
|
|
||||||
|
namespace bayesnet {
|
||||||
|
class Classifier : public BaseClassifier {
|
||||||
|
private:
|
||||||
|
bool fitted;
|
||||||
|
Classifier& build(vector<string>& features, string className, map<string, vector<int>>& states);
|
||||||
|
protected:
|
||||||
|
Network model;
|
||||||
|
int m, n; // m: number of samples, n: number of features
|
||||||
|
Tensor X;
|
||||||
|
vector<vector<int>> Xv;
|
||||||
|
Tensor y;
|
||||||
|
vector<int> yv;
|
||||||
|
Tensor dataset;
|
||||||
|
Metrics metrics;
|
||||||
|
vector<string> features;
|
||||||
|
string className;
|
||||||
|
map<string, vector<int>> states;
|
||||||
|
void checkFitParameters();
|
||||||
|
virtual void train() = 0;
|
||||||
|
public:
|
||||||
|
Classifier(Network model);
|
||||||
|
virtual ~Classifier() = default;
|
||||||
|
Classifier& fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states) override;
|
||||||
|
Classifier& fit(torch::Tensor& X, torch::Tensor& y, vector<string>& features, string className, map<string, vector<int>>& states) override;
|
||||||
|
void addNodes();
|
||||||
|
int getNumberOfNodes() override;
|
||||||
|
int getNumberOfEdges() override;
|
||||||
|
int getNumberOfStates() override;
|
||||||
|
Tensor predict(Tensor& X);
|
||||||
|
vector<int> predict(vector<vector<int>>& X) override;
|
||||||
|
float score(Tensor& X, Tensor& y) override;
|
||||||
|
float score(vector<vector<int>>& X, vector<int>& y) override;
|
||||||
|
vector<string> show() override;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,7 +1,6 @@
|
|||||||
#include "Ensemble.h"
|
#include "Ensemble.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
|
|
||||||
Ensemble::Ensemble() : m(0), n(0), n_models(0), metrics(Metrics()), fitted(false) {}
|
Ensemble::Ensemble() : m(0), n(0), n_models(0), metrics(Metrics()), fitted(false) {}
|
||||||
@@ -23,14 +22,22 @@ namespace bayesnet {
|
|||||||
fitted = true;
|
fitted = true;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
Ensemble& Ensemble::fit(torch::Tensor& X, torch::Tensor& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
||||||
|
{
|
||||||
|
this->X = X;
|
||||||
|
this->y = y;
|
||||||
|
Xv = vector<vector<int>>();
|
||||||
|
yv = vector<int>(y.data_ptr<int>(), y.data_ptr<int>() + y.size(0));
|
||||||
|
return build(features, className, states);
|
||||||
|
}
|
||||||
Ensemble& Ensemble::fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
Ensemble& Ensemble::fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states)
|
||||||
{
|
{
|
||||||
this->X = torch::zeros({ static_cast<int64_t>(X[0].size()), static_cast<int64_t>(X.size()) }, kInt64);
|
this->X = torch::zeros({ static_cast<int>(X[0].size()), static_cast<int>(X.size()) }, kInt32);
|
||||||
Xv = X;
|
Xv = X;
|
||||||
for (int i = 0; i < X.size(); ++i) {
|
for (int i = 0; i < X.size(); ++i) {
|
||||||
this->X.index_put_({ "...", i }, torch::tensor(X[i], kInt64));
|
this->X.index_put_({ "...", i }, torch::tensor(X[i], kInt32));
|
||||||
}
|
}
|
||||||
this->y = torch::tensor(y, kInt64);
|
this->y = torch::tensor(y, kInt32);
|
||||||
yv = y;
|
yv = y;
|
||||||
return build(features, className, states);
|
return build(features, className, states);
|
||||||
}
|
}
|
||||||
@@ -39,7 +46,7 @@ namespace bayesnet {
|
|||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
throw logic_error("Ensemble has not been fitted");
|
throw logic_error("Ensemble has not been fitted");
|
||||||
}
|
}
|
||||||
Tensor y_pred = torch::zeros({ X.size(0), n_models }, kInt64);
|
Tensor y_pred = torch::zeros({ X.size(0), n_models }, kInt32);
|
||||||
for (auto i = 0; i < n_models; ++i) {
|
for (auto i = 0; i < n_models; ++i) {
|
||||||
y_pred.index_put_({ "...", i }, models[i]->predict(X));
|
y_pred.index_put_({ "...", i }, models[i]->predict(X));
|
||||||
}
|
}
|
||||||
@@ -47,7 +54,7 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
vector<int> Ensemble::voting(Tensor& y_pred)
|
vector<int> Ensemble::voting(Tensor& y_pred)
|
||||||
{
|
{
|
||||||
auto y_pred_ = y_pred.accessor<int64_t, 2>();
|
auto y_pred_ = y_pred.accessor<int, 2>();
|
||||||
vector<int> y_pred_final;
|
vector<int> y_pred_final;
|
||||||
for (int i = 0; i < y_pred.size(0); ++i) {
|
for (int i = 0; i < y_pred.size(0); ++i) {
|
||||||
vector<float> votes(states[className].size(), 0);
|
vector<float> votes(states[className].size(), 0);
|
||||||
@@ -70,12 +77,26 @@ namespace bayesnet {
|
|||||||
for (auto i = 0; i < n_; i++) {
|
for (auto i = 0; i < n_; i++) {
|
||||||
Xd[i] = vector<int>(X[i].begin(), X[i].end());
|
Xd[i] = vector<int>(X[i].begin(), X[i].end());
|
||||||
}
|
}
|
||||||
Tensor y_pred = torch::zeros({ m_, n_models }, kInt64);
|
Tensor y_pred = torch::zeros({ m_, n_models }, kInt32);
|
||||||
for (auto i = 0; i < n_models; ++i) {
|
for (auto i = 0; i < n_models; ++i) {
|
||||||
y_pred.index_put_({ "...", i }, torch::tensor(models[i]->predict(Xd), kInt64));
|
y_pred.index_put_({ "...", i }, torch::tensor(models[i]->predict(Xd), kInt32));
|
||||||
}
|
}
|
||||||
return voting(y_pred);
|
return voting(y_pred);
|
||||||
}
|
}
|
||||||
|
float Ensemble::score(Tensor& X, Tensor& y)
|
||||||
|
{
|
||||||
|
if (!fitted) {
|
||||||
|
throw logic_error("Ensemble has not been fitted");
|
||||||
|
}
|
||||||
|
auto y_pred = predict(X);
|
||||||
|
int correct = 0;
|
||||||
|
for (int i = 0; i < y_pred.size(0); ++i) {
|
||||||
|
if (y_pred[i].item<int>() == y[i].item<int>()) {
|
||||||
|
correct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (double)correct / y_pred.size(0);
|
||||||
|
}
|
||||||
float Ensemble::score(vector<vector<int>>& X, vector<int>& y)
|
float Ensemble::score(vector<vector<int>>& X, vector<int>& y)
|
||||||
{
|
{
|
||||||
if (!fitted) {
|
if (!fitted) {
|
||||||
@@ -109,4 +130,28 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
int Ensemble::getNumberOfNodes()
|
||||||
|
{
|
||||||
|
int nodes = 0;
|
||||||
|
for (auto i = 0; i < n_models; ++i) {
|
||||||
|
nodes += models[i]->getNumberOfNodes();
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
int Ensemble::getNumberOfEdges()
|
||||||
|
{
|
||||||
|
int edges = 0;
|
||||||
|
for (auto i = 0; i < n_models; ++i) {
|
||||||
|
edges += models[i]->getNumberOfEdges();
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
int Ensemble::getNumberOfStates()
|
||||||
|
{
|
||||||
|
int nstates = 0;
|
||||||
|
for (auto i = 0; i < n_models; ++i) {
|
||||||
|
nstates += models[i]->getNumberOfStates();
|
||||||
|
}
|
||||||
|
return nstates;
|
||||||
|
}
|
||||||
}
|
}
|
@@ -1,20 +1,20 @@
|
|||||||
#ifndef ENSEMBLE_H
|
#ifndef ENSEMBLE_H
|
||||||
#define ENSEMBLE_H
|
#define ENSEMBLE_H
|
||||||
#include <torch/torch.h>
|
#include <torch/torch.h>
|
||||||
#include "BaseClassifier.h"
|
#include "Classifier.h"
|
||||||
#include "Metrics.hpp"
|
#include "BayesMetrics.h"
|
||||||
#include "bayesnetUtils.h"
|
#include "bayesnetUtils.h"
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
class Ensemble {
|
class Ensemble : public BaseClassifier {
|
||||||
private:
|
private:
|
||||||
bool fitted;
|
bool fitted;
|
||||||
long n_models;
|
long n_models;
|
||||||
Ensemble& build(vector<string>& features, string className, map<string, vector<int>>& states);
|
Ensemble& build(vector<string>& features, string className, map<string, vector<int>>& states);
|
||||||
protected:
|
protected:
|
||||||
vector<unique_ptr<BaseClassifier>> models;
|
vector<unique_ptr<Classifier>> models;
|
||||||
int m, n; // m: number of samples, n: number of features
|
int m, n; // m: number of samples, n: number of features
|
||||||
Tensor X;
|
Tensor X;
|
||||||
vector<vector<int>> Xv;
|
vector<vector<int>> Xv;
|
||||||
@@ -30,13 +30,17 @@ namespace bayesnet {
|
|||||||
public:
|
public:
|
||||||
Ensemble();
|
Ensemble();
|
||||||
virtual ~Ensemble() = default;
|
virtual ~Ensemble() = default;
|
||||||
Ensemble& fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states);
|
Ensemble& fit(vector<vector<int>>& X, vector<int>& y, vector<string>& features, string className, map<string, vector<int>>& states) override;
|
||||||
|
Ensemble& fit(torch::Tensor& X, torch::Tensor& y, vector<string>& features, string className, map<string, vector<int>>& states) override;
|
||||||
Tensor predict(Tensor& X);
|
Tensor predict(Tensor& X);
|
||||||
vector<int> predict(vector<vector<int>>& X);
|
vector<int> predict(vector<vector<int>>& X) override;
|
||||||
float score(Tensor& X, Tensor& y);
|
float score(Tensor& X, Tensor& y) override;
|
||||||
float score(vector<vector<int>>& X, vector<int>& y);
|
float score(vector<vector<int>>& X, vector<int>& y) override;
|
||||||
vector<string> show();
|
int getNumberOfNodes() override;
|
||||||
vector<string> graph(string title);
|
int getNumberOfEdges() override;
|
||||||
|
int getNumberOfStates() override;
|
||||||
|
vector<string> show() override;
|
||||||
|
vector<string> graph(string title) override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
#include "KDB.h"
|
#include "KDB.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
|
|
||||||
KDB::KDB(int k, float theta) : BaseClassifier(Network()), k(k), theta(theta) {}
|
KDB::KDB(int k, float theta) : Classifier(Network()), k(k), theta(theta) {}
|
||||||
void KDB::train()
|
void KDB::train()
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
#ifndef KDB_H
|
#ifndef KDB_H
|
||||||
#define KDB_H
|
#define KDB_H
|
||||||
#include "BaseClassifier.h"
|
#include "Classifier.h"
|
||||||
#include "bayesnetUtils.h"
|
#include "bayesnetUtils.h"
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
class KDB : public BaseClassifier {
|
class KDB : public Classifier {
|
||||||
private:
|
private:
|
||||||
int k;
|
int k;
|
||||||
float theta;
|
float theta;
|
||||||
@@ -13,7 +13,8 @@ namespace bayesnet {
|
|||||||
protected:
|
protected:
|
||||||
void train() override;
|
void train() override;
|
||||||
public:
|
public:
|
||||||
KDB(int k, float theta = 0.03);
|
explicit KDB(int k, float theta = 0.03);
|
||||||
|
virtual ~KDB() {};
|
||||||
vector<string> graph(string name = "KDB") override;
|
vector<string> graph(string name = "KDB") override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -7,9 +7,8 @@
|
|||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
using namespace std;
|
||||||
Graph::Graph(int V)
|
Graph::Graph(int V) : V(V), parent(vector<int>(V))
|
||||||
{
|
{
|
||||||
parent = vector<int>(V);
|
|
||||||
for (int i = 0; i < V; i++)
|
for (int i = 0; i < V; i++)
|
||||||
parent[i] = i;
|
parent[i] = i;
|
||||||
G.clear();
|
G.clear();
|
||||||
@@ -34,10 +33,10 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
void Graph::kruskal_algorithm()
|
void Graph::kruskal_algorithm()
|
||||||
{
|
{
|
||||||
int i, uSt, vEd;
|
|
||||||
// sort the edges ordered on decreasing weight
|
// sort the edges ordered on decreasing weight
|
||||||
sort(G.begin(), G.end(), [](auto& left, auto& right) {return left.first > right.first;});
|
sort(G.begin(), G.end(), [](const auto& left, const auto& right) {return left.first > right.first;});
|
||||||
for (i = 0; i < G.size(); i++) {
|
for (int i = 0; i < G.size(); i++) {
|
||||||
|
int uSt, vEd;
|
||||||
uSt = find_set(G[i].second.first);
|
uSt = find_set(G[i].second.first);
|
||||||
vEd = find_set(G[i].second.second);
|
vEd = find_set(G[i].second.second);
|
||||||
if (uSt != vEd) {
|
if (uSt != vEd) {
|
||||||
@@ -103,7 +102,7 @@ namespace bayesnet {
|
|||||||
|
|
||||||
// Make a complete graph
|
// Make a complete graph
|
||||||
for (int i = 0; i < num_features - 1; ++i) {
|
for (int i = 0; i < num_features - 1; ++i) {
|
||||||
for (int j = i; j < num_features; ++j) {
|
for (int j = i + 1; j < num_features; ++j) {
|
||||||
g.addEdge(i, j, weights[i][j].item<float>());
|
g.addEdge(i, j, weights[i][j].item<float>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ namespace bayesnet {
|
|||||||
private:
|
private:
|
||||||
Tensor weights;
|
Tensor weights;
|
||||||
vector<string> features;
|
vector<string> features;
|
||||||
int root;
|
int root = 0;
|
||||||
public:
|
public:
|
||||||
MST() = default;
|
MST() = default;
|
||||||
MST(vector<string>& features, Tensor& weights, int root);
|
MST(vector<string>& features, Tensor& weights, int root);
|
||||||
@@ -23,7 +23,7 @@ namespace bayesnet {
|
|||||||
vector <pair<float, pair<int, int>>> T; // vector for mst
|
vector <pair<float, pair<int, int>>> T; // vector for mst
|
||||||
vector<int> parent;
|
vector<int> parent;
|
||||||
public:
|
public:
|
||||||
Graph(int V);
|
explicit Graph(int V);
|
||||||
void addEdge(int u, int v, float wt);
|
void addEdge(int u, int v, float wt);
|
||||||
int find_set(int i);
|
int find_set(int i);
|
||||||
void union_set(int u, int v);
|
void union_set(int u, int v);
|
||||||
|
@@ -1,14 +1,15 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include "Network.h"
|
#include "Network.h"
|
||||||
|
#include "bayesnetUtils.h"
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
Network::Network() : laplaceSmoothing(1), features(vector<string>()), className(""), classNumStates(0), maxThreads(0.8), fitted(false) {}
|
Network::Network() : laplaceSmoothing(1), features(vector<string>()), className(""), classNumStates(0), maxThreads(0.8), fitted(false) {}
|
||||||
Network::Network(float maxT) : laplaceSmoothing(1), features(vector<string>()), className(""), classNumStates(0), maxThreads(maxT), fitted(false) {}
|
Network::Network(float maxT) : laplaceSmoothing(1), features(vector<string>()), className(""), classNumStates(0), maxThreads(maxT), fitted(false) {}
|
||||||
Network::Network(float maxT, int smoothing) : laplaceSmoothing(smoothing), features(vector<string>()), className(""), classNumStates(0), maxThreads(maxT), fitted(false) {}
|
Network::Network(float maxT, int smoothing) : laplaceSmoothing(smoothing), features(vector<string>()), className(""), classNumStates(0), maxThreads(maxT), fitted(false) {}
|
||||||
Network::Network(Network& other) : laplaceSmoothing(other.laplaceSmoothing), features(other.features), className(other.className), classNumStates(other.getClassNumStates()), maxThreads(other.getmaxThreads()), fitted(other.fitted)
|
Network::Network(Network& other) : laplaceSmoothing(other.laplaceSmoothing), features(other.features), className(other.className), classNumStates(other.getClassNumStates()), maxThreads(other.getmaxThreads()), fitted(other.fitted)
|
||||||
{
|
{
|
||||||
for (auto& pair : other.nodes) {
|
for (const auto& pair : other.nodes) {
|
||||||
nodes[pair.first] = make_unique<Node>(*pair.second);
|
nodes[pair.first] = std::make_unique<Node>(*pair.second);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
float Network::getmaxThreads()
|
float Network::getmaxThreads()
|
||||||
@@ -19,17 +20,18 @@ namespace bayesnet {
|
|||||||
{
|
{
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
void Network::addNode(string name, int numStates)
|
void Network::addNode(const string& name, int numStates)
|
||||||
{
|
{
|
||||||
if (find(features.begin(), features.end(), name) == features.end()) {
|
if (find(features.begin(), features.end(), name) == features.end()) {
|
||||||
features.push_back(name);
|
features.push_back(name);
|
||||||
}
|
}
|
||||||
if (nodes.find(name) != nodes.end()) {
|
if (nodes.find(name) != nodes.end()) {
|
||||||
// if node exists update its number of states
|
// if node exists update its number of states and remove parents, children and CPT
|
||||||
|
nodes[name]->clear();
|
||||||
nodes[name]->setNumStates(numStates);
|
nodes[name]->setNumStates(numStates);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
nodes[name] = make_unique<Node>(name, numStates);
|
nodes[name] = std::make_unique<Node>(name, numStates);
|
||||||
}
|
}
|
||||||
vector<string> Network::getFeatures()
|
vector<string> Network::getFeatures()
|
||||||
{
|
{
|
||||||
@@ -67,7 +69,7 @@ namespace bayesnet {
|
|||||||
recStack.erase(nodeId); // remove node from recursion stack before function ends
|
recStack.erase(nodeId); // remove node from recursion stack before function ends
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
void Network::addEdge(const string parent, const string child)
|
void Network::addEdge(const string& parent, const string& child)
|
||||||
{
|
{
|
||||||
if (nodes.find(parent) == nodes.end()) {
|
if (nodes.find(parent) == nodes.end()) {
|
||||||
throw invalid_argument("Parent node " + parent + " does not exist");
|
throw invalid_argument("Parent node " + parent + " does not exist");
|
||||||
@@ -87,27 +89,50 @@ namespace bayesnet {
|
|||||||
nodes[child]->removeParent(nodes[parent].get());
|
nodes[child]->removeParent(nodes[parent].get());
|
||||||
throw invalid_argument("Adding this edge forms a cycle in the graph.");
|
throw invalid_argument("Adding this edge forms a cycle in the graph.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
map<string, std::unique_ptr<Node>>& Network::getNodes()
|
map<string, std::unique_ptr<Node>>& Network::getNodes()
|
||||||
{
|
{
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
void Network::fit(torch::Tensor& X, torch::Tensor& y, const vector<string>& featureNames, const string& className)
|
||||||
|
{
|
||||||
|
features = featureNames;
|
||||||
|
this->className = className;
|
||||||
|
dataset.clear();
|
||||||
|
// Specific part
|
||||||
|
classNumStates = torch::max(y).item<int>() + 1;
|
||||||
|
samples = torch::cat({ X, y.view({ y.size(0), 1 }) }, 1);
|
||||||
|
for (int i = 0; i < featureNames.size(); ++i) {
|
||||||
|
auto column = torch::flatten(X.index({ "...", i }));
|
||||||
|
auto k = vector<int>();
|
||||||
|
for (auto z = 0; z < X.size(0); ++z) {
|
||||||
|
k.push_back(column[z].item<int>());
|
||||||
|
}
|
||||||
|
dataset[featureNames[i]] = k;
|
||||||
|
}
|
||||||
|
dataset[className] = vector<int>(y.data_ptr<int>(), y.data_ptr<int>() + y.size(0));
|
||||||
|
completeFit();
|
||||||
|
}
|
||||||
void Network::fit(const vector<vector<int>>& input_data, const vector<int>& labels, const vector<string>& featureNames, const string& className)
|
void Network::fit(const vector<vector<int>>& input_data, const vector<int>& labels, const vector<string>& featureNames, const string& className)
|
||||||
{
|
{
|
||||||
features = featureNames;
|
features = featureNames;
|
||||||
this->className = className;
|
this->className = className;
|
||||||
dataset.clear();
|
dataset.clear();
|
||||||
|
// Specific part
|
||||||
|
classNumStates = *max_element(labels.begin(), labels.end()) + 1;
|
||||||
// Build dataset & tensor of samples
|
// Build dataset & tensor of samples
|
||||||
samples = torch::zeros({ static_cast<int64_t>(input_data[0].size()), static_cast<int64_t>(input_data.size() + 1) }, torch::kInt64);
|
samples = torch::zeros({ static_cast<int>(input_data[0].size()), static_cast<int>(input_data.size() + 1) }, torch::kInt32);
|
||||||
for (int i = 0; i < featureNames.size(); ++i) {
|
for (int i = 0; i < featureNames.size(); ++i) {
|
||||||
dataset[featureNames[i]] = input_data[i];
|
dataset[featureNames[i]] = input_data[i];
|
||||||
samples.index_put_({ "...", i }, torch::tensor(input_data[i], torch::kInt64));
|
samples.index_put_({ "...", i }, torch::tensor(input_data[i], torch::kInt32));
|
||||||
}
|
}
|
||||||
dataset[className] = labels;
|
dataset[className] = labels;
|
||||||
samples.index_put_({ "...", -1 }, torch::tensor(labels, torch::kInt64));
|
samples.index_put_({ "...", -1 }, torch::tensor(labels, torch::kInt32));
|
||||||
classNumStates = *max_element(labels.begin(), labels.end()) + 1;
|
completeFit();
|
||||||
|
}
|
||||||
|
void Network::completeFit()
|
||||||
|
{
|
||||||
|
|
||||||
int maxThreadsRunning = static_cast<int>(std::thread::hardware_concurrency() * maxThreads);
|
int maxThreadsRunning = static_cast<int>(std::thread::hardware_concurrency() * maxThreads);
|
||||||
if (maxThreadsRunning < 1) {
|
if (maxThreadsRunning < 1) {
|
||||||
maxThreadsRunning = 1;
|
maxThreadsRunning = 1;
|
||||||
@@ -117,15 +142,9 @@ namespace bayesnet {
|
|||||||
condition_variable cv;
|
condition_variable cv;
|
||||||
int activeThreads = 0;
|
int activeThreads = 0;
|
||||||
int nextNodeIndex = 0;
|
int nextNodeIndex = 0;
|
||||||
|
|
||||||
while (nextNodeIndex < nodes.size()) {
|
while (nextNodeIndex < nodes.size()) {
|
||||||
unique_lock<mutex> lock(mtx);
|
unique_lock<mutex> lock(mtx);
|
||||||
cv.wait(lock, [&activeThreads, &maxThreadsRunning]() { return activeThreads < maxThreadsRunning; });
|
cv.wait(lock, [&activeThreads, &maxThreadsRunning]() { return activeThreads < maxThreadsRunning; });
|
||||||
|
|
||||||
if (nextNodeIndex >= nodes.size()) {
|
|
||||||
break; // No more work remaining
|
|
||||||
}
|
|
||||||
|
|
||||||
threads.emplace_back([this, &nextNodeIndex, &mtx, &cv, &activeThreads]() {
|
threads.emplace_back([this, &nextNodeIndex, &mtx, &cv, &activeThreads]() {
|
||||||
while (true) {
|
while (true) {
|
||||||
unique_lock<mutex> lock(mtx);
|
unique_lock<mutex> lock(mtx);
|
||||||
@@ -135,7 +154,6 @@ namespace bayesnet {
|
|||||||
auto& pair = *std::next(nodes.begin(), nextNodeIndex);
|
auto& pair = *std::next(nodes.begin(), nextNodeIndex);
|
||||||
++nextNodeIndex;
|
++nextNodeIndex;
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
|
|
||||||
pair.second->computeCPT(dataset, laplaceSmoothing);
|
pair.second->computeCPT(dataset, laplaceSmoothing);
|
||||||
lock.lock();
|
lock.lock();
|
||||||
nodes[pair.first] = std::move(pair.second);
|
nodes[pair.first] = std::move(pair.second);
|
||||||
@@ -145,7 +163,6 @@ namespace bayesnet {
|
|||||||
--activeThreads;
|
--activeThreads;
|
||||||
cv.notify_one();
|
cv.notify_one();
|
||||||
});
|
});
|
||||||
|
|
||||||
++activeThreads;
|
++activeThreads;
|
||||||
}
|
}
|
||||||
for (auto& thread : threads) {
|
for (auto& thread : threads) {
|
||||||
@@ -213,7 +230,6 @@ namespace bayesnet {
|
|||||||
evidence[features[i]] = sample[i];
|
evidence[features[i]] = sample[i];
|
||||||
}
|
}
|
||||||
return exactInference(evidence);
|
return exactInference(evidence);
|
||||||
|
|
||||||
}
|
}
|
||||||
double Network::computeFactor(map<string, int>& completeEvidence)
|
double Network::computeFactor(map<string, int>& completeEvidence)
|
||||||
{
|
{
|
||||||
@@ -243,9 +259,7 @@ namespace bayesnet {
|
|||||||
|
|
||||||
// Normalize result
|
// Normalize result
|
||||||
double sum = accumulate(result.begin(), result.end(), 0.0);
|
double sum = accumulate(result.begin(), result.end(), 0.0);
|
||||||
for (double& value : result) {
|
transform(result.begin(), result.end(), result.begin(), [sum](double& value) { return value / sum; });
|
||||||
value /= sum;
|
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
vector<string> Network::show()
|
vector<string> Network::show()
|
||||||
@@ -261,7 +275,7 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
vector<string> Network::graph(string title)
|
vector<string> Network::graph(const string& title)
|
||||||
{
|
{
|
||||||
auto output = vector<string>();
|
auto output = vector<string>();
|
||||||
auto prefix = "digraph BayesNet {\nlabel=<BayesNet ";
|
auto prefix = "digraph BayesNet {\nlabel=<BayesNet ";
|
||||||
@@ -287,5 +301,4 @@ namespace bayesnet {
|
|||||||
}
|
}
|
||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
class Network {
|
class Network {
|
||||||
private:
|
private:
|
||||||
map<string, std::unique_ptr<Node>> nodes;
|
map<string, unique_ptr<Node>> nodes;
|
||||||
map<string, vector<int>> dataset;
|
map<string, vector<int>> dataset;
|
||||||
bool fitted;
|
bool fitted;
|
||||||
float maxThreads;
|
float maxThreads;
|
||||||
@@ -24,15 +24,16 @@ namespace bayesnet {
|
|||||||
double entropy(torch::Tensor&);
|
double entropy(torch::Tensor&);
|
||||||
double conditionalEntropy(torch::Tensor&, torch::Tensor&);
|
double conditionalEntropy(torch::Tensor&, torch::Tensor&);
|
||||||
double mutualInformation(torch::Tensor&, torch::Tensor&);
|
double mutualInformation(torch::Tensor&, torch::Tensor&);
|
||||||
|
void completeFit();
|
||||||
public:
|
public:
|
||||||
Network();
|
Network();
|
||||||
Network(float, int);
|
explicit Network(float, int);
|
||||||
Network(float);
|
explicit Network(float);
|
||||||
Network(Network&);
|
explicit Network(Network&);
|
||||||
torch::Tensor& getSamples();
|
torch::Tensor& getSamples();
|
||||||
float getmaxThreads();
|
float getmaxThreads();
|
||||||
void addNode(string, int);
|
void addNode(const string&, int);
|
||||||
void addEdge(const string, const string);
|
void addEdge(const string&, const string&);
|
||||||
map<string, std::unique_ptr<Node>>& getNodes();
|
map<string, std::unique_ptr<Node>>& getNodes();
|
||||||
vector<string> getFeatures();
|
vector<string> getFeatures();
|
||||||
int getStates();
|
int getStates();
|
||||||
@@ -40,13 +41,14 @@ namespace bayesnet {
|
|||||||
int getClassNumStates();
|
int getClassNumStates();
|
||||||
string getClassName();
|
string getClassName();
|
||||||
void fit(const vector<vector<int>>&, const vector<int>&, const vector<string>&, const string&);
|
void fit(const vector<vector<int>>&, const vector<int>&, const vector<string>&, const string&);
|
||||||
|
void fit(torch::Tensor&, torch::Tensor&, const vector<string>&, const string&);
|
||||||
vector<int> predict(const vector<vector<int>>&);
|
vector<int> predict(const vector<vector<int>>&);
|
||||||
//Computes the conditional edge weight of variable index u and v conditioned on class_node
|
//Computes the conditional edge weight of variable index u and v conditioned on class_node
|
||||||
torch::Tensor conditionalEdgeWeight();
|
torch::Tensor conditionalEdgeWeight();
|
||||||
vector<vector<double>> predict_proba(const vector<vector<int>>&);
|
vector<vector<double>> predict_proba(const vector<vector<int>>&);
|
||||||
double score(const vector<vector<int>>&, const vector<int>&);
|
double score(const vector<vector<int>>&, const vector<int>&);
|
||||||
vector<string> show();
|
vector<string> show();
|
||||||
vector<string> graph(string title); // Returns a vector of strings representing the graph in graphviz format
|
vector<string> graph(const string& title); // Returns a vector of strings representing the graph in graphviz format
|
||||||
inline string version() { return "0.1.0"; }
|
inline string version() { return "0.1.0"; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,14 @@ namespace bayesnet {
|
|||||||
: name(name), numStates(numStates), cpTable(torch::Tensor()), parents(vector<Node*>()), children(vector<Node*>())
|
: name(name), numStates(numStates), cpTable(torch::Tensor()), parents(vector<Node*>()), children(vector<Node*>())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
void Node::clear()
|
||||||
|
{
|
||||||
|
parents.clear();
|
||||||
|
children.clear();
|
||||||
|
cpTable = torch::Tensor();
|
||||||
|
dimensions.clear();
|
||||||
|
numStates = 0;
|
||||||
|
}
|
||||||
string Node::getName() const
|
string Node::getName() const
|
||||||
{
|
{
|
||||||
return name;
|
return name;
|
||||||
@@ -80,19 +88,15 @@ namespace bayesnet {
|
|||||||
{
|
{
|
||||||
// Get dimensions of the CPT
|
// Get dimensions of the CPT
|
||||||
dimensions.push_back(numStates);
|
dimensions.push_back(numStates);
|
||||||
for (auto father : getParents()) {
|
transform(parents.begin(), parents.end(), back_inserter(dimensions), [](const auto& parent) { return parent->getNumStates(); });
|
||||||
dimensions.push_back(father->getNumStates());
|
|
||||||
}
|
|
||||||
auto length = dimensions.size();
|
|
||||||
// Create a tensor of zeros with the dimensions of the CPT
|
// Create a tensor of zeros with the dimensions of the CPT
|
||||||
cpTable = torch::zeros(dimensions, torch::kFloat) + laplaceSmoothing;
|
cpTable = torch::zeros(dimensions, torch::kFloat) + laplaceSmoothing;
|
||||||
// Fill table with counts
|
// Fill table with counts
|
||||||
for (int n_sample = 0; n_sample < dataset[name].size(); ++n_sample) {
|
for (int n_sample = 0; n_sample < dataset[name].size(); ++n_sample) {
|
||||||
torch::List<c10::optional<torch::Tensor>> coordinates;
|
torch::List<c10::optional<torch::Tensor>> coordinates;
|
||||||
coordinates.push_back(torch::tensor(dataset[name][n_sample]));
|
coordinates.push_back(torch::tensor(dataset[name][n_sample]));
|
||||||
for (auto father : getParents()) {
|
transform(parents.begin(), parents.end(), back_inserter(coordinates), [&dataset, &n_sample](const auto& parent) { return torch::tensor(dataset[parent->getName()][n_sample]); });
|
||||||
coordinates.push_back(torch::tensor(dataset[father->getName()][n_sample]));
|
|
||||||
}
|
|
||||||
// Increment the count of the corresponding coordinate
|
// Increment the count of the corresponding coordinate
|
||||||
cpTable.index_put_({ coordinates }, cpTable.index({ coordinates }) + 1);
|
cpTable.index_put_({ coordinates }, cpTable.index({ coordinates }) + 1);
|
||||||
}
|
}
|
||||||
@@ -104,19 +108,15 @@ namespace bayesnet {
|
|||||||
torch::List<c10::optional<torch::Tensor>> coordinates;
|
torch::List<c10::optional<torch::Tensor>> coordinates;
|
||||||
// following predetermined order of indices in the cpTable (see Node.h)
|
// following predetermined order of indices in the cpTable (see Node.h)
|
||||||
coordinates.push_back(torch::tensor(evidence[name]));
|
coordinates.push_back(torch::tensor(evidence[name]));
|
||||||
for (auto parent : getParents()) {
|
transform(parents.begin(), parents.end(), back_inserter(coordinates), [&evidence](const auto& parent) { return torch::tensor(evidence[parent->getName()]); });
|
||||||
coordinates.push_back(torch::tensor(evidence[parent->getName()]));
|
|
||||||
}
|
|
||||||
return cpTable.index({ coordinates }).item<float>();
|
return cpTable.index({ coordinates }).item<float>();
|
||||||
}
|
}
|
||||||
vector<string> Node::graph(string className)
|
vector<string> Node::graph(const string& className)
|
||||||
{
|
{
|
||||||
auto output = vector<string>();
|
auto output = vector<string>();
|
||||||
auto suffix = name == className ? ", fontcolor=red, fillcolor=lightblue, style=filled " : "";
|
auto suffix = name == className ? ", fontcolor=red, fillcolor=lightblue, style=filled " : "";
|
||||||
output.push_back(name + " [shape=circle" + suffix + "] \n");
|
output.push_back(name + " [shape=circle" + suffix + "] \n");
|
||||||
for (auto& child : children) {
|
transform(children.begin(), children.end(), back_inserter(output), [this](const auto& child) { return name + " -> " + child->getName(); });
|
||||||
output.push_back(name + " -> " + child->getName());
|
|
||||||
}
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -16,7 +16,8 @@ namespace bayesnet {
|
|||||||
vector<int64_t> dimensions; // dimensions of the cpTable
|
vector<int64_t> dimensions; // dimensions of the cpTable
|
||||||
public:
|
public:
|
||||||
vector<pair<string, string>> combinations(const vector<string>&);
|
vector<pair<string, string>> combinations(const vector<string>&);
|
||||||
Node(const std::string&, int);
|
Node(const string&, int);
|
||||||
|
void clear();
|
||||||
void addParent(Node*);
|
void addParent(Node*);
|
||||||
void addChild(Node*);
|
void addChild(Node*);
|
||||||
void removeParent(Node*);
|
void removeParent(Node*);
|
||||||
@@ -29,7 +30,7 @@ namespace bayesnet {
|
|||||||
int getNumStates() const;
|
int getNumStates() const;
|
||||||
void setNumStates(int);
|
void setNumStates(int);
|
||||||
unsigned minFill();
|
unsigned minFill();
|
||||||
vector<string> graph(string clasName); // Returns a vector of strings representing the graph in graphviz format
|
vector<string> graph(const string& clasName); // Returns a vector of strings representing the graph in graphviz format
|
||||||
float getFactorValue(map<string, int>&);
|
float getFactorValue(map<string, int>&);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
|
|
||||||
SPODE::SPODE(int root) : BaseClassifier(Network()), root(root) {}
|
SPODE::SPODE(int root) : Classifier(Network()), root(root) {}
|
||||||
|
|
||||||
void SPODE::train()
|
void SPODE::train()
|
||||||
{
|
{
|
||||||
|
@@ -1,14 +1,16 @@
|
|||||||
#ifndef SPODE_H
|
#ifndef SPODE_H
|
||||||
#define SPODE_H
|
#define SPODE_H
|
||||||
#include "BaseClassifier.h"
|
#include "Classifier.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
class SPODE : public BaseClassifier {
|
class SPODE : public Classifier {
|
||||||
private:
|
private:
|
||||||
int root;
|
int root;
|
||||||
protected:
|
protected:
|
||||||
void train() override;
|
void train() override;
|
||||||
public:
|
public:
|
||||||
SPODE(int root);
|
explicit SPODE(int root);
|
||||||
|
virtual ~SPODE() {};
|
||||||
vector<string> graph(string name = "SPODE") override;
|
vector<string> graph(string name = "SPODE") override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,9 @@
|
|||||||
#include "TAN.h"
|
#include "TAN.h"
|
||||||
|
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
|
|
||||||
TAN::TAN() : BaseClassifier(Network()) {}
|
TAN::TAN() : Classifier(Network()) {}
|
||||||
|
|
||||||
void TAN::train()
|
void TAN::train()
|
||||||
{
|
{
|
||||||
@@ -19,7 +18,7 @@ namespace bayesnet {
|
|||||||
auto mi_value = metrics.mutualInformation(class_dataset, feature_dataset);
|
auto mi_value = metrics.mutualInformation(class_dataset, feature_dataset);
|
||||||
mi.push_back({ i, mi_value });
|
mi.push_back({ i, mi_value });
|
||||||
}
|
}
|
||||||
sort(mi.begin(), mi.end(), [](auto& left, auto& right) {return left.second < right.second;});
|
sort(mi.begin(), mi.end(), [](const auto& left, const auto& right) {return left.second < right.second;});
|
||||||
auto root = mi[mi.size() - 1].first;
|
auto root = mi[mi.size() - 1].first;
|
||||||
// 2. Compute mutual information between each feature and the class
|
// 2. Compute mutual information between each feature and the class
|
||||||
auto weights = metrics.conditionalEdge();
|
auto weights = metrics.conditionalEdge();
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
#ifndef TAN_H
|
#ifndef TAN_H
|
||||||
#define TAN_H
|
#define TAN_H
|
||||||
#include "BaseClassifier.h"
|
#include "Classifier.h"
|
||||||
namespace bayesnet {
|
namespace bayesnet {
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
class TAN : public BaseClassifier {
|
class TAN : public Classifier {
|
||||||
private:
|
private:
|
||||||
protected:
|
protected:
|
||||||
void train() override;
|
void train() override;
|
||||||
public:
|
public:
|
||||||
TAN();
|
TAN();
|
||||||
|
virtual ~TAN() {};
|
||||||
vector<string> graph(string name = "TAN") override;
|
vector<string> graph(string name = "TAN") override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -11,21 +11,16 @@ namespace bayesnet {
|
|||||||
sort(indices.begin(), indices.end(), [&nums](int i, int j) {return nums[i] > nums[j];});
|
sort(indices.begin(), indices.end(), [&nums](int i, int j) {return nums[i] > nums[j];});
|
||||||
return indices;
|
return indices;
|
||||||
}
|
}
|
||||||
vector<vector<int>> tensorToVector(const Tensor& tensor)
|
vector<vector<int>> tensorToVector(Tensor& tensor)
|
||||||
{
|
{
|
||||||
// convert mxn tensor to nxm vector
|
// convert mxn tensor to nxm vector
|
||||||
vector<vector<int>> result;
|
vector<vector<int>> result;
|
||||||
auto tensor_accessor = tensor.accessor<int, 2>();
|
// Iterate over cols
|
||||||
|
for (int i = 0; i < tensor.size(1); ++i) {
|
||||||
// Iterate over columns and rows of the tensor
|
auto col_tensor = tensor.index({ "...", i });
|
||||||
for (int j = 0; j < tensor.size(1); ++j) {
|
auto col = vector<int>(col_tensor.data_ptr<int>(), col_tensor.data_ptr<int>() + tensor.size(0));
|
||||||
vector<int> column;
|
result.push_back(col);
|
||||||
for (int i = 0; i < tensor.size(0); ++i) {
|
|
||||||
column.push_back(tensor_accessor[i][j]);
|
|
||||||
}
|
}
|
||||||
result.push_back(column);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -6,6 +6,6 @@ namespace bayesnet {
|
|||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace torch;
|
using namespace torch;
|
||||||
vector<int> argsort(vector<float>& nums);
|
vector<int> argsort(vector<float>& nums);
|
||||||
vector<vector<int>> tensorToVector(const Tensor& tensor);
|
vector<vector<int>> tensorToVector(Tensor& tensor);
|
||||||
}
|
}
|
||||||
#endif //BAYESNET_UTILS_H
|
#endif //BAYESNET_UTILS_H
|
@@ -1,4 +1,8 @@
|
|||||||
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
||||||
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
||||||
add_executable(main Experiment.cc ArffFiles.cc CPPFImdlp.cpp Metrics.cpp platformUtils.cc)
|
include_directories(${BayesNet_SOURCE_DIR}/lib/Files)
|
||||||
target_link_libraries(main BayesNet "${TORCH_LIBRARIES}")
|
include_directories(${BayesNet_SOURCE_DIR}/lib/mdlp)
|
||||||
|
include_directories(${BayesNet_SOURCE_DIR}/lib/argparse/include)
|
||||||
|
include_directories(${BayesNet_SOURCE_DIR}/lib/json/include)
|
||||||
|
add_executable(main main.cc Folding.cc platformUtils.cc Experiment.cc Datasets.cc Models.cc)
|
||||||
|
target_link_libraries(main BayesNet ArffFiles mdlp "${TORCH_LIBRARIES}")
|
@@ -1,221 +0,0 @@
|
|||||||
#include <numeric>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <set>
|
|
||||||
#include <cmath>
|
|
||||||
#include "CPPFImdlp.h"
|
|
||||||
#include "Metrics.h"
|
|
||||||
|
|
||||||
namespace mdlp {
|
|
||||||
|
|
||||||
CPPFImdlp::CPPFImdlp(size_t min_length_, int max_depth_, float proposed) : min_length(min_length_),
|
|
||||||
max_depth(max_depth_),
|
|
||||||
proposed_cuts(proposed)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
CPPFImdlp::CPPFImdlp() = default;
|
|
||||||
|
|
||||||
CPPFImdlp::~CPPFImdlp() = default;
|
|
||||||
|
|
||||||
size_t CPPFImdlp::compute_max_num_cut_points() const
|
|
||||||
{
|
|
||||||
// Set the actual maximum number of cut points as a number or as a percentage of the number of samples
|
|
||||||
if (proposed_cuts == 0) {
|
|
||||||
return numeric_limits<size_t>::max();
|
|
||||||
}
|
|
||||||
if (proposed_cuts < 0 || proposed_cuts > static_cast<float>(X.size())) {
|
|
||||||
throw invalid_argument("wrong proposed num_cuts value");
|
|
||||||
}
|
|
||||||
if (proposed_cuts < 1)
|
|
||||||
return static_cast<size_t>(round(static_cast<float>(X.size()) * proposed_cuts));
|
|
||||||
return static_cast<size_t>(proposed_cuts);
|
|
||||||
}
|
|
||||||
|
|
||||||
void CPPFImdlp::fit(samples_t& X_, labels_t& y_)
|
|
||||||
{
|
|
||||||
X = X_;
|
|
||||||
y = y_;
|
|
||||||
num_cut_points = compute_max_num_cut_points();
|
|
||||||
depth = 0;
|
|
||||||
discretizedData.clear();
|
|
||||||
cutPoints.clear();
|
|
||||||
if (X.size() != y.size()) {
|
|
||||||
throw invalid_argument("X and y must have the same size");
|
|
||||||
}
|
|
||||||
if (X.empty() || y.empty()) {
|
|
||||||
throw invalid_argument("X and y must have at least one element");
|
|
||||||
}
|
|
||||||
if (min_length < 3) {
|
|
||||||
throw invalid_argument("min_length must be greater than 2");
|
|
||||||
}
|
|
||||||
if (max_depth < 1) {
|
|
||||||
throw invalid_argument("max_depth must be greater than 0");
|
|
||||||
}
|
|
||||||
indices = sortIndices(X_, y_);
|
|
||||||
metrics.setData(y, indices);
|
|
||||||
computeCutPoints(0, X.size(), 1);
|
|
||||||
sort(cutPoints.begin(), cutPoints.end());
|
|
||||||
if (num_cut_points > 0) {
|
|
||||||
// Select the best (with lower entropy) cut points
|
|
||||||
while (cutPoints.size() > num_cut_points) {
|
|
||||||
resizeCutPoints();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pair<precision_t, size_t> CPPFImdlp::valueCutPoint(size_t start, size_t cut, size_t end)
|
|
||||||
{
|
|
||||||
size_t n;
|
|
||||||
size_t m;
|
|
||||||
size_t idxPrev = cut - 1 >= start ? cut - 1 : cut;
|
|
||||||
size_t idxNext = cut + 1 < end ? cut + 1 : cut;
|
|
||||||
bool backWall; // true if duplicates reach beginning of the interval
|
|
||||||
precision_t previous;
|
|
||||||
precision_t actual;
|
|
||||||
precision_t next;
|
|
||||||
previous = X[indices[idxPrev]];
|
|
||||||
actual = X[indices[cut]];
|
|
||||||
next = X[indices[idxNext]];
|
|
||||||
// definition 2 of the paper => X[t-1] < X[t]
|
|
||||||
// get the first equal value of X in the interval
|
|
||||||
while (idxPrev > start && actual == previous) {
|
|
||||||
previous = X[indices[--idxPrev]];
|
|
||||||
}
|
|
||||||
backWall = idxPrev == start && actual == previous;
|
|
||||||
// get the last equal value of X in the interval
|
|
||||||
while (idxNext < end - 1 && actual == next) {
|
|
||||||
next = X[indices[++idxNext]];
|
|
||||||
}
|
|
||||||
// # of duplicates before cutpoint
|
|
||||||
n = cut - 1 - idxPrev;
|
|
||||||
// # of duplicates after cutpoint
|
|
||||||
m = idxNext - cut - 1;
|
|
||||||
// Decide which values to use
|
|
||||||
cut = cut + (backWall ? m + 1 : -n);
|
|
||||||
actual = X[indices[cut]];
|
|
||||||
return { (actual + previous) / 2, cut };
|
|
||||||
}
|
|
||||||
|
|
||||||
void CPPFImdlp::computeCutPoints(size_t start, size_t end, int depth_)
|
|
||||||
{
|
|
||||||
size_t cut;
|
|
||||||
pair<precision_t, size_t> result;
|
|
||||||
// Check if the interval length and the depth are Ok
|
|
||||||
if (end - start < min_length || depth_ > max_depth)
|
|
||||||
return;
|
|
||||||
depth = depth_ > depth ? depth_ : depth;
|
|
||||||
cut = getCandidate(start, end);
|
|
||||||
if (cut == numeric_limits<size_t>::max())
|
|
||||||
return;
|
|
||||||
if (mdlp(start, cut, end)) {
|
|
||||||
result = valueCutPoint(start, cut, end);
|
|
||||||
cut = result.second;
|
|
||||||
cutPoints.push_back(result.first);
|
|
||||||
computeCutPoints(start, cut, depth_ + 1);
|
|
||||||
computeCutPoints(cut, end, depth_ + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t CPPFImdlp::getCandidate(size_t start, size_t end)
|
|
||||||
{
|
|
||||||
/* Definition 1: A binary discretization for A is determined by selecting the cut point TA for which
|
|
||||||
E(A, TA; S) is minimal amongst all the candidate cut points. */
|
|
||||||
size_t candidate = numeric_limits<size_t>::max();
|
|
||||||
size_t elements = end - start;
|
|
||||||
bool sameValues = true;
|
|
||||||
precision_t entropy_left;
|
|
||||||
precision_t entropy_right;
|
|
||||||
precision_t minEntropy;
|
|
||||||
// Check if all the values of the variable in the interval are the same
|
|
||||||
for (size_t idx = start + 1; idx < end; idx++) {
|
|
||||||
if (X[indices[idx]] != X[indices[start]]) {
|
|
||||||
sameValues = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sameValues)
|
|
||||||
return candidate;
|
|
||||||
minEntropy = metrics.entropy(start, end);
|
|
||||||
for (size_t idx = start + 1; idx < end; idx++) {
|
|
||||||
// Cutpoints are always on boundaries (definition 2)
|
|
||||||
if (y[indices[idx]] == y[indices[idx - 1]])
|
|
||||||
continue;
|
|
||||||
entropy_left = precision_t(idx - start) / static_cast<precision_t>(elements) * metrics.entropy(start, idx);
|
|
||||||
entropy_right = precision_t(end - idx) / static_cast<precision_t>(elements) * metrics.entropy(idx, end);
|
|
||||||
if (entropy_left + entropy_right < minEntropy) {
|
|
||||||
minEntropy = entropy_left + entropy_right;
|
|
||||||
candidate = idx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool CPPFImdlp::mdlp(size_t start, size_t cut, size_t end)
|
|
||||||
{
|
|
||||||
int k;
|
|
||||||
int k1;
|
|
||||||
int k2;
|
|
||||||
precision_t ig;
|
|
||||||
precision_t delta;
|
|
||||||
precision_t ent;
|
|
||||||
precision_t ent1;
|
|
||||||
precision_t ent2;
|
|
||||||
auto N = precision_t(end - start);
|
|
||||||
k = metrics.computeNumClasses(start, end);
|
|
||||||
k1 = metrics.computeNumClasses(start, cut);
|
|
||||||
k2 = metrics.computeNumClasses(cut, end);
|
|
||||||
ent = metrics.entropy(start, end);
|
|
||||||
ent1 = metrics.entropy(start, cut);
|
|
||||||
ent2 = metrics.entropy(cut, end);
|
|
||||||
ig = metrics.informationGain(start, cut, end);
|
|
||||||
delta = static_cast<precision_t>(log2(pow(3, precision_t(k)) - 2) -
|
|
||||||
(precision_t(k) * ent - precision_t(k1) * ent1 - precision_t(k2) * ent2));
|
|
||||||
precision_t term = 1 / N * (log2(N - 1) + delta);
|
|
||||||
return ig > term;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Argsort from https://stackoverflow.com/questions/1577475/c-sorting-and-keeping-track-of-indexes
|
|
||||||
indices_t CPPFImdlp::sortIndices(samples_t& X_, labels_t& y_)
|
|
||||||
{
|
|
||||||
indices_t idx(X_.size());
|
|
||||||
iota(idx.begin(), idx.end(), 0);
|
|
||||||
stable_sort(idx.begin(), idx.end(), [&X_, &y_](size_t i1, size_t i2) {
|
|
||||||
if (X_[i1] == X_[i2])
|
|
||||||
return y_[i1] < y_[i2];
|
|
||||||
else
|
|
||||||
return X_[i1] < X_[i2];
|
|
||||||
});
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
void CPPFImdlp::resizeCutPoints()
|
|
||||||
{
|
|
||||||
//Compute entropy of each of the whole cutpoint set and discards the biggest value
|
|
||||||
precision_t maxEntropy = 0;
|
|
||||||
precision_t entropy;
|
|
||||||
size_t maxEntropyIdx = 0;
|
|
||||||
size_t begin = 0;
|
|
||||||
size_t end;
|
|
||||||
for (size_t idx = 0; idx < cutPoints.size(); idx++) {
|
|
||||||
end = begin;
|
|
||||||
while (X[indices[end]] < cutPoints[idx] && end < X.size())
|
|
||||||
end++;
|
|
||||||
entropy = metrics.entropy(begin, end);
|
|
||||||
if (entropy > maxEntropy) {
|
|
||||||
maxEntropy = entropy;
|
|
||||||
maxEntropyIdx = idx;
|
|
||||||
}
|
|
||||||
begin = end;
|
|
||||||
}
|
|
||||||
cutPoints.erase(cutPoints.begin() + static_cast<long>(maxEntropyIdx));
|
|
||||||
}
|
|
||||||
labels_t& CPPFImdlp::transform(const samples_t& data)
|
|
||||||
{
|
|
||||||
discretizedData.reserve(data.size());
|
|
||||||
for (const precision_t& item : data) {
|
|
||||||
auto upper = upper_bound(cutPoints.begin(), cutPoints.end(), item);
|
|
||||||
discretizedData.push_back(upper - cutPoints.begin());
|
|
||||||
}
|
|
||||||
return discretizedData;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,45 +0,0 @@
|
|||||||
#ifndef CPPFIMDLP_H
|
|
||||||
#define CPPFIMDLP_H
|
|
||||||
|
|
||||||
#include "typesFImdlp.h"
|
|
||||||
#include "Metrics.h"
|
|
||||||
#include <limits>
|
|
||||||
#include <utility>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
namespace mdlp {
|
|
||||||
class CPPFImdlp {
|
|
||||||
protected:
|
|
||||||
size_t min_length = 3;
|
|
||||||
int depth = 0;
|
|
||||||
int max_depth = numeric_limits<int>::max();
|
|
||||||
float proposed_cuts = 0;
|
|
||||||
indices_t indices = indices_t();
|
|
||||||
samples_t X = samples_t();
|
|
||||||
labels_t y = labels_t();
|
|
||||||
Metrics metrics = Metrics(y, indices);
|
|
||||||
cutPoints_t cutPoints;
|
|
||||||
size_t num_cut_points = numeric_limits<size_t>::max();
|
|
||||||
labels_t discretizedData = labels_t();
|
|
||||||
|
|
||||||
static indices_t sortIndices(samples_t&, labels_t&);
|
|
||||||
|
|
||||||
void computeCutPoints(size_t, size_t, int);
|
|
||||||
void resizeCutPoints();
|
|
||||||
bool mdlp(size_t, size_t, size_t);
|
|
||||||
size_t getCandidate(size_t, size_t);
|
|
||||||
size_t compute_max_num_cut_points() const;
|
|
||||||
pair<precision_t, size_t> valueCutPoint(size_t, size_t, size_t);
|
|
||||||
|
|
||||||
public:
|
|
||||||
CPPFImdlp();
|
|
||||||
CPPFImdlp(size_t, int, float);
|
|
||||||
~CPPFImdlp();
|
|
||||||
void fit(samples_t&, labels_t&);
|
|
||||||
inline cutPoints_t getCutPoints() const { return cutPoints; };
|
|
||||||
labels_t& transform(const samples_t&);
|
|
||||||
inline int get_depth() const { return depth; };
|
|
||||||
static inline string version() { return "1.1.2"; };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
#endif
|
|
231
src/Platform/Datasets.cc
Normal file
231
src/Platform/Datasets.cc
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
#include "Datasets.h"
|
||||||
|
#include "platformUtils.h"
|
||||||
|
#include "ArffFiles.h"
|
||||||
|
namespace platform {
|
||||||
|
void Datasets::load()
|
||||||
|
{
|
||||||
|
ifstream catalog(path + "/all.txt");
|
||||||
|
if (catalog.is_open()) {
|
||||||
|
string line;
|
||||||
|
while (getline(catalog, line)) {
|
||||||
|
vector<string> tokens = split(line, ',');
|
||||||
|
string name = tokens[0];
|
||||||
|
string className = tokens[1];
|
||||||
|
datasets[name] = make_unique<Dataset>(path, name, className, discretize, fileType);
|
||||||
|
}
|
||||||
|
catalog.close();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Unable to open catalog file. [" + path + "/all.txt" + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vector<string> Datasets::getNames()
|
||||||
|
{
|
||||||
|
vector<string> result;
|
||||||
|
transform(datasets.begin(), datasets.end(), back_inserter(result), [](const auto& d) { return d.first; });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
vector<string> Datasets::getFeatures(string name)
|
||||||
|
{
|
||||||
|
if (datasets[name]->isLoaded()) {
|
||||||
|
return datasets[name]->getFeatures();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map<string, vector<int>> Datasets::getStates(string name)
|
||||||
|
{
|
||||||
|
if (datasets[name]->isLoaded()) {
|
||||||
|
return datasets[name]->getStates();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string Datasets::getClassName(string name)
|
||||||
|
{
|
||||||
|
if (datasets[name]->isLoaded()) {
|
||||||
|
return datasets[name]->getClassName();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int Datasets::getNSamples(string name)
|
||||||
|
{
|
||||||
|
if (datasets[name]->isLoaded()) {
|
||||||
|
return datasets[name]->getNSamples();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pair<vector<vector<float>>&, vector<int>&> Datasets::getVectors(string name)
|
||||||
|
{
|
||||||
|
if (!datasets[name]->isLoaded()) {
|
||||||
|
datasets[name]->load();
|
||||||
|
}
|
||||||
|
return datasets[name]->getVectors();
|
||||||
|
}
|
||||||
|
pair<vector<vector<int>>&, vector<int>&> Datasets::getVectorsDiscretized(string name)
|
||||||
|
{
|
||||||
|
if (!datasets[name]->isLoaded()) {
|
||||||
|
datasets[name]->load();
|
||||||
|
}
|
||||||
|
return datasets[name]->getVectorsDiscretized();
|
||||||
|
}
|
||||||
|
pair<torch::Tensor&, torch::Tensor&> Datasets::getTensors(string name)
|
||||||
|
{
|
||||||
|
if (!datasets[name]->isLoaded()) {
|
||||||
|
datasets[name]->load();
|
||||||
|
}
|
||||||
|
return datasets[name]->getTensors();
|
||||||
|
}
|
||||||
|
bool Datasets::isDataset(const string& name)
|
||||||
|
{
|
||||||
|
return datasets.find(name) != datasets.end();
|
||||||
|
}
|
||||||
|
Dataset::Dataset(const Dataset& dataset) : path(dataset.path), name(dataset.name), className(dataset.className), n_samples(dataset.n_samples), n_features(dataset.n_features), features(dataset.features), states(dataset.states), loaded(dataset.loaded), discretize(dataset.discretize), X(dataset.X), y(dataset.y), Xv(dataset.Xv), Xd(dataset.Xd), yv(dataset.yv), fileType(dataset.fileType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
string Dataset::getName()
|
||||||
|
{
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
string Dataset::getClassName()
|
||||||
|
{
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
vector<string> Dataset::getFeatures()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return features;
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int Dataset::getNFeatures()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return n_features;
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int Dataset::getNSamples()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return n_samples;
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map<string, vector<int>> Dataset::getStates()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return states;
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pair<vector<vector<float>>&, vector<int>&> Dataset::getVectors()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return { Xv, yv };
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pair<vector<vector<int>>&, vector<int>&> Dataset::getVectorsDiscretized()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return { Xd, yv };
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pair<torch::Tensor&, torch::Tensor&> Dataset::getTensors()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
buildTensors();
|
||||||
|
return { X, y };
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Dataset not loaded.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Dataset::load_csv()
|
||||||
|
{
|
||||||
|
ifstream file(path + "/" + name + ".csv");
|
||||||
|
if (file.is_open()) {
|
||||||
|
string line;
|
||||||
|
getline(file, line);
|
||||||
|
vector<string> tokens = split(line, ',');
|
||||||
|
features = vector<string>(tokens.begin(), tokens.end() - 1);
|
||||||
|
className = tokens.back();
|
||||||
|
for (auto i = 0; i < features.size(); ++i) {
|
||||||
|
Xv.push_back(vector<float>());
|
||||||
|
}
|
||||||
|
while (getline(file, line)) {
|
||||||
|
tokens = split(line, ',');
|
||||||
|
for (auto i = 0; i < features.size(); ++i) {
|
||||||
|
Xv[i].push_back(stof(tokens[i]));
|
||||||
|
}
|
||||||
|
yv.push_back(stoi(tokens.back()));
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
} else {
|
||||||
|
throw invalid_argument("Unable to open dataset file.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Dataset::computeStates()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < features.size(); ++i) {
|
||||||
|
states[features[i]] = vector<int>(*max_element(Xd[i].begin(), Xd[i].end()) + 1);
|
||||||
|
iota(begin(states[features[i]]), end(states[features[i]]), 0);
|
||||||
|
}
|
||||||
|
states[className] = vector<int>(*max_element(yv.begin(), yv.end()) + 1);
|
||||||
|
iota(begin(states[className]), end(states[className]), 0);
|
||||||
|
}
|
||||||
|
void Dataset::load_arff()
|
||||||
|
{
|
||||||
|
auto arff = ArffFiles();
|
||||||
|
arff.load(path + "/" + name + ".arff", className);
|
||||||
|
// Get Dataset X, y
|
||||||
|
Xv = arff.getX();
|
||||||
|
yv = arff.getY();
|
||||||
|
// Get className & Features
|
||||||
|
className = arff.getClassName();
|
||||||
|
auto attributes = arff.getAttributes();
|
||||||
|
transform(attributes.begin(), attributes.end(), back_inserter(features), [](const auto& attribute) { return attribute.first; });
|
||||||
|
}
|
||||||
|
void Dataset::load()
|
||||||
|
{
|
||||||
|
if (loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fileType == CSV) {
|
||||||
|
load_csv();
|
||||||
|
} else if (fileType == ARFF) {
|
||||||
|
load_arff();
|
||||||
|
}
|
||||||
|
if (discretize) {
|
||||||
|
Xd = discretizeDataset(Xv, yv);
|
||||||
|
computeStates();
|
||||||
|
n_samples = Xd[0].size();
|
||||||
|
n_features = Xd.size();
|
||||||
|
}
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
|
void Dataset::buildTensors()
|
||||||
|
{
|
||||||
|
if (discretize) {
|
||||||
|
X = torch::zeros({ static_cast<int>(n_features), static_cast<int>(n_samples) }, torch::kInt32);
|
||||||
|
} else {
|
||||||
|
X = torch::zeros({ static_cast<int>(n_features), static_cast<int>(n_samples) }, torch::kFloat32);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < features.size(); ++i) {
|
||||||
|
if (discretize) {
|
||||||
|
X.index_put_({ i, "..." }, torch::tensor(Xd[i], torch::kInt32));
|
||||||
|
} else {
|
||||||
|
X.index_put_({ i, "..." }, torch::tensor(Xv[i], torch::kFloat32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y = torch::tensor(yv, torch::kInt32);
|
||||||
|
}
|
||||||
|
}
|
65
src/Platform/Datasets.h
Normal file
65
src/Platform/Datasets.h
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#ifndef DATASETS_H
|
||||||
|
#define DATASETS_H
|
||||||
|
#include <torch/torch.h>
|
||||||
|
#include <map>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
namespace platform {
|
||||||
|
using namespace std;
|
||||||
|
enum fileType_t { CSV, ARFF };
|
||||||
|
class Dataset {
|
||||||
|
private:
|
||||||
|
string path;
|
||||||
|
string name;
|
||||||
|
fileType_t fileType;
|
||||||
|
string className;
|
||||||
|
int n_samples{ 0 }, n_features{ 0 };
|
||||||
|
vector<string> features;
|
||||||
|
map<string, vector<int>> states;
|
||||||
|
bool loaded;
|
||||||
|
bool discretize;
|
||||||
|
torch::Tensor X, y;
|
||||||
|
vector<vector<float>> Xv;
|
||||||
|
vector<vector<int>> Xd;
|
||||||
|
vector<int> yv;
|
||||||
|
void buildTensors();
|
||||||
|
void load_csv();
|
||||||
|
void load_arff();
|
||||||
|
void computeStates();
|
||||||
|
public:
|
||||||
|
Dataset(const string& path, const string& name, const string& className, bool discretize, fileType_t fileType) : path(path), name(name), className(className), discretize(discretize), loaded(false), fileType(fileType) {};
|
||||||
|
explicit Dataset(const Dataset&);
|
||||||
|
string getName();
|
||||||
|
string getClassName();
|
||||||
|
vector<string> getFeatures();
|
||||||
|
map<string, vector<int>> getStates();
|
||||||
|
pair<vector<vector<float>>&, vector<int>&> getVectors();
|
||||||
|
pair<vector<vector<int>>&, vector<int>&> getVectorsDiscretized();
|
||||||
|
pair<torch::Tensor&, torch::Tensor&> getTensors();
|
||||||
|
int getNFeatures();
|
||||||
|
int getNSamples();
|
||||||
|
void load();
|
||||||
|
const bool inline isLoaded() const { return loaded; };
|
||||||
|
};
|
||||||
|
class Datasets {
|
||||||
|
private:
|
||||||
|
string path;
|
||||||
|
fileType_t fileType;
|
||||||
|
map<string, unique_ptr<Dataset>> datasets;
|
||||||
|
bool discretize;
|
||||||
|
void load(); // Loads the list of datasets
|
||||||
|
public:
|
||||||
|
explicit Datasets(const string& path, bool discretize = false, fileType_t fileType = ARFF) : path(path), discretize(discretize), fileType(fileType) { load(); };
|
||||||
|
vector<string> getNames();
|
||||||
|
vector<string> getFeatures(string name);
|
||||||
|
int getNSamples(string name);
|
||||||
|
string getClassName(string name);
|
||||||
|
map<string, vector<int>> getStates(string name);
|
||||||
|
pair<vector<vector<float>>&, vector<int>&> getVectors(string name);
|
||||||
|
pair<vector<vector<int>>&, vector<int>&> getVectorsDiscretized(string name);
|
||||||
|
pair<torch::Tensor&, torch::Tensor&> getTensors(string name);
|
||||||
|
bool isDataset(const string& name);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
62
src/Platform/DotEnv.h
Normal file
62
src/Platform/DotEnv.h
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#ifndef DOTENV_H
|
||||||
|
#define DOTENV_H
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include "platformUtils.h"
|
||||||
|
namespace platform {
|
||||||
|
class DotEnv {
|
||||||
|
private:
|
||||||
|
std::map<std::string, std::string> env;
|
||||||
|
std::string trim(const std::string& str)
|
||||||
|
{
|
||||||
|
std::string result = str;
|
||||||
|
result.erase(result.begin(), std::find_if(result.begin(), result.end(), [](int ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
result.erase(std::find_if(result.rbegin(), result.rend(), [](int ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), result.end());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
DotEnv()
|
||||||
|
{
|
||||||
|
std::ifstream file(".env");
|
||||||
|
if (!file.is_open()) {
|
||||||
|
std::cerr << "File .env not found" << std::endl;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
line = trim(line);
|
||||||
|
if (line.empty() || line[0] == '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::istringstream iss(line);
|
||||||
|
std::string key, value;
|
||||||
|
if (std::getline(iss, key, '=') && std::getline(iss, value)) {
|
||||||
|
env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::string get(const std::string& key)
|
||||||
|
{
|
||||||
|
return env[key];
|
||||||
|
}
|
||||||
|
std::vector<int> getSeeds()
|
||||||
|
{
|
||||||
|
auto seeds = std::vector<int>();
|
||||||
|
auto seeds_str = env["seeds"];
|
||||||
|
seeds_str = trim(seeds_str);
|
||||||
|
seeds_str = seeds_str.substr(1, seeds_str.size() - 2);
|
||||||
|
auto seeds_str_split = split(seeds_str, ',');
|
||||||
|
transform(seeds_str_split.begin(), seeds_str_split.end(), back_inserter(seeds), [](const std::string& str) {
|
||||||
|
return stoi(str);
|
||||||
|
});
|
||||||
|
return seeds;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
@@ -1,201 +1,176 @@
|
|||||||
#include <iostream>
|
#include "Experiment.h"
|
||||||
#include <string>
|
#include "Datasets.h"
|
||||||
#include <torch/torch.h>
|
#include "Models.h"
|
||||||
#include <thread>
|
|
||||||
#include <getopt.h>
|
|
||||||
#include "ArffFiles.h"
|
|
||||||
#include "Network.h"
|
|
||||||
#include "Metrics.hpp"
|
|
||||||
#include "CPPFImdlp.h"
|
|
||||||
#include "KDB.h"
|
|
||||||
#include "SPODE.h"
|
|
||||||
#include "AODE.h"
|
|
||||||
#include "TAN.h"
|
|
||||||
#include "platformUtils.h"
|
|
||||||
|
|
||||||
|
namespace platform {
|
||||||
using namespace std;
|
using json = nlohmann::json;
|
||||||
|
string get_date()
|
||||||
/* print a description of all supported options */
|
|
||||||
void usage(const char* path)
|
|
||||||
{
|
{
|
||||||
/* take only the last portion of the path */
|
time_t rawtime;
|
||||||
const char* basename = strrchr(path, '/');
|
tm* timeinfo;
|
||||||
basename = basename ? basename + 1 : path;
|
time(&rawtime);
|
||||||
|
timeinfo = std::localtime(&rawtime);
|
||||||
cout << "usage: " << basename << "[OPTION]" << endl;
|
std::ostringstream oss;
|
||||||
cout << " -h, --help\t\t Print this help and exit." << endl;
|
oss << std::put_time(timeinfo, "%Y-%m-%d");
|
||||||
cout
|
return oss.str();
|
||||||
<< " -f, --file[=FILENAME]\t {diabetes, glass, iris, kdd_JapaneseVowels, letter, liver-disorders, mfeat-factors}."
|
|
||||||
<< endl;
|
|
||||||
cout << " -p, --path[=FILENAME]\t folder where the data files are located, default " << PATH << endl;
|
|
||||||
cout << " -m, --model={AODE, KDB, SPODE, TAN}\t " << endl;
|
|
||||||
}
|
}
|
||||||
|
string get_time()
|
||||||
tuple<string, string, string> parse_arguments(int argc, char** argv)
|
|
||||||
{
|
{
|
||||||
string file_name;
|
time_t rawtime;
|
||||||
string model_name;
|
tm* timeinfo;
|
||||||
string path = PATH;
|
time(&rawtime);
|
||||||
const vector<struct option> long_options = {
|
timeinfo = std::localtime(&rawtime);
|
||||||
{"help", no_argument, nullptr, 'h'},
|
std::ostringstream oss;
|
||||||
{"file", required_argument, nullptr, 'f'},
|
oss << std::put_time(timeinfo, "%H:%M:%S");
|
||||||
{"path", required_argument, nullptr, 'p'},
|
return oss.str();
|
||||||
{"model", required_argument, nullptr, 'm'},
|
|
||||||
{nullptr, no_argument, nullptr, 0}
|
|
||||||
};
|
|
||||||
while (true) {
|
|
||||||
const auto c = getopt_long(argc, argv, "hf:p:m:", long_options.data(), nullptr);
|
|
||||||
if (c == -1)
|
|
||||||
break;
|
|
||||||
switch (c) {
|
|
||||||
case 'h':
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(0);
|
|
||||||
case 'f':
|
|
||||||
file_name = string(optarg);
|
|
||||||
break;
|
|
||||||
case 'm':
|
|
||||||
model_name = string(optarg);
|
|
||||||
break;
|
|
||||||
case 'p':
|
|
||||||
path = optarg;
|
|
||||||
if (path.back() != '/')
|
|
||||||
path += '/';
|
|
||||||
break;
|
|
||||||
case '?':
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
default:
|
|
||||||
abort();
|
|
||||||
}
|
}
|
||||||
}
|
string Experiment::get_file_name()
|
||||||
if (file_name.empty()) {
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
return make_tuple(file_name, path, model_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline constexpr auto hash_conv(const std::string_view sv)
|
|
||||||
{
|
{
|
||||||
unsigned long hash{ 5381 };
|
string result = "results_" + score_name + "_" + model + "_" + platform + "_" + get_date() + "_" + get_time() + "_" + (stratified ? "1" : "0") + ".json";
|
||||||
for (unsigned char c : sv) {
|
return result;
|
||||||
hash = ((hash << 5) + hash) ^ c;
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline constexpr auto operator"" _sh(const char* str, size_t len)
|
json Experiment::build_json()
|
||||||
{
|
{
|
||||||
return hash_conv(std::string_view{ str, len });
|
json result;
|
||||||
|
result["title"] = title;
|
||||||
|
result["date"] = get_date();
|
||||||
|
result["time"] = get_time();
|
||||||
|
result["model"] = model;
|
||||||
|
result["version"] = model_version;
|
||||||
|
result["platform"] = platform;
|
||||||
|
result["score_name"] = score_name;
|
||||||
|
result["language"] = language;
|
||||||
|
result["language_version"] = language_version;
|
||||||
|
result["discretized"] = discretized;
|
||||||
|
result["stratified"] = stratified;
|
||||||
|
result["folds"] = nfolds;
|
||||||
|
result["seeds"] = randomSeeds;
|
||||||
|
result["duration"] = duration;
|
||||||
|
result["results"] = json::array();
|
||||||
|
for (const auto& r : results) {
|
||||||
|
json j;
|
||||||
|
j["dataset"] = r.getDataset();
|
||||||
|
j["hyperparameters"] = r.getHyperparameters();
|
||||||
|
j["samples"] = r.getSamples();
|
||||||
|
j["features"] = r.getFeatures();
|
||||||
|
j["classes"] = r.getClasses();
|
||||||
|
j["score_train"] = r.getScoreTrain();
|
||||||
|
j["score_test"] = r.getScoreTest();
|
||||||
|
j["score"] = r.getScoreTest();
|
||||||
|
j["score_std"] = r.getScoreTestStd();
|
||||||
|
j["score_train_std"] = r.getScoreTrainStd();
|
||||||
|
j["score_test_std"] = r.getScoreTestStd();
|
||||||
|
j["train_time"] = r.getTrainTime();
|
||||||
|
j["train_time_std"] = r.getTrainTimeStd();
|
||||||
|
j["test_time"] = r.getTestTime();
|
||||||
|
j["test_time_std"] = r.getTestTimeStd();
|
||||||
|
j["time"] = r.getTestTime() + r.getTrainTime();
|
||||||
|
j["time_std"] = r.getTestTimeStd() + r.getTrainTimeStd();
|
||||||
|
j["scores_train"] = r.getScoresTrain();
|
||||||
|
j["scores_test"] = r.getScoresTest();
|
||||||
|
j["times_train"] = r.getTimesTrain();
|
||||||
|
j["times_test"] = r.getTimesTest();
|
||||||
|
j["nodes"] = r.getNodes();
|
||||||
|
j["leaves"] = r.getLeaves();
|
||||||
|
j["depth"] = r.getDepth();
|
||||||
|
result["results"].push_back(j);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
void Experiment::save(const string& path)
|
||||||
tuple<string, string, string> get_options(int argc, char** argv)
|
|
||||||
{
|
{
|
||||||
map<string, bool> datasets = {
|
json data = build_json();
|
||||||
{"diabetes", true},
|
ofstream file(path + "/" + get_file_name());
|
||||||
{"ecoli", true},
|
file << data;
|
||||||
{"glass", true},
|
|
||||||
{"iris", true},
|
|
||||||
{"kdd_JapaneseVowels", false},
|
|
||||||
{"letter", true},
|
|
||||||
{"liver-disorders", true},
|
|
||||||
{"mfeat-factors", true},
|
|
||||||
};
|
|
||||||
vector <string> models = { "AODE", "KDB", "SPODE", "TAN" };
|
|
||||||
string file_name;
|
|
||||||
string path;
|
|
||||||
string model_name;
|
|
||||||
tie(file_name, path, model_name) = parse_arguments(argc, argv);
|
|
||||||
if (datasets.find(file_name) == datasets.end()) {
|
|
||||||
cout << "Invalid file name: " << file_name << endl;
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
if (!file_exists(path + file_name + ".arff")) {
|
|
||||||
cout << "Data File " << path + file_name + ".arff" << " does not exist" << endl;
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
if (find(models.begin(), models.end(), model_name) == models.end()) {
|
|
||||||
cout << "Invalid model name: " << model_name << endl;
|
|
||||||
usage(argv[0]);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
return { file_name, path, model_name };
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
string file_name, path, model_name;
|
|
||||||
tie(file_name, path, model_name) = get_options(argc, argv);
|
|
||||||
auto handler = ArffFiles();
|
|
||||||
handler.load(path + file_name + ".arff");
|
|
||||||
// Get Dataset X, y
|
|
||||||
vector<mdlp::samples_t>& X = handler.getX();
|
|
||||||
mdlp::labels_t& y = handler.getY();
|
|
||||||
// Get className & Features
|
|
||||||
auto className = handler.getClassName();
|
|
||||||
vector<string> features;
|
|
||||||
for (auto feature : handler.getAttributes()) {
|
|
||||||
features.push_back(feature.first);
|
|
||||||
}
|
|
||||||
// Discretize Dataset
|
|
||||||
vector<mdlp::labels_t> Xd;
|
|
||||||
map<string, int> maxes;
|
|
||||||
tie(Xd, maxes) = discretize(X, y, features);
|
|
||||||
maxes[className] = *max_element(y.begin(), y.end()) + 1;
|
|
||||||
map<string, vector<int>> states;
|
|
||||||
for (auto feature : features) {
|
|
||||||
states[feature] = vector<int>(maxes[feature]);
|
|
||||||
}
|
|
||||||
states[className] = vector<int>(
|
|
||||||
maxes[className]);
|
|
||||||
double score;
|
|
||||||
vector<string> lines;
|
|
||||||
vector<string> graph;
|
|
||||||
auto kdb = bayesnet::KDB(2);
|
|
||||||
auto aode = bayesnet::AODE();
|
|
||||||
auto spode = bayesnet::SPODE(2);
|
|
||||||
auto tan = bayesnet::TAN();
|
|
||||||
switch (hash_conv(model_name)) {
|
|
||||||
case "AODE"_sh:
|
|
||||||
aode.fit(Xd, y, features, className, states);
|
|
||||||
lines = aode.show();
|
|
||||||
score = aode.score(Xd, y);
|
|
||||||
graph = aode.graph();
|
|
||||||
break;
|
|
||||||
case "KDB"_sh:
|
|
||||||
kdb.fit(Xd, y, features, className, states);
|
|
||||||
lines = kdb.show();
|
|
||||||
score = kdb.score(Xd, y);
|
|
||||||
graph = kdb.graph();
|
|
||||||
break;
|
|
||||||
case "SPODE"_sh:
|
|
||||||
spode.fit(Xd, y, features, className, states);
|
|
||||||
lines = spode.show();
|
|
||||||
score = spode.score(Xd, y);
|
|
||||||
graph = spode.graph();
|
|
||||||
break;
|
|
||||||
case "TAN"_sh:
|
|
||||||
tan.fit(Xd, y, features, className, states);
|
|
||||||
lines = tan.show();
|
|
||||||
score = tan.score(Xd, y);
|
|
||||||
graph = tan.graph();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for (auto line : lines) {
|
|
||||||
cout << line << endl;
|
|
||||||
}
|
|
||||||
cout << "Score: " << score << endl;
|
|
||||||
auto dot_file = model_name + "_" + file_name;
|
|
||||||
ofstream file(dot_file + ".dot");
|
|
||||||
file << graph;
|
|
||||||
file.close();
|
file.close();
|
||||||
cout << "Graph saved in " << model_name << "_" << file_name << ".dot" << endl;
|
}
|
||||||
cout << "dot -Tpng -o " + dot_file + ".png " + dot_file + ".dot " << endl;
|
|
||||||
return 0;
|
void Experiment::show()
|
||||||
|
{
|
||||||
|
json data = build_json();
|
||||||
|
cout << data.dump(4) << endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Experiment::go(vector<string> filesToProcess, const string& path)
|
||||||
|
{
|
||||||
|
cout << "*** Starting experiment: " << title << " ***" << endl;
|
||||||
|
for (auto fileName : filesToProcess) {
|
||||||
|
cout << "- " << setw(20) << left << fileName << " " << right << flush;
|
||||||
|
cross_validation(path, fileName);
|
||||||
|
cout << endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Experiment::cross_validation(const string& path, const string& fileName)
|
||||||
|
{
|
||||||
|
auto datasets = platform::Datasets(path, true, platform::ARFF);
|
||||||
|
// Get dataset
|
||||||
|
auto [X, y] = datasets.getTensors(fileName);
|
||||||
|
auto states = datasets.getStates(fileName);
|
||||||
|
auto features = datasets.getFeatures(fileName);
|
||||||
|
auto samples = datasets.getNSamples(fileName);
|
||||||
|
auto className = datasets.getClassName(fileName);
|
||||||
|
cout << " (" << setw(5) << samples << "," << setw(3) << features.size() << ") " << flush;
|
||||||
|
// Prepare Result
|
||||||
|
auto result = Result();
|
||||||
|
auto [values, counts] = at::_unique(y);;
|
||||||
|
result.setSamples(X.size(1)).setFeatures(X.size(0)).setClasses(values.size(0));
|
||||||
|
int nResults = nfolds * static_cast<int>(randomSeeds.size());
|
||||||
|
auto accuracy_test = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto accuracy_train = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto train_time = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto test_time = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto nodes = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto edges = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
auto num_states = torch::zeros({ nResults }, torch::kFloat64);
|
||||||
|
Timer train_timer, test_timer;
|
||||||
|
int item = 0;
|
||||||
|
for (auto seed : randomSeeds) {
|
||||||
|
cout << "(" << seed << ") doing Fold: " << flush;
|
||||||
|
Fold* fold;
|
||||||
|
if (stratified)
|
||||||
|
fold = new StratifiedKFold(nfolds, y, seed);
|
||||||
|
else
|
||||||
|
fold = new KFold(nfolds, y.size(0), seed);
|
||||||
|
for (int nfold = 0; nfold < nfolds; nfold++) {
|
||||||
|
auto clf = Models::instance()->create(model);
|
||||||
|
setModelVersion(clf->getVersion());
|
||||||
|
train_timer.start();
|
||||||
|
auto [train, test] = fold->getFold(nfold);
|
||||||
|
auto train_t = torch::tensor(train);
|
||||||
|
auto test_t = torch::tensor(test);
|
||||||
|
auto X_train = X.index({ "...", train_t });
|
||||||
|
auto y_train = y.index({ train_t });
|
||||||
|
auto X_test = X.index({ "...", test_t });
|
||||||
|
auto y_test = y.index({ test_t });
|
||||||
|
cout << nfold + 1 << ", " << flush;
|
||||||
|
clf->fit(X_train, y_train, features, className, states);
|
||||||
|
nodes[item] = clf->getNumberOfNodes();
|
||||||
|
edges[item] = clf->getNumberOfEdges();
|
||||||
|
num_states[item] = clf->getNumberOfStates();
|
||||||
|
train_time[item] = train_timer.getDuration();
|
||||||
|
auto accuracy_train_value = clf->score(X_train, y_train);
|
||||||
|
test_timer.start();
|
||||||
|
auto accuracy_test_value = clf->score(X_test, y_test);
|
||||||
|
test_time[item] = test_timer.getDuration();
|
||||||
|
accuracy_train[item] = accuracy_train_value;
|
||||||
|
accuracy_test[item] = accuracy_test_value;
|
||||||
|
// Store results and times in vector
|
||||||
|
result.addScoreTrain(accuracy_train_value);
|
||||||
|
result.addScoreTest(accuracy_test_value);
|
||||||
|
result.addTimeTrain(train_time[item].item<double>());
|
||||||
|
result.addTimeTest(test_time[item].item<double>());
|
||||||
|
item++;
|
||||||
|
}
|
||||||
|
cout << "end. " << flush;
|
||||||
|
delete fold;
|
||||||
|
}
|
||||||
|
result.setScoreTest(torch::mean(accuracy_test).item<double>()).setScoreTrain(torch::mean(accuracy_train).item<double>());
|
||||||
|
result.setScoreTestStd(torch::std(accuracy_test).item<double>()).setScoreTrainStd(torch::std(accuracy_train).item<double>());
|
||||||
|
result.setTrainTime(torch::mean(train_time).item<double>()).setTestTime(torch::mean(test_time).item<double>());
|
||||||
|
result.setNodes(torch::mean(nodes).item<double>()).setLeaves(torch::mean(edges).item<double>()).setDepth(torch::mean(num_states).item<double>());
|
||||||
|
result.setDataset(fileName);
|
||||||
|
addResult(result);
|
||||||
|
}
|
||||||
}
|
}
|
@@ -0,0 +1,113 @@
|
|||||||
|
#ifndef EXPERIMENT_H
|
||||||
|
#define EXPERIMENT_H
|
||||||
|
#include <torch/torch.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <string>
|
||||||
|
#include <chrono>
|
||||||
|
#include "Folding.h"
|
||||||
|
#include "BaseClassifier.h"
|
||||||
|
#include "TAN.h"
|
||||||
|
#include "KDB.h"
|
||||||
|
#include "AODE.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
namespace platform {
|
||||||
|
using json = nlohmann::json;
|
||||||
|
class Timer {
|
||||||
|
private:
|
||||||
|
chrono::high_resolution_clock::time_point begin;
|
||||||
|
public:
|
||||||
|
Timer() = default;
|
||||||
|
~Timer() = default;
|
||||||
|
void start() { begin = chrono::high_resolution_clock::now(); }
|
||||||
|
double getDuration()
|
||||||
|
{
|
||||||
|
chrono::high_resolution_clock::time_point end = chrono::high_resolution_clock::now();
|
||||||
|
chrono::duration<double> time_span = chrono::duration_cast<chrono::duration<double>>(end - begin);
|
||||||
|
return time_span.count();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
class Result {
|
||||||
|
private:
|
||||||
|
string dataset, hyperparameters, model_version;
|
||||||
|
int samples{ 0 }, features{ 0 }, classes{ 0 };
|
||||||
|
double score_train{ 0 }, score_test{ 0 }, score_train_std{ 0 }, score_test_std{ 0 }, train_time{ 0 }, train_time_std{ 0 }, test_time{ 0 }, test_time_std{ 0 };
|
||||||
|
float nodes{ 0 }, leaves{ 0 }, depth{ 0 };
|
||||||
|
vector<double> scores_train, scores_test, times_train, times_test;
|
||||||
|
public:
|
||||||
|
Result() = default;
|
||||||
|
Result& setDataset(const string& dataset) { this->dataset = dataset; return *this; }
|
||||||
|
Result& setHyperparameters(const string& hyperparameters) { this->hyperparameters = hyperparameters; return *this; }
|
||||||
|
Result& setSamples(int samples) { this->samples = samples; return *this; }
|
||||||
|
Result& setFeatures(int features) { this->features = features; return *this; }
|
||||||
|
Result& setClasses(int classes) { this->classes = classes; return *this; }
|
||||||
|
Result& setScoreTrain(double score) { this->score_train = score; return *this; }
|
||||||
|
Result& setScoreTest(double score) { this->score_test = score; return *this; }
|
||||||
|
Result& setScoreTrainStd(double score_std) { this->score_train_std = score_std; return *this; }
|
||||||
|
Result& setScoreTestStd(double score_std) { this->score_test_std = score_std; return *this; }
|
||||||
|
Result& setTrainTime(double train_time) { this->train_time = train_time; return *this; }
|
||||||
|
Result& setTrainTimeStd(double train_time_std) { this->train_time_std = train_time_std; return *this; }
|
||||||
|
Result& setTestTime(double test_time) { this->test_time = test_time; return *this; }
|
||||||
|
Result& setTestTimeStd(double test_time_std) { this->test_time_std = test_time_std; return *this; }
|
||||||
|
Result& setNodes(float nodes) { this->nodes = nodes; return *this; }
|
||||||
|
Result& setLeaves(float leaves) { this->leaves = leaves; return *this; }
|
||||||
|
Result& setDepth(float depth) { this->depth = depth; return *this; }
|
||||||
|
Result& addScoreTrain(double score) { scores_train.push_back(score); return *this; }
|
||||||
|
Result& addScoreTest(double score) { scores_test.push_back(score); return *this; }
|
||||||
|
Result& addTimeTrain(double time) { times_train.push_back(time); return *this; }
|
||||||
|
Result& addTimeTest(double time) { times_test.push_back(time); return *this; }
|
||||||
|
const float get_score_train() const { return score_train; }
|
||||||
|
float get_score_test() { return score_test; }
|
||||||
|
const string& getDataset() const { return dataset; }
|
||||||
|
const string& getHyperparameters() const { return hyperparameters; }
|
||||||
|
const int getSamples() const { return samples; }
|
||||||
|
const int getFeatures() const { return features; }
|
||||||
|
const int getClasses() const { return classes; }
|
||||||
|
const double getScoreTrain() const { return score_train; }
|
||||||
|
const double getScoreTest() const { return score_test; }
|
||||||
|
const double getScoreTrainStd() const { return score_train_std; }
|
||||||
|
const double getScoreTestStd() const { return score_test_std; }
|
||||||
|
const double getTrainTime() const { return train_time; }
|
||||||
|
const double getTrainTimeStd() const { return train_time_std; }
|
||||||
|
const double getTestTime() const { return test_time; }
|
||||||
|
const double getTestTimeStd() const { return test_time_std; }
|
||||||
|
const float getNodes() const { return nodes; }
|
||||||
|
const float getLeaves() const { return leaves; }
|
||||||
|
const float getDepth() const { return depth; }
|
||||||
|
const vector<double>& getScoresTrain() const { return scores_train; }
|
||||||
|
const vector<double>& getScoresTest() const { return scores_test; }
|
||||||
|
const vector<double>& getTimesTrain() const { return times_train; }
|
||||||
|
const vector<double>& getTimesTest() const { return times_test; }
|
||||||
|
};
|
||||||
|
class Experiment {
|
||||||
|
private:
|
||||||
|
string title, model, platform, score_name, model_version, language_version, language;
|
||||||
|
bool discretized{ false }, stratified{ false };
|
||||||
|
vector<Result> results;
|
||||||
|
vector<int> randomSeeds;
|
||||||
|
int nfolds{ 0 };
|
||||||
|
float duration{ 0 };
|
||||||
|
json build_json();
|
||||||
|
public:
|
||||||
|
Experiment() = default;
|
||||||
|
Experiment& setTitle(const string& title) { this->title = title; return *this; }
|
||||||
|
Experiment& setModel(const string& model) { this->model = model; return *this; }
|
||||||
|
Experiment& setPlatform(const string& platform) { this->platform = platform; return *this; }
|
||||||
|
Experiment& setScoreName(const string& score_name) { this->score_name = score_name; return *this; }
|
||||||
|
Experiment& setModelVersion(const string& model_version) { this->model_version = model_version; return *this; }
|
||||||
|
Experiment& setLanguage(const string& language) { this->language = language; return *this; }
|
||||||
|
Experiment& setLanguageVersion(const string& language_version) { this->language_version = language_version; return *this; }
|
||||||
|
Experiment& setDiscretized(bool discretized) { this->discretized = discretized; return *this; }
|
||||||
|
Experiment& setStratified(bool stratified) { this->stratified = stratified; return *this; }
|
||||||
|
Experiment& setNFolds(int nfolds) { this->nfolds = nfolds; return *this; }
|
||||||
|
Experiment& addResult(Result result) { results.push_back(result); return *this; }
|
||||||
|
Experiment& addRandomSeed(int randomSeed) { randomSeeds.push_back(randomSeed); return *this; }
|
||||||
|
Experiment& setDuration(float duration) { this->duration = duration; return *this; }
|
||||||
|
string get_file_name();
|
||||||
|
void save(const string& path);
|
||||||
|
void cross_validation(const string& path, const string& fileName);
|
||||||
|
void go(vector<string> filesToProcess, const string& path);
|
||||||
|
void show();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
95
src/Platform/Folding.cc
Normal file
95
src/Platform/Folding.cc
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#include "Folding.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <map>
|
||||||
|
Fold::Fold(int k, int n, int seed) : k(k), n(n), seed(seed)
|
||||||
|
{
|
||||||
|
random_device rd;
|
||||||
|
random_seed = default_random_engine(seed == -1 ? rd() : seed);
|
||||||
|
srand(seed == -1 ? time(0) : seed);
|
||||||
|
}
|
||||||
|
KFold::KFold(int k, int n, int seed) : Fold(k, n, seed), indices(vector<int>(n))
|
||||||
|
{
|
||||||
|
iota(begin(indices), end(indices), 0); // fill with 0, 1, ..., n - 1
|
||||||
|
shuffle(indices.begin(), indices.end(), random_seed);
|
||||||
|
}
|
||||||
|
pair<vector<int>, vector<int>> KFold::getFold(int nFold)
|
||||||
|
{
|
||||||
|
if (nFold >= k || nFold < 0) {
|
||||||
|
throw out_of_range("nFold (" + to_string(nFold) + ") must be less than k (" + to_string(k) + ")");
|
||||||
|
}
|
||||||
|
int nTest = n / k;
|
||||||
|
auto train = vector<int>();
|
||||||
|
auto test = vector<int>();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (i >= nTest * nFold && i < nTest * (nFold + 1)) {
|
||||||
|
test.push_back(indices[i]);
|
||||||
|
} else {
|
||||||
|
train.push_back(indices[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { train, test };
|
||||||
|
}
|
||||||
|
StratifiedKFold::StratifiedKFold(int k, torch::Tensor& y, int seed) : Fold(k, y.numel(), seed)
|
||||||
|
{
|
||||||
|
n = y.numel();
|
||||||
|
this->y = vector<int>(y.data_ptr<int>(), y.data_ptr<int>() + n);
|
||||||
|
build();
|
||||||
|
}
|
||||||
|
StratifiedKFold::StratifiedKFold(int k, const vector<int>& y, int seed)
|
||||||
|
: Fold(k, y.size(), seed)
|
||||||
|
{
|
||||||
|
this->y = y;
|
||||||
|
n = y.size();
|
||||||
|
build();
|
||||||
|
}
|
||||||
|
void StratifiedKFold::build()
|
||||||
|
{
|
||||||
|
stratified_indices = vector<vector<int>>(k);
|
||||||
|
int fold_size = n / k;
|
||||||
|
// Compute class counts and indices
|
||||||
|
auto class_indices = map<int, vector<int>>();
|
||||||
|
vector<int> class_counts(*max_element(y.begin(), y.end()) + 1, 0);
|
||||||
|
for (auto i = 0; i < n; ++i) {
|
||||||
|
class_counts[y[i]]++;
|
||||||
|
class_indices[y[i]].push_back(i);
|
||||||
|
}
|
||||||
|
// Shuffle class indices
|
||||||
|
for (auto& [cls, indices] : class_indices) {
|
||||||
|
shuffle(indices.begin(), indices.end(), random_seed);
|
||||||
|
}
|
||||||
|
// Assign indices to folds
|
||||||
|
for (auto label = 0; label < class_counts.size(); ++label) {
|
||||||
|
auto num_samples_to_take = class_counts[label] / k;
|
||||||
|
if (num_samples_to_take == 0)
|
||||||
|
continue;
|
||||||
|
auto remainder_samples_to_take = class_counts[label] % k;
|
||||||
|
for (auto fold = 0; fold < k; ++fold) {
|
||||||
|
auto it = next(class_indices[label].begin(), num_samples_to_take);
|
||||||
|
move(class_indices[label].begin(), it, back_inserter(stratified_indices[fold])); // ##
|
||||||
|
class_indices[label].erase(class_indices[label].begin(), it);
|
||||||
|
}
|
||||||
|
while (remainder_samples_to_take > 0) {
|
||||||
|
int fold = (rand() % static_cast<int>(k));
|
||||||
|
if (stratified_indices[fold].size() == fold_size + 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto it = next(class_indices[label].begin(), 1);
|
||||||
|
stratified_indices[fold].push_back(*class_indices[label].begin());
|
||||||
|
class_indices[label].erase(class_indices[label].begin(), it);
|
||||||
|
remainder_samples_to_take--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pair<vector<int>, vector<int>> StratifiedKFold::getFold(int nFold)
|
||||||
|
{
|
||||||
|
if (nFold >= k || nFold < 0) {
|
||||||
|
throw out_of_range("nFold (" + to_string(nFold) + ") must be less than k (" + to_string(k) + ")");
|
||||||
|
}
|
||||||
|
vector<int> test_indices = stratified_indices[nFold];
|
||||||
|
vector<int> train_indices;
|
||||||
|
for (int i = 0; i < k; ++i) {
|
||||||
|
if (i == nFold) continue;
|
||||||
|
train_indices.insert(train_indices.end(), stratified_indices[i].begin(), stratified_indices[i].end());
|
||||||
|
}
|
||||||
|
return { train_indices, test_indices };
|
||||||
|
}
|
37
src/Platform/Folding.h
Normal file
37
src/Platform/Folding.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#ifndef FOLDING_H
|
||||||
|
#define FOLDING_H
|
||||||
|
#include <torch/torch.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <random>
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
class Fold {
|
||||||
|
protected:
|
||||||
|
int k;
|
||||||
|
int n;
|
||||||
|
int seed;
|
||||||
|
default_random_engine random_seed;
|
||||||
|
public:
|
||||||
|
Fold(int k, int n, int seed = -1);
|
||||||
|
virtual pair<vector<int>, vector<int>> getFold(int nFold) = 0;
|
||||||
|
virtual ~Fold() = default;
|
||||||
|
int getNumberOfFolds() { return k; }
|
||||||
|
};
|
||||||
|
class KFold : public Fold {
|
||||||
|
private:
|
||||||
|
vector<int> indices;
|
||||||
|
public:
|
||||||
|
KFold(int k, int n, int seed = -1);
|
||||||
|
pair<vector<int>, vector<int>> getFold(int nFold) override;
|
||||||
|
};
|
||||||
|
class StratifiedKFold : public Fold {
|
||||||
|
private:
|
||||||
|
vector<int> y;
|
||||||
|
vector<vector<int>> stratified_indices;
|
||||||
|
void build();
|
||||||
|
public:
|
||||||
|
StratifiedKFold(int k, const vector<int>& y, int seed = -1);
|
||||||
|
StratifiedKFold(int k, torch::Tensor& y, int seed = -1);
|
||||||
|
pair<vector<int>, vector<int>> getFold(int nFold) override;
|
||||||
|
};
|
||||||
|
#endif
|
@@ -1,78 +0,0 @@
|
|||||||
#include "Metrics.h"
|
|
||||||
#include <set>
|
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
namespace mdlp {
|
|
||||||
Metrics::Metrics(labels_t& y_, indices_t& indices_): y(y_), indices(indices_),
|
|
||||||
numClasses(computeNumClasses(0, indices.size()))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
int Metrics::computeNumClasses(size_t start, size_t end)
|
|
||||||
{
|
|
||||||
set<int> nClasses;
|
|
||||||
for (auto i = start; i < end; ++i) {
|
|
||||||
nClasses.insert(y[indices[i]]);
|
|
||||||
}
|
|
||||||
return static_cast<int>(nClasses.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void Metrics::setData(const labels_t& y_, const indices_t& indices_)
|
|
||||||
{
|
|
||||||
indices = indices_;
|
|
||||||
y = y_;
|
|
||||||
numClasses = computeNumClasses(0, indices.size());
|
|
||||||
entropyCache.clear();
|
|
||||||
igCache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
precision_t Metrics::entropy(size_t start, size_t end)
|
|
||||||
{
|
|
||||||
precision_t p;
|
|
||||||
precision_t ventropy = 0;
|
|
||||||
int nElements = 0;
|
|
||||||
labels_t counts(numClasses + 1, 0);
|
|
||||||
if (end - start < 2)
|
|
||||||
return 0;
|
|
||||||
if (entropyCache.find({ start, end }) != entropyCache.end()) {
|
|
||||||
return entropyCache[{start, end}];
|
|
||||||
}
|
|
||||||
for (auto i = &indices[start]; i != &indices[end]; ++i) {
|
|
||||||
counts[y[*i]]++;
|
|
||||||
nElements++;
|
|
||||||
}
|
|
||||||
for (auto count : counts) {
|
|
||||||
if (count > 0) {
|
|
||||||
p = static_cast<precision_t>(count) / static_cast<precision_t>(nElements);
|
|
||||||
ventropy -= p * log2(p);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entropyCache[{start, end}] = ventropy;
|
|
||||||
return ventropy;
|
|
||||||
}
|
|
||||||
|
|
||||||
precision_t Metrics::informationGain(size_t start, size_t cut, size_t end)
|
|
||||||
{
|
|
||||||
precision_t iGain;
|
|
||||||
precision_t entropyInterval;
|
|
||||||
precision_t entropyLeft;
|
|
||||||
precision_t entropyRight;
|
|
||||||
size_t nElementsLeft = cut - start;
|
|
||||||
size_t nElementsRight = end - cut;
|
|
||||||
size_t nElements = end - start;
|
|
||||||
if (igCache.find(make_tuple(start, cut, end)) != igCache.end()) {
|
|
||||||
return igCache[make_tuple(start, cut, end)];
|
|
||||||
}
|
|
||||||
entropyInterval = entropy(start, end);
|
|
||||||
entropyLeft = entropy(start, cut);
|
|
||||||
entropyRight = entropy(cut, end);
|
|
||||||
iGain = entropyInterval -
|
|
||||||
(static_cast<precision_t>(nElementsLeft) * entropyLeft +
|
|
||||||
static_cast<precision_t>(nElementsRight) * entropyRight) /
|
|
||||||
static_cast<precision_t>(nElements);
|
|
||||||
igCache[make_tuple(start, cut, end)] = iGain;
|
|
||||||
return iGain;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
#ifndef CCMETRICS_H
|
|
||||||
#define CCMETRICS_H
|
|
||||||
|
|
||||||
#include "typesFImdlp.h"
|
|
||||||
|
|
||||||
namespace mdlp {
|
|
||||||
class Metrics {
|
|
||||||
protected:
|
|
||||||
labels_t& y;
|
|
||||||
indices_t& indices;
|
|
||||||
int numClasses;
|
|
||||||
cacheEnt_t entropyCache = cacheEnt_t();
|
|
||||||
cacheIg_t igCache = cacheIg_t();
|
|
||||||
public:
|
|
||||||
Metrics(labels_t&, indices_t&);
|
|
||||||
void setData(const labels_t&, const indices_t&);
|
|
||||||
int computeNumClasses(size_t, size_t);
|
|
||||||
precision_t entropy(size_t, size_t);
|
|
||||||
precision_t informationGain(size_t, size_t, size_t);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
#endif
|
|
54
src/Platform/Models.cc
Normal file
54
src/Platform/Models.cc
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include "Models.h"
|
||||||
|
namespace platform {
|
||||||
|
using namespace std;
|
||||||
|
// Idea from: https://www.codeproject.com/Articles/567242/AplusC-2b-2bplusObjectplusFactory
|
||||||
|
Models* Models::factory = nullptr;;
|
||||||
|
Models* Models::instance()
|
||||||
|
{
|
||||||
|
//manages singleton
|
||||||
|
if (factory == nullptr)
|
||||||
|
factory = new Models();
|
||||||
|
return factory;
|
||||||
|
}
|
||||||
|
void Models::registerFactoryFunction(const string& name,
|
||||||
|
function<bayesnet::BaseClassifier* (void)> classFactoryFunction)
|
||||||
|
{
|
||||||
|
// register the class factory function
|
||||||
|
functionRegistry[name] = classFactoryFunction;
|
||||||
|
}
|
||||||
|
shared_ptr<bayesnet::BaseClassifier> Models::create(const string& name)
|
||||||
|
{
|
||||||
|
bayesnet::BaseClassifier* instance = nullptr;
|
||||||
|
|
||||||
|
// find name in the registry and call factory method.
|
||||||
|
auto it = functionRegistry.find(name);
|
||||||
|
if (it != functionRegistry.end())
|
||||||
|
instance = it->second();
|
||||||
|
// wrap instance in a shared ptr and return
|
||||||
|
if (instance != nullptr)
|
||||||
|
return shared_ptr<bayesnet::BaseClassifier>(instance);
|
||||||
|
else
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
vector<string> Models::getNames()
|
||||||
|
{
|
||||||
|
vector<string> names;
|
||||||
|
transform(functionRegistry.begin(), functionRegistry.end(), back_inserter(names),
|
||||||
|
[](const pair<string, function<bayesnet::BaseClassifier* (void)>>& pair) { return pair.first; });
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
string Models::toString()
|
||||||
|
{
|
||||||
|
string result = "";
|
||||||
|
for (const auto& pair : functionRegistry) {
|
||||||
|
result += pair.first + ", ";
|
||||||
|
}
|
||||||
|
return "{" + result.substr(0, result.size() - 2) + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
Registrar::Registrar(const string& name, function<bayesnet::BaseClassifier* (void)> classFactoryFunction)
|
||||||
|
{
|
||||||
|
// register the class factory function
|
||||||
|
Models::instance()->registerFactoryFunction(name, classFactoryFunction);
|
||||||
|
}
|
||||||
|
}
|
32
src/Platform/Models.h
Normal file
32
src/Platform/Models.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#ifndef MODELS_H
|
||||||
|
#define MODELS_H
|
||||||
|
#include <map>
|
||||||
|
#include "BaseClassifier.h"
|
||||||
|
#include "AODE.h"
|
||||||
|
#include "TAN.h"
|
||||||
|
#include "KDB.h"
|
||||||
|
#include "SPODE.h"
|
||||||
|
namespace platform {
|
||||||
|
class Models {
|
||||||
|
private:
|
||||||
|
map<string, function<bayesnet::BaseClassifier* (void)>> functionRegistry;
|
||||||
|
static Models* factory; //singleton
|
||||||
|
Models() {};
|
||||||
|
public:
|
||||||
|
Models(Models&) = delete;
|
||||||
|
void operator=(const Models&) = delete;
|
||||||
|
// Idea from: https://www.codeproject.com/Articles/567242/AplusC-2b-2bplusObjectplusFactory
|
||||||
|
static Models* instance();
|
||||||
|
shared_ptr<bayesnet::BaseClassifier> create(const string& name);
|
||||||
|
void registerFactoryFunction(const string& name,
|
||||||
|
function<bayesnet::BaseClassifier* (void)> classFactoryFunction);
|
||||||
|
vector<string> getNames();
|
||||||
|
string toString();
|
||||||
|
|
||||||
|
};
|
||||||
|
class Registrar {
|
||||||
|
public:
|
||||||
|
Registrar(const string& className, function<bayesnet::BaseClassifier* (void)> classFactoryFunction);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endif
|
123
src/Platform/main.cc
Normal file
123
src/Platform/main.cc
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <argparse/argparse.hpp>
|
||||||
|
#include "platformUtils.h"
|
||||||
|
#include "Experiment.h"
|
||||||
|
#include "Datasets.h"
|
||||||
|
#include "DotEnv.h"
|
||||||
|
#include "Models.h"
|
||||||
|
#include "modelRegister.h"
|
||||||
|
|
||||||
|
using namespace std;
|
||||||
|
const string PATH_RESULTS = "results";
|
||||||
|
const string PATH_DATASETS = "datasets";
|
||||||
|
|
||||||
|
argparse::ArgumentParser manageArguments(int argc, char** argv)
|
||||||
|
{
|
||||||
|
auto env = platform::DotEnv();
|
||||||
|
argparse::ArgumentParser program("BayesNetSample");
|
||||||
|
program.add_argument("-d", "--dataset").default_value("").help("Dataset file name");
|
||||||
|
program.add_argument("-p", "--path")
|
||||||
|
.help("folder where the data files are located, default")
|
||||||
|
.default_value(string{ PATH_DATASETS }
|
||||||
|
);
|
||||||
|
program.add_argument("-m", "--model")
|
||||||
|
.help("Model to use " + platform::Models::instance()->toString())
|
||||||
|
.action([](const std::string& value) {
|
||||||
|
static const vector<string> choices = platform::Models::instance()->getNames();
|
||||||
|
if (find(choices.begin(), choices.end(), value) != choices.end()) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
throw runtime_error("Model must be one of " + platform::Models::instance()->toString());
|
||||||
|
}
|
||||||
|
);
|
||||||
|
program.add_argument("--title").default_value("").help("Experiment title");
|
||||||
|
program.add_argument("--discretize").help("Discretize input dataset").default_value((bool)stoi(env.get("discretize"))).implicit_value(true);
|
||||||
|
program.add_argument("--stratified").help("If Stratified KFold is to be done").default_value((bool)stoi(env.get("stratified"))).implicit_value(true);
|
||||||
|
program.add_argument("-f", "--folds").help("Number of folds").default_value(stoi(env.get("n_folds"))).scan<'i', int>().action([](const string& value) {
|
||||||
|
try {
|
||||||
|
auto k = stoi(value);
|
||||||
|
if (k < 2) {
|
||||||
|
throw runtime_error("Number of folds must be greater than 1");
|
||||||
|
}
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
catch (const runtime_error& err) {
|
||||||
|
throw runtime_error(err.what());
|
||||||
|
}
|
||||||
|
catch (...) {
|
||||||
|
throw runtime_error("Number of folds must be an integer");
|
||||||
|
}});
|
||||||
|
auto seed_values = env.getSeeds();
|
||||||
|
program.add_argument("-s", "--seeds").nargs(1, 10).help("Random seeds. Set to -1 to have pseudo random").scan<'i', int>().default_value(seed_values);
|
||||||
|
try {
|
||||||
|
program.parse_args(argc, argv);
|
||||||
|
auto file_name = program.get<string>("dataset");
|
||||||
|
auto path = program.get<string>("path");
|
||||||
|
auto model_name = program.get<string>("model");
|
||||||
|
auto discretize_dataset = program.get<bool>("discretize");
|
||||||
|
auto stratified = program.get<bool>("stratified");
|
||||||
|
auto n_folds = program.get<int>("folds");
|
||||||
|
auto seeds = program.get<vector<int>>("seeds");
|
||||||
|
auto complete_file_name = path + file_name + ".arff";
|
||||||
|
auto title = program.get<string>("title");
|
||||||
|
if (title == "" && file_name == "") {
|
||||||
|
throw runtime_error("title is mandatory if dataset is not provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const exception& err) {
|
||||||
|
cerr << err.what() << endl;
|
||||||
|
cerr << program;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
auto program = manageArguments(argc, argv);
|
||||||
|
bool saveResults = false;
|
||||||
|
auto file_name = program.get<string>("dataset");
|
||||||
|
auto path = program.get<string>("path");
|
||||||
|
auto model_name = program.get<string>("model");
|
||||||
|
auto discretize_dataset = program.get<bool>("discretize");
|
||||||
|
auto stratified = program.get<bool>("stratified");
|
||||||
|
auto n_folds = program.get<int>("folds");
|
||||||
|
auto seeds = program.get<vector<int>>("seeds");
|
||||||
|
vector<string> filesToTest;
|
||||||
|
auto datasets = platform::Datasets(path, true, platform::ARFF);
|
||||||
|
auto title = program.get<string>("title");
|
||||||
|
if (file_name != "") {
|
||||||
|
if (!datasets.isDataset(file_name)) {
|
||||||
|
cerr << "Dataset " << file_name << " not found" << endl;
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (title == "") {
|
||||||
|
title = "Test " + file_name + " " + model_name + " " + to_string(n_folds) + " folds";
|
||||||
|
}
|
||||||
|
filesToTest.push_back(file_name);
|
||||||
|
} else {
|
||||||
|
filesToTest = platform::Datasets(path, true, platform::ARFF).getNames();
|
||||||
|
saveResults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Begin Processing
|
||||||
|
*/
|
||||||
|
auto experiment = platform::Experiment();
|
||||||
|
experiment.setTitle(title).setLanguage("cpp").setLanguageVersion("1.0.0");
|
||||||
|
experiment.setDiscretized(discretize_dataset).setModel(model_name).setPlatform("BayesNet");
|
||||||
|
experiment.setStratified(stratified).setNFolds(n_folds).setScoreName("accuracy");
|
||||||
|
for (auto seed : seeds) {
|
||||||
|
experiment.addRandomSeed(seed);
|
||||||
|
}
|
||||||
|
platform::Timer timer;
|
||||||
|
timer.start();
|
||||||
|
experiment.go(filesToTest, path);
|
||||||
|
experiment.setDuration(timer.getDuration());
|
||||||
|
if (saveResults)
|
||||||
|
experiment.save(PATH_RESULTS);
|
||||||
|
else
|
||||||
|
experiment.show();
|
||||||
|
cout << "Done!" << endl;
|
||||||
|
return 0;
|
||||||
|
}
|
11
src/Platform/modelRegister.h
Normal file
11
src/Platform/modelRegister.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#ifndef MODEL_REGISTER_H
|
||||||
|
#define MODEL_REGISTER_H
|
||||||
|
static platform::Registrar registrarT("TAN",
|
||||||
|
[](void) -> bayesnet::BaseClassifier* { return new bayesnet::TAN();});
|
||||||
|
static platform::Registrar registrarS("SPODE",
|
||||||
|
[](void) -> bayesnet::BaseClassifier* { return new bayesnet::SPODE(2);});
|
||||||
|
static platform::Registrar registrarK("KDB",
|
||||||
|
[](void) -> bayesnet::BaseClassifier* { return new bayesnet::KDB(2);});
|
||||||
|
static platform::Registrar registrarA("AODE",
|
||||||
|
[](void) -> bayesnet::BaseClassifier* { return new bayesnet::AODE();});
|
||||||
|
#endif
|
@@ -1,5 +1,18 @@
|
|||||||
#include "platformUtils.h"
|
#include "platformUtils.h"
|
||||||
|
|
||||||
|
using namespace torch;
|
||||||
|
|
||||||
|
vector<string> split(const string& text, char delimiter)
|
||||||
|
{
|
||||||
|
vector<string> result;
|
||||||
|
stringstream ss(text);
|
||||||
|
string token;
|
||||||
|
while (getline(ss, token, delimiter)) {
|
||||||
|
result.push_back(token);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features)
|
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features)
|
||||||
{
|
{
|
||||||
vector<mdlp::labels_t> Xd;
|
vector<mdlp::labels_t> Xd;
|
||||||
@@ -14,7 +27,19 @@ pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t
|
|||||||
return { Xd, maxes };
|
return { Xd, maxes };
|
||||||
}
|
}
|
||||||
|
|
||||||
bool file_exists(const std::string& name)
|
vector<mdlp::labels_t> discretizeDataset(vector<mdlp::samples_t>& X, mdlp::labels_t& y)
|
||||||
|
{
|
||||||
|
vector<mdlp::labels_t> Xd;
|
||||||
|
auto fimdlp = mdlp::CPPFImdlp();
|
||||||
|
for (int i = 0; i < X.size(); i++) {
|
||||||
|
fimdlp.fit(X[i], y);
|
||||||
|
mdlp::labels_t& xd = fimdlp.transform(X[i]);
|
||||||
|
Xd.push_back(xd);
|
||||||
|
}
|
||||||
|
return Xd;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool file_exists(const string& name)
|
||||||
{
|
{
|
||||||
if (FILE* file = fopen(name.c_str(), "r")) {
|
if (FILE* file = fopen(name.c_str(), "r")) {
|
||||||
fclose(file);
|
fclose(file);
|
||||||
@@ -24,7 +49,40 @@ bool file_exists(const std::string& name)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tuple<vector<vector<int>>, vector<int>, vector<string>, string, map<string, vector<int>>> loadFile(string name)
|
tuple<Tensor, Tensor, vector<string>, string, map<string, vector<int>>> loadDataset(const string& path, const string& name, bool class_last, bool discretize_dataset)
|
||||||
|
{
|
||||||
|
auto handler = ArffFiles();
|
||||||
|
handler.load(path + static_cast<string>(name) + ".arff", class_last);
|
||||||
|
// Get Dataset X, y
|
||||||
|
vector<mdlp::samples_t>& X = handler.getX();
|
||||||
|
mdlp::labels_t& y = handler.getY();
|
||||||
|
// Get className & Features
|
||||||
|
auto className = handler.getClassName();
|
||||||
|
vector<string> features;
|
||||||
|
auto attributes = handler.getAttributes();
|
||||||
|
transform(attributes.begin(), attributes.end(), back_inserter(features), [](const auto& pair) { return pair.first; });
|
||||||
|
Tensor Xd;
|
||||||
|
auto states = map<string, vector<int>>();
|
||||||
|
if (discretize_dataset) {
|
||||||
|
auto Xr = discretizeDataset(X, y);
|
||||||
|
Xd = torch::zeros({ static_cast<int>(Xr[0].size()), static_cast<int>(Xr.size()) }, torch::kInt32);
|
||||||
|
for (int i = 0; i < features.size(); ++i) {
|
||||||
|
states[features[i]] = vector<int>(*max_element(Xr[i].begin(), Xr[i].end()) + 1);
|
||||||
|
iota(begin(states[features[i]]), end(states[features[i]]), 0);
|
||||||
|
Xd.index_put_({ "...", i }, torch::tensor(Xr[i], torch::kInt32));
|
||||||
|
}
|
||||||
|
states[className] = vector<int>(*max_element(y.begin(), y.end()) + 1);
|
||||||
|
iota(begin(states[className]), end(states[className]), 0);
|
||||||
|
} else {
|
||||||
|
Xd = torch::zeros({ static_cast<int>(X[0].size()), static_cast<int>(X.size()) }, torch::kFloat32);
|
||||||
|
for (int i = 0; i < features.size(); ++i) {
|
||||||
|
Xd.index_put_({ "...", i }, torch::tensor(X[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { Xd, torch::tensor(y, torch::kInt32), features, className, states };
|
||||||
|
}
|
||||||
|
|
||||||
|
tuple<vector<vector<int>>, vector<int>, vector<string>, string, map<string, vector<int>>> loadFile(const string& name)
|
||||||
{
|
{
|
||||||
auto handler = ArffFiles();
|
auto handler = ArffFiles();
|
||||||
handler.load(PATH + static_cast<string>(name) + ".arff");
|
handler.load(PATH + static_cast<string>(name) + ".arff");
|
||||||
@@ -34,9 +92,8 @@ tuple<vector<vector<int>>, vector<int>, vector<string>, string, map<string, vect
|
|||||||
// Get className & Features
|
// Get className & Features
|
||||||
auto className = handler.getClassName();
|
auto className = handler.getClassName();
|
||||||
vector<string> features;
|
vector<string> features;
|
||||||
for (auto feature : handler.getAttributes()) {
|
auto attributes = handler.getAttributes();
|
||||||
features.push_back(feature.first);
|
transform(attributes.begin(), attributes.end(), back_inserter(features), [](const auto& pair) { return pair.first; });
|
||||||
}
|
|
||||||
// Discretize Dataset
|
// Discretize Dataset
|
||||||
vector<mdlp::labels_t> Xd;
|
vector<mdlp::labels_t> Xd;
|
||||||
map<string, int> maxes;
|
map<string, int> maxes;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
#ifndef PLATFORM_UTILS_H
|
#ifndef PLATFORM_UTILS_H
|
||||||
#define PLATFORM_UTILS_H
|
#define PLATFORM_UTILS_H
|
||||||
|
#include <torch/torch.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -10,6 +11,11 @@ using namespace std;
|
|||||||
const string PATH = "../../data/";
|
const string PATH = "../../data/";
|
||||||
|
|
||||||
bool file_exists(const std::string& name);
|
bool file_exists(const std::string& name);
|
||||||
|
vector<string> split(const string& text, char delimiter);
|
||||||
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features);
|
pair<vector<mdlp::labels_t>, map<string, int>> discretize(vector<mdlp::samples_t>& X, mdlp::labels_t& y, vector<string> features);
|
||||||
tuple<vector<vector<int>>, vector<int>, vector<string>, string, map<string, vector<int>>> loadFile(string name);
|
vector<mdlp::labels_t> discretizeDataset(vector<mdlp::samples_t>& X, mdlp::labels_t& y);
|
||||||
|
pair<torch::Tensor, map<string, vector<int>>> discretizeTorch(torch::Tensor& X, torch::Tensor& y, vector<string>& features, const string& className);
|
||||||
|
tuple<vector<vector<int>>, vector<int>, vector<string>, string, map<string, vector<int>>> loadFile(const string& name);
|
||||||
|
tuple<torch::Tensor, torch::Tensor, vector<string>, string, map<string, vector<int>>> loadDataset(const string& path, const string& name, bool class_last, bool discretize_dataset);
|
||||||
|
map<string, vector<int>> get_states(vector<string>& features, string className, map<string, int>& maxes);
|
||||||
#endif //PLATFORM_UTILS_H
|
#endif //PLATFORM_UTILS_H
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
#ifndef TYPES_H
|
|
||||||
#define TYPES_H
|
|
||||||
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
|
||||||
#include <stdexcept>
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
namespace mdlp {
|
|
||||||
typedef float precision_t;
|
|
||||||
typedef vector<precision_t> samples_t;
|
|
||||||
typedef vector<int> labels_t;
|
|
||||||
typedef vector<size_t> indices_t;
|
|
||||||
typedef vector<precision_t> cutPoints_t;
|
|
||||||
typedef map<pair<int, int>, precision_t> cacheEnt_t;
|
|
||||||
typedef map<tuple<int, int, int>, precision_t> cacheIg_t;
|
|
||||||
}
|
|
||||||
#endif
|
|
@@ -2,8 +2,10 @@ if(ENABLE_TESTING)
|
|||||||
set(TEST_MAIN "unit_tests")
|
set(TEST_MAIN "unit_tests")
|
||||||
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
include_directories(${BayesNet_SOURCE_DIR}/src/BayesNet)
|
||||||
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
include_directories(${BayesNet_SOURCE_DIR}/src/Platform)
|
||||||
set(TEST_SOURCES BayesModels.cc BayesNetwork.cc ${BayesNet_SOURCES} ${Platform_SOURCES})
|
include_directories(${BayesNet_SOURCE_DIR}/lib/Files)
|
||||||
|
include_directories(${BayesNet_SOURCE_DIR}/lib/mdlp)
|
||||||
|
set(TEST_SOURCES BayesModels.cc BayesNetwork.cc ${BayesNet_SOURCE_DIR}/src/Platform/platformUtils.cc ${BayesNet_SOURCES})
|
||||||
add_executable(${TEST_MAIN} ${TEST_SOURCES})
|
add_executable(${TEST_MAIN} ${TEST_SOURCES})
|
||||||
target_link_libraries(${TEST_MAIN} PUBLIC "${TORCH_LIBRARIES}" Catch2::Catch2WithMain)
|
target_link_libraries(${TEST_MAIN} PUBLIC "${TORCH_LIBRARIES}" ArffFiles mdlp Catch2::Catch2WithMain)
|
||||||
add_test(NAME ${TEST_MAIN} COMMAND ${TEST_MAIN})
|
add_test(NAME ${TEST_MAIN} COMMAND ${TEST_MAIN})
|
||||||
endif(ENABLE_TESTING)
|
endif(ENABLE_TESTING)
|
||||||
|
Reference in New Issue
Block a user