keras note

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=(13, 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

权重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 定义模型的方式

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里面