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
3model.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
15from 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)
模型结构图如下:
1.3 高级:自定义损失函数
step 1. 把y_true定义为一个输入
step 2. 把loss写成一个层,作为网络的最终输出
step 3. 在compile的时候,将loss设置为y_pred
1 | # yolov3 train.py create_model: |
2. keras 自定义loss
补充1.3: 也可以不定义为网络层,直接调用自定义loss函数
参数:
- y_true: 真实标签,Theano/Tensorflow 张量。
- y_pred: 预测值。和 y_true 相同尺寸的 Theano/TensorFlow 张量。
1
2
3
4def 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 | # build model |
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
16def 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 | class CenterLossLayer(Layer): |
有一些自定义层,有时候会不实现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 | # 官方示例:Custom loss layer |
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 | # use tf.Variable |
build中add_weight实际上也是调用tf.Variable创建一个varaible
4.5 Layer内部也可以创建a Layer instance作为其attribute——torch style
也是放在__init__()里面,如learnable positional embedding
1 | class MLPBlock(Layer): |
!!!需要注意的是:用躲避build的方法创建的层,在层被fisrt call的时候才会创建权重,而不是在模型创建阶段
5. keras Generator
本质上就是python的生成器,每次返回一个batch的样本及标签
自定义generator的时候要写成死循环(while true),因为model.fit_generator()在使用在个函数的时候,并不会在每一个epoch之后重新调用,那么如果这时候generator自己结束了就会有问题。
栗子是我为mixup写的generator:
没有显示的while True是因为创建keras自带的generator的时候已经是死循环了(for永不跳出)
1 | def Datagen_mixup(data_path, img_size, batch_size, is_train=True, mix_prop=0.8, alpha=1.0): |
【多进程】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 | import keras |
经验值:
- 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 | from keras.util import multi_gpu_model |
step2. 在定义checkpoint时,要用ParallelModelCheckpoint封一层,初始化参数的model要传原始模型。
1 | from keras.callbacks import ModelCheckpoint |
step3. 在保存权重时,通过cpu模型来保存。
1 | # 实例化基础模型,这样定义模型权重会存储在CPU内存中 |
【attention】同理,在load权重时,也是load单模型的权重,再调用multi_gpu_model将模型复制到多个GPU上:
1 | model = Model(inputs=[anchor_input,positive_input,negative_input], outputs=[x, merged]) |
step4. 如果保存成了多GPU权重,可以如下代码解封:
1 | model = EfficientV2(input_shape=380, n_classes=2) |
【ATTENTION】实验中发现一个问题:在有些case中,我们使用了自定义loss作为网络的输出,此时网络的输出是个标量,但是在调用multi_gpu_model这个方法时,具体实现在multi_gpu_utils.py中,最后一个步骤要merge几个device的输出,通过axis=0的concat实现,网络输出是标量的话就会报错——list assignment index out of range。
尝试的解决方案是改成相加:
1 | # Merge outputs under expected scope. |
【ATTENTION++】网络的输出不能是标量!!永远会隐藏保留一个batch dim,之前是写错了!!
- model loss是一个标量
- 作为输出层的loss是保留batch dim的!!
6.2 设备并行
设备并行适用于多分支结构,一个分支用一个GPU。通过使用TensorFlow device scopes实现。
栗子:
1 | # Model where a shared LSTM is used to encode two different sequences in parallel |
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 | # return_sequences |
7.2.5 TimeDistributed
顺便再说下TimeDistributed,当我们使用many-to-many模型,最后一层LSTM的输出维度为k,而我们想要的最终输出维度为n,那么就需要引入Dense层,对于时序模型,我们要对每一个time-step引入dense层,这实质上是多个Dense操作,那么我们就可以用TimeDistributed来包裹Dense层来实现。
1 | model = Sequential() |
官方文档:这个封装器将一个层应用于输入的每个时间片。
- 当该层作为第一层时,应显式说明input_shape
- TimeDistributed可以应用于任意层,如Conv3D:
1 | # 例如我的crnn model |
7.3 Embedding
用于将稀疏编码映射为固定尺寸的密集表示。
输入形如(samples,sequence_length)的2D张量,输出形如(samples, sequence_length, output_dim)的3D张量。
参数:
- input_dim:字典长度,即输入数据最大下标+1
- output_dim:
- input_length:
栗子:
1 | # centerloss branch |
7.4 plot_model
1 | from keras.utils import plot_model |
7.5 K.function
获取模型某层的输出,一种方法是创建一个新的模型,使它的输出是目标层,然后调用predict。
1 | model = ... # the original model |
也可以创建一个函数来实现:keras.backend.function(inputs, outputs, updates=None)
1 | # 这是写center-loss时写的栗子: |
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
模式中,方向会自动从被监测的数据的名字(不靠谱🤷♀️)中判断出来。
- filepath可以由
学习率衰减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
28from 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 | # fcn example: current feature map x (,32,32,32), input_shape (512,512,2), output_shape (,512,512,1) |
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 | import keras.backend as K |
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
11import 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 | intra_distance = tf.Print(intra_distance, |
会报错:AttributeError: ‘Tensor’ object has no attribute ‘_keras_history’
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 | # wrapper function |
【夹带私货】tf.Print同时也可以打印wrapper function内的中间变量,都放在列表里面就可以了。
8.3 tf.while_loop(cond, body, init_value)
tensorflow中实现循环的语句
- 终止条件cond:是一个函数
- 循环体body:是一个函数
- init_value:是一个list,保存循环相关参数
- cond、body的参数是要与init_value列表中变量一一对应的
- body返回值的格式要与init_value变量一致(tensor形状保持不变)
- 若非要变怎么办(有时候我们希望在while_loop的过程中,维护一个list)?动态数组TensorArray/高级参数shape_invariants
8.3.1 动态数组
1 | # 定义 |
tensor array变量中一个位置只能写入一次
8.3.2 shape_invariants
1 | i = tf.constant(0) |
在while_loop中显示地指定参数的shape,上面的例子用了tf.TensorShape([None])令其自动推断,而不是固定检查,因此可以解决变化长度列表。
一个完整的栗子:第一次见while_loop,在yolo_loss里面
- 基于batch维度做遍历
- loop结束后将动态数据stack起来,重获batch dim
1 | # Find ignore mask, iterate over each of batch. |
8.4 tf.image.non_max_suppression()
非最大值抑制:贪婪算法,按scores由大到小排序,选定第一个,依次对之后的框求iou,删除那些和选定框iou大于阈值的box。
1 | # 返回是被选中边框在参数boxes中的下标位置 |
- 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
2import os
os.environ["CUDA_VISIBLE_DEVICES"] = "2"限制GPU用量
1
2
3
4
5
6
7
8
9
10
11
12
13
14import 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 | # y_trues: [b,h,w,a,4] |
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,决定优化器的保存和重载
- self.updates:stores the variables that will be updated with every batch that is processed by the model in training
1 | class Optimizer(object): |
9.3 实例化一个优化器
- based on keras.Optimizer对象
- 主要需要重写get_updates和get_config方法
- get_updates用来定义梯度更新的计算方法
- get_config用来定义实例用到的参数
- 以SoftSGD为例:
- 每隔一定的batch才更新一次参数,不更新梯度的step梯度不清空,执行累加,从而实现batchsize的变相扩大
- 建议搭配间隔更新参数的BN层来使用,否则BN还是基于小batchsize来更新均值和方差
1 | class SoftSGD(Optimizer): |
10. keras自定义激活函数activation
10.1 定义激活函数
1 | def gelu(x): |
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 | from keras.layers import Activation |
这种写法在使用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
8layer = 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 | def my_regularizer(x): |
- 子类继承版,可以加额外参数,需要补充get_config方法,支持读写权重时的串行化
1 | class MyRegularizer(regularizers.Regularizer): |
11.3 强行global
- 每层加起来太烦了,批量加的实质也是逐层加,只不过写成循环
- 核心是layer的add_loss方法
1 | model = keras.applications.ResNet50(include_top=True, weights='imagenet') |
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 | class ExponentialMovingAverage: |
- then train
1 | EMAer = ExponentialMovingAverage(model) # 在模型compile之后执行 |
- then inference
1 | MAer.apply_ema_weights() # 将EMA的权重应用到模型中 |
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 | import keras |
- 可以看到,类继承模型是没有指明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
34class 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
43loss_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()}
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
25class 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
- a for loop:iter over the dataset
- a for loop:iter for each epoch
栗子🌰
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 | # 用名字字符串 |
16.2 自定义初始化器:
必须使用参数shape和dtype
1 | from keras import backend as K |
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 | history = model.fit_generator(train_gen, |
18.3 保存:就是保存字典
1 | import pickle |
19. keras 分层学习率
19.1 包装层,添加层参数
19.2 写在optmizer里面