深度学习3-从一个例子看torch用于深度学习开发的一般流程

  • 本文旨在介绍 torch 模型研发 的整体流程, 对数据加载、建模的过程没有深究细节;
  • 此外包含了一些 损失函数、优化器函数 的延申阅读(1.2节),可起到简单的“查函数”的作用

1. 速览

1.1 深度学习开发的一般流程

这里的 深度学习开发 是指 模型研发,不包括工程化落地的部分(比如 转onnx、模型集成 等后续工作不在本文的介绍范围之内)。

1.1.1 流程简述

模型开发的一般过程可大致分为如下步骤:

  • step1: 数据
    收集数据、数据加载函数

  • step2: 建模
    搭建神经网络结构:①继承;②初始化时创建层;③重写forword。

  • step3: 训练 & 模型参数保存
    选好优化器类型、学习率;设计损失函数;保存模型参数;训练过程追踪。

  • step4: 模型参数加载 & 测试
    加载模型参数;测试该模型参数的效果;分析badcase。

1.1.2 流程详述

  • step1: 数据
    – step1.1: 收集数据
    (a) 来源:各种官方数据集、用户提供的数据集、自己采集、爬虫(慎用😁);
    (b) 划分训练集&测试集:在模型验证阶段需要划分测试集用于验证模型效果(比如可以采用7:3的比例划分),在最终成品阶段如果期待进一步提高模型效果,可以采用全集重新训练,或者在现有参数基础上 以较小的学习率在测试集上训练(不一定真的提高哦)。
    – step1.2: 数据加载
    主要考虑加载一个batch的数据,也就是如何加载 一次迭代训练 用到的一批数据(包括输入张量&输出标签)。

  • step2: 建模
    也就是搭建神经网络结构。
    在pytorch中:
    ①需要继承Module类;
    ②在初始化时创建大量 神经网络层对象;
    ③重写forword函数,也就是编写正向传播过程。

  • step3: 训练 & 模型参数保存
    – (a) 核心步骤
    ① 选好优化器类型、学习率:优化器就是迭代模型参数的工具,其原理来自于工程优化(如梯度下降算法)以及 链式求导(反向传播过程);
    ② 设计损失函数:损失函数也就是优化目标,即工程优化中构造有最小值的最优化目标;
    ③ 定期地保存训练过程中的模型参数:这个“期”值得探讨,可以每隔固定的迭代次数保存一次模型参数,也可以每个epoch(完整跑完一遍训练集数据称为一个epoch)保存一遍模型参数,也可以根据训练过程的loss(小于某阈值)、acc(大于某阈值)保存模型参数。
    – (b) 训练过程追踪
    方便我们观察模型参数的收敛情况:
    ① loss是否呈现明显的下降趋势(或 acc 呈现明显地上升趋势);
    ② 模型是否已经收敛,是否可以终止训练了,又或者需要增加训练轮数。

  • step4: 模型参数加载 & 测试
    ① 可以直接测试 训练得到的最终的模型(也就是最后一轮训练结束得到的模型);
    ② 更常用的是,我们加载训练过程中保存的某个模型参数,然后测试该模型参数的效果;
    ③ 保存测试结果(负例的输入、标签、模型输出),以便分析模型犯错的特点,进而给出模型改进的方向。

1.2 初识torch关键类和函数(含大量延申阅读)

1.2.1 建模 & 模型层信息

(1) 继承 torch.nn.Module;重写 init()、forward(x)

(2) 常用的 激活函数 & 损失函数 在 torch.nn.functional. 下 或者 torch.nn. 下

(3) 模型层信息: mdl.named_parameters()
详见2.2节的show_model() 函数,包含:层名、张量形状、是否可训练、驱动信息(cpu|cuda|…) 的打印

1.2.2 优化器 & 迭代

(1)创建优化器: torch.optim. 下有各种常用的优化器

(2)一次迭代优化
optimizer.zero_grad() # 梯度清空
loss.backward() # 反向传播
optimizer.step() # 优化模型参数

1.2.3 模型保存 & 加载

(1)关键函数: torch.save()、torch.load()

(2)保存&加载 模型整体
torch.save(model, save_path_str)
model = torch.load(save_path_str)

(3)保存&加载 模型参数
torch.save(model.state_dict(), save_path_str)
model.load_state_dict( torch.load(save_path_str) )

2. 举例:手写数字识别(含代码)

2.1 任务&模型简介

手写数字识别是一个非常经典的任务,以现在的眼光看似乎十分简单,但这正适合作为入门教材。

(1)输入
输入是 12828 的张量, 实际上是一张小图片,也就是一个手写数字。

(2)输出
输出目标是 0,1, … ,9 中的一个数字。

(3)模型结构
模型结构非常简单,将输入展平为向量,通过三层全连接层得到输出向量 (输出向量维度为10, 也就是一个类别向量,以 onehot 编码作为优化目标)

(4)优化相关
① 本文在输出层上 - 甚至没采用softmax函数来作为最终输出概率向量的函数;
② 本文在损失函数上 - 没有采用 交叉熵损失函数CrossEntropyLoss作为损失函数,而是简单采用 均方差损失函数mse_loss;
③ 本文在优化器上 - 直接采用了梯度下降优化器SGD

虽然文章采用的神经网络结构相当落后,但损失的下降过程还是非常明显的,且3个epoch 的正确率在95%左右。说明这个模型对该任务是“适配”的,似乎是因为这个任务比较“纯粹”(毕竟只有手写数字数据集,业务范围相当狭窄),当然如果采用卷积神经网络处理图像分类应该会有更好的效果。

2.2 代码

#%% global params
# 1. 模型参数
class_n = 10

pic_w = 28
pic_h = 28
pic_flat = pic_w*pic_h

# 2. 训练参数
epoch_num = 3
batch_size_train = 64
learn_rate = 0.01
momentum = 0.9
show_train_step = 10

save_mdl_param_path = "./output/model_param/<tag>/model_param_dict.bin"
save_loss_pic_path = "./output/model_param/loss.jpeg"

# 3. 测试参数
batch_size_test = 100
show_test_step = 10
save_badcase_path = "./output/bad_case/<tag>/badcase.txt"
save_acc_path = "./output/bad_case/<tag>/acc.txt"

test_epc_num = 1 # 0,1,2 即 [0,epoch_num)
badcase_show_id = 0
save_badcase_pic_path = "./output/bad_case/<tag>/badcase_"+str(badcase_show_id)+".jpeg"

# 4. 硬件调用参数
device = "cuda"

#%% import
import os
import numpy as np
import torch
import torchvision
from matplotlib import pyplot as plt

#%% com func - 1. dir
def make_dir_by_tag(org_path, tag):
    base_folder, file_name = org_path.split("<tag>")
    save_folder = base_folder + str(tag)
    save_path = save_folder + file_name

    if not os.path.exists(save_folder):
        os.makedirs(save_folder)
    
    return save_path

#%% work func - 1. func: data loader
def get_dataLoader_by_batchSize(batch_size):
    return torch.utils.data.DataLoader(
    torchvision.datasets.MNIST('./data/', train=True, download=True,
                               transform=torchvision.transforms.Compose([
                                   torchvision.transforms.ToTensor(),
                                   torchvision.transforms.Normalize(
                                       (0.1307,), (0.3081,))
                               ])),
    batch_size=batch_size, shuffle=True)

def one_hot(y, class_n=class_n):
    batch_size = y.size(0)
    ret = torch.zeros((batch_size, class_n))
    for i in range(batch_size):
        class_idx = y[i]
        ret[i][class_idx] = 1
    return ret

#%% work func - 2. class: model
class MyFirstModel(torch.nn.Module):
    def __init__(self):
        super(MyFirstModel, self).__init__()

        # 核心网络层
        self.fc1 = torch.nn.Linear(pic_flat, 256)
        self.fc2 = torch.nn.Linear(256,64)
        self.fc3 = torch.nn.Linear(64,10)

    def forward(self, x):
        """正向传播"""
        x = self.fc1(x)
        x = torch.nn.functional.relu(x) # 激活函数层

        x = self.fc2(x)
        x = torch.nn.functional.relu(x) # 激活函数层

        x = self.fc3(x)
        
        return x

def show_model(mdl, msg = ""):
    print("-"*15)
    print(msg, "| model params:")
    model_params = mdl.named_parameters()
    for name,param in model_params:
        print(
            "name:",name,
            "| shape:", param.size(),
            "| requires_grad:", param.requires_grad,
            "| device:",param.device
        )
    print("-"*15)

#%% work func - 3. train func
def save_model_by_epcNum(mdl, epc_num):
    torch.save(
        mdl.state_dict(),
        make_dir_by_tag(save_mdl_param_path, "epc_" + str(epc_num) )
    )

def trainning():
    ret_trainLoss = []

    train_data_loader = get_dataLoader_by_batchSize(batch_size_train)
    mdl = MyFirstModel().to(device)
    show_model(mdl, "trainning")
    optimizer = torch.optim.SGD(mdl.parameters(), lr=learn_rate, momentum=momentum)
    for epc in range(epoch_num):
        for batch_idx, (x, y) in enumerate(train_data_loader):
            x = x.view(x.size(0), pic_flat).to(device)
            y = one_hot(y).to(device)

            pred_y = mdl(x)

            loss = torch.nn.functional.mse_loss(y, pred_y).to(device)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            # 训练过程 打印
            if batch_idx % show_train_step == 0:
                print("epc:",epc, ", step:",batch_idx, "| loss:",loss.item())
                ret_trainLoss += [loss.item()]
        
        # 保存每个 epoch 的 模型参数
        save_model_by_epcNum(mdl, epc)
    
    return mdl, ret_trainLoss

def show_loss(loss_list):
    plt.figure()
    plt.plot(range(len(loss_list)), loss_list)
    # plt.show()
    
    if save_loss_pic_path:
        plt.savefig(save_loss_pic_path)

# work func - 4. test func
def save_one_badcase(x, y_tag, y_pred, save_tag):
    f = open(
        make_dir_by_tag(save_badcase_path, str(save_tag) ),
        'a',
        encoding="utf8"
    )

    f.write(
        "x:"+str(x) 
        + "\ttag:" + str(y_tag) 
        + "\tpred:" + str(y_pred) 
        + "\n"
    )

    f.close()

def save_one_acc(acc, acc_msg, save_tag):
    f = open(
        make_dir_by_tag(save_acc_path, str(save_tag) ),
        'a',
        encoding="utf8"
    )

    f.write("acc:"+str(acc) + " ("+acc_msg+")" + "\n")

    f.close()

def testing(mdl, model_tag:str):
    print("="*20)
    print("start testing ...")
    show_model(mdl, "testing")

    ret_acc = "null"
    ret_acc_n = ""
    
    ret_bad = {
        "x": [],
        "y_tag": [],
        "y_pred": []
    }
    
    total_n = 0
    pred_right_n = 0

    test_data_loader = get_dataLoader_by_batchSize(batch_size_test)

    for batch_idx, (x, y) in enumerate(test_data_loader):
        x_org = x.to(device)
        x = x.view(x.size(0), pic_flat).to(device)

        pred_y = mdl(x)
        pred_classID = pred_y.argmax(dim=1).to(device)

        for i in range(batch_size_test):
            total_n += 1
            
            if y[i] == pred_classID[i]:
                pred_right_n += 1
            
            else:
                # 保存 预测错误的数据
                bad_x = x_org[i][0].cpu().detach().numpy()
                bad_y_tag = y[i].item()
                bad_y_pred = pred_classID[i].item()

                ret_bad["x"] += [bad_x]
                ret_bad["y_tag"] += [bad_y_tag]
                ret_bad["y_pred"] += [bad_y_pred]

                save_one_badcase(bad_x, bad_y_tag, bad_y_pred, model_tag)
        
        # 测试过程打印
        if batch_idx%show_test_step == 0:
            print(str(batch_idx)+",", end="")
    print()
    
    ret_acc = pred_right_n/total_n if total_n else "null"
    ret_acc_n = str(pred_right_n) + "/" + str(total_n)
    
    save_one_acc(ret_acc, ret_acc_n, model_tag)

    return ret_acc, ret_acc_n, ret_bad

def show_test_bad(tesd_bad_dict, idx, save_tag):
    print("="*20)
    print("start show_test_bad()")
    if "x" not in tesd_bad_dict or len(tesd_bad_dict["x"]) == 0:
        print("warn | show_test_bad() | tesd_bad_dict is empty !")
        return

    x = tesd_bad_dict["x"][idx]
    y_tag = tesd_bad_dict["y_tag"][idx]
    y_pred = tesd_bad_dict["y_pred"][idx]
    print("x.shape:", x.shape)
    print("tag:",y_tag)
    print("pred:",y_pred)

    plt.figure()
    plt.imshow(x)
    plt.title("tag:"+str(y_tag)+"\n"+"pred:"+str(y_pred))
    # plt.show()
    
    if save_badcase_pic_path:
        plt.savefig( make_dir_by_tag(save_badcase_pic_path, save_tag) )

# work func - 5. load model
def load_model_by_epcNum(epc_num):
    mdl = MyFirstModel().to(device)
    mdl.load_state_dict(
        torch.load(
            make_dir_by_tag(save_mdl_param_path, "epc_" + str(epc_num) )
        )
    )
    return mdl


#%% run demo
def run_demo1_train_and_test():
    # 1. 模型训练
    model, loss_list = trainning()
    show_loss(loss_list)

    # 2. 训练的最终结果测试
    acc, acc_msg, badcase = testing(model, "final_model")
    print("final model ( epc",epoch_num-1,") | acc:",acc,"("+acc_msg+")")
    show_test_bad(badcase, badcase_show_id, "final_model") # 展示第一个badcase

def run_demo2_load_and_test(epc_num):
    if epc_num >= epoch_num:
        print("error | epoch num:",epc_num," - out of max epoch_num:",epoch_num-1)
        return

    # 1. 通过 训练过程中 保存的模型参数 重新加载模型
    model = load_model_by_epcNum(epc_num)
    
    # 2. 测试模型
    save_tag = "epc_"+str(epc_num)
    acc, acc_msg, badcase = testing(model, save_tag)
    print("epc",epc_num,"| acc:",acc,"("+acc_msg+")")
    show_test_bad(badcase, badcase_show_id, save_tag) # 展示第一个badcase

#%% main
if __name__ == "__main__":
    print("start testing...")

    # 1. 训练 & 测试
    run_demo1_train_and_test()
    
    # 2. 加载 & 测试
    run_demo2_load_and_test(test_epc_num)
    

3. 执行说明

3.1 关于 MNIST 数据集

(1)首次执行 会下载 MNIST手写数字数据集
在这里插入图片描述

  • 可以在 vscode右侧的 资源管理器 里面看到 下载的数据
    在这里插入图片描述
    图中 1是源码位置;
    图中 2是下载到 源码同级的位置 的数据, 同级是因为代码里面写的相对路径(如下图),意味着会保存到执行程序同级下的data文件夹中:
    在这里插入图片描述

(2)后续执行会从本地直接加载数据
在这里插入图片描述

3.2 产物

在这里插入图片描述

① 产物都放在 output 中
② 训练过程中生成 ./output/model_param/ 文件夹及内容,保存了模型参数(每个epoch 保存一次),训练完后会画损失曲线并保存为./output/model_param/loss.jpeg
③ 测试过程中 生成 ./output/badcase/ 文件夹及内容,保存了: acc、测试中所有错误预测的例子、第一个badcase的可视化图片

  • 注:
    a) ./output/badcase/final_model是训练完成后的模型的测试结果,相当于./output/model_param/epc_2的模型参数的测试结果;
    b) ./output/badcase/epc_1 顾名思义,相当于./output/model_param/epc_1的模型参数的测试结果;
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值