• 第三周作业:卷积神经网络(Part1)


    一、理论学习

    1、基本理论

      对于定义一个类,都要继承nn.Module。在类里,通常不可缺少的两个函数分别是__init__()和forward()。__init__()函数首先调用父类的__init__()函数,再定义所需要的层以及所需要的操作;forward()前向函数将输入进行__init__()里定义的操作。

    1 class MLP(nn.Module):
    2   def __init__(self):
    3     super().__init__()
    4     self.hidden = nn.Linear(20, 256)
    5     self.out = nn.Linear(256, 10)
    6 
    7   def forward(self, X):
    8     return self.out(F.relu(self.hidden(X)))

      定义一个顺序块,用循环设置了_modules的字典,构造了一个类似nn.Sequential的类。

     1 class MySequential(nn.Module):
     2   def __init__(self, *args):
     3     super().__init__()
     4     for block in args:
     5       self._modules[block] = block
     6 
     7   def forward(self, X):
     8     for block in self._modules.values():
     9       X = block(X)
    10     return X
    11 
    12 net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
    13 net(X)

      在__init__()函数里可以定义网络,最后应用nn.Sequential实现各种组合块的混合搭配。

     1 class NestMLP(nn.Module):
     2   def __init__(self):
     3     super().__init__()
     4     self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
     5                   nn.Linear(64, 32), nn.ReLU())
     6     self.linear = nn.Linear(32, 16)
     7 
     8   def forward(self, X):
     9     return self.linear(self.net(X))
    10 
    11 chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
    12 chimera(X)

      对于net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))来说,net[2]指的是nn.Sequential的最后一层,net[2].state_dict()指的是网络最后一层的参数,其中weight的大小是(8,1),bias的大小是一个标量。

      一次显示所有参数,分别是访问net[0]即第一层的参数和访问net所有层的参数。因此也可以用net.state_dict()['2.bias'].data中的2.bias来访问第三层网络中的bias的值。

    1 print(*[(name, param.shape) for name, param in net[0].named_parameters()])
    2 print(*[(name, param.shape) for name, param in net.named_parameters()])

      网络嵌套可以先设计net = nn.Sequential(),再用net.add_module()给网络里嵌套别的网络。

      可以设置函数对全连接层的weight和bias进行定义,再使用net.apply(定义的函数)将内置函数初始化。

      参数绑定:将net的第三层和第五层设置为shared,哪怕修改了其中一个的权重的值,修改的是shared这个层,所以另一个也会改变。

    1 shared = nn.Linear(8, 8)
    2 net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared,
    3                     nn.ReLU(), nn.Linear(8, 1))
    4 net(X)
    5 print(net[2].weight.data[0] == net[4].weight.data[0])
    6 net[2].weight.data[0, 0] = 100
    7 print(net[2].weight.data[0] == net[4].weight.data[0])

      

       自定义层和自定义网络本质上都是module的子类。

    1 class CenteredLayer(nn.Module):
    2     def __init__(self):
    3         super().__init__()
    4 
    5     def forward(self, X):
    6         return X - X.mean()
    7 
    8 layer = CenteredLayer()
    9 layer(torch.FloatTensor([1, 2, 3, 4, 5]))

      带参数的层要用到torch.nn.Parameter,该函数是继承自torch.Tensor的子类,其主要作用是作为nn.Module中的可训练参数使用。它与torch.Tensor的区别就是nn.Parameter会自动被认为是module的可训练参数,即加入到parameter()这个迭代器中去;而module中非nn.Parameter()的普通tensor是不在parameter中的。

     1 #带参数的层
     2 class MyLinear(nn.Module):
     3     def __init__(self, in_units, units):
     4         super().__init__()
     5         self.weight = nn.Parameter(torch.randn(in_units, units))
     6         self.bias = nn.Parameter(torch.randn(units,))
     7 
     8     def forward(self, X):
     9         linear = torch.matmul(X, self.weight.data) + self.bias.data
    10         return F.relu(linear)
    11 
    12 linear = MyLinear(5, 3)

      文件的读写还是比较简单的,张量列表读写是torch.save([x, y], 'x-files'),x2, y2 = torch.load('x-files');

    1 x = torch.arange(4)
    2 torch.save(x, 'x-file')#写数据
    3 
    4 x2 = torch.load('x-file')#读数据
    5 x2#tensor([0,1,2,3])
     1 #张量列表
     2 y = torch.zeros(4)
     3 torch.save([x, y], 'x-files')
     4 x2, y2 = torch.load('x-files')
     5 (x2, y2)
     6 
     7 #字典读取
     8 mydict = {'x': x, 'y': y}
     9 torch.save(mydict, 'mydict')
    10 mydict2 = torch.load('mydict')
    11 mydict2

    2、卷积

      对全连接层应用平移不变性和局部性得到卷积层,平移不变性指的是卷积核不随位置变化而变化,即共享权重;局部性指的是感受野对领域有效。卷积层是一个卷积核从左到右、从上至下遍历整个输入,卷积和二维交叉由于对称性,实际应用没有区别。

      

      对该图片的相关实现如下所示:

     1 def corr2d(X, K):  
     2     """计算二维互相关运算。"""
     3     h, w = K.shape
     4     Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
     5     for i in range(Y.shape[0]):
     6         for j in range(Y.shape[1]):
     7             Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
     8     return Y
     9   
    10 X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
    11 K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
    12 corr2d(X, K)

      设置一个二维卷积,可以直接采用nn.Conv2d设置输入层输出层和卷积核大小,以下是二维卷积的实现:conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad做了backward之后grad有了值,与自定义的学习率相乘不断更新权重。

     1 conv2d = nn.Conv2d(1, 1, kernel_size=(1, 2), bias=False)
     2 
     3 X = X.reshape((1, 1, 6, 8))
     4 Y = Y.reshape((1, 1, 6, 7))
     5 
     6 for i in range(10):
     7     Y_hat = conv2d(X)
     8     l = (Y_hat - Y)**2
     9     conv2d.zero_grad()
    10     l.sum().backward()
    11     conv2d.weight.data[:] -= 3e-2 * conv2d.weight.grad
    12     if (i + 1) % 2 == 0:
    13         print(f'batch {i+1}, loss {l.sum():.3f}')

    3、填充和步长

      填充指的是在周围添加padding,规范化的padding使得原先是h*w的输入填充后是(h+padding*2)*(w+padding*2),一般情况下:

      

      步幅指的是行和列的滑动步长

      

      下图中原尺寸为8*8,增加了两边为1的padding后尺寸为10*10,卷积核大小为3*3,所以最后输出的尺寸大小为10-3+1=8,尺寸不变。

      

       

       此处(image_shape-filter_shape+2*padding)/stride + 1,(8-3+2)/2+1=4

    1 conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
    2 comp_conv2d(conv2d, X).shape

       此处(8-3+2*0)/3+1=2,(8-5+2*1)/4+1=2

    1 conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
    2 comp_conv2d(conv2d, X).shape

      彩色图像通常有三个通道,对于多个通道数来说,每个通道都有一个卷积核,结果是所有通道卷积结果的和。

      将三个卷积核在0维度上压栈,可得到输出3,输入2(与X的通道数相同),高和宽分别是2

    1 def corr2d_multi_in_out(X, K):
    2     return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
    3 
    4 K = torch.stack((K, K + 1, K + 2), 0)
    5 K.shape#torch.size([3,2,2,2])

    4、池化层:输出通道=输入通道,缓解卷积层位置的敏感性

      最大池化:选择窗口里最大值;平均池化:选择窗口里平均值。

      

       首先获取池化层的高和宽,与卷积类似进行计算,对于mode为max和avg分别对应最大池化和平均池化。简便方法可以调用nn.MaxPool2d(窗口大小=number,padding=number,stride=number)进行池化,number不仅可以是个值,也可以设为任意大小的窗口。深度学习框架中的步幅与池化窗口的大小相同。

     1 def pool2d(X, pool_size, mode='max'):
     2     p_h, p_w = pool_size
     3     Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
     4     for i in range(Y.shape[0]):
     5         for j in range(Y.shape[1]):
     6             if mode == 'max':
     7                 Y[i, j] = X[i:i + p_h, j:j + p_w].max()
     8             elif mode == 'avg':
     9                 Y[i, j] = X[i:i + p_h, j:j + p_w].mean()
    10     return Y

     二、猫狗大战

      1、先去官网下载数据,对数据进行预处理:

       首先是解压数据集

    1 pip install pyunpack
    2 !pip install patool
    3 from pyunpack import Archive
    4 Archive('/content/drive/MyDrive/test_learn/cat_dog.rar').extractall('/content/drive/MyDrive/test_learn')

      创建文件夹,对给定图像的类别放在相应文件夹里

     1 !ls drive//MyDrive/test_learn/cat_dog/train
     2 %cd drive//MyDrive/test_learn/cat_dog/train
     3 !mkdir cat dog
     4 %cd ..
     5 %cd val
     6 !mkdir cat dog
     7 %cd ..
     8 !mv train/cat*.jpg train/cat/
     9 !mv train/dog*.jpg train/dog/
    10 !mv val/cat*.jpg val//cat/
    11 !mv val/dog*.jpg val/dog/

      创建dataset和dataloader,由于数据的尺寸不一致,需要做transform,构建好数据集

     1 cd_path = '/content/drive/MyDrive/test_learn/cat_dog'
     2 
    
    data_transform = TF.Compose([
            TF.ToTensor(),
            TF.RandomResizedCrop((224, 224), (0.8, 1.2)),
            TF.RandomHorizontalFlip(),
            
        ])
     9 
    10 #训练时可以打乱顺序增加多样性,测试时没有必要,所以shuffle=False
    11 dataset={x:datasets.ImageFolder(root=os.path.join(cd_path,x),transform=data_transform) for x in ['train','val']}
    12 trainloader=torch.utils.data.DataLoader(dataset=dataset['train'],batch_size=64,shuffle=True,num_workers=6)
    13 #testloader=torch.utils.data.DataLoader(dataset=dataset['test'],batch_size=64,shuffle=False,num_workers=6)
    14 valloader=torch.utils.data.DataLoader(dataset=dataset['val'],batch_size=64,shuffle=False,num_workers=6)

      创建LeNet模型,主要是依据李沐的课程源码

     1 device=torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
     2 class LeNet(nn.Module):
     3     def __init__(self):
     4         super().__init__()
     5         self.classifier = nn.Sequential(nn.Conv2d(3, 6, kernel_size=5), nn.Sigmoid(),
     6               nn.AvgPool2d(kernel_size=2, stride=2),
     7               nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
     8               nn.AvgPool2d(kernel_size=2, stride=2), nn.Flatten(),
     9               nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    10               nn.Linear(120, 84), nn.Sigmoid(), nn.Linear(84, 2))
    11     def forward(self, img):
    12         cls = self.classifier(img)
    13         return cls
    14 model = LeNet().to(device)

      损失函数使用交叉熵损失函数,优化器选择SGD随机梯度下降函数

    1 criterion = nn.CrossEntropyLoss()
    2 optimizer = torch.optim.SGD(model.parameters(), lr=0.1, weight_decay=0.01, momentum=0.9)

      预训练和测试代码用的是之前的代码,发现之前的错误是对数据的裁剪不对,不能因为LeNet的输入大小就裁剪到那样的大小。

      训练的数据跑的真慢,不过正确率还在慢慢提升,5个epoch已经从0.5提升到0.61了,继续训练吧。。。明天再来更新

  • 相关阅读:
    SSL原理
    花不是玫瑰的全部
    sqlserver2008数据类型说明
    js识别半角字符的正则表达式
    js全角字符转半角字符
    java 全角字符转半角
    MySQL数据目录结构
    git使用
    [iOS]MVVM-框架介绍
    圆形进度条
  • 原文地址:https://www.cnblogs.com/sun-or-moon/p/15306854.html
Copyright © 2020-2023  润新知