docs: def finetune_classification_cnn(config):
      """ Main function. """
      # read params
      dataset = config['dataset']
      x_names = config['x_names']
      y_name = config['y_name']
      model_dir = config['model_dir']
      debug = config['debug']
  
      num_classes = None
      if 'num_classes' in config.keys():
          num_classes = config['num_classes']
  
      batch_size = config['training']['batch_size']
      train_pct = config['training']['train_pct']
      model_save_period = config['training']['model_save_period']
  
      data_aug_config = config['data_augmentation']
      preproc_config = config['preprocessing']
      iterator_config = config['data_iteration']
      model_config = config['model']
      base_model_config = model_config['base']
      optimization_config = config['optimization']
      train_config = config['training']
  
      generator_image_shape = None
      if 'image_shape' in data_aug_config.keys():
          generator_image_shape = data_aug_config['image_shape']
      optimizer_name = optimization_config['optimizer']
  
      model_params = {}
      if 'params' in model_config.keys():
          model_params = model_config['params']
      
      base_model_params = {}
      if 'params' in base_model_config.keys():
          base_model_params = base_model_config['params']
  
      if debug:
          seed = 108
          random.seed(seed)
          np.random.seed(seed)
  
      # generate model dir
      if not os.path.exists(model_dir):
          os.mkdir(model_dir)
      model_id = utils.gen_experiment_id()
      model_dir = os.path.join(model_dir, 'model_%s' %(model_id))
      if not os.path.exists(model_dir):
          os.mkdir(model_dir)
      logging.info('Saving model to %s' %(model_dir))
      latest_model_filename = os.path.join(model_dir, 'weights_{epoch:05d}.h5')
      best_model_filename = os.path.join(model_dir, 'weights.h5')
  
      # save config
      training_config_filename = os.path.join(model_dir, 'training_config.yaml')
      config.save(training_config_filename)
  
      # open dataset
      dataset = TensorDataset.open(dataset)
      
      # split dataset
      indices_filename = os.path.join(model_dir, 'splits.npz')
      if os.path.exists(indices_filename):
          indices = np.load(indices_filename)['arr_0'].tolist()
          train_indices = indices['train']
          val_indices = indices['val']
      else:
          train_indices, val_indices = dataset.split(train_pct)
          indices = np.array({'train':train_indices,
                              'val':val_indices})
          np.savez_compressed(indices_filename, indices)
      num_train = train_indices.shape[0]
      num_val = val_indices.shape[0]
      val_steps = int(np.ceil(float(num_val) / batch_size))
  
      # init generator
      train_generator_filename = os.path.join(model_dir, 'train_preprocessor.pkl')
      val_generator_filename = os.path.join(model_dir, 'val_preprocessor.pkl')
      if os.path.exists(train_generator_filename):
          logging.info('Loading generators')
          train_generator = pkl.load(open(train_generator_filename, 'rb'))
          val_generator = pkl.load(open(val_generator_filename, 'rb'))
      else:
          logging.info('Fitting generator')
          train_generator = TensorDataGenerator(num_classes=num_classes,
                                                **data_aug_config)
          val_generator = TensorDataGenerator(featurewise_center=data_aug_config['featurewise_center'],
                                              featurewise_std_normalization=data_aug_config['featurewise_std_normalization'],
                                              image_shape=generator_image_shape,
                                              num_classes=num_classes)
          fit_start = time.time()
          train_generator.fit(dataset, x_names, y_name, indices=train_indices, **preproc_config)
          val_generator.mean = train_generator.mean
          val_generator.std = train_generator.std
          val_generator.min_output = train_generator.min_output
          val_generator.max_output = train_generator.max_output
          val_generator.num_classes = train_generator.num_classes
          fit_stop = time.time()
          logging.info('Generator fit took %.3f sec' %(fit_stop - fit_start))
          pkl.dump(train_generator, open(train_generator_filename, 'wb'))
          pkl.dump(val_generator, open(val_generator_filename, 'wb'))
          
      if num_classes is None:
          num_classes = int(train_generator.num_classes)
  
      # init iterator
      train_iterator = train_generator.flow_from_dataset(dataset, x_names, y_name,
                                                         indices=train_indices,
                                                         batch_size=batch_size,
                                                         **iterator_config)
      val_iterator = val_generator.flow_from_dataset(dataset, x_names, y_name,
                                                     indices=val_indices,
                                                     batch_size=batch_size,
                                                     **iterator_config)
  
      # setup model
      base_cnn = ClassificationCNN.open(base_model_config['model'],
                                        base_model_config['type'],
                                        input_name=x_names[0],
                                        **base_model_params)
      cnn = FinetunedClassificationCNN(base_cnn=base_cnn,
                                       name='dexresnet',
                                       num_classes=num_classes,
                                       output_name=y_name,
                                       im_preprocessor=val_generator,
                                       **model_params)
  
      # setup training
      cnn.freeze_base_cnn()
      if optimizer_name == 'sgd':
          optimizer = SGD(lr=optimization_config['lr'],
                          momentum=optimization_config['momentum'])
      elif optimizer_name == 'adam':
          optimizer = Adam(lr=optimization_config['lr'])
      else:
          raise ValueError('Optimizer %s not supported!' %(optimizer_name))
      model = cnn.model
      model.compile(optimizer=optimizer,
                    loss=optimization_config['loss'],
                    metrics=optimization_config['metrics'])
  
      # train
      steps_per_epoch = int(np.ceil(float(num_train) / batch_size))
      latest_model_ckpt = ModelCheckpoint(latest_model_filename, period=model_save_period)
      best_model_ckpt = ModelCheckpoint(best_model_filename,
                                        save_best_only=True,
                                        period=model_save_period)
      train_history_cb = TrainHistory(model_dir)
      callbacks = [latest_model_ckpt, best_model_ckpt, train_history_cb]
      history = model.fit_generator(train_iterator,
                                    steps_per_epoch=steps_per_epoch,
                                    epochs=train_config['epochs'],
                                    callbacks=callbacks,
                                    validation_data=val_iterator,
                                    validation_steps=val_steps,
                                    class_weight=train_config['class_weight'],
                                    use_multiprocessing=train_config['use_multiprocessing'])
  
      # save model
      cnn.save(model_dir)
      
      # save history
      history_filename = os.path.join(model_dir, 'history.pkl')
      pkl.dump(history.history, open(history_filename, 'wb'))