5. Pytorch入门教程——创建一个派生自基类的全连接类

现在,我们已经准备好创建第一个派生类,全连接神经网络。传统上,全连通网络被称为多层感知器(MLP)。在大多数深度学习框架(包括Pytorch)中,它们被简单地称为线性层。

  • 全连接网络依赖Pytorch nn.Linear模块
  • nn.Linear模块由三部分组成:
  1. 输入;
  2. 全连接的隐藏层,每个隐藏层后面都有一个非线性转换(将非线性视为隐藏层的一部分,而不是将其视为一个单独的层);
  3. 输出层和输出数量。

一、全连接网络需求

创建这样一个类需要下列需求:

  • 能够指定任意数量的隐藏层;
  • 能够指定模型的输入和输出的数量;
  • 能够为每一层定义drop out和非线性(‘relu’, tanh等);
  • 能够定义输出层并为分类任务准备输出层;
  • 设置不同的参数和超参数的模型,如优化器,损失函数等。考虑到这些需求,让我们为全连接模型定义一个类。
'''
新类FC派生自我们的基类而不是nn.Module.因此能够在基类获得所有可用的方法
'''
class FC(Network):
    '''
    将模型参数传递到init.我们已经看了大多数参数。另外的参数有num_inputs, num_outputs, 默认为‘relu’的non_linearity和隐藏层维数。
    '''
    def __init__(self,num_inputs,
                 num_outputs,
                 layers=[],
                 lr=0.003,
                 class_names=None,
                 optimizer_name='Adam',
                 dropout_p=0.2,
                 non_linearity='relu',
                 criterion_name='NLLLoss',
                 model_type='classifier',
                 best_accuracy=0.,
                 best_accuracy_file ='best_accuracy.pth',
                 chkpoint_file ='chkpoint_file.pth',
                 device=None):
        
        super().__init__(device=device)
        
        self.set_model_params(criterion_name,
                              optimizer_name,
                              lr,
                              dropout_p,
                              'FC',
                              best_accuracy,
                              best_accuracy_file,
                              chkpoint_file
                              )
  • num_inputs是这个网络将要接受特征的输入的总数;
  • num_outputs是这个网络在通过任何隐藏层之后输出的总数。换句话说,这是输出层的维度;
  • Non-linearity作为属性存储在模型中。注意,我们没有将Non-linearity传递给set_model_params,因为这是特定于模型的,不属于基类;
  • 如果我们想要设置和获取特定于模型的其他参数,我们可能需要在以后实现set_model_params和get_model_params方法的版本。这就像实现我们自己的“init”,然后调用父类的init一样。我们在代码中做额外的工作,调用父进程来完成公共的工作;
  • layers是一个列表,指定每个隐藏层中的单元数。这个列表中的顺序也会指定它们在模型中的顺序。

二、使用nn.Sequential定义网络

  • nn.Sequential是一个Pytorch方法,用于创建简单的序列神经网络,将定义的模块按顺序连接在一起;
  • 在执行时,nn.Sequential在序列中自动的调用每个模块的前向传播方法 这里我们首先定义一个空的nn.Sequential,然后增加输入模块,隐藏层和输出模块。
class FC(Network):
    def __init__(self,num_inputs,
                 num_outputs,
                 layers=[],
                 lr=0.003,
                 class_names=None,
                 optimizer_name='Adam',
                 dropout_p=0.2,
                 non_linearity='relu',
                 criterion_name='NLLLoss',
                 model_type='classifier',
                 best_accuracy=0.,
                 best_accuracy_file ='best_accuracy.pth',
                 chkpoint_file ='chkpoint_file.pth',
                 device=None):
                              
        super().__init__(device=device)
        
        self.set_model_params(criterion_name,
                              optimizer_name,
                              lr,
                              dropout_p,
                              'FC',
                              best_accuracy,
                              best_accuracy_file,
                              chkpoint_file
                              )
        
        self.non_linearity = non_linearity
        
        '''
        我们将实际的网络作为一个连续的块存储在FC对象的模型属性中
        '''
        self.model = nn.Sequential()
        
        
        '''
        我们创建层组并将它们添加到顺序模型中。
        每一组由一个线性层、一个非线性层和一个概率dropout作为参数传递。inplace= True))
        '''
        if len(layers) > 0:
            
            self.model.add_module('fc1',nn.Linear(num_inputs,layers[0]))
            self.model.add_module('relu1',nn.ReLU())
            self.model.add_module('dropout1',nn.Dropout(p=dropout_p,inplace=True))

            for i in range(1,len(layers)):
                self.model.add_module('fc'+str(i+1),nn.Linear(layers[i-1],layers[i]))
                self.model.add_module('relu'+str(i+1),nn.ReLU())
                self.model.add_module('dropout'+str(i+1),nn.Dropout(p=dropout_p,inplace=True))
           
            self.model.add_module('out',nn.Linear(layers[-1],num_outputs))
                                                                    
        else:    
            '''
            如果没有任何隐藏层,我们只需要在序列模型中添加一层,其中包含输入和输出的数量。在这种情况下,我们不添加任何non-Linearity或dropout,因
            non-Linearity通常添加在隐藏层中。
            '''                                                        
            self.model.add_module('out',nn.Linear(num_inputs,num_outputs))
  • nn.Linear是一个Pytorch类,它接受输入和输出的数量,并创建一个带有内部前向函数的线性模型;
  • 注意我们命名输出层为‘out’,隐藏层为‘fcX’,X是层数量(1,2,…)。

三、分类损失函数

  • 我们可以将线性网络大致分为两类:回归和分类;
  • 虽然有很多用于分类的损失函数,但最常见的两种是:
    1. Negative Likehood Log Loss 或者 NLLLoss
    2. CrossEntropy Loss

NLLLoss

  • NLLLoss函数非常简单,它假设输入是一个概率值。它只是取-ve对每个输入的对数然后把它们加起来。(详情请点击这里:https://ljvmiranda921.github.io/notebook/2017/08/13/softmax-and-the-negative-log-likelihood/);
  • 使用NLLLoss前,需要将输出转换为概率值;
  • 最简单的方法是取输入的Softmax,即取每个输入的指数,然后除以指数的和(关于上面相同链接的更多信息)。在此操作之后,输出为概率(因为它们已经在0和1之间缩放或校准),然后输入到输出sum(-log§)的NLLLoss,其中p是每个概率的输出;
  • 然而,在Pytorch中,NLLLoss函数期望log已经被计算,并且它只是放置一个-ve符号并对输入进行求和。因此,我们需要在Softmax之后自己取log。Pytorch中有一个很方便的函数LogSoftmax,它就是这样做的。

Cross_entropy Loss
内置

class FC(Network):
    def __init__(self,num_inputs,
                 num_outputs,
                 layers=[],
                 lr=0.003,
                 class_names=None,
                 optimizer_name='Adam',
                 dropout_p=0.2,
                 non_linearity='relu',
                 criterion_name='NLLLoss',
                 model_type='classifier',
                 best_accuracy=0.,
                 best_accuracy_file ='best_accuracy.pth',
                 chkpoint_file ='chkpoint_file.pth',
                 device=None):
        
        super().__init__(device=device)
        
        self.set_model_params(criterion_name,
                              optimizer_name,
                              lr,
                              dropout_p,
                              'FC',
                              best_accuracy,
                              best_accuracy_file,
                              chkpoint_file
                              )
        
        self.non_linearity = non_linearity
        
        self.model = nn.Sequential()
        
        if len(layers) > 0:
            self.model.add_module('fc1',nn.Linear(num_inputs,layers[0]))
            self.model.add_module('relu1',nn.ReLU())
            self.model.add_module('dropout1',nn.Dropout(p=dropout_p,inplace=True))

            for i in range(1,len(layers)):
                self.model.add_module('fc'+str(i+1),nn.Linear(layers[i-1],layers[i]))
                self.model.add_module('relu'+str(i+1),nn.ReLU())
                self.model.add_module('dropout'+str(i+1),nn.Dropout(p=dropout_p,
                                                                    inplace=True))

            self.model.add_module('out',nn.Linear(layers[-1],num_outputs))
        else:
            self.model.add_module('out',nn.Linear(num_inputs,num_outputs))
        
        '''
        如果loss = NLLLoss则使用Logsoftmax
        '''
        if model_type.lower() == 'classifier' and criterion_name.lower() == 'nllloss':
            self.model.add_module('logsoftmax',nn.LogSoftmax(dim=1))
            
        '''
        我们将模型的属性保存在对象中,以备以后引用
        '''
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.layer_dims = layers
        
        '''
        如果passed,我们存储类名字典,否则我们创建一个简单的字典与每个类id转换成一个字符串id,例如1应转换为'1'作为类名。
        '''
        if class_names is not None:
            self.class_names = class_names
        else:
            self.class_names = {str(k):v for k,v in enumerate(list(range(num_outputs)))}

四、压平输入

  • 在我们把输入喂到FC网络之前,我们需要压平输入张量,这样每一行就是一个一维张量,我们有一批这样的行。换句话说,输入必须是二维的(按行按列),因为大多数用于机器学习都是表格数据(例如来自CSV文件)。这是线性层的一个要求,它期望它的数据是成批的单维张量(向量);
  • 为了实现这一点,我们只需要改变输入张量的视图(如果它们已经在二维中,那么视图中就不会有任何变化);
  • 为此,我们将用简单的一行程序函数来定义。这使得代码更具可读性,因为我们马上就会知道正在进行的是一个扁平化操作,而不是一个相当神秘的.view语句。
def flatten_tensor(x):
    return x.view(x.shape[0],-1)

class FC(Network):
    def __init__(self,num_inputs,
                 num_outputs,
                 layers=[],
                 lr=0.003,
                 class_names=None,
                 optimizer_name='Adam',
                 dropout_p=0.2,
                 non_linearity='relu',
                 criterion_name='NLLLoss',
                 model_type='classifier',
                 best_accuracy=0.,
                 best_accuracy_file ='best_accuracy.pth',
                 chkpoint_file ='chkpoint_file.pth',
                 device=None):
        
        
        super().__init__(device=device)
        
        self.set_model_params(criterion_name,
                              optimizer_name,
                              lr,
                              dropout_p,
                              'FC',
                              best_accuracy,
                              best_accuracy_file,
                              chkpoint_file
                              )
        
        
        self.non_linearity = non_linearity
        
        self.model = nn.Sequential()
        
        if len(layers) > 0:
            self.model.add_module('fc1',nn.Linear(num_inputs,layers[0]))
            self.model.add_module('relu1',nn.ReLU())
            self.model.add_module('dropout1',nn.Dropout(p=dropout_p,inplace=True))

            for i in range(1,len(layers)):
                self.model.add_module('fc'+str(i+1),nn.Linear(layers[i-1],layers[i]))
                self.model.add_module('relu'+str(i+1),nn.ReLU())
                self.model.add_module('dropout'+str(i+1),nn.Dropout(p=dropout_p,
                                                                    inplace=True))

            self.model.add_module('out',nn.Linear(layers[-1],num_outputs))
        else:
            self.model.add_module('out',nn.Linear(num_inputs,num_outputs))
        
        if model_type.lower() == 'classifier' and criterion_name.lower() == 'nllloss':
            self.model.add_module('logsoftmax',nn.LogSoftmax(dim=1))
        
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.layer_dims = layers
        
        if class_names is not None:
            self.class_names = class_names
        else:
            self.class_names = {str(k):v for k,v in enumerate(list(range(num_outputs)))}
    
    '''
    定义前向函数,在压平输入之后,它基本上调用我们模型的前向函数(本例为nn.Sequential)
    '''
    def forward(self,x):
        return self.model(flatten_tensor(x))

五、设置和获得Dropout

  • 我们添加了另外两个方便的方法,使我们能够随时更改dropout概率;
  • 当我们想要用不同的dropout概率值进行快速实验时,或者基于某些条件(如检测严重过拟合)进行训练时动态更改dropout时,这可能会很方便。
class FC(Network):
    ...
    '''
    在Pytorch中Dropout层是'torch.nn.modules.dropout.Dropout'.这可以在顺序模型中对每一个这样的层进行检查和相应的操作。 
    '''
    def _get_dropout(self):
        for layer in self.model:
            if type(layer) == torch.nn.modules.dropout.Dropout:
                return layer.p
            
    def _set_dropout(self,p=0.2):
        for layer in self.model:
            if type(layer) == torch.nn.modules.dropout.Dropout:
                print('FC: setting dropout prob to {:.3f}'.format(p))
                layer.p=p
  • 在这里,我们检查每一层的这种类型的模块,如果是真的,我们采取相应的设置和获取方法;
  • 请注意torch.nn.modules.dropout.Dropout有一个属性p,存储了Dropout概率。

为了正确地恢复我们的FC模型,我们需要保存另外四个属性。它们是numb_inputs,num_outputs, layers和class_names。因为这些都是特定于FC模型的,所以我们应该编写FC模型的get_model_param和set_model_param方法版本,这些方法在内部调用基类,并执行任何额外的东西。

class FC(Network):
    ...
    
    def set_model_params(self,
                         criterion_name,
                         optimizer_name,
                         lr,
                         dropout_p,
                         model_name,
                         model_type,
                         best_accuracy,
                         best_accuracy_file,
                         chkpoint_file,
                         num_inputs,
                         num_outputs,
                         layers,
                         class_names):
        
        '''
        我们调用父类的set_model_params方法,将所有必需的参数传递给它,然后将其他参数作为属性添加到对象中
        '''
        
        super(FC, self).set_model_params(criterion_name,
                              optimizer_name,
                              lr,
                              dropout_p,
                              model_name,
                              best_accuracy,
                              best_accuracy_file,
                              chkpoint_file
                              )
        
        self.num_inputs = num_inputs
        self.num_outputs = num_outputs
        self.layer_dims = layers
        self.model_type = model_type
        
        if class_names is not None:
            self.class_names = class_names
        else:
            self.class_names = {k:str(v) for k,v in enumerate(list(range(num_outputs)))}
        
    def get_model_params(self):
        '''
        我们调用父类的get_model_params方法并检索params的字典,然后将我们的模型特定属性添加到字典中
        '''
        params = super(FC, self).get_model_params()
        params['num_inputs'] = self.num_inputs
        params['num_outputs'] = self.num_outputs
        params['layers'] = self.layer_dims
        params['model_type'] = self.model_type
        params['class_names'] = self.class_names
        params['device'] = self.device
        return params

六、载入一个保存的Checkpoint

  • 创建load_chkpoint实用函数,它被赋予一个检查点文件来检索模型参数并重构适当的模型。因为我们现在只有一个模型类型(FC),所以我们将只对model_type进行检查,然后在创建它们时添加对迁移学习和任何其他类的支持;
  • 代码非常简单。它从chkpoint_file中获取params字典,并调用适当的构造函数,最后从从chkpoint_file中检索到的最佳精度模型的文件名加载到状态字典中。
def load_chkpoint(chkpoint_file):
        
    restored_data = torch.load(chkpoint_file)

    params = restored_data['params']
    print('load_chkpoint: best accuracy = {:.3f}'.format(params['best_accuracy']))  
    
    if params['model_type'].lower() == 'classifier':
        net = FC( num_inputs=params['num_inputs'],
                  num_outputs=params['num_outputs'],
                  layers=params['layers'],
                  device=params['device'],
                  criterion_name = params['criterion_name'],
                  optimizer_name = params['optimizer_name'],
                  model_name = params['model_name'],
                  lr = params['lr'],
                  dropout_p = params['dropout_p'],
                  best_accuracy = params['best_accuracy'],
                  best_accuracy_file = params['best_accuracy_file'],
                  chkpoint_file = params['chkpoint_file'],
                  class_names =  params['class_names']
          )

    net.load_state_dict(torch.load(params['best_accuracy_file']))

    net.to(params['device'])
    
    return net

这就完成了我们的FC类。现在我们应该在进行下一步之前对其进行测试。让我们在MNIST数据集上测试它。

七、测试

首先,我们应该计算MNIST数据集的平均值和std值

train_data = datasets.MNIST(root='data',download=True,
                            transform = transforms.transforms.ToTensor())
mean_,std_= calculate_img_stats(train_data)
mean_,std_

(tensor([0.0839, 0.2038, 0.1042]), tensor([0.2537, 0.3659, 0.2798]))

我们像之前一样使用计算出的平均值和std值创建转换,然后将它们应用到训练集和测试集,并将训练集拆分为训练和验证部分。请记住,如果将测试集作为参数给出,那么split_image_data函数只会将测试集转换为dataloader。

train_transform = transforms.Compose([transforms.RandomHorizontalFlip(),
                                      transforms.RandomRotation(10),
                                      transforms.ToTensor(),
                                      transforms.Normalize((0.1307,), (0.2998,))
                                     ])

test_transform = transforms.Compose([transforms.ToTensor(),
                                     transforms.Normalize((0.1307,), (0.2998,))
                                    ])

mnist图像都是灰度图像,只有一个通道,transforms.Normalize只需要一个数值,因此取三个值平均。

train_dataset = datasets.MNIST(root='data',download=False,train=True, transform = train_transform)
test_dataset = datasets.MNIST(root='data',download=False,train=False,transform = test_transform)
trainloader,validloader,testloader = split_image_data(train_dataset,test_dataset,batch_size=50)
len(trainloader),len(validloader),len(testloader)

(960, 240, 200)

  • 我们创建一个FC层,输入数=784,这是在压平图像尺寸(1 x 28 x 28)后得到的,输出数=10,因为我们有10个类(数字0到9);
  • 我们任意选择两个隐含层,每个层512个单位;
  • 我们将优化器设置为Ada Delta(下面将详细介绍);
  • 我们设置最好的准确性和检查点文件。
from mylib.fc import * 
net =  FC(num_inputs=784,
          num_outputs=10,
          layers=[512,512],
          optimizer_name='Adadelta',
          best_accuracy_file ='best_accuracy_mnist_fc_test.pth',
          chkpoint_file ='chkpoint_file_mnist_fc_test.pth')

setting optim Ada Delta

优化器选择

  • 优化算法有许多变种和形式。他们中的大多数试图通过改变学习率和其他相关参数来优化基本的梯度下降算法;
  • 对优化器的全面研究超出了本教程的范围。这里有详细的概述(http://ruder.io/optimizing-gradient-descent)
  • 最常用的几个(从上面的链接改编而来)之间的主要区别如下:
  1. Batch Gragient Descend(批梯度下降)是最简单的,在查看整个数据集后执行权重更新;
  2. SGD(随机梯度下降)是另一个极端,它为数据集中的每一项(训练示例)执行权重更新;
  3. Mini-batch GD(小批梯度下降)是SGD的一个变体,同时兼顾了两者的优点。它在每一小批数据之后更新权重。换句话说,纯SGD是批大小为1的迷你批。介于1和整个数据集之间的任何内容,我们称之为Mini-batch GD
  4. Momentum有助于在相关方向上加速SGD,并试图收敛到最小值时抑制过多的振荡;
  5. Adagrad根据各个参数之前的平方梯度,调整学习率以适应参数,并应用不同的学习率来更新不同的参数。Adagrad的主要优点是用户不必手动调整学习速率;
  6. Adadelta是Adagrad的扩展,它试图降低其侵略性的、单调递减的学习速率。Adadelta并没有积累所有过去的平方梯度,而是将积累过去梯度的窗口限制为某个固定大小的“w”;
  7. RMSprop是Geoff Hinton在他的Coursera课程第6e课上提出的一种尚未发表的自适应学习速率方法(http://www.cs.toronto.edu/~tijmen/csc321/slides/lecture_slides_lec6.pdf)。RMSprop和Adadelta都是大约在同一时间独立开发的,它们都是为了解决Adagrad的学习效率急剧下降的问题
  8. Adaptive Moment Estimation (Adam)(自适应矩估计)是计算每个参数的自适应学习率的另一种方法。除了存储像Adadelta和RMSprop这样的指数衰减的过去平方梯度平均值,Adam还保存了过去梯度的指数衰减平均值,类似于momentum。

在我的实验中,Adadelta通常在图像数据集上给出了最高的准确性,比Adam和SGD好得多,特别是在Cifar10上。

接下来,我们调用fit函数,来训练和验证dataloader,训练5个epoch,每个epoch输出300个批,同时每个epoch都执行validaton(记住validate_every = 1的默认值)。

net.fit(trainloader,validloader,epochs=5,print_every=300)

我们只在5个epochs就获得较高的精度,增加epoch还可以进一步提高精度。因此,让我们首先测试save和load chkpoint函数,然后继续进行另外10个epoch的训练。

net.save_chkpoint()

get_model_params: best accuracy = 95.467
get_model_params: chkpoint file = chkpoint_file_mnist_fc_test.pth
checkpoint created successfully in chkpoint_file_mnist_fc_test.pth

net2 = load_chkpoint('chkpoint_file_mnist_fc_test.pth')

load_chkpoint: best accuracy = 95.467
setting optim Ada Delta

载入保存的chkpoint加载到另一个变量中,以确保它是一个新模型。

net2.fit(trainloader,validloader,epochs=10,print_every=300)

updating best accuracy: previous best = 96.683 new best = 96.767

经过10个epoch后,我们在验证集上可以达到的最佳准确度为96.767。在测试评估方法之前,让我们再一次保存并恢复模型。

net2.save_chkpoint()

get_model_params: best accuracy = 96.767
get_model_params: chkpoint file = chkpoint_file_mnist_fc_test.pth
checkpoint created successfully in chkpoint_file_mnist_fc_test.pth

net3 = load_chkpoint('chkpoint_file_mnist_fc_test.pth')

load_chkpoint: best accuracy = 96.767
setting optim Ada Delta

net3.evaluate(testloader)

(96.94,
[(‘0’, 98.67346938775509),
(‘1’, 99.55947136563876),
(‘2’, 96.89922480620154),
(‘3’, 97.12871287128712),
(‘4’, 98.06517311608961),
(‘5’, 93.94618834080718),
(‘6’, 96.76409185803759),
(‘7’, 96.49805447470817),
(‘8’, 96.09856262833677),
(‘9’, 95.14370664023785)])

接下来我们还将测试我们的predict函数。要做到这一点,我们需要将testloader转换成Python迭代器,然后使用迭代器的“next”方法从它获取下一批数据。如果您不熟悉Python迭代器,请参阅任何好的教程,例如这里(https://www.datacamp.com/community/tutorials/pythoniterator-tutorial)以获得更多信息

iterator = iter(testloader)
imgs_,labels_ = next(iterator)
imgs_[0].shape,labels_[0].item()

(torch.Size([1, 28, 28]), 7)

上面我们可以看到我们的第一批的第一张图是1x28x28而其标签=7,我们可以转换为numpy,删除额外的维度并使用matplotlib pyplot来显示,让它只有28x28代替1x28x28

注意,要将一个Pytorch张量转换成numpy数组,只需使用Pytorch张量对象上可用的.numpy()方法

import matplotlib.pyplot as plt
%matplotlib inline
    
fig = plt.figure(figsize=(40,10))
ax = fig.add_subplot(2,10, 1, xticks=[], yticks=[])
ax.imshow(np.squeeze(imgs_[0].numpy()), cmap='gray')

5. Pytorch入门教程——创建一个派生自基类的全连接类_第1张图片
可以看到图像预测正确

net3.predict(imgs_[0])[1].item()

7

  • 我们的评估和预测方法似乎很有效,在90年代中期的测试集中,我们的准确率可以达到97%左右;
  • 这是相当不错的,因为我们只训练了15个epochs,不到3分钟,而且使用的是一个简单的完全连接的网络,没有任何花哨的CNN东西;
  • 此外,我们已经将代码重构为类、实用程序函数,并且还能够根据需要保存和恢复模型。

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