base.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import chainer
  2. import chainer.functions as F
  3. import numpy as np
  4. import abc
  5. import logging
  6. from chainer.backends import cuda
  7. from chainer.optimizer_hooks import Lasso
  8. from chainer.optimizer_hooks import WeightDecay
  9. from chainer.training import StandardUpdater, extensions
  10. from chainer.serializers import save_npz
  11. from chainer_addons.models import ModelType
  12. from chainer_addons.models import Classifier
  13. from chainer_addons.models import PrepareType
  14. from chainer_addons.training import optimizer, optimizer_hooks
  15. from chainer_addons.functions import smoothed_cross_entropy
  16. from cvdatasets.annotations import AnnotationType
  17. from cvdatasets.utils import new_iterator
  18. from functools import partial
  19. from os.path import join
  20. from bdb import BdbQuit
  21. def check_param_for_decay(param):
  22. return param.name != "alpha"
  23. def _format_kwargs(kwargs):
  24. return " ".join([f"{key}={value}" for key, value in kwargs.items()])
  25. class _ModelMixin(abc.ABC):
  26. """This mixin is responsible for optimizer creation, model creation,
  27. model wrapping around a classifier and model weights loading.
  28. """
  29. def __init__(self, classifier_cls, classifier_kwargs={}, model_kwargs={}, *args, **kwargs):
  30. super(_ModelMixin, self).__init__(*args, **kwargs)
  31. self.classifier_cls = classifier_cls
  32. self.classifier_kwargs = classifier_kwargs
  33. self.model_kwargs = model_kwargs
  34. def wrap_model(self, opts):
  35. clf_class, kwargs = self.classifier_cls, self.classifier_kwargs
  36. self.clf = clf_class(
  37. model=self.model,
  38. loss_func=self._loss_func(opts),
  39. **kwargs)
  40. logging.info(" ".join([
  41. f"Wrapped the model around {clf_class.__name__}",
  42. f"with kwargs: {_format_kwargs(kwargs)}",
  43. ]))
  44. def _loss_func(self, opts):
  45. if opts.l1_loss:
  46. return F.hinge
  47. elif opts.label_smoothing >= 0:
  48. assert opts.label_smoothing < 1, \
  49. "Label smoothing factor must be less than 1!"
  50. return partial(smoothed_cross_entropy,
  51. N=self.n_classes,
  52. eps=opts.label_smoothing)
  53. else:
  54. return F.softmax_cross_entropy
  55. def init_optimizer(self, opts):
  56. """Creates an optimizer for the classifier """
  57. opt_kwargs = {}
  58. if opts.optimizer == "rmsprop":
  59. opt_kwargs["alpha"] = 0.9
  60. self.opt = optimizer(opts.optimizer,
  61. self.clf,
  62. opts.learning_rate,
  63. decay=0, gradient_clipping=False, **opt_kwargs
  64. )
  65. if opts.decay:
  66. reg_kwargs = {}
  67. if opts.l1_loss:
  68. reg_cls = Lasso
  69. elif opts.pooling == "alpha":
  70. reg_cls = optimizer_hooks.SelectiveWeightDecay
  71. reg_kwargs["selection"] = check_param_for_decay
  72. else:
  73. reg_cls = WeightDecay
  74. logging.info(f"Adding {reg_cls.__name__} ({opts.decay:e})")
  75. self.opt.add_hook(reg_cls(opts.decay, **reg_kwargs))
  76. if opts.only_head:
  77. assert not opts.recurrent, "FIX ME! Not supported yet!"
  78. logging.warning("========= Fine-tuning only classifier layer! =========")
  79. self.model.disable_update()
  80. self.model.fc.enable_update()
  81. def init_model(self, opts):
  82. """creates backbone CNN model. This model is wrapped around the classifier later"""
  83. self.model = ModelType.new(
  84. model_type=self.model_info.class_key,
  85. input_size=opts.input_size,
  86. **self.model_kwargs,
  87. # pooling=opts.pooling,
  88. # pooling_params=dict(
  89. # init_alpha=opts.init_alpha,
  90. # output_dim=8192,
  91. # normalize=opts.normalize),
  92. # aux_logits=False
  93. )
  94. def load_model_weights(self, args):
  95. if args.from_scratch:
  96. logging.info("Training a {0.__class__.__name__} model from scratch!".format(self.model))
  97. loader = self.model.reinitialize_clf
  98. self.weights = None
  99. else:
  100. if args.load:
  101. self.weights = args.load
  102. logging.info("Loading already fine-tuned weights from \"{}\"".format(self.weights))
  103. loader = partial(self.model.load_for_inference, weights=self.weights)
  104. else:
  105. self.weights = join(
  106. self.data_info.BASE_DIR,
  107. self.data_info.MODEL_DIR,
  108. self.model_info.folder,
  109. self.model_info.weights
  110. )
  111. logging.info("Loading pre-trained weights \"{}\"".format(self.weights))
  112. loader = partial(self.model.load_for_finetune, weights=self.weights)
  113. if hasattr(self.clf, "output_size"):
  114. feat_size = self.clf.output_size
  115. if hasattr(self.clf, "loader"):
  116. loader = self.clf.loader(loader)
  117. logging.info(f"Part features size after encoding: {feat_size}")
  118. loader(n_classes=self.n_classes, feat_size=feat_size)
  119. self.clf.cleargrads()
  120. class _DatasetMixin(abc.ABC):
  121. """
  122. This mixin is responsible for annotation loading and for
  123. dataset and iterator creation.
  124. """
  125. def __init__(self, dataset_cls, *args, **kwargs):
  126. super(_DatasetMixin, self).__init__(*args, **kwargs)
  127. self.dataset_cls = dataset_cls
  128. @property
  129. def n_classes(self):
  130. return self.part_info.n_classes + self.dataset_cls.label_shift
  131. def new_dataset(self, opts, size, subset, augment):
  132. """Creates a dataset for a specific subset and certain options"""
  133. kwargs = dict(
  134. subset=subset,
  135. dataset_cls=self.dataset_cls,
  136. )
  137. # if opts.use_parts:
  138. # kwargs.update(dict(
  139. # no_glob=opts.no_global,
  140. # ))
  141. if not opts.only_head:
  142. kwargs.update(dict(
  143. preprocess=self.prepare,
  144. augment=augment,
  145. size=size,
  146. center_crop_on_val=not opts.no_center_crop_on_val,
  147. ))
  148. d = self.annot.new_dataset(**kwargs)
  149. logging.info("Loaded {} images".format(len(d)))
  150. logging.info("Data augmentation is {}abled".format("en" if augment else "dis"))
  151. # logging.info("Global feature is {}used".format("not " if opts.no_global else ""))
  152. return d
  153. def init_annotations(self, opts):
  154. """Reads annotations and creates annotation instance, which holds important infos about the dataset"""
  155. annot_cls = AnnotationType.get(opts.dataset).value
  156. self.annot = annot_cls(opts.data, opts.parts)
  157. self.data_info = self.annot.info
  158. self.model_info = self.data_info.MODELS[opts.model_type]
  159. self.part_info = self.data_info.PARTS[opts.parts]
  160. if opts.only_head:
  161. self.annot.feature_model = opts.model_type
  162. def init_datasets(self, opts):
  163. self.dataset_cls.label_shift = opts.label_shift
  164. size = self.model.meta.input_size
  165. self.prepare = partial(PrepareType[opts.prepare_type](self.model),
  166. swap_channels=opts.swap_channels,
  167. keep_ratio=not opts.no_center_crop_on_val,
  168. )
  169. logging.info(" ".join([
  170. f"Created {self.model.__class__.__name__} model",
  171. f"with \"{opts.prepare_type}\" prepare function.",
  172. f"Image input size: {size}",
  173. ]))
  174. self.train_data = self.new_dataset(opts, size, "train", True)
  175. self.val_data = self.new_dataset(opts, size, "test", False)
  176. def init_iterators(self, opts):
  177. """Creates training and validation iterators from training and validation datasets"""
  178. self.train_iter, _ = new_iterator(self.train_data,
  179. opts.n_jobs, opts.batch_size
  180. )
  181. self.val_iter, _ = new_iterator(self.val_data,
  182. opts.n_jobs, opts.batch_size,
  183. repeat=False, shuffle=False
  184. )
  185. class _TrainerMixin(abc.ABC):
  186. """This mixin is responsible for updater, evaluator and trainer creation.
  187. Furthermore, it implements the run method
  188. """
  189. def __init__(self, updater_cls, updater_kwargs={}, *args, **kwargs):
  190. super(_TrainerMixin, self).__init__(*args, **kwargs)
  191. self.updater_cls = updater_cls
  192. self.updater_kwargs = updater_kwargs
  193. def init_updater(self):
  194. """Creates an updater from training iterator and the optimizer."""
  195. self.updater = self.updater_cls(
  196. iterator=self.train_iter,
  197. optimizer=self.opt,
  198. device=self.device,
  199. **self.updater_kwargs,
  200. )
  201. logging.info(" ".join([
  202. f"Using single GPU: {self.device}.",
  203. f"{self.updater_cls.__name__} is initialized",
  204. f"with following kwargs: {_format_kwargs(self.updater_kwargs)}"
  205. ])
  206. )
  207. def init_evaluator(self, default_name="val"):
  208. """Creates evaluation extension from validation iterator and the classifier."""
  209. self.evaluator = extensions.Evaluator(
  210. iterator=self.val_iter,
  211. target=self.clf,
  212. device=self.device)
  213. self.evaluator.default_name = default_name
  214. def run(self, trainer_cls, opts, *args, **kwargs):
  215. trainer = trainer_cls(
  216. opts=opts,
  217. updater=self.updater,
  218. evaluator=self.evaluator,
  219. weights=self.weights,
  220. *args, **kwargs
  221. )
  222. def dump(suffix):
  223. if opts.only_eval or opts.no_snapshot:
  224. return
  225. save_npz(join(trainer.out,
  226. "clf_{}.npz".format(suffix)), self.clf)
  227. save_npz(join(trainer.out,
  228. "model_{}.npz".format(suffix)), self.model)
  229. try:
  230. trainer.run(init_eval=opts.init_eval or opts.only_eval)
  231. except (KeyboardInterrupt, BdbQuit) as e:
  232. raise e
  233. except Exception as e:
  234. dump("exception")
  235. raise e
  236. else:
  237. dump("final")
  238. class DefaultFinetuner(_ModelMixin, _DatasetMixin, _TrainerMixin):
  239. """ The default Finetuner gathers together the creations of all needed
  240. components and call them in the correct order
  241. """
  242. def __init__(self, opts, *args, **kwargs):
  243. super(DefaultFinetuner, self).__init__(*args, **kwargs)
  244. self.gpu_config(opts, *args, **kwargs)
  245. self.init_annotations(opts)
  246. self.init_model(opts)
  247. self.init_datasets(opts)
  248. self.init_iterators(opts)
  249. self.wrap_model(opts)
  250. self.load_model_weights(opts)
  251. self.init_optimizer(opts)
  252. self.init_updater()
  253. self.init_evaluator()
  254. def gpu_config(self, opts, *args, **kwargs):
  255. if -1 in opts.gpu:
  256. self.device = -1
  257. else:
  258. self.device = opts.gpu[0]
  259. cuda.get_device_from_id(self.device).use()