第三周作业:卷积神经网络(Part 1)

目录

卷积神经网络学习内容总结

卷积神经网络实战——猫狗大战

比赛得分

查看数据基本情况

 搭建数据处理流程

定义网络

训练细节

模型测试/预测代码

训过程可视化与分析

 还有些改进


卷积神经网络学习内容总结

        卷积网络本质上也是一个MLP,不过我们在计算的时候,对原来的计算方式进行了修改。具体来说我们对每一层的神经元进行了特别的索引,每次只选取固定的几个神经元对输入进行计算,这样就实现了“参数共享”的效果,因为不管输入特征的尺度有多大,我们只采用固定的神经元进行计算。

        从更形象的角度来讲,卷积是用一组神经元(通常我们为其赋予空间尺度的形象),采用滑动的形式对输入进行处理,每次滑动处理一个窗口的信息,窗口的大小即为卷积核的大小(一般而言是这样,空洞卷积例外),计算的方式是对应位置的卷积核参数与输入特征进行相乘,最后相加。其计算方式与信号处理中的卷积类似,但是信号处理中的卷积是将卷积核转置之后再计算,但是因为神经网络的参数具有可学习性,因此转置这个操作变得没有必要。  

        更细节的计算还包括步幅(stride),以及填充(padding)。步幅指卷积核在滑动的时候每次以多大的距离移动,如果stride为1,那么卷积核的中心每次平移一个单位(像素),如果stride为2,那么卷积核中心每次隔一个单位移动,一次类推。padding是指在图像的周围进行补0,其目的在于调整输出的特征图(feature map)的尺寸。由于卷积核每次将其窗口内的数值计算出一个值,因次不可避免地会造成输出尺寸的改变。通过调整步幅和padding我们可以调整输出的尺寸。大的步幅会减少输出的尺寸,而大的padding会增大输出的尺寸。具体的计算公式如下:

 图片来自pytorch官方文档Page Redirection

       其中dilation 是空洞卷积的参数。   

       提出卷积操作的考虑来自于对图像数据处理的需要。不同于表格数据,图像的每个像素值都是它的一个特征,RGB图像每个像素就有三个特征,而现代图像又拥有巨量的像素值,因此其特征向量的维度异常巨大。如果还采用全连接的方式进行处理,在计算上就会有很大的成本。

        第二个原因是图像不同于表格数据,表格数据的特征一般都有明确的语义信息,比如价格,面积,距离等等。图像的像素值表示的信息是非常低级的信息,其语义信息不直接体现在像素值的大小上,而是在一定的空间范围内像素值的相互关系上体现,比如一个空间范围内的像素点构成了一个物体的图像。全连接网络将每个像素值都视作独立的一个特征,忽略了图像数据在空间上的信息和联系,使得全连接网络对图像特征的学习和表示非常困难(但并非不可能,因为我们可以用全连接网络对Fashin_mnist数据进行分类,但是那些图像的尺寸非常之小,而且颜色通道只有一个)。

        卷积核的参数代表了某种“模式”(pattern),其对输入进行计算后的输出可以视作对该模式的识别结果。卷积神经网络通常会采用大量的卷积核,其意义就在于训练出不同的模式检测能力。每次计算,卷积核会对输入的所有特征进行计算,具体而言,对(通道,高,宽)这三个维度的所有数据进行计算,得到一个值。在同一层次的不同卷积核的处理结果最终会在通道维度叠加。例如我们对具有形状(通道=5,高=10,宽=10)的特征进行卷积操作,如果我们采用10个卷积核,而每个卷积核的尺寸为10*10,那么该卷积核有可学习参数10*10*5=500个,其对输入的计算结果为一个值。10个卷积核的结果在通道的维度拼接,最后产生一个形状为(10,1,1)的输出。

        正是由于这种叠加的方式,使得卷积神经网络有了强大的模式学习(表示)能力,因为上一层卷积的结果在每个通道上都记录了一种由该通道的卷积核的参数定义的模式的检测结果,下一层卷积的操作就可以使这些模式组合起来产生更复杂,更丰富的模式。但是这也造成了卷积神经网络的解释性困难,尽管其浅层的特征依然保留着人类可以解释的内容,比如不同的卷积核可以检测不同的纹理和形状等,但是其深层特征由于对浅层特征进行了多次抽象,已经无法被人直接理解。

池化可以看作一种特殊的卷积——不带参数的卷积。其目的是对卷积的特征进行压缩,操作的方式类似卷积,使用一个滑动窗口对输入进行处理,在窗口内将数据进行压缩,具体的方法有取最大值(Max Polling),求平均(Average Polling)等。池化的意义之一是降低卷积核对输入特征空间平移的敏感性。更实用的意义是对特征进行降采样,在尽量不损失信息的情况下降低特征的维度,或者也可以视作一种低级的注意力机制(attention machnism),就像人更容易听进去声音大的内容。

卷积神经网络实战——猫狗大战

比赛得分

 目前笔者最好成绩90.1(测试集正确率90.1%)

查看数据基本情况

猫狗大战的任务内容是对一张图片上的物体进行分类,判定其属于猫或者狗的一种。

 我们从比赛网站上下载数据集,查看其形式。

 每个文件夹里都是所有的图片混在一起,类别在每张图片的名称里

 搭建数据处理流程

首先我们定义一个函数用来获得所有图片的路径,并从其文件名中获得类别信息,并对类别进行编码——猫:0 | 狗:1

#数据集的根目录:path/xxxx.jpg
def get_data(file_path):
    file_lst = os.listdir(file_path)#获得所有文件名称 xxxx.jpg
    random.shuffle(file_lst)#随机打乱
    data_lst = []
    for i in range(len(file_lst)):
        clas = file_lst[i][:3] #cat和dog在文件名的开头
        img_path = os.path.join(file_path,file_lst[i])#将文件名与路径合并得到完整路径,以备读 
                                                      #取
        if clas == 'cat':
            data_lst.append((img_path,0))
        else:
            data_lst.append((img_path,1))
    return data_lst

 该函数会返回一个列表,列表元素为元组,元组的第一个元素为图片路径,第二个为类别编号

data_lst = [('path/cat_xx.jpg',0) , ('path/dog_xx.jpg',1) , ......, ()]

 下面我们定义自定义的数据集类

class catdog_set(torch.utils.data.Dataset):
    def __init__(self,path,tsfm):
        super(catdog_set).__init__()
        self.data_lst = get_data(path)#调用刚才的函数获得数据列表
        self.trans = torchvision.transforms.Compose(tsfm)
    def __len__(self):
        return len(self.data_lst)
    def __getitem__(self,index):
        (img,cls) = self.data_lst[index]
        image = self.trans(Image.open(img))
        label = torch.tensor(cls,dtype=torch.float32)
        return image,label

 创建这个类的实例需要提供参数(path:图片文件的目录;tsfm:对图片进行的变换)

然后定义数据加载器

train_iter = torch.utils.data.DataLoader(catdog_set(train_datapath,
                                        [transforms.Resize((224,224)),transforms.ToTensor()]),batch_size=4,
                                        shuffle=False)

val_iter = torch.utils.data.DataLoader(catdog_set(val_datapath,
                                         [transforms.Resize((224,224)),transforms.ToTensor()]),batch_size=4,
                                        shuffle=False)

 使用pytorch的DataLoader高级API,接受的第一个参数为Dataset类的实例,bach_size以及随机打乱shuffle,由于我们在前面已经随机打乱了数据,因此这里设置为False

定义网络

class conv_net(nn.Module):
    def __init__(self):
        super(conv_net,self).__init__()
        self.body=nn.Sequential(
            nn.Conv2d(3,16,5,2,2),
            nn.ReLU(),
            nn.Conv2d(16,64,5,2,2),
            nn.ReLU(),
            #nn.MaxPool2d(2,2),#输出的维度,batch_size*64*55*55
            
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.MaxPool2d(2,2),#28*28
            
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.MaxPool2d(2,2),#14*14
            
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.MaxPool2d(2,2),#7*7
            
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.Conv2d(64,64,3,1,1),
            nn.ReLU(),
            nn.MaxPool2d(2,2),#3*3
        )
        self.classify = nn.Sequential(
            nn.Flatten(),
            nn.Linear(576,200),#576 = 3*3*64
            nn.Linear(200,1)
        )
        
        self.out = nn.Sigmoid()
    def forward(self,x):
        features = self.classify(self.body(x))
        return self.out(features)
        

由于LeNet架构是串行的网络,数据在其中顺序流通,因此我们直接采用Sequential类对我们的网络进行定义。

搭建的网络本质上是类LeNet架构,由一系列串联的卷积层和池化层进行特征提取,最后拉平为特征向量,交给由全连接网络组成的分类器进行分类。

self.body是一系列卷积层,进行图像特征提取。self.classify是两层全连接网络,,第一层将图像特征映射到分类的语义空间,第二层进行分类,输出一个值。self.out是sigmoid激活函数,用于接受最后一层神经网络的输出,得到一个0-1的概率预测,超过0.5我们认为是1,否则为0。

训练细节

def init_weight(m):
    if type(m) == nn.Linear or type(m) == nn.Conv2d:
        nn.init.xavier_uniform_(m.weight)

net = conv_net()
device = torch.device('cuda')
net.to(device)
net.apply(init_weight)
criterion = nn.BCELoss()
optimizer = torch.optim.SGD(net.parameters(),lr=0.02,momentum=0.2,weight_decay=1e-4)

def acc(y_hat,y):
    with torch.no_grad():
        y_hat = y_hat >= 0.5
        y_hat.type(y.dtype)
        return torch.sum(y_hat==y)

loss_lst = []
train_acc = []
val_acc = []
num_epochs = 50
for epoch in range(num_epochs):
    loss_tmp_lst = []
    tic = time.time()
    for X,y in train_iter:
        X_tensor = X.to(device)
        y = y.view(-1,1)
        label = y.to(device)
        output = net(X_tensor)
        loss = criterion(output,label)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        loss_tmp_lst.append(loss.item())
    
    loss_lst.append(sum(loss_tmp_lst)/len(loss_tmp_lst))
    with torch.no_grad():
        num_train = 0
        num_hits = 0
        for X,y in train_iter:
            X_tensor = X.to(device)
            y = y.view(-1,1)
            label = y.to(device)
            output = net(X_tensor)
            hits = acc(output,label)
            num_train += len(y)
            num_hits += hits
        train_accuracy = num_hits/num_train
        train_acc.append(train_accuracy)
        num_val = 0
        num_hits = 0
        for X,y in val_iter:
            X_tensor = X.to(device)
            y = y.view(-1,1)
            label = y.to(device)
            output = net(X_tensor)
            hits = acc(output,label)
            num_val += len(y)
            num_hits += hits
        val_accuracy = num_hits/num_val
        val_acc.append(val_accuracy)
           
    if (epoch+1)%4==0:
        state_dict = {'net':net.state_dict(),'optimizer':optimizer.state_dict(),'epoch':epoch}
        torch.save(state_dict,f'./ckpt/epoch{epoch}_loss{sum(loss_lst)/len(loss_lst)}_tacc{train_accuracy}_vacc{val_accuracy}.pth')
    print(f'time:{time.time()-tic:.3f} loss on epoch:[{epoch+1}]/[{num_epochs}]: ',sum(loss_tmp_lst)/len(loss_tmp_lst),
          f'train acc:{train_accuracy:.3f}', f'val acc:{val_accuracy:.3f}')

init_weight函数用来对模型的参数进行初始化,我们采用Xavier初始化,可以尽量保证模型在训练初期不爆炸。

criterion是我们的损失函数,我们采用Binary Cross Entropy(本质上就是CrossEntropyLoss)

优化器optimizer我们采用SGD(随机梯度下降),学习率设置为0.02,weight_decay设置为0.0001,momentum设置为0.2。

acc函数是我们定义的用来计算正确率的函数,它返回一个批次里预测正确的数量。

loss_lst,train_acc,val_acc三个列表用于记录训练过程中模型的损失,训练集正确率与验证集正确率,用来可视化训练过程,方便后面分析。

训练进行50个epoch,在每个epoch我们循环的从数据加载器中取出一个batchsize的数据进行预测,与label求loss,进行反向传播更新参数,基过程与房价预测一致。每个epoch结束,我们使用with torch.no_grad()来避免计算梯度(不需要保存计算图的中间结果),然后分别对训练集和验证机进行遍历,求得正确率。结束后输出一次训练情况,用于对模型的训练进行观察分细和判断 。每4个epoch我们保存一次模型的权重。

笔者共试验了2种网络参数,第一种网络参数较少,表现为中间的卷积层有很多32通道的卷积,第二种网络参数较多,所有的卷积层通道数都为64。除此之外,两种模型的其它方面一样。

首先我们将所有的输入数据缩放到224*224的尺寸,保证在最后一层的特征数一样。之后,我们在前两层,我们采用大卷积核,大步幅来快速提取底层特征并对输入进行降采样,具体来说,第一个卷积层用16个5*5卷积,步幅为2,padding为2,将输入尺寸降采样到112*112,通道数升至16。之后再经过64个5*5卷积,步幅为2,padding为2,将输入尺寸降采样到56*56,通道升至64。

之后经过多个3*3卷积,步幅为1,padding为1,不改变输出大小,经过窗口为2*2,步幅为2的maxpolling进行降采样。具体形状如下

 卷积部分最后的特征在拉成向量后,有3*3*64=576维。

模型测试/预测代码

我们查看保存的模型权重,选择在训练集和验证机上表现都较好的模型对测试集进行预测,代码如下

import pandas as pd
def get_testdata(file_path):
    file_lst = os.listdir(file_path)
    #random.shuffle(file_lst)
    data_lst = []
    for i in range(len(file_lst)):
        img_path = os.path.join(file_path,file_lst[i])
        data_lst.append((int(file_lst[i][:-4]),img_path))
    return sorted(data_lst)

class catdog_testset(torch.utils.data.Dataset):
    def __init__(self,train_path,tsfm):
        super(catdog_testset).__init__()
        self.data_lst = get_testdata(train_path)
        self.trans = torchvision.transforms.Compose(tsfm)
    def __len__(self):
        return len(self.data_lst)
    def __getitem__(self,index):
        (name,img) = self.data_lst[index]
        image = self.trans(Image.open(img))
        #label = torch.tensor(cls,dtype=torch.float32)
        return image,name

ckpt =torch.load(model_path)
net = conv_net()
net.load_state_dict(ckpt['net'])

result = []
with torch.no_grad():
    for X,name in test_iter:
        X_tensor = X.to(device)
        pred = net(X_tensor)
        pred.to(torch.device('cpu')).numpy()
        if pred >= 0.5:
            result.append((name[0].numpy(),1))
        else:
            result.append((name[0].numpy(),0))
dic={}
dic['id'] = [r[0] for r in result]
dic['pred'] = [r[1] for r in result]

df = pd.DataFrame(dic)
df.to_csv('submission.csv')

读取测试数据与读取训练数据有些差异,因为测试数据并没有标签,图片名称只有ID信息,所以我我们稍微修改一下读取函数。返回的数据列表元素为元组,元组的第一个元素是图像的路径,第二个元素是图像的ID。

测试过程与训练过程差异不大,只是我们只进行推理不计算loss和反向传播,输出的结果我们将其转到cup并转化为numpy数据类型。之后我们创建一个字典dic,用id和pred作为key用所有图像的ID和预测值的列表作为value,之后用pandas将字典转化为dataframe类型,用to_csv方法写入CSV文件。

值得注意的是,CSV文件的第一行和第一列不需要(网站要求的格式是第一列为ID,第二列为预测类别),需要手动删除一下再提交网站。

训过程可视化与分析

开始我将小模型直接在训练集上训练,目标设定在50轮,但是通过观察训练情况输出我发现在25轮之后模型基本稳定,开始上下波动,于是我在35轮手动停止了训练,之后训练的所有模型也有这种情况,所以所有的模型都只看前35轮的情况。

loss曲线

训练集正确率

 验证集正确率

 32代表小模型,64代表大模型。aug代表数据增强,norm代表在数据增强的基础上对图像的三个通道分别进行了归一化。

可以看到,大模型的训练损失通常比小模型要更好,但是不使用数据增强的条件下,大模型的正确率表现接近小模型。引入数据增强后,大模型的正确率表现得到提升,loss的水平也进一步下降。

在最初的实验中,我没有采取任何图像增强,只用原始数据训练小模型,在保存下的模型中选择了最好的模型进行预测,提交之后得到了11.85的好成绩(我直接惊呆……),我仔细检查之后发现原来是我把label搞反了,调整之后重新提交,得到了88.15的成绩(100-11.85=88.15)。

之后我希望在此基础上提升,于是我将模型的特征通道增加,也就是使用了大的模型,训练之后用最好的模型进行预测,得到了86.1分的成绩,反而不如小模型(房价预测的情况又上演了……),但是我观察到这次使用的模型损失比小模型的要小,所以我认为是大模型有点过拟合训练集,于是我找了保存下来的模型中损失大一点的模型进行预测,得到了83.9分和85.7的成绩。而loss越高,正确率越低说明过拟合并不严重,二者还基本呈线性关系。那么为什么loss更低的大模型,正确率却没有更高呢?我的理解是,因为loss水平不和正确率严格成线性关系,也就是说loss越低正确率不一定越高,即使在训练集上。具体来说,因为我们使用的交叉熵损失函数,计算的是正确类别的模型预测值的负log值,那么有可能出现,在训练之后,模型对某些类别的预测增强了,也就是“对的更对”,比如之前模型对正确类别的预测为0.6,现在提升到了0.8,这个提升会造成loss的下降,但是模型并没有因此增加预测正确的数量,因此正确率可以保持不变,甚至因为随机梯度下降导致在某些类别上预测变错而导致正确率降低。因此loss只能提供一个总体的概括,不能用来严格的确定正确率。

我觉得之所以会这样是大模型的训练其实不到位,毕竟参数变得更多,训练难度也会因此上升。于是后面我采用了数据增强,具体来说,我对训练集进行了随机水平翻转与随机旋转,再次训练。数据增强相当于扩大了数据集,并且引入了随机性降低了数据的方差,因此通常可以提升模型表现。这次用最好的模型进行预测,得到了89.5分的成绩。

最后,我加入了输入图像的归一化预处理,将图像调整成接近均值为0,方差为1的正态分布,再次进行训练,提交后得到了90.1分的成绩。

 还有些改进

LeNet5为代表的串行卷积神经网络,无法做到很深的层次,因为会面临梯度爆炸和梯度消失问题,难以训练,如果采用resnet可以训练更深的模型。batchnorm也是很有效的稳定训练的技术。

另外对于现在用的模型,如果在训练后期进行学习率递减,可能会取得更好的效果。

此外,学习率,权重衰减,优化算法等都没有很多尝试,一开始选了一组参数,发现效果可以就没再改了,或许对上述超参数进行搜索可以得到更好的配置。

最后,卷积神经网络虽然比全连接网络更适合处理图像数据,但是其超参数的选择依然需要谨慎。调参的一些经验还是不够。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值