train.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. from config import *
  2. from utils import *
  3. from model import make_model
  4. from data_loader import *
  5. from torch.nn import CrossEntropyLoss
  6. # from torch.optim import Adam
  7. from torch.optim import AdamW
  8. from torch.optim.lr_scheduler import LambdaLR
  9. import time
  10. from datetime import datetime
  11. from pathlib import Path
  12. from torch.utils.tensorboard import SummaryWriter
  13. #自定义调整策略
  14. def lr_lambda_fn(step, wramup):
  15. lr = 0
  16. if step <= wramup:
  17. lr = step / wramup * 10
  18. else:
  19. lr = wramup / step * 10
  20. return max(lr, 0.1)
  21. def run_epoch(loader, model, loss_fn, optimizer=None):
  22. # 初始化loss值,和batch数量总数
  23. total_batchs = 0.
  24. total_loss = 0.
  25. model.to(device)
  26. # 加载数据并开始训练
  27. for src_x, src_mask, tgt_x, tgt_mask, tgt_y, tgt_text in loader:
  28. src_x= src_x.to(device)
  29. src_mask=src_mask.to(device)
  30. tgt_x=tgt_x.to(device)
  31. tgt_mask=tgt_mask.to(device)
  32. tgt_y=tgt_y.to(device)#9,6
  33. output = model(src_x, src_mask, tgt_x, tgt_mask)#9,6,19
  34. # 交叉熵损失,要求目标值是一维的
  35. # out.shape 54,19 label.shape 54
  36. loss = loss_fn(output.reshape(-1, output.shape[-1]), tgt_y.reshape(-1))
  37. # 累积batch数量和loss值
  38. total_batchs += 1
  39. total_loss += loss.item()
  40. # 如果有优化器,则表示需要反向传播
  41. if optimizer:
  42. optimizer.zero_grad()
  43. loss.backward()
  44. optimizer.step()
  45. # 返回epoch的平均loss值
  46. return total_loss / total_batchs
  47. #评估函数
  48. def evaluate(loader, model, max_len=MAX_LEN):
  49. tgt_sent = [] #label 标准中文答案
  50. prob_sent = [] #pre 模型生成的中文
  51. for src_x, src_mask, tgt_x, tgt_mask, tgt_y, tgt_text in loader:
  52. batch_prob_text = batch_greedy_decode(model, src_x, src_mask, max_len)
  53. tgt_sent += tgt_text
  54. prob_sent += batch_prob_text
  55. # 验证集包含数万条句子,完整打印会淹没终端并拖慢训练。
  56. # 只展示少量预测/参考样例,完整指标仍使用全部验证集计算。
  57. preview_size = min(3, len(prob_sent))
  58. for index in range(preview_size):
  59. print(f"验证样例 {index + 1} 预测: {prob_sent[index]}")
  60. print(f"验证样例 {index + 1} 参考: {tgt_sent[index]}")
  61. return bleu_score(prob_sent, [tgt_sent])
  62. if __name__== "__main__":
  63. # 创建日志目录,并为本次训练创建独立的日志文件和 TensorBoard 目录。
  64. run_time = datetime.now().strftime("%Y%m%d_%H%M%S")
  65. log_dir = Path("./logs")
  66. log_dir.mkdir(parents=True, exist_ok=True)
  67. params_dir = Path("./params")
  68. params_dir.mkdir(parents=True, exist_ok=True)
  69. log_path = log_dir / f"train_{run_time}.log"
  70. tensorboard_dir = log_dir / f"tensorboard_{run_time}"
  71. writer = SummaryWriter(log_dir=str(tensorboard_dir))
  72. log_file = log_path.open("w", encoding="utf-8")
  73. # 记录训练开始时间和日志保存位置。
  74. training_start_time = time.time()
  75. start_message = (
  76. f"训练开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
  77. f"日志文件: {log_path}\n"
  78. f"TensorBoard目录: {tensorboard_dir}"
  79. )
  80. print(start_message)
  81. log_file.write(start_message + "\n")
  82. log_file.flush()
  83. en_id2vocab, _ = get_vocab('en')
  84. zh_id2vocab, _ = get_vocab('zh')
  85. SRC_VOCAB_SIZE = len(en_id2vocab)
  86. TGT_VOCAB_SIZE = len(zh_id2vocab)
  87. model = make_model(SRC_VOCAB_SIZE, TGT_VOCAB_SIZE, D_MODEL, N_HEAD, D_FF, N, DROPOUT)
  88. train_dataset = Dataset('train')
  89. train_loader = data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,
  90. collate_fn=train_dataset.collate_fn)
  91. val_dataset = Dataset('val')
  92. val_loader = data.DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False,
  93. collate_fn=val_dataset.collate_fn)
  94. loss_fn = CrossEntropyLoss(ignore_index=PAD_ID,label_smoothing=LABEL_SMOOTHING)
  95. # optimizer = Adam(model.parameters(), lr=LR)
  96. optimizer = AdamW(model.parameters(), lr=LR)
  97. # optimizer = torch.optim.AdamW(
  98. # model.parameters(),
  99. # lr=LR,
  100. # weight_decay=0.01,
  101. # )
  102. lr_scheduler = LambdaLR(optimizer, lr_lambda=lambda step: lr_lambda_fn(step, EPOCH/4))
  103. # 早停改进:以验证集BLEU为标准保存最佳权重。
  104. # 连续early_stop_patience轮没有至少min_bleu_delta的提升时停止。
  105. best_bleu = float("-inf")
  106. best_epoch = 0
  107. early_stop_patience = 6
  108. epochs_without_improvement = 0
  109. min_bleu_delta = 0.01
  110. for e in range(EPOCH):
  111. epoch_start_time = time.time()
  112. # 训练流程
  113. model.train()
  114. train_loss = run_epoch(train_loader, model, loss_fn, optimizer)
  115. lr_scheduler.step()
  116. # 打印当前学习率
  117. # current_lr = optimizer.param_groups[0]['lr']
  118. # print(current_lr)
  119. # 验证流程
  120. model.eval()
  121. val_loss = run_epoch(val_loader, model, loss_fn, None)
  122. #writer.add_scalars("loss",{'train_loss':train_loss, 'val_loss':val_loss},e)
  123. # print('epoch:', e, 'train_loss:', train_loss, 'val_loss:',val_loss)
  124. #评估模型
  125. val_bleu = evaluate(val_loader, model, MAX_LEN)
  126. # 计算单轮耗时,并将 loss、BLEU 和耗时同时打印到终端和日志文件。
  127. epoch_time = time.time() - epoch_start_time
  128. log_message = (
  129. f"epoch: {e + 1}/{EPOCH} "
  130. f"train_loss: {train_loss:.6f} "
  131. f"val_loss: {val_loss:.6f} "
  132. f"val_bleu: {val_bleu:.2f} "
  133. f"epoch_time: {epoch_time:.2f}s"
  134. )
  135. print(log_message)
  136. log_file.write(log_message + "\n")
  137. log_file.flush()
  138. # 分别记录训练 loss 和验证 loss,供 TensorBoard 绘制曲线。
  139. writer.add_scalar("Loss/train", train_loss, e + 1)
  140. writer.add_scalar("Loss/validation", val_loss, e + 1)
  141. writer.add_scalar("BLEU/validation", val_bleu, e + 1)
  142. writer.add_scalar("Time/epoch_seconds", epoch_time, e + 1)
  143. writer.flush()
  144. # 每2轮保存一次模型权重。e从0开始,因此使用e+1表示实际轮数。
  145. if (e + 1) % 2 == 0:
  146. epoch_model_path = params_dir / f"epoch_{e + 1}.pt"
  147. torch.save(model.state_dict(), epoch_model_path)
  148. print("定期模型已保存:", epoch_model_path)
  149. # 早停改进:最佳权重保存和未改善轮数统计必须使用同一套判断。
  150. # val_bleu已经保留两位小数,因此提升0.01及以上视为有效改善。
  151. if val_bleu >= best_bleu + min_bleu_delta:
  152. torch.save(
  153. model.state_dict(),
  154. params_dir / "best.pt",
  155. )
  156. best_bleu = val_bleu
  157. best_epoch = e + 1
  158. epochs_without_improvement = 0
  159. early_stop_status = (
  160. f"发现更好的BLEU:{best_bleu:.2f},"
  161. f"已更新best.pt(epoch {best_epoch})"
  162. )
  163. else:
  164. epochs_without_improvement += 1
  165. early_stop_status = (
  166. f"BLEU连续未改善轮数:"
  167. f"{epochs_without_improvement}/"
  168. f"{early_stop_patience},"
  169. f"当前最佳:{best_bleu:.2f}"
  170. )
  171. print(early_stop_status)
  172. log_file.write(early_stop_status + "\n")
  173. log_file.flush()
  174. writer.add_scalar(
  175. "EarlyStopping/epochs_without_improvement",
  176. epochs_without_improvement,
  177. e + 1,
  178. )
  179. writer.flush()
  180. # 连续多轮没有改善,提前结束
  181. if epochs_without_improvement >= early_stop_patience:
  182. early_stop_message = (
  183. f"触发早停:BLEU连续"
  184. f"{early_stop_patience}轮没有改善;"
  185. f"最佳BLEU为{best_bleu:.2f},"
  186. f"出现在第{best_epoch}轮。"
  187. )
  188. print(early_stop_message)
  189. log_file.write(early_stop_message + "\n")
  190. log_file.flush()
  191. break
  192. # 训练结束后记录总耗时,并关闭日志文件和 TensorBoard 写入器。
  193. total_training_time = time.time() - training_start_time
  194. end_message = (
  195. f"训练结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} "
  196. f"总训练时间: {total_training_time:.2f}s "
  197. f"({total_training_time / 3600:.2f}小时)"
  198. )
  199. print(end_message)
  200. log_file.write(end_message + "\n")
  201. log_file.close()
  202. writer.close()