4. Pytorch入门教程——创建一个基类来构建一个基本的神经网络

现在我们已经准备好了Dataloaders,之后要定义神经网络并训练它。为了定义一个神经网络,最好的方法是定义类来分离和抽象所有类型网络的通用功能,如训练循环,验证,评估,预测,设置不同的超参数等。

我们还需要定义实现特定类型网络的类,例如专门用于迁移学习的类,或为全连接操作的类等等。我们将创建三个主要类:

  • 从Pytorch的核心类nn.Module派生的神经网络的基类,它是Pytorch中任何神经网络的基础;
  • 派生自我们创建的基类的一个类,实现迁移学习的功能;
  • 派生自我们创建的基类的一个类,实现全连接网络的功能。

让我们一步步来创建名叫Network的基类

'''
从创建神经网络的Pytorch核心类nn.Module继承我们的类
'''
class Network(nn.Module):
    def __init__(self,device=None):
        '''
         我们调用父类的构造函数
        '''
        super().__init__()
        '''
        如果过用gpu,我们就设置设备属性为‘cuda’,否则设置为‘cpu'
        这将帮助我们避免在代码中到处检查是否有CUDA可用
        '''
        if device is not None:
            self.device = device
        else:
            self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    '''
    我们创建一个虚构的forward方法。forward方法是Pytorch中的核心方法,它执行网络图并通过网络传递输入,将输入转换后在另一端获得输出
    在Pytorch中,我们编写自己的forward方法,该方法在运行时执行在__init__方法中定义的模块
    因为我们将在派生类中编写forward方法,所以在基类中它是空的
    '''
    def forward(self,x):
        pass

注意forward方法通过nn.Module的“call”方法调用,因此,我们的类对象可以成为一个“可调用的”对象,当它被调用时,forward方法将被自动调用。

一、训练方法

1. 接下来,我们添加了train方法。对于任何神经网络的训练,在训练循环的每次迭代中都需要执行一些常见的任务。这部分代码遍历每个批处理。这里定义了一个single epoch(单遍历整个数据集):

  • 获取数据的下一个批;
  • 将该批Tensors移到设备中(GPU或者CPU);
  • 将所有权重归零;
  • 调用forward函数,通过网络输入;
  • 将得到的输出传递给criterion(损失函数),与标签(目标)进行比较,并计算损失(loss);
  • 计算梯度;
  • 根据梯度和学习率更新所有权重;
  • 更新该epoch中的所有损失。

如果你熟悉神经网络的基础知识,那么你肯定知道这些步骤,因为它们对于所有框架和神经网络类型都是通用的。train_方法中的以下代码执行这些步骤。代码是非常好解释的,下面是Pytorch特定函数的简要介绍:

class Network(nn.Module):
    ...
    
    '''
    print_every代表我们要打印多少批之后的损失信息
    '''
    def train_(self,trainloader,criterion,optimizer,print_every):
        '''
        下面的train方法(self.train())是基类(nn.Module)中的一个内置Pytorch方法,它在模型对象上设置一个标志,代表正在训练
        此标志在几个Pytorch模块使用,这些模块在训练和验证/测试期间表现不同,例如Dropout、batch normalization等
        '''
        self.train()
        t0 = time.time()
        batches = 0
        running_loss = 0
        
        '''
        inputs和labels分别来自trainloader的一批图像及其对应的标签
        '''
        for inputs, labels in trainloader:
            batches += 1
            #t1 = time.time()
            
            inputs, labels = inputs.to(self.device), labels.to(self.device)
            optimizer.zero_grad()
            outputs = self.forward(inputs)
            '''
            Criterion是基本的损失函数,计算网络输出和实际标签的差异
            '''
            loss = criterion(outputs, labels)
            
            '''
            loss.backward()实现反向传播,计算根据连通张量的完全图计算网络中的梯度
            '''
            loss.backward()
            
            '''
            损失函数执行完后,Optimizer.step实现优化算法的一步,产生新的梯度
            '''
            
            optimizer.step()
            
            '''
            item()给出一个标量值,它用于返回单个值的张量(在本例中,loss是一个浮点数值)
            '''
            loss = loss.item()
            
            #print('training this batch took {:.3f} seconds'.format(time.time() - t1))
            '''
            计算一个epoch完整的loss
            '''
            running_loss += loss
            
            '''
            输出loss信息,如果批的数量已经达到print_every
            '''
            if batches % print_every == 0:
                print(f"{time.asctime()}.."
                        f"Time Elapsed = {time.time()-t0:.3f}.."
                        f"Batch {batches+1}/{len(trainloader)}.. "
                        f"Average Training loss: {running_loss/(batches):.3f}.. "
                        f"Batch Training loss: {loss:.3f}.. "
                        )
                t0 = time.time()
        
        '''
        最后我们返回该epoch的平均loss
        '''
        return running_loss/len(trainloader) 

2. 损失函数
注意,Pytorch附带了许多内置的损失函数,用于分类和回归等常见情况。这里我们将损失函数作为参数传递给train_。在分类中使用的一些常见的损失函数有交叉损失、Negative Likehood Log损失(NLLLoss)和二进制交叉损失。在本教程稍后讨论全连接类时,我们将更多地讨论损失函数。
3. 优化器模块
优化器模块应用梯度下降或其变体,执行梯度和学习率的更新。优化器有几种不同的算法,可以在torch.optim模块中找到。例子包括随机梯度下降(SGD),Adam,AdaDelta等。

二、验证方法

验证是将模型应用到验证集进行评估。目的是定期评估我们在训练方面的性能。如果你熟悉机器学习的概念,你很可能知道bias(拟合不足)和variance(过拟合)。如果我们在验证集上的损失显著且始终的高于在训练集上的损失,就是过拟合。这基本上意味着我们的模型在任何其他数据集上都不能泛化得足够好,因为模型与训练集的联系太紧密了。

  • 在每隔几个epochs之后在验证集上评估模型(默认是在每个epoch之后),测量损失并将其输出,以查看是否过拟合;
  • 验证方法与训练方法的区别在于,在验证过程中我们不需要进行反向传播、计算梯度、应用梯度下降和更新权重。我们所需要做的就是通过我们的模型分批传递验证数据集,并使用损失函数评估loss;
  • 当模型在几个epochs后性能变得更好后,验证loss就会下降;
  • 在验证中我们还想做的另一件事是计算分类的准确性,就是在预测中有多少次是正确的百分比:100x(准确预测类的数量/数据集大小);
  • 计算每个类的准确性,也就是说,对于每个单独的类,我们计算有多少是正确的;
  • 因此,我们还编写了一个实用函数来计算每个类(classwise)的准确性,如下所示。当我们对测试集或任何其他图像集进行预测时,这可能会很方便。
from collections import defaultdict

def update_classwise_accuracies(preds,labels,class_correct,class_totals):
    
    correct = np.squeeze(preds.eq(labels.data.view_as(preds)))
    
    '''
    我们只需遍历batch(shape[0]是batch大小)并更新classwise正确的数量和总的数量
    '''
    for i in range(labels.shape[0]):
        label = labels.data[i].item()
        class_correct[label] += correct[i].item()
        class_totals[label] += 1
        
class Network(nn.Module):
    ...
    
    def validate_(self,validloader):
        running_loss = 0.
        accuracy = 0
        
        '''
        创建两个默认字典来存储每个类classwise正确的预测和总的图片数
        '''
        class_correct = defaultdict(int)
        class_totals = defaultdict(int)
        
        '''
        self.eval()是一个将模型放入验证模式的Pytorch方法。它告诉Pytorch,我们只想在网络中执行正向传递,而不需要反向传播。它与我们在训练循环中使用的训练方法相反
        '''
        self.eval()
        
        '''
        无论我们在torch.no_grad()块中放入什么,都会告诉Pytorch不要计算梯度。
        我们要确保在计算循环中梯度永远不会被计算。
        '''
        with torch.no_grad():
            for inputs, labels in validloader:
                inputs, labels = inputs.to(self.device), labels.to(self.device)
                outputs = self.forward(inputs)
                loss = self.criterion(outputs, labels)
                running_loss += loss.item()
                _, preds = torch.max(torch.exp(outputs), 1) # you can safely remove the call to 
                                                            #torch.exp(as described below)
                    
                update_classwise_accuracies(preds,labels,class_correct,class_totals)
        
        '''
        用前面讨论过的简单公式来计算精度。
        '''
        accuracy = (100*np.sum(list(class_correct.values()))/np.sum(list(class_totals.values())))
        
        '''
        把模型放回训练模式
        '''
        self.train()
        
        '''
        Running loss是所有批的loss,用它除以trainloader的长度(批次的数量)就得到了整个验证集的平均损失
        '''
        return (running_loss/len(validloader),accuracy)
  • np.squeeze(preds.eq(labels.data.view_as(preds)))
  • 这似乎是一个相当模糊的语句,所以让我们来分析一下:实际的标签包含在dataloader的数据属性中。预测是网络的输出,view_as方法根据参数传递的张量的维数来重新组织一个张量。在我们的例子中,这个语句会将批处理中的标签与预测张量(即batch_size x 10)对齐,因为我们的网络有10个类,而我们最终的全连接层会为每个批输出这么多outputs。eq(equal)方法比较张量的每一行,如果这些行相等则为1(True),否则为0。最终的结果将是一个50x1张量,我们通过挤压额外的批维度使其变平,成为一个50维的向量(1维张量),其中包含1s(预测等于标签,否则为0s)。np.squeeze从数组的形状中删除单维度条目,即把shape中为1的维度去掉,labels.data.view_as(preds))让预测的维度和labels的维度一样。
  • _, preds = torch.max(torch.exp(outputs), 1)
  • 我们在全连接模型中使用带有负对数似然损失(NLLLoss)的Softmax log(稍后将详细介绍)。因此,我们的输出应该是概率值的对数(也称为Logits)。我们不需要对logit取幂因为logit的最大值仍然会给我们相同的类下标。在这里这样做只是为了让我们的预测看起来像概率,这有时有助于调试。torch.max返回一个包含最大值和张量最大值索引的元组。

三、评估方法

评估方法的目的是在完成训练后在测试集上评估模型的性能。假设我们为要传递给这个方法的数据集提供了可用的标签。

代码几乎与validate相同。唯一的区别是我们不需要计算这种情况下的损失,因为我们已经完成了训练。

因为这个方法返回总体精度和类级精度,所以我们需要另一个实用函数get_accuracies。我们还需要class_names来获得类的实际名称。在创建迁移学习模型时(本教程稍后),我们将把类名存储为字典映射ids(numbers)到类名字符串。

from collections import defaultdict

def update_classwise_accuracies(preds,labels,class_correct,class_totals):
    correct = np.squeeze(preds.eq(labels.data.view_as(preds)))
    for i in range(labels.shape[0]):
        label = labels.data[i].item()
        class_correct[label] += correct[i].item()
        class_totals[label] += 1

def get_accuracies(class_names,class_correct,class_totals):
    
    accuracy = (100*np.sum(list(class_correct.values()))/np.sum(list(class_totals.values())))
    
    '''
    我们获得这个类的名称,并通过用这个类的正确预测除以在测试数据集中拥有的这个类的图像的总数来得到这个类的准确性
    我们添加了一个额外的条件,即我们至少有一个类的图片,以避免除以0
    '''
    class_accuracies = [(class_names[i],100.0*(class_correct[i]/class_totals[i])) 
                        for i in class_names.keys() if class_totals[i] > 0]
    return accuracy,class_accuracies

class Network(nn.Module):
    ...
    
    def evaluate(self,testloader):
        self.eval()
        self.model.to(self.device)
        class_correct = defaultdict(int)
        class_totals = defaultdict(int)
        with torch.no_grad():
            for inputs, labels in testloader:
                inputs, labels = inputs.to(self.device), labels.to(self.device)
                outputs = self.forward(inputs)
                ps = torch.exp(outputs)
                _, preds = torch.max(ps, 1)
                update_classwise_accuracies(preds,labels,class_correct,class_totals)
                
        self.train()    
        return get_accuracies(self.class_names,class_correct,class_totals)

五、预测方法

预测方法用于从训练模型中预测或得出推断,以确定我们没有标签的图像的类别。这是在实际部署模型时将调用的方法。

  • 除了没有标签之外,它与evaluate非常相似;
  • 另一个不同之处在于,我们还对预测类的概率感兴趣;
  • 我们可能还想知道一个以上类别的预测概率,比如前三个最有可能的类别以及它们的指数。
class Network(nn.Module):
    ...
    
    '''
    因为我们需要概率和(可能的)多个类的排序,我们传递topk参数,它告诉我们的函数有多少排序类和它们的概率
    '''
    def predict(self,inputs,topk=1):
        self.eval()
        self.model.to(self.device)
        with torch.no_grad():
            inputs = inputs.to(self.device)
            outputs = self.forward(inputs)
            ps = torch.exp(outputs)
            p,top = ps.topk(topk, dim=1)
        return p,top

Pytorch中张量的topk方法返回k个指标和它们在一个维度上的值(dim=1表示行,也就是水平方向上的值)。因为张量是50倍的类数,这将返回topk类和它们每一行的概率)。

六、拟合方法

这是我们类在启动训练时调用的主要方法。它实现了epoch循环的主训练循环。

它调用train_方法,定期调用验证来监视性能和过拟合等,追踪到目前为止获得的最佳精度,保存最佳精度模型,将完整模型及其超参数和其他变量保存到磁盘作为checkpoint。如果由于某种原因断电或训练中断,可以恢复checkpoint并在以后继续训练。

让我们一步一步来构建这个方法:

class Network(nn.Module):
    ...
    
    def fit(self,trainloader,validloader,epochs=2,print_every=10,validate_every=1):
            
        for epoch in range(epochs):
            '''
            将模型移到设备('gpu'或'cpu')
            '''
            self.model.to(self.device)
            
            print('epoch {:3d}/{}'.format(epoch+1,epochs))
            
            epoch_train_loss =  self.train_(trainloader,self.criterion,
                                            self.optimizer,print_every)
            
            
            '''
            检查是否需要在每个validate_every epochs之后调用validate,调用它并输出验证损失和准确性
            '''
            if  validate_every and (epoch % validate_every == 0):
                t2 = time.time()
                epoch_validation_loss,epoch_accuracy = self.validate_(validloader)
                time_elapsed = time.time() - t2
                print(f"{time.asctime()}--Validation time {time_elapsed:.3f} seconds.."
                      f"Epoch {epoch+1}/{epochs}.. "
                      f"Epoch Training loss: {epoch_train_loss:.3f}.. "
                      f"Epoch validation loss: {epoch_validation_loss:.3f}.. "
                      f"validation accuracy: {epoch_accuracy:.3f}")
                    
                self.train()

七、保存最佳模型

拟合函数还应该监视到目前为止在所有epoch中获得的最佳精度,并在获得一个比以前更好的新模型时保存最佳精度模型。这确保了即使没有检查点,如果在训练期间验证损失开始下降,我们也应该能够检索出我们的最佳模型。

这是一个常见的场景,因为训练可能需要几个小时才能完成,我们可能不得不离开系统。这样我们可以确保我们总是重新加载最佳精度模型的权重,并使用它们进行推断。

from collections import defaultdict
import math

class Network(nn.Module):
    def __init__(self,device=None):
        ...
        
        '''
        初始化best_accuracy为0.
        '''
        self.best_accuracy = 0.

    ...
    
    def fit(self,trainloader,validloader,epochs=2,print_every=10,validate_every=1):
            
        for epoch in range(epochs):
            self.model.to(self.device)
            print('epoch {:3d}/{}'.format(epoch+1,epochs))
            epoch_train_loss =  self.train_(trainloader,self.criterion,
                                            self.optimizer,print_every)
                    
            if  validate_every and (epoch % validate_every == 0):
                t2 = time.time()
                epoch_validation_loss,epoch_accuracy = self.validate_(validloader)
                time_elapsed = time.time() - t2
                print(f"{time.asctime()}--Validation time {time_elapsed:.3f} seconds.."
                      f"Epoch {epoch+1}/{epochs}.. "
                      f"Epoch Training loss: {epoch_train_loss:.3f}.. "
                      f"Epoch validation loss: {epoch_validation_loss:.3f}.. "
                      f"validation accuracy: {epoch_accuracy:.3f}")
                
                
                '''
                如果验证返回更好的精度,检查并保存最佳精度模型
                '''
                if self.best_accuracy == 0. or (epoch_accuracy > self.best_accuracy):
                    print('updating best accuracy: previous best = {:.3f} new best = {:.3f}'.format(self.best_accuracy,
                                                                                     epoch_accuracy))
                    self.best_accuracy = epoch_accuracy
                    
                    '''
                    Pytorch保存方法通过使用Python的Pickle模块序列化Pytorch张量数据结构来保存。在这里,我们存储由state_dict()方法返回的模型状态字典,该方法                     包含模型全图的所有权值(体系结构中的每个张量)
                    '''
                    torch.save(self.state_dict(),self.best_accuracy_file)
                    
                self.train() # just in case we forgot to put the model back to train mode in validate
                
        print('loading best accuracy model')
        
        '''
        当我们完成训练循环时,我们恢复最佳精度的模型。这确保我们使用最佳精度的模型。
        This ensures that any evaluation or inference we perform while the model 
        remains in memory shall be done using the best accuracy model instead of 
        the one obtained in the last iteration of the training loop.
        '''
        self.load_state_dict(torch.load(self.best_accuracy_file))

注意self.best_accuracy_file应该是在模型参数初始化期间设置的文件名(请参见下一节)

八、设置并获得不同的参数和超参数

我们需要设置模型中不同的参数和超参数。包括损失函数(criterion),优化器,dropout概率,学习率和其他参数。我们写四个方法:

  • set_criterion创建一个损失函数的实例并将其设置在模型上;
  • set_optimizer创建一个优化器的实例并将其设置在模型上;
  • set_model_params调用以上两个函数,设置另外的超参数;
  • get_model_params检索模型上当前设置的参数。当我们想要保存一个完整的模型检查点时,这将非常方便。
class Network(nn.Module):
    ...
    
    def set_criterion(self,criterion_name):
            if criterion_name.lower() == 'nllloss':
                self.criterion_name = 'NLLLoss'
                self.criterion = nn.NLLLoss()
            elif criterion_name.lower() == 'crossentropyloss':
                self.criterion_name = 'CrossEntropyLoss'
                self.criterion = nn.CrossEntropyLoss()

    def set_optimizer(self,params,optimizer_name='adam',lr=0.003):
        from torch import optim

        if optimizer_name.lower() == 'adam':
            print('setting optim Adam')
            self.optimizer = optim.Adam(params,lr=lr)
            self.optimizer_name = optimizer_name
        elif optimizer.lower() == 'sgd':
            print('setting optim SGD')
            self.optimizer = optim.SGD(params,lr=lr)

        elif optimizer.lower() == 'adadelta':
            print('setting optim Ada Delta')
            self.optimizer = optim.Adadelta(params)
            
    def set_model_params(self,
                         criterion_name,
                         optimizer_name,
                         lr, # learning rate
                         dropout_p,
                         model_name,
                         best_accuracy,
                         best_accuracy_file,
                         class_names):
        
        self.set_criterion(criterion_name)
        self.set_optimizer(self.parameters(),optimizer_name,lr=lr)
        self.lr = lr
        self.dropout_p = dropout_p
        self.model_name =  model_name
        self.best_accuracy = best_accuracy
        self.best_accuracy_file = best_accuracy_file
        self.class_names = class_names
    
    def get_model_params(self):
        params = {}
        params['device'] = self.device
        params['model_name'] = self.model_name
        params['optimizer_name'] = self.optimizer_name
        params['criterion_name'] = self.criterion_name
        params['lr'] = self.lr
        params['dropout_p'] = self.dropout_p
        params['best_accuracy'] = self.best_accuracy
        params['best_accuracy_file'] = self.best_accuracy_file
        params['class_names'] = self.class_names
        return params
  • set_criterion支持两个损失函数:CrossEntropy和NLLLoss;
  • 它传递给损失函数的名称,并使用Pytorch API实例化一个对象;
  • set_optimizer通过使用Pytorch API对优化器进行实例化,从而启用优化器。它默认为“Adam”,而SGD和Adadelta也可以设置。同样,对其他优化器的支持可以很容易地添加;
  • set_model_params是一种更高级的方法,它调用set_criterion和set_optimizer以及其他参数,如model_name、当前最佳精度值、best_accuracy_file,我们在其中存储最佳精度、模型权重、学习率和dropout概率;
  • 为了简洁,我们省略了参数类型的完整性检查(例如model_name、optimizer_name应该是字符串、dropout_p、lr应该是浮点数等等);
  • set_model_param方法应该从主要的模型类中调用,例如迁移学习和全连接模型,我们接下来将从这个基础网络类中派生这些模型的类
  • get_model_param只返回当前参数作为字典。它将用于创建检查点(参见下一节);
  • class_names是一个字典,它包含类标识符(整数)到类名(字符串)的映射。

九、保存模型Checkpoint

  • 在训练深度学习模型时,保存模型的检查点是一项重要的任务;
  • 这样我们就可以轻松地执行长时间运行的训练循环;
  • 如果有任何中断,例如机器崩溃,电源故障,Jupyter Notebpook崩溃或任何其他不可预见的问题发生,我们的训练被中断,我们可以从上一个检查点恢复并继续训练。我们的训练时间不会浪费;
  • 现在我们将实现一个方法save_checkpoint;
  • 稍后在本教程中我们将实现一个实用函数load_checkpoint。
class Network(nn.Module):
    ...
    
    '''
    增加chkpoint_file参数到set_params函数
    '''
    def set_model_params(self,
                         criterion_name,
                         optimizer_name,
                         lr, # learning rate
                         dropout_p,
                         model_name,
                         best_accuracy,
                         best_accuracy_file,
                         chkpoint_file):
        
        self.criterion_name = criterion_name
        self.set_criterion(criterion_name)
        self.optimizer_name = optimizer_name
        self.set_optimizer(self.parameters(),optimizer_name,lr=lr)
        self.lr = lr
        self.dropout_p = dropout_p
        self.model_name =  model_name
        self.best_accuracy = best_accuracy
        print('set_model_params: best accuracy = {:.3f}'.format(self.best_accuracy))  
        self.best_accuracy_file = best_accuracy_file
        self.chkpoint_file = chkpoint_file
    
    def get_model_params(self):
        params = {}
        params['device'] = self.device
        params['model_name'] = self.model_name
        params['optimizer_name'] = self.optimizer_name
        params['criterion_name'] = self.criterion_name
        params['lr'] = self.lr
        params['dropout_p'] = self.dropout_p
        params['best_accuracy'] = self.best_accuracy
        print('get_model_params: best accuracy = {:.3f}'.format(self.best_accuracy))  
        params['best_accuracy_file'] = self.best_accuracy_file
        params['chkpoint_file'] = self.chkpoint_file
        print('get_model_params: chkpoint file = {}'.format(self.chkpoint_file))  
        return params
    
    def save_chkpoint(self):
        saved_model = {}
        '''
        通过get_model_params获取所有参数和类名,并将它们存储到chkpoint文件中
        '''
        saved_model['params'] = self.get_model_params()    
        torch.save(saved_model,self.chkpoint_file)
        print('checkpoint created successfully in {}'.format(self.chkpoint_file))

你可能感兴趣的:(Pytorch入门教程,神经网络,机器学习,深度学习,pytorch)