图片标注问题image_caption

作者: 菜田的守望者w | 来源:发表于2019-06-04 18:16 被阅读65次

    数据集:

    • 数据集中的训练集使用的是coco train 2014,82783张图片,测试集使用的是 val 2017 ,5000张图片,对应的caption是captions_train2014.json,和captions_val2017.json。
      该文件中是以字典的形式包含其内容信息,key值由“info”,”licenses“,”images“,”annotations“组成。
      info:其中包括数据集建立的时间。下载地址,版本号等。
      licenses:是数据集使用条款。
      images:包括图片的filename,height,width ,图片对应的网址,图片对应的caption的id。
      annotation:包含image_id,对应的caption的id,和每个图片对应的5句描述

    • 训练:

      训练集使用的是coco train 2014中的全部数据。

    • 测试:

      测试集使用的是coco val 2017中的的数据。

    • 数据预处理:

      build_data.py中定义了以下几个函数:

      1.def load_and_process_metadata(captions_file,image_dir): 
             with open(captions_file, encoding='utf-8') as f:
             lines = f.readline()
             d = json.loads(lines)
             print(d)
             f.close()
                    
    

    d["images"]

    d["images"]中包含的内容

    [{'license':6,'file_name':'000000492110.jpg','coco_url':'http://images.cocodataset.org/val2017/000000492110.jpg', 'height': 427, 'width': 640, 'date_captured': '2013-11-24 00:43:28', 'flickr_url': 'http://farm9.staticflickr.com/8195/8139005828_fda85b4b72_z.jpg', 'id': 492110}]

    d["annotations"]中包含的内容

    [{'image_id': 54592, 'id': 562033, 'caption': 'Couple of people walking up the snowy mountain with skis'},
    {'image_id': 23034, 'id': 561930, 'caption': 'A man smiles watching a rider approach with two horses.'}]

    captions_file:caption_train2014.json和val_2014.json文件的路径。
    image_dir:train_2014和val_2014的路径。
    函数作用:
    读取json文件将image和对应的caption对应,将内容封装到 ImageMetadata中,这时的一个image_id对应一个filename对应五个captions

    效果
    COCO_train2014_000000458922.jpg
    描述captions
    'Beef and other food on a plate next to a wine glass',
    'A finished steak dinner and glass of wine are on a table.',
    'some food and a fork is on a plate',
    'A plate with fork and steak and a wine glass on placemat with papers in background.',
    'a rare steak with a fork and a glass of wine
    COCO_train2014_000000058245.jpg
    图片描述captions
    'A blue vase filled with colorful flowers sitting on the ground.',
    'A blue vase on table holding red flowers next to door.',
    'A bunch of orange flowers in a blue vase on a runner on a table.',
    'A blue glass vase with some flowers on a table.',
    'A blue glass vase with red and yellow flowers

    namedtuple("ImageMetadata",["image_id","filename",'captions']

    1.读取json文件使用json中的json.loads()方法。
    2.索取json中的信息就直接使用字典中的方法就好。

          2.def process_caption(caption):
                  tokenized_caption=[FLAGS.start_word]
                  tokenized_caption.extend(nltk.tokenize.word_tokenize(caption.lower()))
                  tokenized_caption.extend(FLAGS.end_word)
    

    函数作用:将”start_word“和”end_word“加载到每句话中的开始和结束,作用类似于起始密码子,终止密码子,蛋白质合成的起始,和终止。并把caption变成小写,目的不让同一个单词在vocabulary字典中出现两次节省资源。
    1.使用方法是用列表中的entend方法

         3.def create_vocab(captions):
                 counter = Counter()
                 for c in captions:
                      counter.update(c)
                 word_counts = [x for x in counter.items() if x[1] >= 4]
                 word_counts.sort(key=lambda x: x[1], reverse=True)
    

    函数作用:统计caption中的所有单词,和出现的频率,并把单词以及单词出现的频率
    写入到txt文件中。
    1.使用到的方法有collections 中 Counter,Counter是一个计数器,将单词输入其中可
    直接将单词的出现频率显示出来。并将重复的单词去除。并只取单词中频率大于4的。并从小到大排序。然后创建一个字典将排序好的单词取出,将单词从0~vocab_size给单词赋一个id。

     4.class Vocabulary(object):
          def __init__(self, vocab, unk_id):
    

    vocab:是单词带有对应id的字典。
    unk_id:是对应vocab_size,也就是单词的长度,如果单词没在制作的dict中就返回
    一个空的字符。

    word_to_id(self, word):

    给定caption中的单词,如果单词在dict中返回对应的word_id,不在就返回unk_id。

    5. def process_dataset()
    

    函数作用:这时的一个image仍然是对应一个caption。将image遍历,使每一个image对应一个caption。然后将caption,和image读入tfrecord中。

    images = [ImageMetadata(image.image_id, image.filename, [caption])
    for image in images for caption in image.captions]
    random.seed(12345)
    random.shuffle(images)

    使用random的方法将image_caption的顺序打乱。

    • tfrecord的使用方法

    定义写入tfrecord文件地writer
    writer=tf.python_io.TFRecordwriter(filename) # filename:tfrecord保存的地址
    文件名队列在上述的images中已经构造好了
    for i in range(len(images))
    image=images[i]:
    获取每个image对应的图片地址+文件名
    image_filename=image.filename
    获取每句image的描述
    caption=image.captions
    获得每句caption的id
    caption_id=word_to_id(caption)

    • 读入image,并解码

      with tf.gfile.FastGFile(image.filename, "rb") as f:
      encoded_image = f.read()

    解码:
    1. tf.decode_csv() 解码文件文本内容
    2.tf.image.decode_jpeg(countent) 将jpeg编码的图像解码为uint8张量
    3.tf.image.decode_png(content) 将png编码的图像解码为uint8张量
    4.tf.decode_raw(value,uint8)解码二进制内容

    构造example:

    def _int64_feature(value):
      """将int 64特性插入序列中的包装器"""
      return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
    def _bytes_feature(value):
      """将字节特性插入序列示例Proto的包装器"""
      value = tf.compat.as_bytes(value)
      return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
    def _int64_feature_list(values):
      """将int 64 FeatureList插入序列的包装器-示例Proto"""
      return tf.train.FeatureList(feature=[_int64_feature(v) for v in values])
    def _bytes_feature_list(values):
      """将字节FeatureList插入序列示例Proto的包装器"""
      return tf.train.FeatureList(feature=[_bytes_feature(v) for v in values])
     context = tf.train.Features(feature={     
          "image/image_id":_int64_feature(image.image_id),
          "image/data": _bytes_feature(encoded_image),
      })
    feature_lists = tf.train.FeatureLists(feature_list={
          "image/caption": _bytes_feature_list(caption),
          "image/caption_ids": _int64_feature_list(caption_ids)
      })
    

    那么什么样的数据需要映射为FeatureList或Feature?
    我的理解是数字表示,二分类就是0或1,那么就class=0映射为tf.train.Feature(tf.train.Int64List(value=[0])), 只要这个字段包含的数据维度是固定的,就可以封装为 Feature。
    对于长度不固定的字段类型,映射为FeatureList。比如NLP样本有一个特征是一句话,那么一句话的长度是不固定的,NLP中一般是先分词,然后把每个词对应为该词在字典中的索引,一句话就用一个一维整形数组来表示 [2, 3, 5, 20, ...],这个数组的长度是不固定的,我们就映射为
    这里将caption通过上述的函数word_to_id中将caption转化为caption_id。
    sequence_example = tf.train.SequenceExample(
    context=context, feature_lists=feature_lists)

    将caption的feature和images的feature保存到tf.train.SequenceExample中,
    将序列化后的example写入文件

    wtiter.write(example.SerializeToString())
    write.close()

    读取tfrecord
    • 构造文件名读取队列

    1.将需要读取的文件名放入到文件名队列中

    tf.train.string_input_producer(data_file,shuffle=True)
    data_file:tfrecord的文件名
    shuffle=True:默认文件名随机打乱

    • 读取

    使用TFRecorder的方法读入
    _,serialized_example=reader.read(filename_queue)

    • 解析example
    context, sequence = tf.parse_single_sequence_example(example_serialized,
                        context_features={
                            image_feature: tf.FixedLenFeature([], dtype=tf.string)},
                        sequence_features={
                            caption_feature: tf.FixedLenSequenceFeature([], dtype=tf.int64) })
    self.encoded_image = context[image_feature]
    self.caption = sequence[caption_feature]
    
    • 解码

    解码仍然使用上述中的解码方法
    tf.image.decode_jpeg(self.encoded_image)
    使用tf.cast()方法将其转换为tf.float32,由于每张图片的大小可能不是统一大小的,为了方便后面的cnn提取特征,将图片统一大小。使用tf.image.resize_images(image,size=[])

    在图片处理时将图片进行数据归一化,优点是
    1.归一化后加快梯度下降求最优解的速度
    2.归一化后有可能提高精度
    3.使用梯度法求最优解时,归一化往往非常有必要,否则模型很难收敛,甚至不能收敛

    方法1
    image = tf.image.convert_image_dtype(image, dtype=tf.float32) # 将图片像素值变为[0,1]
    image = tf.subtract(image, 0.5) # 图片像素值域由[0, 1]变换为[-0.5,0.5]
    image = tf.multiply(image, 2.0) # 图片像素值域由[0,0.5]变为[-1,1]

    方法2
    def image_to_float(image)
    image = tf.image.convert_image_dtype(image, dtype=tf.float32)
    return (image/127.5) - 1.0

    方法3
    在人脸识别中用到的直接将特征值除以255的简单缩放法

    图片处理

    在对图片处理时要进行以下操作,目的是为了防止过拟合,过拟合是指我们训练的模型“实在是太好了”对与训练的数据分类预测效果都特别好,而面对新的数据时变现的效果可能就没那么好了。我们训练数据的目的是让模型既不过拟合,也不是欠拟合,而是处于过拟合与欠拟合之间的一种状态。欠拟合是指无论是对于测试集还是训练集的效果,表现得都不是很好。就比如我们复习数学吧,我们只把课本上的题会做理解,而考试面对新的题,可能只是修改个数我们就做的不太好,这种状态称为过拟合。而我们连书的内容看都不看的话,啥题都不会的情况就是欠拟合。

     image = tf.image.random_flip_left_right(image)
    调整图片的随机亮度
    image = tf.image.random_brightness(image, max_delta=32 / 255)
     调整图片饱和度
     image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
     随机调整RGB图像的色调
     image = tf.image.random_hue(image, max_delta=0.032)
     在某范围随机调整图片对比度
    image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
    输入一个张量image,把image中的每一个元素的值都压缩在min和max之间。小于min的让它等于min,大于max的元素值等于max
    image = tf.clip_by_value(image, 0, 1.0)
    
    • 对caption构造input_sequence,target_sequence
           images_and_captions = []
           images_and_captions.append([image, self.caption])
           enqueue_list = []
            for image, caption in images_and_captions:
                # 获取每个句子的长度
                caption_length = tf.shape(caption)[0]
                # 将句子id向后移动一
                input_length = tf.expand_dims(tf.subtract(caption_length, 1), 0)
                # 真正句子的id
                input_seq = tf.slice(caption, [0], input_length)
                # 句子向有移动后的句子id目的是为了预测下一个单词
                target_seq = tf.slice(caption, [1], input_length)
                indicator = tf.ones(input_length, dtype=tf.int32)
                enqueue_list.append([image, input_seq, target_seq, indicator])
    
    • 构造批出理队列

    有两种方法:tf.train.batch和tf.train.batch_join的区别,一般来说,单一文件多线程,那么选用tf.train.batch(需要打乱样本,有对应的tf.train.shuffle_batch);而对于多线程多文件的情况,一般选用tf.train.batch_join来获取样本(打乱样本同样也有对应的tf.train.shuffle_batch_join使用)。

    第一种
    tf.train.batch_join(enqueue_list,
                batch_size=self.batch_size,
                capacity=queue_capacity,
                dynamic_pad=True,
                name="batch_and_pad"))
    第二种
    tf.train.batch(enquene_list,
                        batch=self.batch_size,
                        capacity=queue_capacity,
                        name="batch_and_pad") 
    
    • 构造image_embedding
      将解码后的图片经过CNN的全连接层后生成[batch_size,512]的向量
    • 构造caption_embedding

    将输入的seq变成词向量,词向量才能在lstm中进行训练计算

    原理就是 image.png
    这里假设词表中有“我,爱,河,南,科,技”6个字,在类比one-hot编码的时候就形成了一个[6,6]的矩阵emdedding-map,然后我如果想取出其中的两个单词“河”,“南”那么就需要查emdedding-map。进行类似的矩阵相乘得到两个词的词向量。

    embedding_map=tf.get_variable(name="map",shape[vocab_size,512],initializer=self.initializer)
    seq_embeddings = tf.nn.embedding_lookup(embedding_map, self.input_seqs)
    返回的是tensor 的 shape是 [batch_size,word_size,512]

    模型:

    1.结构

    • CNN

    1,cnn使用的是inceptionv3模型:主要提取图片的特征,
    inceptionv3具有很少的参数量,节省计算时间加快学习速度;5x5的卷积核用两个3x3的 卷积核代替增强表达能力,为减少计算复杂度将卷积转化为稀疏连接以下展示cnn的结构图;


    7D7A837EF287B16474B652BF3EE657EA.jpg
    290284015DEB91BC4B9006A7E4D66F5C.jpg
    225EBB66053EF93D5E7263B64BA6319E.jpg 10EEDBD008D7BD87A4846F2F3BC0CA54.jpg 35642BE60F475B4592BE6A50E809DB3C.jpg
    2.模型输入的image尺寸为299x299X3 ,batch_size=32 数据类型为float32

    shape:[batch_size, height, width, channels]

    4.使用batch_normalization.(简称BN)

    BN是一种解决深度神经网络层数太多,而没有办法有效向前传递的问题,因为每一层的输出值都会有不同的均值,和方差,所以输出的数据分布也不一样。
    优点:
    1.他不仅加快模型的收敛速度,而且更重要的是在一定程度上缓解了深度网络中的“梯度弥散”(就是在靠近输出层的hidden layer梯度越大,参数更新快,但是靠近输入层的hidden layer梯度小,参数更新慢,几乎和初始状态一样,随机分布。梯度爆炸与之相反)总体来说就是梯度相当不稳定。
    2.控制过拟合可以减少或不用dropout
    3.降低网络对初始化权重不敏感
    4.允许使用较大的学习率
    所以使用BN可以使得模型训练更加容易和稳定。BN从字面意思来说就是每一批数据进行归一化,这里分批在前面的数据准备里已经进行了分批处理。BN可以在网络中任意一层进行归一化处理,V3-inception使用到的优化器是SGD。
    没有使用BN的时候每层的值迅速全部变为零,也就是说所有的神经元都已经死了,而有BN,relu过后,每层的值都能有一个比较好的分布效果。


    QQ截图20190604122213.png

    数据如果在梯度很小的区域,那么学习率就会很慢甚至长时间停滞,但是减均值除方差后,数据就被移到中心区域,如右图所示,对于大多数激活函数来说,这个区域的梯度都是很大的。这个用来解决梯度消失。但是如果每一层都是这样做的话,数据的分布总是随着变化敏感的区域,相当于不用考虑数据分布变化了这样训练起来更有效率。但是减均值除方差得到的分布是正态分布,但是并不是正态分布就是最好或者最能体现我们训练样本的特征分布。因为数据本身很多情况下是不对称的,或者激活函数未必是对方差为1的数据最好的效果,就比如sigmoid激活函数,在-1到1之间的梯度变化不大。这样的话对于非线性变化的作用就不能很好的提现,换言之就是,均方误差操作后可能会削弱网络的性能,所以再加一步利用优化变一下方差大小和均值位置,使得新的分布更切合数据的真实分布,保证模型的非线性表达能力。

    卷积层

    作用:通过在原始图像上平移来提取特征

    tf.nn.conv2(input,fitter,strides,padding)

    input:是传入四个维度的图片或者上一层卷积得到的tensor
    fitter:卷积核相当于权重有初始值有形状,
    strides:步长大小,决定卷积核每次移动的步长,决定了卷积后的图片大小。
    padding:可设置为‘’SAME“,"VALID"当padding=SAME时下一层的图片长宽计算new_widh=w/s(向上取整)。padding=VALID时new_width=(w-f+1)/s。w为图片的长宽,f是卷积核大小,s是步长

    激活函数

    作用:增加非线性分割能力,对输入的数据原始空间进行扭曲。

    • relu
      1,有效的解决了梯度小时问题
      2,计算速比较快
      SGD:(批梯度下降)的求解速度远快于sigmoid和tanh
      sigmoid:采用sigmoid缺点:计算量相对大,而采用relu激活函数,整个过程的计算量节省很多,在深层神经网络中,sigmoid函数反向传播时很容易出现梯度消失的情况

    池化层

    作用:减小学习参数,降低网络的复杂度
    池化层分为最大池化和平均池化,一般最常用最大池化,最大池化是在卷积核大小的feature_map上取最大值,因为这个最大值可以反映了这个feature周围的特征。
    内部的池化窗口和卷积核类似,以及步长,padding

    Incpetion V3中将5×5的卷积替换成了两个3×3的卷积
    而且在这个module中还是用了卷积的分解,将一个7×7的卷积拆分成了一个1×7的卷积和一个7×1的卷积,不仅能够大大节省参数降低模型的过拟合,还能比一个7×7的卷积多一个非线性的变换。另外inceptionv3将卷积网络进行了拆分,其结果比对称的拆分为几个相同的小卷积核效果更明显,可以处理更多,更丰富的空间特征,增加特征多样性。

    • LSTM

    对于LSTM神经网络无论是从什么角度描述,中间做的一堆事情,还是找什么样的特征是最适合做这样的一个分类任务,最终连接softmax是用于分类。时序t就是每个单词输入的先后顺序,它结合上一时刻的输出同时乘以权重当做这一时刻的输入 20181205233257613.png

    定义RNN的基本单元

    lstm_cell=tf.contrib.rnn.BasicLSTMCell(num_units=512,state_is_tuple=True)

    为了防止过拟合,在LSTM中也添加了dropout层,分别使input,和output的值控制在0.7

    lstm_cell=tf.contrib.rnn.DropoutWrapper(lstm_cell,input_keep_prob=0.7,
    output_keep_prob=0.7)

    定义图中的隐状态:hi,可以把隐状态视作记忆体,它捕捉了之前时间点上的信息。记忆体在RNN迭代的过程也在不断地更新内部的记忆,就像人的大脑一样,不断地记住新的东西,又在忘记旧的

    获得lstm全零状态
    zero_state=lstm_cell.zero_state(batch_size=batch_size,dtype=tf.float32)
    更新lstm状态,这里将经过CNN后提取得到的特征向量输入到lstm_cell
    _,initial_state=lstm_cell(self.image_embeddings,zero_state)

    定义RNN循环,在这里将句子的特征向量self.seq_embeddings输入到RNN中.sequence_length:图片对应的caption的句子长度。
    initial_state:更新后的initial_state。

    lstm_outputs, _ = tf.nn.dynamic_rnn(cell=lstm_cell, inputs=self.seq_embeddings,
    sequence_length=sequence_length,
    initial_state=initial_state,
    dtype=tf.float32,
    scope=lstm_scope)

    在lstm最后添加一个全连接层作为最终的输出
    num_outputs:定义输出的维度
    weights_initializer:设置权重参数,
    该函数默认使用relu的激活函数

    logits = tf.contrib.layers.fully_connected(
    inputs=lstm_outputs,
    num_outputs=10000,
    activation_fn=None,
    weights_initializer=self.initializer,
    scope=logits_scope)

    2.原理

    • 构造损失函数

    损失函数使用softmax的交叉熵损失函数,target是logits经过向右移动一后得到的caption的id,target经过tf.cast(tf.reshape(self.seq_target,[-1]),dtype=tf.float32)后才能传到交叉熵中计算logits对于target的偏离程度

    方法1
    losses=tf.nn.sparse_softmax_cross_entropy_with_logits(logits=logits, labels=targets)

    weights:是一个数值全为1的shape=[batch_size*word_size]的一维数组
    因为每个批次的数据都会产生不同的loss值,将每个批次得到的损失值除以批次大小,得到每个批次的平均损失

    batch_loss=tf.div(tf.reduce_sum(tf.multiply(losses,weights)),tf.reduce_sum(weights),name="batch_loss")
    tf.losses.add_loss(batch_loss)
    total_loss=tf.losses.get_total_loss()

    方法2

    
    正确的答案,这里将[batch_size,num_steps]二维数组转换为一维数组损失的权重。在这里所有的权重都为1,也就是说不同batch和不同时刻的重要程度是一样的。
     loss=tf.contrib.legacy_seq2seq.sequence_loss_by_example(
     [logits],[tf.reshape(self.targets,[-1][tf.ones([batch_size*num_steps],dtype=tf.float32)] )
      计算得到每个batch的平均损失
      total_cost=tf.reduce_sum(loss)/batch_size
    

    3.实验

    • 训练

      • 超参数

        1.learning_rate

        指数衰减法:

        如果staircase=True,代表每decay_step步更新一次learning_rate,如果staircase=False,那就每次迭代都会更新学习率。学习率计算方法 image

    最初设置learning_rate=2.0
    tf.train.exponential_decay(learning_rate,global_step,decay_steps=decay_step,decay_rate=0.5 , staircase=True)
    1,global_step:用于衰减计算的全局步数。喂入一次batch_size 计为一次global_step
    2,decay_step:衰减速度(num_examples_per_epoch/batch_size)*num_epochs_per_decay
    num_example_per_epoch:所有的image_caption的数量
    batch_size:batch_size一般设置为2的n次幂,这里设置为32
    num_epochs_per_decay设置的是8
    3,decay_rate:衰减系数

    多项式学习率衰减

    polynomial_decay(learning_rate, global_step, decay_steps,end_learning_rate=0.0001, power=1.0,cycle=False, name=None):

    在对抗神经网络中使用的一种学习率衰减方法,特点是确定结束的学习率,学习率更新公式是:

    decayed_learning_rate = (learning_rate - end_learning_rate) *(1 - global_step / decay_steps) ^ (power) + end_learning_rate

    对于神经网络中三个参数的概念:

    epoch: 训练时,所有训练数据都训练一次,即所有数据的个数
    batch:使用训练集中的一小部分样本对模型进行一次反向传播的参数更新,这一小部分样本称为“一批数据”。
    batch_size: 在训练集中选择一组样本用来更新权值。一个batch_size包含的样本数目,通常设为2的n次幂,常用的包括32,64,128,256,网络较小的时候使用256,较大时使用32,或者16.

    实例:

    数据集中有50000张训练的图片数量,现在选择batch_size=256对模型进行训练。

    • 每个epoch要训练的图片数量:50000

    • 训练集具有的batch数50000/256=196(向上取整)

    • 每个epoch需要完成的batch数196

    • 每个epoch中发生的模型权重更新次数:196

    • 不同次的epoch训练虽然用的都是训练集的五万张图片,但是对模型的权重跟新值却是不同的,因为不同次的模型处于的loss在空间上的不同位置,模型的训练次数越靠后,越接近谷底,loss也就越小。

    优化函数

    优化函数使用的是随机梯度下降SGD:SGD是每一次迭代计算mini-batch的梯度,然后对参数进行更新,计算梯度的方法是求偏导,对于训练数据集,我们首先将其分成n个batch,每个batch包含m个样本。我们每次更新都利用一个batch的数据,而非整个训练集、
    优点:
    当训练数据太多时,将训练集分成一个一个批次,可以减少机器的压力,并且可以更快地收敛

    • 测试

    • 运行结果

    p是概率,因为生成的图片描述不是只有三句,所以三句话的概率和不为零,只是挑选了三句概率最大的打印出来。

    QQ浏览器截图20190526085307.png
    • 结果分析
    效果由于在训练时loss值没有下降到最小,所以效果不是太好,图中是两个长颈鹿站在草地上,但是在后面两句将长颈鹿预测成大象。

    参考链接:

    1.在构建读取队列时可以开启多线程:https://zhuanlan.zhihu.com/p/47760620https://zhuanlan.zhihu.com/p/40588218
    2.在lstm的构建和理解:https://www.cnblogs.com/hypnus-ly/p/8407905.html
    3.caption的词向量方面:https://www.bilibili.com/video/av35575799/?p=7
    4.cnn模型https://blog.csdn.net/loveliuzz/article/details/79135583

    相关文章

      网友评论

        本文标题:图片标注问题image_caption

        本文链接:https://www.haomeiwen.com/subject/vadztctx.html