base.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  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. msg = "Loading already fine-tuned weights from \"{}\""
  103. loader_func = self.model.load_for_inference
  104. else:
  105. if args.weights:
  106. msg = "Loading custom pre-trained weights \"{}\""
  107. self.weights = args.weights
  108. else:
  109. msg = "Loading default pre-trained weights \"{}\""
  110. self.weights = join(
  111. self.data_info.BASE_DIR,
  112. self.data_info.MODEL_DIR,
  113. self.model_info.folder,
  114. self.model_info.weights
  115. )
  116. loader_func = self.model.load_for_finetune
  117. logging.info(msg.format(self.weights))
  118. kwargs = dict(
  119. weights=self.weights,
  120. strict=args.load_strict,
  121. headless=args.headless,
  122. )
  123. loader = partial(loader_func, **kwargs)
  124. feat_size = self.model.meta.feature_size
  125. if hasattr(self.clf, "output_size"):
  126. feat_size = self.clf.output_size
  127. if hasattr(self.clf, "loader"):
  128. loader = self.clf.loader(loader)
  129. logging.info(f"Part features size after encoding: {feat_size}")
  130. loader(n_classes=self.n_classes, feat_size=feat_size)
  131. self.clf.cleargrads()
  132. class _DatasetMixin(abc.ABC):
  133. """
  134. This mixin is responsible for annotation loading and for
  135. dataset and iterator creation.
  136. """
  137. def __init__(self, dataset_cls, dataset_kwargs_factory, *args, **kwargs):
  138. super(_DatasetMixin, self).__init__(*args, **kwargs)
  139. self.dataset_cls = dataset_cls
  140. self.dataset_kwargs_factory = dataset_kwargs_factory
  141. @property
  142. def n_classes(self):
  143. return self.part_info.n_classes + self.dataset_cls.label_shift
  144. def new_dataset(self, opts, size, subset, augment):
  145. """Creates a dataset for a specific subset and certain options"""
  146. if self.dataset_kwargs_factory is not None and callable(self.dataset_kwargs_factory):
  147. kwargs = self.dataset_kwargs_factory(opts, subset, augment)
  148. else:
  149. kwargs = dict()
  150. kwargs.update(dict(
  151. subset=subset,
  152. dataset_cls=self.dataset_cls,
  153. ))
  154. # if opts.use_parts:
  155. # kwargs.update(dict(
  156. # no_glob=opts.no_global,
  157. # ))
  158. if not opts.only_head:
  159. kwargs.update(dict(
  160. preprocess=self.prepare,
  161. augment=augment,
  162. size=size,
  163. center_crop_on_val=not opts.no_center_crop_on_val,
  164. ))
  165. d = self.annot.new_dataset(**kwargs)
  166. logging.info("Loaded {} images".format(len(d)))
  167. logging.info("Data augmentation is {}abled".format("en" if augment else "dis"))
  168. # logging.info("Global feature is {}used".format("not " if opts.no_global else ""))
  169. return d
  170. def init_annotations(self, opts):
  171. """Reads annotations and creates annotation instance, which holds important infos about the dataset"""
  172. annot_cls = AnnotationType.get(opts.dataset).value
  173. self.annot = annot_cls(root_or_infofile=opts.data, parts=opts.parts, load_strict=False)
  174. self.data_info = self.annot.info
  175. self.model_info = self.data_info.MODELS[opts.model_type]
  176. self.part_info = self.data_info.PARTS[opts.parts]
  177. if opts.only_head:
  178. self.annot.feature_model = opts.model_type
  179. self.dataset_cls.label_shift = opts.label_shift
  180. def init_datasets(self, opts):
  181. size = self.model.meta.input_size
  182. self.prepare = partial(PrepareType[opts.prepare_type](self.model),
  183. swap_channels=opts.swap_channels,
  184. keep_ratio=not opts.no_center_crop_on_val,
  185. )
  186. logging.info(" ".join([
  187. f"Created {self.model.__class__.__name__} model",
  188. f"with \"{opts.prepare_type}\" prepare function.",
  189. f"Image input size: {size}",
  190. ]))
  191. self.train_data = self.new_dataset(opts, size, "train", True)
  192. self.val_data = self.new_dataset(opts, size, "test", False)
  193. def init_iterators(self, opts):
  194. """Creates training and validation iterators from training and validation datasets"""
  195. kwargs = dict(n_jobs=opts.n_jobs, batch_size=opts.batch_size)
  196. if hasattr(self.train_data, "new_iterator"):
  197. self.train_iter, _ = self.train_data.new_iterator(**kwargs)
  198. else:
  199. self.train_iter, _ = new_iterator(self.train_data, **kwargs)
  200. if hasattr(self.val_data, "new_iterator"):
  201. self.val_iter, _ = self.val_data.new_iterator(**kwargs,
  202. repeat=False, shuffle=False
  203. )
  204. else:
  205. self.val_iter, _ = new_iterator(self.val_data,
  206. **kwargs, repeat=False, shuffle=False
  207. )
  208. class _TrainerMixin(abc.ABC):
  209. """This mixin is responsible for updater, evaluator and trainer creation.
  210. Furthermore, it implements the run method
  211. """
  212. def __init__(self, updater_cls, updater_kwargs={}, *args, **kwargs):
  213. super(_TrainerMixin, self).__init__(*args, **kwargs)
  214. self.updater_cls = updater_cls
  215. self.updater_kwargs = updater_kwargs
  216. def init_updater(self):
  217. """Creates an updater from training iterator and the optimizer."""
  218. self.updater = self.updater_cls(
  219. iterator=self.train_iter,
  220. optimizer=self.opt,
  221. device=self.device,
  222. **self.updater_kwargs,
  223. )
  224. logging.info(" ".join([
  225. f"Using single GPU: {self.device}.",
  226. f"{self.updater_cls.__name__} is initialized",
  227. f"with following kwargs: {_format_kwargs(self.updater_kwargs)}"
  228. ])
  229. )
  230. def init_evaluator(self, default_name="val"):
  231. """Creates evaluation extension from validation iterator and the classifier."""
  232. self.evaluator = extensions.Evaluator(
  233. iterator=self.val_iter,
  234. target=self.clf,
  235. device=self.device)
  236. self.evaluator.default_name = default_name
  237. def run(self, trainer_cls, opts, *args, **kwargs):
  238. trainer = trainer_cls(
  239. opts=opts,
  240. updater=self.updater,
  241. evaluator=self.evaluator,
  242. weights=self.weights,
  243. *args, **kwargs
  244. )
  245. logging.info("Snapshotting is {}abled".format("dis" if opts.no_snapshot else "en"))
  246. def dump(suffix):
  247. if opts.only_eval or opts.no_snapshot:
  248. return
  249. save_npz(join(trainer.out,
  250. "clf_{}.npz".format(suffix)), self.clf)
  251. save_npz(join(trainer.out,
  252. "model_{}.npz".format(suffix)), self.model)
  253. try:
  254. trainer.run(opts.init_eval or opts.only_eval)
  255. except (KeyboardInterrupt, BdbQuit) as e:
  256. raise e
  257. except Exception as e:
  258. dump("exception")
  259. raise e
  260. else:
  261. dump("final")
  262. class DefaultFinetuner(_ModelMixin, _DatasetMixin, _TrainerMixin):
  263. """ The default Finetuner gathers together the creations of all needed
  264. components and call them in the correct order
  265. """
  266. def __init__(self, opts, *args, **kwargs):
  267. super(DefaultFinetuner, self).__init__(*args, **kwargs)
  268. self.gpu_config(opts, *args, **kwargs)
  269. cuda.get_device_from_id(self.device).use()
  270. self.init_annotations(opts)
  271. self.init_model(opts)
  272. self.init_datasets(opts)
  273. self.init_iterators(opts)
  274. self.wrap_model(opts)
  275. self.load_model_weights(opts)
  276. self.init_optimizer(opts)
  277. self.init_updater()
  278. self.init_evaluator()
  279. def gpu_config(self, opts, *args, **kwargs):
  280. if -1 in opts.gpu:
  281. self.device = -1
  282. else:
  283. self.device = opts.gpu[0]