Less is More


  • 首页

  • 标签

  • 归档

  • 搜索

keras note

发表于 2019-08-14 |

1. keras Lambda自定义层

官方文档:将任意表达式(function)封装为 Layer 对象。

1
keras.layers.Lambda(function, output_shape=None, mask=None, arguments=None)

  • function: 需要封装的函数。 将输入张量作为第一个参数。
  • output_shape: 预期的函数输出尺寸。(使用 TensorFlow 时,可自动推理得到)
  • arguments: 可选的需要传递给函数的关键字参数。以字典形式传入。

几个栗子:

1.1 最简:使用匿名函数

1
2
3
model.add(Lambda(lambda x: x ** 2))

x = Lambda(lambda image: K.image.resize_images(image, (target_size, target_size)))(inpt)

其中,lambda是python的匿名函数,后面的[xx: xxxx]用来描述函数的表达形式,
lambda xx: xxxx整体作为Lambda函数的function参数。

1.2 中级:通过字典传参,封装自定义函数,实现数据切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from keras.layers import Input, Lambda, Dense, Activation, Reshape, concatenate
from keras.utils import plot_model
from keras.models import Model

def slice(x, index):
return x[:, :, index]

a = Input(shape=(4,2))
x1 = Lambda(slice,output_shape=(4,1),arguments={'index':0})(a)
x2 = Lambda(slice,output_shape=(4,1),arguments={'index':1})(a)
x1 = Reshape((4,1,1))(x1)
x2 = Reshape((4,1,1))(x2)
output = concatenate([x1,x2])
model = Model(a, output)
plot_model(model, to_file='model.png', show_shapes=True, show_layer_names=True)

模型结构图如下:

slice img

1.3 高级:自定义损失函数
step 1. 把y_true定义为一个输入
step 2. 把loss写成一个层,作为网络的最终输出
step 3. 在compile的时候,将loss设置为y_pred

1
2
3
4
5
6
7
8
9
10
11
12
# yolov3 train.py create_model:
model_loss = Lambda(yolo_loss, output_shape=(1,), name='yolo_loss',
arguments={'anchors': anchors, 'num_classes': num_classes, 'ignore_thresh': 0.5})(
[*model_body.output, *y_true])
model = Model([model_body.input, *y_true], model_loss)
model.compile(optimizer=Adam(lr=1e-3), loss={'yolo_loss': lambda y_true, y_pred: y_pred})


# yolov3 model.py yolo_loss
def yolo_loss(args, anchors, num_classes, ignore_thresh=.5, print_loss=False):
...
return loss

2. keras 自定义loss

补充1.3: 也可以不定义为网络层,直接调用自定义loss函数
参数:

  • y_true: 真实标签,Theano/Tensorflow 张量。
  • y_pred: 预测值。和 y_true 相同尺寸的 Theano/TensorFlow 张量。
    1
    2
    3
    4
    def mycrossentropy(y_true, y_pred, e=0.1):
    return (1-e)*K.categorical_crossentropy(y_pred,y_true) + e*K.categorical_crossentropy(y_pred, K.ones_like(y_pred)/num_classes)

    model.compile(optimizer='rmsprop',loss=mycrossentropy, metrics=['accuracy'])

带参数的自定义loss:

有时候我们计算loss的时候不只要用到y_true和y_pred,还想引入一些参数,但是keras限定构造loss函数时只能接收(y_true, y_pred)两个参数,如何优雅的传入参数?

优雅的解决方案如下:

1
2
3
4
5
6
7
8
9
10
11
# build model
model = my_model()
# define loss func
model_loss = dice_loss(smooth=1e-5, thresh=0.5)
model.compile(loss=model_loss)

# 实现
def dice_loss(smooth, thresh):
def dice(y_true, y_pred):
return 1-dice_coef(y_true, y_pred, smooth, thresh)
return dice

3. keras 自定义metrics

model.compile里面除了loss还有一个metrics,用于模型性能评估
参数:

  • y_true: 真实标签,Theano/Tensorflow 张量。
  • y_pred: 预测值。和 y_true 相同尺寸的 Theano/TensorFlow 张量。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    def precision(y_true, y_pred):
    # Calculates the precision
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    predicted_positives = K.sum(K.round(K.clip(y_pred, 0, 1)))
    precision = true_positives / (predicted_positives + K.epsilon())
    return precision


    def recall(y_true, y_pred):
    # Calculates the recall
    true_positives = K.sum(K.round(K.clip(y_true * y_pred, 0, 1)))
    possible_positives = K.sum(K.round(K.clip(y_true, 0, 1)))
    recall = true_positives / (possible_positives + K.epsilon())
    return recall

    model.compile(optimizer='rmsprop',loss=mycrossentropy, metrics=['accuracy', recall, precision])

4. keras 自定义Layer

源代码:https://github.com/keras-team/keras/blob/master/keras/engine/base_layer.py

自定义layer继承keras的Layer类,需要实现三个方法:

  • build(input_shape):定义权重,调用add_weight来创建层的权重矩阵,其中有参数trainable声明该参数的权重是否为可训练权重,若trainable==True,会执行self._trainable_weights.append(weight)将该权重加入到可训练权重的lst中。
  • call(x):编写层逻辑
  • compute_output_shape(input_shape):定义张量形状的变化逻辑
  • get_config:返回一个字典,获取当前层的参数信息

看了keras一些层的实现,keras中层(如conv、depthwise conv)的call函数基本都是通过调用tf.backend中的方法来实现

4.1 栗子:CenterLossLayer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class CenterLossLayer(Layer):

def __init__(self, alpha=0.5, **kwargs): # alpha:center update的学习率
super().__init__(**kwargs)
self.alpha = alpha

def build(self, input_shape):
# add_weight:为该层创建一个可训练/不可训练的权重
self.centers = self.add_weight(name='centers',
shape=(10, 2),
initializer='uniform',
trainable=False)
# 一定要在最后调用它
super().build(input_shape)

def call(self, x, mask=None):

# x[0] is Nx2, x[1] is Nx10 onehot, self.centers is 10x2
delta_centers = K.dot(K.transpose(x[1]), (K.dot(x[1], self.centers) - x[0])) # 10x2
center_counts = K.sum(K.transpose(x[1]), axis=1, keepdims=True) + 1 # 10x1
delta_centers /= center_counts
new_centers = self.centers - self.alpha * delta_centers
# add_update:更新层内参数(build中定义的参数)的方法
self.add_update((self.centers, new_centers), x)
self.result = x[0] - K.dot(x[1], self.centers)
self.result = K.sum(self.result ** 2, axis=1, keepdims=True) #/ K.dot(x[1], center_counts)
return self.result # Nx1

def compute_output_shape(self, input_shape):
return K.int_shape(self.result)

有一些自定义层,有时候会不实现compute_output_shape和get_config

  • 在call方法中,输出tensor如果发生了shape的变化,keras layer是不会自动推导出输出shape的,所以要显示的自定义compute_output_shape
  • 不管定不定义get_config方法,都可以使用load_weights方法加载保存的权重
  • 但是如果要使用load_model方法载入包含自定义层的model,必须要显示自定义get_config方法,否则keras 无法获知 Linear 的配置参数!
    • 在 __init__ 的最后加上 **kwargs 参数,并用 **kwargs 参数初始化父类。
    • 实现上述的 get_config 方法,返回自定义的参数配置和默认的参数配置

4.2 补充1.3 & 2: 自定义损失函数除了可以用Lambda层,也可以定义Layer层,这是个没有权重的自定义Layer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 官方示例:Custom loss layer
class CustomVariationalLayer(Layer):
def __init__(self, **kwargs):
self.is_placeholder = True
super(CustomVariationalLayer, self).__init__(**kwargs)

def vae_loss(self, x, x_decoded_mean):
xent_loss = original_dim * metrics.binary_crossentropy(x, x_decoded_mean)#Square Loss
kl_loss = - 0.5 * K.sum(1 + z_log_var - K.square(z_mean) - K.exp(z_log_var), axis=-1)# KL-Divergence Loss
return K.mean(xent_loss + kl_loss)

def call(self, inputs):
x = inputs[0]
x_decoded_mean = inputs[1]
loss = self.vae_loss(x, x_decoded_mean)
self.add_loss(loss, inputs=inputs)
# We won't actually use the output.
return x

4.3 补充

call方法的完整参数:call(self, inputs, args, *kwargs)

  • 其中inputs就是层输入,tensor/tensors
  • 除此之外还有两个reserved keyword arguments:training&mask,一个用于bn/dropout这种train/test计算有区别的flag,一个用于RNNlayers约束时序相关关系
  • args和*kwargs是预留为了以后扩展更多输入参数的

4.4 更加flexible的自定义层:https://keras.io/guides/making_new_layers_and_models_via_subclassing/

我们可以将trainable variable直接定义在__init__()里面,省略build,直接call:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# use tf.Variable
class Linear(Layer):
def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()
w_init = tf.random_normal_initializer()
self.w = tf.Variable(
initial_value=w_init(shape=(input_dim, units), dtype="float32"),
trainable=True,
)
b_init = tf.zeros_initializer()
self.b = tf.Variable(
initial_value=b_init(shape=(units,), dtype="float32"), trainable=True
)

def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b

# use self.add_weight方法
class Linear(Layer):
def __init__(self, units=32, input_dim=32):
super(Linear, self).__init__()
self.w = self.add_weight(
shape=(input_dim, units), initializer="random_normal", trainable=True
)
self.b = self.add_weight(shape=(units,), initializer="zeros", trainable=True)

def call(self, inputs):
return tf.matmul(inputs, self.w) + self.b


!!!我实验下来发现对tf1,第一种写法没法创建权重,第二种才能,可能要到tf2才行

build中add_weight实际上也是调用tf.Variable创建一个varaible

4.5 Layer内部也可以创建a Layer instance作为其attribute——torch style

也是放在__init__()里面,如learnable positional embedding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MLPBlock(Layer):
def __init__(self):
super(MLPBlock, self).__init__()
self.linear_1 = Linear(32)
self.linear_2 = Linear(32)
self.linear_3 = Linear(1)

def call(self, inputs):
x = self.linear_1(inputs)
x = tf.nn.relu(x)
x = self.linear_2(x)
x = tf.nn.relu(x)
return self.linear_3(x)


mlp = MLPBlock()
y = mlp(tf.ones(shape=(3, 64))) # The first call to the `mlp` will create the weights
print("weights:", len(mlp.weights))
print("trainable weights:", len(mlp.trainable_weights))

!!!tf2才支持,别瞎搞

!!!需要注意的是:用躲避build的方法创建的层,在层被fisrt call的时候才会创建权重,而不是在模型创建阶段

5. keras Generator

本质上就是python的生成器,每次返回一个batch的样本及标签
自定义generator的时候要写成死循环(while true),因为model.fit_generator()在使用在个函数的时候,并不会在每一个epoch之后重新调用,那么如果这时候generator自己结束了就会有问题。
栗子是我为mixup写的generator:
没有显示的while True是因为创建keras自带的generator的时候已经是死循环了(for永不跳出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def Datagen_mixup(data_path, img_size, batch_size, is_train=True, mix_prop=0.8, alpha=1.0):
if is_train:
datagen = ImageDataGenerator()
else:
datagen = ImageDataGenerator()

# using keras库函数
generator = datagen.flow_from_directory(data_path, target_size=(img_size, img_size),
batch_size=batch_size,
color_mode="grayscale",
shuffle=True)

for x,y in generator: # a batch of <img, label>
if alpha > 0:
lam = np.random.beta(alpha, alpha)
else:
lam = 1
idx = [i for i in range(x.shape[0])]
random.shuffle(idx)
mixed_x = lam*x + (1-lam)*x[idx]
mixed_y = lam*y + (1-lam)*y[idx]

n_origin = int(batch_size * mix_prop)
gen_x = np.vstack(x[:n_origin], mixed_x[:(batch_size-n_origin)])
gen_y = np.vstack(y[:n_origin], mixed_y[:(batch_size-n_origin)])

yield gen_x, gen_y

【多进程】fit_generator中有一个参数use_multiprocessing,默认设置为false,因为‘using a generator with use_multiprocessing=True and multiple workers may duplicate your data. Please consider using the`keras.utils.Sequence’ class’

如果设置多进程use_multiprocessing,代码会把你的数据复制几份,分给不同的workers进行输入,这显然不是我们希望的,我们希望一份数据直接平均分给多个workers帮忙输入,这样才是最快的。而Sequence数据类能完美解决这个问题。

keras.utils.Sequence():

  • 每一个 Sequence 必须实现 __getitem__ 和 __len__ 方法
  • __getitem__ 方法应该范围一个完整的批次
  • 如果你想在迭代之间修改你的数据集,你可以实现 on_epoch_end(会在每个迭代之间被隐式调用)\
  • github上有issue反映on_epoch_end不会没调用,解决方案:在__len__方法中显示自行调用

直接看栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import keras
import math
import os
import cv2
import numpy as np
from keras.applications import ResNet50
from keras.optimizers import SGD


class DataGenerator(keras.utils.Sequence):

def __init__(self, data, batch_size=1, shuffle=True):
self.batch_size = batch_size
self.data = data
self.indexes = np.arange(len(self.data))
self.shuffle = shuffle

def __len__(self):
# 计算每一个epoch的迭代次数
return math.ceil(len(self.data) / float(self.batch_size))

def __getitem__(self, index):
# 生成每个batch数据
batch_indices = self.indexes[index*self.batch_size:(index+1)*self.batch_size]
batch_data = [self.data[k] for k in batch_indices]

x_batch, y_batch = self.data_generation(batch_data)
return x_batch, y_batch

def on_epoch_end(self):
if self.shuffle == True:
np.random.shuffle(self.indexes)

def data_generation(self, batch_data):
images = []
labels = []

# 生成数据
for i, data in enumerate(batch_data):
image = cv2.imread(data, 0)
image = cv2.resize(image, dsize=(64,64), interpolation=cv2.INTER_LINEAR)
if np.max(image)>1:
image = image / 255.
image = np.expand_dims(image, axis=-1)
images.append(image)
if 'd0' in data:
labels.append([1,0])
else:
labels.append([0,1])
return np.array(images), np.array(labels)


if __name__ == '__main__':

# data
data_dir = "/Users/amber/dataset/mnist"
data_lst = []
for file in os.listdir(data_dir+"/d0")[:]:
data_lst.append(os.path.join(data_dir, "d0", file))
for file in os.listdir(data_dir+"/d1")[:]:
data_lst.append(os.path.join(data_dir, "d1", file))
training_generator = DataGenerator(data_lst, batch_size=128)

# model
model = ResNet50(input_shape=(64,64,1),weights=None, classes=2)
model.compile(optimizer=SGD(1e-3), loss='categorical_crossentropy', metrics=['accuracy'])

model.fit_generator(training_generator, epochs=50,max_queue_size=200,workers=2)

经验值:

  • workers:2/3
  • max_queue_size:默认10,具体基于GPU处于空闲状态适量调节

【附加】实验中还发现一个问题,最开始定义了一个sequential model,然后在调用fit_generator一直报错:model not compile,但是显然model是compile过了的,网上查到的解释:‘Sequential model works with model.fit but not with model.fit_generator’

6. 多GPU

多GPU运行分为两种情况:

* 数据并行
* 设备并行

6.1 数据并行

数据并行将目标模型在多个GPU上各复制一份,使用每个复制品处理数据集的不同部分。

一个栗子:写tripleNet模型时,取了batch=4,总共15类,那么三元组总共有$(4/2)^2*15=60$个,训练用了224的图像,单张GPU内存会溢出,因此需要单机多卡数据并行。

​ step1. 在模型定义中,用multi_gpu_model封一层,需要在model.compile之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from keras.util import multi_gpu_model

def triple_model(input_shape=(512,512,1), n_classes=10, multi_gpu=False):
anchor_input = Input(shape=input_shape)
positive_input = Input(shape=input_shape)
negative_input = Input(shape=input_shape)

sharedCNN = base_model(input_shape)
encoded_anchor = sharedCNN(anchor_input)
encoded_positive = sharedCNN(positive_input)
encoded_negative = sharedCNN(negative_input)

# class branch
x = Dense(n_classses, activation='softmax')(encoded_anchor)

# distance branch
encoded_anchor = Activation('sigmoid')(encoded_anchor)
encoded_positive = Activation('sigmoid')(encoded_positive)
encoded_negative = Activation('sigmoid')(encoded_negative)
merged = concatenate([encoded_anchor, encoded_positive, encoded_negative], axis=-1, name='tripleLossLayer')

model = Model(inputs=[anchor_input,positive_input,negative_input], outputs=[x, merged])
if multi_gpu:
model = multi_gpu_model(model, GPU_COUNT)

model.compile(optimizer=SGD, loss=[cls_loss, triplet_loss], metrics=[cls_acc])

return model

​ step2. 在定义checkpoint时,要用ParallelModelCheckpoint封一层,初始化参数的model要传原始模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from keras.callbacks import ModelCheckpoint

class ParallelModelCheckpoint(ModelCheckpoint):
def __init__(self,single_model,multi_model, filepath, monitor='val_loss', verbose=0,
save_best_only=False, save_weights_only=False,
mode='auto', period=1):
self.single_model = single_model
self.multi_model = multi_model
super(ParallelModelCheckpoint,self).__init__(filepath, monitor, verbose,save_best_only, save_weights_only,mode, period)

def set_model(self, model):
self.single_model.optimizer = self.multi_model.optimizer
super(ParallelModelCheckpoint,self).set_model(self.single_model)

def on_epoch_end(self, epoch, logs=None):
# save optimizer weights
self.single_model.optimizer = self.multi_model.optimizer
super(ParallelModelCheckpoint, self).on_epoch_end(epoch, logs)

model = triple_model(multi_gpu=True)
single_model = triple_model(multi_gpu=False)
filepath = "./tripleNet_{epoch:02d}_val_loss_{val_loss:.3f}.h5"
check_point = ParallelModelCheckpoint(single_model, filepath)

​ step3. 在保存权重时,通过cpu模型来保存。

1
2
3
4
5
6
7
# 实例化基础模型,这样定义模型权重会存储在CPU内存中
with tf.device('/cpu:0'):
model = Resnet50(input_shape=(512,512,3), classes=4, weights=None)

parallel_model = multi_gpu_model(model, GPU_COUNT)
parallel_model.fit(x,y, epochs=20, batch_size=32)
model.save('model.h5')

​ 【attention】同理,在load权重时,也是load单模型的权重,再调用multi_gpu_model将模型复制到多个GPU上:

1
2
3
4
5
model = Model(inputs=[anchor_input,positive_input,negative_input], outputs=[x, merged])
if multi_gpu:
if os.path.exists(weight_pt):
model.load_weights(weight_pt)
model = multi_gpu_model(model, GPU_COUNT)

​ step4. 如果保存成了多GPU权重,可以如下代码解封:

1
2
3
4
5
6
model = EfficientV2(input_shape=380, n_classes=2)
model = multi_gpu_model(model, 2)
model.load_weigts('multi.h5')

single_model = model.layers[-2]
single_model.save_weights('single.h5')

【ATTENTION】实验中发现一个问题:在有些case中,我们使用了自定义loss作为网络的输出,此时网络的输出是个标量,但是在调用multi_gpu_model这个方法时,具体实现在multi_gpu_utils.py中,最后一个步骤要merge几个device的输出,通过axis=0的concat实现,网络输出是标量的话就会报错——list assignment index out of range。

尝试的解决方案是改成相加:

1
2
3
4
5
6
7
# Merge outputs under expected scope.
with tf.device('/cpu:0' if cpu_merge else '/gpu:%d' % target_gpu_ids[0]):
merged = []
for name, outputs in zip(output_names, all_outputs):
merged.append(Lambda(lambda x: K.sum(x))(outputs))
# merged.append(concatenate(outputs, axis=0, name=name))
return Model(model.inputs, merged)

【ATTENTION++】网络的输出不能是标量!!永远会隐藏保留一个batch dim,之前是写错了!!

  • model loss是一个标量
  • 作为输出层的loss是保留batch dim的!!

6.2 设备并行

设备并行适用于多分支结构,一个分支用一个GPU。通过使用TensorFlow device scopes实现。

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Model where a shared LSTM is used to encode two different sequences in parallel
input_a = keras.Input(shape=(140, 256))
input_b = keras.Input(shape=(140, 256))

shared_lstm = keras.layers.LSTM(64)

# Process the first sequence on one GPU
with tf.device_scope('/gpu:0'):
encoded_a = shared_lstm(tweet_a)
# Process the next sequence on another GPU
with tf.device_scope('/gpu:1'):
encoded_b = shared_lstm(tweet_b)

# Concatenate results on CPU
with tf.device_scope('/cpu:0'):
merged_vector = keras.layers.concatenate([encoded_a, encoded_b],axis=-1)

7. 库函数讲解

7.1 BatchNormalization(axis=-1)

用于在每个batch上将前一层的激活值重新规范化,即使得其输出数据的均值接近0,其标准差接近1。

常用参数axis:指定要规范化的轴,通常为特征轴,如在“channels_first”的data format下,axis=1,反之axis=-1。

7.2 LSTM

参数:

  • units:输出维度(最后一维),标准输入NxTxD,N for batch,T for time-step,D for vector-dimension。
  • activation:激活函数
  • recurrent_activation:用于循环时间步的激活函数
  • dropout:在 0 和 1 之间的浮点数。 单元的丢弃比例,用于输入的线性转换
  • recurrent_dropout:在 0 和 1 之间的浮点数。 单元的丢弃比例,用于循环层状态的线性转换
  • return_sequences: 布尔值,默认False。是返回输出序列中的最后一个输出,还是全部序列的输出。即many-to-one还是many-to-many,简单来讲,当我们需要时序输出(many-to-many)的时候,就set True。
  • return_state: 布尔值,默认False。除了输出之外是否返回最后一个状态(cell值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# return_sequences
inputs1 = Input(tensor=(1,3, 1))
lstm1 = LSTM(1, return_sequences=True)(inputs1)
'''
输出结果为
[[[-0.02243521]
[-0.06210149]
[-0.11457888]]]
表示每个time-step,LSTM cell的输出
'''

# return_state
lstm1, state_h, state_c = LSTM(1, return_state=True)(inputs1)
'''
输出结果为
[array([[ 0.10951342]], dtype=float32),
array([[ 0.10951342]], dtype=float32),
array([[ 0.24143776]], dtype=float32)]
list中依次为网络输出,最后一个time-step的LSTM cell的输出值和cell值
'''

7.2.5 TimeDistributed

顺便再说下TimeDistributed,当我们使用many-to-many模型,最后一层LSTM的输出维度为k,而我们想要的最终输出维度为n,那么就需要引入Dense层,对于时序模型,我们要对每一个time-step引入dense层,这实质上是多个Dense操作,那么我们就可以用TimeDistributed来包裹Dense层来实现。

1
2
3
model = Sequential()
model.add(LSTM(3, input_shape=(length, 1), return_sequences=True))
model.add(TimeDistributed(Dense(1)))

官方文档:这个封装器将一个层应用于输入的每个时间片。

  • 当该层作为第一层时,应显式说明input_shape
  • TimeDistributed可以应用于任意层,如Conv3D:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 例如我的crnn model
def crnn(input_shape, cnn, n_classes=24):

inpt = Input(input_shape)

x = TimeDistributed(cnn, input_shape=input_shape)(inpt)
x = LSTM(128, return_sequences=True)(x)
x = LSTM(256, return_sequences=True)(x)

x = TimeDistributed(Dense(n_classes))(x)

model = Model(inpt, x)
model.summary()

return model

crnn_model = crnn((24,128,128,128,2), cnn_model)

7.3 Embedding

用于将稀疏编码映射为固定尺寸的密集表示。

输入形如(samples,sequence_length)的2D张量,输出形如(samples, sequence_length, output_dim)的3D张量。

参数:

  • input_dim:字典长度,即输入数据最大下标+1
  • output_dim:
  • input_length:

栗子:

1
2
3
4
5
6
7
8
9
10
11
12
# centerloss branch
lambda_c = 1
input_ = Input(shape=(1,))
centers = Embedding(10,2)(input_) # (None, 1, 2)
# 这里的输入是0-9的枚举(dim=10),然后映射成一个簇心
intra_loss = Lambda(lambda x:K.sum(K.square(x[0]-x[1][:,0]),1,keepdims=True))([out1,centers])
model_center_loss = Model([inputs,input_],[out2,intra_loss])
model_center_loss.compile(optimizer="sgd",
loss=["categorical_crossentropy",lambda y_true,y_pred:y_pred],
loss_weights=[1,lambda_c/2.],
metrics=["acc"])
model_center_loss.summary()

7.4 plot_model

1
2
from keras.utils import plot_model
plot_model(model, to_file='model.png', show_shapes=False, show_layer_names=True)

7.5 K.function

获取模型某层的输出,一种方法是创建一个新的模型,使它的输出是目标层,然后调用predict。

1
2
3
4
5
6
model = ...    # the original model

new_model = Model(input=model.input,
output=model.get_layer('my_layer').output)

intermediate_output = new_model.predict(input_data)

也可以创建一个函数来实现:keras.backend.function(inputs, outputs, updates=None)

1
2
3
4
5
6
# 这是写center-loss时写的栗子:
func = K.function(inputs=[model.input[0]],
outputs=[model.get_layer('out1').output])
# model.input[0]: one input of the multi-input model

test_features = func([x_test])[0]

7.6 K.gradients(y,x)

求y关于x的导数,y和x可以是张量/张量列表。返回张量列表,列表长度同x列表,列表中元素shape同x列表中元素。

对于$y=[y_1, y_2], x=[x_1, x_2, x_3]$,有返回值$[grad_1, grad_2, grad_3]$,真实的计算过程为:

7.7 ModelCheckpoint、ReduceLROnPlateau、EarlyStopping、LearningRateScheduler、Tensorboard

  • 模型检查点ModelCheckpoint

    1
    2

    ModelCheckpoint(filepath, monitor='val_loss', verbose=0, save_best_only=False, save_weights_only=False, mode='auto', period=1)
    • filepath可以由 epoch 的值和 logs 的键来填充,如weights.{epoch:02d}-{val_loss:.2f}.hdf5。
    • moniter:被监测的数据
    • mode:在 auto 模式中,方向会自动从被监测的数据的名字(不靠谱🤷‍♀️)中判断出来。
  • 学习率衰减ReduceLROnPlateau

    学习率的方案相对简单,要么在验证集的损失或准确率开始稳定时调低学习率,要么在固定间隔上调低学习率。

    1
    ReduceLROnPlateau(monitor='val_loss', factor=0.1, patience=10, verbose=0, mode='auto', min_delta=0.0001, cooldown=0, min_lr=0)

    当学习停止时,模型总是会受益于降低 2-10 倍的学习速率。

    • moniter:被监测的数据
    • factor:新的学习速率 = 学习速率 * factor
    • patience:被监测数据没有进步的训练轮数,在这之后训练速率会被降低。
  • 更复杂的学习率变化模式定义LearningRateScheduler

    前提是只需要用到默认参数是epoch

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 首先定义一个变化模式
    def warmup_scheduler(epoch, mode='power_decay'):
    lr_base = 1e-5
    lr_stable = 1e-4

    lr = lr_base * math.pow(10, epoch)
    if lr>lr_stable:
    return lr_stable
    else:
    return lr

    # 然后调用LearningRateScheduler方法wrapper这个scheduler
    scheduler = LearningRateScheduler(warmup_scheduler)

    # 在使用的时候放在callbacks的list里面,在每个epoch结束触发
    callbacks = [checkpoint, reduce_lr, scheduler, early_stopping]
  • 更更复杂的学习率变化模式定义可以直接继承Callback:https://kexue.fm/archives/5765

    • 当我们需要传入更丰富的自定义参数/需要进行by step的参数更新等,可以直接继承Callback,进行更自由的自定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    # 以余弦退火算法为例:
    class CosineAnnealingScheduler(Callback):
    """Cosine annealing scheduler.
    """

    def __init__(self, epochs, scale=1.6, shift=0, verbose=0):
    super(CosineAnnealingScheduler, self).__init__()
    self.epochs = epochs
    self.scale = scale
    self.shift = shift
    self.verbose = verbose

    def on_epoch_begin(self, epoch, logs=None):
    if epoch<=6:
    # linearly increase from 0 to 1.6 in first 5 epochs
    lr = 1.6 / 5 * (epoch+1)
    else:
    # cosine annealing
    lr = self.shift + self.scale * (1 + math.cos(math.pi * (epoch+1-5) / self.epochs)) / 2
    K.set_value(self.model.optimizer.lr, lr)
    if self.verbose > 0:
    print('\nEpoch %05d: CosineAnnealingScheduler setting learning rate to %s.' % (epoch+1, lr))

    def on_epoch_end(self, epoch, logs=None):
    logs = logs or {}
    logs['lr'] = K.get_value(self.model.optimizer.lr)


    # 调用
    lrscheduler = CosineAnnealingScheduler(epochs=2, verbose=1)
    callbacks = [checkpoint, lrscheduler]
    model.fit(...,
    callbacks=callbacks)
  • 有一些计算指标,不好写成张量形式,也可以放在Callback器里面,想咋写就咋写

    说白了就是on_epoch_end里面的数据是array,而不是tensor,比较好写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    from keras.callbacks import Callback


    # 定义Callback器,计算验证集的acc,并保存最优模型
    class Evaluate(Callback):

    def __init__(self):
    self.accs = []
    self.highest = 0.

    def on_epoch_end(self, epoch, logs=None):
    ###### 自由发挥区域
    pred = model.predict(x_test)
    acc = np.mean(pred.argmax(axis=1) == y_test)
    ########
    self.accs.append(acc)
    if acc >= self.highest: # 保存最优模型权重
    self.highest = acc
    model.save_weights('best_model.weights')

    print('acc: %s, highest: %s' % (acc, self.highest))


    evaluator = Evaluate()
    model.fit(x_train,
    y_train,
    epochs=10,
    callbacks=[evaluator])
  • Callback类共支持六种在不同阶段的执行函数:

    • on_epoch_begin:warmup
    • on_epoch_end:metrics
    • on_batch_begin
    • on_batch_end
    • on_train_begin
    • on_train_end
  • 提前停止训练EarlyStopping

    1
    EarlyStopping(monitor='val_loss', min_delta=0, patience=0, verbose=0, mode='auto', baseline=None, restore_best_weights=False)
    • moniter:被监测的数据
    • patience:被监测数据没有进步的训练轮数,在这之后训练速率会被降低。
    • min_delta:在被监测的数据中被认为是提升的最小变化,小于 min_delta 的绝对变化会被认为没有提升。
    • baseline: 要监控的数量的基准值。
  • 以上这四个都是继承自keras.callbacks()

  • 可视化工具TensorBoard

    这个回调函数为 Tensorboard 编写一个日志, 这样你可以可视化测试和训练的标准评估的动态图像, 也可以可视化模型中不同层的激活值直方图。

    1
    TensorBoard(log_dir='./logs', histogram_freq=0, batch_size=32, write_graph=True, write_grads=False, write_images=False, embeddings_freq=0, embeddings_layer_names=None, embeddings_metadata=None, embeddings_data=None, update_freq='epoch')

    实际使用时关注第一个参数log_dir就好,查看时通过命令行启动:

    1
    tensorboard --logdir=/full_path_to_your_logs

这几个回调函数,通通在训练时(model.fit / fit_generator)放在callbacks关键字里面。

7.8 反卷积 Conv2DTranspose

三个核心的参数filtes、kernel_size、strides、padding=’valid’

  • filtes:输出通道数
  • strides:步长
  • kernel_size:一般需要通过上面两项计算得到

反卷积运算和正向卷积运算保持一致,即:

1
2
3
4
# fcn example: current feature map x (,32,32,32), input_shape (512,512,2), output_shape (,512,512,1)
strides = 2
kernel_size = input_shape[0] - (x.get_shape().as_list()[1] - 1)*strides
y = Conv2DTranspose(1, kernel_size, padding='valid', strides=strides)

7.9 K.shape & K.int_shape & tensor._keras_shape

  • tensor._keras_shape等价于K.int_shape:张量的shape,返回值是个tuple
  • K.shape:返回值是个tensor,tensor是个一维向量,其中每一个元素可以用[i]来访问,是个标量tensor

两个方法的主要区别是:前者返回值是个常量,只能表征语句执行时刻(如构建图)tensor的状态,后者返回值是个变量,wrapper的方法可以看成一个节点,在graph的作用域内始终有效,在构建图的时候可以是None,在实际流入数据流的时候传值就行,如batch_size!!!

1
2
3
4
5
6
7
8
9
10
11
import keras.backend as K
from keras.layers import Input

x = Input((22,22,1))
print(K.shape(x))
# Tensor("Shape:0", shape=(4,), dtype=int32)
print(K.shape(x)[0])
# Tensor("strided_slice:0", shape=(), dtype=int32)

print(K.int_shape(x))
# (None, 22, 22, 1)

7.10 binary_crossentropy(y_true, y_pred, from_logits=False, label_smoothing=0)

  • from_logits:logits表示网络的直接输出——没经过sigmoid或者softmax的概率化,默认情况下,我们认为y_pred是已经处理过的概率分布

8. 衍生:一些tf函数

8.1 tf.where(condition, x=None, y=None,name=None)

两种用法:

  • 如果x,y为空,返回值是满足condition元素的索引,每个索引占一行。
  • 如果x,y不为空,那么condition、x、y 和返回值相同维度,condition为True的位置替换x中对应元素,condition为False的位置替换y中对应元素。

关于索引indices:

  • condition的shape的dim,就是每一行索引vector的shape,例:

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import tensorflow as tf
    import numpy as np

    condition1 = np.array([[True,False,False],[False,True,True]])
    print(condition1.shape) # (2,3)
    with tf.compat.v1.Session() as sess:
    print(sess.run(tf.where(condition1)))
    # [[0 0]
    # [1 1]
    # [1 2]]
    # condition是2x3的arr,也就是dim=2,那么索引vector的shape就是2,纵轴的shape是满足cond的数量
  • 索引通常与tf.gather和tf.gather_nd搭配使用:

    • tf.gather(params,indices,axis=0.name=None):tf.gather只能接受1-D的索引,axis用来指定轴,一个索引取回对应维度的一个向量
    • tf.gather_nd(params,indices):tf.gather_nd可以接受多维的索引,如果索引的dim小于params的dim,则从axis=0开始索引,后面的取全部。
    • tf.batch_gather(params,indices,name=None):对张量的批量索引

8.2 tf.Print()

相当于一个节点,定义了数据的流入和流出。

一个error:在模型定义中,直接调用:

1
2
3
4
intra_distance = tf.Print(intra_distance,
[intra_distance],
message='Debug info: ',
summarize=10)

会报错:AttributeError: ‘Tensor’ object has no attribute ‘_keras_history’

参考:https://stackoverflow.com/questions/56096399/creating-model-throws-attributeerror-tensor-object-has-no-attribute-keras

You cannot use backend functions directly in Keras tensors, every operation in these tensors must be a layer. You need to wrap each custom operation in a Lambda layer and provide the appropriate inputs to the layer.

之前一直没注意到这个问题,凡是调用了tf.XXX的operation,都要wrapper在Lambda层里。

改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# wrapper function
def debug(args):
intra_distance, min_inter_distance = args
intra_distance = tf.Print(intra_distance,
[intra_distance],
message='Debug info: ',
summarize=10)
min_inter_distance = tf.Print(min_inter_distance,
[min_inter_distance],
message='Debug info: ',
summarize=10)
return [intra_distance, min_inter_distance]

# 模型内
intra_distance, min_inter_distance = Lambda(debug)([intra_distance, min_inter_distance])

【夹带私货】tf.Print同时也可以打印wrapper function内的中间变量,都放在列表里面就可以了。

8.3 tf.while_loop(cond, body, init_value)

tensorflow中实现循环的语句

  • 终止条件cond:是一个函数
  • 循环体body:是一个函数
  • init_value:是一个list,保存循环相关参数
  1. cond、body的参数是要与init_value列表中变量一一对应的
  2. body返回值的格式要与init_value变量一致(tensor形状保持不变)
  3. 若非要变怎么办(有时候我们希望在while_loop的过程中,维护一个list)?动态数组TensorArray/高级参数shape_invariants

8.3.1 动态数组

1
2
3
4
5
# 定义
b_boxes = tf.TensorArray(K.dtype(boxes), size=1, dynamic_size=True, clear_after_read=False)

# 写入指定位置
b_boxes = b_boxes.write(b, boxes_)

​ tensor array变量中一个位置只能写入一次

8.3.2 shape_invariants

​ reference stackoverflow

1
2
3
4
5
6
7
8
9
10
i = tf.constant(0)
l = tf.Variable([])

def body(i, l):
temp = tf.gather(array,i)
l = tf.concat([l, [temp]], 0)
return i+1, l

index, list_vals = tf.while_loop(cond, body, [i, l],
shape_invariants=[i.get_shape(), tf.TensorShape([None])])

​ 在while_loop中显示地指定参数的shape,上面的例子用了tf.TensorShape([None])令其自动推断,而不是固定检查,因此可以解决变化长度列表。

一个完整的栗子:第一次见while_loop,在yolo_loss里面

  1. 基于batch维度做遍历
  2. loop结束后将动态数据stack起来,重获batch dim
1
2
3
4
5
6
7
8
9
10
11
12
13
# Find ignore mask, iterate over each of batch.
# extract the elements on the mask which has iou < ignore_thresh
ignore_mask = tf.TensorArray(K.dtype(y_true[0]), size=1, dynamic_size=True) # 动态size数组
object_mask_bool = K.cast(object_mask, 'bool')
def loop_body(b, ignore_mask):
true_box = tf.boolean_mask(y_true[l][b,...,0:4], object_mask_bool[b,...,0]) # (H,W,3,5)
iou = box_iou(pred_box[b], true_box) # (H,W,3,1)
best_iou = K.max(iou, axis=-1)
ignore_mask = ignore_mask.write(b, K.cast(best_iou<ignore_thresh, K.dtype(true_box)))
return b+1, ignore_mask
_, ignore_mask = K.control_flow_ops.while_loop(lambda b,*args: b<m, loop_body, [0, ignore_mask])
ignore_mask = ignore_mask.stack()
ignore_mask = K.expand_dims(ignore_mask, -1) # (N,H,W,3,1)

8.4 tf.image.non_max_suppression()

非最大值抑制:贪婪算法,按scores由大到小排序,选定第一个,依次对之后的框求iou,删除那些和选定框iou大于阈值的box。

1
2
3
4
5
# 返回是被选中边框在参数boxes中的下标位置
selected_indices=tf.image.non_max_suppression(boxes, scores, max_output_size, iou_threshold=0.5, name=None)

# 根据indices获取边框
selected_boxes=tf.gather(boxes,selected_indices)
  • boxes:2-D的float类型的,大小为[num_boxes,4]的张量
  • scores:1-D的float类型的,大小为[num_boxes],对应的每一个box的一个score
  • max_output_size:标量整数Tensor,输出框的最大数量
  • iou_threshold:浮点数,IOU阈值
  • selected_indices:1-D的整数张量,大小为[M],留下来的边框下标,M小于等于max_output_size

【拓展】还有Multi-class version of NMS——tf.multiclass_non_max_suppression()

8.5 限制GPU用量

  • linux下查看GPU使用情况,1秒刷新一次:

    1
    watch -n 1 nvidia-smi
  • 指定显卡号

    1
    2
    import os
    os.environ["CUDA_VISIBLE_DEVICES"] = "2"
  • 限制GPU用量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import tensorflow as tf
    import keras.backend as K

    # 设置百分比
    config = tf.ConfigProto()
    config.gpu_options.per_process_gpu_memory_fraction = 0.3
    session = tf.Session(config=config)
    K.set_session(session)

    # 设置动态申请
    config = tf.ConfigProto()
    config.gpu_options.allow_growth=True #不全部占满显存, 按需分配
    session = tf.Session(config=config)
    K.set_session(session)

8.6 tf.boolean_mask()

tf.boolean_mask(tensor,mask,name=’boolean_mask’,axis=None)

其中,tensor是N维度的,mask是K维度的,$K \leq N$

axis表示mask的起始维度,被mask的维度只保留mask为True的数据,同时这部分数据flatten成一维,最终tensor的维度是N-K+1

栗子:yolov3里面,把特征图上有object的grid提取出来:

1
2
3
# y_trues: [b,h,w,a,4]
# conf_gt: [b,h,w,a,1]
true_box = tf.boolean_mask(y_trues[i][b,...,0:4], conf_gt[b,...,0])

8.7 tf.clip

梯度阶段,用来在optmizer之前修正梯度,主要有以下4个方法:

— tf.clip_by_value

— tf.clip_by_norm

— tf.clip_by_global_norm

— tf.clip_by_average_norm

  • tf.clip_by_value(t, clip_value_min, clip_value_max, name=None)
    • 输入single tensor:t
    • t中任意大于或者小于相应阈值的value都被截断
    • 然后返回这个截断后的t
  • tf.clip_by_norm(t, clip_norm, axes=None, name=None)
    • 输入single tensor:t
    • 首先计算它的l2norm
    • 如果l2norm(t)>clip_norm,计算t * clip_norm / l2norm(t),否则不改变t
    • 不指定axes默认计算all dimension的l2 norm(得到一个标量),指定的话计算对应维度(对应维度的dim变为1)
    • 返回这个修正后的t
  • tf.clip_by_global_norm(t_list, clip_norm, use_norm=None, name=None)
    • 输入是一组tensors:t_list,当list里面只有一个元素的时候,就退化成了tf.clip_by_norm
    • 首先计算global norm:sqrt(sum([l2norm(t)**2 for t in t_list])),所有梯度的l2norm的平方和的平方根
    • 对每个t,如果l2norm(t)>global_norm,计算t * clip_norm / l2norm(t),否则不改变t
    • 返回修正后的t_list,以及global_norm
  • tf.clip_by_average_norm(t, clip_norm, name=None)
    • 输入single tensor:t
    • 首先计算它的平均L2范数:l2norm_avg,【这是个啥?】
    • 如果l2norm_avg(t)>clip_norm,计算t * clip_norm / l2norm_avg(t),否则不改变t
    • 返回这个修正后的t

9. keras自定义优化器optimizer

9.1 关于梯度的优化器公共参数,用于梯度裁剪

  • clipnorm:对所有梯度进行downscale,使得梯度vector中l2范数最大为1(g * 1 / max(1, l2_norm))
  • clipvalue:对绝对值进行上下限截断

9.2 keras的Optimizier对象

  • keras的官方代码有optimizier_v1和optimizier_v2两版,分别面向tf1和tf2,v1的看起来简洁一些
  • self.updates & self.weights
    • self.updates:stores the variables that will be updated with every batch that is processed by the model in training
      • 用来保存与模型训练相关的参数(iterations、params、moments、accumulators,etc)
      • symbolic graph variable,通过K.update_add方法说明图的operation
    • self.weights:the functions that save and load optimizers will save and load this property
      • 用来保存与优化器相关的参数
      • model.save()方法中涉及include_optimizer=False,决定优化器的保存和重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class Optimizer(object):
# - 抽象类,所有真实的优化器继承自Optimizer对象
# - 提供两个用于梯度截断的公共参数

def __init__(self, **kwargs):
allowed_kwargs = {'clipnorm', 'clipvalue'}
for k in kwargs:
if k not in allowed_kwargs:
raise TypeError('Unexpected keyword argument passed to optimizer: ' + str(k))
# checks that clipnorm >= 0 and clipvalue >= 0
if kwargs[k] < 0:
raise ValueError('Expected {} >= 0, received: {}'.format(k, kwargs[k]))
self.__dict__.update(kwargs)
self.updates = [] # 计算更新的参数
self.weights = [] # 优化器带来的权重,在get_updates以后才有元素,在保存模型时会被保存

# Set this to False, indicating `apply_gradients` does not take the
# `experimental_aggregate_gradients` argument.
_HAS_AGGREGATE_GRAD = False

def _create_all_weights(self, params):
# 声明除了grads以外用于梯度更新的参数,创建内存空间,在get_updates方法中使用
raise NotImplementedError

def get_updates(self, loss, params):
# 定义梯度更新的计算方法, 更新self.updates
raise NotImplementedError

def get_config(self):
# config里面是优化器相关的参数,默认只有两个梯度截断的参数,需要根据实际优化器添加(lr、decay ...)
config = {}
if hasattr(self, 'clipnorm'):
config['clipnorm'] = self.clipnorm
if hasattr(self, 'clipvalue'):
config['clipvalue'] = self.clipvalue
return config

def get_gradients(self, loss, params):
# 计算梯度值,并在有必要时进行梯度截断
grads = K.gradients(loss, params)
if any(g is None for g in grads):
raise ValueError('An operation has `None` for gradient. '
'Please make sure that all of your ops have a '
'gradient defined (i.e. are differentiable). '
'Common ops without gradient: '
'K.argmax, K.round, K.eval.')
if hasattr(self, 'clipnorm'):
grads = [tf.clip_by_norm(g, self.clipnorm) for g in grads]
if hasattr(self, 'clipvalue'):
grads = [
tf.clip_by_value(g, -self.clipvalue, self.clipvalue)
for g in grads
]
return grads

def set_weights(self, weights):
# 给optimizer的weights用一系列np array赋值
# 没看到有调用,省略code: K.batch_set_value()

def get_weights(self):
# 获取weights的np array值
# 没看到有调用,省略code: K.batch_get_value()

@classmethod
def from_config(cls, config):
return cls(**config)

9.3 实例化一个优化器

  • based on keras.Optimizer对象
  • 主要需要重写get_updates和get_config方法
    • get_updates用来定义梯度更新的计算方法
    • get_config用来定义实例用到的参数
  • 以SoftSGD为例:
    • 每隔一定的batch才更新一次参数,不更新梯度的step梯度不清空,执行累加,从而实现batchsize的变相扩大
    • 建议搭配间隔更新参数的BN层来使用,否则BN还是基于小batchsize来更新均值和方差
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class SoftSGD(Optimizer):
# [new arg] steps_per_update: how many batch to update gradient
def __init__(self, lr=0.01, momentum=0., decay=0.,
nesterov=False, steps_per_update=2, **kwargs):
super(SoftSGD, self).__init__(**kwargs)
with K.name_scope(self.__class__.__name__):
self.iterations = K.variable(0, dtype='int64', name='iterations')
self.lr = K.variable(lr, name='lr')
self.steps_per_update = steps_per_update # 多少batch才更新一次
self.momentum = K.variable(momentum, name='momentum')
self.decay = K.variable(decay, name='decay')
self.initial_decay = decay
self.nesterov = nesterov

def get_updates(self, loss, params):
# learning rate decay
lr = self.lr
if self.initial_decay > 0:
lr = lr * (1. / (1. + self.decay * K.cast(self.iterations, K.dtype(self.decay))))

shapes = [K.int_shape(p) for p in params]
sum_grads = [K.zeros(shape) for shape in shapes] # 平均梯度,用来梯度下降
grads = self.get_gradients(loss, params) # 当前batch梯度
self.updates = [K.update_add(self.iterations, 1)]
self.weights = [self.iterations] + sum_grads
for p, g, sg in zip(params, grads, sum_grads):
# momentum 梯度下降
v = self.momentum * sg / float(self.steps_per_update) - lr * g # velocity
if self.nesterov:
new_p = p + self.momentum * v - lr * sg / float(self.steps_per_update)
else:
new_p = p + v

# 如果有约束,对参数加上约束
if getattr(p, 'constraint', None) is not None:
new_p = p.constraint(new_p)

# 满足条件才更新参数
cond = K.equal(self.iterations % self.steps_per_update, 0)
self.updates.append(K.switch(cond, K.update(p, new_p), p))
self.updates.append(K.switch(cond, K.update(sg, g), K.update(sg, sg + g)))
return self.updates

def get_config(self):
config = {'lr': float(K.get_value(self.lr)),
'steps_per_update': self.steps_per_update,
'momentum': float(K.get_value(self.momentum)),
'decay': float(K.get_value(self.decay)),
'nesterov': self.nesterov
}
base_config = super(SoftSGD, self).get_config()
return dict(list(base_config.items()) + list(config.items()))

10. keras自定义激活函数activation

10.1 定义激活函数

1
2
3
def gelu(x):
cdf = 0.5 * (1.0 + tf.erf(x / tf.sqrt(2.0)))
return x*cdf

10.2 使用自定义激活函数

  • 使用Activation方法

    1
    x = Activation(gelu)(x)
  • 不能整合进带有activation参数的层(如Conv2D),因为Conv基类的get_config()方法从keras.activations里面读取相应的激活函数,其中带参数的激活函数如PReLU(Advanced activations)、以及自定义的激活函数都不在这个字典中,否则会报错:

    AttributeError: ‘Activation’ object has no attribute ‘name‘

10.3 checkpoint issue

网上还有另一种写法:

1
2
3
4
5
6
7
8
9
10
11
from keras.layers import Activation
from keras.utils.generic_utils import get_custom_objects

def gelu(x):
cdf = 0.5 * (1.0 + tf.erf(x / tf.sqrt(2.0)))
return x*cdf

get_custom_objects().update({'gelu': Activation(gelu)})

# 后面可以通过名字调用激活函数
x = Activation('gelu')(x)

这种写法在使用ModelCheckpoints方法保存权重时会报错:

AttributeError: ‘Activation’ object has no attribute ‘name‘

看log发现当使用名字代表激活层的时候,在保存模型的时候,又会有一个get_config()函数从keras.activations中查表

11. keras自定义正则化器regularizers

11.1 使用封装好的regularizers

  • keras的正则化器没有global的一键添加方法,要layer-wise为每一层添加

  • keras的层share 3 common参数接口:

    • kernel_regularizer
    • bias_regularizer
    • activity_regularizer
  • 可选用的正则化器

    • keras.regularizers.l1(0.01)
    • keras.regularizers.l2(0.01)
    • keras.regularizers.l1_l2(l1=0.01, l2=0.01)
  • 使用

    1
    2
    3
    4
    5
    6
    7
    8
    layer = tf.keras.layers.Dense(5, kernel_initializer='ones',
    kernel_regularizer=tf.keras.regularizers.l1(0.01),
    activity_regularizer=tf.keras.regularizers.l2(0.01))
    tensor = tf.ones(shape=(5, 5)) * 2.0
    out = layer(tensor)
    # The kernel regularization term is 0.25
    # The activity regularization term (after dividing by the batch size) is 5
    print(tf.math.reduce_sum(layer.losses)) # 5.25 (= 5 + 0.25)

11.2 custom regularizer

一般不会自定义这个东西,硬要custom的话,两种方式

  • 简单版,接口参数是weight_matrix,无额外参数,层直接调用
1
2
3
4
def my_regularizer(x):
return 1e-3 * tf.reduce_sum(tf.square(x))

layer = tf.keras.layers.Dense(5, kernel_initializer='ones', kernel_regularizer=my_regularizer)
  • 子类继承版,可以加额外参数,需要补充get_config方法,支持读写权重时的串行化
1
2
3
4
5
6
7
8
9
10
11
12
class MyRegularizer(regularizers.Regularizer):

def __init__(self, strength):
self.strength = strength

def __call__(self, x):
return self.strength * tf.reduce_sum(tf.square(x))

def get_config(self):
return {'strength': self.strength}

layer = tf.keras.layers.Dense(5, kernel_initializer='ones', kernel_regularizer=MyRegularizer(0.01))

11.3 强行global

  • 每层加起来太烦了,批量加的实质也是逐层加,只不过写成循环
  • 核心是layer的add_loss方法
1
2
3
4
5
6
7
8
model = keras.applications.ResNet50(include_top=True, weights='imagenet')
alpha = 0.00002 # weight decay coefficient

for layer in model.layers:
if isinstance(layer, keras.layers.Conv2D) or isinstance(layer, keras.layers.Dense):
layer.add_loss(lambda: keras.regularizers.l2(alpha)(layer.kernel))
if hasattr(layer, 'bias_regularizer') and layer.use_bias:
layer.add_loss(lambda: keras.regularizers.l2(alpha)(layer.bias))

12. keras查看梯度&权重

12.1 easiest way

  • 查看梯度最简单的方法:通过K.gradients方法定义一个求梯度的func,然后给定输入,得到梯度(CAM就是这么干的)
  • 查看权重最简单的方法:存在h5文件,然后花式h5py解析

12.2 dig deeper

  • 一个思路:将梯度保存在optimizer的self.weights中,并在model.save得到的模型中解析

13. keras实现权重滑动平均

13.1 why EMA on weights

  • [reference1][https://www.jiqizhixin.com/articles/2019-05-07-18]:权重滑动平均是提供训练稳定性的有效方法,要么在优化器里面实现,要么外嵌在训练代码里

  • [reference2][https://cloud.tencent.com/developer/article/1636781]:这里面举的例子很清晰了,就是为了权重每个step前后变化不大

权重EMA的计算方式有点类似于BN的running mean&var:

  • 在训练阶段:它不改变每个training step的优化方向,而是从initial weights开始,另外维护一组shadow weights,用每次的updating weights来进行滑动更新
  • 在inference阶段,我们要用shadow weights来替换当前权重文件保存的weights(current step下计算的新权重)
  • 如果要继续训练,要将替换的权重在换回来,因为【EMA不影响模型的优化轨迹】

13.2 who uses EMA

  • 很多GAN的论文都用了EMA,
  • 还有NLP阅读理解模型QANet,
  • 还有Google的efficientNet、resnet_rs

13.3 how to implement outside

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ExponentialMovingAverage:
"""对模型权重进行指数滑动平均。
用法:在model.compile之后、第一次训练之前使用;
先初始化对象,然后执行inject方法。
"""
def __init__(self, model, momentum=0.9999):
self.momentum = momentum
self.model = model
self.ema_weights = [K.zeros(K.shape(w)) for w in model.weights]
def inject(self):
"""添加更新算子到model.metrics_updates。
"""
self.initialize()
for w1, w2 in zip(self.ema_weights, self.model.weights):
op = K.moving_average_update(w1, w2, self.momentum)
self.model.metrics_updates.append(op)
def initialize(self):
"""ema_weights初始化跟原模型初始化一致。
"""
self.old_weights = K.batch_get_value(self.model.weights)
K.batch_set_value(zip(self.ema_weights, self.old_weights))
def apply_ema_weights(self):
"""备份原模型权重,然后将平均权重应用到模型上去。
"""
self.old_weights = K.batch_get_value(self.model.weights)
ema_weights = K.batch_get_value(self.ema_weights)
K.batch_set_value(zip(self.model.weights, ema_weights))
def reset_old_weights(self):
"""恢复模型到旧权重。
"""
K.batch_set_value(zip(self.model.weights, self.old_weights))
  • then train
1
2
3
4
EMAer = ExponentialMovingAverage(model) # 在模型compile之后执行
EMAer.inject() # 在模型compile之后执行

model.fit(x_train, y_train) # 训练模型
  • then inference
1
2
3
4
5
MAer.apply_ema_weights() # 将EMA的权重应用到模型中
model.predict(x_test) # 进行预测、验证、保存等操作

EMAer.reset_old_weights() # 继续训练之前,要恢复模型旧权重。还是那句话,EMA不影响模型的优化轨迹。
model.fit(x_train, y_train) # 继续训练

14. keras的Model类继承

14.1 定义模型的方式

  • Sequential:最简单,但是不能表示复杂拓扑结构
  • 函数式 API:和Sequential用法基本一致,输入张量和输出张量用于定义 tf.keras.Model实例
  • 模型子类化:引入于 Keras 2.2.0
  • keras源代码定义在:https://github.com/keras-team/keras/blob/master/keras/engine/training.py

14.2 模型子类化overview

  • 既可以用来定义一个model,也可以用来定义一个复杂的网络层,为实现复杂模型提供更大的灵活性
  • 有点类似于torch的语法
    • 网络层定义在 __init__(self, ...) 中:跟torch语法的主要区别在于层不能复用,torch同一个层在forward中每调用一次能够创建一个实例,keras每个层应该是在init中声明并创建,所以不能复用
    • 前向传播在 call(self, inputs) 中,这里面也可以添加loss
    • compute_output_shape计算模型输出的形状
  • 和keras自定义层的语法也很相似
    • build(input_shape):主要区别就在于build,因为自定义层有build,显式声明了数据流的shape,能够构造出静态图
    • call(x):
    • compute_output_shape(input_shape):
  • 【以下方法和属性不适用于类继承模型】,所以还是推荐优先使用函数式 API
    • model.inputs & model.outputs
    • model.to_yaml() & model.to_json()
    • model.get_config() & model.save():!!!只能save_weights!!!

14.3 栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import keras
import numpy as np


class SimpleMLP(keras.Model):

def __init__(self, num_classes=10):
super(SimpleMLP, self).__init__(name='mlp')
self.num_classes = num_classes
self.dense1 = keras.layers.Dense(32, activation='relu')
self.dense2 = keras.layers.Dense(num_classes, activation='softmax')
self.dp = keras.layers.Dropout(0.5)
self.bn = keras.layers.BatchNormalization(axis=-1)

def call(self, inputs, training=None, mask=None):
x = self.dense1(inputs)
x = self.dp(x)
x = self.bn(x, training=training)
return self.dense2(x)

def compute_output_shape(self, input_shape):
batch, dim = input_shape
return (batch, self.num_classes)


model = SimpleMLP()
model.compile('adam', loss='categorical_crossentropy')
x = np.random.uniform(0,1,(32,100))
y = np.random.randint(0, 2, (32,10))
model.fit(x, y)
model.summary()
  • 可以看到,类继承模型是没有指明input_shape的,所以也就不存在静态图,要在有真正数据流以后,model才被build,才能够调用summay方法,查看图结构
  • 第二个是,call方法的默认参数:def call(self, inputs, training=None, mask=None),
    • 子类继承模型不支持显式的多输入定义,所有的输入构成inputs
    • 需要手工管理training参数,bn/dropout等在train/inference mode下计算不一样的情况,要显式传入training参数
    • mask在构建Attention机制或者序列模型时会使用到,如果previous layer生成了掩码(embedding的mask_zero参数为True),前两种构建模型的方法中,mask会自动传入当前层的call方法中

14.4 get layer output

  • 有时候我们用Model类去定义一个层,以便获得更自由的表达能力,但是在inference阶段用get_layer(xxx).output去获取这个层的输出的时候会报错:multi-node… get_output_at…
  • 说这个层含有多个输出节点,需要用get_output_at来声明
    • get_output_at(0)是当前模型level的节点
    • get_output_at(1)是当前模型作为层对象的输出节点
    • 这俩本质是一样的,但是Model类就是会创建这样的副本,这可能就是目前keras复现版本的显存占用要高于pytorch版的原因

15. low-level training & evaluation loops

15.1 keras的Model类提供了build-in的train/eval方法

* fit()
* evaluate()
* predict()
* reference: https://keras.io/api/models/model_training_apis/
* reference: https://keras.io/guides/training_with_built_in_methods/

15.2 如果你想修改模型的训练过程,但仍旧通过fit()方法进行训练,Model类中提供了train_step()可以继承和重载

  • reference: https://keras.io/guides/customizing_what_happens_in_fit/

  • Model类中有一个train_step()方法,fit每个batch的时候都会调用一次

  • 在重写这个train_step()方法时

    • 传入参数data:取决于fit()方法传入的参数形式,tuple(x,y) / tf.data.Dataset
    • forward pass:self(model)
    • 计算loss:self.compiled_loss
    • 计算梯度:tf.GradientTape()
    • 更新权重:self.optimizer
    • 更新metrics:self.compiled_metrics
    • 返回值a dictionary mapping metric names
  • 栗子🌰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class CustomModel(keras.Model):
    def train_step(self, data):
    # Unpack the data
    x, y = data

    with tf.GradientTape() as tape:
    y_pred = self(x, training=True) # Forward pass
    # Compute the loss value
    # (the loss function is configured in `compile()`)
    loss = self.compiled_loss(y, y_pred, regularization_losses=self.losses)

    # Compute gradients
    trainable_vars = self.trainable_variables
    gradients = tape.gradient(loss, trainable_vars)
    # Update weights
    self.optimizer.apply_gradients(zip(gradients, trainable_vars))
    # Update metrics (includes the metric that tracks the loss)
    self.compiled_metrics.update_state(y, y_pred)
    # Return a dict mapping metric names to current value
    return {m.name: m.result() for m in self.metrics}


    import numpy as np

    # Construct and compile an instance of CustomModel
    inputs = keras.Input(shape=(32,))
    outputs = keras.layers.Dense(1)(inputs)
    model = CustomModel(inputs, outputs)
    model.compile(optimizer="adam", loss="mse", metrics=["mae"])

    # Just use `fit` as usual
    x = np.random.random((1000, 32))
    y = np.random.random((1000, 1))
    model.fit(x, y, epochs=3)
      * 注意到这里面我们调用了self.compiled_loss和self.compiled_metrics,这就是在调用compile()方法的时候传入的loss和metrics参数
    
  • get lower

    • loss和metrics也可以不传,直接在CustomModel里面声明和定义
    • 声明:重载metrics()方法,创建metric instances,用于计算loss和metrics,把他们放在这里模型会在fit()/evaluate()方法的每个epoch起始阶段调用reset_states()方法,确保loss和metrics的states都是per epoch的,而不是avg from the beginning
    • 更新:调用update_state()方法更新他们的状态参数,调用result()方法拿到他们的current value

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      loss_tracker = keras.metrics.Mean(name="loss")
      mae_metric = keras.metrics.MeanAbsoluteError(name="mae")


      class CustomModel(keras.Model):
      def train_step(self, data):
      x, y = data

      with tf.GradientTape() as tape:
      y_pred = self(x, training=True) # Forward pass
      # Compute our own loss
      loss = keras.losses.mean_squared_error(y, y_pred)

      # Compute gradients
      trainable_vars = self.trainable_variables
      gradients = tape.gradient(loss, trainable_vars)

      # Update weights
      self.optimizer.apply_gradients(zip(gradients, trainable_vars))

      # Compute our own metrics
      loss_tracker.update_state(loss)
      mae_metric.update_state(y, y_pred)
      return {"loss": loss_tracker.result(), "mae": mae_metric.result()}

      @property
      def metrics(self):
      # We list our `Metric` objects here so that `reset_states()` can be called automatically per epoch
      return [loss_tracker, mae_metric]


      # Construct an instance of CustomModel
      inputs = keras.Input(shape=(32,))
      outputs = keras.layers.Dense(1)(inputs)
      model = CustomModel(inputs, outputs)

      # We don't passs a loss or metrics here.
      model.compile(optimizer="adam")

      # Just use `fit` as usual -- you can use callbacks, etc.
      x = np.random.random((1000, 32))
      y = np.random.random((1000, 1))
      model.fit(x, y, epochs=5)
  • 相对应地,也可以定制model.evaluate()的计算过程——override test_step()方法

    • 传入参数data:取决于fit()方法传入的参数形式,tuple(x,y) / tf.data.Dataset
    • forward pass:self(model)
    • 计算loss:self.compiled_loss
    • 计算metrics:self.compiled_metrics
    • 返回值a dictionary mapping metric names

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      class CustomModel(keras.Model):
      def test_step(self, data):
      # Unpack the data
      x, y = data
      # Compute predictions
      y_pred = self(x, training=False)
      # Updates the metrics tracking the loss
      self.compiled_loss(y, y_pred, regularization_losses=self.losses)
      # Update the metrics.
      self.compiled_metrics.update_state(y, y_pred)
      # Return a dict mapping metric names to current value.
      # Note that it will include the loss (tracked in self.metrics).
      return {m.name: m.result() for m in self.metrics}


      # Construct an instance of CustomModel
      inputs = keras.Input(shape=(32,))
      outputs = keras.layers.Dense(1)(inputs)
      model = CustomModel(inputs, outputs)
      model.compile(loss="mse", metrics=["mae"])

      # Evaluate with our custom test_step
      x = np.random.random((1000, 32))
      y = np.random.random((1000, 1))
      model.evaluate(x, y)

15.3 实现完整的train loops

  • reference: https://keras.io/guides/writing_a_training_loop_from_scratch/

    • a train loop

      • a for loop:iter for each epoch
        • a for loop:iter over the dataset
           * open a `GradientTape()` scope:tensorflow的梯度API,用于给定loss计算梯度
           * Inside this scope:forward pass,compute loss
           * Outside the scope:retrieve the gradients 
           * use optimizer to update the gradients:`optimizer.apply_gradients`,使用计算得到的梯度来更新对应的variable
          
  • 栗子🌰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    # model
    model = keras.Model(inputs=inputs, outputs=outputs)
    optimizer = keras.optimizers.SGD(learning_rate=1e-3)
    loss_fn = keras.losses.SparseCategoricalCrossentropy(from_logits=True)

    # data
    train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
    train_dataset = train_dataset.shuffle(buffer_size=1024).batch(batch_size)

    # iter epochs
    epochs = 2
    for epoch in range(epochs):
    print("\nStart of epoch %d" % (epoch,))

    # iter batches
    for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):

    # Open a GradientTape to record the operations run
    # during the forward pass, which enables auto-differentiation.
    with tf.GradientTape() as tape:

    # Run the forward pass of the layer.
    logits = model(x_batch_train, training=True) # Logits for this minibatch

    # Compute the loss value for this minibatch.
    loss_value = loss_fn(y_batch_train, logits)

    # automatically retrieve the gradients of the trainable variables with respect to the loss
    grads = tape.gradient(loss_value, model.trainable_weights)

    # Run one step of gradient descent by updating the value of the variables to minimize the loss.
    optimizer.apply_gradients(zip(grads, model.trainable_weights))

    # Log every 200 batches.
    if step % 200 == 0:
    print(
    "Training loss (for one batch) at step %d: %.4f"
    % (step, float(loss_value))
    )
    print("Seen so far: %s samples" % ((step + 1) * 64))

16. keras自定义初始化initializers

16.1 基类:keras.initializers.Initializer(),用于给层参数kernel_initializer和bias_initializer传入初始化方法

内置初始化器:

  • keras.initializers.Zeros():全0
  • keras.initializers.Ones():全1
  • keras.initializers.Constant(value=0):全赋值为指定常量
  • keras.initializers.RandomNormal(mean=0.0, stddev=0.05, seed=None):正态分布
  • keras.initializers.RandomUniform(minval=-0.05, maxval=0.05, seed=None):均匀分布
  • keras.initializers.VarianceScaling(scale=1.0, mode=’fan_in’, distribution=’normal’, seed=None):根据层属性决定分布的参数,如果distribution=’normal’,那么正态分布的stddev=sqrt(scale/n),如果distribution=”uniform”,那么均匀分布的limit=sqrt(3 * scale/n),n决定于mode,可以是层输入单元数/输出单元数/两者平均
  • keras.initializers.Orthogonal(gain=1.0, seed=None):随机正交矩阵
  • keras.initializers.Identity(gain=1.0):单位矩阵
  • keras.initializers.glorot_normal(seed=None): Xavier 正态分布,0为中心,stddev=sqrt(2 / (fan_in + fan_out))
  • keras.initializers.glorot_uniform(seed=None): Xavier 均匀分布,limit=sqrt(6 / (fan_in + fan_out))
  • keras.initializers.he_normal(seed=None):Kaiming正态分布,0为中心,stddev=sqrt(2 / fan_in)
  • keras.initializers.he_uniform(seed=None):Kaiming均匀分布,limit=sqrt(6/fan_in)

传入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 用名字字符串
model.add(Dense(64,kernel_initializer='random_uniform', bias_initializer='zeros'))


# 用初始化器的实例
model.add(Dense(64,kernel_initializer=keras.initializers.Constant(value=3.0),
bias_initializer=keras.initializers.RandomNormal(mean=0.,stddev=.05)))

# use config_dict,这个是在efficientDet源码里面看到的
DENSE_KERNEL_INITIALIZER = {
'class_name': 'VarianceScaling',
'config': {
'scale': 1. / 3.,
'mode': 'fan_out',
'distribution': 'uniform'
}
}
x = Dense(n_classes, activation='softmax', kernel_initializer=DENSE_KERNEL_INITIALIZER)(x)

16.2 自定义初始化器:

必须使用参数shape和dtype

1
2
3
4
5
6
from keras import backend as K

def my_init(shape, dtype=None):
return K.random_normal(shape, dtype=dtype)

model.add(Dense(64, kernel_initializer=my_init))

17. keras的model.save() & model.save_weights()

17.1 首先明确包含内容

  • model.save()
    • 模型结构,能够基于这个信息rebuild模型
    • 模型权重
    • 训练设置(loss,metric,optmizer),也就是compile的信息
    • 优化器状态,to exactly resume the training state
  • model.save_weights()
    • 只有模型权重

17.2 一些特殊情况

  • 使用类继承模型时,因为没有model.get_config方法,所以没法调用model.save(),只能save_weights,如果同时也想保存优化器状态,需要手动保存:
    • Optimizer类的get_weights()方法:Returns the current weights of the optimizer as a list of Numpy arrays
    • Optimizer类的set_weights()方法:Pass a list of Numpy arrays to set the new state of the optimizer

18. keras logger

18.1 主要有如下方式

* 原生:fit /fit_generator 都会返回一个history(Callback)对象,这里面记录了loss & metrics (train & val)
* nohup log:保存打印日志,print什么就记录什么
* Logger:同上

18.2 查看

1
2
3
4
5
6
7
8
9
history = model.fit_generator(train_gen,
steps_per_epoch=32,
epochs=3,
validation_data=val_gen,
validation_steps=8,
)
print(history.history)
print(history.epoch)
print(history.history['val_loss'])

18.3 保存:就是保存字典

1
2
3
4
5
6
import pickle
with open('xxx_hist.pkl', 'wb') as f:
pickle.dump(history.history, f)

with open('xxx_hist.pkl', 'rb') as f:
history = pickle.load(f)

19. keras 分层学习率

19.1 包装层,添加层参数

19.2 写在optmizer里面

opencv库函数

发表于 2019-08-11 |

1. imshow

有时候imshow的图片会显示的和原图不一样,要查看read进来的数据格式,imshow会根据读入的数据格式自动进行归一化,映射到0-255。

  • 如果image是默认的8-bit unsigned(0-255),不做处理。
  • 如果image是16-bit unsigned(0-65535)或者32-bit integer(??贼大),像素值除以256,[0,255*256]归一化到[0,255]。
  • 如果image是32-bit float,像素值乘以255,[0,1]归一化到[0,255]。

2. imwrite

通常imwrite把所有数据都强制转换成uchar(0-255)。

beautifulsoup saving file

发表于 2019-06-11 |

最近用bs4处理xml文件,遇到了一个在爬虫时候从未思考过的问题——

修正从xml文件中解析出的文件树,并将changes保存到原来的xml文件中。

我一直在beautifulsoup的手册中去寻找库函数,实际只需要简单的文件读写操作:

1
2
3
4
5
6
7
8
9
from bs4 import BeautifulSoup

soup = BeautifulSoup(open('test.xml'), 'xml')
add = BeautifulSoup("<a>Foo</a>", 'xml')
soup.orderlist.append(add)
print(soup.prettify())
f = open('test.xml', 'w')
f.write(str(soup))
f.close()

附一个简单xml文件用来实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8"?>
<orderlist>
<order>
<customer>姓名1</customer>
<phone>电话1</phone>
<address>地址1</address>
<count>点餐次数1</count>
</order>
<order>
<customer>姓名2</customer>
<phone>电话2</phone>
<address>地址2</address>
<count>点餐次数2</count>
</order>

ssh visualization

发表于 2018-12-29 |
  1. ssh boocax@192.168.1.100

    密码:robot123

  2. echo $ROS_MASTER_URI

    查看端口号11311

  3. 小车端:

    export ROS_MASTER_URI=http://192.168.1.100:11311

    export ROS_IP=192.168.1.100

  4. 虚拟机端:

    export ROS_MASTER_URI=http://192.168.1.100:11311

    export ROS_IP=172.16.128.142

#

  1. nav远程开启三个终端(代码重构以前):

    move_base: roslaunch teleop_twist_joy real_nav.launch

    mapserver: rosrun map_server map_server catkin_ws2/src/patrol/map/p1.yaml

    amcl: roslaunch patrol real_loc.launch

  2. 本地可视化:rviz/rqt_graph / rosservice call /rostopic pub

    • 全局定位:

      1
      rosservice call /global_localization "{}"
    • 设置导航目标点:

      1
      2
      3
      4
      5
      // 相对base_link坐标系
      rostopic pub -1 /navigation_simple/goal geometry_msgs/PoseStamped '{ header: { frame_id: "base_link" }, pose: { position: { x: 0.5, y: 0.0, z: 0 }, orientation: { x: 0, y: 0, z: 0, w: 1 } } }'

      // 相对map坐标系
      rostopic pub -1 /navigation_simple/goal geometry_msgs/PoseStamped '{ header: { frame_id: "map" }, pose: { position: { x: 5, y: 0.0, z: 0 }, orientation: { x: 0, y: 0, z: 0, w: 1 } } }'

      注意-1,否则循环发布。

#

往回备份:

1
scp -r boocax@192.168.1.100:/home/boocax/catkin_ws2019 bkp/

Occupancy Grid Map

发表于 2018-12-04 |

to be completed…

  • Inverse Sensor Model
  • Incremental Updating

object tracking

发表于 2018-12-03 |
  1. 范围限定:过滤掉较远范围的点云数据

  2. 聚类:K-Means/欧式聚类,因为前者需要设定K,故使用后者。

    如果连续扫描点之间的距离小于一个阈值$D_t$,那么这两个点被认为属于同一个对象。这个阈值是根据当前参考点的距离动态调整的。

  3. 运动目标特征提取:(中心坐标,长/宽/半径,反射强度)

  4. 由上一时刻的位置速度设置ROI:

    • 基于局部匹配:通过相似度计算选取响应值最高的目标
    • 基于分类器:动态目标已知(人腿),采集正负样本,构造分类器,
  5. 卡尔曼滤波:

matplotlib的colormap

发表于 2018-11-29 |

用plt的imshow画图,总是找不到心仪的colorbar,可以自定义:

  1. 在原有cmap基础上自定义:

    1
    2
    3
    4
    5
    colorbar = plt.get_cmap('Greys')(range(180))
    cm = LinearSegmentedColormap.from_list(name="grey_cm", colors=colorbar)
    plt.register_cmap(cmap=cm)

    plt.imshow(map2d.data, cmap='grey_cm')
  2. define一个新的cmap:

    1
    2
    3
    4
    5
    6
    def colormap():
    colors = ['#FFFFFF', '#9ff113', '#5fbb44', '#f5f329', '#e50b32']
    return colors.ListedColormap(colors, 'my_cmap')

    my_cmap = colormap()
    plt.imshow(map2d.data, cmap=my_cmap)

mpv-video-cutter

发表于 2018-11-26 |

mpv的小插件,能够一键(三键)剪辑。

工程地址:https://github.com/rushmj/mpv-video-cutter

step1:把c_concat.sh和cutter.lua两个文件复制到~/.config/mpv/scripts/目录下。

step2:给c_concat.sh脚本添加执行权限。

step3:用命令行打开文件,c-c-o在原目录下生成剪辑文件。

problems with ROS

发表于 2018-11-22 |
  1. [WARN] Detected jump back in time of 5.51266s. Clearing TF buffer.

    手动建图的时候,时不时的就跳出来这个,然后小车跳变到初始位置,而且还是根据TF buffer回溯回去的,真高级。。。

    排查原因发现竟然是忘记运行roscore了,mmp。

  2. [rosrun] Couldn't find executable named patrol.py below /home/carrol/catkin_ws/src/patrol

    原因如提示,python是脚本执行,要添加可执行权限。

  3. error: ‘array’ is not a member of ‘std’

    编译导航包时反复出现这个错误,因为cmake版本比较低(2.8),不会自动找c++11,解决办法在对应package的cmake文件中添加c++声明:add_definitions(-std=c++11)

  4. 同样的错误catkin_make时重复出现,我还以为问题没解决:

    删除build文件夹中对应包,再进行catkin_make。如果删除了某个包,还要删除devel文件夹再编译。

  5. cmake warning conflicts with Anaconda:

    编译到最后会卡死,错误具体啥意思我也没弄明白,粗暴解决了,将系统环境变量里面的anaconda path暂时屏蔽,首先查看环境变量:echo $PATH,然后返回结果:

    /home/[username]/anaconda/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games

    然后在当前命令行执行:export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"

  6. c++: internal compiler error: Killed (program cc1plus)

    虚拟机内存不足。

  7. undefined error with CONSOLE_BRIDGE_logError/CONSOLE_BRIDGE_logWarn

    安装并编译console_bridge包,注意build instructions:

    1
    2
    3
    4
    5
    git clone git://github.com/ros/console_bridge.git
    cd console_bridge
    cmake .
    make
    sudo make install
  8. there are no arguments to ‘logDebug’ that depend on a template parameter, so a declaration of ‘logDebug’ must be available [-fpermissive]

    参考(Reference),还是上面的问题, console_bridge的API变了,将logDebug改成CONSOLE_BRIDGE_logDebug就行了。

  9. running environment相关包的缺失和安装:

    在官网查找相关包和依赖,然后执行:

    1
    2
    3
    4
    5
    # install
    sudo dpkg -i 软件包名.deb

    # uninstall
    sudo apt-get remove 软件包名称

amcl

发表于 2018-11-16 |

滤波:

机器人从已知点$x_0$开始运动,里程计误差逐渐累积,位置不确定性将越来越大($x_1, x_2$)。因此需要借助外部环境信息对自己进行定位,于是引入测量值$d$,计算出当前位置$x_2^{‘}$,再结合预测值$x_2$,得到一个矫正位置$x_2^{‘’}$,使其不确定性降到最小。

贝叶斯滤波:$p(x|z) = \frac{p(z|x)p(x)}{p(z)}$

先验:$p(x_t|u_t, x_{t-1})$,通过预测方程得到

似然:$p(z_t| x_t)$,通过测量方程得到

后验:$p(x_t|z_t)$,通过贝叶斯方程得到

对于一般的非线性、非高斯系统,很难通过上述方法得到后验概率的解析解。

蒙特卡洛采样:

假设能从一个目标分布$p(x)$获得一系列样本$x_1, x2, …, x_N$,那么就能利用这些样本去估计这个分布的某些函数的期望值。

蒙特卡洛采样的核心思想就是用均值来代替积分。

假设可以从后验概率中采样到N个样本,那么后验概率可以表示为:

粒子滤波:

用采样粒子(服从后验概率)的状态值直接平均作为期望值,这就是粒子滤波。

MCL:蒙特卡洛定位/粒子滤波定位

  1. Randomly generate a bunch of particles

  2. Predict next state of the particles

  3. Update the weighting of the particles based on the measurement.

  4. Resample:Discard highly improbable particle and replace them with copies of the more probable particles.

    This leads to a new particle set with uniform importance weights, but with an increased number of particles near the three likely places.

  5. Compute the weighted mean and covariance of the set of particles to get a state estimate.

权值退化:如果任由粒子权值增长,只有少数粒子的权值较大,其余粒子的权值可以忽略不计,变成无效粒子,因此需要引入重采样。采用$N_{eff}$衡量粒子权值的退化程度。

粒子多样性:通常我们会舍弃权值较小的粒子,代之以权值较大的粒子。这样会导致权值小的粒子逐渐绝种,粒子群多样性减弱,从而不足以近似表征后验密度。

重要性采样

实际上后验概率并不知道,谈何采样($x_n^i$)。我们可以从一个已知的分布$q(x|z)$里来采样,间接得到滤波值。

相比较于原始的均值表示,变成了加权平均值。不同粒子拥有了不同的权重。

已知的$q$分布叫做重要性概率密度函数。

递推算法:序贯重要性采样

首先假设重要性分布$q(x|z)$满足:

即只和前一时刻的状态$x_{k-1}$和测量$y_k$有关。于是有:

伪代码:

1
2
3
4
5
6
7
For i=1:N
(1)采样:式1
(2)权值更新:式2
End For
权值归一化
加权平均得到粒子滤波值,也就是当前状态的估计值
重采样

重采样

既然权重小的那些粒子不起作用了,那就不要了。为了保持粒子数目不变,就要补充新粒子,最简单的办法就是复制权重大的粒子。用$x_k^i$表示k时刻的粒子,$x_k^j$表示重采样以后的粒子,那么:

总的来说,新粒子按照权重比例来补充,算法流程为:

1
2
3
4
5
6
7
8
计算概率累积和wcum(N)
用[0,1]之间的均匀分布随机采样N个值u(N)
for i in 1:N:
k = 1
while u(i)<wcum(k):
k += 1
end while
resample(i) = k

SIR滤波器(Sampling Importance Resampling Filter )

选取特定的重要性概率密度函数:

于是权重更新公式可以简化:

由于重采样以后,粒子分布更新,权值统一为$\frac{1}{N}$,于是权重更新公式进一步简化:

根据测量方程可知,上面这个概率就是以真实测量值为均值,以噪声方差为方差的高斯分布。

此算法中的采样,并没有加入测量$z_k$,只凭先验知识$p(x_k|x_{k-1})$,虽然简单易用,但是存在效率不高和对奇异点(outliers)敏感的问题。

AMCL

MCL算法能够用于全局定位,但是无法从机器人绑架或全局定位失败中恢复过来,因为随着位置被获取,其他地方的不正确粒子会逐渐消失。稳定状态下,粒子只“生存”在一个单一的姿态附近,如果这个姿态恰好不正确(在重采样步骤中可能意外的丢弃所有正确位姿附近的粒子),算法就无法恢复。

AMCL就是为了解决上述问题:结合了自适应(Augmented_MCL)和库尔贝克-莱不勒散度采样(KLD_Sampling_MCL)

  • Augmented_MCL:在机器人遭到绑架的时候,它会在发现粒子们的平均分数突然降低了,这意味着正确的粒子在某次迭代中被抛弃了,此时会随机的全局注入粒子(injection of random particles)。
  • KLD_Sampling_MCL:动态调整粒子数,当机器人定位差不多得到了的时候,粒子都集中在一块了,就没必要维持这么多的粒子了——在栅格地图中,看粒子占了多少栅格。占得多,说明粒子很分散,在每次迭代重采样的时候,允许粒子数量的上限高一些。占得少,说明粒子都已经集中了,那就将上限设低。

mcl&amcl

算法流程上看,augmented_MCL算法最显著的区别就是引入了四个参数用于失效恢复:

  • $w_{slow}$:长期似然平均估计
  • $w_{fast}$:短期似然平均估计
  • $\alpha_{slow}$:长期指数滤波器衰减率
  • $\alpha_{fast}$:短期指数滤波器衰减率

失效恢复的核心思想是:测量似然的一个突然衰减(短期似然劣于长期似然)象征着粒子质量的下降,这将引起随机采样数目的增加。

$w_{avg}$计算了粒子的平均权重,当粒子质量下降时,平均权重随之下降,$w_{slow}、w_{fast}$也会随之下降,但是显然$w_{fast}$下降的速度要快于$w_{slow}$——这由衰减率决定,因此随机概率$p = 1 - \frac{w_{fast}}{w_{slow}}$会增大,随机粒子数目增加。而当粒子质量提高时,粒子短期权重要好于长期,随机概率小于0,不生成随机粒子。

重定位

ROS amcl参数解析

Augmented_MCL:

  • <param name="recovery_alpha_slow" value="0.0"/>:默认0(MCL),我的0.001。
  • <param name="recovery_alpha_fast" value="0.0"/>:默认0(MCL),我的0.8。
  • 在rviz里通过2D Pose Estimate按钮移动机器人来触发,机器人位置突变后要过一会儿才注入随机粒子,因为概率是渐变的。

KLD:

  • <param name="kld_z" value="0.99"/>: KLD采样以概率$1-\delta(kld_z)$确定样本数。
  • <param name="kld_err" value="0.05"/>: 真实的后验与基于采样的近似之间的误差。

动态障碍物:环境中的动态物体总是会获得比静态障碍物更短的读数,因此可以根据这样的不对称性去除异常值。

  • 静态障碍物应该服从稳定的高斯分布,以距离传感器的真实距离为均值。
  • 扫描到动态目标的beam则服从衰减分布,$-\eta e ^{-\lambda z}$。
  • laser_model_type:使用beam model时会用到四个混合权重参数z_hit,z_short,z_max和z_rand,使用likelihood_field model时使用两个z_hit和z_rand。
    • laser_z_hit:default=0.95,以真实值为均值的噪声高斯分布
    • laser_z_rand:defualt=0.05,随机测量权重,均匀分布
    • laser_z_short:default=0.1,意外对象权重,衰减分布
    • laser_z_max:default=0.05,测量失败权重,0/1分布

初始位姿:

  • 可以在rviz里通过2D Pose Estimate按钮设定(rviz会发布initialPose话题)。

  • 或者写在launch文件中:

    1
    2
    3
    4
    5
    6
    <param name="initial_pose_x"            value="0.0"/>
    <param name="initial_pose_y" value="0.0"/>
    <param name="initial_pose_a" value="0.0"/>
    <param name="initial_cov_xx" value="0.25"/>
    <param name="initial_cov_yy" value="0.25"/>
    <param name="initial_cov_aa" value="(pi/12)*(pi/12)"/>
  • 调用全局定位服务:

    1
    rosservice call /global_localization "{}"

    位姿随机初始化,粒子洒满地图:

transform_tolerance:

  • 默认是0.1seconds,官方定义是Time with which to post-date the transform that is published, to indicate that this transform is valid into the future. tf变换发布推迟的时间,意思是tf变换在未来这段时间内是可用的。
  • 【存疑】我个人理解tf的更新频率应该越快越准确,launch文件中最开始设定为0.5,但是实际上机器人移动速度调快时,会报错Costmap2DROS transform timeout...Could not get robot pose, cancelling reconfiguration,然后我调整为1.5就不报错了。
  • 目前设定为1.0,仿真里观测不出差异。
1…141516…18
amber.zhang

amber.zhang

要糖有糖,要猫有猫

180 日志
98 标签
GitHub
© 2023 amber.zhang
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4