sw-mlp 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mlp/__init__.py +10 -0
- mlp/__main__.py +124 -0
- mlp/data_process/__init__.py +5 -0
- mlp/data_process/config_parser.py +137 -0
- mlp/data_process/data_generation.py +29 -0
- mlp/data_process/load_csv.py +58 -0
- mlp/neural_network/__init__.py +6 -0
- mlp/neural_network/activation_func.py +62 -0
- mlp/neural_network/nnClass.py +408 -0
- mlp/neural_network/nnUtils.py +135 -0
- sw_mlp-1.0.0.dist-info/METADATA +439 -0
- sw_mlp-1.0.0.dist-info/RECORD +14 -0
- sw_mlp-1.0.0.dist-info/WHEEL +4 -0
- sw_mlp-1.0.0.dist-info/licenses/LICENSE +21 -0
mlp/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .neural_network.activation_func import relu, leaky_relu, sigmoid, get_activation_funcs_by_name
|
|
2
|
+
from .neural_network.nnClass import MultiLayerPerceptron
|
|
3
|
+
from .neural_network.nnUtils import *
|
|
4
|
+
|
|
5
|
+
from .data_process.load_csv import load, preprocess_data
|
|
6
|
+
from .data_process.config_parser import conf_parser
|
|
7
|
+
from .data_process.data_generation import *
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ["MultiLayerPerceptron", relu, leaky_relu, sigmoid, split_dataset, get_activation_funcs_by_name, load, preprocess_data, conf_parser, generate_data_rand]
|
mlp/__main__.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
from numpy import ndarray as array
|
|
4
|
+
import numpy as np
|
|
5
|
+
from mlp.neural_network import MultiLayerPerceptron, get_activation_funcs_by_name
|
|
6
|
+
from mlp.data_process import load, preprocess_data, conf_parser, generate_data_rand
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_help() -> None:
|
|
11
|
+
"""Show help messages."""
|
|
12
|
+
|
|
13
|
+
print("-----------------------------------------------------------------------------------------------------------------")
|
|
14
|
+
print("Multilayer perceptron")
|
|
15
|
+
print("Usage:")
|
|
16
|
+
print(" uv run -m mlp <option> <config.json> <data.csv> [params.json]")
|
|
17
|
+
print("Options:")
|
|
18
|
+
print(" -s: Split dataset into (train.csv) and (test.csv), no params required")
|
|
19
|
+
print(" -t: Train dataset, if params.json is provided, it performs fine tuning.")
|
|
20
|
+
print(" -p: Test on a test dataset, if params.json is provided, it performs prediction based on trained model.")
|
|
21
|
+
print(" --help: Show help messages.")
|
|
22
|
+
print(" -More features to come.")
|
|
23
|
+
print("Note:")
|
|
24
|
+
print(" There is built in dataset generated by script, use --gen-data1d to replace dataset")
|
|
25
|
+
print("-----------------------------------------------------------------------------------------------------------------")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mlp_train(nn: MultiLayerPerceptron, conf: dict, inputs: array, truths: array) -> None:
|
|
29
|
+
"""Train a model with traing data and configuration."""
|
|
30
|
+
|
|
31
|
+
nn.train(inputs, truths,
|
|
32
|
+
conf["max_epoch"],
|
|
33
|
+
conf["learning_rate"],
|
|
34
|
+
batch_size=conf["batch_size"],
|
|
35
|
+
test_ratio=conf["train_ratio"],
|
|
36
|
+
threshold=conf["threshold"],
|
|
37
|
+
animation=conf["animation"])
|
|
38
|
+
nn.save_plots()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mlp_getdata(source: str, conf: dict) -> tuple:
|
|
42
|
+
"""Get data either from input dataset file or from internal options."""
|
|
43
|
+
|
|
44
|
+
if source == "--gen-data1d":
|
|
45
|
+
inputs, truths = generate_data_rand(142, 500, 0.02)
|
|
46
|
+
else:
|
|
47
|
+
df = load(source, False)
|
|
48
|
+
if df is None:
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
inputs, truths = preprocess_data(df)
|
|
51
|
+
return inputs, truths
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def mlp_splitdata(conf:dict, file:str, ratio:float=0.8) -> None:
|
|
55
|
+
"""Split dataset into 2 parts, training data and test / validation data."""
|
|
56
|
+
|
|
57
|
+
df = load(file, conf["index"])
|
|
58
|
+
if df is None:
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
seed = conf["seed"]
|
|
61
|
+
df_shuffled = df.sample(frac=1, random_state=seed).reset_index(drop=True)
|
|
62
|
+
|
|
63
|
+
l = len(df_shuffled)
|
|
64
|
+
train_data = df_shuffled[:int(ratio * l)]
|
|
65
|
+
test_data = df_shuffled[int(ratio * l):]
|
|
66
|
+
print("[Original Data]", df.shape)
|
|
67
|
+
print("[Train data]", train_data.shape)
|
|
68
|
+
print("[Test data]", test_data.shape)
|
|
69
|
+
train_data.to_csv("data/train.csv", index=False, encoding="utf-8")
|
|
70
|
+
test_data.to_csv("data/test.csv", index=False, encoding="utf-8")
|
|
71
|
+
print("[Save splited data into (data/train.csv) (data/test.csv)]")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def mlp_create_nn(argv:list) -> tuple:
|
|
75
|
+
"""Initialize neural network instance."""
|
|
76
|
+
|
|
77
|
+
conf = conf_parser(argv[2])
|
|
78
|
+
if conf is None:
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
np.random.seed(conf["seed"])
|
|
81
|
+
nn = MultiLayerPerceptron(conf["shape"], get_activation_funcs_by_name(conf["activation_funcs"]),
|
|
82
|
+
conf["weights_init"],
|
|
83
|
+
classification=conf["classification"],
|
|
84
|
+
loss=conf["loss"])
|
|
85
|
+
inputs, truths = mlp_getdata(argv[3], conf)
|
|
86
|
+
if len(argv) == 5:
|
|
87
|
+
nn.load_weights(argv[4])
|
|
88
|
+
return nn, inputs, truths, conf
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main():
|
|
92
|
+
try:
|
|
93
|
+
argv = sys.argv
|
|
94
|
+
if len(argv) == 1 or ((len(argv) == 2 and argv[1] == "--help")):
|
|
95
|
+
print_help()
|
|
96
|
+
elif argv[1] == "-s" and len(argv) == 4:
|
|
97
|
+
conf = conf_parser(argv[2])
|
|
98
|
+
if conf is None:
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
mlp_splitdata(conf, argv[3], conf["train_ratio"])
|
|
101
|
+
elif len(argv) == 4 or len(argv) == 5:
|
|
102
|
+
nn, inputs, truths, conf = mlp_create_nn(argv)
|
|
103
|
+
if argv[1] == "-t":
|
|
104
|
+
mlp_train(nn, conf, inputs, truths)
|
|
105
|
+
elif argv[1] == "-p":
|
|
106
|
+
nn.test(inputs, truths)
|
|
107
|
+
# nn.show_test_img(inputs, (28, 28), 8329)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("Wrong arguments. Try: uv run -m mlp --help")
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError("Wrong arguments. Try: uv run -m mlp --help")
|
|
112
|
+
|
|
113
|
+
except KeyboardInterrupt as e:
|
|
114
|
+
print()
|
|
115
|
+
print("Stopped by user.\033[?25h")
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print("Error:", e)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def wrong_type(conf:dict, field: str, field_type: type, mandatory:bool) -> bool:
|
|
8
|
+
"""Check if a filed in configuration file has right type"""
|
|
9
|
+
|
|
10
|
+
if mandatory and not field in conf:
|
|
11
|
+
raise ValueError("Missing mandatory field.")
|
|
12
|
+
if field in conf and isinstance(conf[field], field_type):
|
|
13
|
+
return False
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_arg_type(conf: dict) -> bool:
|
|
18
|
+
"""Check argument type in json configuration file"""
|
|
19
|
+
|
|
20
|
+
if wrong_type(conf, "shape", (list,tuple), True):
|
|
21
|
+
return False
|
|
22
|
+
elif wrong_type(conf, "activation_funcs", (list,tuple), True):
|
|
23
|
+
return False
|
|
24
|
+
elif wrong_type(conf, "weights_init", (list,tuple), True):
|
|
25
|
+
return False
|
|
26
|
+
elif wrong_type(conf, "seed", int, True):
|
|
27
|
+
return False
|
|
28
|
+
elif wrong_type(conf, "loss", str, True):
|
|
29
|
+
return False
|
|
30
|
+
elif wrong_type(conf, "max_epoch", int, True):
|
|
31
|
+
return False
|
|
32
|
+
elif wrong_type(conf, "learning_rate", float, True):
|
|
33
|
+
return False
|
|
34
|
+
elif wrong_type(conf, "batch_size", int, True):
|
|
35
|
+
return False
|
|
36
|
+
elif wrong_type(conf, "classification", bool, True):
|
|
37
|
+
return False
|
|
38
|
+
elif wrong_type(conf, "animation", str, True):
|
|
39
|
+
return False
|
|
40
|
+
elif wrong_type(conf, "train_ratio", float, True):
|
|
41
|
+
return False
|
|
42
|
+
elif wrong_type(conf, "threshold", (bool,float), True):
|
|
43
|
+
return False
|
|
44
|
+
elif wrong_type(conf, "index", bool, True):
|
|
45
|
+
return False
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def valid_net_struct(conf: dict) -> bool:
|
|
50
|
+
"""Check if the neural network has valid structure."""
|
|
51
|
+
|
|
52
|
+
shape = conf["shape"]
|
|
53
|
+
activ_funcs = conf["activation_funcs"]
|
|
54
|
+
loss = conf["loss"]
|
|
55
|
+
init = conf["weights_init"]
|
|
56
|
+
|
|
57
|
+
if len(shape) < 4:
|
|
58
|
+
raise ValueError("Minimum layer is 4.")
|
|
59
|
+
if len(shape) != len(activ_funcs) + 1:
|
|
60
|
+
raise ValueError("Mismatched shape and activation funcions.")
|
|
61
|
+
for num in shape:
|
|
62
|
+
if not isinstance(num, int) or num < 1:
|
|
63
|
+
raise ValueError("Wrong layer neuron numbers")
|
|
64
|
+
for act in activ_funcs:
|
|
65
|
+
if act not in ["relu", "sigmoid", "leaky_relu", "softmax", "none"]:
|
|
66
|
+
raise ValueError("Wrong activation funcion")
|
|
67
|
+
if loss not in ["CategoricalCrossEntropy", "MeanSquareError", "BinaryCrossEntropy"]:
|
|
68
|
+
raise ValueError("Wrong loss function")
|
|
69
|
+
if (loss == "CategoricalCrossEntropy" or loss == "BinaryCrossEntropy") and activ_funcs[-1] not in ["sigmoid", "softmax"]:
|
|
70
|
+
raise ValueError("CrossEntropy loss only accept sigmoid or softmax as last layer activation function")
|
|
71
|
+
|
|
72
|
+
for init_method in init:
|
|
73
|
+
if not init_method in ["he", "xavier", "zero"]:
|
|
74
|
+
raise ValueError("Not supported initialization method")
|
|
75
|
+
if len(init) != len(activ_funcs):
|
|
76
|
+
raise ValueError("Wrong initialization method numbers")
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_arg_value(conf: dict) -> bool:
|
|
81
|
+
"""Check configuration value."""
|
|
82
|
+
|
|
83
|
+
if conf["max_epoch"] <= 0:
|
|
84
|
+
raise ValueError("Max epoch must be positive")
|
|
85
|
+
if conf["learning_rate"] <= 0:
|
|
86
|
+
raise ValueError("Learning rate must be positive")
|
|
87
|
+
if conf["batch_size"] <= 0:
|
|
88
|
+
raise ValueError("Batch size must be positive")
|
|
89
|
+
if conf["train_ratio"] <= 0 or conf["train_ratio"] >= 1:
|
|
90
|
+
raise ValueError("Invalid train ratio")
|
|
91
|
+
if conf["seed"] <= 0:
|
|
92
|
+
raise ValueError("Invalid seed.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_valid_config(conf: dict) -> bool:
|
|
96
|
+
"""Check if configuration file is correct."""
|
|
97
|
+
|
|
98
|
+
if check_arg_type(conf) == False:
|
|
99
|
+
return False
|
|
100
|
+
if valid_net_struct(conf) == False:
|
|
101
|
+
return False
|
|
102
|
+
check_arg_value(conf)
|
|
103
|
+
if conf["animation"] != "none" and conf["animation"] != "scatter" and conf["animation"] != "plot":
|
|
104
|
+
raise ValueError("Wrong animation type")
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def conf_parser(config: str):
|
|
109
|
+
"""Parse the configuration file and check errors."""
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with open(config, mode='r') as conf_data:
|
|
113
|
+
conf = json.load(conf_data)
|
|
114
|
+
|
|
115
|
+
if not is_valid_config(conf):
|
|
116
|
+
raise ValueError("Wrong config file.")
|
|
117
|
+
|
|
118
|
+
print(f"\033[33m-------------------------------------------------------------")
|
|
119
|
+
print(f"|Load config from file {config}")
|
|
120
|
+
for (key, value) in conf.items():
|
|
121
|
+
print("|", key, ":", value)
|
|
122
|
+
print(f"-------------------------------------------------------------\033[0m")
|
|
123
|
+
return conf
|
|
124
|
+
|
|
125
|
+
except Exception as e:
|
|
126
|
+
print("Error in configuration:", e)
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main():
|
|
131
|
+
conf = conf_parser(sys.argv[1])
|
|
132
|
+
if conf is None:
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_data_rand(seed, number, noise_scale=0.2):
|
|
8
|
+
"""Generate 1d data with added random noise."""
|
|
9
|
+
|
|
10
|
+
np.random.seed(seed)
|
|
11
|
+
inputs = []
|
|
12
|
+
truths = []
|
|
13
|
+
for _ in range(number):
|
|
14
|
+
x = np.random.rand()
|
|
15
|
+
inputs.append(np.array([x]))
|
|
16
|
+
# y = 0.5 * x**2 + 0.3 * np.sin(2 * np.pi * x) + 0.2 * np.exp(-2*x)
|
|
17
|
+
# y = 2*x
|
|
18
|
+
# y = x * math.sin(x)
|
|
19
|
+
# y = x * (1 - math.exp(-x**2))
|
|
20
|
+
y = math.sin(2*x) * math.exp(-0.1*x) + 0.5 * math.cos(5*x)
|
|
21
|
+
# y = 0.5 * x**2 + 0.3 * np.sin(2 * np.pi * x)
|
|
22
|
+
# y = - 0.5 * (x - 0.5)**2 + 0.3
|
|
23
|
+
noise = np.random.normal(0, noise_scale)
|
|
24
|
+
y_noisy = y + noise
|
|
25
|
+
truths.append(np.array([float(y_noisy)]))
|
|
26
|
+
|
|
27
|
+
inputs = np.array(inputs)
|
|
28
|
+
truths = np.array(truths)
|
|
29
|
+
return inputs, truths
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_to_float(type_name:str):
|
|
9
|
+
"""Convert labels to number."""
|
|
10
|
+
|
|
11
|
+
if type_name == "M" or type_name=="1" or type_name == "1.0":
|
|
12
|
+
return 1.0
|
|
13
|
+
elif type_name == "B" or type_name=="0" or type_name == "0.0":
|
|
14
|
+
return 0.0
|
|
15
|
+
elif isinstance(type_name, int):
|
|
16
|
+
return type_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def preprocess_data(df: pd.DataFrame):
|
|
20
|
+
"""Preprocess data set to prepare inputs and label truths."""
|
|
21
|
+
|
|
22
|
+
df.iloc[:, 0] = df.iloc[:, 0].apply(convert_to_float)
|
|
23
|
+
data = np.array(df).astype(np.float32)
|
|
24
|
+
# truths and inputs
|
|
25
|
+
truths = data[:, 0] # (N,)
|
|
26
|
+
inputs = data[:, 1:] # (N,M)
|
|
27
|
+
# nomalize col
|
|
28
|
+
X_min = inputs.min(axis=0)
|
|
29
|
+
X_max = inputs.max(axis=0)
|
|
30
|
+
inputs_norm = (inputs - X_min) / (X_max - X_min + 1e-8)
|
|
31
|
+
truths = truths[:, np.newaxis] # (N,V)
|
|
32
|
+
inputs = inputs_norm[:, :] # (N,M)
|
|
33
|
+
print("[INPUTS]", inputs.shape) # (N,M)
|
|
34
|
+
print("[TRUTHS]", truths.shape) # (N,V)
|
|
35
|
+
return inputs, truths
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load(path: str, index: bool) -> pd.DataFrame:
|
|
39
|
+
"""Load a csv file and return it's pandas.DataFrame object."""
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
assert isinstance(path, str), "Wrong file path type."
|
|
43
|
+
assert os.path.exists(path), "Data file not exists."
|
|
44
|
+
assert path.endswith(".csv"), "Not csv data."
|
|
45
|
+
|
|
46
|
+
if index:
|
|
47
|
+
df = pd.read_csv(path, index_col=0)
|
|
48
|
+
else:
|
|
49
|
+
df = pd.read_csv(path)
|
|
50
|
+
return df
|
|
51
|
+
|
|
52
|
+
except AssertionError as e:
|
|
53
|
+
print("AssertionError:", e)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print("Error:", e)
|
|
58
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
|
|
3
|
+
from numpy import ndarray as array
|
|
4
|
+
import math
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def relu(value: array):
|
|
9
|
+
return np.maximum(0, value)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def relu_deriv(value: array):
|
|
13
|
+
return (value > 0).astype(float)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def leaky_relu(value: array):
|
|
17
|
+
return np.maximum(0.02 * value, value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def leaky_relu_deriv(value: array):
|
|
21
|
+
return np.where(value > 0, 1.0, 0.02)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def sigmoid(value: array):
|
|
25
|
+
return 1.0 / (1.0 + math.e ** -value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def softmax(value: array):
|
|
29
|
+
exp = np.exp(value - np.max(value, axis=0, keepdims=True))
|
|
30
|
+
sum = np.sum(exp, axis=0, keepdims=True)
|
|
31
|
+
soft = exp / sum
|
|
32
|
+
return soft
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sigmoid_deriv(value: array):
|
|
36
|
+
return value * (1 - value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def activ_deriv(active_func: callable, value:array, deriv_map: dict):
|
|
40
|
+
"""Get the derivative of an activation function."""
|
|
41
|
+
|
|
42
|
+
if active_func is None:
|
|
43
|
+
return 1
|
|
44
|
+
if active_func == softmax:
|
|
45
|
+
raise ValueError("Softmax derivative not implemented")
|
|
46
|
+
deriv_func = deriv_map[active_func]
|
|
47
|
+
if deriv_func is None:
|
|
48
|
+
return 1
|
|
49
|
+
return deriv_func(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_activation_funcs_by_name(activs):
|
|
53
|
+
"""Get activation funcs by name, convert string to actual function."""
|
|
54
|
+
|
|
55
|
+
activmap = {
|
|
56
|
+
"relu":relu,
|
|
57
|
+
"sigmoid": sigmoid,
|
|
58
|
+
"softmax": softmax,
|
|
59
|
+
"leaky_relu": leaky_relu,
|
|
60
|
+
"none": None
|
|
61
|
+
}
|
|
62
|
+
return [activmap[a] for a in activs]
|