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 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,5 @@
1
+ from .load_csv import load, preprocess_data
2
+ from .config_parser import conf_parser
3
+ from .data_generation import *
4
+
5
+ __all__ = [load, preprocess_data, conf_parser, generate_data_rand]
@@ -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,6 @@
1
+ from .activation_func import relu, leaky_relu, sigmoid, get_activation_funcs_by_name
2
+ from .nnClass import MultiLayerPerceptron
3
+ from .nnUtils import *
4
+
5
+
6
+ __all__ = ["MultiLayerPerceptron", relu, leaky_relu, sigmoid, split_dataset, get_activation_funcs_by_name]
@@ -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]