defplot_logs(experiments:List[Summary],smooth_factor:float=0,ignore_metrics:Optional[Set[str]]=None,pretty_names:bool=False,include_metrics:Optional[Set[str]]=None)->FigureFE:"""A function which will plot experiment histories for comparison viewing / analysis. Args: experiments: Experiment(s) to plot. smooth_factor: A non-negative float representing the magnitude of gaussian smoothing to apply (zero for none). pretty_names: Whether to modify the metric names in graph titles (True) or leave them alone (False). ignore_metrics: Any keys to ignore during plotting. include_metrics: A whitelist of keys to include during plotting. If None then all will be included. Returns: The handle of the pyplot figure. """# Sort to keep same colors between multiple runs of visualizationexperiments=humansorted(to_list(experiments),lambdaexp:exp.name)n_experiments=len(experiments)ifn_experiments==0:returnFigureFE.from_figure(make_subplots())ignore_keys=ignore_metricsorset()ignore_keys=to_set(ignore_keys)ignore_keys|={'epoch'}include_keys=to_set(include_metrics)# TODO: epoch should be indicated on the axis (top x axis?). Problem - different epochs per experiment.# TODO: figure out how ignore_metrics should interact with mode# TODO: when ds_id switches during training, prevent old id from connecting with new one (break every epoch?)ds_ids=set()metric_histories=defaultdict(_MetricGroup)# metric: MetricGroupforidx,experimentinenumerate(experiments):history=experiment.history# Since python dicts remember insertion order, sort the history so that train mode is always plotted on bottomformode,metricsinsorted(history.items(),key=lambdax:0ifx[0]=='train'else1ifx[0]=='eval'else2ifx[0]=='test'else3ifx[0]=='infer'else4):formetric,step_valinmetrics.items():base_metric,ds_id,*_=f'{metric}|'.split('|')# Plot acc|ds1 and acc|ds2 on same acc graphiflen(step_val)==0:continue# Ignore empty metricsifmetricinignore_keysorbase_metricinignore_keys:continue# Here we intentionally check against metric and not base_metric. If user wants to display per-ds they# can specify that in their include list: --include mcc 'mcc|usps'ifinclude_keysandmetricnotininclude_keys:continuemetric_histories[base_metric].add(idx,mode,ds_id,step_val)ds_ids.add(ds_id)metric_list=list(sorted(metric_histories.keys()))iflen(metric_list)==0:returnFigureFE.from_figure(make_subplots())ds_ids=humansorted(ds_ids)# Sort them to have consistent ordering (and thus symbols) between plot runsn_plots=len(metric_list)iflen(ds_ids)>9:# 9 b/c None is includedwarn("Plotting more than 8 different datasets isn't well supported. Symbols will be reused.")# Non-Shared legends aren't supported yet. If they get supported then maybe can have that feature here too.# https://github.com/plotly/plotly.js/issues/5099# https://github.com/plotly/plotly.js/issues/5098# map the metrics into an n x n grid, then remove any extra columns. Final grid will be n x m with m <= nn_rows=math.ceil(math.sqrt(n_plots))n_cols=math.ceil(n_plots/n_rows)metric_grid_location={}nd1_metrics=[]idx=0formetricinmetric_list:ifmetric_histories[metric].ndim()==1:# Delay placement of the 1D plots until the endnd1_metrics.append(metric)else:metric_grid_location[metric]=(idx//n_cols,idx%n_cols)idx+=1formetricinnd1_metrics:metric_grid_location[metric]=(idx//n_cols,idx%n_cols)idx+=1titles=[kfork,vinsorted(list(metric_grid_location.items()),key=lambdae:e[1][0]*n_cols+e[1][1])]ifpretty_names:titles=[prettify_metric_name(title)fortitleintitles]fig=make_subplots(rows=n_rows,cols=n_cols,subplot_titles=titles,shared_xaxes='all')fig.update_layout({'plot_bgcolor':'#FFF','hovermode':'closest','margin':{'t':50},'modebar':{'add':['hoverclosest','hovercompare'],'remove':['select2d','lasso2d']},'legend':{'tracegroupgap':5,'font':{'size':11}}})# Set x-labelsforidx,metricinenumerate(titles,start=1):plotly_idx=idxifidx>1else""x_axis_name=f'xaxis{plotly_idx}'y_axis_name=f'yaxis{plotly_idx}'ifmetric_histories[metric].ndim()>1:fig['layout'][x_axis_name]['title']='Steps'fig['layout'][x_axis_name]['showticklabels']=Truefig['layout'][x_axis_name]['linecolor']="#BCCCDC"fig['layout'][y_axis_name]['linecolor']="#BCCCDC"else:# Put blank data onto the axis to instantiate the domainrow,col=metric_grid_location[metric][0],metric_grid_location[metric][1]fig.add_annotation(text='',showarrow=False,row=row+1,col=col+1)# Hide the axis stufffig['layout'][x_axis_name]['showgrid']=Falsefig['layout'][x_axis_name]['zeroline']=Falsefig['layout'][x_axis_name]['visible']=Falsefig['layout'][y_axis_name]['showgrid']=Falsefig['layout'][y_axis_name]['zeroline']=Falsefig['layout'][y_axis_name]['visible']=False# If there is only 1 experiment, we will use alternate colors based on modecolor_offset=defaultdict(lambda:0)n_colors=n_experimentsifn_experiments==1:n_colors=4color_offset['eval']=1color_offset['test']=2color_offset['infer']=3colors=get_colors(n_colors=n_colors)alpha_colors=get_colors(n_colors=n_colors,alpha=0.3)# exp_id : {mode: {ds_id: {type: True}}}add_label=defaultdict(lambda:defaultdict(lambda:defaultdict(lambda:defaultdict(lambda:True))))# {row: {col: (x, y)}}ax_text=defaultdict(lambda:defaultdict(lambda:(0.0,0.9)))# Where to put the text on a given axis# Set up ds_id markers. The empty ds_id will have no extra marker. After that there are 4 configurations of 3-arm# marker, followed by 'x', '+', '*', and pound. After that it will just repeat the symbol set.ds_id_markers=[None,37,38,39,40,34,33,35,36]# https://plotly.com/python/marker-style/ds_id_markers={k:vfork,vinzip(ds_ids,cycle(ds_id_markers))}# Plotly doesn't support z-order, so delay insertion until all the plots are figured out:# https://github.com/plotly/plotly.py/issues/2345z_order=defaultdict(list)# {order: [(plotly element, row, col), ...]}# Figure out the legend orderinglegend_order=[]forexp_idx,experimentinenumerate(experiments):formetric,groupinmetric_histories.items():formodeingroup.modes(exp_idx):fords_idingroup.ds_ids(exp_idx,mode):ds_title=f"{ds_id} "ifds_idelse''title=f"{experiment.name} ({ds_title}{mode})"ifn_experiments>1elsef"{ds_title}{mode}"legend_order.append(title)legend_order.sort()legend_order={legend:orderfororder,legendinenumerate(legend_order)}# Actually do the plottingforexp_idx,experimentinenumerate(experiments):formetric,groupinmetric_histories.items():row,col=metric_grid_location[metric][0],metric_grid_location[metric][1]ifgroup.ndim()==1:# Single valueformodeingroup.modes(exp_idx):fords_idingroup.ds_ids(exp_idx,mode):ds_title=f"{ds_id} "ifds_idelse''prefix=f"{experiment.name} ({ds_title}{mode})"ifn_experiments>1elsef"{ds_title}{mode}"plotly_idx=row*n_cols+col+1ifrow*n_cols+col+1>1else''fig.add_annotation(text=f"{prefix}: {group.get_val(exp_idx,mode,ds_id)}",font={'color':colors[exp_idx+color_offset[mode]]},showarrow=False,xref=f'x{plotly_idx} domain',xanchor='left',x=ax_text[row][col][0],yref=f'y{plotly_idx} domain',yanchor='top',y=ax_text[row][col][1],exclude_empty_subplots=False)ax_text[row][col]=(ax_text[row][col][0],ax_text[row][col][1]-0.1)ifax_text[row][col][1]<0:ax_text[row][col]=(ax_text[row][col][0]+0.5,0.9)elifgroup.ndim()==2:formode,dsvingroup[exp_idx].items():color=colors[exp_idx+color_offset[mode]]fords_id,dataindsv.items():ds_title=f"{ds_id} "ifds_idelse''title=f"{experiment.name} ({ds_title}{mode})"ifn_experiments>1elsef"{ds_title}{mode}"ifdata.shape[0]<2:x=data[0][0]y=data[0][1]y_min=Noney_max=Noneifisinstance(y,ValWithError):y_min=y.y_miny_max=y.y_maxy=y.ymarker_style='circle'ifmode=='train'else'diamond'ifmode=='eval' \
else'square'ifmode=='test'else'hexagram'limit_data=[(y_max,y_min)]ify_maxisnotNoneandy_minisnotNoneelseNonetip_text="%{x}: (%{customdata[1]:.3f}, %{y:.3f}, %{customdata[0]:.3f})"if \
limit_dataisnotNoneelse"%{x}: %{y:.3f}"error_y=Noneiflimit_dataisNoneelse{'type':'data','symmetric':False,'array':[y_max-y],'arrayminus':[y-y_min]}z_order[2].append((go.Scatter(x=[x],y=[y],name=title,legendgroup=title,customdata=limit_data,hovertemplate=tip_text,mode='markers',marker={'color':color,'size':12,'symbol':_symbol_mash(marker_style,ds_id_markers[ds_id]),'line':{'width':1.5,'color':'White'}},error_y=error_y,showlegend=add_label[exp_idx][mode][ds_id]['patch'],legendrank=legend_order[title]),row,col))add_label[exp_idx][mode][ds_id]['patch']=Falseelse:# We can draw a liney=data[:,1]y_min=Noney_max=Noneifisinstance(y[0],ValWithError):y=np.stack([e.as_tuple()foreiny])y_min=y[:,0]y_max=y[:,2]y=y[:,1]ifsmooth_factor!=0:y_min=gaussian_filter1d(y_min,sigma=smooth_factor)y_max=gaussian_filter1d(y_max,sigma=smooth_factor)# TODO - for smoothed lines, plot original data in background but greyed outifsmooth_factor!=0:y=gaussian_filter1d(y,sigma=smooth_factor)x=data[:,0]linestyle='solid'ifmode=='train'else'dash'ifmode=='eval'else'dot'if \
mode=='test'else'dashdot'limit_data=[(mx,mn)formx,mninzip(y_max,y_min)]ify_maxisnotNoneandy_minis \
notNoneelseNonetip_text="%{x}: (%{customdata[1]:.3f}, %{y:.3f}, %{customdata[0]:.3f})"if \
limit_dataisnotNoneelse"%{x}: %{y:.3f}"z_order[1].append((go.Scatter(x=x,y=y,name=title,legendgroup=title,mode="lines+markers"ifds_id_markers[ds_id]else'lines',marker={'color':color,'size':8,'line':{'width':2,'color':'DarkSlateGrey'},'maxdisplayed':10,'symbol':ds_id_markers[ds_id]},line={'dash':linestyle,'color':color},customdata=limit_data,hovertemplate=tip_text,showlegend=add_label[exp_idx][mode][ds_id]['line'],legendrank=legend_order[title]),row,col))add_label[exp_idx][mode][ds_id]['line']=Falseiflimit_dataisnotNone:z_order[0].append((go.Scatter(x=x,y=y_max,mode='lines',line={'width':0},legendgroup=title,showlegend=False,hoverinfo='skip'),row,col))z_order[0].append((go.Scatter(x=x,y=y_min,mode='lines',line={'width':0},fillcolor=alpha_colors[exp_idx+color_offset[mode]],fill='tonexty',legendgroup=title,showlegend=False,hoverinfo='skip'),row,col))else:# Some kind of image or matrix. Not implemented yet.passforzinsorted(list(z_order.keys())):plts=z_order[z]forplt,row,colinplts:fig.add_trace(plt,row=row+1,col=col+1)# If inside a jupyter notebook then force the height based on number of rowsifin_notebook():fig.update_layout(height=280*n_rows)returnFigureFE.from_figure(fig)
defvisualize_logs(experiments:List[Summary],save_path:str=None,smooth_factor:float=0,pretty_names:bool=False,ignore_metrics:Optional[Set[str]]=None,include_metrics:Optional[Set[str]]=None,verbose:bool=True):"""A function which will save or display experiment histories for comparison viewing / analysis. Args: experiments: Experiment(s) to plot. save_path: The path where the figure should be saved, or None to display the figure to the screen. smooth_factor: A non-negative float representing the magnitude of gaussian smoothing to apply (zero for none). pretty_names: Whether to modify the metric names in graph titles (True) or leave them alone (False). ignore_metrics: Any metrics to ignore during plotting. include_metrics: A whitelist of metric keys (None whitelists all keys). verbose: Whether to print out the save location. """fig=plot_logs(experiments,smooth_factor=smooth_factor,pretty_names=pretty_names,ignore_metrics=ignore_metrics,include_metrics=include_metrics)fig.show(save_path=save_path,verbose=verbose,scale=5)