1. 本篇文章所用数据:
文本数据——size(2, 3, 5) (N, C, L) 例如:我爱吃苹果 你爱吃吗? 可以理解为: 两句话,每句话五个字,每个字的特征长度为3(批次大小为2,每一批有五个特征,每个特征尺寸为3),即batch:2 ,channel:3 , length:5
图1 文本数据batch normalization的示意图
2. 五种归一化方式的概念及理解:
图2 五种归一化方式的理解
2.1 BatchNormalization
对多个batch的多个length的每个channel进行归一化,即对于(2, 3, 5)尺寸的数据,会有3个mean和3个stdvar。即每次归一化是以图1中黑色矩形框选的内容作为一个单元。
ps:对于nlp任务而言,基本不使用BN方法,一方面是因为nlp任务中不同的词条长度不同,即难以保持length这个参数是一样的,很不方便使用BN;另一方面是因为,同一句子中的同一个字的特征表示可能完全不同,或者同一句子中的不同字特征完全一样,例如图1中的”我“和”她“特征就是一样的,但是实际上这是完全不同的含义,因此nlp任务中很少使用BN。但是对于图像中的像素来讲就没有这种烦恼,因此更多是图像领域在使用BN。
代码实现如下:
import torch
import torch.nn as nn
batch_size = 2 # batch数
channel = 3 # 句子中每个字的特征长度
length = 5 # 句子长度
torch.manual_seed(5)
inputX = torch.randn(batch_size, length, channel) # N*L*C
#- 1.batch normalization的实现
#= 调用batch_norm API
batch_norm_op = torch.nn.BatchNorm1d(channel, affine=False)
bn_re = batch_norm_op(inputX.transpose(-1, -2)).transpose(-1, -2)
print(bn_re)
#= 手写 batch_norm
bn_mean = inputX.mean(dim=(0, 1),keepdim=True)
print(bn_mean)
bn_std = inputX.std(dim=(0, 1), unbiased=False, keepdim=True)
print(bn_std)
verify_bn_re = (inputX - bn_mean)/(bn_std + 1e-5)
print(verify_bn_re)
2.2 LayerNormalization
以channel为单位进行归一化,即对于(2, 3, 5)尺寸的数据,会有2*5个mean和2*5个stdvar。例如图3中黑色矩形和黄色矩形框起来的内容都是归一化的一个单元。
ps:nlp领域更多的是使用LN做归一化。
图3LayerNorm归一化示意图
代码实现如下:
import torch
import torch.nn as nn
batch_size = 2 # batch数
channel = 3 # 句子中每个字的特征长度
length = 5 # 句子长度
torch.manual_seed(5)
inputX = torch.randn(batch_size, length, channel) # N*L*C
#- 2.layer normalization的实现
#= 调用layer_norm API
layer_norm_op = torch.nn.LayerNorm(channel, elementwise_affine=False)
ln_re = layer_norm_op(inputX)
print(ln_re)
#= 手写layer_norm
ln_mean = inputX.mean(dim=-1, keepdim=True)
print(ln_mean)
ln_std = inputX.std(dim=-1, unbiased=False, keepdim=True)
print(ln_std)
verify_ln_re = (inputX - ln_mean)/(ln_std + 1e-5)
print(verify_ln_re)
2.3 InstanceNormalization
以length为单位进行归一化,即对于(2, 3, 5)尺寸的数据,会有2*3个mean和2*3个stdvar。例如图4中蓝色矩形框起来的内容是归一化的一个单元。
ps:instance norm一般用来做风格迁移,那么为什么可以用来做风格迁移呢?因为如果针对不同时序的数据进行了求均值和标准差的操作,就相当于把所有时刻都不变的东西给提取出来,减均值除标准差之后就相当于将这些不变的东西给消除掉,那么什么是时序不变的呢?比如说当输入数据是声音时,音色是不变的;当输入数据是艺术画时,其风格是不变的。
图4 InstanceNormalization归一化示意图
代码实现如下:
import torch
import torch.nn as nn
import logger
logger1 = logger.logger_('/media/vcl/Data91T/LZ/Code_Repository/Start_to_learning_deeplearning/logger.log')
batch_size = 2 # batch数
channel = 3 # 句子中每个字的特征长度
length = 5 # 句子长度
torch.manual_seed(5)
inputX = torch.randn(batch_size, length, channel) # N*L*C
#- 3.instance normalization的实现
#= 调用instance_norm API
instance_norm_op = torch.nn.InstanceNorm1d(channel, affine=False)
inn_re = instance_norm_op(inputX.transpose(-1, -2)).transpose(-1, -2)
logger1.info(f'官方API调用计算出来的inn值为{inn_re}')
#= 手写instance_norm
in_mean = inputX.mean(dim=1, keepdim=True)
logger1.info(f'in_mean的值为:{in_mean}')
in_std = inputX.std(dim=1, unbiased=False, keepdim=True)
logger1.info(f'in_std的值为:{in_std}')
verify_in_re = (inputX - in_mean)/(in_std + 1e-5)
logger1.info(f'手写代码计算出来的in值为:{verify_in_re}')
以上三种方法的总结可以用图5来清晰表示
图5 BN、LN、IN的图示
2.4 GroupNormalization
group normalization其实与Batch normalization差不多,只不过沿着channel维度将其分为了若干个group,因此在应用group normalization的时候基本上都要求channel是偶数。
其示意图如下所示:
图6 将channel为4的输入分为两份求norm的示意图
代码实现如下所示:
import torch
import torch.nn as nn
import logger
logger1 = logger.logger_('/media/vcl/Data91T/LZ/Code_Repository/Start_to_learning_deeplearning/logger.log')
batch_size = 2 # batch数
length = 5 # 句子长度
#- 4.group normalization的实现
channel = 4
inputX = torch.randn(batch_size, length, channel)
num_groups = 2
#= 调用官方group normalization API
group_norm_op = torch.nn.GroupNorm(num_groups, channel, affine=False)
gn_re = group_norm_op(inputX.transpose(-1, -2)).transpose(-1, -2)
logger1.info(f'官方API调用计算出来的gn值为:{gn_re}')
#= 手写group_norm
splited_inputX = torch.tensor_split(inputX, num_groups, dim=-1)
logger1.info(f'splited_inputX的值为:{splited_inputX}')
result = []
for group_unit in splited_inputX:
group_unit_mean = group_unit.mean(dim=(1, 2), keepdim=True)
group_unit_std = group_unit.std(dim=(1, 2), unbiased=False, keepdim=True)
logger1.info(f'group_unit_mean的值为:{group_unit_mean}')
logger1.info(f'group_unit_std的值为:{group_unit_std}')
verify_gn_unit_re = (group_unit - group_unit_mean)/(group_unit_std + 1e-5)
result.append(verify_gn_unit_re)
verify_gn_re = torch.cat(result, dim=-1)
logger1.info(f'手写代码计算出来的gn值为:{verify_gn_re}')
2.5 weight normalization
weight norm是一个函数,接收的参数是module,在使用weight norm时其实就是对module进行了一层包裹,例如:将一个linear层传入module,如果不考虑b的话,传入时只有参数w,但是输出却有两个(weight_g、weight_v),也就是下图中的g和v。
其实就是将原来module中的权重w给分解了,与其叫权重归一化,不如叫权重分解来得实在。
这里的g其实是w的幅度值,后面一项关于v的分式其实就是一个单位向量(方向向量)。这种方法将原本对于w进行梯度更新变成了对g和v进行更新。具体公式如下图所示:
ps:上面公式中的v其实就是w,只是为了计算出单位向量。
具体代码如下:
import torch
import torch.nn as nn
import logger
logger1 = logger.logger_('/media/vcl/Data91T/LZ/Code_Repository/Start_to_learning_deeplearning/logger.log')
batch_size = 2 # batch数
length = 5 # 句子长度
channel = 4
inputX = torch.randn(batch_size, length, channel)
#- 5.weight normalization的实现
#= 调用官方weight normalization API
linear = torch.nn.Linear(channel, 3, bias=False) # 线性层,将channel映射为3
wn = torch.nn.utils.weight_norm(linear)
wn_linear_re = wn(inputX)
logger1.info(f'官方API调用计算出的wn值为:{wn_linear_re}')
logger1.info(f'转换后的尺寸为:{wn_linear_re.shape}') # [2, 5, 3]
#= 手写weight_norm
logger1.info(f'linear.weight的形状是:{linear.weight.shape}') # [3, 4]
weight_direction = linear.weight / linear.weight.norm(dim=1, keepdim=True) # 计算方向向量,对于二维张量,dim=1时计算每一行的L2范数
weight_magnitude = wn.weight_g # 计算幅度
logger1.info(f'单位向量weight_direction的值为:{weight_direction.shape}')
logger1.info(f'单位向量weight_magnitude的值为:{weight_magnitude.shape}')
verify_wn_re = inputX @ weight_direction.transpose(-1, -2) * weight_magnitude.transpose(-1, -2)
logger1.info(f'手写代码计算出来的wn值为:{verify_wn_re}')
logger1.info(f'手写代码计算出来的wn尺寸为:{verify_wn_re.shape}')