基于面向对象重构模型训练器

news2024/9/23 9:34:56

引言

深度学习领域我们常用jupyter来演练代码,但实际生产环境中不可能像jupyter一样,所有代码逻辑都在面向过程编程,这会导致代码可复用性差,维护难度高。

前面这篇文章 基于pytorch+可视化重学线性回归模型 已经封装了数据加载器,本文我们将要对整个训练循环的逻辑进行重构,采用封装的方式来提升代码的可复用性,降低维护难度。

步骤大概是:

  1. 封装小批量单次训练
  2. 封装小批量单次测试
  3. 封装训练循环
  4. 封装损失数据的收集和可视化
  5. 封装参数和梯度变化的数据可视化
  6. 封装保存和加载模型

首先,导入需要的包

import torch
import numpy as np
import matplotlib.pyplot as plt
import torch.nn as nn
import torch.optim as optim

1. 数据准备

1.1 数据生成

鉴于正式工程中不会自己生成数据,所以数据生成部分始终会保持不变。

true_w = 2
true_b = 1
N = 100

np.random.seed(42)
x = np.random.rand(N, 1)
eplison = 0.1 * np.random.randn(N, 1)
y = true_w * x + true_b + eplison
x.shape, y.shape, eplison.shape
((100, 1), (100, 1), (100, 1))
1.2 数据拆分改造

将数据集转换为张量,这里将不作发送到设备to(device)的操作,而是推迟到小批量训练时再将数据发送到设备上,以节省和优化GPU显存的使用。

x_tensor = torch.as_tensor(x).float()
y_tensor = torch.as_tensor(y).float()

对于单纯的tensor数据可以直接使用pytorch内置的TensorDataset类来封装数据集, 并使用random_split来划分训练集和测试集。

from torch.utils.data import TensorDataset, DataLoader, random_split

ratio = 0.8
batch_size = 8

dataset = TensorDataset(x_tensor, y_tensor)

train_size = int(len(dataset) * ratio)
test_size = len(dataset) - train_size

train_dataset, test_dataset = random_split(dataset, [train_size, test_size])

train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True)

len(train_dataset), len(test_dataset), next(iter(train_loader))[0].shape, next(iter(test_loader))[0].shape
(80, 20, torch.Size([8, 1]), torch.Size([8, 1]))

2. 训练器的面向对象改造

2.1 定义模型的基本组件

基本组件目前固定是模型、损失函数和优化器,为方便后续复用,这里定义一个函数来生成这些组件。

线性回归模型在pytorch中已经有封装,这里直接使用nn.Linear来代替自定义。

lr = 0.2

def make_model_components(lr):
    torch.manual_seed(42)
    model = nn.Linear(1, 1)
    lossfn = nn.MSELoss(reduction='mean')
    optimizer = optim.SGD(model.parameters(), lr=lr)
    return model, lossfn, optimizer

model, lossfn, optimizer = make_model_components(lr)
model.state_dict()
OrderedDict([('weight', tensor([[0.7645]])), ('bias', tensor([0.8300]))])
2.2 创建训练器

为了实现训练器的高内聚、低耦合,分离动态与静态,我们使用面向对象的方法对其进行重构,对于模型训练这个业务来说:

  • 变化的内容应该是模型、数据源、损失函数、优化器、随机数种子等,这些内容应该由外部传入;
  • 不变的内容应该是训练循环、小批量迭代训练、评估模型损失、模型的保存和加载、预测计算等,这些内容应该由内部封装。

首先,我们定义一个训练器类,它包含以下功能:

class LinearTrainer:

    def __init__(self, model, lossfn, optimizer, verbose=False):
        self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
        self.model = model.to(self.device)
        self.lossfn = lossfn
        self.optimizer = optimizer
        self.verbose = verbose   # 用于调试模式打印日志
    
trainer = LinearTrainer(model, lossfn, optimizer, verbose=True)
trainer.model.state_dict(), trainer.device, trainer.lossfn, trainer.optimizer.state_dict()

(OrderedDict([('weight', tensor([[0.7645]])), ('bias', tensor([0.8300]))]),
 'cpu',
 MSELoss(),
 {'state': {},
  'param_groups': [{'lr': 0.2,
    'momentum': 0,
    'dampening': 0,
    'weight_decay': 0,
    'nesterov': False,
    'maximize': False,
    'foreach': None,
    'differentiable': False,
    'params': [0, 1]}]})
2.3 设置数据加载器

数据源是变化的,但训练逻辑其实只需要依赖符合pytorch定义的数据加载器,所以需要给训练器添加一个设置数据加载器的方法。

def set_loader(self, train_loader, test_loader=None):
    self.train_loader = train_loader
    self.test_loader = test_loader
    print(f'set train_loader: {self.train_loader}\ntest_loader: {self.test_loader}')

setattr(LinearTrainer, 'set_loader', set_loader)
trainer.set_loader(train_loader, test_loader)
set train_loader: <torch.utils.data.dataloader.DataLoader object at 0x14ad551e0>
test_loader: <torch.utils.data.dataloader.DataLoader object at 0x1150a5900>
2.4 添加单次迭代构建器

给训练器添加一个单次迭代构建器,用于构建单次迭代训练函数和单次迭代测试函数。

  • build_train_step: 构建单次迭代训练函数,返回一个能够完成单次迭代训练的函数train_step。
  • build_test_step: 构建单次迭代测试函数,返回一个能够完成单次迭代测试的函数test_step。

注:关于梯度清零,常规做法是放在optimizer.step()更新参数之后调用optimizer.zero_grad(),但这样一来是无法记录和观测梯度值的,给排查问题造成阻碍,所以这里将梯度清零的步骤移到下一次训练之前,目的是允许主循环获取当前梯度值。

def build_train_step(self):
    def train_step(x, y):
        # 切换模型为训练模式
        self.model.train()
        # 将梯度清零的步骤移到下一次训练之前,目的是允许主循环获取当前梯度值
        self.optimizer.zero_grad()
        # 计算预测值
        yhat = self.model(x)
        # 计算损失
        loss = self.lossfn(yhat, y)
        # 反向传播计算梯度
        loss.backward()
        # 使用优化器更新参数
        self.optimizer.step()
        return loss.item()
    return train_step

def build_test_step(self):
    def test_step(x, y):
        # 切换模型为测试模式
        self.model.eval()
        # 计算预测值
        yhat = self.model(x)
        # 计算损失
        loss = self.lossfn(yhat, y)
        return loss.item()
    return test_step

setattr(LinearTrainer, 'build_train_step', build_train_step)
setattr(LinearTrainer, 'build_test_step', build_test_step)
trainer.build_train_step(), trainer.build_test_step()
(<function __main__.build_train_step.<locals>.train_step(x, y)>,
 <function __main__.build_test_step.<locals>.test_step(x, y)>)
2.5 添加小批量迭代方法

在小批量迭代训练过程中,是训练和测试两个环节交叉进行。这两个环节的逻辑很相似,都是输入数据输出损失,不同之处在于所使用的数据加载器和单次迭代函数不同。我们可以封装一个统一的小批量迭代方法,来屏蔽这个差别。

def mini_batch(self, test=False):
    data_loader = None
    step_fn = None
    if test:
        data_loader = self.test_loader
        step_fn = self.build_test_step()
    else:
        data_loader = self.train_loader
        step_fn = self.build_train_step()

    if data_loader is None:
        raise ValueError("No data loader")

    x_batch, y_batch = next(iter(data_loader))
    x = x_batch.to(self.device)
    y = y_batch.to(self.device)
    loss = step_fn(x, y)
    return loss

setattr(LinearTrainer, "mini_batch", mini_batch)

LinearTrainer.mini_batch
<function __main__.mini_batch(self, test=False)>
2.6 设置随机数种子

为了确保结果的可复现性,我们需要为numpy和torch指定随机种子。除此之外,还需要设置cudnn的确定性

  • torch.backends.cudnn.deterministic: 当设置为True时,这个选项会确保cuDNN算法是确定性的,对于相同的输入和配置,它们将总是产生相同的输出。但是此选项可能会降低性能。
  • torch.backends.cudnn.benchmark:当设置为True时,cuDNN将会花费一些时间来“基准测试”各种可能的算法,并选择一个最快的。而设置为False时,则始终使用一种确定的算法,常和deterministic配合使用。
def set_seed(self, seed):
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False    

setattr(LinearTrainer, 'set_seed', set_seed)
trainer.set_seed(42)
2.7 添加主训练方法

此方法主要完成一个循环迭代的训练过程,每次迭代都会执行一个小批量训练和小批量测试,并实时收集训练损失和测试损失用于观察,迭代的次数由参数epoch_n决定。

def train(self, epoch_n):
    self.train_losses = []
    self.test_losses = []

    for i in range(epoch_n):
        loss = self.mini_batch(test=False)
        self.train_losses.append(loss)

        with torch.no_grad():
            test_loss = self.mini_batch(test=True)
            self.test_losses.append(test_loss)
    print(f'train loss: {self.train_losses[-1]}')
        
setattr(LinearTrainer, 'train', train)
trainer.train(100)
trainer.model.state_dict()
train loss: 0.006767683196812868
2.8 显示损失曲线

将生成的损失数据用matplotlib显示出来,以观察训练和测试两条损失曲线是否随着迭代次数而稳定下降。

def show_losses(self):
    fig, ax = plt.subplots(1, 1, figsize=(6, 4))
    ax.plot(self.train_losses, label='train losses', color='blue')
    ax.plot(self.test_losses, label='test losses', color='red')
    ax.legend(loc='upper right')
    ax.set_title('Loss descent')
    ax.set_xlabel('epochs')
    ax.set_ylabel('loss')
    ax.set_yscale('log')

    plt.show()

setattr(LinearTrainer, 'show_losses', show_losses)
trainer.show_losses()

在这里插入图片描述

我们每次为了可视化数据,都要手动记录损失数据,并手动写函数来绘制损失曲线。是否有更简单的方法呢?答案是有的,那就是tensorboard。

3. tensorboard

Tensorboard 是一个来自Tensorflow的可视化工具,但pytorch也提供了类和方法集成和使用它,可见它有多么的好用。

手动收集数据过于麻烦,而且每次画图都要写一个绘图函数,而且数据量和参数很多的时候,将需要写很多函数。

3.1 Tensorboard的基本使用

Tensorboard的使用分为两个部分:收集数据和显示数据。

  • 收集数据:主要靠SummaryWriter类,集成到pytorch中使用。
  • 显示数据:主要靠tensorboard命令,类似jupyter一样启动一个服务,然后通过浏览器访问。

SummaryWriter类提供了很多常用的方法来收集数据:
- add_graph: 收集模型的网络结构。
- add_scalar/add_scalars:收集标量数据,像损失函数值,准确率等。
- add_image/add_images:收集图片数据,像输入图片,输出图片等。
- add_text: 收集文本数据,可以记录一些文字。
- add_histogram: 收集直方图数据,可以用来观察参数分布。
- add_video: 收集视频数据,可以用来观察训练过程。
- add_embedding: 收集嵌入数据,可以用来观察数据分布。
- add_audio: 收集音频数据,可以用来观察训练过程。

from torch.utils.tensorboard import SummaryWriter
# 告诉tensorboard,要将日志记录到哪个文件夹
writer = SummaryWriter("../log/tensorboard_test")
# 取一个样例数据,连同model一起传给add_graph函数,它将能够从这个样例数据的预测过程中,收集到模型的计算图
x_sample, y_sample = next(iter(train_loader))
writer.add_graph(model, input_to_model=x_sample)

将模型的损失收集到tensorboard中,add_scalars可以将多组数据添加到一个图表中(训练和测试同图显示),而add_scalar只适用于一个图标一组数据的情况。

for i in range(len(trainer.train_losses)):
    writer.add_scalars("loss", {"train": trainer.train_losses[i], "test": trainer.test_losses[i]}, i)

运行Tensorboard,这里有两条命令:

  1. 第一条命令:是用于为jupyter notebook加载tensorboard扩展。
  2. 第二条命令:将在6006端口上启动一个服务器,并自动在当前jupyter notebook中内嵌一个网页来访问此服务。
%load_ext tensorboard
# 告诉tensorboard在logdir指定的文件夹中查找日志
%tensorboard --logdir "../log/tensorboard_test" --port 6006
The tensorboard extension is already loaded. To reload it, use:
  %reload_ext tensorboard



Reusing TensorBoard on port 6006 (pid 15193), started 0:00:04 ago. (Use '!kill 15193' to kill it.)

在这里插入图片描述

这个图上是可以点击进行操作的,可以在graphs、scalars、histoogam、images页签间切换。

3.2 使用tensorboard来改造训练器

tensorboard将收集数据与显示数据的工作分离,这样我们就不用等到训练完再查看数据,可以训练模型时,单开一个任务来可视化观察训练过程。

首先,我们需要一个设置tensorboard的方法,将SummaryWriter内置到训练器中,这样我们就可以在训练过程中收集数据了。

import os
import shutil

def set_tensorboard(self, name, log_dir, clear=True):
    log_file_path = f"{log_dir}/{name}"
        # 删除训练日志
    if clear == True and os.path.exists(log_file_path):
        shutil.rmtree(log_file_path)
        print(f"clear tensorboard path: {log_file_path}") if self.verbose else None

    self.writer = SummaryWriter(log_file_path)
    if hasattr(self, "train_loader") and self.train_loader is not None:
        sample_x, _ = next(iter(self.train_loader))
        self.writer.add_graph(self.model, sample_x)

    print(f"Tensorboard log dir: {self.writer.log_dir}") if self.verbose else None

setattr(LinearTrainer, "set_tensorboard", set_tensorboard)

具体收集的数据,除了之前的损失值外,我们还有必要收集参数的值和梯度,这对于排查损失不下降的原因很有帮助。

为避免收集数据的代码污染主循环,我们单独封装两个方法用来收集数据,分别是:

  • record_train_data: 收集训练数据的主方法,包括收集数据和执行flush操作。
  • record_parameters: 专门用于收集参数的方法,包括参数值本身和梯度。
def record_parameters(self, epoch_idx):
     for name, param in self.model.named_parameters():
          self.writer.add_scalar(name, param.data, epoch_idx)
          if param.grad is not None:
               self.writer.add_scalar(name+"/grad", param.grad.item(), epoch_idx)
          if self.verbose:
               print(f"epoch_idx={epoch_idx}, name={name}, param.data: {param.data}, param.grad.item: {param.grad.item() if param.grad is not None else 'None'}")

def record_train_data(self, train_loss, test_loss, epoch_idx):
     # 记录损失数据,训练损失和验证损失对比显示
     self.writer.add_scalars('loss', {'train': train_loss, 'test': test_loss}, epoch_idx)
     if self.verbose:
          print(f"epoch_idx={epoch_idx}, train_loss: {train_loss}, test_loss: {test_loss}")
     
     # 记录模型的所有参数变化,以及参数梯度的变化过程
     self.record_parameters(epoch_idx)
     self.writer.flush()

setattr(LinearTrainer, 'record_parameters', record_parameters)
setattr(LinearTrainer, 'record_train_data', record_train_data)

是时候改造训练主循环了,我们将收集数据的操作统一放到record_train_data()这个函数调用来完成,主循环反而变得更简单清晰:

注:在训练之前,先收集原始参数值,是为了保证原始参数值也被收集,并在图表中显示出来。

def train(self, eporch_n):
    # 收集原始参数
    self.record_parameters(0)
    # 开始训练
    for i in range(eporch_n):
        train_loss = self.mini_batch(test=False)

        with torch.no_grad():
            test_loss = self.mini_batch(test=True)
        # 记录训练数据
        self.record_train_data(train_loss, test_loss, i+1)
        
 
setattr(LinearTrainer, 'train', train)

由于刚才已经训练过一次,所以需要重置下模型的参数,从头开始训练并收集中间过程中的数据。

注:考虑到训练是反复进行的,为了后续方便,封装一个reset函数来重置模型,主要功能是将模型和优化器重置,并删除旧的训练日志。

import shutil
import os

def reset(self, model, lossfn, optimizer):
    if hasattr(self, "model"):
        self.model.cpu() if self.model != None else None
        del self.model
        del self.optimizer

    self.model = model
    self.lossfn = lossfn
    self.optimizer = optimizer
    print(f"reset model and optimizer: {self.model.state_dict()}, {self.optimizer.state_dict()}") if self.verbose else None

setattr(LinearTrainer, "reset", reset)

model, lossfn, optimizer = make_model_components(lr)
trainer.reset(model, lossfn, optimizer)
trainer.set_seed(42)
trainer.set_tensorboard(name="linear_objected-1", log_dir="../log")
trainer.model.state_dict()
OrderedDict([('weight', tensor([[0.7645]])), ('bias', tensor([0.8300]))])

可以看到,经过重置后,参数又恢复了原始值,下面调用train方法重新开始训练。

trainer.train(100)
trainer.model.state_dict()
OrderedDict([('weight', tensor([[1.8748]])), ('bias', tensor([1.0477]))])
# %load_ext tensorboard
%tensorboard --logdir "../log/linear_objected-1" --port 6007
Reusing TensorBoard on port 6007 (pid 26888), started 0:00:03 ago. (Use '!kill 26888' to kill it.)

在这里插入图片描述

4. 保存和加载模型

我们这个场景使用的是最简单的线性回归模型,所以保存和加载模型非常快。但实际中,我们可能使用更复杂的模型,这些模型可能包含很多层,每层参数都可能非常多,整个训练过程可能需要几个小时甚至几天,所以保存训练结果就显得非常重要了。

4.1 保存模型

保存模型本质上是保存模型的状态,包括模型参数、优化器状态、损失值等。这些数据都保存包裹到一个dict中,然后使用torhc.save()函数保存到文件中。

def save_checkpoint(self, checkpoint_path):
    checkpoint = {
        "model_state_dict": self.model.state_dict(),
        "optimizer_state_dict": self.optimizer.state_dict(),
    }
    torch.save(checkpoint, checkpoint_path)
    print(f"save checkpoint: {self.model.state_dict()}") if self.verbose else None


setattr(LinearTrainer, "save_checkpoint", save_checkpoint)

checkpoint_path = "../checkpoint/torch_linear-1.pth"
trainer.save_checkpoint(checkpoint_path)

4.2 加载模型

当我们需要部署模型进行数据预测,或者重新开始未完成的训练时,就需要使用torch.load()将之前保存在文件中的模型和参数加载进来。

def load_checkpoint(self, checkpoint_path):
    checkpoint = torch.load(checkpoint_path)
    self.model.load_state_dict(checkpoint["model_state_dict"])
    self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
    print(f"load checkpoint: {self.model.state_dict()}") if self.verbose else None

setattr(LinearTrainer, "load_checkpoint", load_checkpoint)


  device
  train_loader
  test_loader
  train_losses
  test_losses
  writer
  debug
  optimizer

为了与前面的训练结果完全隔离开,我们重新创建一个训练器,一个新训练器需要进行的初始化总共包括以下几项:

  1. 模型、损失函数和优化器
  2. 训练数据集和测试数据集的加载器
  3. 随机数种子
  4. 设置训练数据的收集位置,便于tensorboard可视化
model, lossfn, optimizer = make_model_components(lr)
trainer2 = LinearTrainer(model, lossfn, optimizer, verbose=True)
trainer2.set_seed(42)
trainer2.set_loader(train_loader, test_loader)
trainer2.set_tensorboard(name='linear_objected-2', log_dir="../log")
set train_loader: <torch.utils.data.dataloader.DataLoader object at 0x14ad551e0>
test_loader: <torch.utils.data.dataloader.DataLoader object at 0x1150a5900>
Tensorboard log dir: ../log/linear_objected-2

然后从checkpoint加载模型参数,可以看到之前的训练结果已经加载进新的训练器。

print(f"before load: {trainer2.model.state_dict()}")
trainer2.load_checkpoint(checkpoint_path)
print(f"after load: {trainer2.model.state_dict()}")
before load: OrderedDict([('weight', tensor([[0.7645]])), ('bias', tensor([0.8300]))])
load checkpoint: OrderedDict([('weight', tensor([[1.8748]])), ('bias', tensor([1.0477]))])
after load: OrderedDict([('weight', tensor([[1.8748]])), ('bias', tensor([1.0477]))])

接着之前的训练结果继续训练

trainer2.train(100)
%tensorboard --logdir "../log/linear_objected-2" --port 6009
Reusing TensorBoard on port 6009 (pid 32774), started 0:00:03 ago. (Use '!kill 32774' to kill it.)

在这里插入图片描述

可以看到,经过又一轮的训练后,权重weight从1.87学习到了1.9159,离真实值2更接近了。

5. 训练器封装结果

到目前为止,给训练器添加的所有方法汇总如下:

for key, value in vars(LinearTrainer).items():
    if callable(value) and not key.startswith("__"):  # 忽略内置或特殊方法
        print(f"  {key}()")
  set_loader()
  build_train_step()
  build_test_step()
  mini_batch()
  set_seed()
  set_tensorboard()
  train()
  reset()
  record_parameters()
  record_train_data()
  save_checkpoint()
  load_checkpoint()

给训练器添加的所有字段汇总如下:

for key, value in vars(trainer).items():
    if not callable(value) and not key.startswith("__"):  # 忽略内置或特殊方法
        print(f"  {key}")
  device
  verbose
  train_loader
  test_loader
  writer
  optimizer

这些后面在方法中添加的字段,由于初始化的顺序不同,很容易引发AttributeError: object has no attribute 'xxx',所以需要对__init__方法进行改造,以便对这些字段提前初始化。

def __init__(self, model, lossfn, optimizer):
    self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
    self.model = model
    self.lossfn = lossfn
    self.optimizer = optimizer
    self.verbose = False
    self.writer = None
    self.train_loader = None
    self.test_loader = None

setattr(LinearTrainer, '__init__', __init__)
test_trainer = LinearTrainer(model, lossfn, optimizer)
test_trainer.writer

通过初始化的改造后,上面新创建的test_trainer虽然没有调用set_tensorboard,但是仍然可以访问.writer字段而不报错。

最后LinearTrainer类的完整代码:

import os
import shutil
import torch
import numpy as np

class LinearTrainer:

    def __init__(self, model, lossfn, optimizer, verbose=False):
        self.device = 'cuda' if torch.cuda.is_available() else 'cpu'
        self.model = model.to(self.device)
        self.lossfn = lossfn
        self.optimizer = optimizer
        self.verbose = False
        self.writer = None
        self.train_loader = None
        self.test_loader = None

    def set_loader(self, train_loader, test_loader=None):
        self.train_loader = train_loader
        self.test_loader = test_loader
        print(f'set train_loader: {self.train_loader}\ntest_loader: {self.test_loader}') if self.verbose else None

    def build_train_step(self):
        def train_step(x, y):
            # 切换模型为训练模式
            self.model.train()
            # 将梯度清零的步骤移到下一次训练之前,目的是允许主循环获取当前梯度值
            self.optimizer.zero_grad()
            # 计算预测值
            yhat = self.model(x)
            # 计算损失
            loss = self.lossfn(yhat, y)
            # 反向传播计算梯度
            loss.backward()
            # 使用优化器更新参数
            self.optimizer.step()
            return loss.item()
        return train_step

    def build_test_step(self):
        def test_step(x, y):
            # 切换模型为测试模式
            self.model.eval()
            # 计算预测值
            yhat = self.model(x)
            # 计算损失
            loss = self.lossfn(yhat, y)
            return loss.item()
        return test_step
    
    def mini_batch(self, test=False):
        data_loader = None
        step_fn = None
        if test:
            data_loader = self.test_loader
            step_fn = self.build_test_step()
        else:
            data_loader = self.train_loader
            step_fn = self.build_train_step()

        if data_loader is None:
            raise ValueError("No data loader")

        x_batch, y_batch = next(iter(data_loader))
        x = x_batch.to(self.device)
        y = y_batch.to(self.device)
        loss = step_fn(x, y)
        return loss
    
    def train(self, eporch_n):
        # 收集原始参数
        self.record_parameters(0)
        # 开始训练
        for i in range(eporch_n):
            train_loss = self.mini_batch(test=False)

            with torch.no_grad():
                test_loss = self.mini_batch(test=True)
            # 记录训练数据
            self.record_train_data(train_loss, test_loss, i+1)
    
    def set_seed(self, seed):
        np.random.seed(seed)
        torch.manual_seed(seed)
        torch.backends.cudnn.deterministic = True
        torch.backends.cudnn.benchmark = False    

    def set_tensorboard(self, name, log_dir, clear=True):
        log_file_path = f"{log_dir}/{name}"
            # 删除训练日志
        if clear == True and os.path.exists(log_file_path):
            shutil.rmtree(log_file_path)
            print(f"clear tensorboard path: {log_file_path}") if self.verbose else None

        self.writer = SummaryWriter(log_file_path)
        if hasattr(self, "train_loader") and self.train_loader is not None:
            sample_x, _ = next(iter(self.train_loader))
            self.writer.add_graph(self.model, sample_x)

        print(f"Tensorboard log dir: {self.writer.log_dir}") if self.verbose else None

    def record_parameters(self, epoch_idx):
        for name, param in self.model.named_parameters():
            self.writer.add_scalar(name, param.data, epoch_idx)
            if param.grad is not None:
                self.writer.add_scalar(name+"/grad", param.grad.item(), epoch_idx)
            if self.verbose:
                print(f"epoch_idx={epoch_idx}, name={name}, param.data: {param.data}, param.grad.item: {param.grad.item() if param.grad is not None else 'None'}")

    def record_train_data(self, train_loss, test_loss, epoch_idx):
        # 记录损失数据,训练损失和验证损失对比显示
        self.writer.add_scalars('loss', {'train': train_loss, 'test': test_loss}, epoch_idx)
        if self.verbose:
            print(f"epoch_idx={epoch_idx}, train_loss: {train_loss}, test_loss: {test_loss}")
        
        # 记录模型的所有参数变化,以及参数梯度的变化过程
        self.record_parameters(epoch_idx)
        self.writer.flush()

    def save_checkpoint(self, checkpoint_path):
        checkpoint = {
            "model_state_dict": self.model.state_dict(),
            "optimizer_state_dict": self.optimizer.state_dict(),
        }
        torch.save(checkpoint, checkpoint_path)
        print(f"save checkpoint: {self.model.state_dict()}") if self.verbose else None

    def load_checkpoint(self, checkpoint_path):
        checkpoint = torch.load(checkpoint_path)
        self.model.load_state_dict(checkpoint["model_state_dict"])
        self.optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
        print(f"load checkpoint: {self.model.state_dict()}") if self.verbose else None

    def reset(self, model, lossfn, optimizer):
        if hasattr(self, "model"):
            self.model.cpu() if self.model != None else None
            del self.model
            del self.optimizer

        self.model = model
        self.lossfn = lossfn
        self.optimizer = optimizer
        print(f"reset model and optimizer: {self.model.state_dict()}, {self.optimizer.state_dict()}") if self.verbose else None

参考资料

  • 基于pytorch+可视化重学线性回归模型
  • 基于numpy演练可视化梯度下降

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1946592.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jenkins卡在等待界面解决方法

一、问题 部署jenkins服务器出现Please wait while Jenkins is getting ready to work。 二、原因分析 jenkins里面文件指向国外的官网&#xff0c;因为防火墙的原因连不上。 三、解决方法 将配置文件里面的url换成国内镜像&#xff1a; &#xff08;1&#xff09;修改配…

[k8s源码]9.workqueue

client-go 是一个库&#xff0c;提供了与 Kubernetes API 服务器交互的基础设施。它提供了诸如 Informer、Lister、ClientSet 等工具&#xff0c;用于监听、缓存和操作 Kubernetes 资源。而自定义控制器则利用这些工具来实现特定的业务逻辑和自动化任务。业务逻辑实现&#xff…

jmeter实战(2)- 入门使用教程

一、运行Jmeter 参考上一篇博客&#xff1a;jmeter实战&#xff08;1&#xff09;- Mac环境安装 二、创建线程组 JMeter的线程组是进行负载测试的基本构建单元&#xff0c;它用于模拟多个用户对目标系统进行并发访问。线程组中的属性允许你控制测试的并发级别和执行模式。 1.…

聚观早报 | Meta将推出新款AR眼镜;iPhone SE 4将升级显示屏

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 7月24日消息 Meta将推出新款AR眼镜 iPhone SE 4将升级显示屏 华硕天选Air 2024开启预约 巴菲特再次减持比亚迪股…

DT浏览器首页征集收录海内外网址

DT浏览器首页征集收录海内外网址&#xff0c;要求页面整洁&#xff0c;内容丰富&#xff0c;知识性和可读性强&#xff0c;符合大众价值观&#xff0c;不含恶意代码

linux添加普通用户后无法使用K8S的kubectl命令怎么办/Linux普通用户管理K8S/Linux下普通用户无法使用K8S命令

1.给Linux添加普通用户 sudo useradd mqq #添加mqq账号 sudo passwd mqq #给mqq账号设置密码&#xff0c;需要输入2次&#xff0c;我输入密码是Admin1232.利用mqq用户输入K8S命令报错 3.给mqq用户提权 suduers文件位于路径/etc/sudoers #编辑文件/etc/sudoers vim /etc/su…

第十四章 数据库

第十四章 数据库 14.1 引言 数据存储在传统上是使用单独的没有关联的文件&#xff0c;称为平面文件 14.1.1 定义 定义&#xff1a;数据库是一个组织内被应用程序使用的逻辑相一致的相关数据的集合 14.1.2 数据库的优点 数据库的优点&#xff1a; 冗余少避免数据的不一致…

ZLMRTCClient配置说明与用法(含示例)

webRTC播放视频 后面在项目中会用到通过推拉播放视频流的技术&#xff0c;所以最近预研了一下webRTC 首先需要引入封装好的webRTC客户端的js文件ZLMRTCClient.js 下面是地址需要的自行下载 http://my.zsyou.top/2024/ZLMRTCClient.js 配置说明 new ZLMRTCClient.Endpoint…

小猪佩奇.js

闲着没事 使用js 画一个小猪佩奇把 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</tit…

BUUCTF [MRCTF2020]Ezpop

这道题对于刚接触到pop链的我直接把我整懵了&#xff0c;一边看着魔术方法一边分析 魔术方法可以看这里PHP 魔术方法 - 简介 - PHP 魔术方法 - 简单教程&#xff0c;简单编程 (twle.cn) 代码解析 经过以上的分析我们可以理一下解题思路&#xff1a;接收参数反序列化之前先触发…

基于 HTML+ECharts 实现智慧交通数据可视化大屏(含源码)

构建智慧交通数据可视化大屏&#xff1a;基于 HTML 和 ECharts 的实现 随着城市化进程的加快&#xff0c;智慧交通系统已成为提升城市管理效率和居民生活质量的关键。通过数据可视化&#xff0c;交通管理部门可以实时监控交通流量、事故发生率、道路状况等关键指标&#xff0c;…

C#使用csvhelper实现csv的操作

新建控制台项目 安装csvhelper 33.0.1 写入csv 新建Foo.cs namespace CsvSut02;public class Foo {public int Id { get; set; }public string Name { get; set; } }批量写入 using System.Globalization; using CsvHelper; using CsvHelper.Configuration;namespace Csv…

Python3网络爬虫开发实战(2)爬虫基础库

文章目录 一、urllib1. urlparse 实现 URL 的识别和分段2. urlunparse 用于构造 URL3. urljoin 用于两个链接的拼接4. urlencode 将 params 字典序列化为 params 字符串5. parse_qs 和 parse_qsl 用于将 params 字符串反序列化为 params 字典或列表6. quote 和 unquote 对 URL的…

通信原理思科实验五:家庭终端以太网接入Internet实验

实验五 家庭终端以太网接入Internet实验 一实验内容 二实验目的 三实验原理 四实验步骤 1.按照上图选择对应的设备&#xff0c;并连接起来 为路由器R0两个端口配置IP 为路由器R1端口配置IP 为路由器设备增加RIP&#xff0c;配置接入互联网的IP的动态路由项 5.为路由器R1配置静…

Mysql-索引视图

目录 1.视图 1.1什么是视图 1.2为什么需要视图 1.3视图的作用和优点 1.4创建视图 1.5更新视图 1.6视图使用规则 1.7修改视图 1.8删除视图 2.索引 2.1什么是索引 2.2索引特点 2.3索引分类 2.4索引优缺点 2.5创建索引 2.6查看索引 2.7删除索引 1.视图 1.1什么是…

[Javascript】前端面试基础3【每日学习并更新10】

Web开发中会话跟踪的方法有那些 cookiesessionurl重写隐藏inputip地址 JS基本数据类型 String&#xff1a;用于表示文本数据。Number&#xff1a;用于表示数值&#xff0c;包括整数和浮点数。BigInt&#xff1a;用于表示任意精度的整数。Boolean&#xff1a;用于表示逻辑值…

流量录制与回放:jvm-sandbox-repeater工具详解

在软件开发和测试过程中&#xff0c;流量录制与回放是一个非常重要的环节&#xff0c;它可以帮助开发者验证系统在特定条件下的行为是否符合预期。本文将详细介绍一款强大的流量录制回放工具——jvm-sandbox-repeater&#xff0c;以及如何利用它来提高软件测试的效率和质量。 …

linux进程——解析命令行参数——环境变量详解

前言&#xff1a;本节内容还是linux进程&#xff0c; 主要讲解里面的环境变量——我们首先要知道的就是环境变量其实就是操作系统维护的一组kv值&#xff0c; 环境变量是系统提供的一组 变量名变量值 形式的变量。不同的环境变量之间具有不同的用途&#xff0c; 并且具有全局属…

刷机维修进阶教程-----何谓“tee损坏” 指纹丢失 掉帧 传感器失效?详细修复步骤教程

TEE损坏指的是安卓机型中Key Attestation密钥认证所依赖的可信应用中的证书库被破坏了。然后拒绝为指纹密匙认证提供服务。加密的密匙由TEE负责管理。tee损坏只影响当前机型的密匙认证。不影响加密。通俗的理解。如果你机型维修或者刷机或者解锁或者格机 全檫除分区等等后有异常…

Python自然语言处理库之NLTK与spaCy使用详解

概要 自然语言处理(NLP)是人工智能和数据科学领域的重要分支,致力于让计算机理解、解释和生成人类语言。在Python中,NLTK(Natural Language Toolkit)和spaCy是两个广泛使用的NLP库。本文将详细介绍NLTK和spaCy的特点、功能及其使用方法,并通过具体示例展示如何使用这两…