文章目录

  • Deep Learning with Pytorch 中文简明笔记 第八章 Using convolutions to generalize
  • 主要内容
  • 1. 卷积
  • 2. 卷积的实现
  • 2.1 卷积的Padding
  • 2.2 卷积的简单理解
  • 2.3 更进一步:深度和池化(pooling)
  • 2.4 整合进神经网络
  • 3. 使用nn.Module来创建模型
  • 4. 训练网络
  • 4.1 评价指标
  • 4.2 保存和加载模型
  • 4.3 在GPU上进行训练
  • 5. 模型设计
  • 5.1 增加网络的宽度
  • 5.2 帮助模型收敛并且泛化:正则化(Regularization)


主要内容

  • 理解卷积
  • 建立卷积神经网络
  • 创建定制nn.Module的子类
  • module和API的不同
  • 设计神经网络的建议

1. 卷积

主要介绍了什么是卷积,卷积的运算操作

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积

卷积有一些特性:

  • 对相邻像素的局部操作
  • 平移不变性
  • 相对于其他方式有更少的参数

2. 卷积的实现

在Pytorch中,torch.nn模型提供了一维、二维和三维卷积,其中nn.Conv1d用于序列,nn.Conv2d用于图像,nn.Conv3d用于立方体或者视频。

对于CIFAR-10数据集,应当使用nn.Conv2d。在nn.Conv2d中需要传递的参数有,输入特征数目(通道数目),输出特征数目和kernel的大小。例如定义一个输入为3,输出为16,kernel为3×3的卷积层。

# In[11]: 
conv = nn.Conv2d(3, 16, kernel_size=3) 
conv

# Out[11]: 
Conv2d(3, 16, kernel_size=(3, 3), stride=(1, 1))

实际上有多少个卷积核呢,应该16×3个。相当于对于原图片的每一个通道,分别用16个不同卷积核去做卷积。也可以理解为原来的每一个channel,运算完之后编程16个channel。那么权重的数目即为16×3×3×3。bias的数目为16。

# In[12]: 
conv.weight.shape, conv.bias.shape

# Out[12]: 
(torch.Size([16, 3, 3, 3]), torch.Size([16]))

尝试对图片做一下卷积。

# In[13]: 
img, _ = cifar2[0] 
output = conv(img.unsqueeze(0)) 
img.unsqueeze(0).shape, output.shape

# Out[13]: 
(torch.Size([1, 3, 32, 32]), torch.Size([1, 16, 30, 30]))

# In[15]: 
plt.imshow(output[0, 0].detach(), cmap='gray') 
plt.show()

Deep learning pytorch出版社 with deep learning with pytorch中文版_python_02

2.1 卷积的Padding

由于卷积运算的特殊性,会导致图片变小。对于奇数大小的kernel,图片的高度和宽度会减小(kernel_size -1) / 2,在上面的例子中即减小1个像素。这里可以使用padding来解决这个问题。

# In[16]: 
conv = nn.Conv2d(3, 1, kernel_size=3, padding=1) 
output = conv(img.unsqueeze(0)) 
img.unsqueeze(0).shape, output.shape

# Out[16]: 
(torch.Size([1, 3, 32, 32]), torch.Size([1, 1, 32, 32]))

Deep learning pytorch出版社 with deep learning with pytorch中文版_2d_03

使用padding的原因有两个,一是避免卷积操作影响图片尺寸,二是对于一些网络结构,我们希望图片经过卷积前后的大小一致,以便进行两者的加减操作。

2.2 卷积的简单理解

对卷积核手动设置权重和偏置

# In[17]: 
with torch.no_grad(): 
	conv.bias.zero_()
with torch.no_grad():
	conv.weight.fill_(1.0 / 9.0)

# In[18]: 
output = conv(img.unsqueeze(0)) 
plt.imshow(output[0, 0].detach(), cmap='gray') 
plt.show()

简单来看,就是对卷积核所在范围内的图像取平均

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积_04

下面换另一个卷积核

# In[19]: 
conv = nn.Conv2d(3, 1, kernel_size=3, padding=1)
with torch.no_grad(): 
	conv.weight[:] = torch.tensor([[-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [-1.0, 0.0, 1.0]])
	conv.bias.zero_()

从卷积核来看,卷积核右侧的权重大,左侧的权重小,对于垂直的边缘响应应该更大,

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积_05

结果确实如此,实际上上面的卷积核就是垂直边缘检测的卷积核。而卷积神经网络,从一定程度上就是学习到不同的卷积核,来从图片中提取并组合各种特征,来达到最终的目的。

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积核_06

2.3 更进一步:深度和池化(pooling)

对于小的图片,小的卷积核就足够提取到一些特征。而当图片变大时,小的卷积核就显得不够了,但是使用更大的卷积核意味着更大的参数量,更臃肿的模型。有没有什么办法可以在大图像时依旧使用小卷积核呢?解决问题的方法就是池化。

池化是的基本思想就是下采样,将周围几个像素合并为一个像素。大致可分为:

  • 平均值池化(average pooling),周围像素取平均值作为新像素。
  • 最大值池化(max pooling),周围像素取最大值作为新像素。

此外,可以以采用有间隔的卷积核。这行方法都会损失一些信息,但是更重要是保留图片最重要信息,供下层卷积层进一步提取高级特征。图中展示了最大值池化的详细步骤。

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积核_07

# In[21]: 
pool = nn.MaxPool2d(2) 
output = pool(img.unsqueeze(0))
img.unsqueeze(0).shape, output.shape

# Out[21]: 
(torch.Size([1, 3, 32, 32]), torch.Size([1, 3, 16, 16]))

我们可以将卷积和池化组合到一起,组成网络。

Deep learning pytorch出版社 with deep learning with pytorch中文版_卷积_08

这里涉及到感受野(receptive field)的概念,感受野即当前层中的一个像素,可以追溯到原始输入多少像素。这样说有点抽象,用一个示例来说明。(图片来自深度神经网络中的感受野

Deep learning pytorch出版社 with deep learning with pytorch中文版_python_09

在原始图像中,使用3×3的卷积核,之后再使用2×2的卷积核,最后得到Conv2。对于Conv1的一个像素,来自于原始图像3×3个单元,故感受野为3×3。对于Conv2的一个像素,来自于Conv1的4个单元,来自原始图像5×5个单元,所以感受野为5×5.

2.4 整合进神经网络

下面将Conv和pooling放在一起,建立神经网络。

# In[22]: 
model = nn.Sequential( 
	nn.Conv2d(3, 16, kernel_size=3, padding=1),
	nn.Tanh(),
	nn.MaxPool2d(2), 
	nn.Conv2d(16, 8, kernel_size=3, padding=1), 
	nn.Tanh(),
	nn.MaxPool2d(2), 
	# ... )

现在要解决分类问题,所以最后也是要分类,输出类别的概率值,考虑最后加入全连接层。

# In[23]: 
model = nn.Sequential( 
	nn.Conv2d(3, 16, kernel_size=3, padding=1), 
	nn.Tanh(),
	nn.MaxPool2d(2), 
	nn.Conv2d(16, 8, kernel_size=3, padding=1), 
	nn.Tanh(),
	nn.MaxPool2d(2),
	# ...
	nn.view(-1, 8*8*8), # reshape
	nn.Linear(8*8*8, 32), 
	nn.Tanh(), 
	nn.Linear(32, 2))

注意,在接入全连接层之前要先reshape。

3. 使用nn.Module来创建模型

使用nn.Sequential在创建复杂模型的时候不是很方便,尤其是当需要定制一些模块的时候。此时可以选择nn.Module来创建模型,并且只需要设计前向传播过程,而梯度求解由Pytorch的自动求导机帮我们实现。

具体而言,需要重写nn.Module的__init__()构造函数和forward函数

# In[26]: 
class Net(nn.Module): 
	def __init__(self): 
		super().__init__() 
		self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) 
		self.act1 = nn.Tanh() 
		self.pool1 = nn.MaxPool2d(2) 
		self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1) 
		self.act2 = nn.Tanh() self.pool2 = nn.MaxPool2d(2) 
		self.fc1 = nn.Linear(8 * 8 * 8, 32) 
		self.act3 = nn.Tanh() 
		self.fc2 = nn.Linear(32, 2)
	
	def forward(self, x): 
		out = self.pool1(self.act1(self.conv1(x))) 
		out = self.pool2(self.act2(self.conv2(out))) 
		out = out.view(-1,8*8* 8) 
		out = self.act3(self.fc1(out)) 
		out = self.fc2(out) 
		return out

注意在构造函数中,需要先调用父类对象即nn.Module的构造函数。

对于分类问题,最终我们期望得到的是属于每个类别的概率,但是可以发现在建立模型的第一层,不仅没有对输入图片进行归减的特征提取,反而增加了很多,这种把低维映射高维的技巧称为核方法(kernel trick),此处可以理解为一种近似。

Pytorch还提供了一些功能性函数的API,此类函数没有待学习的参数,所以它的输出仅和输入有关。我们用functional的pooling来代替上面的pooling。

# In[28]: 
import torch.nn.functional as F
class Net(nn.Module): 
	def __init__(self): 
		super().__init__() 
		self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) 
		self.conv2 = nn.Conv2d(16, 8, kernel_size=3, padding=1) 
		self.fc1 = nn.Linear(8 * 8 * 8, 32) 
		self.fc2 = nn.Linear(32, 2)
			
	def forward(self, x): 
		out = F.max_pool2d(torch.tanh(self.conv1(x)), 2) 
		out = F.max_pool2d(torch.tanh(self.conv2(out)), 2) 
		out = out.view(-1,8*8* 8) 
		out = torch.tanh(self.fc1(out)) 
		out = self.fc2(out) 
		return out

可以看到,主要差别是不需要在模型中定义池化层,而是直接在前向传播的过程中使用池化操作,可以说更加方便了。

4. 训练网络

训练大概分为几步:

  1. 输入数据到模型(前向传播)
  2. 计算损失(前向传播)
  3. 将旧的梯度置0
  4. 调用backward(),计算梯度(反向传播)
  5. 优化器根据梯度更新参数

根据这个步骤,编写代码

# In[30]: 
import datetime
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader):
	for epoch in range(1, n_epochs + 1):
		loss_train = 0.0
		for imgs, labels in train_loader:
			outputs = model(imgs)
			loss = loss_fn(outputs, labels)
			optimizer.zero_grad()
			loss.backward()
			optimizer.step()
			loss_train += loss.item()
			
		if epoch == 1 or epoch % 10 == 0:
			print('{} Epoch {}, Training loss {}'.format(datetime.datetime.now(), epoch, loss_train / len(train_loader)))
# In[31]: 
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True)
model = Net()
optimizer = optim.SGD(model.parameters(), lr=1e-2)
loss_fn = nn.CrossEntropyLoss()
training_loop( 
	n_epochs = 100, 
	optimizer = optimizer, 
	model = model, 
	loss_fn = loss_fn, 
	train_loader = train_loader,
)

4.1 评价指标

下面加入验证的步骤,同时使用准确率作为评价指标。

# In[32]: 
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=False)
val_loader = torch.utils.data.DataLoader(cifar2_val, batch_size=64, shuffle=False)

def validate(model, train_loader, val_loader): 
	for name, loader in [("train", train_loader), ("val", val_loader)]: 
		correct = 0 total = 0

		with torch.no_grad(): # 因为不需要更新参数,所以不计算梯度
			for imgs, labels in loader: 
				outputs = model(imgs) 
				_, predicted = torch.max(outputs, dim=1) 
				total += labels.shape[0] 
				correct += int((predicted == labels).sum())

			print("Accuracy {}: {:.2f}".format(name , correct / total))

validate(model, train_loader, val_loader)

4.2 保存和加载模型

只保存模型参数,不保存模型结构

# In[33]: 
torch.save(model.state_dict(), data_path + 'birds_vs_airplanes.pt')

# In[34]:
loaded_model = Net() loaded_model.load_state_dict(torch.load(data_path + 'birds_vs_airplanes.pt'))

4.3 在GPU上进行训练

首先可以通过torch.cuda.is_avilable()来检测是否可以使用gpu

# In[35]: 
device = (torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu'))
print(f"Training on device {device}.")

之后结合定义的device变量,使用to方法的device属性,来实现将数据迁移至GPU上。

# In[36]: 
import datetime
def training_loop(n_epochs, optimizer, model, loss_fn, train_loader): 
	for epoch in range(1, n_epochs + 1): 
		loss_train = 0.0

		for imgs, labels in train_loader: 
			imgs = imgs.to(device=device) 
			labels = labels.to(device=device) 
			outputs = model(imgs) 
			loss = loss_fn(outputs, labels)

			optimizer.zero_grad() 
			loss.backward()
			optimizer.step()
		
			loss_train += loss.item()

	if epoch == 1 or epoch % 10 == 0: 
		print('{} Epoch {}, Training loss {}'.format( datetime.datetime.now(), epoch, loss_train / len(train_loader)))

之后,再将模型定义在GPU上。

# In[37]: 
train_loader = torch.utils.data.DataLoader(cifar2, batch_size=64, shuffle=True)
model = Net().to(device=device) 
optimizer = optim.SGD(model.parameters(), lr=1e-2) 
loss_fn = nn.CrossEntropyLoss()
training_loop( n_epochs = 100, optimizer = optimizer, model = model, loss_fn = loss_fn, train_loader = train_loader,

模型模型参数,或者模型和数据必须在同一位置,Pytorch才可以正常训练和预测。在加载模型的时候,可以使用map_location属性,手动指定模型加载的位置。

# In[39]: 
loaded_model = Net().to(device=device) 
loaded_model.load_state_dict(torch.load(data_path + 'birds_vs_airplanes.pt', map_location=device))

5. 模型设计

5.1 增加网络的宽度

刚才提到,一般网络的第一层使用较多的卷积核,可以提取更多的特征。

# In[40]:
class NetWidth(nn.Module) :
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 16, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(16 * 8 * 8, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x) :
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 16 * 8 * 8)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

可以传入参数,来指定第一层卷积核个数。需要注意view也要进行修改。

# In[42]:
class NetWidth(nn.Module) : 
    def __init__(self, n_chans1=32): 
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, padding=1)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x) : 
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

5.2 帮助模型收敛并且泛化:正则化(Regularization)

在损失函数上加上正则项,防止过拟合,帮助取得更好的泛化能力。

# In[45]: 
def training_loop_l2reg(n_epochs, optimizer, model, loss_fn, train_loader):
    for epoch in range(1, n_epochs + 1) : 
        loss_train = 0.0
        for imgs, labels in train_loader : 
            imgs = imgs.to(device=device)
            labels = labels.to(device=device)
            outputs = model(imgs)
            loss = loss_fn(outputs, labels)
            
            l2_lambda = 0.001
            l2_norm = sum(p.pow(2.0).sum() for p in model.parameters()) # 正则项
            loss = loss + l2_lambda * l2_norm
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            
            loss_train += loss.item()
        if epoch == 1 or epoch % 10 == 0 : print(
            '{} Epoch {}, Training loss {}'.format(datetime.datetime.now(), epoch, loss_train / len(train_loader)))

此处是L2正则化,l2_lambda作为因子调整正则项的权重。

另外可以使用dropout技术。

# In[47]: 
class NetDropout(nn.Module) : 
    def __init__(self, n_chans1=32): 
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv1_dropout = nn.Dropout2d(p=0.4)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, padding=1)
        self.conv2_dropout = nn.Dropout2d(p=0.4)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x) : 
        out = F.max_pool2d(torch.tanh(self.conv1(x)), 2)
        out = self.conv1_dropout(out)
        out = F.max_pool2d(torch.tanh(self.conv2(out)), 2)
        out = self.conv2_dropout(out)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

需要注意的是,在train下dropout会激活,而在eval模型下,会忽略。所以使用model.train()和model.eval()来切换。

此外,可以使用批标准化(batch normalization)。

# In[49]:
class NetBatchNorm(nn.Module) : 
    def __init__(self, n_chans1=32): 
        super().__init__()
        self.n_chans1 = n_chans1
        self.conv1 = nn.Conv2d(3, n_chans1, kernel_size=3, padding=1)
        self.conv1_batchnorm = nn.BatchNorm2d(num_features=n_chans1)
        self.conv2 = nn.Conv2d(n_chans1, n_chans1 // 2, kernel_size=3, padding=1)
        self.conv2_batchnorm = nn.BatchNorm2d(num_features=n_chans1 // 2)
        self.fc1 = nn.Linear(8 * 8 * n_chans1 // 2, 32)
        self.fc2 = nn.Linear(32, 2)

    def forward(self, x) : 
        out = self.conv1_batchnorm(self.conv1(x))
        out = F.max_pool2d(torch.tanh(out), 2)
        out = self.conv2_batchnorm(self.conv2(out))
        out = F.max_pool2d(torch.tanh(out), 2)
        out = out.view(-1, 8 * 8 * self.n_chans1 // 2)
        out = torch.tanh(self.fc1(out))
        out = self.fc2(out)
        return out

此外就是残差网络和加深网络,此处不再赘述。