包括3个文件 init.py, rnn.py, clip_grad.py, weight_norm.py 这里面是一些nn的工具,比如rnn中的序列打包成PackedSequence和解包还原成程度不等序列
from . import rnn
from .clip_grad import clip_grad_norm
from .weight_norm import weight_norm, remove_weight_norm
#三句分别从当前目录的三个文件当中导入需要的函数或者类
#下面先看clip_grad.py
def clip_grad_norm(parameters, max_norm, norm_type=2):
#修剪可迭代Parameters的梯度范数
#范数由所有梯度共同计算得到, 把它们看做一个向量。
#梯度被in-place operation修改。
#参数:
#parameters (Iterable[Variable]): 要进行梯度归一化的可迭代的
# 变量Variable
#max_norm (float or int): 梯度的最大范数
#norm_type (float or int): p范数类型'inf' 代表无穷范数.
#返回值:
#所有参数的范数 (看成一个向量).
parameters = list(filter(lambda p: p.grad is not None, parameters))
max_norm = float(max_norm)
norm_type = float(norm_type)
if norm_type == float('inf'):
total_norm = max(p.grad.data.abs().max() for p in parameters) #无穷范数||X||inf = max(|Xi|)
else:
total_norm = 0
for p in parameters:
param_norm = p.grad.data.norm(norm_type)
# tensor.norm(p) 计算p范数
total_norm += param_norm ** norm_type
total_norm = total_norm ** (1. / norm_type)
#||X||p = Σ(Xi ** p) ** (1/p)
clip_coef = max_norm / (total_norm + 1e-6)
#防止total_norm等于0
if clip_coef < 1:
for p in parameters:
p.grad.data.mul_(clip_coef)
return total_norm
这个函数的作用是归一化p范数到max_norm,使得parameters的参数的p范数和为max_norm。默认为2范数,返回值为所有参数梯度的p范数 Gradient Clipping的引入是为了处理gradient explosion或者gradients vanishing的问题。 当在一次迭代中权重的更新过于迅猛的话,很容易导致loss divergence。Gradient Clipping的直观作用就是让权重的更新限制在一个合适的范围。所以经常在一个epoch之后加入clip_grad_norm在max_norm范围内。
from collections import namedtuple
#namedtuple创建一个tuple对象,具备tuple的不变性,可以根据属性来引用。
import torch
from torch.autograd import Variable
PackedSequence_ = namedtuple('PackedSequence', ['data', 'batch_sizes'])
class PackedSequence(PackedSequence_):
pass
#这个类包含数据和由每一序列长度的batch_size大小组成的列表
#所有的RNN都可以接收这种序列作为输入,这种序列是没有补零的,
#即一个batch中每个样本的长度可以不一致
# 说明:
# 这个类的实例不能被创建,只能在`pack_padded_sequence`中创建.
# 参数:
#data (Variable): 包含打包序列的Variable
#batch_sizes (list[int]): 每一个序列长度的batch_size大小所组成的list
#这个类为之后的补零对齐序列长度的输入打包做准备。目的是为了去掉这些零。RNN可以接收这个类的数据作为输入。
def pack_padded_sequence(input, lengths, batch_first=False):
#将输入长度不等进行补零后的序列进行pack,即把0去掉。pack理解为压缩数据
#输入的大小为 ``TxBx*``T是最长序列的序列长度(equal to lengths[0]
#因为需要按序列长度降序排列)
#B 是 batch size,大小,* 是包括0在内的任意维,一般是特征维度.
#如果 ``batch_first``=True,那么
#输入就是`BxTx*``,即第一维是batch_size大小。输入需要按照序列
#长度大小降序排列。
# 说明:
#函数只接受最少二维的输入。可以用来打包标签,也可以将使用他们的
#RNN输出直接计算loss。
#A Variable可以直接访问PackedSequence的成员data得到。
#参数:
#input (Variable): 不同长度补零后的序列.
#lengths (list[int]): 每个序列长度真实长度组成的list列表
#batch_first (bool, optional): if True, the input is expected in BxTx* format.
#返回值:
#一个`PackedSequence`对象
if lengths[-1] <= 0:
raise ValueError("length of all samples has to be greater than 0, " "but found an element in 'lengths' that is <=0")
if batch_first:
input = input.transpose(0, 1)
steps = []
batch_sizes = []
lengths_iter = reversed(lengths) #变成从小到大排列,迭代类型
current_length = next(lengths_iter) #遍历下一个元素
batch_size = input.size(1)
if len(lengths) != batch_size:
raise ValueError("lengths array has incorrect size")
for step, step_value in enumerate(input, 1): #enumerate(input,1)下标从1开始
steps.append(step_value[:batch_size])
#得到对应索引下的值,即位置(指的是特征在序列的位置)为1的数据
batch_sizes.append(batch_size)
#batch_sizes保存每个位置的batch大小,第一个位置肯定每个batch都有
#直到得到最短的序列的位置,都是每个batch都有,所以从current_length
#开始出现batch_size发生变化
while step == current_length:
try:
new_length = next(lengths_iter) #得到下一个长度
except StopIteration:
current_length = None #迭代完毕得到空
break
if current_length > new_length:
#因为输入是降序排列,reversed之后是升序排列防止输入错误提出异常
raise ValueError("lengths array has to be sorted in decreasing order")
batch_size -= 1
#batch_size长度减一, 其实是减去每一种长度的个数,一种长度
#有n个,就有n个输入序列,即n个样本
current_length = new_length
#更新current_length,如果长度不变,继续减一
if current_length is None: #迭代完毕结束循环
break
return PackedSequence(torch.cat(steps), batch_sizes)
#返回由step数据和batch_sizes组成的PackedSequence实例
#这个函数解除了RNN输入需要序列长度需要相等的限制,在补零使得序列长度相等后对数据进行压缩,根据真实的序列长度提取
#数据,并按照每个位置的batch大小保存结果以便经过RNN后还原成长度相等的序列进行loss的计算。即下一个函数。
def pad_packed_sequence(sequence, batch_first=False, padding_value=0.0):
#与上一个函数进行相反的操作,给定一个packedSequence,进行padding操作
#,可以设置默认的padding值为0,即进行补零操作。返回结果为3维Varaible,
#TxBx*,T是最长序列的长度,B 是batch size. 如果batch_first== True,
#输出数据不转置为BxTx* 格式.
#输出会按照length的长度进行降序排列.
#参数:
#sequence (PackedSequence): batch to pad
#batch_first (bool, optional): if True, 输出会设置为 BxTx*格式
#padding_value (float, optional): padding的值,默认为0
#返回值:
#元组变量包括Variable padded sequence 和每个样本的序列长度
var_data, batch_sizes = sequence
max_batch_size = batch_sizes[0]
#从位置0开始填batch,所以batch_size 是越来越小
output = var_data.data.new(len(batch_sizes), max_batch_size, *var_data.size()[1:]).fill_(padding_value)
#输出tensor全部填充padding_value
output = Variable(output)
lengths = []
data_offset = 0
prev_batch_size = batch_sizes[0]
for i, batch_size in enumerate(batch_sizes):
output[i, :batch_size] = var_data[data_offset:data_offset + batch_size]
data_offset += batch_size
#上一个函数的输出是concat是一个一维向量所以按照batch_size剪开就行
dec = prev_batch_size - batch_size
if dec > 0:
#dec=0,说明该位置还没有到达最短长度
lengths.extend((i,) * dec)
#i即为位置,即序列的长度,dec表示该长度也有多少个样本,
#所以extend((i)*dec)
prev_batch_size = batch_size
lengths.extend((i + 1,) * batch_size)
#添加序列最长的样本,个数为batch_size个,batch_size为降序,
#在循环中未能添加
lengths.reverse()
#长度反转 输出长度降序排列,是为了之后使用上一个函数
if batch_first:
output = output.transpose(0, 1)
return output, lengths
#这个函数是为了解压,得到padding的规整的三维tensor数据,可以用来计算loss等。
#如果在RNN中间加入batch_norm或者线性层,就不要不断地使用这两个函数来进行Variable的变换。
from torch.nn.parameter import Parameter
def _norm(p, dim):
#计算除了dim维度之外所有维度的2范数
if dim is None:
return p.norm()
elif dim == 0:
#保持0维不变,计算其他维数的范数
output_size = (p.size(0),) + (1,) * (p.dim() - 1)
return p.contiguous().view(p.size(0), -1).norm(dim=1).view(output_size)
#先化为p.size(0) * n 计算范数后化为原来维度
elif dim == p.dim() - 1:
output_size = (1,) * (p.dim() - 1) + (p.size(-1),)
#保持最后一维不变
return p.contiguous().view(-1, p.size(-1)).norm(dim=0).view(*output_size)
else:
return _norm(p.transpose(0, dim), 0).transpose(0, dim)
#其他情况将dim维转置到0维,迭代该函数计算范数再转置回来
class WeightNorm(object):
def __init__(self, name, dim):
self.name = name
self.dim = dim
def compute_weight(self, module):
#得到module的name值计算norm
g = getattr(module, self.name + '_g')
#getattr获取类的属性
v = getattr(module, self.name + '_v')
return v * (g / _norm(v, self.dim))
@staticmethod
def apply(module, name, dim):
fn = WeightNorm(name, dim)
weight = getattr(module, name)
# remove w from parameter list
del module._parameters[name]
# add g and v as new parameters and express w as g/||v|| * v
module.register_parameter(name + '_g', Parameter(_norm(weight, dim).data)) #二范数大小,即模大小数据
module.register_parameter(name + '_v', Parameter(weight.data)) #weight本身的数据
#给module._parameters添加weight_g和weight_v 其为OrderedDict类型
setattr(module, name, fn.compute_weight(module))
# recompute weight before every forward()
module.register_forward_pre_hook(fn)
#每次计算forward(input)前,都会重新计算fn(imodule,input),
#即调用__call__计算归一化weight
return fn
def remove(self, module):
weight = self.compute_weight(module)
delattr(module, self.name)
del module._parameters[self.name + '_g']
del module._parameters[self.name + '_v']
module.register_parameter(self.name, Parameter(weight.data))
def __call__(self, module, inputs):
#该类的实例化对象被调用时执行该函数,比如实例化module之后,
#调用先执行了__call__,再调用forward进行前向传播
setattr(module, self.name, self.compute_weight(module))
#setattr给类的属性幅值,不存在则创建
def weight_norm(module, name='weight', dim=0):
#给定一个module,对参数parameter进行权重归一化
. #w = g*v/||v|| g*v的单位向量
#权重归一化是一种参数重新初始化来减弱权重在它方向上的模大小。
#理解为使用小的权重来防止过拟合。它由参数名字为name的两个参数决定,
#一个是决定大小的weight_g,一个是决定方向的weight_v权重归一化使用
#过hook来在每个forward()前向传播之前从大小和方向重新计算权重tensor。
#默认情况下,dim=0,对每个输出channel范数计算是独立的。
#如果计算这个权重,dim=None
#参数:
#module (nn.Module): containing module
#name (str, optional): 权重参数的名称
#dim (int, optional): 需要计算范数的维度
#返回值:
# The original module with the weight norm hook
#Example::
# >>> m = weight_norm(nn.Linear(20, 40), name='weight')
# Linear (20 -> 40)
#>>> m.weight_g.size()
# torch.Size([40, 1])
# >>> m.weight_v.size()
# torch.Size([40, 20])
WeightNorm.apply(module, name, dim)
return module
def remove_weight_norm(module, name='weight'):
#从一个module中一处权重归一化
#参数:
#module (nn.Module): containing module
#name (str, optional): name of weight parameter
#Example:
# >>> m = weight_norm(nn.Linear(20, 40))
# >>> remove_weight_norm(m)
for k, hook in module._forward_pre_hooks.items():
if isinstance(hook, WeightNorm) and hook.name == name:
#移除weight权重归一化对象hook
hook.remove(module)
del module._forward_pre_hooks[k]
return module
raise ValueError("weight_norm of '{}' not found in {}"
.format(name, module))
整个文件定义了一个WeightNorm类来实现每次前向传播之前对weight进行归一化处理,将类加入到forward_hook中实现。 归一化主要是在相应的维度上进行了模大小的规整,方向保持不变。防止过拟合。 与clip_grad不同的是clip_grad是规整了反方向传播梯度的大小,限制在一定范围,防止梯度爆炸或者梯度消失问题 rnn.py主要是为RNN的输入输出序列做处理。