【机器学习实战】从零开始深度学习(通过GPU服务器进行深度学习)

2022-05-05 11:56:35 浏览数 (1)

注:如需查看算法直接看《三》

一·利用PyTorch开始深度学习
  • 0 写在前面
  • 1 神经网络的组成部分
    • 1.1 层
    • 1.2 非线性激活函数
  • 2 利用Pytorch构建深度学习框架
    • 2.1 数据预处理与特征工程
    • 2.2 如何决定要使用的层?
    • 2.3 损失函数
    • 2.4 优化器的选择
    • 2.5 评估机器学习模型
    • 2.6 模型的选择
  • 3 案例实践——猫狗图像分类
    • 3.1 数据集的建立(训练集 验证集)
    • 3.2 数据预处理(图片数据转换成PyTorch张量)
    • 3.3 批量加载PyTorch张量
    • 3.4 构建网络架构
    • 3.5 训练模型

二·多层全连接神经网络与MNIST手写数字分类

  • 0. 写在前面
  • 1. PyTorch基础
    • 1.1 张量(Tensor)
    • 1.2 变量(Variable)
    • 1.3 数据集(Dataset)
    • 1.4 模组(nn.Module)
    • 1.5 优化(torch.optim)
    • 1.6 模型保存与加载
  • 2. 案例实践:多层全连接神经网络实现 MNIST 手写数字分类
    • 2.1 定义简单三层全连接神经网络
    • 2.2 改进网络——增加激活函数
    • 2.3 再改进一下网络——添加批标准化
    • 2.4 训练网络
    • 2.5 三个神经网络模型的比较

三·卷积神经网络与计算机视觉
  • 0 写在前面
  • 1. 卷积神经网络
    • 1.1 三个重要的思想
    • 1.2 卷积神经网络的主要结构
      • 1.2.1 卷积层(Convolution Layer)
      • 1.2.2 池化层
      • 1.2.3 全连接层
      • 1.2.4 卷积神经网络的基本形式
  • 2. 构建CNN模型架构——熟悉PyTorch的卷积模块
    • 2.1 卷积层nn.Conv2d()
    • 2.2 池化层
    • 2.3 扁平化操作(view函数)
  • 3. 一些卷积神经网络的案例
  • 4. 案例应用一:使用CNN实现MNIST手写数字分类
  • 5. 案例应用二:使用CNN重新实现猫狗图片分类
  • 6. 迁移学习
    • 6.1 提取模型中的层结构
    • 6.2 提取参数及自定义初始化
  • 7. 案例应用三:再一次猫狗分类——迁移学习,从VGG16模型开始
    • 7.1 创建和探索VGG16模型
    • 7.2 微调VGG模型
    • 7.3 设置优化器和损失函数
    • 7.6 训练VGG16模型
    • 7.7 改进模型泛化能力的小技巧
  • 8. 案例应用四:计算预卷积特征——再改进一下我们对猫狗图片分类的训练框架

四·生成对抗网络——深度学习中的非监督学习问题

1. 生成模型(Generative Model)

  • 1.1 自编码器(Autoencoder)

  • 2. 生成对抗网络(Generative Adversarial Networks,GAN)
    • 2.1 生成对抗网络模型概述
    • 2.2 生成对抗网络的数学原理
      • 2.2.1 预备知识
      • 2.2.2 生成对抗网络的数学原理

      3 【案例一】利用PyTorch实现GAN【生成新的图片】

  • 3.1 模型构建
  • 3.2 损失函数和优化器
  • 3.3 训练模型
  • 3.4 采用不同的loss函数
  • 3.5 使用更复杂的卷积神经网络

《之一》

0 写在前面

0.1. 利用GPU加速深度学习   疫情期间没有办法用实验室的电脑来跑模型,用领取的腾讯云实例来弄刚刚好。发现如果没有GPU来跑的话真的是太慢了,非常推荐利用GPU加速深度学习的训练速度。     如果采用GPU的话,训练函数train_model(*)中数据的输入要改变一下,也就是需要将数据放在GPU上  

代码语言:javascript复制
inputs, labels = Variable(inputs).cuda(), Variable(labels).cuda()

  另外,使用GPU训练可能会导致GPU内存不足的情况(CUDA out of memory),有一个办法就是将batch_size的值调小(其中一个原因是GPU没有办法一下子处理打包过来的那么多图片)。batch_size调小之后面临的问题自然就是训练的速度变慢。   0.2. 监控你的显存占用情况   在训练的过程中可以随时监控自己的显存占用情况,输入下面这个命令就可以:  

代码语言:javascript复制
C:Program FilesNVIDIA CorporationNVSMI>nvidia-smi

  得到的结果如下:  

0.3. optimal.step()和scheduler.step() 如果有更新到PyTorch 1.1.0之后的版本,就会出现“Detected call of lr_scheduler.step() beforeoptimizer.step().”这样的错误。建议自行百度解决。

1 神经网络的组成部分

抽象出底层的运算并训练深度学习算法的过程如下图所示  

1.1 层

  层(Layer)是神经网络的基本组成,线性层是其中最重要的一种。在pytorch里面,线性层只需要一行代码就可以实现:  

代码语言:javascript复制
from torch.nn import Linear
myLayer = Linear(in_features = 10, out_features = 5, bias = True)

  上面这行代码的作用在于对输入数据进行一个线性变换 y=Wx b   其中,in_features是输入数据的维度,out_features是输出数据的维度,bias是“b”的值,默认为True;如果bias=False,则b=0。   例子:  

代码语言:javascript复制
import torch
from torch.nn import Linear
m = Linear(20, 30)
inp = torch.randn(128, 20)
out = m(inp)
print(out.size())
# output:torch.Size([128, 30])

  线性层Linear可以查询两个训练参数:W和b  

代码语言:javascript复制
# 查询weight
w = m.weight
# 查询bias
b = m.bias

1.2 非线性激活函数

  我们知道,神经网络每一层的的输出应该是 z=g(Wx b),其中g(*)为非线性的激活函数。Pytorch里面也提供了一些非线性的激活函数可以使用。  

f(x)=max(0, x)

线性整流函数(Rectified Linear Unit, ReLU),又称修正线性单元,是一种人工神经网络中常用的激活函数(activation function),通常指代以斜坡函数及其变种为代表的非线性函数。  

2 利用Pytorch构建深度学习框架

  在PyTorch中,所有的网络都实现为类,因此,神经网络MyFirstNetwork被创建为PyTorch类nn.Module的子类(从nn.Module继承),并实现init和forward方法。super方法用于将子类的参数传给父类。   在init方法中,初始化层,这里是构建了两个线性层。 在forward方法中,把数据传入init方法中初始化的层,并返回最终的输出。非线性层经常被forward函数直接调用,有些时候也可以在init方法中实现。  

2.1 数据预处理与特征工程

  • 特征提取 深度学习算法可以使用大量的数据自己学习出特征,不再使用手动的特征工程。

2.2 如何决定要使用的层?

2.3 损失函数

  定义好了网络架构,还剩下了最重要的两步——评估和优化。 评估神经网络通常会利用损失函数,一般来说,损失函数越小,模型越好。损失函数的梯度可以对模型的参数进行优化。   PyTorch提供了一些可用的损失函数:  

  在PyTorch里面使用这些损失函数,只要调用相应的函数就可以了:  

  这里以交叉熵Cross-entropy loss为例。

2.4 优化器的选择

2.5 评估机器学习模型

2.6 模型的选择

  要使模型能够工作,有三个选择至关重要:  

  • 最后一层的选择(激活函数的选择)和损失函数的选择 对于不同的机器学习问题,激活函数的选择和损失函数的选择可以概括成下表:
代码语言:javascript复制
torch.optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.1)

MultiStepLR:与StepLR类似,只不过步长是以列表的形式给出。  

代码语言:javascript复制
torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones=[30,80], gamma=0.1)

ExponentialLR:每一轮都将学习率乘上gamma值。  

代码语言:javascript复制
torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma=0.1)

ReduceLROnPlateau:这是最常用的学习率改变策略之一。当特定的度量指标,如训练损失、验证损失或者准确率不再变化时,学习率就会改变。通常会将学习率的原始值降低为原来的1/2~1/10。  

3 案例实践——猫狗图像分类

  光说不练假把式,我们下面就通过一个案例来巩固一下学习到的东西吧。   数据集:Dogs vs Cats。数据集有个文件夹,一个是train(训练数据集),一个是test(测试集)。在训练集中,有猫和狗的照片各12500张,每一张都通过文件名打标签:  

3.1 数据集的建立(训练集 验证集)

  首先是数据的分类,将数据集分为训练集(training set)和验证集(validation set)。  

代码语言:javascript复制
import os
import numpy as np

'''
数据集的分配
'''
path = 'train'
# 读取文件夹内的所有文件
files= os.listdir(path) #得到文件夹下的所有文件名称
print(f'Total no of images {len(files)}')
no_of_images = len(files)
# 创建验证集的随机文件索引
shuffle = np.random.permutation(no_of_images)
# 将训练数据集进行划分,2000个样本归入验证集,剩下的样本归入训练集
validation_index = shuffle[:2000]
train_index = shuffle[2000:]
# 创建验证集和训练集文件夹
os.mkdir(os.path.join(path, 'validation'))
os.mkdir(os.path.join(path, 'training'))
for t in ['training', 'validation']:
    for folder in ['dog', 'cat']:
        os.mkdir(os.path.join(path, t, folder))
# 将图片的一小部分复制到validation文件夹      
for i in validation_index:
    folder = files[i].split('/')[-1].split('.')[0]
    image = files[i].split('/')[-1]
    os.rename(os.path.join(path, files[i]), os.path.join(path, 'validation', folder, image))
# 将剩下的图片复制到training文件夹
for i in train_index:
    folder = files[i].split('/')[-1].split('.')[0]
    image = files[i].split('/')[-1]
    os.rename(os.path.join(path, files[i]), os.path.join(path, 'training', folder, image))

  上面的代码将数据集中的样本图片分成了训练集(23000个样本)和验证集(2000个样本),并在相应的目录底下创建了对应的类别文件夹(cat和dog)  

3.2 数据预处理(图片数据转换成PyTorch张量)

  数据预处理的目的是将图片加载成PyTorch张量。PyTorch的torchvision.datasets包提供了一个名为ImageFolder的工具类,可以用于加载图片以及相应的标签。  

  PyTorch在transforms模块中提供了很多工具函数,可以用于完成这些预处理的步骤。  

在train对象中,保留了所有图片以及相应的标签:  

代码语言:javascript复制
print(train.class_to_idx)
# out: {'cat': 0, 'dog': 1}
print(train.classes)
# out: ['cat', 'dog']

  同样,可以对得到的张量进行再次变形并将值反归一化,就可以得到相应的图片:  

代码语言:javascript复制
# 对张量进行可视化
def imshow(inp):
    inp = inp.numpy().transpose((1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    inp = std * inp   mean
    inp = np.clip(inp, 0, 1)
    plt.imshow(inp)

imshow(train[50][0])

  输出图片:  

3.3 批量加载PyTorch张量

下面的代码将前面的train数据集和valid数据集转换到数据加载器(data loader)中:  

代码语言:javascript复制
import torch 
# 按批加载Pytorch张量
train_data_gen = torch.utils.data.DataLoader(train, batch_size = 64, num_workers = 3)
valid_data_gen = torch.utils.data.DataLoader(valid, batch_size = 64, num_workers = 3)
dataset_sizes = {'train':len(train_data_gen.dataset),'valid':len(valid_data_gen.dataset)}
dataloaders = {'train':train_data_gen,'valid':valid_data_gen}

  这里的num_workers用于多线程处理的设置。如果在运行代码的时候碰到下面这个错误代码:  

代码语言:javascript复制
[Errno 32] Broken pipe

  那么可以参考这个原因: 把上面的train_data_gen和valid_data_gen改成下面的形式就可以了:  

代码语言:javascript复制
train_data_gen = torch.utils.data.DataLoader(train, batch_size = 64)
valid_data_gen = torch.utils.data.DataLoader(valid, batch_size = 64)

  关于DataLoader的我给出了一些参数的介绍:  

3.4 构建网络架构

  对于计算机视觉中的大多数案例,我们可以使用已有的不同架构来解决实际的问题。torchvision.models模块里面提供了很多现成的应用:  

代码语言:javascript复制
import torchvision.models as models
resnet18 = models.resnet18()
alexnet = models.alexnet()
vgg16 = models.vgg16()
squeezenet = models.squeezenet1_0()
densenet = models.densenet161()
inception = models.inception_v3()
googlenet = models.googlenet()
shufflenet = models.shufflenet_v2_x1_0()
mobilenet = models.mobilenet_v2()
resnext50_32x4d = models.resnext50_32x4d()
wide_resnet50_2 = models.wide_resnet50_2()
mnasnet = models.mnasnet1_0()

  在这里我们使用ResNet架构来解决。  

代码语言:javascript复制
# 构建网络架构
import torchvision.models as models
import torch.nn as nn
import torch.optim as op
model_ft = models.resnet18(pretrained = True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)

  在这里,model_fit = models.resnet18(pretrained = True)创建了算法的实例,实例是PyTorch层的集合。ResNet架构是一个层的集合.同时,可以在这里预下载好ResNet-18模型,模型放在“C:UsersAdministrator.torchmodels”文件夹下面。  

pretrained (bool) – If True, returns a model pre-trained on ImageNet

代码语言:javascript复制
# 检查是否可以在GPU上运行
if torch.cuda.is_available():
    model_ft = model_ft.cuda()

  接下来建立损失函数和基于SGD的优化器  

代码语言:javascript复制
# 损失函数和优化器
learning_rate = 0.001
criterion = nn.CrossEntropyLoss()
optimizer_ft = op.SGD(model_ft.parameters(), lr = learning_rate, momentum = 0.9)
exp_lr_scheduler = op.lr_scheduler.StepLR(optimizer_ft, step_size = 7, gamma = 0.1)

  StepLR函数帮助动态修改学习率。在scheduler的step_size表示scheduler.step()每调用step_size次,对应的学习率就会按照策略调整一次。  

3.5 训练模型

  我们首先来看一下训练模型的整体代码,再详细进行解读。下面的train_model函数获取模型输入,并通过多轮训练调优算法的权重,降低损失函数:  

代码语言:javascript复制
import time
from torch.autograd import Variable
# 训练模型
def train_model(model, criterion, optimizer, scheduler, num_epochs = 25):
    since = time.time()
    
    best_model_wts = model.state_dict()
    best_acc = 0.0
    
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs-1))
        print('-'*10)
        
        # 每轮都有训练和验证的阶段
        for phase in ['train', 'valid']:
            if phase == 'train':
                scheduler.step()
                model.train(True) # 模型设置为训练模式
            else:
                model.train(False) # 模型设置为评估模式
            
            running_loss = 0.0
            running_correct = 0
            
            # 在数据上迭代
            for data in dataloaders[phase]:
                # 获取输入
                inputs, labels = data
                # 封装成变量
                inputs, labels = Variable(inputs), Variable(labels)
                # 梯度参数清零
                optimizer.zero_grad()
                # 前向
                outputs = model(inputs)
                _, preds = torch.max(outputs.data, 1)
                loss = criterion(outputs, labels)
                # 只在训练阶段反向优化
                if phase == 'train':
                    loss.backward()
                    optimizer.step()
                # 统计
                running_loss  = loss.item()
                running_correct  = torch.sum(preds == labels.data)
            
            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_correct / dataset_sizes[phase]
            
            print('{} Loss: (:.4f) Acc: (:.4f)'.format(phase, epoch_loss, epoch_acc))
            
            # 深度复刻模型
            if phase == 'valid' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = model.state_dict()
                
        print()
    
    time_elapsed = time.time() - since
    print('Training Complete in {:.0f}m {:.0f}s'.format(time_elapsed//60, time_elapsed`))
    
    #加载最优权重
    model.load_state_dict(best_model_wts)
    return model

  上述函数主要实现以下四个功能:  

  • 传入图片并计算损失;
  • 在训练阶段反向传播,在验证/测试阶段不调整权重;
  • 每轮训练中的损失值跨批次累加
  • 存储最优模型并打印验证准确率。

  输入参数:  

  • model:构建好的神经网络框架
  • criterion:损失函数
  • optimizer:构建的优化器
  • scheduler:学习率的修改
  • num_epochs = 25:循环次数
代码语言:javascript复制
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=2)

  代码要点:  

《之二》

0. 写在前面

多层全连接神经网络是深度学习各种复杂神经网络的基础,同时可以借用多层全连接神经网络,对PyTorch的一些基础概念进行一些了解。   关于神经网络的一些知识

学习内容

参考资料

python

对于机器学习方向的同学来说掌握Python基础即可。Edx: Introduction to Computer Science and Programming Using Python,这是公开课,以Python作为入门语言,简洁、全面地讲述了计算机科学的内容,适合更进一步的学习。

线性代数

MIT的线性代数公开课这门课程建议在学习之前先对线性代数的知识体系有个基础的了解,老师讲课的思路比较跳跃,如果一点基础都没有的同学可能会觉得比较难。Coding the Matrix

机器学习基础

(1)吴恩达的机器学习入门课程(2)林轩田的机器学习基石和机器学习技法(3)Udacity 的机器学习纳米学位(4)周志华著的 《 机器学习 》(5)李航著的《统计学习方法 》 这本书真的特别好,对理解一些模型和理论有很大的帮助。(6) Pattern Recognition and Machine Learning

深度学习

(1)Udacity 的两个深度学习课程 (2)Coursera 的 Neural 入{etworks for Machine Learning (3)Stanford 的 cs231n (4)Stanford 的 cs224n

1. PyTorch基础

1.1 张量(Tensor)

  张量(Tensor)是PyTorch里面最基本的操作对象,可以和numpy的ndarray相互转换;它们的区别在于前者可以在GPU上运行,而后者只能在CPU上运行。可以通过下面这样的方式来定义一个三行两列给定元素的矩阵:  

代码语言:javascript复制
a = torch.Tensor(([2,3], [4,8], [7,9]))
print('a is {}'.format(a))
print('a size is {}'.format(a.size()))
'''
out:
a is tensor([[2., 3.],
        [4., 8.],
        [7., 9.]])
a size is torch.Size([3, 2])
'''

  可以像操作numpy一样用索引来改变张量的值:  

代码语言:javascript复制
a[0,1] = 100
print('a is changed to {}'.format(a))
'''
out:
a is changed to tensor([[  2., 100.],
        [  4.,   8.],
        [  7.,   9.]])
'''

  也可以实现Tensor与ndarray之间的转换:  

代码语言:javascript复制
numpy_a = a.numpy()
print('conver to numpy is n {}'.format(numpy_a))
'''
out:
conver to numpy is 
 [[  2. 100.]
 [  4.   8.]
 [  7.   9.]]
'''

import numpy as np
b = np.array([[2,3], [4,5]])
torch_b = torch.from_numpy(b)
print('from numpy to torch.Tensor is {}'.format(torch_b))
'''
out:
from numpy to torch.Tensor is tensor([[2, 3],
        [4, 5]], dtype=torch.int32)
'''

  torch.Tensor 默认的是 torch.FloatTensor 数据类型,也可以定义我们想要的数据类型:  

代码语言:javascript复制
a = torch.LongTensor(([2,3], [4,8], [7,9]))
print('a is {}'.format(a))
'''
out:
a is tensor([[2, 3],
        [4, 8],
        [7, 9]])
'''

  同样可以创建全为0的张量或者随机创建张量:  

代码语言:javascript复制
a = torch.zeros((3,2))
print('a is {}'.format(a))
'''
out:
a is tensor([[0., 0.],
        [0., 0.],
        [0., 0.]])
'''
a = torch.randn((3,2))
print('a is {}'.format(a))
'''
out:
a is tensor([[ 0.9284,  0.4900],
        [ 0.3578, -1.0652],
        [ 0.5255, -1.2100]])
'''

1.2 变量(Variable)

变量(Variable)是PyTorch里面一个比较特殊的概念,其与Tensor没有本质上的区别,想让一个Tensor变成Variable,只需要执行 torch.autograd.Variable(x) 就可以了。但不同的是,Variable 由三个重要的属性构成:data,grad,grad_fn。可以通过data取得Variable里面存储的Tensor值,grad表示的是这个Variable的反向传播梯度,grad_fn表示的是得到这个Variable的操作(加减乘除等)。  

代码语言:javascript复制
X = Variable(torch.Tensor ( [1) ) , requìres_grad=True) 

  构建一个Variable,需要指明一个参数requìres_grad,这个参数默认为False,当被设置为True时,表示需要对这个Variable求梯度。   1.3 数据集(Dataset)   数据读取和预处理是深度学习问题的基础性的一步。PyTorch提供了很多工具可以帮助实现:  

  • 继承和重写torch.utils.data.Dataset,例如:
代码语言:javascript复制
import torch.utils.data.dataset as dataset
import pandas as pd

class myDataset(dataset):
    def __init__(self, csv_file, txt_file, root_dir, other_file):
        self.csv_data = pd.read_csv(csv_file)
        with open(txt_file, 'r') as f:
            data_list = f.readlines()
        self.txt_data = data_list
        self.root_dir = root_dir
    
    def __len__(self):
        return len(self.csv_data)
    
    def __getitem__(self, idx):
        data = (self.csv_data(idx), self.txt_data[idx])
        return data
  • 通过 torch.utils.data.DataLoader 进行多线程读取数据
  • torchvision 这个包中还有有关于计算机视觉的数据读取类:ImageFolder ,主要功能是处理图片

1.4 模组(nn.Module)

  nn.Module是利用PyTorch建立神经网络的核心工具之一,神经网络中的层、损失函数都在这个包里面。所有模型的构建都是从nn.Module这个类继承来的。  

1.5 优化(torch.optim)

  优化是调整模型中参数更新的一种策略。一般来说,优化算法分为两大类:

  • 一阶优化算法 最常用的一阶优化算法就是梯度下降。
  • 二阶优化算法 二阶优化算法使用的是二阶导数,但是计算成本太高。torch.optim包提供了各种优化算法的实现,如随机梯度下降,以及添加动量的随机棉度下降,自适应学习率等。例如:
代码语言:javascript复制
optimizer = torch.optim.SGD(model.parameters() , lr=0.01 , momentum=0.9 ) 
# 将模型的参数作为需要更新的参数传入优化器,设定学习率是 0.01 ,动量是 0.9 的随机梯度下降
optimizer.zeros() # 在优化之前需要先将梯度归零
loss.backward() # 反向传播,自动求导得到每个参数的梯度
optimizer.step() # 以通过梯度做一步参数更新

1.6 模型保存与加载

  PyTorch里面提供了两种模型的保存方式,对应的也有两种模型的加载方式。 第一种是保存整个模型的结构信息和参数信息,保存的对象是模型 model;在网络较大的时候加载的时间比较长,同时存储空间也比较大;  

代码语言:javascript复制
# 保存
torch.save(model, './model/pth')
# 加载
load_model = torch.load('model.pth')

  第二种是保存模型的参数,保存的对象是模型的状态 model.state dict()  

代码语言:javascript复制
# 保存
torch.save(model.state_dict(), './model_state.pth')
# 加载
model.load_state_dic(torch.load('model_state.pth'))

2. 案例实践:多层全连接神经网络实现 MNIST 手写数字分类

2.1 定义简单三层全连接神经网络

代码语言:javascript复制
import torch.nn as nn

class simpleNet(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(simpleNet, self).__init__()
        self.layer1 = nn.Linear(in_dim, n_hidden_1)
        self.layer2 = nn.Linear(n_hidden_1, n_hidden_2)
        self.layer3 = nn.Linear(n_hidden_2, out_dim)
        
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

  上面这个就是三层全连接神经网络架构的定义,输入参数包括:输入维度,输入的维度、第一层网络的神经元个数、第二层网络神经元的个数,以及第三层网络(输出层)神经元的个数。 全连接神经网络如下图所示:

2.2 改进网络——增加激活函数

代码语言:javascript复制
class Activation_Net(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(Activation_Net, self).__init__()
        self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1),
                                    nn.ReLU(True))
        self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, n_hidden_2),
                                    nn.ReLU(True))
        self.layer3 = nn.Sequential(nn.Linear(n_hidden_2, out_dim))
    
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

  只需要在每层网络的输出部分添加激活函数就可以了,利用 nn.Sequential() 将网络的层组合到一起作为 self.layer。

2.3 再改进一下网络——添加批标准化

代码语言:javascript复制
class Batch_Net(nn.Module):
    def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
        super(Batch_Net, self).__init__()
        self.layer1 = nn.Sequential(nn.Linear(in_dim, n_hidden_1),
                                    nn.BatchNorm1d(n_hidden_1),
                                    nn.ReLU(True))
        self.layer2 = nn.Sequential(nn.Linear(n_hidden_1, n_hidden_2),
                                    nn.BatchNorm1d(n_hidden_2),
                                    nn.ReLU(True))
        self.layer3 = nn.Sequential(nn.Linear(n_hidden_2, out_dim))
    
    def forward(self, x):
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        return x

  同样使用 nn.Sequential( )将 nn. BatchNorm1d ()组合到网络层中。注意批标准化一般放在全连接层的后面、非线性层(激活函数)的前面。BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的。

2.4 训练网络

  首先导入需要的包,net是上面三个网络模型文件  

代码语言:javascript复制
import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

import net

  接着,设置模型的一些超参数:  

代码语言:javascript复制
# 设置超参数
batch_size = 64
learning_rate = 1e-2
num_epoches = 20

  定义预处理方式:  

代码语言:javascript复制
#数据预处理
data_tf = transforms.Compose([transforms.ToTensor(),
                              transforms.Normalize([0.5], [0.5])])

  torchvision.transforms提供了很多图片的预处理方法。这里的transforms.ToTensor()将图片转换成PyTorch中处理的Tensor对象,在转换的过程中PyTorch自动将图片标准化了;transforms.Normalize()需要传入两个参数,第一个参数市均值,第二个参数是方差,做的处理就是减均值,再除以方差。transforms.Compose()将各种预处理操作组合在一起。   注意这里由于是灰度图片,所以只有一个通道——transforms.Normalize([0.5], [0.5])。如果是彩色图片,则有三个通道,那么需要用transforms.Normalize([a,b,c], [d,e,f])来表示每个通道对应的均值和方差。   下面是下载数据集,读入数据。使用torch.utils.data.DataLoader 建立一个数据迭代器,传入数据集和 batch_size , 通过 shuffle=True 来表示每次迭代数据的时候是否将数据打乱。  

代码语言:javascript复制
#下载训练集-MNIST手写数字训练集
train_dataset = datasets.MNIST(root="./data", train=True, transform=data_tf, download=True)
test_dataset = datasets.MNIST(root="./data", train=False, transform=data_tf)
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)

  接下来,导入网络,定义损失函数和优化方法:  

代码语言:javascript复制
model = net.simpleNet(28*28, 300, 100, 10)
if torch.cuda.is_available():
    model = model.cuda()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr = learning_rate)

  这里首先构建了一个简单网络,网络的输出层有10个神经元(因为识别手写数字是个多分类问题,共有0-9这10个数字)。   下面就可以开始训练网络了:  

代码语言:javascript复制
# 训练模型
def train_model(model, criterion, optimizer, num_epoches):
    for epoch in range(num_epoches):
        print('epoch {}/{}'.format(epoch, num_epoches-1))
        print('-'*10)
        ## training------------------
#        model.train()
        train_loss = 0.0
        train_acc = 0.0
        # 获取数据输入和标签,封装成变量
        for data in train_loader: #获得一个batch样本
            img, label = data # 获得图片和标签
            img = img.view(img.size(0), -1) #将图片进行img的转换
            if torch.cuda.is_available():
                img = Variable(img).cuda()
                label = Variable(label).cuda()
            else:
                img, label = Variable(img), Variable(label)
            # 梯度参数清零
            optimizer.zero_grad()
            # 前向
            out = model(img) # 等价于 out = model.forward(img)
            loss = criterion(out, label)
            _, preds = torch.max(out.data, 1)
            # 反向传播
            loss.backward()
            optimizer.step()
            # 统计
            train_loss  = loss.item()
            train_correct = torch.sum(preds == label.data)
            train_acc  = train_correct
        print('Train Loss: {:.6f}, Acc: {:.6f}'.format(train_loss/(len(train_loader)), train_acc/(len(train_loader))))
        
        ## evaluation-------------
        model.eval()
        eval_loss = 0.0
        eval_acc = 0.0
        for data in test_loader:
            img, label = data
            img = img.view(img.size(0), -1)
            if torch.cuda.is_available():
                with torch.no_grad():
                    img = Variable(img).cuda()
                    label = Variable(label).cuda()
            else:
                img = Variable(img, volatile = True)
                label = Variable(label, volatile = True)
            out = model(img)
            loss = criterion(out, label)
            eval_loss  = loss.item()
            _, preds = torch.max(out.data, 1)
            num_correct = torch.sum(preds == label.data)
            eval_acc  = num_correct
        print('Test Loss:{:.6f}, Acc: {:.6f}'.format(eval_loss/(len(test_loader)), eval_acc/(len(test_loader))))

  这里的view()函数的功能与reshape类似,用来转换size大小。view()函数作用是将一个多行的Tensor,拼接成一行。案例可以看这里:PyTorch中view()函数

代码语言:javascript复制
import torch
 
a = torch.Tensor(2,3)
print(a)
# tensor([[0.0000, 0.0000, 0.0000],
#        [0.0000, 0.0000, 0.0000]])
 
print(a.view(1,-1))
# tensor([[0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000]])

  最后运行训练模型的函数,就可以得到训练的网络啦:  

代码语言:javascript复制
train_model(model, criterion, optimizer, num_epoches) 

2.5 三个神经网络模型的比较

  分别对前面创建的三个模型进行训练20轮,得到的准确率如下:  

《之三》

1. 卷积神经网络

  关于卷积神经网络的一些理论知识

1.1 三个重要的思想

  卷积神经网络有三个非常重要的思想,这些思想也是为什么CNN能够真正起作用的原因。   1.局部性 对于一个图片而言,要对图片进行分类,就要获取图片的特征;通常情况下,这些特征不是由整张图片决定的,而是由一些局部区域来决定的。比如下面这张图片,通过鸟喙就可以判断这张图片是一张关于鸟的图片,那么分类根据的特征就是鸟喙这个区域的特征。  

  2.相同性 对于不同的图片,如果它们具有相同的特征,即使这些特征出现在图片的不同位置,也可以用同样的检测模式去检测不同图片的相同特征。   3.不变性 对一张大图片进行采样(图像采样方法),图片的性质基本保持不变。  

1.2 卷积神经网络的主要结构

  上图:全连接神经网络   卷积神经网络和全连接神经网络是相似的,都是由一些神经元构成,这些神经元有需要学习的参数,通过网络输入最后输出结构,并通过损失函数来优化网络中的参数。   然而,如果采用全连接神经网络去处理图片,当处理比较大的彩色图片时i,神经网络的参数增加的特别快,效率特别低。   而卷积神经网络的处理过程,不同于一般的全连接神经网络,卷积神经网络的层结构是不同的(如下图)。  

卷积神经网络是一个3D容量的神经元,神经元是以三个维度来排列的:宽度、高度和深度。卷积神经网络中的主要层结构有三个:卷积层、池化层和全连接层,通过堆叠这些层结构形成一个完整的卷积神经网络。卷积神经网络将原始图片转化成最后的类别得分,其中一些层包含参数,一些层没有包含参数,比如卷积层和全连接层拥有参数,而激活层和池化层不含参数。这些参数通过梯度下降法来更新,最后使模型尽可能正确地识别出图片类别。  

1.2.1 卷积层(Convolution Layer)

  卷积层是卷积神经网络的核心,大多数计算都是在卷积层中进行的。   概述   卷积神经网络的参数是由一些可学习的滤波器集合构成,每个滤波器在空间上(宽度和高度)都比较小,但是深度和输入数据的深度保持一致。在前向传播的时候,让每个滤波器都在输入数据的宽度和高度上滑动(卷积),然后计算整个滤波器和输入数据任意一处的内积。   滤波器可以视为二维数字矩阵。卷积操作可以看成以下四个步骤:  

  1. 在图像的某个位置上覆盖滤波器;
  2. 将滤波器中的值与图像中的对应像素的值相乘;
  3. 把上面的乘积加起来,得到的和是输出图像中目标像素的值;
  4. 对图像的所有位置重复此操作。

  在卷积层中还有一个重要的概念——感受野(receptive field)。与神经元连接的空间大小叫做神经元的感受野,它的大小是人为设置的一个超参数。   在滑动滤波器的时候,需要设置步长限制,步长就是滤波器一次移动的像素格的个数。输出图片的尺寸可以由公式 (W-F 2P)/S 1 来计算。其中W表示输入数据的大小,F表示卷积层中神经元的感受野尺寸,S表示步长,P表示边界填充0的数量。步长的设置不能使上述公式计算的结果为非整数。   举个例子来说明,图片像素中的数字表示像素格的亮度(步长=1):  

  卷积有助于我们找到特定的局部图像特征(如边缘),用在后面的网络中。比如上面这个滤波器(索伯滤波器,Sobel filter)就可以对图片进行如下所示的处理,这个滤波器的作用就是输出图像中更亮的像素表示原始图像中存在的边缘。  

  我们可以看到,上面4X4的图片通过3X3的滤波器,就变成了2X2的图片。为了解决这个问题,可以再图片的像素矩阵周围填充0像素:  

  最后,总结一下卷积层的一些性质: (1)输入数据体的尺寸是W1×H1×D1。 (2)有4个超参数:滤波器数量K,滤波器空间尺寸F,滑动步长S,零填充的数量P。 (3)输出数据体的尺寸为W2×H2×D2,其中W2=(W1-F 2P)/S 1,H2=(H1-F 2P)/S 1,D2=K。 (4)由于参数共享,每个滤波器包含的权重数目为F×F×D1,卷积层一共有F×F×D1×K个权重和K个偏置。 (5)在输出体数据中,第d个深度切片(空间尺寸是W2×H2),用第d个滤波器和输入数据进行有效卷积运算的结果,再加上第d个偏置。   对于卷积神经网络的一些超参数,常见的设置是F=3,S=1,P=1。  

1.2.2 池化层

  通常会在卷积层之间周期性插入一个池化层,其作用是逐渐降低数据体的空间尺寸,这样就能够减少网络中参数的数量,减少计算资源耗费,同时也能有效地控制过拟合。   池化一般通过简单的最大值、最小值或平均值操作完成。以下是池大小为2的最大池层的示例。除了最大值池化外,还有一些其他的池化函数,比如平均池化,或者L2范数池化。在实际中证明,在卷积层之间引入最大池化的效果是最好的,而平均池化一般放在卷积神经网络的最后一层。

1.2.3 全连接层

1.2.4 卷积神经网络的基本形式

 

2. 构建CNN模型架构——熟悉PyTorch的卷积模块

  整体架构如下:  

代码语言:javascript复制
import torch.nn as nn
import torch.nn.functional as F

class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        layer1 = nn.Sequential()
        layer1.add_module('conv1', nn.Conv2d(3, 32, 3, 1, padding = 1))
        layer1.add_module('relu1', nn.ReLU(True))
        layer1.add_module('pool1', nn.MaxPool2d(2,2))
        self.conv1 = layer1
        
        layer2 = nn.Sequential()
        layer2.add_module('conv2', nn.Conv2d(32, 64, 3, 1, padding = 1))
        layer2.add_module('relu2', nn.ReLU(True))
        layer2.add_module('pool2', nn.MaxPool2d(2,2))
        self.conv2 = layer2
        
        layer3 = nn.Sequential()
        layer3.add_module('conv3', nn.Conv2d(64, 128, 3, 1, padding = 1))
        layer3.add_module('relu3', nn.ReLU(True))
        layer3.add_module('pool3', nn.MaxPool2d(2,2))
        self.conv3 = layer3
        
        layer4 = nn.Sequential()
        layer4.add_module('fc1', nn.Linear(2048, 512))
        layer4.add_module('fc_relu1', nn.ReLU(True))
        layer4.add_module('fc2', nn.Linear(512, 64))
        layer4.add_module('fc_relu2', nn.ReLU(True))
        layer4.add_module('fc3', nn.Linear(64, 10))
        self.fc = layer4
    
    def forward(self, x):
        conv1 = self.conv1(x)
        conv2 = self.conv2(conv1)
        conv3 = self.conv3(conv2)
        fc_input = conv3.view(conv3.size(0), -1)
        fc_out = self.fc(fc_input)
        return fc_out

2.1 卷积层nn.Conv2d()

代码语言:javascript复制
torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, padding_mode='zeros')

 

2.2 池化层

代码语言:javascript复制
torch.nn.MaxPool2d(kernel_size, stride=None, padding=0, dilation=1, return_indices=False, ceil_mode=False)

  kernel_size、stride、padding、dilation的参数含义和卷积层的一样。 其他参数(一般情况下下面这两个参数都不会设置): return_indices:表示是否返回最大值所处的下标。默认为False。 ceil_mode:表示使用一些方格代替层结构,默认为False。   PyTorch也提供了其他的池化层,在官方文档里面可以找到。  

2.3 扁平化操作(view函数)

  view()函数的功能是把原先tensor中的数据按照行优先的顺序排成一个一维的数据(这里应该是因为要求地址是连续存储的),然后按照参数组合成其他维度的tensor。  

3. 一些卷积神经网络的案例

4. 案例应用一:使用CNN实现MNIST手写数字分类

导入相应的包

代码语言:javascript复制
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets,transforms
from torch.autograd import Variable
import matplotlib.pyplot as plt

建立CNN模型架构

代码语言:javascript复制
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Sequential(nn.Conv2d(1, 10, kernel_size = 5),
                                   nn.ReLU(True),
                                   nn.MaxPool2d(2, 2))
        self.conv2 = nn.Sequential(nn.Conv2d(10, 20, kernel_size = 5),
                                   nn.Dropout2d(),
                                   nn.ReLU(True),
                                   nn.MaxPool2d(2, 2))
        self.fc1 = nn.Sequential(nn.Linear(320, 50),
                                 nn.ReLU(True),
                                 nn.Dropout2d())
        self.fc2 = nn.Sequential(nn.Linear(50, 10),
                                 nn.LogSoftmax(dim = 1))
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(-1, 320)
        x = self.fc1(x)
        x = self.fc2(x)
        return x

  上面建立的卷积神经网络含有2个卷积层,2个最大池化层,使用ReLU激活函数增加非线性,最后使用全连接层输出分类得分。   数据预处理

代码语言:javascript复制
#数据预处理  
transformation = transforms.Compose([transforms.ToTensor(),
                                     transforms.Normalize((0.1307,), (0.3081,))])

train_dataset = datasets.MNIST('data/',train=True, transform=transformation, download=True)
test_dataset = datasets.MNIST('data/',train=False, transform=transformation, download=True)

train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=32, shuffle=True)

is_cuda=False
if torch.cuda.is_available():
    is_cuda = True
model = Net()
if is_cuda:
    model.cuda()

设置优化器

代码语言:javascript复制
optimizer = optim.SGD(model.parameters(),lr=0.01)

训练模型

代码语言:javascript复制
def fit(epoch, model, data_loader, phase='training', volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , (data,target) in enumerate(data_loader):
        if is_cuda:
            data,target = data.cuda(),target.cuda()
        data , target = Variable(data,volatile),Variable(target)
        if phase == 'training':
            optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output,target)
        
        running_loss  = F.nll_loss(output,target,reduction = 'sum').item()
        preds = output.data.max(dim=1,keepdim=True)[1]
        running_correct  = preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()
    
    loss = running_loss/len(data_loader.dataset)
    accuracy = 100. * running_correct/len(data_loader.dataset)
    
    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
num_epoches = 20
for epoch in range(num_epoches):
    print('-'*10)
    print('epoch {}/{}'.format(epoch 1, num_epoches))
    epoch_loss, epoch_accuracy = fit(epoch, model, train_loader, phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch, model, test_loader, phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

模型训练结果:   分别对前面创建的三个模型进行训练20轮,得到的准确率如下:  

5. 案例应用二:使用CNN重新实现猫狗图片分类

导入相应的包

代码语言:javascript复制
import matplotlib.pyplot as plt
from torchvision import transforms
import torch
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
from torchvision.datasets import ImageFolder

建立CNN模型架构

代码语言:javascript复制
class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Sequential(nn.Conv2d(3, 10, kernel_size = 5),
                                   nn.ReLU(),
                                   nn.MaxPool2d(2, 2))
        self.conv2 = nn.Sequential(nn.Conv2d(10, 20, kernel_size = 5),
                                   nn.Dropout2d(),
                                   nn.ReLU(),
                                   nn.MaxPool2d(2, 2))
        self.fc1 = nn.Sequential(nn.Linear(56180, 500),
                                 nn.ReLU(),
                                 nn.Dropout2d())
        self.fc2 = nn.Sequential(nn.Linear(500, 50),
                                 nn.ReLU(),
                                 nn.Dropout2d())
        self.fc3 = nn.Sequential(nn.Linear(50, 2),
                                 nn.LogSoftmax(dim = 1))
    
    def forward(self, x):
        x = self.conv1(x)
        x = self.conv2(x)
        x = x.view(x.size(0), -1)
        x = self.fc1(x)
        x = self.fc2(x)
        x = self.fc3(x)
        return x

数据预处理

代码语言:javascript复制
#数据预处理  
simple_transform = transforms.Compose([transforms.Resize((224,224)),
                                       transforms.ToTensor(),
                                       transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                      ])
train = ImageFolder('cat_and_dog/train/training/',simple_transform)
valid = ImageFolder('cat_and_dog/train/validation/',simple_transform)
batch_size = 32
train_data_loader = torch.utils.data.DataLoader(train, batch_size=batch_size, num_workers=0, shuffle=True)
valid_data_loader = torch.utils.data.DataLoader(valid, batch_size=batch_size, num_workers=0, shuffle=True)

is_cuda=False
if torch.cuda.is_available():
    is_cuda = True

设置优化器

代码语言:javascript复制
model = Net()

if is_cuda:
    model.cuda()
optimizer = optim.SGD(model.parameters(),lr=0.01)

训练模型 训练模型部分的代码和案例应用一种的一样。 最后可以绘制训练损失、验证损失、训练准确率、验证准确率的图,便于分析模型性能。  

6. 迁移学习

  从案例二中可以看到,虽然每次迭代训练集的损失都在减少,但验证集的损失却没有很大的改善;在训练过程中,准确率也在增加,但在78%左右时几乎饱和。

6.1 提取模型中的层结构

  给定一个模型,如果只想提取模型中的某一层或者某几层,可以采用nn.Module里面提供的一些属性来解决。   children() 例子,提取上面构建好的网络的前两层:  

代码语言:javascript复制
model = SimpleCNN()
new_model = nn.Sequential(*list(model.children())[:2])
print(new_model)
'''
out:
Sequential(
  (0): Sequential(
    (conv1): Conv2d(3, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu1): ReLU(inplace=True)
    (pool1): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (1): Sequential(
    (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (relu2): ReLU(inplace=True)
    (pool2): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
)
'''

name_modules() 如果想提取出模型中的所有卷积层:  

6.2 提取参数及自定义初始化

named_parameters() 给出网络层的名字和参数的迭代器 parameters() 给出网络的全部参数的迭代器  

代码语言:javascript复制
model = SimpleCNN()
for param in model.named_parameters():
    print(param[0])
'''
out:
conv1.conv1.weight
conv1.conv1.bias
conv2.conv2.weight
conv2.conv2.bias
conv3.conv3.weight
conv3.conv3.bias
fc.fc1.weight
fc.fc1.bias
fc.fc2.weight
fc.fc2.bias
fc.fc3.weight
fc.fc3.bias
'''

  对于权重的初始化,只需要取出其中的data属性,对它进行所需要的处理即可:  

7. 案例应用三:再一次猫狗分类——迁移学习,从VGG16模型开始

7.1 创建和探索VGG16模型

  PyTorch在torchvision库中提供了一组已经训练好的模型,这些模型可以通过设置其参数pretrained=True,来下载为ImageNet分类问题调整好的权重。  

代码语言:javascript复制
from torchvision import models

vgg = models.vgg16(pretrained = True)

  我们把VGG16模型打印出来,可以看到:  

代码语言:javascript复制
VGG(
  (features): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU(inplace=True)
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU(inplace=True)
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU(inplace=True)
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU(inplace=True)
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU(inplace=True)
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU(inplace=True)
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU(inplace=True)
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU(inplace=True)
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU(inplace=True)
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU(inplace=True)
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (25): ReLU(inplace=True)
    (26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (27): ReLU(inplace=True)
    (28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (29): ReLU(inplace=True)
    (30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
  (classifier): Sequential(
    (0): Linear(in_features=25088, out_features=4096, bias=True)
    (1): ReLU(inplace=True)
    (2): Dropout(p=0.5, inplace=False)
    (3): Linear(in_features=4096, out_features=4096, bias=True)
    (4): ReLU(inplace=True)
    (5): Dropout(p=0.5, inplace=False)
    (6): Linear(in_features=4096, out_features=1000, bias=True)
  )
)

7.2 微调VGG模型

冻结层  

代码语言:javascript复制
# 冻结features 模型的所有参数
for param in vgg.features.parameters():
    param.requires_grad = False

改变VGG模型的输出特征  

代码语言:javascript复制
# 修改最后一层的输出
vgg.classifier[-1] = nn.Linear(in_features=4096, out_features=2, bias=True)

  我们再次打印VGG16模型架构,可以发现已经对VGG16模型进行了修改。  

7.3 设置优化器和损失函数

  由于我们只需要训练VGG16模型的classifier序列模型,因此我们只需要将classifier.parameters()传入优化器:  

代码语言:javascript复制
# 构建优化器和损失函数
optimizer = optim.SGD(vgg.classifier.parameters(), lr = 0.0001, momentum = 0.5)
cost = torch.nn.CrossEntropyLoss()

7.6 训练VGG16模型

代码语言:javascript复制
def fit(epoch, model, data_loader, phase='training', volatile=False):
    if phase == 'training':
        model.train()
    if phase == 'validation':
        model.eval()
        volatile=True
    running_loss = 0.0
    running_correct = 0
    for batch_idx , (data,target) in enumerate(data_loader):
        if is_cuda:
            data,target = data.cuda(),target.cuda()
        data, target = Variable(data,volatile),Variable(target)
        if phase == 'training':
            optimizer.zero_grad()
        output = model(data)
        preds = output.data.max(dim=1,keepdim=True)[1]
        loss = cost(output,target)
        
        running_loss  = loss.item()
        running_correct  = preds.eq(target.data.view_as(preds)).cpu().sum()
        if phase == 'training':
            loss.backward()
            optimizer.step()
    
    loss = running_loss/len(data_loader.dataset)
    accuracy = 100. * running_correct/len(data_loader.dataset)
    
    print(f'{phase} loss is {loss:{5}.{2}} and {phase} accuracy is {running_correct}/{len(data_loader.dataset)}{accuracy:{10}.{4}}')
    return loss,accuracy

train_losses , train_accuracy = [],[]
val_losses , val_accuracy = [],[]
num_epoches = 1
for epoch in range(num_epoches):
    print('-'*10)
    print('epoch {}/{}'.format(epoch 1, num_epoches))
    epoch_loss, epoch_accuracy = fit(epoch, vgg, train_data_loader, phase='training')
    val_epoch_loss , val_epoch_accuracy = fit(epoch, vgg, valid_data_loader, phase='validation')
    train_losses.append(epoch_loss)
    train_accuracy.append(epoch_accuracy)
    val_losses.append(val_epoch_loss)
    val_accuracy.append(val_epoch_accuracy)

  训练模型的代码和案例二的基本一致,做了一点小小的改动。   由于训练速度实在是太慢了,而且我的电脑的GPU内存不够,我只选了50张猫的图片和50张狗的图片作为数据集进行训练。  

  可以看到,使用预训练好的权重,模型的准确率有了很大的提升。  

7.7 改进模型泛化能力的小技巧

  我们可以应用一些技巧,例如数据增强和使用不同的dropout值来改进模型的泛化能力。   将dropout值从0.5改成0.2

    Dropout()中的参数p的含义是“一个神经元被归零(dropout)的概率”,其默认为0.5。将p=0.5改成p=0.2,降低了神经元被dropout的概率,模型的复杂度增加,参与训练的参数增加,模型的准确率也会改善。  

数据增强   改进模型泛化能力的另一个技巧是添加更多的数据或者进行数据增强。例如,可以随机地水平翻转图像或以小角度旋转图像来进行数据增强。torchvision包里面为数据增强提供了很多工具:  

  还有一些其他的工具,可以参考官方的文档。  

8. 案例应用四:计算预卷积特征——再改进一下我们对猫狗图片分类的训练框架

 

代码语言:javascript复制
vgg = models.vgg16(pretrained = True)
vgg = vgg.cuda()
features = vgg.features

def preconvfeat(dataset,model):
    conv_features = []
    labels_list = []
    for data in dataset:
        inputs,labels = data
        if is_cuda:
            inputs , labels = inputs.cuda(),labels.cuda()       
        inputs , labels = Variable(inputs),Variable(labels)
        output = model(inputs)
        conv_features.extend(output.data.cpu().numpy())
        labels_list.extend(labels.data.cpu().numpy())
    conv_features = np.concatenate([[feat] for feat in conv_features])
    
    return (conv_features,labels_list)

conv_feat_train,labels_train = preconvfeat(train_data_loader,features)
conv_feat_val,labels_val = preconvfeat(valid_data_loader,features)

  在获得了train和validation集的卷积特征后,可以创建我们自己的数据加载类,建立新的数据集。  

代码语言:javascript复制
class My_dataset(Dataset):
    def __init__(self,feat,labels):
        self.conv_feat = feat
        self.labels = labels
    
    def __len__(self):
        return len(self.conv_feat)
    
    def __getitem__(self,idx):
        return self.conv_feat[idx],self.labels[idx]

train_feat_dataset = My_dataset(conv_feat_train,labels_train)
val_feat_dataset = My_dataset(conv_feat_val,labels_val)

batch_size = 5
train_feat_loader = DataLoader(train_feat_dataset,batch_size=batch_size,shuffle=True)
val_feat_loader = DataLoader(val_feat_dataset,batch_size=batch_size,shuffle=True)

def data_gen(conv_feat,labels,batch_size=64,shuffle=True):
    labels = np.array(labels)
    if shuffle:
        index = np.random.permutation(len(conv_feat))
        conv_feat = conv_feat[index]
        labels = labels[index]
    for idx in range(0,len(conv_feat),batch_size):
        yield(conv_feat[idx:idx batch_size],labels[idx:idx batch_size])

train_batches = data_gen(conv_feat_train,labels_train)
val_batches = data_gen(conv_feat_val,labels_val)

《之四》

1. 生成模型(Generative Model)

 

1.1 自编码器(Autoencoder)

数据准备

  我们还是使用MNIST手写数据集  

构建模型

我们定义一个简单的4层网络作为编码器,中间使用ReLU激活函数;解码器同样使用简单的4层网络,前面三层采用ReLU激活函数,最后一层采用Tanh函数。  

代码语言:javascript复制
class autoencoder(nn.Module):
    def __init__(self):
        super(autoencoder, self).__init__()
        
        self.encoder = nn.Sequential(
            nn.Linear(28*28, 128),
            nn.ReLU(True),
            nn.Linear(128, 64),
            nn.ReLU(True),
            nn.Linear(64, 12),
            nn.ReLU(True),
            nn.Linear(12, 3) # 输出的 code 是 3 维,便于可视化
        )
        
        self.decoder = nn.Sequential(
            nn.Linear(3, 12),
            nn.ReLU(True),
            nn.Linear(12, 64),
            nn.ReLU(True),
            nn.Linear(64, 128),
            nn.ReLU(True),
            nn.Linear(128, 28*28),
            nn.Tanh()
        )

    def forward(self, x):
        encode = self.encoder(x)
        decode = self.decoder(encode)
        return encode, decode

开始训练

代码语言:javascript复制
net = autoencoder()
    
criterion = nn.MSELoss(size_average=False)
optimizer = torch.optim.Adam(net.parameters(), lr=1e-3)

def to_img(x):
    '''
    定义一个函数将最后的结果转换回图片
    '''
    x = 0.5 * (x   1.)
    x = x.clamp(0, 1)
    x = x.view(x.shape[0], 1, 28, 28)
    return x

# 开始训练自动编码器
for e in range(100):
    for im, _ in train_data:
        im = im.view(im.shape[0], -1)
        im = Variable(im)
        # 前向传播
        _, output = net(im)
        loss = criterion(output, im) / im.shape[0] # 平均
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    if (e 1) % 20 == 0: # 每 20 次,将生成的图片保存一下
        print('epoch: {}, Loss: {:.4f}'.format(e   1, loss.data[0]))
        pic = to_img(output.cpu().data)
        if not os.path.exists('./simple_autoencoder'):
            os.mkdir('./simple_autoencoder')
        save_image(pic, './simple_autoencoder/image_{}.png'.format(e   1))

2. 生成对抗网络(Generative Adversarial Networks,GAN)

  下面这张图表示生成对抗网络生成数据过程。

2.1 生成对抗网络模型概述

生成模型

  在生成对抗网络中,不再是将图片输入编码器得到隐含向量然后生成图片,而是随机初始化一个隐含向量,根据变分自编码器的特点,初始化一个正态分布的隐含向量,通过类似解码的过程,将它映射到一个更高的维度,最后生成一个与输入数据相似的数据,这就是假的图片。生成对抗网络会通过对抗过程来计算出这个损失函数。  

对抗模型

  对抗模型简单来说就是一个判断真假的判别器,相当于一个二分类问题,真的图片输出1,假的图片输出0。对抗模型不关心图片的分类(例如,是猫还是狗),只关心图片的真假(不管是猫的图片还是狗的图片,对抗模型都会认为它是真的图片)。  

生成对抗网络的训练

 

2.2 生成对抗网络的数学原理

2.2.1 预备知识

KL散度

  若P ( x ) 和Q ( x ) 是连续概率分布,则他们的KL散度定义为

  举个例子,假设有两个分布A 和B,它们出现0和1的概率分别为

  那么,这两个概率分布的KL散度为

我们先来看一下贝叶斯决策:  

一种可行的办法就是对先验概率P ( ω ) 和类条件概率P ( x ∣ ω ) 进行估计,然后再套用上述公式进行分类。   对先验概率P ( ω ) P(omega)P(ω)的估计是比较简单的,我们可以利用经验或者依据数据样本中各类别出现的频率进行估计。   而对类条件概率P ( x ∣ ω ) P(x|omega)P(x∣ω)的估计就难得多。原因包括:概率密度函数包含了一个随机变量的全部信息;样本数据可能不多;特征向量x的维度可能很大等等。一个解决的方法就是把概率密度p ( x ∣ ω i ) p(x|omega_i)p(x∣ωi​)的估计转化为参数估计问题(参数估计方法)。比如可以假设为正态分布,那么需要估计的参数就是σ sigmaσ和μ muμ。当然,概率密度分布p ( x ∣ ω i ) p(x|omega_i)p(x∣ωi​)非常重要。同时,采用这种参数估计的方法,要求数据满足“独立同分布”的假设,并且要保证样本量充足。   那么,假设我们已经确定了概率密度分布的模型,我们怎么确定哪些参数组合时最好的呢?这就是“极大似然估计”的目的。  

假设已知的样本集为D = { x 1 , x 2 , . . . , x n },需要估计的参数向量为θ thetaθ,则相对于{ x 1 , x 2 , . . . , x n } 的θ 的似然函数(likehood function)为其联合概率密度函数P ( D ∣ θ ),可以理解为在参数为θ 的条件下D = { x 1 , x 2 , . . . , x n } 发生的概率(数据样本服从独立同分布):  

  这样一来,要使似然函数最大(即在参数为θ 的条件下D = { x 1 , x 2 , . . . , x n } 发生的概率最大),就是求似然函数l ( θ ) 的极值θ ^,θ ^ 称作极大似然函数估计值  

  为了便于分析,定义似然函数的对数  

  求极值自然就是求导了。  

  1. 如果θ为标量,则在似然函数满足连续、可微的正则条件下,极大似然估计量是下面微分方程的解
  1. 如果未知参数有很多个,即θ thetaθ为向量,则可以对每个参数求偏导数,得到梯度算子

若似然函数满足连续可导的条件,则最大似然估计量就是如下方程的解。

2.2.2 生成对抗网络的数学原理

  我们再回到我们的生成对抗网络。   我们想要将一个随机高斯噪声z zz通过一个生成网络G 得到一个和真实数据分布P d a t a ( x ) 差不多的生成分布P G ( x ; θ ) ,其中参数θ是 网络的参数,并且我们希望θ 可以使P G ( x ; θ )尽可能和P d a t a ( x ) 接近。2   我们从真实数据分布P d a t a ( x ) 中取样m 个点,{ x 1 , x 2 , . . . x m } 。根据给定的参数θ可以计算概率P G ( x i ; θ ) ,那么生成m 个样本数据的似然就是  

    则极大似然函数估计值θ ^为  

也就是说,极大似然的另一个角度就是使KL散度极小。其中

里面的I II表示指示性函数,即 在生成对抗网络中,生成器(生成模型,Generator,G)的作用就是将服从正态分布的样本得到输出集合,通过计算输出集合中各类样本的频次,会得到一个概率分布,这个概率分布就是P G ( x ; θ )。这里的θ 就是生成器的神经网络模型的参数。换句话说,机器学习的任务就是不断地训练、学习参数θ ,使得P G ( x ; θ ) 与P d a t a ( x ; θ )之间的KL散度最小。而判别器(对抗模型,Discriminator,D)的作用就是衡量P G ( x ; θ ) 与P d a t a ( x ; θ ) 之间的差距。   我们知道,判别器的作用其实是一个二分类问题,那么我们可以定义判别器的目标函数V ( G , D ) 为  

  这个函数和逻辑回归的目标函数的形式是一样的。   我们的目标是找到一个合适的判别器D DD,使得max ⁡ D V ( G , D ) max_{D} V(G,D)maxD​V(G,D)。为什么呢?因为我们希望判别器能够判断哪个是真,哪个是假,自然是他们之间的差距越大越好。由  

  那么,

  在给定生成器G GG的前提下,P d a t a ( x ) P_{data}(x)Pdata​(x)与P G ( x ) P_G(x)PG​(x)都可以看成是常数,分别用a aa和b bb来表示,那么,等价于求解f ( D ) f(D)f(D)的极值,f ( D ) f(D)f(D)为  

  这样就求得了在给定G GG的前提下,使得V ( D ) V(D)V(D)取得最大值的D DD。将D DD带回到V ( D ) V(D)V(D),得到  

  其中J S D ( ∗ )为JS散度,定义为  

  这也就是为什么我们再训练生成对抗网络的时候,先训练判别器,我们在这个较优判别器的基础上,再训练生成器,我们希望得到生成器的参数θ G ​,能够和真实数据之间的概率分布的KL散度最小,即整体的优化目标是  

3 【案例一】利用PyTorch实现GAN【生成新的图片】

完整代码   在这个案例里面,我们同样是希望能够生成手写数字的图片。数据集是MNIST。  

3.1 模型构建

构建判别器——对抗模型   判别器的结构非常简单,由三层全连接神经网络构成,中间使用了斜率为0.2的LeakyReLU激活函数。 leakyrelu 是指 f(x) = max(α alphaα x, x)。  

代码语言:javascript复制
class discriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(784, 256),
                                 nn.LeakyReLU(0.2),
                                 nn.Linear(256, 256),
                                 nn.LeakyReLU(0.2),
                                 nn.Linear(256, 1)
                                 )
    
    def forward(self, x):
        x = self.net(x)
        return x

构建生成器——生成模型   生成网络的结构也很简单,最后一层的激活函数选择tanh()函数的目的是将图片规范化到-1~1之间。  

代码语言:javascript复制
class generator(nn.Module):
    def __init__(self, input_size):
        super(generator, self).__init__()
        self.gen =nn.Sequential(
                nn.Linear(input_size, 1024),
                nn.ReLU(True),
                nn.Linear(1024, 1024),
                nn.ReLU(True),
                nn.Linear(1024, 784),
                nn.Tanh()
                )
    
    def forward(self, x):
        x = self.gen(x)
        return x

3.2 损失函数和优化器

代码语言:javascript复制
bce_loss = nn.BCEWithLogitsLoss()

def discriminator_loss(logits_real, logits_fake): # 判别器的 loss
    size = logits_real.shape[0]
    true_labels = Variable(torch.ones(size, 1)).float().cuda()
    false_labels = Variable(torch.zeros(size, 1)).float().cuda()
    loss = bce_loss(logits_real, true_labels)   bce_loss(logits_fake, false_labels)
    return loss

def generator_loss(logits_fake): # 生成器的 loss  
    size = logits_fake.shape[0]
    true_labels = Variable(torch.ones(size, 1)).float().cuda()
    loss = bce_loss(logits_fake, true_labels)
    return loss

# 使用 adam 来进行训练,学习率是 3e-4, beta1 是 0.5, beta2 是 0.999
def get_optimizer(net):
    optimizer = torch.optim.Adam(net.parameters(), lr=3e-4, betas=(0.5, 0.999))
    return optimizer

3.3 训练模型

代码语言:javascript复制
def train_a_gan(D_net, G_net, D_optimizer, G_optimizer, discriminator_loss, generator_loss, show_every=250, 
                noise_size=NOISE_DIM, num_epochs=10):
    iter_count = 0
    for epoch in range(num_epochs):
        for x, _ in train_data:
            bs = x.shape[0]
            # 判别网络
            real_data = Variable(x).view(bs, -1).cuda() # 真实数据
            logits_real = D_net(real_data) # 判别网络得分
            
            sample_noise = (torch.rand(bs, noise_size) - 0.5) / 0.5 # -1 ~ 1 的均匀分布
            g_fake_seed = Variable(sample_noise).cuda()
            fake_images = G_net(g_fake_seed) # 生成的假的数据
            logits_fake = D_net(fake_images) # 判别网络得分

            d_total_error = discriminator_loss(logits_real, logits_fake) # 判别器的 loss
            D_optimizer.zero_grad()
            d_total_error.backward()
            D_optimizer.step() # 优化判别网络
            
            # 生成网络
            g_fake_seed = Variable(sample_noise).cuda()
            fake_images = G_net(g_fake_seed) # 生成的假的数据

            gen_logits_fake = D_net(fake_images)
            g_error = generator_loss(gen_logits_fake) # 生成网络的 loss
            G_optimizer.zero_grad()
            g_error.backward()
            G_optimizer.step() # 优化生成网络

            if (iter_count % show_every == 0):
                print('Iter: {}, D: {:.4}, G:{:.4}'.format(iter_count, d_total_error.item(), g_error.item()))
                imgs_numpy = deprocess_img(fake_images.data.cpu().numpy())
                show_images(imgs_numpy[0:16])
                plt.show()
                print()
            iter_count  = 1

  训练结果如下:  

可以看到,在iteration=4000的时候,隐约有了手写数字的样子。

3.4 采用不同的loss函数

Least Squares GAN 比最原始的 GANs 的 loss 更加稳定。我们可以定义一下loss函数:  

代码语言:javascript复制
def ls_discriminator_loss(scores_real, scores_fake):
    loss = 0.5 * ((scores_real - 1) ** 2).mean()   0.5 * (scores_fake ** 2).mean()
    return loss

def ls_generator_loss(scores_fake):
    loss = 0.5 * ((scores_fake - 1) ** 2).mean()
    return loss

 

3.5 使用更复杂的卷积神经网络

  前面只是使用了简单的全连接网络来构建生成器和判别器,我们同样可以使用更复杂的卷积神经网络来构建生成器和判别器。   判别器

代码语言:javascript复制
class build_dc_classifier(nn.Module):
    def __init__(self):
        super(build_dc_classifier, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 32, 5, 1),
            nn.LeakyReLU(0.01),
            nn.MaxPool2d(2, 2),
            nn.Conv2d(32, 64, 5, 1),
            nn.LeakyReLU(0.01),
            nn.MaxPool2d(2, 2)
        )
        self.fc = nn.Sequential(
            nn.Linear(1024, 1024),
            nn.LeakyReLU(0.01),
            nn.Linear(1024, 1)
        )
        
    def forward(self, x):
        x = self.conv(x)
        x = x.view(x.shape[0], -1)
        x = self.fc(x)
        return x

生成器

代码语言:javascript复制
class build_dc_generator(nn.Module): 
    def __init__(self, noise_dim=NOISE_DIM):
        super(build_dc_generator, self).__init__()
        self.fc = nn.Sequential(
            nn.Linear(noise_dim, 1024),
            nn.ReLU(True),
            nn.BatchNorm1d(1024),
            nn.Linear(1024, 7 * 7 * 128),
            nn.ReLU(True),
            nn.BatchNorm1d(7 * 7 * 128)
        )
        
        self.conv = nn.Sequential(
            nn.ConvTranspose2d(128, 64, 4, 2, padding=1),
            nn.ReLU(True),
            nn.BatchNorm2d(64),
            nn.ConvTranspose2d(64, 1, 4, 2, padding=1),
            nn.Tanh()
        )
        
    def forward(self, x):
        x = self.fc(x)
        x = x.view(x.shape[0], 128, 7, 7) # reshape 通道是 128,大小是 7x7
        x = self.conv(x)
        return x

  可以看到,采用卷积神经网络训练的效果要好很多很多。

累了,就写到这里吧哈哈哈.后续其他文章会更新新操作的.....五一快乐!

0 人点赞