PyTorch的数据读取机制

写在前面

虽然PyTorch实战上手较快,但是由于之前并没有系统的学习PyTorch,对于其使用逻辑也不是很清楚。所以本人在日常阅读论文源码过程中,发现可以读懂别人编写的模型,但是自己亲手搭建神经网络的时候却无从下手(基础不足)。故想要通过学习PyTorch框架了解其工作机制,进而形成自己的知识体系后便于学习其他更厉害的(当然一切目标是发论文!冲冲冲!!!)。
这一系列文章均以 ABSA(细粒度情感分析)中的 AEN模型 为基础进行PyTorch的学习。
AEN具体模型结构可参考Attentional Encoder Network for Targeted Sentiment Classificatio 论文解读

模型学习分为五大模块:数据、模型、损失函数、优化器、迭代训练(前四个)
首先来学习PyTorch中的数据读取机制

1. DataLoader

torch.utils.data.DataLoader() :构建一个可迭代的数据装载器,在训练的时候,每一个for循环,每一次iteration,就是从DataLoader中获取一个batch_size大小的数据的。

我们首先在train.py(训练代码)中找到数据读取模块,如下:

# 构建ABSADataset实例
self.trainset = ABSADataset(opt.dataset_file['train'], tokenizer)
self.testset = ABSADataset(opt.dataset_file['test'], tokenizer)
......
......
......

train_data_loader = DataLoader(dataset=self.trainset, batch_size=self.opt.batch_size, shuffle=True)
test_data_loader = DataLoader(dataset=self.testset, batch_size=self.opt.batch_size, shuffle=False)
val_data_loader = DataLoader(dataset=self.valset, batch_size=self.opt.batch_size, shuffle=False)

接下来进入到 DataLoder.py 源码中查看 DataLoader 类:

DataLoader(dataset: Dataset[T_co],
    	   batch_size: Optional[int],
   		   num_workers: int,
    	   pin_memory: bool,
    	   drop_last: bool,
    	   timeout: float,
           sampler: Sampler,
   		   prefetch_factor: int,
   		   _iterator : Optional['_BaseDataLoaderIter'],
           __initialized = False)

DataLoader 的参数很多,通常主要用以下5个:

  • dataset:Dataset类,决定数据从哪读取以及如何读取;
  • batch_size:批大小;
  • num_works:是否多进程读取机制;
  • shuffle:每个Epoch是否乱序;
  • drop_last:当样本数不能被batch_size整除时,是否舍弃最后一批数据。

三个小概念:(1)Epoch:所有训练样本都已输入到模型中,是一个Epoch;(2)Iteration:一批样本输入到模型中,称为一个Iteration;(3)Batch_size:一批样本的大小,决定一个Epoch有多少个Iteration。

2、DataSet

torch.utils.data.Dataset() :Dataset抽象类,所有自定义的Dataset都需要继承它,并且必须复写__getitem__()类方法
从创建的 ABSADataset 类中进入到Dataset.py源码:

class Dataset(Generic[T_co]):

    functions: Dict[str, Callable] = {}

    def __getitem__(self, index) -> T_co:
        raise NotImplementedError

    def __add__(self, other: 'Dataset[T_co]') -> 'ConcatDataset[T_co]':
        return ConcatDataset([self, other])

__getitem__()方法是Dataset的核心,作用是接收一个索引,返回一个样本。参数里面接收index,我们需要编写究竟如何根据这个索引去读取数据部分。

3. 怎么用?

关于数据读取的三个问题:
(1)读哪些数据?
(2)从哪里读数据?
(3)如何读数据?

有关部分代码截取

        # 构建ABSADataset实例
        self.trainset = ABSADataset(opt.dataset_file['train'], tokenizer)
        self.testset = ABSADataset(opt.dataset_file['test'], tokenizer)
		......
		......
		......


        # 构建DataLoader
        # 接收的参数是上面的trainset(即ABSADataset),还跟了一个batch_size
        # 有了一个batch的样本数量,有了样本总数,就能得到总共有多少个batch
        # DataLoader做的事情:我们指定了batch_size(假设指定为10,总共有100个训练样本,计算出批数是10,那么DataLoader将样本分成10批顺序打乱的数据)
        train_data_loader = DataLoader(dataset=self.trainset, batch_size=self.opt.batch_size, shuffle=True)
        test_data_loader = DataLoader(dataset=self.testset, batch_size=self.opt.batch_size, shuffle=False)
        val_data_loader = DataLoader(dataset=self.valset, batch_size=self.opt.batch_size, shuffle=False)
		......
		......
		......
		
	# 位于主函数中
    dataset_files = {
        'twitter': {
            'train': './datasets/acl-14-short-data/train.raw',
            'test': './datasets/acl-14-short-data/test.raw'
        },
        'restaurant': {
            'train': './datasets/semeval14/Restaurants_Train.xml.seg',
            'test': './datasets/semeval14/Restaurants_Test_Gold.xml.seg'
        },
        'laptop': {
            'train': './datasets/semeval14/Laptops_Train.xml.seg',
            'test': './datasets/semeval14/Laptops_Test_Gold.xml.seg'
        }
    }

首先是数据路径dataset_files部分,即训练集和测试集的位置,我们的模型将会从这里读入数据,然后是tokenizer的数据预处理部分(这块的预处理部分比较简单),主要来看一下所创建 ABSADataset 实例后面的 DataLoader。

我们从self.trainset = ABSADataset(opt.dataset_file['train'], tokenizer)开始看起,核心是 ABSADataset,这个是自己实现的一个类,继承了抽象类Dataset,并重写了__getitem__()方法,这个类的目的就是传入数据路径和预处理部分,然后返回数据:

class ABSADataset(Dataset):
    def __init__(self, fname, tokenizer):
        """

        :param fname: 数据集所在路径
        :param tokenizer: 数据预处理
        """
        fin = open(fname, 'r', encoding='utf-8', newline='\n', errors='ignore')
        lines = fin.readlines()
        fin.close()
        fin = open(fname+'.graph', 'rb')
        idx2graph = pickle.load(fin)
        fin.close()

        all_data = []    # 这里!! 下面就是对数据集进行了预处理,然后返回最下面大括号里面的一系列东西(一个列表)
        for i in range(0, len(lines), 3):
            text_left, _, text_right = [s.lower().strip() for s in lines[i].partition("$T$")]
            aspect = lines[i + 1].lower().strip()
            polarity = lines[i + 2].strip()    # 看数据集文件理解!

            text_indices = tokenizer.text_to_sequence(text_left + " " + aspect + " " + text_right)
            context_indices = tokenizer.text_to_sequence(text_left + " " + text_right)
            left_indices = tokenizer.text_to_sequence(text_left)
            left_with_aspect_indices = tokenizer.text_to_sequence(text_left + " " + aspect)
            right_indices = tokenizer.text_to_sequence(text_right, reverse=True)
            right_with_aspect_indices = tokenizer.text_to_sequence(aspect + " " + text_right, reverse=True)
            aspect_indices = tokenizer.text_to_sequence(aspect)
            left_len = np.sum(left_indices != 0)
            aspect_len = np.sum(aspect_indices != 0)
            aspect_boundary = np.asarray([left_len, left_len + aspect_len - 1], dtype=np.int64)
            polarity = int(polarity) + 1

            text_len = np.sum(text_indices != 0)
            concat_bert_indices = tokenizer.text_to_sequence('[CLS] ' + text_left + " " + aspect + " " + text_right + ' [SEP] ' + aspect + " [SEP]")
            concat_segments_indices = [0] * (text_len + 2) + [1] * (aspect_len + 1)
            concat_segments_indices = pad_and_truncate(concat_segments_indices, tokenizer.max_seq_len)

            text_bert_indices = tokenizer.text_to_sequence("[CLS] " + text_left + " " + aspect + " "
                                                           + text_right + " [SEP]")
            aspect_bert_indices = tokenizer.text_to_sequence("[CLS] " + aspect + " [SEP]")

            dependency_graph = np.pad(idx2graph[i], ((0, tokenizer.max_seq_len-idx2graph[i].shape[0]),
                                                     (0, tokenizer.max_seq_len-idx2graph[i].shape[0])),
                                      'constant')

            data1 = {
                'concat_bert_indices': concat_bert_indices,
                'concat_segments_indices': concat_segments_indices,
                'text_bert_indices': text_bert_indices,
                'aspect_bert_indices': aspect_bert_indices,
                'text_indices': text_indices,
                'context_indices': context_indices,
                'left_indices': left_indices,
                'left_with_aspect_indices': left_with_aspect_indices,
                'right_indices': right_indices,
                'right_with_aspect_indices': right_with_aspect_indices,
                'aspect_indices': aspect_indices,
                'aspect_boundary': aspect_boundary,
                'dependency_graph': dependency_graph,
                'polarity': polarity,
            }

            all_data.append(data1)
        self.data2 = all_data    # 又调用了all_data,找到all_data看做了什么事情

    def __getitem__(self, index):
        return self.data2[index]
        # 只要给定了index,就可以通过这句代码获取样本
        # data是ABSADataset这个类的成员,再往上找

    # 告诉机器一共有多少个样本数据。 返回总的样本个数
    def __len__(self):
        return len(self.data2)

我们可以看到__getitem__()函数中的data[index],只要给定了这个index,就可以通过这句代码获取样本。

【源代码中的data有点多,理解有困难,下面解释的时候将all_data.append(data)这句及以上的data改称为 data1,以下改称为 data2】

那这个data[index]做了什么事情呢?我们 ctrl+左键 看看这个data2,跑到了初始化__init__部分中的self.data2 = all_data,再继续看all_data,又all_data.append(data1)。上面代码中,初始化了一个all_data=[],在找到数据路径之后读取数据并将其预处理,预处理的结果data被一条一条封装成字典形式(看代码中data部分)存入all_data,最终形成了一个list,list的每个元素是dict,格式为 [{样本1}, {样本2}, {样本3}, …, {样本n}]。所以,data2拿到了这个list,有了这个list,然后又给了data2一个index,那么取数据就容易了。data2[index]就可以取出某个{样本i}。

接下来看代码train_data_loader = DataLoader(dataset=self.trainset, batch_size=self.opt.batch_size, shuffle=True),DataLoader类接收(1)参数 ABSADataset ,我们知道其返回一个样本(dict);后面又跟了一个(2)batch_size,这个就是说一个batch里有多少个样本,如果有了一个batch的样本数量和样本总数,那么就能得到总共有多少个batch;(3)shuffle,就是将数据打乱一下。
接下来我们看看具体使用过程中,每一批数据是如何获得的?

# 训练部分核心
# 两层循环,外循环表示迭代Epoch(全部训练样本喂入模型一次);
# 内循环表示的批次的循环,每一个Epoch中,都是一批一批的喂入
for i_epoch in range(self.opt.num_epoch):
    logger.info('>' * 100)
    logger.info('epoch: {}'.format(i_epoch))
    n_correct, n_total, loss_total = 0, 0, 0
    # switch model to training mode
    self.model.train()

    # 数据读取具体使用的核心
    # 利用enumerate进行迭代 获取一批批的数据,然后训练模型。这样所有批次的数据都喂入模型,完后了一次epoch
    for i_batch, batch in enumerate(train_data_loader):
        global_step += 1
        # clear gradient accumulators
        optimizer.zero_grad()

        # forward
        inputs = [batch[col].to(self.opt.device) for col in self.opt.inputs_cols]
        outputs = self.model(inputs)
        targets = batch['polarity'].to(self.opt.device)

        # compute loss
        loss = criterion(outputs, targets)

        # backward
        loss.backward()
        # update weights
        optimizer.step()

数据读取的核心for i_batch, batch in enumerate(train_data_loader),我们debug一下看看这个函数是如何得到数据的?
PyTorch的数据读取机制_第1张图片

点击下面的stepinto步入这个函数里面,看看调用DataLoader中的哪个方法:
PyTorch的数据读取机制_第2张图片
可以看到,程序跳转到了DataLoader的__iter__(self)方法,发现它是一个判断,在说是用单进程还是用多进程读取机制进行处理,与读取数据无关,所以使用stepover进行下一步。
明天再写吧

  1. 读哪些数据?
    根据 Sampler(DataLoader源码中)输出的 index 决定。

  2. 从哪里读数据?
    根据Dataset中设置的数据路径读取数据。

  3. 如何读数据?
    Dataset的 __getitem__() 方法,可以帮助我们获取一个样本。

代码示例(以下代码均选自细粒度情感分析的AEN模型):

        # 构建ABSADataset实例
        self.trainset = ABSADataset(opt.dataset_file['train'], tokenizer)
        self.testset = ABSADataset(opt.dataset_file['test'], tokenizer)
        # 构建DataLoader
        # 接收的参数是上面的trainset(即ABSADataset),还跟了一个batch_size
        # 有了一个batch的样本数量,有了样本总数,就能得到总共有多少个batch
        # DataLoader做的事情:我们指定了batch_size(假设指定为10,总共有100个训练样本,计算出批数是10,那么DataLoader将样本分成10批顺序打乱的数据)
        train_data_loader = DataLoader(dataset=self.trainset, batch_size=self.opt.batch_size, shuffle=True)
        test_data_loader = DataLoader(dataset=self.testset, batch_size=self.opt.batch_size, shuffle=False)
        val_data_loader = DataLoader(dataset=self.valset, batch_size=self.opt.batch_size, shuffle=False)

		# 训练部分核心
        # 有两层循环,外循环表示迭代Epoch(全部训练样本喂入模型一次);
        # 内循环表示的批次的循环,每一个Epoch中,都是一批一批的喂入
        for i_epoch in range(self.opt.num_epoch):
            logger.info('>' * 100)
            logger.info('epoch: {}'.format(i_epoch))
            n_correct, n_total, loss_total = 0, 0, 0
            # switch model to training mode
            self.model.train()

            # 数据读取具体使用的核心
            # 利用enumerate进行迭代 获取一批批的数据,然后训练模型。这样所有批次的数据都喂入模型,完后了一次epoch
            for i_batch, batch in enumerate(train_data_loader):
                global_step += 1
                # clear gradient accumulators
                optimizer.zero_grad()

                # forward
                inputs = [batch[col].to(self.opt.device) for col in self.opt.inputs_cols]
                outputs = self.model(inputs)
                targets = batch['polarity'].to(self.opt.device)

                # compute loss
                loss = criterion(outputs, targets)

                # backward
                loss.backward()
                # update weights
                optimizer.step()
class ABSADataset(Dataset):
    '''
    Dataset抽象类,所有自定义的Dataset都需要继承它,必须复写__getitem__()这个类方法。
    这个类的目的就是 传入数据的路径 ,还有 预处理部分
    '''
    def __init__(self, fname, tokenizer):
        """

        :param fname: 数据集所在路径
        :param tokenizer: 数据预处理
        """
        fin = open(fname, 'r', encoding='utf-8', newline='\n', errors='ignore')
        lines = fin.readlines()
        fin.close()
        fin = open(fname+'.graph', 'rb')
        idx2graph = pickle.load(fin)
        fin.close()

        all_data = []    # 这里!! 下面就是对数据集进行了预处理,然后返回最下面大括号里面的一系列东西(一个列表)
        for i in range(0, len(lines), 3):
            text_left, _, text_right = [s.lower().strip() for s in lines[i].partition("$T$")]
            aspect = lines[i + 1].lower().strip()
            polarity = lines[i + 2].strip()    # 看数据集文件理解!

            text_indices = tokenizer.text_to_sequence(text_left + " " + aspect + " " + text_right)
            context_indices = tokenizer.text_to_sequence(text_left + " " + text_right)
            left_indices = tokenizer.text_to_sequence(text_left)
            left_with_aspect_indices = tokenizer.text_to_sequence(text_left + " " + aspect)
            right_indices = tokenizer.text_to_sequence(text_right, reverse=True)
            right_with_aspect_indices = tokenizer.text_to_sequence(aspect + " " + text_right, reverse=True)
            aspect_indices = tokenizer.text_to_sequence(aspect)
            left_len = np.sum(left_indices != 0)
            aspect_len = np.sum(aspect_indices != 0)
            aspect_boundary = np.asarray([left_len, left_len + aspect_len - 1], dtype=np.int64)
            polarity = int(polarity) + 1   # 极性变成0,1,2??

            text_len = np.sum(text_indices != 0)
            concat_bert_indices = tokenizer.text_to_sequence('[CLS] ' + text_left + " " + aspect + " " + text_right + ' [SEP] ' + aspect + " [SEP]")
            concat_segments_indices = [0] * (text_len + 2) + [1] * (aspect_len + 1)
            concat_segments_indices = pad_and_truncate(concat_segments_indices, tokenizer.max_seq_len)

            text_bert_indices = tokenizer.text_to_sequence("[CLS] " + text_left + " " + aspect + " "
                                                           + text_right + " [SEP]")
            aspect_bert_indices = tokenizer.text_to_sequence("[CLS] " + aspect + " [SEP]")

            dependency_graph = np.pad(idx2graph[i], ((0, tokenizer.max_seq_len-idx2graph[i].shape[0]),
                                                     (0, tokenizer.max_seq_len-idx2graph[i].shape[0])),
                                      'constant')

            data = {
                'concat_bert_indices': concat_bert_indices,
                'concat_segments_indices': concat_segments_indices,
                'text_bert_indices': text_bert_indices,
                'aspect_bert_indices': aspect_bert_indices,
                'text_indices': text_indices,
                'context_indices': context_indices,
                'left_indices': left_indices,
                'left_with_aspect_indices': left_with_aspect_indices,
                'right_indices': right_indices,
                'right_with_aspect_indices': right_with_aspect_indices,
                'aspect_indices': aspect_indices,
                'aspect_boundary': aspect_boundary,
                'dependency_graph': dependency_graph,
                'polarity': polarity,
            }

            all_data.append(data)
        self.data = all_data    # 又调用了all_data,找到all_data看做了什么事情

    '''
    __getitem__()方法  是Dataset的核心,作用:接收一个索引,返回一个样本
    参数里面接受index,然后我们需要编写 如何根据这个索引去读取我们的数据部分。
    '''
    def __getitem__(self, index):
        return self.data[index]
        # 只要给定了index,就可以通过这句代码获取样本
        # data是ABSADataset这个类的成员,再往上找

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

关于第一条,自己可以尝试debug一下,会很清晰。
DataLoader 读取数据过程如下:
PyTorch的数据读取机制_第3张图片
总结:
DataLoader的作用就是构建一个数据装载器,根据我们提供的batch_size的大小,将数据样本分成一个个batch去训练模型,而这个分的过程中需要把数据取到,这就要借助Dataset中的__getitem__()方法。

自己利用PyTorch编写代码时,首先应该自己写一个 XXDataset类,这个类要继承Dataset类并且实现里面的__getitem__(),在这里面告诉机器怎样读取数据。还有__len__() 方法,这个方法是告诉机器一共有多少个样本数据,实际编写就是返回总的样本个数即可,可参考以上代码。

之后,机器就可以根据 Dataset 去硬盘中读取数据,接下来就是用 DataLoder 构建一个可迭代的数据装载器,传入如何读取数据的机制Dataset,传入batch_size,就可以返回一批批的数据。这个 DataLoader 是在模型训练的时候使用。
PyTorch的数据读取机制_第4张图片

你可能感兴趣的:(深度学习,pytorch,深度学习,python)