0

多相机行人跟踪[04]-用于行人重识别的余弦相似度网络(中)

 2 years ago
source link: https://yerfor.github.io/2019/12/13/dl-07/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

上一篇深度学习随笔中, 我们成功地在TensorRT框架下跑通了行人特征提取网络,但模型性能不佳,这一篇文章的主要工作是,优化网络结构和训练过程,尽可能地提升了模型的性能。

本文的主要内容在于复现<cosine metric learning>论文实现的行人特征提取网络,这部分的工作包括:

  • 采用了轻量级的Resnet-block
  • 自定义了针对余弦相似度优化的cosine-softmax作为分类器(classifier)的激活函数
  • 为获取理想的feature层向量,对feature层和分类器的权重做出约束
  • 使用生成器(Generator)来支持大批量数据集

1.网络结构

由于时间有限,来不及在电脑上画好看的图了,先手写一个placeholder。

network_structure

1.1 轻量级的Resnet-block

Cosine-metric-learning采用的Resnet-block相比起Resnet论文里的结构有所改进。

先说共同点,都有Identity和Convolutional两种Block,并且输入和输出的关系也是一样的,即Identity的输入输出Shape一致,而Convolutional的输出比输入在尺寸上少了一半,通道数增加了一倍。

再说不同点,cosine-metric-learning将Block内(1,1)的卷积层删去,转而采用两个(3,3)的卷积层,如下图所示:

resblock

1.2 自定义的cosine-softmax激活函数

1.2.1 传统Softmax函数在cosine-metric中面临的问题

假设一个二分类问题,输出的feature是一个shape=(2,)的向量(用下图所示的二维平面表示)。将该向量送入softmax中,会得到一个概率分布的向量,shape也是(2,),该向量中的每一位标量代表输入的图像是某一类的概率。我们把概率分布的第一个标量取出来,作为下图的画的等高线考察的值。

softmax

10softmax

1.2.2 针对余弦区分度优化的cosine-softmax函数

传统的softmax可以用下式描述:

cosine-softmax的定义式如下:

二者的对比:

1

参数k的作用:

2

cosine-softmaxTensorflow2.0实现:

class TrainableCosineSoftmax(tf.keras.layers.Layer):
def __init__(self, W, **kwargs):
super(TrainableCosineSoftmax, self).__init__(**kwargs)
self.w = W

def build(self, input_shape):
self.k = self.add_weight(name='k', shape=(1,),
initializer=tf.keras.initializers.Ones(), trainable=True)

def call(self, x):
return tf.exp(self.k * x) / tf.reduce_sum(tf.exp(self.k * self.w))

实际使用方法:

feature_layer = Dense(classes, name='feature', activation='relu', kernel_regularizer=tf.keras.regularizers.l2(),kernel_constraint=tf.keras.constraints.UnitNorm(), use_bias=False)
X = feature_layer(X)

W = feature_layer.kernel
X = l2_normalization(X)

X = TrainableCosineSoftmax(W)(X)

1.2.3 两种激活函数的效果对比:

1.3 对feature层的参数约束

1.3.1 feature层的参数约束

  • 对参数进行l2正则化,防止过拟合
  • 保证权重的参数二阶范数是1
  • 不使用bias

具体实现:

feature_layer = Dense(classes, name='feature', activation='relu', kernel_regularizer=tf.keras.regularizers.l2(),kernel_constraint=tf.keras.constraints.UnitNorm(), use_bias=False)

1.4 对feature层输出进行归一化

feature的输出,即为下面cosine-softmax函数里的r,必须要保证归一化,即单位方向向量。

  • 具体实现——使用tensorflow1.0的函数,自定义了一个层:
l2_normalization = tf.keras.layers.Lambda(lambda x: tf.nn.l2_normalize(x, dim=1), name='l2_normalization')

1.5 一些已经解决的小问题

引入cosine-softmax函数后,网络的loss在训练时总会在某一个新的epoch骤然变成nan:

acc-cosine-softmax

经过排查,发现可能是因为我们的cosine-softmax过于严格,导致其输出时,正确的那一类对应的probability是0,由于我们用的损失函数是交叉熵,其计算公式为:

上式中,$y_{true}$是一个one-hot格式的向量,只在所表示的那类的位置取1,其他均为0;$y_{pred}$即为网络输出的预测向量,输出的是一个图像相对于所有分类的一个概率分布.

言归正传,对上面的式子,整个$\Sigma$里面实际上只有正确的那一类对应的项不等于0,所以当网络输出的预测向量对正确那类的预测概率是0时,交叉熵的值就是$ln(0)\rightarrow -\infin$。导致loss变成nan

  • 我的解决方法是在交叉熵计算信息量的公式里面加上一个很小的偏置,以确保$ln()$里面的数不会取到0,于是新的交叉熵公式变成:

修改loss函数后,nan的现象确实消失了。

1.6 一些论文采用的新特性的失败尝试

1.6.1 Dropout/AlphaDropout

Dropout层不能转换成TRT

drop->trt

1.6.2 ‘elu’/‘selu’

会导致梯度消失等问题。

1.6.3 BatchNormalization

不能转换成TRT

BN->trt

1.6.4 triplet loss/magnet loss

还没搞明白这两个Loss,现在用的还是交叉熵,看看效果怎么样。

2.训练过程

2.1 数据准备

2.1.1 MARS数据集的使用

2.1.2 保证各class采样的数量级一致

2.1.3 使用生成器函数来调用大批量数据集

在使用大批量数据集的时候,一次性将所有数据都读取到内存中是不现实的,针对这种情况,tensorflow提供了model.fit_generator的API,允许我们自己编写“生成器”提供batch数据,进行训练。

生成器是Python内置的函数类型,与一般函数采用return一次返回全部内容不同,它的函数体由一个while循环组成,在while循环内有一个yield,每次调用生成器函数,都会运行一次循环,然后通过yield返回本次循环生成的值。于是结合之前的数据集处理函数,编写了一个Generator。

具体实现如下:

# 应对大批量数据时,需要使用生成器产生数据
def data_generator(filenames, ids, batch_size=128, image_shape=(128, 64), classes=625):
reshape_fn = (
(lambda x: (x / 255-0.5) * 2) if image_shape == IMAGE_SHAPE[:2]
else (lambda x: (cv2.resize(x, image_shape[::-1]) / 255-0.5) * 2))

while True:
data = [(filename, _id) for filename, _id in zip(filenames, ids)]
np.random.shuffle(data) # 将数据文件名随机洗牌
data = data[:batch_size] # 选取前batch_size组作为本次的输出
image_names = [filename for filename, _id in data]
bathch_ids = [_id for filename, _id in data]

images = np.zeros((len(image_names),) + image_shape + (3,), np.float32)
for i, filename in enumerate(image_names):
image = cv2.imread(filename, cv2.IMREAD_COLOR)
images[i] = reshape_fn(image)
bathch_ids = np.asarray(bathch_ids, dtype=np.int64)
bathch_ids = convert_to_one_hot(bathch_ids, classes)
bathch_ids = bathch_ids.T
yield (images, bathch_ids)

2.1.4 数据的预处理

2.1.4.1 归一化

对之前训练的网络做了测试,以下是测试样本:

test

测试了两种归一化方法,一般来说,最通用的归一化方法应该是:

但是因为我们的数据集实在太大了(450,000张照片),加载这么大量的数据需要巨大的内存(所以我们训练用的是生成器的方式获得batch而不是一开始全部读取到内存里),scikit-learn现有的函数无法处理这么大的数据,所以我试了两种方法来近似:

  • 该模型训练了80个epoch

样本名 余弦相似度(=1-余弦距离)
yellow1 /
yellow2 0.7345311
yellow3 0.43169522
red 0.8405349
liushishi 0.55780655
huge 0.6379674
  • 该模型训练了15个epoch

样本名 余弦距离(=1-余弦距离)
yellow1 /
yellow2 0.78016734
yellow3 0.608993
red 0.7567972
liushishi 0.42149162
huge 0.47507387

2.1.4.2 数据类型:

  • 在上述归一化的过程中,要先cv2.resize成(128,64)后再除255,因为opencv采用uint8的数据类型,会把0.xxx近似成0
  • tensorrt不支持float64,而该类型是numpy的默认类型,所以输入前要转成float32

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK