Sampler类与4种采样方式

由于我们不能将大量数据一次性的放入网络中进行训练,所以需要分批进行数据读取,这一过程涉及到如何从数据集中读取数据的问题,pytorch提供了Sampler基类,与多个子类实现不同方式的数据采用。
子类包含:

  • Sequential Sampler: 顺序采样
  • Random Sampler: 随机采样
  • Subset Random Sampler : 子集随机采样
  • Weighted Random Sampler: 加权随机采样等。

基类Sampler

Class Sampler(object):
   def __init__(self,data_source):
      padd
   def __iter__(self):
      raise NotImplementedError

对于所有采样器来说, 都需要继承Sampler类,必须实现方法为__iter__(),也就是定义迭代器的行为,返回可迭代对象,除此之外,Sampler类,并没有定义任何其它的方法。

顺序采样:Sequential Sampler

Class SequentialSampler(Sampler)def __init__(self,data_source):
      self.data_source = data_source
  def __iter__(self):
     #iter()用来生成迭代器
     #
      return iter(range(len(self.data_source)))
  def __len__(self):
     return len(self.data_source)

顺序采用并没有定义过多的方法,其中初始化方法仅仅需要一个Dataset类对象作为参数,对于__len__()只负责返回数据源包含的数据个数,iter(): 负责返回一个可迭代对象,这个可迭代对象是由range产生的顺序数值序列,也就是说迭代是按照顺序进行的,前述几种方法都只需要self.data_source实现了__len__()方法,因为这几种方法仅仅使用了len (self.data_source)函数,所以下面采用了同样实现了__len__()的list类型来代替, D a t a s e t Dataset Dataset类型做测试。

定义数据和对应的采样器

#定义数据对应的采样器
data = list([17,22,3,41,8])
seq_sampler = sampler.SequentialSampler(data_source = data)
#迭代获取采用器生成的索引
for index in seq_sampler:
   print("index: {}, data: {}".format(str(index), str(data[index])))

得到下面的输出,说明Sequential Sampler产生的索引是顺序索引.

index: 0, data: 17
index: 1, data: 22
index: 2, data: 3
index: 3, data: 41
index: 4, data: 8

子随机采样Subset Random Sampler

Class SubsetRandomSampler(Sampler)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)
    

上述代码中,len()的作用是与前面几个类相同,依旧是返回数据集的长度,区别在于__iter__()放回的并不是随机数序列,而是通过随机数序列作为indices的索引,进而返回打乱的数据本身,需要注意的是仍然是采样时不重复的,也是通过randperm()函数来实现的,按照网上可以搜集到的资料,
Subset Random Sampler应该用于训练集、测试集和验证集的划分,下面将data划分为train和vall两个部分,再次指出__iter__()
返回的不是索引,而是索引对应的数据。

sub_sampler_train = sampler.SubsetRandomSampler(indices=data[0:2])
sub_sampler_val = sampler.SubsetRandomSampler(indices=data[2:])
# 下面是train输出
index: 17
index: 22
*************
# 下面是val输出
index: 8
index: 41
index: 3

加权随机采样Weighted RandomSampler

class WeightedRandomSampler(Sampler):
    r"""Samples elements from ``[0,..,len(weights)-1]`` with given probabilities (weights)."""
    def __init__(self, weights, num_samples, replacement=True):
         # ...省略类型检查
         # weights用于确定生成索引的权重
        self.weights = torch.as_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())

对于Weighted Random Sampler类__iter__()方法,replacement参数也就时用于控制采样是否有放回的,num_samples用于控制生成的个数,weights参数对应的是样本的权重,而不是类别的权重,
其中__iter__()方法返回的数值为随机数序列,只不过生成的随机数序列是按照 W e i g h t s Weights Weights指定的权重确定的,测试代码如下:

# 位置[0]的权重为0,位置[1]的权重为10,其余位置权重均为1.1
weights = torch.Tensor([0, 10, 1.1, 1.1, 1.1, 1.1, 1.1])
wei_sampler = sampler.WeightedRandomSampler(weights, 6, True)
# 下面是输出:
index: 1
index: 2
index: 3
index: 4
index: 1
index: 1

从输出可以看出,位置[1]由于权重较大,被采样的次数较多,位置[0]由于权重为0.所以没有被采样到,其余位置权重低,所以都仅仅被采样一次。

随机采样 RandomSampler

Class RandomSampler(Sampler)def __init__(self,data_source,replacement = False,num_samples = None):
  self.data_source = data_source
  #这个参数控制是否应该重复采样
  self.replacement = replacement
  self._num_samples = num_samples
  #省略类型检查
  @property
  def num_sample(self):
    #dataset size might change at runtime
    #初始化时不传入num_samples的时候使用数据源的长度
    if self._num_samples is None:
      return len(self.data_source)
     
  #返回数据集长度
  def __len__(self):
    return self.num_samples
`

``
什么是property* 简单来说就是一个类里面的方法一旦被@property装饰,就可以像使用属性一样地去调用这个方法,其能够简化调用者获取数据的流程,而且不用担心将属性暴露出来,有人对其进行赋值操作,(避免使用者的不合理操作)需要注意两点的是:
     * 调用被装饰方法时候是不用加括号的。
     * 方法定义的时候有且只能有一个self参数.
     
    最最要的是__iter__()方法,定义了核心的索引生成行为
    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.randomperm(n).tolist())
其中$if$判断返回了两种随机值,根据是否在初始化中给出,replacement参数决定是否重复采样,区别核心在于randint()函数生成的随机数字是可能包含重复数值的,而randperm()函数生成的随机数列是绝对不会包含重复数值的,下面分别测试是否使用replacement作为输入的情况,首先是不使用时:
ran_sampler = sampler.RandomSampler(data_source = data)
#得到下面的输出
ran_sampler = sampler.RandomSampler(data_source = data)
index: 0, data: 17
index: 2, data: 3
index: 3, data: 41
index: 4, data: 8
index: 1, data: 22
可以看出生成的随机索引是不重复的,下面采用replacement参数情况。
ran_sampler = sampler.RandomSampler(data_source=data, replacement=True)

# 得到下面的输出
index: 0, data: 17
index: 4, data: 8
index: 3, data: 41
index: 4, data: 8
index: 2, data: 3
由于生成的随机数值是有重复的,也就是说第4条数据可能会被重复采样.
 
# 批采样BatchSampler

```python
class BatchSampler(Sampler):
    r"""Wraps another sampler to yield a mini-batch of indices."""
    def __init__(self, sampler, batch_size, drop_last):# ...省略类型检查
        # 定义使用何种采样器Sampler
        self.sampler = sampler
        self.batch_size = batch_size
        # 是否在采样个数小于batch_size时剔除本次采样
        self.drop_last = drop_last
    def __iter__(self):
        batch = []
        for idx in self.sampler:
            batch.append(idx)
            # 如果采样个数和batch_size相等则本次采样完成
            if len(batch) == self.batch_size:
                yield batch
                batch = []
        # for结束后在不需要剔除不足batch_size的采样个数时返回当前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

在定义好各种采样器之后,需要进行batch采样,BatchSampler类的**init()函数中sampler参数对应前面介绍的XxxSampler类实例,也就是采样方式的定义**;drop_last为“True”时,如果采样得到的数据个数小于batch_size则抛弃本个batch的数据。对于__init__()中的for循环,作用应该是以**“生成器”的方式不断的从sampler中获取batch**,比如sampler中包含1.5个batch_size的数据,那么在drop_last为False的情况下,for循环就会yield出个batch,下面的例子中batch sampler采用的采样器为顺序采样器

seq_sampler = sampler.SequentialSampler(data_source=data)
batch_sampler = sampler.BatchSampler(seq_sampler, 3, False)
# 下面是输出
batch: [0, 1, 2]
batch: [3, 4]

总结

对于深度神经网络来说,数据的读取方式对网络的优化过程是有很大影响的,比如常见的策略为打乱数据集,使得每个epoch中各个Batch包含的数据是不同的,在训练网络时,应该从上述几种采样策略中进行选择,从而确定最优的方式。

采样方式理论介绍

    • 顺序采样 Sequential Sampler
  • 随机采样 RandomSampler
  • 子集随机采样: Subset Random Sampler
  • 加权随机采样:Weighted Random Sampler
  • 批采样:BatchSampler

顺序采样

  • 顺序逐个采样数据

RandomSampler 随机采样

  • data_source :为数据集
  • replacement: 是否为有放回采样

RandSampler:当replacement开关关闭时,返回原数据集长度下标数组随机打乱后的采样值。而当replacement开关打开时,则根据n_samples长度生成采样序列长度。
具体可见如下代码,在replacement=False时,RandomSampler对数组t下标随机打乱输出,迭代器长度与源数据长度一致。
当replacement=True并设定num_samples=20,这时迭代器长度大于源数据,故会出现重复值

SubsetRandomSampler索引随机采样

  • torch.randperm(): 对数组随机排序。
  • indices为给定的下标数组

所以SubsetRandomSampler的功能时为给定一个数据集下标后,对该下标数组随机排序,然后不放回取样。

加权随机采样

  • weights:为index权重,权重越大取到的概率越高。
  • num_samples: 生成的采样长度。
  • replacement: 是否为有放回取样。
  • multinomial: 伯努利随机数生成函数,也就是根据概率设定生成
    { 0 , 1 , 2 , ⋯   , n 0,1,2,\cdots,n 0,1,2,,n}
    当不放回取样时,replacement = False 若num_samples小于输入数组权重非零值个数,那么非零权重大小基本上不起什么作用,反正所有的值都会取到一次。
    当放回取样时,权重越大取到的概率越高。

批采样

其中

  • drop_last: 为布尔类型值,当其为真时,如果数据集长度不是batch_size长度的整数倍,最后一批数据将会被丢弃。

你可能感兴趣的:(Torch的使用及参数解释,python,人工智能,深度学习)