引言
这一章节我们稍缓一下,讲讲每一层的原理、实现和参数设置。目前能够实现深度学习的语言有很多,其中Python相对拥有比较完善的环境,因此使用的也较多。
Tensorflow、Keras、PyTorch是目前使用较多的三种环境。由于博主比较熟悉PyTorch,就用它来讲解了。本文以RSOD目标检测数据集为例,进行图像分类任务。
开局先放几个引用在这里。
import torch
from torch import optim
import torch.nn.functional as F
0_细胞:层
我们首先看看pytorch中对几个关键功能的定义
卷积层
如上文所述,卷积操作指的是通过使用卷积核计算更新矩阵的值,以达成平滑或者锐化的目的。
对于卷积神经网络来说,由于我们的目的是找寻特征,所以我们所使用的卷积层所达成的锐化的目的。pytorch按照输入数据的维度对卷积层做出了如下的分类
#一维卷积
class torch.nn.Conv1d(in_channels, out_channels,
kernel_size, stride=1, padding=0,
dilation=1, groups=1, bias=True)
#二维卷积
class torch.nn.Conv2d(in_channels, out_channels,
kernel_size, stride=1, padding=0,
dilation=1, groups=1, bias=True)
#三维卷积
class torch.nn.Conv3d(in_channels, out_channels,
kernel_size, stride=1, padding=0,
dilation=1, groups=1, bias=True)
参数说明:
in_channels:输入矩阵的通道数
out_channels:输出矩阵的通道数
kernel_size:卷积核尺寸
stride:卷积核处理步长
padding:为将输出与输入的尺寸相统一,在输入矩阵外围补充0值的行列数
dilation:卷积核元素之间的间距(空洞卷积运算时使用)
groups:对输入按维度进行分组,分组进行卷积
bias:添加偏置
- Tips:
- 1.卷积核的尺寸选择并没有一个统一的标准。但一般来说,小一点的卷积核能够降低参数量和计算难度,反正模型里会有多层的卷积操作,与其想要贪心地用较大的卷积核,不如交给多个卷积核的叠加。
- 2.输出矩阵的通道数指的是卷积核的数量。若有8x256x256的矩阵输入卷积层,输出矩阵的通道数设置为64,则卷积层会自动生成64个不同卷积核,使用每一个卷积对输入进行卷积,则最后的输入尺寸变为8x256x256x64
- 3.空洞卷积可以在扩大感受野的同时不降低特征尺寸,但对于图像分类任务没有什么作用。
- 4.分组是为了方便多卡训练的设置。pytorch有整体的多卡训练模式,无需具体到每一层上。
激活层
Sigmoid激活函数在pytorch的实现如下:
class torch.nn.sigmoid()
池化层
池化层的目的是降低矩阵尺寸,放大特征在矩阵中的占比。在分类任务中,池化层通常跟在卷积层+激活层之后。
#最大池化: 取最大值的方式
nn.MaxPool2d(kernel_size, stride=None,
padding=0, dilation=1,
return_indices=False, ceil_mode=False)
#平均池化:取平均值的方式
torch.nn.AvgPool2d(kernel_size, stride=None,
padding=0, ceil_mode=False,
count_include_pad=True, divisor_override=None)
参数说明:
kernel_size:池化核尺寸
stride:步长,通常与 kernel_size 一致
padding:填充宽度,主要是为了调整输出的特征图大小,一般把 padding 设置合适的值后,保持输入和输出的图像尺寸不变。
dilation:池化间隔大小,默认为 1。常用于图像分割任务中,主要是为了提升感受野
ceil_mode:默认为 False,尺寸向下取整。为 True 时,尺寸向上取整
return_indices:为 True 时,返回最大池化所使用的像素的索引,这些记录的索引通常在反最大池化时使用,把小的特征图反池化到大的特征图时,每一个像素放在哪个位置。
count_include_pad:在计算平均值时,是否把填充值考虑在内计算
divisor_override:除法因子。在计算平均值时,分子是像素值的总和,分母默认是像素值的个数。如果设置了 divisor_override,把分母改为 divisor_override。
全连接层
全连接层即感知机中的线性神经元。在图像分类任务中,全连接层用于将输入变为nx1的矩阵。
class torch.nn.Linear(input_size,output_size)
Softmax
Softmax将nx1的矩阵映射为kx1的矩阵,且输出的矩阵要素之和为1。
class torch.nn.softmax(dim=i)
1_组织:Block
前面我们讲过,卷积神经网络相对于多层感知机机制的差异,如感受野机制,部分连接和特征。但其实还有一点是多层感知机无论如何也比不了,就是卷积神经网络在网络扩展方面的便捷性。
block = torch.nn.Sequential(torch.nn.Conv2d(in_size, out_size, kernel_size, stride, padding),
torch.nn.sigmoid(inplace=True),
torch.nn.MaxPool2d(kernel_size, stride)
torch.nn.Conv2d(out_size, out_size, kernel_size, stride, padding),
torch.nn.sigmoid(inplace=True)
torch.nn.MaxPool2d(kernel_size, stride)
)
如图是一个包含了两次和池化操作的block。pytorch中torch.nn.Sequential()是专为类似使用场景构建的一个有序容器。当发生调用时,容器内填入的层会自动按照填入的顺序依次处理。
2_器官:LeNet-5实现分类
上一篇文章中,我们已经讲解了LeNet模型的结构,今天我们用pytorch来实现它。LeNet-5是LeNet模型专为MNist手写识别任务搭建的构型,5的意思是共有五层工作结构。本文将其转移至RSOD分类任务中。
第一步,读取并分割数据集
import os
import random
dir = r'E:\RSOD'
读取文件目录中所有的jpg文件并打乱顺序
def search_file(dir_path, filters):
#迭代查找符合条件的文件,并输出文件名列表
print("search files begin in dir {}".format(dir_path))
w = []
for cur_dir, sub_dir, files in os.walk(dir_path) :
for file in files :
if os.path.splitext(file)[1] in filters :
file_abs_path = os.path.join(cur_dir, file)
w.append(file_abs_path)
print("search end.")
return w
file_list = search_file(dir, filters=['.jpg'])
random.shuffle(file_list) #输入list随机化,并覆盖原list
建立类别与数值的对应关系(多分类)
classes = {'playground':1,'overpass':2,'aircraft':3,'oiltank':4}
按8:2的比例将数据集划分为train、test,并保存为txt。
from sklearn.model_selection import train_test_split
train_files, test_files = train_test_split(file_list, test_size=0.2, random_state=0, shuffle=True)
#写入train
with open(osp.join(dir, 'train.txt'), 'w') as f:
for name in train:
name = str(name)
f.write(name)
# 写入test
with open(osp.join(dir, 'test.txt'), 'w') as f:
for name in test:
name = str(name)
f.write(name)
建立LeNet网络结构。
请注意网络的搭建方式。
import torch
import torch.nn as nn
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Sequential( #input_size=(1*1082*922)
nn.Conv2d(3, 6, 5, 1, 2), #padding=2保证输入输出尺寸相同
nn.sigmoid(), #input_size=(6*1082*922)
nn.MaxPool2d(kernel_size=2, stride=2),#output_size=(6*541*461)
)
self.conv2 = nn.Sequential( #input_size=(6*541*461)
nn.Conv2d(3, 6, 16, 5),#(16*537*457)
nn.sigmoid(), #input_size=(16*537*457)
nn.MaxPool2d(2, 2, 1) #output_size=(16*269*229)
)
self.fc1 = nn.Sequential(#input_size=(16*269*269)
nn.Linear(16*269*269, 120),
nn.sigmoid()
)
self.fc2 = nn.Sequential(
nn.Linear(120, 84),
nn.sigmoid()
nn.Linear(84, 4),
nn.sigmoid()
)
self.fc3 = nn.softmax(dim=1)
# 定义前向传播过程,输入为x
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
# nn.Linear()的输入输出都是维度为一的值,所以要把多维度的tensor展平成一维
x = x.view(x.size()[0], -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
读取数据,准备填入网络中。
Pytorch提供了Dataset类,通常我们会以继承该类的方式获得其功能,并在类中添加注入缩放、旋转、裁剪、归一化等操作。
Pytorch还提供了dataloader类以调用dataset的输出。我们会在train中加入。
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
from PIL import Image
class ds(Dataset):
def __init__(self, target_files, transform = None):
super(ds, self).__init__()
self.img_list = target_files
self.transform = transform
def __len__(self):
return len(self.img_list)
def __getitem__(self, index):
img = self.img_list[index]
img_label = classes[img.split('\\')[-1].split('.')[0].split('_')[0]]
img = Image.open(img)
if self.transform:
img = self.transform(img)
img = normalize(img)
else:
img = img.ToTensor()
img = mormalize(img)
return img, torch.Tensor(img_label)
def transform(image):
transforms.Resize(864, interpolation=2)
transforms.CenterCrop(864)
transforms.ToTensor()
)
def normalize(image):
img = np.array(img)
for i in range(img.shape[0]):#对影像进行处理
img = img.numpy().astype(np.float64)
mean = np.mean(img[i, :, :]) #求取每个波段的均值
img[i, :, :] -= mean #标准化
img[i, :, :] = (img[i, :, :] - np.min(img[i, :, :])) * 1 / (np.max(img[i, :, :]) - np.min(img[i, :, :])) + 0
#对每一个波段进行归一化
img = torch.from_numpy(img)
return img
下面我们构建训练器。
class Trainer():
def __init__(self, file_list, batch_size, workers, epoch):
self.batch_size = batch_size
self.workers = workers
self.epoch = epoch
self.dataset = ds(train_files, transform = True)
self.train_loader = Dataloader(self.dataset,
batch_size=self.batch_size,
shuffle=True
)
self.model = LeNet()
self.criterion = nn.CrossEntropyLoss(size_average=False)
self.optimizer = torch.optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.99))
def weight_init(m):
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
elif isinstance(m, nn.BatchNorm2d):
m.weigth.data.fill_(1)
m.bias.data.zero_()
def train(epoch):
self.model.train() #此处并非指的是开始训练,我们尚未实例化model。此处的含义是将model调整为train模式
tbar = tqdm(self.train_loader) #tbar是可见化进度条
for i, agg in enumerate(tbar):
image = agg[0]
label = agg[-1]
self.optimizer.zero_grad() #初始化时,要清空梯度
output = self.model(data)
loss = self.criterion(output, label)
loss.backward()
self.optimizer.step() #相当于更新权重值
if i % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.data[0]))
self.model.apply(weight_init)
def main(self.epoch)
for epoch in range(1,self.epoch+1):
train(epoch)
trainer = Trainer(file_list=train_files, batch_size=6, workers=6, epoch=100)
trainer.main()
测试器也是大同小异。
class Evaluator():
self.__init__(self,)
def test():
model.eval() #转换模型为eval也即测试模式
test_loss = 0
correct = 0
for agg in test_loader:
image = agg[0]
label = agg[-1]
output = model(image)
#计算总的损失
test_loss += c riterion(output, target).data[0]
pred = output.data.max(1, keepdim=True)[1] #获得得分最高的类别
correct += pred.eq(target.data.view_as(pred)).cpu().sum()
test_loss /= len(test_loader.dataset)
print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.2f}%)\n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))