| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- from config import *
- from utils import *
- from model import make_model
- from data_loader import *
- from torch.nn import CrossEntropyLoss
- # from torch.optim import Adam
- from torch.optim import AdamW
- from torch.optim.lr_scheduler import LambdaLR
- import time
- from datetime import datetime
- from pathlib import Path
- from torch.utils.tensorboard import SummaryWriter
- #自定义调整策略
- def lr_lambda_fn(step, wramup):
- lr = 0
- if step <= wramup:
- lr = step / wramup * 10
- else:
- lr = wramup / step * 10
- return max(lr, 0.1)
- def run_epoch(loader, model, loss_fn, optimizer=None):
- # 初始化loss值,和batch数量总数
- total_batchs = 0.
- total_loss = 0.
- model.to(device)
- # 加载数据并开始训练
- for src_x, src_mask, tgt_x, tgt_mask, tgt_y, tgt_text in loader:
- src_x= src_x.to(device)
- src_mask=src_mask.to(device)
- tgt_x=tgt_x.to(device)
- tgt_mask=tgt_mask.to(device)
- tgt_y=tgt_y.to(device)#9,6
- output = model(src_x, src_mask, tgt_x, tgt_mask)#9,6,19
- # 交叉熵损失,要求目标值是一维的
- # out.shape 54,19 label.shape 54
- loss = loss_fn(output.reshape(-1, output.shape[-1]), tgt_y.reshape(-1))
- # 累积batch数量和loss值
- total_batchs += 1
- total_loss += loss.item()
- # 如果有优化器,则表示需要反向传播
- if optimizer:
- optimizer.zero_grad()
- loss.backward()
- optimizer.step()
- # 返回epoch的平均loss值
- return total_loss / total_batchs
- #评估函数
- def evaluate(loader, model, max_len=MAX_LEN):
- tgt_sent = [] #label 标准中文答案
- prob_sent = [] #pre 模型生成的中文
- for src_x, src_mask, tgt_x, tgt_mask, tgt_y, tgt_text in loader:
- batch_prob_text = batch_greedy_decode(model, src_x, src_mask, max_len)
- tgt_sent += tgt_text
- prob_sent += batch_prob_text
- # 验证集包含数万条句子,完整打印会淹没终端并拖慢训练。
- # 只展示少量预测/参考样例,完整指标仍使用全部验证集计算。
- preview_size = min(3, len(prob_sent))
- for index in range(preview_size):
- print(f"验证样例 {index + 1} 预测: {prob_sent[index]}")
- print(f"验证样例 {index + 1} 参考: {tgt_sent[index]}")
- return bleu_score(prob_sent, [tgt_sent])
- if __name__== "__main__":
- # 创建日志目录,并为本次训练创建独立的日志文件和 TensorBoard 目录。
- run_time = datetime.now().strftime("%Y%m%d_%H%M%S")
- log_dir = Path("./logs")
- log_dir.mkdir(parents=True, exist_ok=True)
- params_dir = Path("./params")
- params_dir.mkdir(parents=True, exist_ok=True)
- log_path = log_dir / f"train_{run_time}.log"
- tensorboard_dir = log_dir / f"tensorboard_{run_time}"
- writer = SummaryWriter(log_dir=str(tensorboard_dir))
- log_file = log_path.open("w", encoding="utf-8")
- # 记录训练开始时间和日志保存位置。
- training_start_time = time.time()
- start_message = (
- f"训练开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
- f"日志文件: {log_path}\n"
- f"TensorBoard目录: {tensorboard_dir}"
- )
- print(start_message)
- log_file.write(start_message + "\n")
- log_file.flush()
- en_id2vocab, _ = get_vocab('en')
- zh_id2vocab, _ = get_vocab('zh')
- SRC_VOCAB_SIZE = len(en_id2vocab)
- TGT_VOCAB_SIZE = len(zh_id2vocab)
- model = make_model(SRC_VOCAB_SIZE, TGT_VOCAB_SIZE, D_MODEL, N_HEAD, D_FF, N, DROPOUT)
-
- train_dataset = Dataset('train')
- train_loader = data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,
- collate_fn=train_dataset.collate_fn)
- val_dataset = Dataset('val')
- val_loader = data.DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False,
- collate_fn=val_dataset.collate_fn)
-
- loss_fn = CrossEntropyLoss(ignore_index=PAD_ID,label_smoothing=LABEL_SMOOTHING)
- # optimizer = Adam(model.parameters(), lr=LR)
- optimizer = AdamW(model.parameters(), lr=LR)
- # optimizer = torch.optim.AdamW(
- # model.parameters(),
- # lr=LR,
- # weight_decay=0.01,
- # )
- lr_scheduler = LambdaLR(optimizer, lr_lambda=lambda step: lr_lambda_fn(step, EPOCH/4))
-
- # 早停改进:以验证集BLEU为标准保存最佳权重。
- # 连续early_stop_patience轮没有至少min_bleu_delta的提升时停止。
- best_bleu = float("-inf")
- best_epoch = 0
- early_stop_patience = 6
- epochs_without_improvement = 0
- min_bleu_delta = 0.01
- for e in range(EPOCH):
-
- epoch_start_time = time.time()
- # 训练流程
- model.train()
- train_loss = run_epoch(train_loader, model, loss_fn, optimizer)
- lr_scheduler.step()
- # 打印当前学习率
- # current_lr = optimizer.param_groups[0]['lr']
- # print(current_lr)
- # 验证流程
- model.eval()
- val_loss = run_epoch(val_loader, model, loss_fn, None)
- #writer.add_scalars("loss",{'train_loss':train_loss, 'val_loss':val_loss},e)
- # print('epoch:', e, 'train_loss:', train_loss, 'val_loss:',val_loss)
-
- #评估模型
- val_bleu = evaluate(val_loader, model, MAX_LEN)
- # 计算单轮耗时,并将 loss、BLEU 和耗时同时打印到终端和日志文件。
- epoch_time = time.time() - epoch_start_time
- log_message = (
- f"epoch: {e + 1}/{EPOCH} "
- f"train_loss: {train_loss:.6f} "
- f"val_loss: {val_loss:.6f} "
- f"val_bleu: {val_bleu:.2f} "
- f"epoch_time: {epoch_time:.2f}s"
- )
- print(log_message)
- log_file.write(log_message + "\n")
- log_file.flush()
- # 分别记录训练 loss 和验证 loss,供 TensorBoard 绘制曲线。
- writer.add_scalar("Loss/train", train_loss, e + 1)
- writer.add_scalar("Loss/validation", val_loss, e + 1)
- writer.add_scalar("BLEU/validation", val_bleu, e + 1)
- writer.add_scalar("Time/epoch_seconds", epoch_time, e + 1)
- writer.flush()
- # 每2轮保存一次模型权重。e从0开始,因此使用e+1表示实际轮数。
- if (e + 1) % 2 == 0:
- epoch_model_path = params_dir / f"epoch_{e + 1}.pt"
- torch.save(model.state_dict(), epoch_model_path)
- print("定期模型已保存:", epoch_model_path)
- # 早停改进:最佳权重保存和未改善轮数统计必须使用同一套判断。
- # val_bleu已经保留两位小数,因此提升0.01及以上视为有效改善。
- if val_bleu >= best_bleu + min_bleu_delta:
- torch.save(
- model.state_dict(),
- params_dir / "best.pt",
- )
- best_bleu = val_bleu
- best_epoch = e + 1
- epochs_without_improvement = 0
- early_stop_status = (
- f"发现更好的BLEU:{best_bleu:.2f},"
- f"已更新best.pt(epoch {best_epoch})"
- )
- else:
- epochs_without_improvement += 1
- early_stop_status = (
- f"BLEU连续未改善轮数:"
- f"{epochs_without_improvement}/"
- f"{early_stop_patience},"
- f"当前最佳:{best_bleu:.2f}"
- )
- print(early_stop_status)
- log_file.write(early_stop_status + "\n")
- log_file.flush()
- writer.add_scalar(
- "EarlyStopping/epochs_without_improvement",
- epochs_without_improvement,
- e + 1,
- )
- writer.flush()
- # 连续多轮没有改善,提前结束
- if epochs_without_improvement >= early_stop_patience:
- early_stop_message = (
- f"触发早停:BLEU连续"
- f"{early_stop_patience}轮没有改善;"
- f"最佳BLEU为{best_bleu:.2f},"
- f"出现在第{best_epoch}轮。"
- )
- print(early_stop_message)
- log_file.write(early_stop_message + "\n")
- log_file.flush()
- break
- # 训练结束后记录总耗时,并关闭日志文件和 TensorBoard 写入器。
- total_training_time = time.time() - training_start_time
- end_message = (
- f"训练结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} "
- f"总训练时间: {total_training_time:.2f}s "
- f"({total_training_time / 3600:.2f}小时)"
- )
- print(end_message)
- log_file.write(end_message + "\n")
- log_file.close()
- writer.close()
|