From 04ca4d00ac3d18e5013fa61dd69d8f27f78eaed2 Mon Sep 17 00:00:00 2001 From: "raver119@gmail.com" Date: Tue, 21 Jul 2020 18:48:02 +0300 Subject: [PATCH] Initial commit --- LICENSE.txt | 384 ++++++++++++++++++++++++++++++++++ README.md | 3 + easytune/GridBuilder.py | 112 ++++++++++ easytune/TuneKeras.py | 209 ++++++++++++++++++ easytune/__init__.py | 4 + examples/sin.h5 | Bin 0 -> 132936 bytes examples/sin_regression.py | 55 +++++ setup.cfg | 2 + setup.py | 27 +++ tests/__init__.py | 0 tests/test_GridBuilder.py | 38 ++++ tests/test_KerasGridSearch.py | 73 +++++++ 12 files changed, 907 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 easytune/GridBuilder.py create mode 100644 easytune/TuneKeras.py create mode 100644 easytune/__init__.py create mode 100644 examples/sin.h5 create mode 100644 examples/sin_regression.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/test_GridBuilder.py create mode 100644 tests/test_KerasGridSearch.py diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..40eee3f --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,384 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +########################## + +Keras code + +Auto-generated documentation: https://github.com/deeplearning4j/deeplearning4j/blob/master/docs/doc_generator.py + +COPYRIGHT + +All contributions by Fran�ois Chollet: +Copyright (c) 2015 - 2018, Fran�ois Chollet. +All rights reserved. + +All contributions by Google: +Copyright (c) 2015 - 2018, Google, Inc. +All rights reserved. + +All contributions by Microsoft: +Copyright (c) 2017 - 2018, Microsoft, Inc. +All rights reserved. + +All other contributions: +Copyright (c) 2015 - 2018, the respective contributors. +All rights reserved. + +Each contributor holds copyright over their respective contributions. +The project versioning (Git) records all such contribution source information. + +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +########################## + +OpenCSV Code + +CSVParser: https://github.com/deeplearning4j/deeplearning4j/blob/master/datavec/datavec-api/src/main/java/org/datavec/api/records/reader/impl/csv/SerializableCSVParser.java + +Apache 2.0 License + +All contributions by Bytecode Pty Ltd. +Copyright 2005 Bytecode Pty Ltd. +All rights reserved. + + +########################## + +Aeron Code + +Modifed Code: nd4j/nd4j-serde/nd4j-aeron/src/main/java/org/nd4j/aeron/ipc/AeronUtil.java + +Copyright 2014 - 2016 Real Logic Ltd. All rights reserved. + +Apache License, Version 2.0 + + +########################## + +cnpy Code + +Forked Code: libnd4j/include/cnpy/ + +The MIT License + +Copyright (c) Carl Rogers, 2011 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +########################## + +Protocol Buffers Code + +Codebase: nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/protobuf/tf/google/protobuf/ + +Protocol Buffers - Google's data interchange format +Copyright 2008 Google Inc. All rights reserved. +https://developers.google.com/protocol-buffers/ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +########################## + +ONNX Code + +Protocol Buffers: nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/protobuf/onnx/ + +Copyright (c) Facebook Inc. and Microsoft Corporation. All rights reserved. + +Licensed under the MIT license. + + +########################## + +TensorFlow Code + +Protocol Buffers: nd4j/nd4j-backends/nd4j-api-parent/nd4j-api/src/main/protobuf/tf/tensorflow/core/ +Operations: libnd4j/include/ops/declarable/generic/parity_ops/ + +Copyright 2015-2017 The TensorFlow Authors. All rights reserved. + +Apache License, Version 2.0 + + +########################## + +Ansj Code + +Codebase: deeplearning4j/deeplearning4j-nlp-parent/deeplearning4j-nlp-chinese/src/main/java/org/ansj/ +Resources: deeplearning4j/deeplearning4j-nlp-parent/deeplearning4j-nlp-chinese/src/main/resources/ + +Copyright 2011-2016 ansj_seg. All rights reserved. + +Apache License, Version 2.0 + + +########################## + +Kuromoji Code + +Codebase: deeplearning4j/deeplearning4j-nlp-parent/deeplearning4j-nlp-japanese/src/main/java/com/atilika/kuromoji/ + +Copyright (c) 2010-2015 Atilika Inc. and contributors. All rights reserved. + +Apache License, Version 2.0 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..49ab4ad --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +Simple framework for neural network hyperparameters optimization via grid search + +Suited for TensorFlow Keras \ No newline at end of file diff --git a/easytune/GridBuilder.py b/easytune/GridBuilder.py new file mode 100644 index 0000000..3dbab21 --- /dev/null +++ b/easytune/GridBuilder.py @@ -0,0 +1,112 @@ +""" +GridBuilder +""" +import random +from typing import Iterable, Dict, Any + + +class GridBuilder: + """ + GridBuilder class provides utilities for building grid of all possible params, in a form suitable for hyperparameters search + """ + def __init__(self, parameters: Dict[Any, Any]): + if parameters is None or not isinstance(parameters, dict) or len(parameters) == 0: + raise ValueError("Parameters should be a valid non-empty dict") + + self.__parameters = parameters + self.__combinations = -1 + + def combinations(self) -> int: + """ + This method calculates number of all possible combinations within grid + """ + prod = 1 + for key in self.__parameters: + row = self.__parameters[key] + prod *= len(row) + + return prod + + def random(self) -> Iterable[dict]: + """ + This method returns a randomized list of all possible combinations within Grid + :return: + """ + # TODO: this can be improved with sliding window + # pulling all possible values out of sequential grid + result = list() + for space in self.sequenial(): + result.append(space) + + # shuffle sequential results and yield them one by one + random.shuffle(result) + for v in result: + yield v + + def sequenial(self) -> Iterable[dict]: + """ + This method provides a generator with all possible combinations within Grid + :return: + """ + if self.__combinations < 0: + self.__combinations = self.combinations() + + # first of all we build list of counters + keys = self.__parameters.keys() + + # special case: there's only 1 key, so we just roll through params in this key + if len(keys) == 1: + for key in keys: + for v in self.__parameters[key]: + yield v + else: + # if we have more than 1 key, we'll need to + counters = dict() + for key in keys: + counters[key] = 0 + + # we will be incrementing counter of this primary key + for primary in keys: + result = dict() + + filtered_keys = list() + for key in keys: + if key == primary: + continue + + filtered_keys.append(key) + + # now for each primary key we'll iterate over all other keys + for pv in self.__parameters[primary]: + result[primary] = pv + + # nullify counters + counters = dict() + for key in keys: + counters[key] = 0 + + # now, for every primary key, we'll roll through all possible combinations of other keys + combinations_left = 1 + for incremental in filtered_keys: + combinations_left *= len(self.__parameters[incremental]) + + key_index = 0 + for i in range(combinations_left): + # now we increment 1 specific index + index = i + for r in range(len(filtered_keys) - 1, 0, -1): + key = filtered_keys[r] + counters[key] = index % len(self.__parameters[key]) + index //= len(self.__parameters[key]) + + counters[filtered_keys[0]] = index + + # fix current state + for secondary in filtered_keys: + result[secondary] = self.__parameters[secondary][counters[secondary]] + + # yield detached copy of fixed state + yield result.copy() + + # we have outer loop, but we use it only to start things + break diff --git a/easytune/TuneKeras.py b/easytune/TuneKeras.py new file mode 100644 index 0000000..17ec3b7 --- /dev/null +++ b/easytune/TuneKeras.py @@ -0,0 +1,209 @@ +""" +This file contains TF.Keras-specific classes/implementations +""" +from typing import Callable, Any, Iterable, Union, Tuple, Dict, List + +import tensorflow as tf +import numpy as np + + +class StatsCallback(tf.keras.callbacks.Callback): + def __init__(self): + super(StatsCallback, self).__init__() + self.__train_score = list() + self.__test_score = list() + + def on_epoch_end(self, epoch, logs=None): + # loss key must exist no matter what + self.__train_score.append(logs['loss']) + + # val_loss might be absent, i.e. if there was no validation dataset + if 'val_loss' in logs: + self.__test_score.append(logs['val_loss']) + + def last_test_score(self): + return self.__test_score[len(self.__test_score) - 1] + + def last_train_score(self): + return self.__train_score[len(self.__train_score) - 1] + + def last_score(self): + return self.last_test_score() if len(self.__test_score) > 0 else self.last_train_score() + + +class TensorFlowWrapper: + """ + This class provides a simple wrapper for regular generators to make them endless + """ + def __init__(self, generator_callable: Callable, epoch_limit: int = 0): + """ + :param generator_callable: Callable that creates generator + """ + self.__generator = generator_callable + self.__limit = epoch_limit + + def __iter__(self): + """ + Endless iterable here + :return: + """ + while True: + for v in self.__generator(): + yield v + + def callable(self): + """ + This method acts as wrapper, providing en endless iterable for TF + :return: + """ + while True: + cnt = 0 + for v in self.__generator(): + yield v + cnt += 1 + if self.__limit == cnt: + break + + +class TuneKeras: + """ + This class provides methods for hyperparameters search for TF.Keras models + """ + def __init__(self, parameters: Dict[str, Union[Tuple[Any], List[Any]]], + train_generator: Callable[[], Iterable], train_batches: int, + class_weight=None, + test_generator: Callable[[], Iterable] = None, test_batches: int = 0, + workers: int = 1): + """ + :param parameters: Dictionary with all possible values for parameters + :param train_generator: void callable, that returns Python generator, yielding either pair of numpy arrays or + pair of dictionaries, containing string keys and numpy array values + :param train_batches: number of unique batches to expect from train_gnerator + :param test_generator: void callable, that returns Python generator, yielding either pair of numpy arrays or + pair of dictionaries, containing string keys and numpy array values + :param test_batches: number of unique batches to expect from test_gnerator + :param workers: Number of workers for the search process + """ + self.__parameters = parameters + self.__workers = workers + self.__train_generator = train_generator + self.__train_batches = train_batches + self.__test_generator = test_generator + self.__test_batches = test_batches + self.__class_weight = class_weight + + def __convert_dtype(self, dtype: np.dtype) -> tf.dtypes: + """ + This method converts NumPy dtype to TF dtype + :param dtype: + :return: + """ + return tf.dtypes.as_dtype(dtype) + + def __convert_shape(self, shape: Tuple[int]) -> tf.TensorShape: + """ + This method converts shape to tf.TensorShape + sets batch dim to None + :param shape: + :return: + """ + result = [None] + for i in range(1, len(shape)): + result.append(shape[i]) + + return tf.TensorShape(result) + + def __build_tf_dataset(self, generator: Callable[[], Iterable[Union[Tuple[np.ndarray, np.ndarray], Tuple[Dict[str, np.ndarray], Dict[str, np.ndarray]]]]]) -> tf.data.Dataset: + """ + This method builds TF dataset out of python generator + :return: + """ + # instantiating generator and fetching 1 dataset out of it + gen = generator() + tpl = next(gen.__iter__()) + # we must have exactly 2 objects in tuple + assert len(tpl) == 2 + + # it's either 2 np.arrays or 2 dictionaries here, but both types must be the same + assert type(tpl[0]) == type(tpl[1]) + + if isinstance(tpl[0], np.ndarray): + return tf.data.Dataset.from_generator(generator, + output_types=((self.__convert_dtype(tpl[0].dtype), + self.__convert_dtype(tpl[1].dtype))), + output_shapes=((self.__convert_shape(tpl[0].shape), + self.__convert_shape(tpl[1].shape)))) + elif isinstance(tpl[0], dict): + # now all shapes/types will be pulled into separate dicts + in_types = dict() + out_types = dict() + in_shapes = dict() + out_shapes = dict() + + in_keys = tpl[0].keys() + out_keys = tpl[1].keys() + + # filling inputs first + for key in in_keys: + array = tpl[0][key] + assert isinstance(array, np.ndarray) + in_types[key] = self.__convert_dtype(array.dtype) + in_shapes[key] = self.__convert_shape(array.shape) + + # now filling outputs + for key in out_keys: + array = tpl[1][key] + assert isinstance(array, np.ndarray) + out_types[key] = self.__convert_dtype(array.dtype) + out_shapes[key] = self.__convert_shape(array.shape) + + # and returning the dataset + return tf.data.Dataset.from_generator(generator, + output_types=(in_types, out_types), + output_shapes=(in_shapes, out_shapes)) + else: + raise ValueError("Generator must yield tuples of NumPy.ndarray or tuples of dict") + + def search(self, model_provider: Callable[[Any], tf.keras.Model], + epochs: int = 1) -> tf.keras.Model: + """ + This method iterates over all possible combinations of parameters, in order to find the best possible model + :param model_provider: callable that accepts some arguments, and returns a TF.Keras model instead. + Model must be compiled upon return. + :param epochs: number of epochs per shot + :return: Model with best validation loss + """ + from easytune import GridBuilder + grid = GridBuilder(self.__parameters) + + best_score = float("inf") + best_params = dict() + best_model = None + cnt = 1 + # TODO: make this async + for p in grid.random(): + print(f"Model {cnt} of {grid.combinations()}...") + model = model_provider(**p) + train_wrapper = TensorFlowWrapper(self.__train_generator) + train_dataset = self.__build_tf_dataset(train_wrapper.callable) + + # let's make sure we're not passing None here an there + test_wrapper = None if self.__test_generator is None else TensorFlowWrapper(self.__test_generator) + test_dataset = None if self.__test_generator is None else self.__build_tf_dataset(test_wrapper.callable) + test_steps = 0 if self.__test_generator is None else self.__test_batches + + callback = StatsCallback() + + # time to attach callable, and fit the model + model.fit(train_dataset, verbose=0, workers=1, steps_per_epoch=self.__train_batches, epochs=epochs, callbacks=callback, + validation_data=test_dataset, validation_steps=test_steps, class_weight=self.__class_weight) + + # picking the best model based on train or test score + if callback.last_score() < best_score: + best_score = callback.last_score() + best_params = p + best_model = model + + cnt += 1 + + print(f"Best loss: {best_score:.4f}; params: {best_params}") + return best_model diff --git a/easytune/__init__.py b/easytune/__init__.py new file mode 100644 index 0000000..bed330a --- /dev/null +++ b/easytune/__init__.py @@ -0,0 +1,4 @@ +""" +pewpew +""" +from easytune.GridBuilder import GridBuilder \ No newline at end of file diff --git a/examples/sin.h5 b/examples/sin.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1f122d0c5ed792e8fc5401c6a4291df305ea2388 GIT binary patch literal 132936 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 Iterable[Tuple[np.ndarray, np.ndarray]]: + for i in range(start, start + num, batch_size): + features = np.zeros((batch_size, 32), dtype=np.float32) + labels = np.zeros((batch_size, 1), dtype=np.float32) + for b in range(0, batch_size, 1): + features[b, :] = i + b + labels[b, :] = math.sin(i + b) + + yield features,labels + + +def train_generator() -> Iterable[Tuple[np.ndarray, np.ndarray]]: + return sin_generator(-train_batches // 2, train_batches) + + +def test_generator() -> Iterable[Tuple[np.ndarray, np.ndarray]]: + return sin_generator(-test_batches // 2, test_batches) + + +def build_model(lr: float, loss: str, act_in: str, act_out: str) -> tf.keras.Model: + input = k.Input(shape=(32, ), dtype=tf.float32) + x = k.layers.Dense(units=128, activation=act_in)(input) + x = k.layers.Dense(units=32, activation=act_in)(x) + output = k.layers.Dense(units=1, activation=act_out, name='out')(x) + + model = k.Model(inputs=[input,], outputs=[output, ]) + optimizer = tf.keras.optimizers.Adam(learning_rate=lr) + model.compile(optimizer=optimizer, loss={'out': loss}) + return model + + +params = {'lr': [0.001, 0.0005], + 'loss': ['mean_squared_error'], + 'act_in': ['relu', 'swish', 'tanh'], + 'act_out': ['tanh', 'sigmoid']} + +kt = TuneKeras(params, train_generator, 1000, test_generator=test_generator, test_batches=test_batches) + +model = kt.search(build_model, epochs=3) +model.save("./sin.h5") \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..82bdf12 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +from distutils.core import setup +setup( + name = 'easytune', + packages = ['easytune'], + version = '0.1', + license='apache-2.0', + description = 'Simple framework for hyperparameters optimization', + author = 'Vyacheslav Kokorin', + author_email = 'raver119@gmail.com', + url = 'https://github.com/raver119/easytune', + download_url = 'https://github.com/raver119/easytune/archive/v_01.tar.gz', + keywords = ['hyperparameters', 'hyperparams', 'optimization'], + install_requires=[ + 'numpy', + 'tensorflow>=2.0.0' + ], + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + 'License :: OSI Approved :: Apache License 2.0', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + ], +) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_GridBuilder.py b/tests/test_GridBuilder.py new file mode 100644 index 0000000..56889b8 --- /dev/null +++ b/tests/test_GridBuilder.py @@ -0,0 +1,38 @@ +""" +Tests suit for GridBuilder +""" +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' + +from unittest import TestCase +from easytune.GridBuilder import GridBuilder + + +class TestGridBuilder(TestCase): + def test_sequenial_1(self): + b = GridBuilder({'alpha': [1, 2, 3, 4]}) + + self.assertEqual(4, b.combinations()) + + cnt = 0 + for v in b.sequenial(): + cnt += 1 + + self.assertEqual(b.combinations(), cnt) + + def test_sequenial_2(self): + b = GridBuilder({'alpha': [1, 2], 'beta': [1, 2]}) + + self.assertEqual(4, b.combinations()) + + cnt = 0 + for v in b.sequenial(): + cnt += 1 + + self.assertEqual(b.combinations(), cnt) + + cnt = 0 + for v in b.random(): + cnt += 1 + + self.assertEqual(b.combinations(), cnt) diff --git a/tests/test_KerasGridSearch.py b/tests/test_KerasGridSearch.py new file mode 100644 index 0000000..467d0f1 --- /dev/null +++ b/tests/test_KerasGridSearch.py @@ -0,0 +1,73 @@ +import os +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' + +from typing import Iterable, Tuple, Dict +from unittest import TestCase +from easytune.TuneKeras import TuneKeras +import numpy as np +import tensorflow as tf + + +def simple_numpy_generator() -> Iterable[Tuple[np.ndarray, np.ndarray]]: + """ + Simple generator, yields numpy ndarrays + :return: + """ + for i in range(0, 100): + features = np.zeros((8, 32), dtype=np.float32) + i + labels = np.zeros((8, 4), dtype=np.int32) + i + yield features, labels + +def simple_dict_generator() -> Iterable[Tuple[Dict[str,np.ndarray], Dict[str, np.ndarray]]]: + """ + Simple generator, yields numpy ndarrays + :return: + """ + for i in range(0, 100): + features = np.zeros((8, 32), dtype=np.float32) + i + labels = np.zeros((8, 4), dtype=np.int32) + i + yield {'features_A': features, 'features_B': features}, {'labels': labels} + + +class Test(TestCase): + def test_dataset_builder_1(self): + """ + This test checks NumPy -> TF Dataset conversionv (np version) + """ + kgs = TuneKeras({'lr': [0.001, 0.002, 0.003]}, None, 0) + dts = kgs._TuneKeras__build_tf_dataset(simple_numpy_generator) + self.assertTrue(isinstance(dts, tf.data.Dataset)) + + cnt = 0 + for ds in dts.as_numpy_iterator(): + self.assertEqual(2, len(ds)) + self.assertEqual(np.float32, ds[0].dtype) + self.assertEqual(np.int32, ds[1].dtype) + + self.assertEqual((8, 32), ds[0].shape) + self.assertEqual((8, 4), ds[1].shape) + cnt += 1 + + self.assertEqual(100, cnt) + + def test_dataset_builder_2(self): + """ + This test checks NumPy -> TF Dataset conversionv (dict version) + """ + kgs = TuneKeras({'lr': [0.001, 0.002, 0.003]}, None, 0) + dts = kgs._TuneKeras__build_tf_dataset(simple_dict_generator) + self.assertTrue(isinstance(dts, tf.data.Dataset)) + + cnt = 0 + for ds in dts.as_numpy_iterator(): + self.assertEqual(2, len(ds)) + self.assertEqual(np.float32, ds[0]['features_A'].dtype) + self.assertEqual(np.float32, ds[0]['features_B'].dtype) + self.assertEqual(np.int32, ds[1]['labels'].dtype) + + self.assertEqual((8, 32), ds[0]['features_A'].shape) + self.assertEqual((8, 32), ds[0]['features_B'].shape) + self.assertEqual((8, 4), ds[1]['labels'].shape) + cnt += 1 + + self.assertEqual(100, cnt)