base.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. #copyright (c) 2020 PaddlePaddle Authors. All Rights Reserve.
  2. #
  3. #Licensed under the Apache License, Version 2.0 (the "License");
  4. #you may not use this file except in compliance with the License.
  5. #You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. #Unless required by applicable law or agreed to in writing, software
  10. #distributed under the License is distributed on an "AS IS" BASIS,
  11. #WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. #See the License for the specific language governing permissions and
  13. #limitations under the License.
  14. from __future__ import absolute_import
  15. import paddle.fluid as fluid
  16. import os
  17. import numpy as np
  18. import time
  19. import math
  20. import yaml
  21. import copy
  22. import json
  23. import functools
  24. import paddlex.utils.logging as logging
  25. from paddlex.utils import seconds_to_hms
  26. import paddlex
  27. from collections import OrderedDict
  28. from os import path as osp
  29. from paddle.fluid.framework import Program
  30. from .utils.pretrain_weights import get_pretrain_weights
  31. def dict2str(dict_input):
  32. out = ''
  33. for k, v in dict_input.items():
  34. try:
  35. v = round(float(v), 6)
  36. except:
  37. pass
  38. out = out + '{}={}, '.format(k, v)
  39. return out.strip(', ')
  40. class BaseAPI:
  41. def __init__(self, model_type):
  42. self.model_type = model_type
  43. # 现有的CV模型都有这个属性,而这个属且也需要在eval时用到
  44. self.num_classes = None
  45. self.labels = None
  46. self.version = paddlex.__version__
  47. if paddlex.env_info['place'] == 'cpu':
  48. self.places = fluid.cpu_places()
  49. else:
  50. self.places = fluid.cuda_places()
  51. self.exe = fluid.Executor(self.places[0])
  52. self.train_prog = None
  53. self.test_prog = None
  54. self.parallel_train_prog = None
  55. self.train_inputs = None
  56. self.test_inputs = None
  57. self.train_outputs = None
  58. self.test_outputs = None
  59. self.train_data_loader = None
  60. self.eval_metrics = None
  61. # 若模型是从inference model加载进来的,无法调用训练接口进行训练
  62. self.trainable = True
  63. # 是否使用多卡间同步BatchNorm均值和方差
  64. self.sync_bn = False
  65. # 当前模型状态
  66. self.status = 'Normal'
  67. def _get_single_card_bs(self, batch_size):
  68. if batch_size % len(self.places) == 0:
  69. return int(batch_size // len(self.places))
  70. else:
  71. raise Exception("Please support correct batch_size, \
  72. which can be divided by available cards({}) in {}".
  73. format(paddlex.env_info['num'],
  74. paddlex.env_info['place']))
  75. def build_program(self):
  76. # 构建训练网络
  77. self.train_inputs, self.train_outputs = self.build_net(mode='train')
  78. self.train_prog = fluid.default_main_program()
  79. startup_prog = fluid.default_startup_program()
  80. # 构建预测网络
  81. self.test_prog = fluid.Program()
  82. with fluid.program_guard(self.test_prog, startup_prog):
  83. with fluid.unique_name.guard():
  84. self.test_inputs, self.test_outputs = self.build_net(
  85. mode='test')
  86. self.test_prog = self.test_prog.clone(for_test=True)
  87. def arrange_transforms(self, transforms, mode='train'):
  88. # 给transforms添加arrange操作
  89. if self.model_type == 'classifier':
  90. arrange_transform = paddlex.cls.transforms.ArrangeClassifier
  91. elif self.model_type == 'segmenter':
  92. arrange_transform = paddlex.seg.transforms.ArrangeSegmenter
  93. elif self.model_type == 'detector':
  94. arrange_name = 'Arrange{}'.format(self.__class__.__name__)
  95. arrange_transform = getattr(paddlex.det.transforms, arrange_name)
  96. else:
  97. raise Exception("Unrecognized model type: {}".format(
  98. self.model_type))
  99. if type(transforms.transforms[-1]).__name__.startswith('Arrange'):
  100. transforms.transforms[-1] = arrange_transform(mode=mode)
  101. else:
  102. transforms.transforms.append(arrange_transform(mode=mode))
  103. def build_train_data_loader(self, dataset, batch_size):
  104. # 初始化data_loader
  105. if self.train_data_loader is None:
  106. self.train_data_loader = fluid.io.DataLoader.from_generator(
  107. feed_list=list(self.train_inputs.values()),
  108. capacity=64,
  109. use_double_buffer=True,
  110. iterable=True)
  111. batch_size_each_gpu = self._get_single_card_bs(batch_size)
  112. generator = dataset.generator(
  113. batch_size=batch_size_each_gpu, drop_last=True)
  114. self.train_data_loader.set_sample_list_generator(
  115. dataset.generator(batch_size=batch_size_each_gpu),
  116. places=self.places)
  117. def export_quant_model(self,
  118. dataset,
  119. save_dir,
  120. batch_size=1,
  121. batch_num=10,
  122. cache_dir="./temp"):
  123. self.arrange_transforms(transforms=dataset.transforms, mode='quant')
  124. dataset.num_samples = batch_size * batch_num
  125. try:
  126. from .slim.post_quantization import PaddleXPostTrainingQuantization
  127. except:
  128. raise Exception(
  129. "Model Quantization is not available, try to upgrade your paddlepaddle>=1.7.0"
  130. )
  131. is_use_cache_file = True
  132. if cache_dir is None:
  133. is_use_cache_file = False
  134. post_training_quantization = PaddleXPostTrainingQuantization(
  135. executor=self.exe,
  136. dataset=dataset,
  137. program=self.test_prog,
  138. inputs=self.test_inputs,
  139. outputs=self.test_outputs,
  140. batch_size=batch_size,
  141. batch_nums=batch_num,
  142. scope=None,
  143. algo='KL',
  144. quantizable_op_type=["conv2d", "depthwise_conv2d", "mul"],
  145. is_full_quantize=False,
  146. is_use_cache_file=is_use_cache_file,
  147. cache_dir=cache_dir)
  148. post_training_quantization.quantize()
  149. post_training_quantization.save_quantized_model(save_dir)
  150. model_info = self.get_model_info()
  151. model_info['status'] = 'Quant'
  152. # 保存模型输出的变量描述
  153. model_info['_ModelInputsOutputs'] = dict()
  154. model_info['_ModelInputsOutputs']['test_inputs'] = [
  155. [k, v.name] for k, v in self.test_inputs.items()
  156. ]
  157. model_info['_ModelInputsOutputs']['test_outputs'] = [
  158. [k, v.name] for k, v in self.test_outputs.items()
  159. ]
  160. with open(
  161. osp.join(save_dir, 'model.yml'), encoding='utf-8',
  162. mode='w') as f:
  163. yaml.dump(model_info, f)
  164. def net_initialize(self,
  165. startup_prog=None,
  166. pretrain_weights=None,
  167. fuse_bn=False,
  168. save_dir='.',
  169. sensitivities_file=None,
  170. eval_metric_loss=0.05):
  171. pretrain_dir = osp.join(save_dir, 'pretrain')
  172. if not os.path.isdir(pretrain_dir):
  173. if os.path.exists(pretrain_dir):
  174. os.remove(pretrain_dir)
  175. os.makedirs(pretrain_dir)
  176. if hasattr(self, 'backbone'):
  177. backbone = self.backbone
  178. else:
  179. backbone = self.__class__.__name__
  180. pretrain_weights = get_pretrain_weights(
  181. pretrain_weights, self.model_type, backbone, pretrain_dir)
  182. if startup_prog is None:
  183. startup_prog = fluid.default_startup_program()
  184. self.exe.run(startup_prog)
  185. if pretrain_weights is not None:
  186. logging.info(
  187. "Load pretrain weights from {}.".format(pretrain_weights))
  188. paddlex.utils.utils.load_pretrain_weights(
  189. self.exe, self.train_prog, pretrain_weights, fuse_bn)
  190. # 进行裁剪
  191. if sensitivities_file is not None:
  192. from .slim.prune_config import get_sensitivities
  193. sensitivities_file = get_sensitivities(sensitivities_file, self,
  194. save_dir)
  195. from .slim.prune import get_params_ratios, prune_program
  196. prune_params_ratios = get_params_ratios(
  197. sensitivities_file, eval_metric_loss=eval_metric_loss)
  198. prune_program(self, prune_params_ratios)
  199. self.status = 'Prune'
  200. def get_model_info(self):
  201. info = dict()
  202. info['version'] = paddlex.__version__
  203. info['Model'] = self.__class__.__name__
  204. info['_Attributes'] = {'model_type': self.model_type}
  205. if 'self' in self.init_params:
  206. del self.init_params['self']
  207. if '__class__' in self.init_params:
  208. del self.init_params['__class__']
  209. info['_init_params'] = self.init_params
  210. info['_Attributes']['num_classes'] = self.num_classes
  211. info['_Attributes']['labels'] = self.labels
  212. try:
  213. primary_metric_key = list(self.eval_metrics.keys())[0]
  214. primary_metric_value = float(self.eval_metrics[primary_metric_key])
  215. info['_Attributes']['eval_metrics'] = {
  216. primary_metric_key: primary_metric_value
  217. }
  218. except:
  219. pass
  220. if hasattr(self.test_transforms, 'to_rgb'):
  221. if self.test_transforms.to_rgb:
  222. info['TransformsMode'] = 'RGB'
  223. else:
  224. info['TransformsMode'] = 'BGR'
  225. if hasattr(self, 'test_transforms'):
  226. if self.test_transforms is not None:
  227. info['Transforms'] = list()
  228. for op in self.test_transforms.transforms:
  229. name = op.__class__.__name__
  230. attr = op.__dict__
  231. info['Transforms'].append({name: attr})
  232. return info
  233. def save_model(self, save_dir):
  234. if not osp.isdir(save_dir):
  235. if osp.exists(save_dir):
  236. os.remove(save_dir)
  237. os.makedirs(save_dir)
  238. fluid.save(self.train_prog, osp.join(save_dir, 'model'))
  239. model_info = self.get_model_info()
  240. model_info['status'] = self.status
  241. with open(
  242. osp.join(save_dir, 'model.yml'), encoding='utf-8',
  243. mode='w') as f:
  244. yaml.dump(model_info, f)
  245. # 评估结果保存
  246. if hasattr(self, 'eval_details'):
  247. with open(osp.join(save_dir, 'eval_details.json'), 'w') as f:
  248. json.dump(self.eval_details, f)
  249. if self.status == 'Prune':
  250. # 保存裁剪的shape
  251. shapes = {}
  252. for block in self.train_prog.blocks:
  253. for param in block.all_parameters():
  254. pd_var = fluid.global_scope().find_var(param.name)
  255. pd_param = pd_var.get_tensor()
  256. shapes[param.name] = np.array(pd_param).shape
  257. with open(
  258. osp.join(save_dir, 'prune.yml'), encoding='utf-8',
  259. mode='w') as f:
  260. yaml.dump(shapes, f)
  261. # 模型保存成功的标志
  262. open(osp.join(save_dir, '.success'), 'w').close()
  263. logging.info("Model saved in {}.".format(save_dir))
  264. def export_inference_model(self, save_dir):
  265. test_input_names = [
  266. var.name for var in list(self.test_inputs.values())
  267. ]
  268. test_outputs = list(self.test_outputs.values())
  269. if self.__class__.__name__ == 'MaskRCNN':
  270. from paddlex.utils.save import save_mask_inference_model
  271. save_mask_inference_model(
  272. dirname=save_dir,
  273. executor=self.exe,
  274. params_filename='__params__',
  275. feeded_var_names=test_input_names,
  276. target_vars=test_outputs,
  277. main_program=self.test_prog)
  278. else:
  279. fluid.io.save_inference_model(
  280. dirname=save_dir,
  281. executor=self.exe,
  282. params_filename='__params__',
  283. feeded_var_names=test_input_names,
  284. target_vars=test_outputs,
  285. main_program=self.test_prog)
  286. model_info = self.get_model_info()
  287. model_info['status'] = 'Infer'
  288. # 保存模型输出的变量描述
  289. model_info['_ModelInputsOutputs'] = dict()
  290. model_info['_ModelInputsOutputs']['test_inputs'] = [
  291. [k, v.name] for k, v in self.test_inputs.items()
  292. ]
  293. model_info['_ModelInputsOutputs']['test_outputs'] = [
  294. [k, v.name] for k, v in self.test_outputs.items()
  295. ]
  296. with open(
  297. osp.join(save_dir, 'model.yml'), encoding='utf-8',
  298. mode='w') as f:
  299. yaml.dump(model_info, f)
  300. # 模型保存成功的标志
  301. open(osp.join(save_dir, '.success'), 'w').close()
  302. logging.info(
  303. "Model for inference deploy saved in {}.".format(save_dir))
  304. def train_loop(self,
  305. num_epochs,
  306. train_dataset,
  307. train_batch_size,
  308. eval_dataset=None,
  309. save_interval_epochs=1,
  310. log_interval_steps=10,
  311. save_dir='output',
  312. use_vdl=False):
  313. if not osp.isdir(save_dir):
  314. if osp.exists(save_dir):
  315. os.remove(save_dir)
  316. os.makedirs(save_dir)
  317. if use_vdl:
  318. from visualdl import LogWriter
  319. vdl_logdir = osp.join(save_dir, 'vdl_log')
  320. # 给transform添加arrange操作
  321. self.arrange_transforms(
  322. transforms=train_dataset.transforms, mode='train')
  323. # 构建train_data_loader
  324. self.build_train_data_loader(
  325. dataset=train_dataset, batch_size=train_batch_size)
  326. if eval_dataset is not None:
  327. self.eval_transforms = eval_dataset.transforms
  328. self.test_transforms = copy.deepcopy(eval_dataset.transforms)
  329. # 获取实时变化的learning rate
  330. lr = self.optimizer._learning_rate
  331. if isinstance(lr, fluid.framework.Variable):
  332. self.train_outputs['lr'] = lr
  333. # 在多卡上跑训练
  334. if self.parallel_train_prog is None:
  335. build_strategy = fluid.compiler.BuildStrategy()
  336. build_strategy.fuse_all_optimizer_ops = False
  337. if paddlex.env_info['place'] != 'cpu' and len(self.places) > 1:
  338. build_strategy.sync_batch_norm = self.sync_bn
  339. exec_strategy = fluid.ExecutionStrategy()
  340. exec_strategy.num_iteration_per_drop_scope = 1
  341. self.parallel_train_prog = fluid.CompiledProgram(
  342. self.train_prog).with_data_parallel(
  343. loss_name=self.train_outputs['loss'].name,
  344. build_strategy=build_strategy,
  345. exec_strategy=exec_strategy)
  346. total_num_steps = math.floor(
  347. train_dataset.num_samples / train_batch_size)
  348. num_steps = 0
  349. time_stat = list()
  350. time_train_one_epoch = None
  351. time_eval_one_epoch = None
  352. total_num_steps_eval = 0
  353. # 模型总共的评估次数
  354. total_eval_times = math.ceil(num_epochs / save_interval_epochs)
  355. # 检测目前仅支持单卡评估,训练数据batch大小与显卡数量之商为验证数据batch大小。
  356. eval_batch_size = train_batch_size
  357. if self.model_type == 'detector':
  358. eval_batch_size = self._get_single_card_bs(train_batch_size)
  359. if eval_dataset is not None:
  360. total_num_steps_eval = math.ceil(
  361. eval_dataset.num_samples / eval_batch_size)
  362. if use_vdl:
  363. # VisualDL component
  364. log_writer = LogWriter(vdl_logdir, sync_cycle=20)
  365. train_step_component = OrderedDict()
  366. eval_component = OrderedDict()
  367. best_accuracy_key = ""
  368. best_accuracy = -1.0
  369. best_model_epoch = 1
  370. for i in range(num_epochs):
  371. records = list()
  372. step_start_time = time.time()
  373. epoch_start_time = time.time()
  374. for step, data in enumerate(self.train_data_loader()):
  375. outputs = self.exe.run(
  376. self.parallel_train_prog,
  377. feed=data,
  378. fetch_list=list(self.train_outputs.values()))
  379. outputs_avg = np.mean(np.array(outputs), axis=1)
  380. records.append(outputs_avg)
  381. # 训练完成剩余时间预估
  382. current_time = time.time()
  383. step_cost_time = current_time - step_start_time
  384. step_start_time = current_time
  385. if len(time_stat) < 20:
  386. time_stat.append(step_cost_time)
  387. else:
  388. time_stat[num_steps % 20] = step_cost_time
  389. # 每间隔log_interval_steps,输出loss信息
  390. num_steps += 1
  391. if num_steps % log_interval_steps == 0:
  392. step_metrics = OrderedDict(
  393. zip(list(self.train_outputs.keys()), outputs_avg))
  394. if use_vdl:
  395. for k, v in step_metrics.items():
  396. if k not in train_step_component.keys():
  397. with log_writer.mode('Each_Step_while_Training'
  398. ) as step_logger:
  399. train_step_component[
  400. k] = step_logger.scalar(
  401. 'Training: {}'.format(k))
  402. train_step_component[k].add_record(num_steps, v)
  403. # 估算剩余时间
  404. avg_step_time = np.mean(time_stat)
  405. if time_train_one_epoch is not None:
  406. eta = (num_epochs - i - 1) * time_train_one_epoch + (
  407. total_num_steps - step - 1) * avg_step_time
  408. else:
  409. eta = ((num_epochs - i) * total_num_steps - step -
  410. 1) * avg_step_time
  411. if time_eval_one_epoch is not None:
  412. eval_eta = (total_eval_times - i //
  413. save_interval_epochs) * time_eval_one_epoch
  414. else:
  415. eval_eta = (
  416. total_eval_times - i // save_interval_epochs
  417. ) * total_num_steps_eval * avg_step_time
  418. eta_str = seconds_to_hms(eta + eval_eta)
  419. logging.info(
  420. "[TRAIN] Epoch={}/{}, Step={}/{}, {}, time_each_step={}s, eta={}"
  421. .format(i + 1, num_epochs, step + 1, total_num_steps,
  422. dict2str(step_metrics), round(
  423. avg_step_time, 2), eta_str))
  424. train_metrics = OrderedDict(
  425. zip(list(self.train_outputs.keys()), np.mean(records, axis=0)))
  426. logging.info('[TRAIN] Epoch {} finished, {} .'.format(
  427. i + 1, dict2str(train_metrics)))
  428. time_train_one_epoch = time.time() - epoch_start_time
  429. epoch_start_time = time.time()
  430. # 每间隔save_interval_epochs, 在验证集上评估和对模型进行保存
  431. eval_epoch_start_time = time.time()
  432. if (i + 1) % save_interval_epochs == 0 or i == num_epochs - 1:
  433. current_save_dir = osp.join(save_dir, "epoch_{}".format(i + 1))
  434. if not osp.isdir(current_save_dir):
  435. os.makedirs(current_save_dir)
  436. if eval_dataset is not None:
  437. self.eval_metrics, self.eval_details = self.evaluate(
  438. eval_dataset=eval_dataset,
  439. batch_size=eval_batch_size,
  440. epoch_id=i + 1,
  441. return_details=True)
  442. logging.info('[EVAL] Finished, Epoch={}, {} .'.format(
  443. i + 1, dict2str(self.eval_metrics)))
  444. # 保存最优模型
  445. best_accuracy_key = list(self.eval_metrics.keys())[0]
  446. current_accuracy = self.eval_metrics[best_accuracy_key]
  447. if current_accuracy > best_accuracy:
  448. best_accuracy = current_accuracy
  449. best_model_epoch = i + 1
  450. best_model_dir = osp.join(save_dir, "best_model")
  451. self.save_model(save_dir=best_model_dir)
  452. if use_vdl:
  453. for k, v in self.eval_metrics.items():
  454. if isinstance(v, list):
  455. continue
  456. if isinstance(v, np.ndarray):
  457. if v.size > 1:
  458. continue
  459. if k not in eval_component:
  460. with log_writer.mode('Each_Epoch_on_Eval_Data'
  461. ) as eval_logger:
  462. eval_component[k] = eval_logger.scalar(
  463. 'Evaluation: {}'.format(k))
  464. eval_component[k].add_record(i + 1, v)
  465. self.save_model(save_dir=current_save_dir)
  466. time_eval_one_epoch = time.time() - eval_epoch_start_time
  467. eval_epoch_start_time = time.time()
  468. logging.info(
  469. 'Current evaluated best model in eval_dataset is epoch_{}, {}={}'
  470. .format(best_model_epoch, best_accuracy_key,
  471. best_accuracy))