GN,Interpolate, Sampler,DataLoader, DataSet(Pytorch)

1 GN

Face book AI research(FAIR)吴育昕-何恺明联合推出重磅新作Group Normalization(GN),GN解决了BN式归一化对batch size依赖的影响。

BN全名是Batch Normalization,见名知意,其是一种归一化方式,而且是以batch的维度做归一化,那么问题就来了,此归一化方式对batch是independent的,过小的batch size会导致其性能下降,一般来说每GPU上batch设为32最合适;

但是对于一些其他深度学习任务batch size往往只有1-2,比如目标检测,图像分割,视频分类上,输入的图像数据很大,较大的batchsize显存吃不消。那么,对于较小的batch size,其performance是什么样的呢?如下图:

横轴表示每个GPU上的batch size大小,从左到右一次递减,纵轴是误差率,可见,在batch较小的时候,GN较BN有少于10%的误差率。

另外,Batch Normalization是在batch这个维度上Normalization,但是这个维度并不是固定不变的,比如训练和测试时一般不一样,一般都是训练的时候在训练集上通过滑动平均预先计算好平均-mean,和方差-variance参数。在测试的时候,不再计算这些值,而是直接调用这些预计算好的来用,但是,当训练数据和测试数据分布有差别是时,训练机上预计算好的数据并不能代表测试数据,这就导致在训练,验证,测试这三个阶段存在inconsistency。

既然明确了问题,解决起来就简单了,归一化的时候避开batch这个维度是不是可行呢,于是就出现了layer normalization和instance normalization等工作,但是仍比不GN

GN本质上仍是归一化,但是它灵活的避开了BN的问题,同时又不同于Layer Norm,Instance Norm

 

众所周知,深度网络中的数据维度一般是[N, C, H, W]或者[N, H, W,C]格式,N是batch size,H/W是feature的高/宽,C是feature的channel,压缩H/W至一个维度,其三维的表示如上图,假设单个方格的长度是1,那么其表示的是[6, 6,*, * ]

上图形象的表示了四种norm的工作方式:

BN在batch的维度上norm,归一化维度为[N,H,W],对batch中对应的channel归一化;

LN避开了batch维度,归一化的维度为[C,H,W];

IN 归一化的维度为[H,W];

而GN介于LN和IN之间,其首先将channel分为许多组(group),对每一组做归一化,及先将feature的维度由[N, C, H, W]reshape为[N, G,C//G , H, W],归一化的维度为[C//G , H, W]

计算:

bn将单通道的特征图展开成一列(H,W), 与蓝色屏幕相垂直的任意一个平面就是一张图片在某层输出的所有特征图(C,HW),共有N个平面。 bn会将N张图片的同一个通道中所有数据整合起来(分通道整合,也就意味着每个通道都有自己的4个参数),用以下公式计算:

其中E(x)与Var(x)是通过滑动平均统计得到,存储在模型中的,而在gn中,则没有去统计E(x)与Var(x),仅仅保存了gama 和beta,E(x)与Var(x)会在前向过程中根据实际数据进行计算。gn是在单张图片的多个通道进行数据整和,这是跟bn最明显的差异。

 

 

 

 

事实上,GN的极端情况就是LN和I N,分别对应G等于C和G等于1,作者在论文中给出G设为32较好

2. Interpolate

torch.nn.functional.interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None)

根据给定的size或scale_factor参数来对输入进行下/上采样

支持目前的temporal(1D, 如向量数据), spatial(2D, 如jpg、png等图像数据)和volumetric(3D, 如点云数据)类型的采样数据作为输入,输入数据的格式为minibatch x channels x [optional depth] x [optional height] x width,具体为:

  • 对于一个temporal输入,期待着3D张量的输入,即minibatch x channels x width
  • 对于一个空间spatial输入,期待着4D张量的输入,即minibatch x channels x height x width
  • 对于体积volumetric输入,则期待着5D张量的输入,即minibatch x channels x depth x height x width

可用于重置大小的mode有:最近邻、线性(3D-only),、双线性, 双三次(bicubic,4D-only)和三线性(trilinear,5D-only)插值算法和area算法

参数:

  • input (Tensor) – 输入张量

  • size (int or Tuple[int] or Tuple[intint] or Tuple[intintint]) –输出大小.

  • scale_factor (float or Tuple[float]) – 指定输出为输入的多少倍数。如果输入为tuple,其也要制定为tuple类型

  • mode (str) – 可使用的上采样算法,有'nearest''linear''bilinear''bicubic' , 'trilinear'和'area'. 默认使用'nearest'

  • align_corners (booloptional) –几何上,我们认为输入和输出的像素是正方形,而不是点。如果设置为True,则输入和输出张量由其角像素的中心点对齐,从而保留角像素处的值。如果设置为False,则输入和输出张量由它们的角像素的角点对齐,插值使用边界外值的边值填充;当scale_factor保持不变时,使该操作独立于输入大小。仅当使用的算法为'linear''bilinear', 'bilinear'or 'trilinear'时可以使用。默认设置为False

注意:

使用mode='bicubic'时,可能会导致overshoot问题,即它可以为图像生成负值或大于255的值。如果你想在显示图像时减少overshoot问题,可以显式地调用result.clamp(min=0,max=255)。

When using the CUDA backend, this operation may induce nondeterministic behaviour in be backward that is not easily switched off. Please see the notes on Reproducibility for background.

 

https://www.cnblogs.com/wanghui-garcia/p/11399034.html

https://www.cnblogs.com/wanghui-garcia/p/11400866.html

3.Sampler

首先需要知道的是所有的采样器都继承自Sampler这个类,如下:

可以看到主要有三种方法:分别是:

  • __init__: 这个很好理解,就是初始化
  • __iter__: 这个是用来产生迭代索引值的,也就是指定每个step需要读取哪些数据
  • __len__: 这个是用来返回每次迭代器的长

 

Pytorch给我们提供的采样器:

1)sequentialsampler

顺序对数据集采样。

其原理是首先在初始化的时候拿到数据集data_source,之后在__iter__方法中首先得到一个和data_source一样长度的range可迭代器。每次只会返回一个索引值

class SequentialSampler(Sampler):
    r"""Samples elements sequentially, always in the same order.
    Arguments:
        data_source (Dataset): dataset to sample from
    """
   # 产生顺序 迭代器
    def __init__(self, data_source):
        self.data_source = data_source

    def __iter__(self):
        return iter(range(len(self.data_source)))

    def __len__(self):
        return len(self.data_source)

使用示例:

 

a = [1,5,78,9,68]
b = torch.utils.data.SequentialSampler(a)
for x in b:
    print(x)
    
>>> 0
    1
    2
    3
    4

 

2) RandomSampler

参数作用:

  • data_source: 同上
  • num_samples: 指定采样的数量,默认是所有。
  • replacement: 若为True,则表示可以重复采样,即同一个样本可以重复采样,这样可能导致有的样本采样不到。所以此时我们可以设置num_samples来增加采样数量使得每个样本都可能被采样到。
class RandomSampler(Sampler):
    r"""Samples elements randomly. If without replacement, then sample from a shuffled dataset.
    If with replacement, then user can specify ``num_samples`` to draw.
    Arguments:
        data_source (Dataset): dataset to sample from
        num_samples (int): number of samples to draw, default=len(dataset)
        replacement (bool): samples are drawn with replacement if ``True``, default=False
    """

    def __init__(self, data_source, replacement=False, num_samples=None):
        self.data_source = data_source
        self.replacement = replacement
        self.num_samples = num_samples

        if self.num_samples is not None and replacement is False:
            raise ValueError("With replacement=False, num_samples should not be specified, "
                             "since a random permute will be performed.")

        if self.num_samples is None:
            self.num_samples = len(self.data_source)

        if not isinstance(self.num_samples, int) or self.num_samples <= 0:
            raise ValueError("num_samples should be a positive integeral "
                             "value, but got num_samples={}".format(self.num_samples))
        if not isinstance(self.replacement, bool):
            raise ValueError("replacement should be a boolean value, but got "
                             "replacement={}".format(self.replacement))

    def __iter__(self):
        n = len(self.data_source)
        if self.replacement:
            return iter(torch.randint(high=n, size=(self.num_samples,), dtype=torch.int64).tolist())
        return iter(torch.randperm(n).tolist())

    def __len__(self):
        return len(self.data_source)

3) SubsetRandomSampler

 

class SubsetRandomSampler(Sampler):
    r"""Samples elements randomly from a given list of indices, without replacement.
    Arguments:
        indices (sequence): a sequence of indices
    """

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

    def __iter__(self):
        return (self.indices[i] for i in torch.randperm(len(self.indices)))

    def __len__(self):
        return len(self.indices)

这个采样器常见的使用场景是将训练集划分成训练集和验证集,示例如下:

_train = len(train_dataset)
split = n_train // 3
indices = random.shuffle(list(range(n_train)))
train_sampler = torch.utils.data.sampler.SubsetRandomSampler(indices[split:])
valid_sampler = torch.utils.data.sampler.SubsetRandomSampler(indices[:split])
train_loader = DataLoader(..., sampler=train_sampler, ...)
valid_loader = DataLoader(..., sampler=valid_sampler, ...)

4) WeightedRandomSampler

参数作用同上面的RandomSampler,不再赘述。

lass WeightedRandomSampler(Sampler):
    r"""Samples elements from [0,..,len(weights)-1] with given probabilities (weights).
    Arguments:
        weights (sequence)   : a sequence of weights, not necessary summing up to one
        num_samples (int): number of samples to draw
        replacement (bool): if ``True``, samples are drawn with replacement.
            If not, they are drawn without replacement, which means that when a
            sample index is drawn for a row, it cannot be drawn again for that row.
    """

    def __init__(self, weights, num_samples, replacement=True):
        if not isinstance(num_samples, _int_classes) or isinstance(num_samples, bool) or \
                num_samples <= 0:
            raise ValueError("num_samples should be a positive integeral "
                             "value, but got num_samples={}".format(num_samples))
        if not isinstance(replacement, bool):
            raise ValueError("replacement should be a boolean value, but got "
                             "replacement={}".format(replacement))
        self.weights = torch.tensor(weights, dtype=torch.double)
        self.num_samples = num_samples
        self.replacement = replacement

    def __iter__(self):
        return iter(torch.multinomial(self.weights, self.num_samples, self.replacement).tolist())

    def __len__(self):
        return self.num_samples  ## 指的是一次一共采样的样本的数量

 

5)BatchSampler

前面的采样器每次都只返回一个索引,但是我们在训练时是对批量的数据进行训练,而这个工作就需要BatchSampler来做。也就是说BatchSampler的作用就是将前面的Sampler采样得到的索引值进行合并,当数量等于一个batch大小后就将这一批的索引值返回

class BatchSampler(Sampler):
    r"""Wraps another sampler to yield a mini-batch of indices.
    Args:
        sampler (Sampler): Base sampler.
        batch_size (int): Size of mini-batch.
        drop_last (bool): If ``True``, the sampler will drop the last batch if
            its size would be less than ``batch_size``
    Example:
        >>> list(BatchSampler(SequentialSampler(range(10)), batch_size=3, drop_last=False))
        [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
        >>> list(BatchSampler(SequentialSampler(range(10)), batch_size=3, drop_last=True))
        [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
    """
# 批次采样
    def __init__(self, sampler, batch_size, drop_last):
        if not isinstance(sampler, Sampler):
            raise ValueError("sampler should be an instance of "
                             "torch.utils.data.Sampler, but got sampler={}"
                             .format(sampler))
        if not isinstance(batch_size, _int_classes) or isinstance(batch_size, bool) or \
                batch_size <= 0:
            raise ValueError("batch_size should be a positive integeral value, "
                             "but got batch_size={}".format(batch_size))
        if not isinstance(drop_last, bool):
            raise ValueError("drop_last should be a boolean value, but got "
                             "drop_last={}".format(drop_last))
        self.sampler = sampler
        self.batch_size = batch_size
        self.drop_last = drop_last

    def __iter__(self):
        batch = []
        for idx in self.sampler:
            batch.append(idx)
            if len(batch) == self.batch_size:
                yield batch
                batch = []
        if len(batch) > 0 and not self.drop_last:
            yield batch

    def __len__(self):
        if self.drop_last:
            return len(self.sampler) // self.batch_size
        else:
            return (len(self.sampler) + self.batch_size - 1) // self.batch_size

 

4 一文弄懂Pytorch的DataLoader, DataSet, Sampler之间的关系

见:

https://www.cnblogs.com/marsggbo/p/11308889.html

可以假设我们的数据是一组图像,每一张图像对应一个index,那么如果我们要读取数据就只需要对应的index即可,即上面代码中的indices,而选取index的方式有多种,有按顺序的,也有乱序的,所以这个工作需要Sampler完成,现在你不需要具体的细节,你只需要知道DataLoader和Sampler在这里产生关系。

DataLoader,Sampler和Dataset三者关系如下:

 

DataLoader 的源代码:

class DataLoader(object):
    def __init__(self, dataset, batch_size=1, shuffle=False, sampler=None,
                 batch_sampler=None, num_workers=0, collate_fn=default_collate,
                 pin_memory=False, drop_last=False, timeout=0,
                 worker_init_fn=None)

可以看到初始化参数里有两种sampler:samplerbatch_sampler,都默认为None。前者的作用是生成一系列的index,而batch_sampler则是将sampler生成的indices打包分组,得到一个又一个batch的index。例如下面示例中,BatchSamplerSequentialSampler生成的index按照指定的batch size分组。

>>>in : list(BatchSampler(SequentialSampler(range(10)), batch_size=3, drop_last=False))
>>>out: [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Pytorch中已经实现的Sampler有如前文

需要注意的是DataLoader的部分初始化参数之间存在互斥关系,这个你可以通过阅读源码更深地理解,这里只做总结:

  • 如果你自定义了batch_sampler,那么这些参数都必须使用默认值:batch_size, shuffle,sampler,drop_last.
  • 如果你自定义了sampler,那么shuffle需要设置为False
  • 如果samplerbatch_sampler都为None,那么batch_sampler使用Pytorch已经实现好的BatchSampler,而sampler分两种情况:
    • shuffle=True,则sampler=RandomSampler(dataset)
    • shuffle=False,则sampler=SequentialSampler(dataset)

自定义sampler等:

要做的就是定义好__iter__(self)函数,不过要注意的是该函数的返回值需要是可迭代的。例如SequentialSampler返回的是iter(range(len(self.data_source)))

另外BatchSampler与其他Sampler的主要区别是它需要将Sampler作为参数进行打包,进而每次迭代返回以batch size为大小的index列表。也就是说在后面的读取数据过程中使用的都是batch sampler。

Dataset定义方式如下:

class Dataset(object):
	def __init__(self):
		...
		
	def __getitem__(self, index):
		return ...
	
	def __len__(self):
		return ...

上面三个方法是最基本的,其中__getitem__是最主要的方法,它规定了如何读取数据。但是它又不同于一般的方法,因为它是python built-in方法,其主要作用是能让该类可以像list一样通过索引值对数据进行访问。假如你定义好了一个dataset,那么你可以直接通过dataset[0]来访问第一个数据。如果你想对__getitem__方法进行调试,你可以写一个for循环遍历dataset来进行调试了,而不用构建dataloader等一大堆东西了,建议学会使用ipdb这个库,非常实用!!!以后有时间再写一篇ipdb的使用教程。另外,其实我们通过最前面的Dataloader的__next__函数可以看到DataLoader对数据的读取其实就是用了for循环来遍历数据

class DataLoader(object): 
    ... 
     
    def __next__(self): 
        if self.num_workers == 0:   
            indices = next(self.sample_iter)  
            batch = self.collate_fn([self.dataset[i] for i in indices]) # this line 
            if self.pin_memory: 
                batch = _utils.pin_memory.pin_memory_batch(batch) 
            return batch

还有一个self.collate_fn方法

  • indices: 表示每一个iteration,sampler返回的indices,即一个batch size大小的索引列表
  • self.dataset[i]: 就是对第i个数据进行读取操作,一般来说self.dataset[i]=(img, label)

不难猜出collate_fn的作用就是将一个batch的数据进行合并操作。默认的collate_fn是将img和label分别合并成imgs和labels,所以如果你的__getitem__方法只是返回 img, label,那么你可以使用默认的collate_fn方法,但是如果你每次读取的数据有img, box, label等等,那么你就需要自定义collate_fn来将对应的数据合并成一个batch数据,这样方便后续的训练步骤。

 

 

 

 

 

参考:

http://www.dataguru.cn/article-13318-1.html

https://zhuanlan.zhihu.com/p/67424009

https://www.cnblogs.com/marsggbo/p/11541054.html

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值