|
| 1 | +import os |
| 2 | +from collections import OrderedDict |
| 3 | +import logging |
| 4 | +import itertools |
| 5 | + |
| 6 | +from bokeh.io import output_notebook |
| 7 | +from bokeh.plotting import show, figure, ColumnDataSource |
| 8 | +from bokeh.embed import components |
| 9 | +from bokeh.models import HoverTool, ColorBar, LinearColorMapper, BasicTicker, CustomJS, Slider |
| 10 | +from bokeh.models.sources import CDSView |
| 11 | +from bokeh.models.filters import GroupFilter |
| 12 | +from bokeh.layouts import column, row, widgetbox |
| 13 | +from bokeh.models.widgets import CheckboxButtonGroup, CheckboxGroup, Button |
| 14 | +from bokeh.palettes import Spectral11 |
| 15 | + |
| 16 | +from cave.analyzer.base_analyzer import BaseAnalyzer |
| 17 | +from cave.utils.timing import timing |
| 18 | + |
| 19 | +class BohbLearningCurves(BaseAnalyzer): |
| 20 | + |
| 21 | + def __init__(self, hp_names, result_object=None, result_path=None): |
| 22 | + """ |
| 23 | + Visualize hpbandster learning curves in an interactive bokeh-plot. |
| 24 | +
|
| 25 | + Parameters |
| 26 | + ---------- |
| 27 | + hp_names: List[str] |
| 28 | + list with hyperparameters-names |
| 29 | + result_object: Result |
| 30 | + hpbandster-result object. must be specified if result_path is not |
| 31 | + result_path: str |
| 32 | + path to hpbandster result-folder. must contain configs.json and results.json. must be specified if result_object is not |
| 33 | + """ |
| 34 | + self.logger = logging.getLogger(self.__module__ + '.' + self.__class__.__name__) |
| 35 | + try: |
| 36 | + from hpbandster.core.result import logged_results_to_HBS_result |
| 37 | + from hpbandster.core.result import extract_HBS_learning_curves |
| 38 | + except ImportError as err: |
| 39 | + self.logger.exception(err) |
| 40 | + raise ImportError("You need to install hpbandster (e.g. 'pip install hpbandster') to analyze bohb-results.") |
| 41 | + |
| 42 | + if (result_path and result_object) or not (result_path or result_object): |
| 43 | + raise ValueError("Specify either result_path or result_object. (currently \"%s\" and \"%s\")" % (result_path, result_object)) |
| 44 | + elif result_path: |
| 45 | + result_object = logged_results_to_HBS_result(result_path) |
| 46 | + |
| 47 | + incumbent_trajectory = result_object.get_incumbent_trajectory() |
| 48 | + lcs = result_object.get_learning_curves(lc_extractor=extract_HBS_learning_curves) |
| 49 | + self.bokeh_plot = self.bohb_plot(result_object, lcs, hp_names) |
| 50 | + |
| 51 | + def bohb_plot(self, result_object, learning_curves, hyperparameter_names, reset_times=False): |
| 52 | + # Extract information from learning-curve-dict |
| 53 | + times, losses, config_ids, = [], [], [] |
| 54 | + for conf_id, learning_curves in learning_curves.items(): |
| 55 | + self.logger.debug("Config ID: %s, learning_curves: %s", str(conf_id), str(learning_curves)) |
| 56 | + for l in learning_curves: |
| 57 | + if len(l) == 0: |
| 58 | + continue |
| 59 | + tmp = list(zip(*l)) |
| 60 | + times.append(tmp[0]) |
| 61 | + losses.append(tmp[1]) |
| 62 | + config_ids.append(conf_id) |
| 63 | + |
| 64 | + if reset_times: |
| 65 | + times = [np.array(ts) - ts[0] for ts in times] |
| 66 | + |
| 67 | + # Prepare ColumnDataSource |
| 68 | + data = OrderedDict([ |
| 69 | + ('config_id', []), |
| 70 | + ('config_info', []), |
| 71 | + ('times', []), |
| 72 | + ('losses', []), |
| 73 | + ('duration', []), |
| 74 | + ('HB_iteration', []), |
| 75 | + ('colors', []), |
| 76 | + ]) |
| 77 | + for hp in hyperparameter_names: |
| 78 | + data[hp] = [] |
| 79 | + |
| 80 | + # Colors |
| 81 | + colors = {c_id : color for c_id, color in zip(config_ids, itertools.cycle(Spectral11))} |
| 82 | + |
| 83 | + # Populate |
| 84 | + id2conf = result_object.get_id2config_mapping() |
| 85 | + for counter, c_id in enumerate(config_ids): |
| 86 | + if not (len(times[counter]) == len(losses[counter])): |
| 87 | + raise ValueError() |
| 88 | + longest_run = self.get_longest_run(c_id, result_object) |
| 89 | + if not longest_run: |
| 90 | + continue |
| 91 | + data['config_id'].append(str(c_id)) |
| 92 | + try: |
| 93 | + config_info = '\n'.join([str(k) + "=" +str(v) for k,v in sorted(id2conf[c_id]['config_info'].items())]) |
| 94 | + except: |
| 95 | + config_info = 'Not Available' |
| 96 | + data['config_info'].append(config_info) |
| 97 | + data['times'].append(times[counter]) |
| 98 | + data['losses'].append(losses[counter]) |
| 99 | + if longest_run: |
| 100 | + data['duration'].append(longest_run['time_stamps']['finished'] - longest_run['time_stamps']['started']) |
| 101 | + else: |
| 102 | + data['duration'].append('N/A') |
| 103 | + data['HB_iteration'].append(str(c_id[0])) |
| 104 | + data['colors'].append(colors[c_id]) |
| 105 | + for hp in hyperparameter_names: |
| 106 | + try: |
| 107 | + data[hp].append(id2conf[c_id]['config'][hp]) |
| 108 | + except KeyError: |
| 109 | + data[hp].append("None") |
| 110 | + self.logger.debug(data) |
| 111 | + |
| 112 | + # Tooltips |
| 113 | + tooltips=[(key, '@' + key) for key in data.keys() if not key in ['times', 'duration', 'colors']] |
| 114 | + tooltips.insert(4, ('duration (sec)', '@duration')) |
| 115 | + hover = HoverTool(tooltips=tooltips) |
| 116 | + |
| 117 | + # Create sources |
| 118 | + source_multiline = ColumnDataSource(data=data) |
| 119 | + # Special source for scattering points, since times and losses for multi_line are nested lists |
| 120 | + scatter_data = {key : [] for key in data.keys()} |
| 121 | + for idx, c_id in enumerate(data['config_id']): |
| 122 | + for t, l in zip(data['times'][idx], data['losses'][idx]): |
| 123 | + scatter_data['times'].append(t) |
| 124 | + scatter_data['losses'].append(l) |
| 125 | + for key in list(data.keys()): |
| 126 | + if key in ['times', 'losses']: |
| 127 | + continue |
| 128 | + scatter_data[key].append(data[key][idx]) |
| 129 | + source_scatter = ColumnDataSource(data=scatter_data) |
| 130 | + |
| 131 | + # Create plot |
| 132 | + p = figure(plot_height=500, plot_width=600, |
| 133 | + y_axis_type="log" if len([a for a in scatter_data['losses'] if a <= 0]) == 0 else 'linear', |
| 134 | + tools=[hover, 'save', 'pan', 'wheel_zoom', 'box_zoom', 'reset']) |
| 135 | + |
| 136 | + # Plot per HB_iteration, each config individually |
| 137 | + HB_iterations = sorted(set(data['HB_iteration'])) |
| 138 | + HB_handles = [] |
| 139 | + for it in HB_iterations: |
| 140 | + line_handles = [] |
| 141 | + view = CDSView(source=source_multiline, filters=[GroupFilter(column_name='HB_iteration', group=str(it))]) |
| 142 | + line_handles.append(p.multi_line(xs='times', ys='losses', |
| 143 | + source=source_multiline, |
| 144 | + view=view, |
| 145 | + color='colors', |
| 146 | + line_width=5, |
| 147 | + )) |
| 148 | + view = CDSView(source=source_scatter, filters=[GroupFilter(column_name='HB_iteration', group=str(it))]) |
| 149 | + line_handles.append(p.circle(x='times', y='losses', |
| 150 | + source=source_scatter, |
| 151 | + view=view, |
| 152 | + fill_color='colors', |
| 153 | + line_color='colors', |
| 154 | + size=20, |
| 155 | + )) |
| 156 | + HB_handles.append(line_handles) |
| 157 | + |
| 158 | + # Add interactiveness (select certain lines, etc.) |
| 159 | + num_total_lines = sum([len(group) for group in HB_handles]) |
| 160 | + glyph_renderers_flattened = ['glyph_renderer' + str(i) for i in range(num_total_lines)] |
| 161 | + glyph_renderers = [] |
| 162 | + start = 0 |
| 163 | + for group in HB_handles: |
| 164 | + self.logger.debug("%d elements", len(group)) |
| 165 | + glyph_renderers.append(glyph_renderers_flattened[start: start+len(group)]) |
| 166 | + start += len(group) |
| 167 | + self.logger.debug("%d, %d, %d", len(HB_handles), |
| 168 | + len(glyph_renderers), num_total_lines) |
| 169 | + HB_handles_flattened = [a for b in HB_handles for a in b] |
| 170 | + args_checkbox = {name: glyph for name, glyph in zip(glyph_renderers_flattened, |
| 171 | + HB_handles_flattened)} |
| 172 | + code_checkbox = "len_labels = " + str(len(HB_handles)) + "; glyph_renderers = [" + ','.join(['[' + ','.join([str(idx) for idx |
| 173 | + in group]) + ']' for |
| 174 | + group in glyph_renderers]) + '];' + """ |
| 175 | + for (i = 0; i < len_labels; i++) { |
| 176 | + if (cb_obj.active.includes(i)) { |
| 177 | + // console.log('Setting to true: ' + i + '(' + glyph_renderers[i].length + ')') |
| 178 | + for (j = 0; j < glyph_renderers[i].length; j++) { |
| 179 | + glyph_renderers[i][j].visible = true; |
| 180 | + // console.log('Setting to true: ' + i + ' : ' + j) |
| 181 | + } |
| 182 | + } else { |
| 183 | + // console.log('Setting to false: ' + i + '(' + glyph_renderers[i].length + ')') |
| 184 | + for (j = 0; j < glyph_renderers[i].length; j++) { |
| 185 | + glyph_renderers[i][j].visible = false; |
| 186 | + // console.log('Setting to false: ' + i + ' : ' + j) |
| 187 | + } |
| 188 | + } |
| 189 | + } |
| 190 | + """ |
| 191 | + iteration_labels = ['warmstart data' if l == -1 or l == '-1' else str(l) for l in HB_iterations] |
| 192 | + self.logger.debug("iteration_labels: %s", str(iteration_labels)) |
| 193 | + self.logger.debug("HB_iterations: %s", str(HB_iterations)) |
| 194 | + |
| 195 | + callback = CustomJS(args=args_checkbox, code=code_checkbox) |
| 196 | + # TODO Use the CheckboxButtonGroup code after upgrading bokeh to >0.12.14 (it's prettier) |
| 197 | + #checkbox = CheckboxButtonGroup( |
| 198 | + checkbox = CheckboxGroup( |
| 199 | + labels=iteration_labels, |
| 200 | + active=list(range(len(iteration_labels))), |
| 201 | + callback=callback) |
| 202 | + # Select all/none: |
| 203 | + handle_list_as_string = str(list(range(len(HB_handles)))) |
| 204 | + select_all = Button(label="All", callback=CustomJS(args=dict({'checkbox':checkbox}, **args_checkbox), |
| 205 | + code="var labels = " + handle_list_as_string + "; checkbox.active = labels;" + code_checkbox.replace('cb_obj', 'checkbox'))) |
| 206 | + select_none = Button(label="None", callback=CustomJS(args=dict({'checkbox':checkbox}, **args_checkbox), |
| 207 | + code="var labels = []; checkbox.active = labels;" + code_checkbox.replace('cb_obj', 'checkbox'))) |
| 208 | + # Put it all together |
| 209 | + layout = column(p, row(widgetbox(select_all, select_none), widgetbox(checkbox))) |
| 210 | + return layout |
| 211 | + |
| 212 | + def get_longest_run(self, c_id, result_object): |
| 213 | + all_runs = result_object.get_runs_by_id(c_id) |
| 214 | + longest_run = all_runs[-1] |
| 215 | + while longest_run.loss is None: |
| 216 | + all_runs.pop() |
| 217 | + if len(all_runs) == 0: |
| 218 | + return False |
| 219 | + longest_run = all_runs[-1] |
| 220 | + return longest_run |
| 221 | + |
| 222 | + |
| 223 | + def get_jupyter(self): |
| 224 | + output_notebook() |
| 225 | + show(self.bokeh_plot) |
| 226 | + |
| 227 | + def get_html(self, d=None): |
| 228 | + bokeh_components = components(self.bokeh_plot) |
| 229 | + if d is not None: |
| 230 | + d["BOHB Learning Curves"] = {"bokeh" : bokeh_components} |
| 231 | + return bokeh_components |
| 232 | + |
0 commit comments