图神经网络学习02:Multi-Label Image Recognition with Graph Convolutional Networks论文使用tensorflow复现(图卷积神经网络解决多标签多分类图像问题)
文章目录
前言
在之前学习了基础的图卷积神经网络之后,经过大佬推荐了一篇图卷积神经网络用于图像分类的论文,也就是这篇Multi-Label Image Recognition with Graph Convolutional Networks,在本篇论文中作者提出利用图卷积神经网络对于一个图像拥有多个标签的数据集进行分类并且取得不错的效果。个人在钻研两天后,完成了网络的搭建以及数据的预处理(coco数据集),这里下面我会照着论文介绍与作者提供的源码(作者使用的是pytorch,而我使用tensorflow复现,也算我个人有理解到一定程度才写得出来),一步一步地完成模型搭建,数据处理,训练以及效果评估(这里采用我自己选择的评估方法,因为博主太菜了复杂的不会。。),感觉有帮助的麻烦点个赞,对我过程中有问题的也可以在评论区中指出,谢谢。
1.网络搭建
这里我就直接开始网络搭建将论文的内容在这里夹杂进行叙述,首先我们观察整个网络的结构在蓝色框的内容里是一个非常传统的特征提取网络(原文推荐采用resNet),经过全局最大池化,输出为我们的上半部分。
resnet=keras.applications.ResNet101(include_top=False,weights='imagenet',input_shape=(448,448,3))
input=keras.Input(shape=(448,448,3))
x=resnet(input)
x=keras.layers.GlobalMaxpooling2D()(x)
#这里的输入形状是(448,448)是论文中说明的那么我也就直接采用了
红色框为我们的图卷积神经网络也就是我们的作者提出的创新店,作者提出在数据集上(coco数据集为例),每张图片上是有多个标签的,比如人,网球,球拍,也就是人出现的时候这些标签也可能出现,那么我们会构造一个有向图,表示当某个标签出现的时候,某个标签出现的概率,那么我们构造的图的节点每个代表的是一个标签,就是要是一个类别,然后他们之间连接的应该是一个概率值,那么我们的输入是每个标签对应的文本向量,也就是在这里我们的图卷积神经网络,用于提取特征的矩阵被替换成了该概率矩阵
那么该矩阵如何构造呢,这里我们根据作者给的一步步往下介绍
1.提取节点特征矩阵的创建
“我们以条件概率的形式对标签相关关系进行建模,即P(Lj|Li)表示标签Lj出现时标签Li出现的概率。并且,P(Lj|Li)不等于P(Li|Lj)。因此,相关矩阵是不对称的。”
在这里作者会提供他们提前统计过的一个共生标签矩阵M,M[i] [j]的值是代表作者自己提前统计的标签i与标签j一起出现的次数,我们这里可以直接读取作者提供的数据来展示(以上提供的数据均来自作者在GITHUB上提供的代码https://github.com/Megvii-Nanjing/ML-GCN)
import pickle
result = pickle.load(open('data/coco/coco_adj.pkl', 'rb'))
_adj = result['adj']
_nums = result['nums']
result
可以看到作者提供的是一个邻接矩阵接一个名称为nums的矩阵,这里的adj就是我刚才说明中的M矩阵,那么作者在这里提供的另一个矩阵nums,nums[i]代表的是标签i在整个数据集中共出现的次数,那么作者在这里说我们的该去了吧是共生次数除以所有标签一共出现的次数
import numpy as np
_nums = _nums[:, np.newaxis]
_adj = _adj / _nums
_adj
但是这样的矩阵可能会出现相关问题(参见论文),这里作者认为还需要对该矩阵进行二值化,t是我们自己设置的一个阈值,即概率小于t都是0,大于t则为1,在作者提供的源码中运用于coco数据集(我在最终训练的时候采用的数据集也是coco)为t为0.4,所以做处理如下
t=0.4#这里对应论文中的二值化
_adj[_adj < t] = 0
_adj[_adj >= t] = 1
在这里的话其实我们的A矩阵就可以了,我们就可以像一般图卷积神经网络处理矩阵一样来处理它例如
但是作者在这里说如果采用这个矩阵,会出现
“,二元相关矩阵的一个直接问题是它会导致过平滑。也就是说,节点特征可能被过度平滑,使得来自不同集群的节点(例如,与厨房相关的节点和与客厅相关的节点)可能变成难以区分的。为了缓解这一问题,我们提出了以下重新加权方案:”
同时作者也展示了如果重新加权的效果会很好,所以我这里就直接重新加权,
_adj = _adj * 0.25 / (_adj.sum(0, keepdims=True) + 1e-6)
_adj = _adj + np.identity(80, np.int)
到了这一步,我们的A矩阵可以说是已经完成处理了,接下来我们再像一般图卷积神经网络处理矩阵A得出我们最终用来聚合特征的矩阵Lsym
D=_adj.sum(1)**-0.5
D=np.diag(D)
res=tf.matmul(D,_adj)
res=tf.transpose(res)
res=res*D
res#查看一下我们最终构造出来的矩阵
可以看到这是一个归一化后的对角矩阵,至此我们在图卷积神经网络中聚合节点特征的矩阵就完成了,那么接下来我们就可以完成我们图卷积神经网络的搭建
2.整体模型的创建
在本次论文中的图中每个节点代表的是一个标签,被用文本向量表示,他们之间连接的边是一个该类别,我们这样就构造除了一个有向图,每个标签互相连接最终经过两层图神经网络成为一个分类器,然后与我们对于图片提取特征的部分相乘(矩阵相乘),最终得出我们的结果之后便是去计算损失,开始训练等方法,由于该网络十分简单,我这里也就直接把整个自定义网络写出来
import spektral as sp
#spektral库是一个基于tensorflow和keras实现的一个内置许多经典图神经网络的库,这里我使用他内置的图卷积GCNConv可以直接嵌入tf和keras的
class MLGCN(keras.Model):
def __init__(self,A_matrix):
super(MLGCN,self).__init__()
#作者在论文中说图卷积神经网络两层一个1024,一个2048层
#最后一层图卷积神经网络输出维度数要与我们特征提取模块输出的最终特征的维度数2048一样(因为最终二者要进行矩阵相乘)
self.gcn1=sp.layers.GCNConv(1024)
self.gcn2=sp.layers.GCNConv(2048)
A=tf.Variable(A_matrix,trainable=True)
#A矩阵这里要设置为可训练的
A=tf.cast(A,tf.float32)
self.A_matrix=A
self.maxpool=keras.layers.GlobalMaxPool2D()
def call(self,input,word2vec):
gcnoutput=self.gcn1([word2vec,self.A_matrix])
gcnoutput=tf.nn.leaky_relu(gcnoutput)
#使用leaky_relu可以让模型更快收敛
gcnoutput=self.gcn2([gcnoutput,self.A_matrix])
gcnoutput=tf.experimental.numpy.moveaxis(gcnoutput,0,1)
feature_output=resnet(input)
feature_output=self.maxpool(feature_output)
output=tf.matmul(feature_output,gcnoutput)
output=tf.nn.sigmoid(output)
return output
这里我再详细解释一下整个网络(我个人是非常喜欢了解一个网络每个关键点的输出,以及最后输出的形状,因为如果是自己搭建的话错误是非常难免的,为了调试我需要知道每个网络确切的输出),那么接下来我就给大家详细解释一下,网络的输出。
首先提取特征模块,图片形状(448,448,3)输入resNet,然后再经过一次全局最大池化输出的最终形状是(None,2048)
然后是图卷积神经网络模块(以coco2014数据集为例物品种类八十种),那么A矩阵的形状就为(80,80)(这里特别注意一下,这个A矩阵也要被定义为模型的可训练参数,如果固定不动模型会出现很大错误),文本向量作者提供的是(80,300)那么经过我们的两层图卷积神经网络(一层1024个隐藏单元,一层2048个)最终输出的数据的shape为(80,2048)
是不是很简单,那么最后到了我们最终将两部分结果相乘,这里采用矩阵相乘的方法那我们因为是个分类问题那么我们最终输出的形状就一定要是(None,numclasses),所以这里我就改变了图卷积神级网络输出维度为(2048,80)然后与特征提取模块输出相乘,最终输出为(None,80),那么整个网络的输出我们推到就完成了,接下来我开始对COCO数据的预处理。
2.coco物体分类数据预处理
本次采用的数据集为coco2014,总共含有八万多张图片,其中在annotations/instances_train2014.json文件中详细记录了每张图片的名称以及对应的分类(一张图片可以对应多个标签,在这里整个问题处于多标签多分类的问题,所以作者才想出别出心裁的方式,考虑标签一起出现的概率从而来帮助我们进行分类),那么接下来就开始我们的代码叙述阶段
import json
filedir = "../input/coco2014/captions/annotations/instances_train2014.json"
annos = json.loads(open(filedir).read())#读取json文件
读取的annos其实是一个字典,他有五个键值,这里我们先读取每个分类,然后制作将每个分类对应的标签(这里数据处理的具体步骤我就不详细注释了,大部分是参考源码打出来的)
def categoty_to_idx(category):
cat2idx = {}
for cat in category:
cat2idx[cat] = len(cat2idx)
return cat2idx
category = annos['categories']
category_id = {}
for cat in category:
category_id[cat['id']] = cat['name']
cat2idx = categoty_to_idx(sorted(category_id.values()))
cat2idx
然后我们提取每张图片对应的分类
annotations = annos['annotations']
img_id = {}
annotations_id = {}
for annotation in annotations:
if annotation['image_id'] not in annotations_id:
annotations_id[annotation['image_id']] = set()
annotations_id[annotation['image_id']].add(cat2idx[category_id[annotation['category_id']]])
images = annos['images']
for img in images:
if img['id'] not in annotations_id:
continue
if img['id'] not in img_id:
img_id[img['id']] = {}
img_id[img['id']]['file_name'] = img['file_name']
img_id[img['id']]['labels'] =list(annotations_id[img['id']])
#最终我们查看一下我们构造出来的img_id
img_id
可以看到到了这里我们的的数据已经非常明显,有文件名,有对应标签的编号,然后这还是个字典数据类型相信到了这一步,大家都会处理数据了,我是作如下处理
anno_list = []
for k, v in img_id.items():
anno_list.append(v)
str_list=[]
for i in range(4000):#这里图片太多了设备问题我只选择4000张用于训练
str_list.append(anno_list[i]['file_name'])
label_list=[]
for i in range(4000):
label_list.append(anno_list[i]['labels'])
这样我就读取了所有文件的名字以及对应的标签,然后接下来如何处理多标签多分类方面我个人倾向于将标签变为类似one-hot编码方式,若标签i出现该值为1,否则为0,
label_one=[]
for label in label_list:
target = np.zeros(80, np.float32)
label.sort()
target[label]=1
label_one.append(target)
然后接下来按照我的习惯将所有数据转换为Dataset类型
import tensorflow as tf
#图片读取方式,我这里采用数据增强随机裁剪在加上随机左右翻转
def load_img(path):
path=img_path+"/"+path
img=tf.io.read_file(path)
img=tf.image.decode_jpeg(img,channels=3)
img=tf.image.resize(img,(480,480),method=tf.image.ResizeMethod.NEAREST_NEIGHBOR)
img=tf.image.random_crop(img,[448,448,3])
if tf.random.uniform(())>0.5:
img=tf.image.flip_left_right(img)
img=tf.cast(img,tf.float32)
img=img/255.0
img=img*2-1
return img
img_data=tf.data.Dataset.from_tensor_slices(str_list)
img_data=img_data.map(load_img)
label_data=tf.data.Dataset.from_tensor_slices(label_one)
data=tf.data.Dataset.zip((img_data,label_data))
BATCH_SIZE=64#指定BATCH_SIZE
data=data.shuffle(300).batch(BATCH_SIZE)
data#查看一下最终我们的数据类型
这里可以发现我们的数据集已经是设置成功了,每张图片的大小,标签都符合我们的预期,那么数据预处理完成,我们接下来就开始模型的配置以及初始化
3.模型的配置以及初始化
m1=MLGCN(res)#res为我们刚才处理的矩阵,参见上文
optimizer=keras.optimizers.SGD(learning_rate=0.001,momentum=0.9)#按照论文所说,这里我们使用SGD优化器
loss=keras.losses.BinaryCrossentropy()
train_loss=keras.metrics.Mean(name='train_loss')
train_acc=keras.metrics.BinaryAccuracy(name='train_accuracy')
#自定义训练步骤如下
@tf.function
def train_step(images,labels,word2vec):
with tf.GradientTape() as t:
pred=m1(images,word2vec)
loss_step=loss(labels,pred)
gradies=t.gradient(loss_step,m1.trainable_variables)#求解梯度
optimizer.apply_gradients(zip(gradies,m1.trainable_variables))#将梯度应用于优化器从而让模型的可训练参数改变
train_loss(loss_step)
train_acc(labels,pred)
这里说一句我这里使用的是损失指定二分类交叉熵,这里我们对于多标签多分类中采用最后使用sigmoid激活,然后采用二分类损失来训练,因为博主在浏览资料的手最终查找资料经过数学说明可以使用这种方法,
有更好方法的可以评论区交流谢谢
4.模型训练以及效果评估
开始我们的训练
Epoch=40
for epoch in range(Epoch):
train_loss.reset_states()
train_acc.reset_states()
for images,labels in data:
train_step(images,labels,word2vec)
print('-',end='')#标志训练完一个batch
print('>')
template = 'Epoch {:.3f}, Loss: {:.3f}, Accuracy: {:.3f}'
print (template.format(epoch+1,
train_loss.result(),
train_acc.result()*100
))
可以看到模型迅速就达到了非常高的准确率,最终训练40次模型最终达到了准确率95.685的准确率(可能一般不用这个来评估这样的模型但我个人认为如果真值与预测值的差值小,相似性高,也可以当做是一个较为简单的评估模型效率的方法)吧
那么我们可以测试一下模型的准确性(这里我是这么想的图像有多个预测值,那么如果真值对应的标号在预测值对应的位置值较大,那么也就是说模型预测图片会出现该类标签,所以我查看一下模型对应位置值是不是在整个预测值你较大)
pred=[]
for images,labels in data.take(1):
pred.append(m1(images,word2vec))
label2=labels
num=0
for i in label2[3]:
if i==1 :
print(num)
num=num+1#查看对应标签的编号
这里输出7,49说明图片中出现第7类和第49类,那么我们查看一下预测值的对应位置
我们再查看一下整个预测值来对比这两个对应的值的大小是否合适,
可以看到其中大部分数据数量级都比7和49对应的数量级1e-1次方小,无论是7还是49他们对应的值都较为大(在整个矩阵中总共有3个1e-1的数据,其中49位最大,7位排第三),也就是我们的预测值正确性是可以保证的,出现了略微偏差(一个图片中预测多了一个标签),接下来我多次测试结果也较为满意,感兴趣的可以自己测试一下。
5.结语
在本篇博客中,我们完成了对于论文说明的网络的搭建,以及coco数据的预处理,模型的训练(这里我自认为在采用损失函数和评估选择方式方面做的不够好,有更好方式的可以评论区交流),并且模型最终在准确率和预测效果方面也获得了不错得效果,碍于源码pytorch我这里的实现采用的是tensorflow可能会产生一些不同。如果本文对你有帮助麻烦点个赞谢谢。