计算机视觉基础小总结

在这篇文章里,主要是对深度学习计算机视觉领域的一些概念总结,原理简单的理解,一小部分代码及模型架构的理解。

计算机视觉领域从卷积神经网络开始,视觉领域的输入不再是一个个单点的数据,而是若干张channels*height*weight的图像张量数据。CNN我认为主要是一种思想,这种思想是深度学习在计算机视觉领域众多模型架构的底层原理。具体CNN原理不再详细说明。

有一些重点理解性的东西做简单说明。

一个卷积核指的是什么?

经过多方求证,假如输入的是一张三通道的图像,图像尺寸可大可小,这里的一个卷积核指的是三个矩阵,这三个矩阵各不相同,共同组成一个卷积核,这个卷积核里的每个权重矩阵和对应的通道相乘求和,再把得到的三个值进行加偏置求和,得到隐藏层里的一个神经元作为输出的的一个通道或者叫一层图像的在相对应卷积位置的输出像素值,这里输入的是一幅三通道图像,批次输入图像根据前面的理论知都会依次或者并行处理,究竟是依次还是并行暂不了解,所以只需要真正理解一张图像的处理流程,输入的是三通道一张图像,即三层,经过m个卷积核,每个卷积核含三个权重矩阵对应不同的通道,一个卷积核处理三层图像得到一层图像,m个卷积核处理三层图像得到m层输出通道,所以这里说卷积核个数就是输出通道个数,只不过这一个卷积核含有和图像层数对应数量的权重矩阵,之后进行加权求和得到一层而已。

向下采样,池化核的默认移动原理?

向下采样指的是池化操作,图像尺寸变小操作。

池化核是一块一块移动,和卷积核的一列一列滑动有所不同。且若最后几行的行数小于池化核到该层时池化核的大小时,进行自动补行操作。这个在后续计算输出通道,输出大小时用处广泛。

递增的卷积核设置:

这也是CNN的一个主要思想,通过卷积使得图像尺寸越来越小,层数越来越厚的目标,被证明可以有效提升卷积模型的拟合能力。

import torch.nn.functional as F

这行代码常用到,理解用处。时一个函数式API,在应用中体会其用处。

超参数?

不需要模型训练过程中优化的自己设置的参数,如每层卷积核个数,全连接层单元数,学习速率,优化器参数等,这也是优化模型调参调整的主要参数。

网络容量?过拟合?欠拟合?

网络容量包括层数和每层神经元个数,经过实践证明,提升层数对模型拟合能力效果显著,神经元个数并不显著,这是神经网络模型越来越深的原因。

过拟合在训练集上表现明显优于验证集和测试集,根本原因时过度学习,模型失去了泛化能力,最好的办法是增加训练样本,但这经常难以做到,所以要考虑其他办法,之后说明。

欠拟合就很明显,表现都不好,增大深度,增加神经元数量等。

图像读取和保存?

transform=transform.Compose([transforms.ToTesnsor(),
                             transforms.Normalize(mean=[0.5,0.5,0.5],
                             std=[0.5,0.5,0.5])])
train_ds=torchvision.dataset.ImageFolder(train_dir,transform=transform)

print(train_ds.classes)
print(train_ds.class_to_idx) #['airplane':0,'lake':1]

这里train_dir是一个文件路径,这个数据集是一个已经分好训练数据测试数据的文件,进入train直接就有各个类的文件夹,print(train_ds.classes)这行代码就是在打印train文件夹里分好的类的类别文件夹名称。后续加载数据时返回的时一个路径列表,只不过是分好批次的路径列表,在后续训练过程中会把路径文件打开并进行训练,这样做的好处是减少内存的占用,并且可以灵活的改变数据格式。print(train_ds.class_to_idx) #['airplane':0,'lake':1]直接把文件夹索引联系起来。

模型保存和加载?

torch.save(model.state_dict(),'model_weights.pth')

new_model=Net()
new_model.load_state_dict(torch.load('model_weights.pth'))
new_model.eval()

主要思想是把训练的权重参数保存为一个文件,这个文件名后缀一般为pth或pt,这个可以自己设置,下次测试时直接加载训练好的权重参数,传入测试函数进行测试即可。

使用时常常要保存epoch批次,权重参数,优化器状态等保存到文件里,save的第二个参数就是保存路径,这个代码不再展示。

自定义Dataset类?

在应用中经常要使用的是自己创建的数据集,尽管可以使用提供好的数据集训练参数进行迁移学习,使用存在的模型进行训练,但这个数据集是需要自己进行创建的,完成的是自己的任务。

这个创建代码比较多,为了节省篇幅,不再进行展示,简单说明一下过程:获取全部图片路径,定义类别名称,字典推导式获取类别到编号的字典和编号到类别的字典,使用两个循环加判断遍历所有图片路径,再遍历一下类别名称,这里的图片的文件名称里含有类别名称,比如rain类,图片名称是rain1,rain2,rain3等,我是这样理解的,通过遍历图片和图片下遍历类别找到对应的图片,if c in img: 判断c就是判断文件路径是不是里是否包含类别名称,这个类别名称c是从类别列表里取出来的,进行labels.append(i)把类别编码,i是类别编码,如0,1,2,3,也就是对应的索引,把索引和类别对应起来,labels.appen[i] 就是把类别编码添加到类别列表,这样图像路径和类别列表里相同位置的就组成了图像——标签结构,对应起来了。         定义一个数据增强操作,包括图像裁剪,图像变换,转换张量,标准化等步骤。              定义数据集类:继承自data.Dataset类,重写__getitem__(),__len__()方法,__getitem__()方法返回的是图像和图像对应的标签,初始化方法是把传入自定义数据类的参数赋给自身的属性,__getitem__()方法传入的是一个索引值,找到索引值对应的图片路径和标签,Image.open()打开路径下的图片,返回一个图片数据和对应的标签,__len__()返回的是路径列表的长度,正是数据集的大小。  接下来示例化数据集类,实例化后是一个包含图片路径列表和标签列表的数据集,传入glob.glob()获得的图片路径,和之前构建的标签列表,之后进行切片操作,划分训练集和数据集,这里可以采用随机切片分割操作。      之后构建数据加载,使用切片后的数据构建,接下来就可以返回一个加载的批次数据进行查看了,这里注意next(iter(train_dl)),train_dl是一个包含多批次的图片路径和对应标签的训练加载数据集,因为定义数据集类的时候把他们合并在一起了,切片时候也就同时切片了,这里直接返回一个批次的路径对应的图像和图像对应的标签,之后就可以进行查看了。   之后定义卷积模型,初始化+前向传播,设备选择,实例化模型,损失函数,优化器,训练函数,测试函数,训练过程创建,参数可视化操作。

Dropout抑制过拟合?

随机丢弃部分隐藏神经元的输出。训练模式Dropout层发挥作用,预测模式不发挥作用,训练模式和预测模式代码要放在训练函数和测试函数中,而dropout要放在输出部分的池化层和全连接层后面,默认比例0.5.

Internal Covariate Shift? BN?

训练过程中网络中间层在训练过程中数据分布的改变。

批标准化(BN)提出正是为了解决数据分布发生改变的情况,这里BN的原理不再详细说明,简单说一下代码的实现,在构建神经网络类时,初始化时在每一个卷积层后面加上self.bn=nn.BatchNorm2d(input_features),这里输入的参数时输入图像的特征层数,即通道数。

学习速率衰减?

易于理解,越接近极值点,参数下降速率越慢,防止跳过极值点。直接来看代码实现。

from torch.optim import lr_scheduler
optimizer=torch.optim.Adam(model.parameters(),lr=0.001)
exp_lr_scheduler=lr_scheduler.StepLR(optimizer,step+size=7,gamma=0.1)

接下来还可以把epoch循环代码封装到fit函数中,后续训练时直接调用,这个函数传入的参数依次是:epochs,训练加载数据,测试加载数据,模型实例,损失函数,优化器,学习速率衰减(若没有,exp_lr_scheduler=None)

执行训练时候直接fit(参数列表)即可进行训练

迁移学习?卷积基?

迁移学习概念比较简单,使用大型数据集(预训练模型)已经训练好的参数。难点是如何代码实现,卷积基的概念是利用与训练模型的卷积部分(也叫卷积基)提取图片特征,重新训练最后的全连接部分(也叫作分类器),引申出冻结卷积基,解冻卷积基的概念。

迁移学习的思路:1. 冻结与训练模型卷积基 2. 根据具体问题重新设置分类器 3. 用自己的数据集训练设置好的分类器

下面来看伪代码实现:

model=torchvision.models.vgg16(pretrained=True)
print(model)  #显示VGG16模型架构


for param in model.features.parameters():
    param.requires_grad=False

    

model.classifier[-1].out_features=4  #设置输出为4,对应四分类问题、
model.to(device)
optimizer=torch.optim.Adam(model.calssifier.parameters(),lr=0.0001)
    #今优化分类器部分
loss_fn=nn.CrossEntropyLoss()

这个模型架构不再展示,主要有三个部分组成,(features),(avgpool),(classifier),分别是卷积基,展平层,分类器。

为了使用迁移学习,要冻结卷积基,伪代码如上面代码。


数据增强?

为什么进行数据增强。在少量数据中极可能的变换数据,使模型拟合能力提高,这个被证明是可行的,这个定义在数据读取部分,整个训练模型的最前面,使用是在数据读取部分里面的trainform,即自定义数据集类(类似于已存在数据集的数据读取)里面的__init__()初始化函数里定义一个:

self.transform=transform

 这里还存在的是一个随机乱序问题,代码如下:

np.random.seed(2022)
index=np.random.permutation(len(imgs))
imgs=np.array(imgs)[index]
labels=np.array(labels,dtype=np.int64)[index]

乱序后和前面一样进行数据切片划分训练集和验证集,之后定义数据增强:

train_transform=transforms.Compose([
                    transforms.resize((256,256)),
                    transforms.RandomCrop((224,224)),
                    transforms.RandomHorizontalFlip(p=0.5),
                    transforms,ToTensor(),
                    transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])])

数据增强方法还有很多,比如明亮度变化等等。这里很多都是随机的,都是为了提高模型泛化能力。还要再定义测试数据集的数据增强,测试数据一般不做数据增强,主要是调整大小,转换张量,标准化操作。不再说明。

之后创建dataset类,实例化dataset类为训练数据和测试数据,传入三个参数为训练图像数据,训练标签数据,训练增强方法。测试集实例化也类似。

之后定义train(),test(),fit()进行模型训练。

微调?

使用预训练模型和数据增强,正确率已经很高,微调能使准确率更高,

微调步骤:1. 冻结预训练模型卷积基,训练分类器(迁移学习) 2. 分类器训练完毕后,解冻卷积基,继续模型训练。

注意一定要训练好分类器后再解冻卷积基进行微调,防止卷积基参数发生巨大震荡。


for param in model.features.parameters():   #解冻卷积基
    param.requires_grad=True

extend_epochs=15
#优化器设置一个较小的学习速率
optimizer=torch.optim.Adam(model.parameters().lr=0.00001)
#执行训练
train_loss_,test_loss_,train_acc_,test_acc_=fit(extend_epochs,train_dl,test_dl,model,loss_fn,optimizer)

 注意,上述代码是在迁移学习完成后的微调代码。

最后绘制损失正确率曲线进行对比。

经典网络模型?

VGG,Resnet,TensorBoard可视化,ResNetBasicBlock结构,Inception,DenseNet

图像定位?

主要总结一下遇到的难点和定位主要流程。

图像定位是指预测目标再图像中的位置,通常是预测4个点,组成矩形框。坐标系左下角0.

.xml标记文件结构,这里包含图片size,标记定位框的xmin,xmax,ymin,ymax,和图片类别。

conda install lxml

from lxml import etree


xml=open(r'  /.xml').read()
sel=etree.HTML(xml)

name=sel.xpath('//object/name/text()')[0]   #类别数据
width=sel.xpath('//size/width/text()')[0]

xmin=sel.xpath('//bndbox/xmin/text()')[0]

以上是关于xml,lxml解析库的简单使用。解析出的标注数据是string,要转换为int再进行显示,下面是伪代码,省略了很多。

width=int(width)
xmin=int(xmin)

#绘制矩形定位边框代码
rect=Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),fill=False,color=red)

ax=plt.gca()
ax.axes,add_patch(rect)
plt.show()







#为了使用批量训练,需要调整图像大小,矩形边框应该怎么改变呢?
img=pil_img.resize((224,224))

xmin=xmin*224/width
ymin=ymin*224/height

plt.imshow(img)
rect=Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),fill=False,color=red)

ax=plt.gca()
ax.axes,add_patch(rect)
plt.show()

创建模型输入:

images=glob.glob('dataset/images/*.jpg')
xmls=glob.glob('dataset/annotations/xmls/*.xml')

#提取标注文件名
xmls_names=[x.split('\\')[-1].split('.xml')[0] for x in xmls]
#因为图片数据比标注数据多,要根据标注数据找到图片数据,根据图片和标注文件名一致的特点构造对应的图片路径
imgs=[os.path.join('dataset/images',xml_name) + '.jpg' for xml_name in xmls_names]



np.random.seed(2023)
index=np.random.permutation(len(imgs))
images=np.array(imgs)[index]
xmls=np.array(xmls)[index]

name_to_id{'cat':0,'dog':1}
id_to_name{0:'cat',1:'dag'}

接下来构造一个to_labels(path)函数,功能是根据xml文件路径得到类别名称,图像weight,height,返回的标注数据是xmin/width,.......,name_to_id.get(name),四个比率数据,和一个类别编号数据。这段代码省略了。

然后:

labels=np.array([to_labels(path) for path in xmls],dtype=np.float32)

之后进行数据切片:

sep=int(len(imgs)*0.8)
train_images=images[:sep]
train_labels=labels[:sep]
test_images=[sep:]
test_labels=[sep:]

之后定义数据增强,定义自定义数据集类,自定义数据集类的__getitem()方法返回一个图像数据,标签数据切片前4个是位置数据,最后一个是类别数据。

之后创建训练数据,测试数据的dataset,datasetloader

之后就可以next(iter(train_dl))取出一批数据查看数据形状,图片形状和标注数据形状,绘制图片,矩形框,图片类别等可视化。

接下来创建图像定位模型:

resnet101=torchvision.models.resnet101(pretrained=True)
print(resnet101)
print(list(resnet.children()))

in_f=resnet101.fc.in_features

class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()

        self.conv_base=nn.Sequential(*list(resnet101.children())[:-1])
        self.fc1=nn.Linear(in_f,4)   #位置输出层
        self.fc2=nn.Linear(in_f,2)   #类别输出层
    
    def forward(self,x):
        x=self.conv_base(x)
        x=view(x.size(0),-1)
        x1=self.fc1(x)
        x2=self.fc2(x)

        return x1,x2

接下来进行设备选择,初始化模型,初始化两个损失函数,位置损失函数使用的是均方差计算损失,类别损失是交叉熵损失函数计算损失,初始化优化器,使用学习速率衰减

定义训练函数和测试函数,定义执行函数fit(epochs,train_dl,test_dl),执行函数返回的是训练损失列表和测试损失列表,进行绘图查看。

之后可以进行模型保存与测试,具体代码如下:

PATh='location_model.pth'
torcxh.save(model.state_dict(),PATH)

model=Net()                                  #重新初始化模型
model.load_state_dict(torch.load(PATH))      # 加载模型权重
model=model.cpu()

plt.figure(figsize=(12,8))
imgs,_,_=next(iter(test_dl))                 #加载数据时,对于标注数据不需要使用_占位符
out1,out2=model(imgs)

for i in range(6):
    plt.subplot(2,3,i+1)
    plt.imshow(imgs[i].permute(1,2,0).detach())
    plt.title(id_to_name(torch.argmax(out2[i]).item()))
    xmin,ymin,xmax,ymax=tuple(out1[i].detach().numpy()*scal)  #scal是设置的图片缩小后的尺寸,是224
    rect=Rectangle((xmin,ymin),(xmax-xmin),(ymax-ymin),fill=False,color=red)
    ax=plt.gca()
    ax.axes,add_patch(rect)
plt.show()

图像语义分割?

语义分割是对图片中每个像素进行分类,因此输出分类图大小和输入图是一样的,这里总结一下图像处理任务:

1. 分类

2. 分类和定位

3. 图像语义分割

4. 目标检测

5. 实例分割

其他:目标追踪,视频识别,姿态识别,超分辨率重建,图像描述等。

这里的标注数据是语义分割图,语义分割图的像素值表示其类别的编码,比如(0,1,2,3等)。FCN全卷积网络,端到端网络训练(整个训练过程都是连续的,没有中间的手动处理或预处理步骤),FCN是将深度学习应用到图像分割的开创性模型,U-Net是广泛应用的语义分割模型。FCN2014年提出,U-Net2015年提出。

这里有一些概念要理解,

1. 卷积+池化称为特征提取,卷积+反卷积称为上采样,又称为编码器,解码器结构,

2. skip connection:跳跃连接,目的是特征融合,多尺度的特征融合,上采样是大尺度特征,跳跃连接时小尺度特征,因为感受野相对于整体较小。

3. 反卷积:torch.nn.ConvTranspose2d

4. valid卷积:对输入数据不做填充的卷积

5. same卷积:填充padding,使得输入输出一样

6. weight loss:增大边缘损失权重

这里还有几个问题要解决:

图像数据和标注数据一一对应起来的原理:通过文件名或文件路径关联,常见的作法是使用相同的命名约定或文件结构确保图像和标注数据的对应关系。[os.path.join()  ]这个列表推导式就是在生成关联。glob.glob()读取生成一个路径列表,语义分割图不能用transforms.ToTensor(),因为这样会归一化,改变属性值,语义分割图的属性值是类别编码,不能轻易改变。应先resize(),再torch.tensor()。

放大图像的三种方法:反卷积,反池化,插值法,其中反卷积的效果最好。原理是不同的。反卷积的实现参考:反卷积实现原理。反卷积又称转置卷积。

先写到这里,具体实现过程后续再学习理解应用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值