diff --git a/.gitignore b/.gitignore index 42b3897..c975981 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ venv/* # Miscellaneous Scripts # ######################### activate.bat +tmpscript.py \ No newline at end of file diff --git a/pymind/__init__.py b/pymind/__init__.py index ab4d026..dc648bf 100644 --- a/pymind/__init__.py +++ b/pymind/__init__.py @@ -4,3 +4,4 @@ import activationfn import errfn import util +import datautil diff --git a/pymind/datautil.py b/pymind/datautil.py new file mode 100644 index 0000000..3344753 --- /dev/null +++ b/pymind/datautil.py @@ -0,0 +1,312 @@ +import numpy as np +import json +import scipy.io +from pymind.activationfn import * +from pymind.components import * + +load_routines = {} +save_routines = {} + +""" +Builder class for training data. Used to construct a dataset from scratch. +""" +class DatasetBuilder(object): + + def __init__(self, icount, ocount): + """ Constructs a new Datasetbuilder. + + Parameters: + icount, the number of inputs to the neural network + ocount, the number of outputs from the neural network + """ + self.X = [list() for _ in xrange(icount)] + self.y = [list() for _ in xrange(ocount)] + self.icount = icount + self.ocount = ocount + + def add(self, ivec, ovec): + """ Adds a datapoint to this DatasetBuilder. + + Parameters: + ivec, a vector (list or array) of input features. Must be the same length as self.icount + ovec, a vector (list or array) of output values. Must be the same length as self.ocount + """ + assert len(ivec) == self.icount, "Vector does not match input data." + assert len(ovec) == self.ocount, "Vector does not match output data." + for k, data in enumerate(ivec): + self.X[k].append(data) + for k, data in enumerate(ovec): + self.y[k].append(data) + + def build(self): + """ Returns a dictionary containing matrices X and y, consisting of the training data added to + DatasetBuilder. X is mapped to an xa by xb array, where xa is the number of inputs and xb is + the number of training samples. y is mapped to an ya by yb array, where ya is the number of + outputs and yb is the number of training samples. + """ + return {"X":np.matrix(self.X), "y":np.matrix(self.y)} + +def save_data(fname, data, format=None): + """ Given a file name "fname", format "format" and a dataset "data", attempts to save data to file + formatted using "format" such that it can be loaded using load_data. If format is not specified, + attempts to search the file name for an extension. + + Parameters: + fname, the name of the target file + data, the dataset to save + format, the format of the output file + """ + if format is None: + dot = fname.rfind(".") + if dot != -1: + format = fname[dot+1:] + else: + raise RuntimeError("Please specify a format for file " + fname) + elif len(format) > 0 and format[0]==".": + format = format[1:] + if format in load_routines: + return save_routines[format](fname, data) + else: + raise RuntimeError("Unrecognized file format \"" + "." + format + "\"") + +def __save_json_data(fname, data): + """ Given a file name "fname" and a dataset "data", saves data to .json such that it can be + loaded using load_data or __load_json_data. + + Parameters: + fname, the name of the target file + data, the dataset to save + """ + if ".json" != fname[-5:]: + fname = fname + ".json" + fout = open(fname, "w") + out = {"X":[], "y":[]} + for x in data["X"]: + d = [] + for i in xrange(x.shape[1]): + d.append(float(x[0, i])) + out["X"].append(d) + for y in data["y"]: + d = [] + for i in xrange(y.shape[1]): + d.append(float(y[0, i])) + out["y"].append(d) + enc = json.JSONEncoder() + out = enc.encode(out) + fout.write(out) + fout.close() +save_routines["json"] = __save_json_data + +def __save_mat_data(fname, data): + """ Given a file name "fname" and a dataset "data", saves data to .mat such that it can be + loaded using load_data or __load_mat_data. + + Parameters: + fname, the name of the target file + data, the dataset to save + """ + if ".mat" != fname[-5:]: + fname = fname + ".mat" + scipy.io.savemat(fname, data, oned_as="row") +save_routines["mat"] = __save_mat_data + +def load_data(fname, format=None): + """ Given a file name "fname" and a string "format" indicating the file format, attempts to load + and return the training data contained within the file. If no format is specified, attempts to + search the file name for an extension. + + Parameters: + fname, the name of a file containing a training dataset + format, the format of the input file + """ + if format is None: + dot = fname.rfind(".") + if dot != -1: + format = fname[dot+1:] + else: + raise RuntimeError("Please specify a format for file " + fname) + elif len(format) > 0 and format[0]==".": + format = format[1:] + if format in load_routines: + return load_routines[format](fname) + else: + raise RuntimeError("Unrecognized file format \"" + "." + format + "\"") + +def __load_json_data(fname): + """ Converts a JSON training dataset into Numpy matrix format. + + Parameters: + fname, the name of a JSON file consisting of 2 keys: "X" which binds to an array of arrays + representing the list of input vectors and "y" which binds to an array of arrays representing + the list of output vectors. + """ + if ".json" != fname[-5:]: + fname = fname + ".json" + jsfile = open(fname) + ds = json.load(jsfile) + jsfile.close() + X, y = np.matrix(ds[u"X"]), np.matrix(ds[u"y"]) + return {"X":X, "y":y} +load_routines["json"] = __load_json_data + +def __load_mat_data(fname): + """ Converts a matlab training dataset into Numpy matrix format. + + Parameters: + fname, the name of a matlab file consisting of 2 keys: "X" which binds to an array of arrays + representing the list of input vectors and "y" which binds to an array of arrays representing + the list of output vectors. + """ + ds = scipy.io.loadmat(fname) + X, y = np.matrix(ds["X"]), np.matrix(ds["y"]) + return {"X":X, "y":y} +load_routines["mat"] = __load_mat_data + +def split_data(X, y=None, parts=2): + """ Randomly partitions a set of training data into multiple parts + + Parameters: + X, a matrix representing the inputs for the training data. Alternately, could be a dictionary + containing both "X" and "y" as keys mapped to matrices + y, a matrix representing the outputs for the training data + parts, the number of parts into which the training data will be split, or a list indicating the + proportions of each part into which we split the data + """ + if y is None and type(X) is dict: + y = X["y"] + X = X["X"] + if hasattr(parts, "__len__"): + kparts = reduce(lambda x, y:x+y, parts) + dsparts, dsets = split_data(X, y , kparts), [] + for part in parts: + head, dsparts = dsparts[:part], dsparts[part:] + dsets.append({"X":np.hstack([head[i]["X"] for i in xrange(part)]), + "y":np.hstack([head[i]["y"] for i in xrange(part)])}) + return dsets + else: + scount = int(X.shape[1]) + assert scount==y.shape[1], "Invalid dataset, number of inputs must match number of outputs" + a = np.arange(scount) + np.random.shuffle(a) + start, inc = 0.0, scount/parts + end, dsets = inc, [] + for _ in xrange(parts): + indices = a[round(start):round(end)] + dsets.append({"X":X[:, indices], "y":y[:, indices]}) + start = end + end += inc + return dsets + +def __matrixToList(mtx): + """ Converts a numpy matrix into a 2D Python list. """ + arr = [] + for row in mtx: + arr.append([t[1] for t in np.ndenumerate(row)]) + return arr + +def save_neural_net(fname, nnet, format="json"): + """ Given a file name, neural network and a format serializes a neural network into the specified + format. File contains the following information: the size of each hidden layer, number of input + units, number of output units, each layer"s activation function, whether or not the network is + biased, and the weight of each link in the network. + + Parameters: + fname, the name of the file (may include an extension) + nnet, the neural network to serialize + format, the file format to use + """ + if format == "json" or ".json" == fname[-5:]: + __save_json_neural_net(fname, nnet) + elif format == "mat" or ".mat" == fname[-4:]: + __save_mat_neural_net(fname, nnet) + +def __save_json_neural_net(fname, nnet): + """ Given a file name and neural network, serializes the neural network as a json file. See doc + for save_neural_net for more information. + + Parameters: + fname, the name of the file + nnet, the neural network to serialize + """ + obj = {} + obj["hidden_units"] = nnet.hidden_units + obj["input_units"] = nnet.input_units + obj["output_units"] = nnet.output_units + obj["bias"] = nnet.bias + aflist = [] + for af in nnet.activationfn: + if af is sigmoid: + aflist.append("sigmoid") + elif af is identity: + aflist.append("identity") + else: + aflist.append("unknown") + obj["activationfn"] = aflist + obj["weights"] = [__matrixToList(t) for t in nnet.weights] + enc = json.JSONEncoder() + out = enc.encode(obj) + if ".json" not in fname[-5:]: + fname = fname + ".json" + fout = open(fname, "w") + fout.write(out) + fout.close() + +def __save_mat_neural_net(fname, nnet): + """ Given a file name and neural network, serializes the neural network as a mat file. See doc + for save_neural_net for more information. + + Parameters: + fname, the name of the file + nnet, the neural network to serialize + """ + raise NotImplementedError("Saving neural networks to .mat files is not yet supported.") + +def load_neural_net(fname, format="json"): + """ Given a file name "fname" and a string "format" indicating the file format, attempts to load + and return the neural network contained within the file. If no format is specified, attempts to + search the file name for an extension. + + Parameters: + fname, the name of a file containing a training dataset + format, the format of the input file + """ + if format == "json" or ".json" == fname[-5:]: + return __load_json_neural_net(fname) + elif format == "mat" or ".mat" == fname[-4:]: + return __load_mat_neural_net(fname) + +def __load_json_neural_net(fname): + """ Given a file name, deserializes the neural network as a json file. See doc for load_neural_net + for more information. + + Pameters: + fname, the name of the file + """ + if ".json" not in fname[-5:]: + fname = fname + ".json" + fin = open(fname) + rawstr = fin.read() + dec = json.JSONDecoder() + obj = dec.decode(rawstr) + params = {} + params["hidden_units"] = obj["hidden_units"] + params["input_units"] = obj["input_units"] + params["output_units"] = obj["output_units"] + params["bias"] = obj["bias"] + try: + # type of each afname in obj is unicode, not str + params["activationfn"] = [get(str(afname)) for afname in obj["activationfn"]] + except AssertionError: + raise RuntimeError("Error: Loading custom activation functions is not yet supported.") + nnet = NeuralNetwork(params) + nnet.weights = [np.matrix(t) for t in obj["weights"]] + return nnet + +def __load_mat_neural_net(fname): + """ Given a file name, deserializes the neural network as a mat file. See doc for load_neural_net + for more information. + + Parameters: + fname, the name of the file + """ + raise NotImplementedError("Loading neural networks from .mat files is not yet supported.") diff --git a/pymind/metricfn.py b/pymind/metricfn.py new file mode 100644 index 0000000..55d036b --- /dev/null +++ b/pymind/metricfn.py @@ -0,0 +1,73 @@ +""" Package of common metric functions, as well as combiner functions. + +An metric is a function that takes a neural network and extracts information (i.e. metrics) from the +neural network, returning the information as any datatype. + +A combiner function is a function that takes a running result of calling metric functions and the +result of the latest call to a metric function, and combines them, returning the resulting object. +""" + +import numpy as np +from util import assertType + +_metrics = dict() +def get_metric(name): + """ Gets the metric function corresponding to this name. If the name corresponds to no function, + raises an exception. + + Arguments: + name -- a string representing the name of this metric + Returns: + a metric mapped from the given name + """ + assertType("metricfn.get_metric", "name", name, str) + assert name in _metrics, "(metricfn) %s cannot be found." % name + return _metrics[name] + +def set_metric(name, fn): + """ Sets the metric function corresponding using this name. Overwrites the function if the name + already maps to a function. + + Arguments: + name -- a string representing the name of this metric + fn -- a function that takes a NeuralNetwork and returns some value derived from the NeuralNetwork + """ + assertType("metricfn.set_metric", "name", name, str) + _metrics[name] = fn + +_combiners = dict() +def get_combiner(name): + """ Gets the combiner function corresponding to this name. If the name corresponds to no function, + raises an exception. + + Arguments: + name -- a string representing the name of this combiner + Returns: + a combiner mapped from the given name + """ + assertType("metricfn.get_combiner", "name", name, str) + assert name in _combiners, "(metricfn) %s cannot be found." % name + return _combiners[name] + +def set_combiner(name, fn): + """ Sets the combiner function corresponding using this name. If the name already maps to a + function, overwrites the function. + + Arguments: + name -- a string representing the name of this combiner + fn -- a function that takes a total and a result and returns the combination of the two + """ + assertType("metricfn.set_combiner", "name", name, str) + _combiners[name] = fn + +def __list_combiner(total, res): + """ Returns total concatenated with res. If total is None, returns res as a single element list. + This is the default combiner function. + """ + if total is None: + return [res,] + else: + # using list.append would mutate total. Is this what we want? + return total + [res,] + +set_combiner("list_combiner",__list_combiner) diff --git a/pymind/training.py b/pymind/training.py index 616d673..3c551c0 100644 --- a/pymind/training.py +++ b/pymind/training.py @@ -1,7 +1,11 @@ """ Several methods that train a neural network given a training dataset. """ import numpy as np +import pymind from collections import deque +from pymind.errfn import squaredError +from pymind.metricfn import * +from pymind.components import NeuralNetwork from util import unroll_matrices, reshape_vector def train(nnet, X, y, learn_rate, errfn, minimizer, iterations = 10): @@ -154,3 +158,48 @@ def costfn(weights): print "Calculating cost of a neural network failed. Most likely due to dimension mismatch." raise return costfn + +def train_suites(suites,metric,combiner=get_combiner("list_combiner")): + """ Given a list of suites that can be used to construct and train neural networks, trains each + neural network and runs callback 'metric' on the resulting neural network, combining the metrics + returned by each callback using the reduce function 'combiner'. + + Arguments: + suites -- an iterator of suites (dictionaries containing information necessary to construct and + train neural networks. See nnbuilder.py for more information) + metric -- a metric function wrapper used to extract metrics from trained neural networks. See + metricfn.py for more information + combiner -- a combiner function wrapper used to combine results from multiple calls to metric + functions. See metricfn.py for more information. The default combiner (ListCombiner) concat- + enates all results of calling the metric function in one resulting list. + + Returns: + The final result of combining calls to the metric function using the combiner function. By def- + ault, this is a list. + """ + final_result = None + for suite in suites: + X = suite['X'] + y = suite['y'] + actfns = [pymind.activationfn.get(s) for s in suite['activationfn']] + layer_units = suite['layer_units'] + hiddencount = layer_units[1:-1] + incount = layer_units[0] + outcount = layer_units[-1] + learn_rate = suite['learn_rate'] + errfn = pymind.errfn.get(suite['errfn']) + minimizer = suite['minimizer'] + it = suite['iterations'] + bias = suite['bias'] + params = { + 'input_units': incount, + 'output_units': outcount, + 'hidden_units': hiddencount, + 'activationfn': actfns, + 'bias': bias + } + nnet = NeuralNetwork(params) + res = train(nnet, X, y, learn_rate, errfn, minimizer, iterations=it) + iter_result = metric(nnet,res) + final_result = combiner(final_result, iter_result) + return final_result diff --git a/test/datasets/ds-4-2_00.json b/test/datasets/ds-4-2_00.json new file mode 100644 index 0000000..647cc3d --- /dev/null +++ b/test/datasets/ds-4-2_00.json @@ -0,0 +1 @@ +{"y": [[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0]], "X": [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0], [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0]]} \ No newline at end of file diff --git a/test/datasets/ds-4-2_00.mat b/test/datasets/ds-4-2_00.mat new file mode 100644 index 0000000..a15811b Binary files /dev/null and b/test/datasets/ds-4-2_00.mat differ diff --git a/test/datasets/nnet-54433_00.json b/test/datasets/nnet-54433_00.json new file mode 100644 index 0000000..ade8e14 --- /dev/null +++ b/test/datasets/nnet-54433_00.json @@ -0,0 +1 @@ +{"input_units": 5, "hidden_units": [4, 4, 3], "output_units": 3, "activationfn": ["identity", "sigmoid", "sigmoid", "sigmoid", "sigmoid"], "bias": false, "weights": [[[0.61131075610477981, 0.16235697774252089, 0.13492150791612523, 0.026142450896560865, 0.56953361600280317], [-0.056080781336570573, -0.77550915059533698, -0.49367381709661273, 0.16729205880094344, 0.50086521326932487], [-0.44603194522073741, 0.27283526589940033, -0.44072928880170048, -0.43251691061930442, -0.0028039854649882789], [-0.70143192794741871, 0.52641715766220043, 0.49488403212324739, 0.20234823399708179, -0.47280412871819394]], [[-0.3447939948690576, 0.040387037653220381, 0.13304347989508569, 0.19013637282783147], [0.43536920960132897, 0.12490448129080445, -0.59418804668311898, 0.019080582521416423], [0.24664044168298804, 0.38746584919448013, 0.86303889073706419, 0.37203726245402169], [-0.80575136271791792, -0.74566892844529165, -0.20194092224392857, -0.17014259455707748]], [[0.18116234906323092, -0.49844606643861306, 0.66393105143551334, 0.20099945218946214], [-0.77632668683548811, -0.50581461675684214, -0.81717374683067845, 0.80758687265858897], [-0.47080734171343641, -0.0090060249389148916, -0.29294564202325746, 0.84888455537462126]], [[-0.4731776777118577, -0.71257487040841494, -0.99016954395038992], [-0.67810591726690483, 0.97264791814182239, 0.83000305746880421], [0.63112332325791165, -0.37417653266186202, -0.58773452376774937]]]} \ No newline at end of file diff --git a/test/datautil_test.py b/test/datautil_test.py new file mode 100644 index 0000000..31f9d37 --- /dev/null +++ b/test/datautil_test.py @@ -0,0 +1,101 @@ +import numpy as np +from pymind.datautil import * +from os import popen + +ds_orig = {"y": np.matrix([[1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1]]), + "X": np.matrix([[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0]])} + +params = { + "input_units": 5, + "output_units": 3, + "hidden_units": [4, 4, 3], + "activationfn": [identity, sigmoid, sigmoid, sigmoid, sigmoid], + "bias": False +} +nnet00 = NeuralNetwork(params) + +__check_shape = lambda s,t : (s.shape == t) if type(t) is tuple else (s.shape == t.shape) +__check_diff = lambda s,t : (s - t).sum() == 0 + +def __check_nnet(nnet1, nnet2, msg): + assert len(nnet1.weights) == len(nnet2.weights), msg + " (mismatched number of layers)" + for i in xrange(len(nnet1.weights)): + w1, w2 = nnet1.weights[i], nnet2.weights[i] + assert __check_shape(w1, w2), msg + " (shapes of neural network weights are mismatched)" + assert __check_diff(w1, w2), msg + " (values of neural network weights are mismatched)" + +def __check_dsets(ds1,ds2,msg_X="Input failure",msg_y="Output failure"): + assert (not (ds1["X"] != ds2["X"]).sum()),msg_X + assert (not (ds1["y"] != ds2["y"]).sum()),msg_y + +def test_builder(): + dsb = DatasetBuilder(4,2) + dsb.add([1,1,0,0],[1,0]) + dsb.add([1,0,0,0],[0,0]) + dsb.add([1,0,0,1],[0,1]) + dsb.add([1,0,1,1],[0,1]) + dsb.add([1,1,1,1],[1,1]) + dsb.add([1,0,0,0],[0,0]) + dsb.add([0,0,0,0],[0,1]) + dsb.add([0,0,0,1],[1,1]) + dsb.add([0,1,0,1],[0,1]) + dsb.add([0,1,1,1],[1,1]) + dsb.add([0,1,1,0],[0,0]) + dsb.add([1,1,1,0],[0,1]) + ds = dsb.build() + __check_dsets(ds_orig,ds,"Failure loading input data from .mat file at ds-4-2_00", + "Failure loading output data from .mat file at ds-4-2_00") + +def test_load_data_mat(): + ds_loaded = load_data("test/datasets/ds-4-2_00.mat") + __check_dsets(ds_loaded,ds_orig,"Failure loading input data from .mat file at ds-4-2_00", + "Failure loading output data from .mat file at ds-4-2_00") + +def test_load_data_json(): + ds_loaded = load_data("test/datasets/ds-4-2_00",format="json") + __check_dsets(ds_loaded,ds_orig,"Failure loading input data from .json file at ds-4-2_00", + "Failure saving loading data from .json file at ds-4-2_00") + +def test_save_data_json(): + save_data("test/datasets/ds-4-2_00",ds_orig,format="json") + ds_loaded = load_data("test/datasets/ds-4-2_00.json") + __check_dsets(ds_loaded,ds_orig,"Failure saving input data to .json file at ds-4-2_00", + "Failure saving output data to .json file at ds-4-2_00") + +def test_save_data_mat(): + save_data("test/datasets/ds-4-2_01",ds_orig,format="mat") + ds_loaded = load_data("test/datasets/ds-4-2_01",format="mat") + __check_dsets(ds_loaded,ds_orig,"Failure saving input data to .mat file at ds-4-2_01", + "Failure saving output data to .mat file at ds-4-2_01") + popen("rm -f test/datasets/ds-4-2_01.mat") + +def test_load_nnet_json(): + nnet_loaded1 = load_neural_net("test/datasets/nnet-54433_00.json") + nnet_loaded2 = load_neural_net("test/datasets/nnet-54433_00", "json") + __check_nnet(nnet_loaded1, nnet_loaded2, "Failure loading neural net") + +def test_save_nnet_json(): + save_neural_net("test/datasets/nnet-save-test.json", nnet00) + nnet_loaded = load_neural_net("test/datasets/nnet-save-test", "json") + __check_nnet(nnet_loaded, nnet00, "Failure saving neural net") + popen("rm -f test/datasets/nnet-save-test.json") + +def test_split_dataset1(): + X,y = ds_orig["X"],ds_orig["y"] + s = split_data(X,y,3) + assert len(s)==3, "Data should split into 3 groups, not %i" % len(s) + assert __check_shape(s[0]["X"],(4,4)) and __check_shape(s[1]["X"],(4,4)) and\ + __check_shape(s[2]["X"],(4,4)), "Data should split evenly into ((4,4),(4,4),(4,4)), not\ + (%i,%i,%i)" % (s[0]["X"].shape,s[1]["X"].shape,s[2]["X"].shape) + +def test_split_dataset2(): + X,y = ds_orig["X"],ds_orig["y"] + s = split_data(X,y,[1,2]) + assert len(s)==2, "Data should split into 2 groups, not %i" % len(s) + assert __check_shape(s[0]["X"],(4,4)) and __check_shape(s[1]["X"],(4,8)),\ + "Data should split evenly into ((4,4),(4,8)), not (%i,%i)" %\ + (s[0]["X"].shape,s[1]["X"].shape) diff --git a/test/suite_trainer_test.py b/test/suite_trainer_test.py new file mode 100644 index 0000000..3b9e33b --- /dev/null +++ b/test/suite_trainer_test.py @@ -0,0 +1,69 @@ +import pymind as pm +import pymind.activationfn as af +import pymind.errfn as ef +import numpy as np +from pymind.components.nnbuilder import Builder, DEFAULT +from pymind.training import * +from pymind.metricfn import * + +X = np.matrix([ + [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1], + [1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1], + [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1], + [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0]]) + +y = np.matrix([ + [1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0], + [0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1]]) + +def trivial_metric(nnet, res): + return 1 + +def accumulate_combinator(total, res): + if total is None: + return res + else: + return total + res + +def error_metric(nnet, res): + return res.fun if res.status==0 else None + +def testListCombiner1(): + b = Builder() + b.set(bias=[True, False, True, False]) + b.set(X=X) + b.set(y=y) + b.set(layer_units=[[4,4,2],[4,4,2],[4,6,2],[4,6,2]]) + suites = b.build() + res = train_suites(suites,trivial_metric) + assert res == [1,1,1,1], "List combiner should return [1,1,1,1], not " + str(res) + +def testAccumulationCombiner2(): + b = Builder() + b.set(bias=[True, False, True, False]) + b.set(X=X) + b.set(y=y) + b.set(layer_units=[[4,4,2],[4,4,2],[4,6,2],[4,6,2]]) + suites = b.build() + res = train_suites(suites,trivial_metric,combiner=accumulate_combinator) + assert res == 4, "Accumulation combinator should return 4, not " + str(res) + +def testErrorMetric1(): + b = Builder() + b.set(bias=[True, False, True, False]) + b.set(X=X) + b.set(y=y) + b.set(layer_units=[[4,4,2],[4,4,2],[4,6,2],[4,6,2]]) + suites = b.build() + res = np.array(train_suites(suites,error_metric)) + assert (res < 0.3).sum()==4, "Training error too high: " + str(res) + +def testErrorMetric2(): + b = Builder() + b.set(bias=True) + b.set(X=X) + b.set(y=y) + b.set(layer_units=[[4,1,2],[4,3,2],[4,5,2],[4,7,2],[4,9,2],[4,11,2]]) + suites = b.build() + res = np.array(train_suites(suites,error_metric)) + assert (res < 0.3).sum()==6, "Training error too high: " + str(res)