第三周作业:卷积神经网络(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了,继续训练吧。。。明天再来更新

原文地址:https://www.cnblogs.com/sun-or-moon/p/15306854.html