|
13 | 13 | # See the License for the specific language governing permissions and |
14 | 14 | # limitations under the License. |
15 | 15 | """ |
16 | | -Use black box optimization to tune model hyperparameters |
| 16 | +Use black box optimization to tune model hyperparameters. Call |
| 17 | +this script in a loop to iteratively tune parameters |
17 | 18 |
|
18 | | -:-t --trials-name str - |
| 19 | +:trials_name str |
19 | 20 | Filename to save hyperparameter optimization trials in |
20 | 21 | '.bbopt.json' will automatically be appended |
21 | 22 |
|
22 | | -:-c --cycles int 20 |
23 | | - Number of cycles of optimization to run |
| 23 | +:noise_folder str |
| 24 | + Folder with random noise to evaluate ambient activations |
24 | 25 |
|
25 | | -:-m --model str .cache/optimized.net |
26 | | - Model to load from |
| 26 | +:-ie --interaction-estimate int 100 |
| 27 | + Estimated number of interactions per day |
| 28 | +
|
| 29 | +:-aaa --ambient-activation-annoyance float 1.0 |
| 30 | + An ambient activation is X times as annoying as a failed |
| 31 | + activation when the wake word is spoken |
| 32 | +
|
| 33 | +:-bp --base-params str {} |
| 34 | + Json string containing base ListenerParams for all models |
27 | 35 |
|
28 | 36 | ... |
29 | 37 | """ |
30 | | -import numpy |
31 | | -# Optimizer blackhat |
32 | | -from glob import glob |
33 | | -from os import remove |
34 | | -from os.path import isfile, splitext, join |
35 | | -from pprint import pprint |
| 38 | +import json |
| 39 | +from math import exp |
| 40 | +from uuid import uuid4 |
| 41 | + |
| 42 | +from keras.models import save_model |
36 | 43 | from prettyparse import Usage |
37 | | -from shutil import rmtree |
38 | | -from typing import Any |
39 | 44 |
|
| 45 | +from precise.annoyance_estimator import AnnoyanceEstimator |
40 | 46 | from precise.model import ModelParams, create_model |
| 47 | +from precise.params import pr, save_params |
41 | 48 | from precise.scripts.train import TrainScript |
42 | | -from precise.train_data import TrainData |
| 49 | +from precise.stats import Stats |
43 | 50 |
|
44 | 51 |
|
45 | 52 | class TrainOptimizeScript(TrainScript): |
46 | | - Usage(__doc__) | TrainScript.usage |
| 53 | + usage = Usage(__doc__) | TrainScript.usage |
| 54 | + del usage.arguments['model'] # Remove 'model' argument from original TrainScript |
47 | 55 |
|
48 | 56 | def __init__(self, args): |
49 | | - super().__init__(args) |
50 | 57 | from bbopt import BlackBoxOptimizer |
| 58 | + pr.__dict__.update(json.loads(args.base_params)) |
| 59 | + args.model = args.trials_name + '-cur' |
| 60 | + save_params(args.model) |
| 61 | + super().__init__(args) |
51 | 62 | self.bb = BlackBoxOptimizer(file=self.args.trials_name) |
52 | | - if not self.test: |
53 | | - data = TrainData.from_both(self.args.tags_file, self.args.tags_folder, self.args.folder) |
54 | | - _, self.test = data.load(False, True) |
55 | | - |
56 | | - from keras.callbacks import ModelCheckpoint |
57 | | - for i in list(self.callbacks): |
58 | | - if isinstance(i, ModelCheckpoint): |
59 | | - self.callbacks.remove(i) |
60 | | - |
61 | | - def process_args(self, args: Any): |
62 | | - model_parts = glob(splitext(args.model)[0] + '.*') |
63 | | - if len(model_parts) < 5: |
64 | | - for name in model_parts: |
65 | | - if isfile(name): |
66 | | - remove(name) |
67 | | - else: |
68 | | - rmtree(name) |
69 | | - args.trials_name = args.trials_name.replace('.bbopt.json', '').replace('.json', '') |
70 | | - if not args.trials_name: |
71 | | - if isfile(join('.cache', 'trials.bbopt.json')): |
72 | | - remove(join('.cache', 'trials.bbopt.json')) |
73 | | - args.trials_name = join('.cache', 'trials') |
| 63 | + |
| 64 | + def calc_params_cost(self, model): |
| 65 | + """ |
| 66 | + Models the real world cost of additional model parameters |
| 67 | + Up to a certain point, having more parameters isn't worse. |
| 68 | + However, at a certain point more parameters will risk |
| 69 | + running slower than realtime and become unfeasible. This |
| 70 | + is why it's modelled exponentially with some reasonable |
| 71 | + number of acceptable parameters. |
| 72 | +
|
| 73 | + Ideally, this would be replaced with floating point |
| 74 | + computations and the numbers would be configurable |
| 75 | + rather than chosen relatively arbitrarily |
| 76 | + """ |
| 77 | + return 1.0 + exp((model.count_params() - 11000) / 10000) |
74 | 78 |
|
75 | 79 | def run(self): |
76 | | - print('Writing to:', self.args.trials_name + '.bbopt.json') |
77 | | - for i in range(self.args.cycles): |
78 | | - self.bb.run(backend="random") |
79 | | - print("\n= %d = (example #%d)" % (i + 1, len(self.bb.get_data()["examples"]) + 1)) |
80 | | - |
81 | | - params = ModelParams( |
82 | | - recurrent_units=self.bb.randint("units", 1, 70, guess=50), |
83 | | - dropout=self.bb.uniform("dropout", 0.1, 0.9, guess=0.6), |
84 | | - extra_metrics=self.args.extra_metrics, |
85 | | - skip_acc=self.args.no_validation, |
86 | | - loss_bias=1.0 - self.args.sensitivity |
87 | | - ) |
88 | | - print('Testing with:', params) |
89 | | - model = create_model(self.args.model, params) |
90 | | - model.fit( |
91 | | - *self.sampled_data, batch_size=self.args.batch_size, |
92 | | - epochs=self.epoch + self.args.epochs, |
93 | | - validation_data=self.test * (not self.args.no_validation), |
94 | | - callbacks=self.callbacks, initial_epoch=self.epoch |
95 | | - ) |
96 | | - resp = model.evaluate(*self.test, batch_size=self.args.batch_size) |
97 | | - if not isinstance(resp, (list, tuple)): |
98 | | - resp = [resp, None] |
99 | | - test_loss, test_acc = resp |
100 | | - predictions = model.predict(self.test[0], batch_size=self.args.batch_size) |
101 | | - |
102 | | - num_false_positive = numpy.sum(predictions * (1 - self.test[1]) > 0.5) |
103 | | - num_false_negative = numpy.sum((1 - predictions) * self.test[1] > 0.5) |
104 | | - false_positives = num_false_positive / numpy.sum(self.test[1] < 0.5) |
105 | | - false_negatives = num_false_negative / numpy.sum(self.test[1] > 0.5) |
106 | | - |
107 | | - from math import exp |
108 | | - param_score = 1.0 / (1.0 + exp((model.count_params() - 11000) / 2000)) |
109 | | - fitness = param_score * (1.0 - 0.2 * false_negatives - 0.8 * false_positives) |
110 | | - |
111 | | - self.bb.remember({ |
112 | | - "test loss": test_loss, |
113 | | - "test accuracy": test_acc, |
114 | | - "false positive%": false_positives, |
115 | | - "false negative%": false_negatives, |
116 | | - "fitness": fitness |
117 | | - }) |
118 | | - |
119 | | - print("False positive: ", false_positives * 100, "%") |
120 | | - |
121 | | - self.bb.maximize(fitness) |
122 | | - pprint(self.bb.get_current_run()) |
123 | | - best_example = self.bb.get_optimal_run() |
124 | | - print("\n= BEST = (example #%d)" % self.bb.get_data()["examples"].index(best_example)) |
125 | | - pprint(best_example) |
| 80 | + self.bb.run(alg='tree_structured_parzen_estimator') |
| 81 | + |
| 82 | + model = create_model(None, ModelParams( |
| 83 | + recurrent_units=self.bb.randint("units", 1, 120, guess=30), |
| 84 | + dropout=self.bb.uniform("dropout", 0.05, 0.9, guess=0.2), |
| 85 | + extra_metrics=self.args.extra_metrics, |
| 86 | + skip_acc=self.args.no_validation, |
| 87 | + loss_bias=self.bb.uniform( |
| 88 | + 'loss_bias', 0.01, 0.99, guess=1.0 - self.args.sensitivity |
| 89 | + ), |
| 90 | + freeze_till=0 |
| 91 | + )) |
| 92 | + model.fit( |
| 93 | + *self.sampled_data, batch_size=self.args.batch_size, |
| 94 | + epochs=self.args.epochs, |
| 95 | + validation_data=self.test * (not self.args.no_validation), |
| 96 | + callbacks=[] |
| 97 | + ) |
| 98 | + test_in, test_out = self.test |
| 99 | + test_pred = model.predict(test_in, batch_size=self.args.batch_size) |
| 100 | + stats_dict = Stats(test_pred, test_out, []).to_dict() |
| 101 | + |
| 102 | + ann_est = AnnoyanceEstimator( |
| 103 | + model, self.args.interaction_estimate, |
| 104 | + self.args.ambient_activation_annoyance |
| 105 | + ).estimate( |
| 106 | + model, test_pred, test_out, |
| 107 | + self.args.noise_folder, self.args.batch_size |
| 108 | + ) |
| 109 | + params_cost = self.calc_params_cost(model) |
| 110 | + cost = ann_est.annoyance + params_cost |
| 111 | + |
| 112 | + model_name = '{}-{}.net'.format(self.args.trials_name, str(uuid4())) |
| 113 | + save_model(model, model_name) |
| 114 | + save_params(model_name) |
| 115 | + |
| 116 | + self.bb.remember({ |
| 117 | + 'test_stats': stats_dict, |
| 118 | + 'best_threshold': ann_est.threshold, |
| 119 | + 'cost': cost, |
| 120 | + 'cost_info': { |
| 121 | + 'params_cost': params_cost, |
| 122 | + 'annoyance': ann_est.annoyance, |
| 123 | + 'ww_annoyance': ann_est.ww_annoyance, |
| 124 | + 'nww_annoyance': ann_est.nww_annoyance, |
| 125 | + }, |
| 126 | + 'model': model_name |
| 127 | + }) |
| 128 | + print('Current Run: {}'.format(json.dumps( |
| 129 | + self.bb.get_current_run(), indent=4 |
| 130 | + ))) |
| 131 | + self.bb.minimize(cost) |
126 | 132 |
|
127 | 133 |
|
128 | 134 | main = TrainOptimizeScript.run_main |
|
0 commit comments