Pytorch从入门到放弃(7)——可视化模型训练过程中的loss变化

  • Post author:
  • Post category:其他


深度学习就像炼丹,一次模型的训练需要很长时间,任何人都无法做到一直盯着模型的训练。通常都是开启模型训练之后,只要不报错并且随着迭代次数的增加模型的loss在下降,这时我都会去干别的了让它跑去吧,第二天再来看我的丹炼的怎么样。这时怎么在一堆仙丹(经过一天的训练会保存许多模型的权重,每迭代多少个batch或每个epoch保存一次权重)中找到最好的那一个。如果盲目的取最后几次保存的,容易取到过拟合的模型;相反如果随机在前中期取一个,则容易取到欠拟合的模型。因此,我们需要可以直观的看到模型训练过程中(训练阶段、验证阶段)loss值与accuracy值的变化曲线,选取一个在训练阶段与验证阶段均达到收敛的权重。

通过查找资料,发现可以借助tensorflow的tensorboard工具实现训练过程的可视化。由于是Caffe起家后来转用Pytorch,期间也学过一段时间的Tensorflow,由于不是一个好的TFBOY对TF的一些函数也不是太清楚,有幸在github上找到一位大神封装好的代码(

https://github.com/yunjey/pytorch-tutorial/tree/master/tutorials/04-utils/tensorboard

),所以我就无耻的拿了过来。

首先需要再已有Pytorch环境的基础上安装tensorflow,安一个cpu版本的tensorflow即可,因为只需要tensorflow带的tensorboard工具箱。(强烈建议安装低版本的tensorflow,在实验中我电脑安装的就是1.4版本的即可满足我可视化的需求,pip install tensorflow==1.4.0)。


1、项目代码组织结构


2、logger.py

大佬封装的工具可以实现,loss、accuracy、weight、grad、image在训练过程中变化记录,个人认为loss与accuracy是模型训练时比较关注的主要指标。

# Code referenced from https://gist.github.com/gyglim/1f8dfb1b5c82627ae3efcfbbadb9f514
import tensorflow as tf
import numpy as np
import scipy.misc 
try:
    from StringIO import StringIO  # Python 2.7
except ImportError:
    from io import BytesIO         # Python 3.x


class Logger(object):
    
    def __init__(self, log_dir):
        """Create a summary writer logging to log_dir."""
        self.writer = tf.summary.FileWriter(log_dir)

    def scalar_summary(self, tag, value, step):
        """Log a scalar variable."""
        summary = tf.Summary(value=[tf.Summary.Value(tag=tag, simple_value=value)])
        self.writer.add_summary(summary, step)

    def image_summary(self, tag, images, step):
        """Log a list of images."""

        img_summaries = []
        for i, img in enumerate(images):
            # Write the image to a string
            try:
                s = StringIO()
            except:
                s = BytesIO()
            scipy.misc.toimage(img).save(s, format="png")

            # Create an Image object
            img_sum = tf.Summary.Image(encoded_image_string=s.getvalue(),
                                       height=img.shape[0],
                                       width=img.shape[1])
            # Create a Summary value
            img_summaries.append(tf.Summary.Value(tag='%s/%d' % (tag, i), image=img_sum))

        # Create and write Summary
        summary = tf.Summary(value=img_summaries)
        self.writer.add_summary(summary, step)
        
    def histo_summary(self, tag, values, step, bins=1000):
        """Log a histogram of the tensor of values."""

        # Create a histogram using numpy
        counts, bin_edges = np.histogram(values, bins=bins)

        # Fill the fields of the histogram proto
        hist = tf.HistogramProto()
        hist.min = float(np.min(values))
        hist.max = float(np.max(values))
        hist.num = int(np.prod(values.shape))
        hist.sum = float(np.sum(values))
        hist.sum_squares = float(np.sum(values**2))

        # Drop the start of the first bin
        bin_edges = bin_edges[1:]

        # Add bin edges and counts
        for edge in bin_edges:
            hist.bucket_limit.append(edge)
        for c in counts:
            hist.bucket.append(c)

        # Create and write Summary
        summary = tf.Summary(value=[tf.Summary.Value(tag=tag, histo=hist)])
        self.writer.add_summary(summary, step)
        self.writer.flush()


3、main.py

文件中主要定义了:数据处理+网络定义+训练+验证+log记录。看过前几篇博客的只需关注新加的用于log记录部分的代码即可。在这个文件中我是每个epoch保存一次模型的权重,并记录一次模型的loss与accuracy。加入需要每隔若干个Batch就保存一次模型权重并记录模型的loss与accuracy,只需将用于记录loss和acccuracy的代码移到for循环里即可,设置一个变量每迭代若干个Batch则记录一次(这时记录的是当前这个Batch的loss与accuracy)。

import numpy as np
from PIL import Image
from torch.utils.data import Dataset, DataLoader
import torch
import torch.nn as nn
from torch.optim import lr_scheduler
from torch.autograd import Variable
from torchvision import datasets, models, transforms
import time
import os
# 导入用于记录训练过程的日志类
from logger import Logger

# 是否使用gpu运算
use_gpu = torch.cuda.is_available()

# 定义数据的处理方式
data_transforms = {
    'train': transforms.Compose([
        # 将图像进行缩放,缩放为256*256
        transforms.Resize(256),
        # 在256*256的图像上随机裁剪出227*227大小的图像用于训练
        transforms.RandomResizedCrop(227),
        # 图像用于翻转
        transforms.RandomHorizontalFlip(),
        # 转换成tensor向量
        transforms.ToTensor(),
        # 对图像进行归一化操作
        # [0.485, 0.456, 0.406],RGB通道的均值与标准差
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    # 测试集需要中心裁剪,甚至不裁剪,直接缩放为224*224for,不需要翻转
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(227),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}


# 定义数据读入
def Load_Image_Information(path):
    # 图像存储路径
    image_Root_Dir = r'图像文件夹路径'
    # 获取图像的路径
    iamge_Dir = os.path.join(image_Root_Dir, path)
    # 以RGB格式打开图像
    # Pytorch DataLoader就是使用PIL所读取的图像格式
    # 建议就用这种方法读取图像,当读入灰度图像时convert('')
    return Image.open(iamge_Dir).convert('RGB')


# 定义自己数据集的数据读入类
class my_Data_Set(nn.Module):
    def __init__(self, txt, transform=None, target_transform=None, loader=None):
        super(my_Data_Set, self).__init__()
        # 打开存储图像名与标签的txt文件
        fp = open(txt, 'r')
        images = []
        labels = []
        # 将图像名和图像标签对应存储起来
        for line in fp:
            line.strip('\n')
            line.rstrip()
            information = line.split()
            images.append(information[0])
            labels.append(int(information[1]))
        self.images = images
        self.labels = labels
        self.transform = transform
        self.target_transform = target_transform
        self.loader = loader

    # 重写这个函数用来进行图像数据的读取
    def __getitem__(self, item):
        # 获取图像名和标签
        imageName = self.images[item]
        label = self.labels[item]
        # 读入图像信息
        image = self.loader(imageName)
        # 处理图像数据
        if self.transform is not None:
            image = self.transform(image)
        return image, label

    # 重写这个函数,来看数据集中含有多少数据
    def __len__(self):
        return len(self.images)


# 生成Pytorch所需的DataLoader数据输入格式
train_Data = my_Data_Set(r'train.txt路径', transform=data_transforms['train'],
                         loader=Load_Image_Information)
val_Data = my_Data_Set(r'val.txt路径', transform=data_transforms['val'],
                       loader=Load_Image_Information)
train_DataLoader = DataLoader(train_Data, batch_size=10, shuffle=True)
val_DataLoader = DataLoader(val_Data, batch_size=10)
dataloaders = {'train': train_DataLoader, 'val': val_DataLoader}
# 读取数据集大小
dataset_sizes = {'train': train_Data.__len__(), 'val': val_Data.__len__()}

# 初始化路径来记录模型训练过程中训练阶段与验证阶段的loss变化
# 训练阶段的日志文件存储路径
train_log_path = r"./log/train_log"
train_logger = Logger(train_log_path)
# 验证阶段日志文件存储路径
val_log_path = r"./log/val_log"
val_logger = Logger(val_log_path)

# 训练与验证网络(所有层都参加训练)
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 每训练一个epoch,验证一下网络模型
        for phase in ['train', 'val']:
            running_loss = 0.0
            running_corrects = 0.0
            if phase == 'train':
                # 学习率更新方式
                scheduler.step()
                #  调用模型训练
                model.train()
                # 依次获取所有图像,参与模型训练或测试
                for data in dataloaders[phase]:
                    # 获取输入
                    inputs, labels = data
                    # 判断是否使用gpu
                    if use_gpu:
                        inputs = inputs.cuda()
                        labels = labels.cuda()

                    inputs, labels = Variable(inputs), Variable(labels)

                    # 梯度清零
                    optimizer.zero_grad()

                    # 网络前向运行
                    outputs = model(inputs)
                    # 获取模型预测结果
                    _, preds = torch.max(outputs.data, 1)
                    # 计算Loss值
                    loss = criterion(outputs, labels)
                    # 反传梯度
                    loss.backward()
                    # 更新权重
                    optimizer.step()
                    # 计算一个epoch的loss值
                    running_loss += loss.item() * inputs.size(0)
                    # 计算一个epoch的准确率
                    running_corrects += torch.sum(preds == labels.data)
                # 计算Loss和准确率的均值
                epoch_loss = running_loss / dataset_sizes[phase]
                epoch_acc = float(running_corrects) / dataset_sizes[phase]
                print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
                torch.save(model.state_dict(), 'The_' + str(epoch) + '_epoch_model.pkl')

                # 1. 记录这个epoch的loss值和准确率
                info = {'loss': epoch_loss, 'accuracy': epoch_acc}
                for tag, value in info.items():
                    train_logger.scalar_summary(tag, value, epoch)

                # 2. 记录这个epoch的模型的参数和梯度
                for tag, value in model.named_parameters():
                    tag = tag.replace('.', '/')
                    train_logger.histo_summary(tag, value.data.cpu().numpy(), epoch)
                    train_logger.histo_summary(tag + '/grad', value.grad.data.cpu().numpy(), epoch)

                # 3. 记录最后一个epoch的图像
                info = {'images': inputs.cpu().numpy()}

                for tag, images in info.items():
                    train_logger.image_summary(tag, images, epoch)

            else:
                # 取消验证阶段的梯度
                with torch.no_grad():
                    # 调用模型测试
                    model.eval()
                    # 依次获取所有图像,参与模型训练或测试
                    for data in dataloaders[phase]:
                        # 获取输入
                        inputs, labels = data
                        # 判断是否使用gpu
                        if use_gpu:
                            inputs = inputs.cuda()
                            labels = labels.cuda()

                        inputs, labels = Variable(inputs), Variable(labels)

                        # 网络前向运行
                        outputs = model(inputs)
                        _, preds = torch.max(outputs.data, 1)
                        # 计算Loss值
                        loss = criterion(outputs, labels)
                        # 计算一个epoch的loss值
                        running_loss += loss.item() * inputs.size(0)
                        # 计算一个epoch的准确率
                        running_corrects += torch.sum(preds == labels.data)

                    # 计算Loss和准确率的均值
                    epoch_loss = running_loss / dataset_sizes[phase]
                    epoch_acc = float(running_corrects) / dataset_sizes[phase]
                    print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
                    # 1. 记录这个epoch的loss值和准确率
                    info = {'loss': epoch_loss, 'accuracy': epoch_acc}
                    for tag, value in info.items():
                        val_logger.scalar_summary(tag, value, epoch)

                    # 2. 记录这个epoch的模型的参数和梯度
                    for tag, value in model.named_parameters():
                        tag = tag.replace('.', '/')
                        val_logger.histo_summary(tag, value.data.cpu().numpy(), epoch)
                        val_logger.histo_summary(tag + '/grad', value.grad.data.cpu().numpy(), epoch)

                    # 3. 记录最后一个epoch的图像
                    info = {'images': inputs.cpu().numpy()}
                    for tag, images in info.items():
                        val_logger.image_summary(tag, images, epoch)

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))


# 精调AlexNet
if __name__ == '__main__':

    # 导入Pytorch封装的AlexNet网络模型
    model = models.alexnet(pretrained=True)
    # 获取最后一个全连接层的输入通道数
    num_input = model.classifier[6].in_features
    # 获取全连接层的网络结构
    feature_model = list(model.classifier.children())
    # 去掉原来的最后一层
    feature_model.pop()
    # 添加上适用于自己数据集的全连接层
    feature_model.append(nn.Linear(num_input, 260))
    # 仿照这里的方法,可以修改网络的结构,不仅可以修改最后一个全连接层
    # 还可以为网络添加新的层
    # 重新生成网络的后半部分
    model.classifier = nn.Sequential(*feature_model)
    if use_gpu:
        model = model.cuda()
    # 定义损失函数
    criterion = nn.CrossEntropyLoss()

    # 为不同层设定不同的学习率
    fc_params = list(map(id, model.classifier[6].parameters()))
    base_params = filter(lambda p: id(p) not in fc_params, model.parameters())
    params = [{"params": base_params, "lr": 0.0001},
              {"params": model.classifier[6].parameters(), "lr": 0.001}, ]
    optimizer_ft = torch.optim.SGD(params, momentum=0.9)

    # 定义学习率的更新方式,每5个epoch修改一次学习率
    exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.1)
    train_model(model, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=10)


4、tensorboard可视化

训练完成后可以在train_log与val_log文件家中看到存储的log文件,如下所示(注意:建议整个项目目录中不要出现中文,实验的过程中我的项目中存在中文字符tensorboard一直无法可依正常可视化):

可视化命令:tensorboard –logdir=train_log(日志文件存放的文件夹) –port=6006

–logdir:日志文件存放的文件夹

–port:端口号

在浏览器中输入

http://localhost:6006

即可实现可视化:

1、loss与accuracy的可视化

2、最后一次参与训练的图像可视化

3、权重与梯度可视化

直方图可视化

分布可视化

上述所有图均为训练阶段在训练过程中的变化,同样的我们也可以获取到验证阶段的变化,这样的话我们可以对比训练阶段与验证阶段的loss与accuracy变化,从而选取最好的模型权重作为最终训练结果。

以上是,学习过程中的一个记录与总结,如有理解错误,恳请批评指正。

完整代码地址:

https://github.com/Sun-DongYang/Pytorch.git



版权声明:本文为MiaoB226原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。