文章目录
- 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. 卷积
主要介绍了什么是卷积,卷积的运算操作
卷积有一些特性:
- 对相邻像素的局部操作
- 平移不变性
- 相对于其他方式有更少的参数
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()
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]))
使用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()
简单来看,就是对卷积核所在范围内的图像取平均
下面换另一个卷积核
# 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_()
从卷积核来看,卷积核右侧的权重大,左侧的权重小,对于垂直的边缘响应应该更大,
结果确实如此,实际上上面的卷积核就是垂直边缘检测的卷积核。而卷积神经网络,从一定程度上就是学习到不同的卷积核,来从图片中提取并组合各种特征,来达到最终的目的。
2.3 更进一步:深度和池化(pooling)
对于小的图片,小的卷积核就足够提取到一些特征。而当图片变大时,小的卷积核就显得不够了,但是使用更大的卷积核意味着更大的参数量,更臃肿的模型。有没有什么办法可以在大图像时依旧使用小卷积核呢?解决问题的方法就是池化。
池化是的基本思想就是下采样,将周围几个像素合并为一个像素。大致可分为:
- 平均值池化(average pooling),周围像素取平均值作为新像素。
- 最大值池化(max pooling),周围像素取最大值作为新像素。
此外,可以以采用有间隔的卷积核。这行方法都会损失一些信息,但是更重要是保留图片最重要信息,供下层卷积层进一步提取高级特征。图中展示了最大值池化的详细步骤。
# 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]))
我们可以将卷积和池化组合到一起,组成网络。
这里涉及到感受野(receptive field)的概念,感受野即当前层中的一个像素,可以追溯到原始输入多少像素。这样说有点抽象,用一个示例来说明。(图片来自深度神经网络中的感受野)
在原始图像中,使用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. 训练网络
训练大概分为几步:
- 输入数据到模型(前向传播)
- 计算损失(前向传播)
- 将旧的梯度置0
- 调用backward(),计算梯度(反向传播)
- 优化器根据梯度更新参数
根据这个步骤,编写代码
# 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
此外就是残差网络和加深网络,此处不再赘述。