机器学习李宏毅2020hw11思路总结+函数解析

机器学习李宏毅2020hw11思路总结+函数解析

import os
import random
import numpy as np
import cv2
import glob
import torch
import torch.nn as nn
from torch.autograd import Variable
import torchvision
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
import matplotlib.pyplot as plt

首先:创建一个FaceDataset(Dataset)
一般在创建数据集时,需要重写init, getitem ,len函数

class FaceDataset(Dataset):
    def __init__(self,frames,transform):
        self.transform=transform#数据集变换
        self.frames=frames#这里存储的是所有图像的地址集合
        self.num_samples=len(self.frames)#初始化得到图片数量
    def __getitem__(self, idx):#获取某一帧
        frame=self.frames[idx]#获取某一帧
        img=cv2.imread(frame)#通过frame地址读取图片
        img=self.BGR2RGB(img) #torchvision.utils.save_image()use RGB
        img=self.transform(img)#获取某一帧
        return img

    def __len__(self):
        return self.num_samples

    def BGR2RGB(self,img):
        return cv2.cvtColor(img,cv2.COLOR_BGR2RGB)

glob模块的主要方法就是glob,该方法返回所有匹配的文件路径列表(list);
该方法需要一个参数用来指定匹配的路径字符串(字符串可以为绝对路径也可以为相对路径),其返回的文件名只包括当前目录里的文件名,不包括子文件夹里的文件。
例如:glob.glob(r’c:*.txt’)读取C盘下所有txt文件

def get_dataset(root):
    frames=glob.glob(os.path.join(root,'*'))#读取root/*,相当于读取root下所有文件,将他们的路径返回为一个列表
    transform=transforms.Compose(
        [transforms.ToPILImage(),transforms.Resize((64,64)),transforms.ToTensor(),
         transforms.Normalize(mean=[0.5]*3,std=[0.5]*3)])
    dataset=FaceDataset(frames,transform)
    return dataset

前面的(0.5,0.5,0.5) 是 R G B 三个通道上的均值, 后面(0.5, 0.5, 0.5)是三个通道的标准差,Normalize对每个通道执行以下操作:image =(图像-平均值)/ std在您的情况下,参数mean,std分别以0.5和0.5的形式传递。这将使图像在[-1,1]范围内归一化。例如,最小值0将转换为(0-0.5)/0.5=-1,最大值1将转换为1。
x = (x - mean(x))/std(x)

注意通道顺序是 R G B ,用过opencv的同学应该知道openCV读出来的图像是 BRG顺序。这两个tuple数据是用来对RGB 图像做归一化的,如其名称 Normalize 所示这里都取0.5只是一个近似的操作,实际上其均值和方差并不是这么多,但是就这个示例而言 影响可不计。

精确值是通过分别计算R,G,B三个通道的数据算出来的,
比如你有2张图片,都是100100大小的,那么两图片的像素点共有2100*100 = 20 000 个; 那么这两张图片的

  1. mean求法:
    mean_R: 这20000个像素点的R值加起来,除以像素点的总数,这里是20000;mean_G 和mean_B 两个通道 的计算方法 一样的。

  2. 标准差求法:
    首先标准差就是开了方的方差,所以其实就是求方差,方差公式就是我们数学上的那个求方差的公式:

也是3个通道分开算,
比如算R通道的, 这里X就为20000个像素点 各自的R值,再减去R均值,上面已经算好了;
然后平方;
然后20000个像素点相加,然后求平均除以20000,
得到R的方差,再开方得标准差。
为什么得到的RGB不是0到255?
(a )如果是imagenet数据集,那么ImageNet的数据在加载的时候就已经转换成了[0, 1].
(b) 应用了torchvision.transforms.ToTensor,其作用是将数据归一化到[0,1](是将数据除以255),transforms.ToTensor()会把HWC会变成C *H *W(拓展:格式为(h,w,c),像素顺序为RGB)

你加载的的是pytorch上的预训练模型,自己只是微调模型;
或者你用了常见的数据集比如VOC或者COCO之类的,但是用的是自己的网络结构,即pytorch上没有可选的预训练模型那么可以使用一个pytorch上给的通用的统计值:
mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225]),即

既然已经totensor到【0,1】,为什么还要到【-1,1】呢?
我的理解:totensor并没有使数据服从正态分布。数据如果分布在(0,255)或者(0,1)之间,可能实际的bias,就是神经网络的输入b会比较大,而模型初始化时b=0的,这样会导致神经网络收敛比较慢,经过Normalize后,可以加快模型的收敛速度。

same_seeds函数:
torch.manual_seed(seed) → torch._C.Generator(返回generator对象)
设置CPU生成随机数的种子,方便下次复现实验结果。
函数参数:seed (int) – CPU生成随机数的种子。取值范围为[-0x8000000000000000, 0xffffffffffffffff],十进制是[-9223372036854775808, 18446744073709551615],超出该范围将触发RuntimeError报错。
在这里插入图片描述
在这里插入图片描述
torch.backends.cudnn.benchmark标志位True or False
cuDNN是GPU加速库
在使用GPU的时候,PyTorch会默认使用cuDNN加速,但是,在使用 cuDNN 的时候,torch.backends.cudnn.benchmark模式是为False。

设置这个 flag 为True,我们就可以在 PyTorch 中对模型里的卷积层进行预先的优化,也就是在每一个卷积层中测试 cuDNN 提供的所有卷积实现算法,然后选择最快的那个。这样在模型启动的时候,只要额外多花一点点预处理时间,就可以较大幅度地减少训练时间。

如果我们的网络模型一直变的话,不能设置cudnn.benchmark=True。因为寻找最优卷积算法需要花费时间。
这段代码一般放在训练代码的开头,比如再设置使用GPU的同时,加在后面

如果在 PyTorch 程序中设置了 torch.backends.cudnn.deterministic=True,并且 cudnn.benchmark == False的话,那么就选那个默认的卷积算法

torch.backends.cudnn.deterministic 将这个 flag 置为 True 的话,每次返回的卷积算法将是确定的,即默认算法。如果配合上设置 Torch 的随机种子为固定值的话,应该可以保证每次运行网络的时候相同输入的输出是固定的。

cudnn.benchmark=True的时候,如果不停地改变输入形状,运行效率确实会很低,因为对于每个新遇到的卷积场景 PyTorch 都会去自动寻找最适合的卷积算法。

def same_seeds(seed):#固定输入的随机数
    torch.manual_seed(seed)
    if torch.cuda.is_available():#GPU可以使用的话
        torch.cuda.manual_seed(seed)
        torch.cuda.manual_seed_all(seed)#如果在用多GPU
    np.random.seed(seed)
    random.seed(seed)#当seed()没有参数时,每次生成的随机数是不一样的,
    # 而当seed()有参数时,每次生成的随机数是一样的,同时选择不同的参数生成的随机数也不一样
    torch.backends.cudnn.benchmark=False#使用默认卷积算法
    torch.backends.cudnn.deterministic=True#保证每次运行网络的时候相同输入的输出是固定的。

权重初始化对于训练神经网络至关重要,好的初始化权重可以有效的避免梯度消失等问题的发生。

recommend

def initialize_weights(m):
    if isinstance(m, nn.Conv2d):
        m.weight.data.normal_(0, 0.02)
        m.bias.data.zero_()
    elif isinstance(m, nn.Linear):
        m.weight.data.normal_(0, 0.02)
        m.bias.data.zero_()
def weights_init(m):#初始化网络权重(not recommended)
    classname=m.__class__.__name__# m作为一个形参,原则上可以传递很多的内容,为了实现多实参传递,每一个moudle要给出自己的name. 所以这句话就是返回m的类名。
    if classname.find('Conv')!=-1:#find()函数,实现查找classname中是否含有conv字符,没有返回-1;有返回0.        
    m.weight.data.normal_(0.0,0.02)#m.weight.data表示需要初始化的权重。 nn.init.normal_()表示随机初始化采用正态分布,均值为0,标准差为0.02.
    elif classname.find('BatchNorm')!=-1:
        m.weight.data.normal_(1.0,0.02)
        m.bias.data.fill_(0)#表示将偏差定义为常量0

核心内容:
1.逆卷积:一种上采样的方法

在这里插入图片描述
该函数是用来进行转置卷积的,它主要做了这几件事:
首先,对输入的feature map进行padding操作,得到新的feature map;
然后,随机初始化一定尺寸的卷积核;
最后,用随机初始化的一定尺寸的卷积核在新的feature map上进行卷积操作。

补充一下,卷积核确实是随机初始的,但是后续可以对卷积核进行单独的修改,比如使用双线性卷积核,这样的话卷积核的参数是固定的,不可以进行学习修改。
下面对上面的过程进行演示。

1.得到新的feature map

s=1
在这里插入图片描述
在这里插入图片描述
s>1
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用逆卷积的注意事项

从以上来看,我们要特别注意使用逆卷积时的参数设置。
在我们的DCGAN中:
在这里插入图片描述

对应于DCGAN,我们的设置如下: 除了从噪声z到第一个特征图(441024)的kernel设置不一样,为(4, 1, 0),其他都一样(4,2, 1),括号中依次表示:核大小,步长stride,padding大小
在这里插入图片描述
在调用nn.ConvTranspose2d的时候注意参数满足上述公式。其中H_out是原始feature map的尺寸,而H_in是输入图像的尺寸,也就是目标尺寸,想要通过上采样达到的尺寸。

再给一个实际的代码例子:
参数:

torch.nn.ConvTranspose2d(   in_channels,  #输入数据的通道数
							out_channels, #输出数据的通道数(就是我想让输出多少通道,就设置为多少)
							kernel_size,  #卷积核的尺寸(如(3,2),3与(3,3)等同)
							stride=1,     #卷积步长,就是卷积操作时每次移动的格子数
							padding=0,    #原图周围需要填充的格子行(列)数
							output_padding=0,   #输出特征图边缘需要填充的行(列)数,一般不设置
							groups=1,     #分组卷积的组数,一般默认设置为1,不用管
							bias=True     #卷积偏置,一般设置为False,True的话可以增加模型的泛化能力
							)

在这里插入图片描述

x = x.view(x.size(0), -1) 。

这句话一般出现在model类的forward函数中,具体位置一般都是在调用分类器之前。分类器是一个简单的nn.Linear()结构,输入输出都是维度为一的值,x = x.view(x.size(0), -1) 这句话的出现就是为了将前面多维度的tensor展平成一维。下面是个简单的例子,我将会根据例子来对该语句进行解析。

class NET(nn.Module):
    def __init__(self,batch_size):
        super(NET,self).__init__()
        self.conv = nn.Conv2d(outchannels=3,in_channels=64,kernel_size=3,stride=1)
        self.fc = nn.Linear(64*batch_size,10)
 
    def forward(self,x):
        x = self.conv(x)
        x = x.view(x.size(0), -1)  
        out = self.fc(x)

上面是个简单的网络结构,包含一个卷积层和一个分类层。forward()函数中,input首先经过卷积层,此时的输出x是包含batchsize维度为4的tensor,即(batchsize,channels,x,y),x.size(0)指batchsize的值。 x = x.view(x.size(0), -1)简化x = x.view(batchsize, -1)。
view()函数的功能根reshape类似,用来转换size大小。x = x.view(batchsize, -1)中batchsize指转换后有几行,而-1指在不告诉函数有多少列的情况下,根据原tensor数据和batchsize自动分配列数。

在这里插入图片描述
在这里插入图片描述

class Generator(nn.Module):#目标:将一串随机向量变成64*64图,主要通过逆卷积使得图像变大
    '''
    input:(N,in_dim)
    output(N,3,64,64)
    '''
    def __init__(self,in_dim,dim=64):
        super(Generator,self).__init__()#super函数就是先调用父类的程序,再加入自己的程序
        def dconv_bn_relu(in_dim,out_dim):
            return nn.Sequential(
                nn.ConvTranspose2d(in_dim,out_dim,5,stide=2,padding=2,output_padding=1,bias=False),#逆卷积,使图像变大
                nn.BatchNorm2d(out_dim),
                nn.ReLU())
                
        self.l1=nn.Sequential(
        nn.Linear(in_dim,dim*8*4*4,bias=False),
        nn.BatchNorm1d(dim*8*4*4),
        nn.ReLU())
        self.l2_5=nn.Sequential(
            dconv_bn_relu(dim*8,dim*4),
            dconv_bn_relu(dim*4,dim*2),
            dconv_bn_relu(dim*2,dim),
            nn.ConvTranspose2d(dim,3,5,2,padding=2,output_padding=1),
            nn.Tanh())
        self.apply(weights_init)
        #apply函数会递归地搜索网络内的所有module并把参数表示的函数应用到所有的module上。也就是说apply函数,会一层一层的去拜访Generator网络层。
        #这里面self.apply使得创建generator实例时就完成了初始化
    def forward(self,x):
        y=self.l1(x)
        y=y.view(y.size(0),-1,4,4)#第一维是batchsize,第二是channel,第三第四是图像尺寸
        y=self.l2_5(y)
        return y

一些函数层:
1.Con2d():

 def __init__(
        self,
        in_channels: int,
        out_channels: int,
        kernel_size: _size_2_t,
        stride: _size_2_t = 1,
        padding: _size_2_t = 0,
        dilation: _size_2_t = 1,
        groups: int = 1,
        bias: bool = True,
        padding_mode: str = 'zeros'  # TODO: refine this type
    ):

2.BatchNorm2d():
在卷积神经网络的卷积层之后总会添加BatchNorm2d进行数据的归一化处理,这使得数据在进行Relu之前不会因为数据过大而导致网络性能的不稳定,BatchNorm2d()函数数学原理如下:
在这里插入图片描述
BatchNorm2d()内部的参数如下:

1.num_features:一般输入参数为batch_sizenum_featuresheight*width,即为其中特征的数量

2.eps:分母中添加的一个值,目的是为了计算的稳定性,默认为:1e-5

3.momentum:一个用于运行过程中均值和方差的一个估计参数(我的理解是一个稳定系数,类似于SGD中的momentum的系数)

4.affine:当设为true时,会给定可以学习的系数矩阵gamma和beta

LeakyRelu():
它是一种专门设计用于解决Dead ReLU问题的激活函数:
在这里插入图片描述
Leaky ReLU函数的特点:

Leaky ReLU函数通过把x的非常小的线性分量给予负输入0.01x来调整负值的零梯度问题。
Leaky有助于扩大ReLU函数的范围,通常α的值为0.01左右。
Leaky ReLU的函数范围是负无穷到正无穷。
在这里插入图片描述

class Discriminator(nn.Module):
    '''
    input(N,3,64,64)
    output(N,)
    '''
    def __init__(self,in_dim,dim=64):
        super(Discriminator,self).__init__()
        def con_bn_lrelu(in_dim,out_dim):
            return nn.Sequential(
                nn.Conv2d(in_dim,out_dim,5,2,2),
                nn.BatchNorm2d(out_dim),
                nn.LeakyReLU(0.2))
        self.ls=nn.Sequential(
            nn.Conv2d(in_dim,dim,5,2,2),nn.LeakyReLU(0.2),
            con_bn_lrelu(dim,dim*2),
            con_bn_lrelu(dim*2,dim*4),
            con_bn_lrelu(dim*4,dim*8),
            nn.Conv2d(dim*8,1,4),
            nn.Sigmoid())
        self.apply(weights_init)
    def forward(self,x):
        y=self.ls(x)
        y=y.view(-1)
        return y
##训练:
#超参数
batch_size=64
z_dim=100
lr=1e-4
n_epoch=10
save_dir=os.path.join('/home/lizheng/Study/yolov5-5.0/hw4/crypko_data/','logs')
os.makedirs(save_dir,exist_ok=True)

#model
G=Generator(in_dim=z_dim).cuda()
D=Discriminator(3).cuda()
G.train()
D.train()

BCELoss:
二分类交叉熵损失
在这里插入图片描述
backward()怎么理解?
求导:

官方:如果需要计算导数,可以在Tensor上调用.backward()。

  1. 如果Tensor是一个标量(即它包含一个元素的数据),则不需要为backward()指定任何参数
  2. 但是如果它有更多的元素,则需要指定一个gradient参数,它是形状匹配的张量。
    backward(gradient=None, retain_variables=False)[source]
    当前Variable(理解成函数Y)对leaf variable(理解成变量X=[x1,x2,x3])求偏导。

计算图可以通过链式法则求导。如果Variable是 非标量(non-scalar)的(即是说Y中有不止一个y,即Y=[y1,y2,…]),且requires_grad=True。那么此函数需要指定gradient,它的形状应该和Variable的长度匹配(这个就很好理解了,gradient的长度体与Y的长度一直才能保存每一个yi的梯度值啊),里面保存了Variable的梯度。

此函数累积leaf variable的梯度。你可能需要在调用此函数之前将Variable的梯度置零。(梯度不置零的话为出现累加)

参数:
gradient (Tensor) – 其他函数对于此Variable的导数。仅当Variable不是标量的时候使用,类型和位形状应该和self.data一致。
(补充:这里说的时其他函数对Variable的导数!)
retain_variables (bool) – True, 计算梯度所必要的buffer在经历过一次backward过程后不会被释放。如果你想多次计算某个子图的梯度的时候,设置为True。在某些情况下,使用autograd.backward()效率更高。

矩阵求导相关:
在这里插入图片描述

detach()函数:
目的:神经网络的训练有时候可能希望保持一部分的网络参数不变,只对其中一部分的参数进行调整。或者训练部分分支网络,并不让其梯度对主网络的梯度造成影响.这时候我们就需要使用detach()函数来切断一些分支的反向传播
1 tensor.detach()
返回一个新的tensor,从当前计算图中分离下来。但是仍指向原变量的存放位置,不同之处只是requirse_grad为false.得到的这个tensor永远不需要计算梯度,不具有grad.

即使之后重新将它的requires_grad置为true,它也不会具有梯度grad.这样我们就会继续使用这个新的tensor进行计算,后面当我们进行反向传播时,到该调用detach()的tensor就会停止,不能再继续向前进行传播.

注意:
使用detach返回的tensor和原始的tensor共同一个内存,即一个修改另一个也会跟着改变。

#loss criterion
criterion=nn.BCELoss()
#optimizer
opt_D=torch.optim.Adam(D.parameters(),lr=lr,betas=(0.5,0.999))
opt_G=torch.optim.Adam(G.parameters(),lr=lr,betas=(0.5,0.999))

same_seeds(0)
#dataloader
dataset=get_dataset(os.path.join('/home/lizheng/Study/yolov5-5.0/hw4/crypko_data/','faces'))
dataloader=DataLoader(dataset,batch_size=batch_size,shuffle=True,num_workers=4)


z_sample=Variable(torch.randn(100,z_dim)).cuda()

for e,epoch in enumerate(range(n_epoch)):
    for i,data in enumerate(dataloader):
        imgs=data
        imgs=imgs.cuda()
        bs=imgs.size(0)

        '''Train D'''
        z=Variable(torch.randn(bs,z_dim)).cuda()
        r_imgs=Variable(imgs).cuda()
        f_imgs=G(z)
        # label
        r_label=torch.ones((bs)).cuda()
        f_label=torch.zeros((bs)).cuda()

        #dis
        r_logit=D(r_imgs.detach())
        f_logit=D(f_imgs.detach())

        #compute loss
        r_loss=criterion(r_logit,r_label)
        f_loss=criterion(f_logit,f_label)
        loss_D=(r_loss+f_loss)/2

        #update model
        D.zero_grad()
        loss_D.backward()
        opt_D.step()

        '''train G'''
        #leaf
        z=Variable(torch.randn(bs,z_dim)).cuda()
        f_imgs=G(z)

        #dis
        f_logit=D(f_imgs)

        #compute loss
        loss_G=criterion(f_logit,r_label)

        #update model
        G.zero_grad()
        loss_G.backward()
        opt_G.step()

        #log
        print(f'\rEpoch [{epoch+1}/{n_epoch}] {i+1}/{len(dataloader)}'
              f' Loss_D:{loss_D.item():.4f} Loss_G:{loss_G.item():.4f}',end='')

    G.eval()
    
    f_imgs_sample=(G(z_sample).data+1)/2.0
    filename=os.path.join('/home/lizheng/Study/yolov5-5.0/hw4/crypko_data/',f'Epoch_{epoch+1:03d}.jpg')
    torchvision.utils.save_image(f_imgs_sample,filename,nrow=10)
    print(f'| Save some samples to {filename}.')
    # show generated image
    grid_img=torchvision.utils.make_grid(f_imgs_sample.cpu(),nrow=10)
    plt.figure(figsize=(10,10))
    plt.imshow(grid_img.permute(1,2,0))
    plt.show()
    G.train()
    if (e+1)%5==0:
        torch.save(G.state_dict(),os.path.join('/home/lizheng/Study/yolov5-5.0/hw4/crypko_data/',f'dcgan_g.pth'))
        torch.save(D.state_dict(),os.path.join('/home/lizheng/Study/yolov5-5.0/hw4/crypko_data/',f'dcgan_d.pth'))

在这里插入图片描述

.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值