用途
有时候需要从图片(或文本)中提取出数值型特征,供各种模型使用。深度学习模型不仅可以用于分类回归,还能用于提取特征。通常使用训练好的模型,输入图片,输出为提取到的特征向量。
加入特征之后,结果往往不尽如人意,大致有以下原因:
-
深度学习模型一般有N层结构,不能确定求取哪一层输出更合适。
深度学习模型很抽象——几十层的卷积、池化、信息被分散在网络参数之中。提取自然语言的特征时,常常提取词向量层的输出作为特征,有时也取最后一层用于描述句意;图像处理时往往提取最后一层输出向量;在图像目标识别问题中,常提取后两层子网络的输出作为组合向量。如何选择提取位置,取决于对模型的理解,后文将对图像处理层进行详细说明。 -
针对不同问题训练出的模型,输出的特征也不同。
通常下载的ResNet,VGG,BERT预训练模型,虽然通用性高,但解决具体问题的能力比较弱。比如在自然语言处理中,用GPT-2或者BERT训练的模型只面对普通文章,如果从中提取特征用于判断辱骂,有些脏字可能有效,但是更多的“多义词”会被它的普通含义淹没。 用自己的数据fine-tune后往往更有针对性,而fine-tune的目标也需仔细斟酌,否则可能起到反作用。比如希望用ResNet识别不同的衣服,就需要考虑到衣服的形状、质地、颜色等等因素,如果用衣服类型(大衣、裤子)的分类器去fine-tune模型,新模型可能对形状比较敏感,而对材质、颜色的识别效果反而变差。
原理
图像模型ResNet-50规模适中,效果也很好,因此被广泛使用。下面将介绍该模型各层输出的含义,以及用它提取图片特征的方法。
ResNet原理详见论文:https://arxiv.org/pdf/1512.03385.pdf。常用的ResNet网络参数如下,
可以看到,它包含四层Bottlenect(子网络),越往后,获取的特征越抽象。
以单图为例,输入模型的图像结构为 [1, 3, 224, 224],图片大小为224x224(大小根据具体图片而定),有红、绿、蓝3个通道。
第一步,经过一个7x7卷积层,步长为2,它的输出是:(1,64,112,112),可视为64通道的112x112大小的图片,处理后效果如下图所示。
该层共生成64张图片,由于步长是2,大小变为112x112,每一种特征提取方法对应一组参数,这些参数对每7x7个像素进行同样处理,最终生成一个新的像素。换言之,就是构造了64种特征提取方法,分别提取了颜色,形状,边缘等特征,也可以看到由于处理以卷积为基础,图像位置关系得以保留。
在输入一张图片时,一个224x224的图通过这一层,提取了64x112x112=802816维特征,该层一般称为第一组卷积层conv1,由于该层次太过底层,维度过大,很少使用该层特征。
经过第一层之后,又经过归一化,激活函数,以及步长为2的池化,输出大小为[1, 64, 56, 56],如下图所示:
然后依次传入四个Bottlenext子网络(原理同上),分别称为conv2, conv3, conv4, conv5(也有名为layer1,layer2,layer3,layer4),输出的大小也逐层递减,最终减致2048x7x7=100352,长宽分别是原始参数的1/32。Mask-RCNN中就可获取R-50的第4和第5次作为特征。四层输出如下:
第一维是图像的张数,第二维是通道数,后两维分别是图片的宽高,从任一通道取出数据,都可以直接绘制该通道的图像。数据流至最后一个子网络conv5后输出是2048个7x7大小的图片。有时候将7x7的图片做最大池化或平均池化,最终得到一个值,可理解成从该通道提取的一个特征值。到这一步,特征已经和像素位置无关了。
如果希望提取到的特征不是2048维,则可以在后面再加一个输入为2048,输出为目标维度的全连接层。从上述分析中可以大概了解模型的规模,以及运算量。
另外一个常见的问题是:图像处理对图像的大小有没有要求?是不是所有大小的图片都可以直接代入模型?上例中使用224x224大小的图片,经过多层池化,最终变成了7x7大小。当然也可以代入更大图片(图片越大占用资源越多),比如448x224的图片,最终输出为14x7。一般在训练时,往往经过crop裁剪和resize缩放的步骤,把图片变为统一大小。输入模型时往往以batch为单位,同一batch中的数据大小必须一致,否则只能单张处理。
示例
下例为从指定的层提取ResNet50的特征。
import torch
from torch import nn
import torchvision.models as models
import torchvision.transforms as transforms
import cv2
class FeatureExtractor(nn.Module): # 提取特征工具
def __init__(self, submodule, extracted_layers):
super(FeatureExtractor, self).__init__()
self.submodule = submodule
self.extracted_layers = extracted_layers
def forward(self, x):
outputs = []
for name, module in self.submodule._modules.items():
if name is "fc":
x = x.view(x.size(0), -1)
x = module(x)
if name in self.extracted_layers:
outputs.append(x)
return outputs
model = models.resnet50(pretrained=True) # 加载resnet50工具
model = model.cuda()
model.eval()
img=cv2.imread('test.jpg') # 加载图片
img=cv2.resize(img,(224,224));
img=cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
img=transform(img).cuda()
img=img.unsqueeze(0)
model2 = FeatureExtractor(model, ['layer3']) # 指定提取 layer3 层特征
with torch.no_grad():
out=model2(img)
print(len(out), out[0].shape)