examples/timit/data/load_dataset_ctc.py
#! /usr/bin/env python # -*- coding: utf-8 -*- """Load dataset for the CTC model (TIMIT corpus). In addition, frame stacking and skipping are used. You can use only the single GPU version. """ from __future__ import absolute_import from __future__ import division from __future__ import print_function from os.path import join, isfile import pickle import numpy as np # 首先从这里引入了DatasetBase这个类 from utils.dataset.ctc import DatasetBase # Dataset 继承了 DatasetBase这个类,再去看DatasetBase 这个类可以看到继承了Base这个类 class Dataset(DatasetBase): def __init__(self, data_type, label_type, batch_size, max_epoch=None, splice=1, num_stack=1, num_skip=1, shuffle=False, sort_utt=False, sort_stop_epoch=None, progressbar=False): """A class for loading dataset. Args: data_type (string): train or dev or test label_type (string): phone39 or phone48 or phone61 or character or character_capital_divide batch_size (int): the size of mini-batch max_epoch (int, optional): the max epoch. None means infinite loop. splice (int, optional): frames to splice. Default is 1 frame. num_stack (int, optional): the number of frames to stack num_skip (int, optional): the number of frames to skip shuffle (bool, optional): if True, shuffle utterances. This is disabled when sort_utt is True. sort_utt (bool, optional): if True, sort all utterances by the number of frames and utteraces in each mini-batch are shuffled. Otherwise, shuffle utteraces. sort_stop_epoch (int, optional): After sort_stop_epoch, training will revert back to a random order progressbar (bool, optional): if True, visualize progressbar """ # 这里先调用了父类的构造函数,这里没有参数 super(Dataset, self).__init__() self.is_test = True if data_type == 'test' else False self.data_type = data_type self.label_type = label_type self.batch_size = batch_size self.max_epoch = max_epoch self.splice = splice self.num_stack = num_stack self.num_skip = num_skip self.shuffle = shuffle self.sort_utt = sort_utt self.sort_stop_epoch = sort_stop_epoch self.progressbar = progressbar self.num_gpu = 1 # paths where datasets exist # 设置dataset路径 dataset_root = ['/data/inaguma/timit', '/n/sd8/inaguma/corpus/timit/dataset'] input_path = join(dataset_root[0], 'inputs', data_type) # NOTE: ex.) save_path: timit_dataset_path/inputs/data_type/***.npy label_path = join(dataset_root[0], 'labels', data_type, label_type) # NOTE: ex.) save_path: # timit_dataset_path/labels/data_type/label_type/***.npy # Load the frame number dictionary # 加载帧数字典,看后面的代码可以知道字典键名是文件名,键值是帧数 # 加载的是 .pickle文件, pickle存储的是结构化文件,如用pickle.dump()存储一个字典,再pickle.load()出来就是字典,而不是文本 # 这里的if-else 就是为了判断刚才两个目录哪个下面有这个文件 if isfile(join(input_path, 'frame_num.pickle')): with open(join(input_path, 'frame_num.pickle'), 'rb') as f: self.frame_num_dict = pickle.load(f) else: dataset_root.pop(0) input_path = join(dataset_root[0], 'inputs', data_type) label_path = join(dataset_root[0], 'labels', data_type, label_type) with open(join(input_path, 'frame_num.pickle'), 'rb') as f: self.frame_num_dict = pickle.load(f) # Sort paths to input & label # 为输入和标签的路径排序 axis = 1 if sort_utt else 0 # 刚才加载了frame_num_dict,一个字典。 字典.items()就是字典的值,这一句是常见的字典排序代码 # key=lambda x:x[axis] 表示是按键名还是按键值排序,如果是x[0],则按键名,默认是升序 # 注意sorted函数返回的是列表,不是字典。返回的列表中每个元素对应的是一个由键名和键值组成的元组 frame_num_tuple_sorted = sorted(self.frame_num_dict.items(), key=lambda x: x[axis]) input_paths, label_paths = [], [] # 遍历排好序的列表,按照这个顺序将输入和标签添加到两个列表中 for input_name, frame_num in frame_num_tuple_sorted: # 前面只是设置了输入路径,现在是将具体文件的路径添加到输入和标签的列表中 input_paths.append(join(input_path, input_name + '.npy')) label_paths.append(join(label_path, input_name + '.npy')) # 现在input_paths 和 label_paths中保存的都是文件路径 # 转化为numpy矩阵 # 这里的self.input_paths和 self.label_paths 继承了父类的成员 self.input_paths = np.array(input_paths) self.label_paths = np.array(label_paths) # NOTE: Not load dataset yet # **这里还没加载数据,那在哪里加载的?** # set是无序不重复集合,这里取了input_paths的个数,然后生成了一个set。但是好像成员为数字时set中数据是有序的 # self.rest应该是继承自父类的成员 self.rest = set(range(0, len(self.input_paths), 1))可以看到这个Dataset中代码的主要作用是从一个文件中读取文件路径,将其保存到一个列表中。这个列表是在父类中进行了处理。
在给模型训练数据时,需要先得到一个list,list中保存了文件名,和相关信息,如这个工程就是保存了帧数。 一般需要对这个list的顺序进行一些处理,如shuffle,即随机打乱。如果输入为语音,也可以按长度排序。
utils/dataset/ctc
#! /usr/bin/env python # -*- coding: utf-8 -*- """Base class for loading dataset for the CTC model. In this class, all data will be loaded at each step. You can use the multi-GPU version. """ # 这里注释说了这个类作用是为CTC模型加载数据,可以用多GPU from __future__ import absolute_import from __future__ import division from __future__ import print_function from os.path import basename import random import numpy as np from utils.dataset.base import Base from utils.io.inputs.frame_stacking import stack_frame from utils.io.inputs.splicing import do_splice # 继承了Base类,Base类在 utils/dataset/base.py 中 class DatasetBase(Base): # 构造函数的参数,这里是可变参数 # *args表示任何多个无名参数,它是一个tuple # **kwargs表示关键字参数即有名字的参数,它是一个dict,但是调用这个函数时参数形式为 xx==yy def __init__(self, *args, **kwargs): # 这里调用了父类的构造函数 super(DatasetBase, self).__init__(*args, **kwargs) # 类中的特殊函数,如果定义了__getitem__(self,key),那么当时DatasetBase[x]时,就会调用这个函数,x传给这里的index。 # 常见的元组,列表等容器就是用了这种方式,它们本身也就是不同的class。 def __getitem__(self, index): input_i = np.array(self.input_paths[index]) label_i = np.array(self.label_paths[index]) return (input_i, label_i) # __net__ () 也是特殊的函数,当使用 for ... in b 语句时可以理解为调用了这个函数,每迭代一次就调用一次 # ...是有格式的,是按照这里return的格式 def __next__(self, batch_size=None): """Generate each mini-batch. Args: batch_size (int, optional): the size of mini-batch Returns: A tuple of `(inputs, labels, inputs_seq_len, input_names)` inputs: list of input data of size `[num_gpu, B, T_in, input_size]` labels: list of target labels of size `[num_gpu, B, T_out]` inputs_seq_len: list of length of inputs of size `[num_gpu, B]` input_names: list of file name of input data of size `[num_gpu, B]` is_new_epoch (bool): If true, 1 epoch is finished """ # 函数的作用是生成每个mini_batch # 这里还是没清楚这里输入的数据格式,B指的是数据? # 如果到达了设定的条件,就停止迭代,即for in 执行完毕 if self.max_epoch is not None and self.epoch >= self.max_epoch: raise StopIteration # NOTE: max_epoch = None means infinite loop if batch_size is None: batch_size = self.batch_size # reset if self.is_new_epoch: self.is_new_epoch = False if not self.is_test: self.padded_value = -1 else: self.padded_value = None # TODO(hirofumi): move this # 虽然前面知道了utt的作用。但是这里终于知道utt是什么的缩写的。 # 这一部分是对输入按长度进行排序或shuffle,即打乱顺序 # 注意这里获得 data_indices 后,self.rest 这个set是剪去了取出的这部分的,避免下次取再取相同的 # data_indices 是一个列表,除了最后一个minibatch,都包含了batch_size个数 if self.sort_utt: # Sort all uttrances by length if len(self.rest) > batch_size: data_indices = sorted(list(self.rest))[:batch_size] self.rest -= set(data_indices) # NOTE: rest is uttrance length order else: # Last mini-batch data_indices = list(self.rest) self.reset() self.is_new_epoch = True self.epoch += 1 if self.epoch == self.sort_stop_epoch: self.sort_utt = False self.shuffle = True # Shuffle data in the mini-batch random.shuffle(data_indices) elif self.shuffle: # Randomly sample uttrances if len(self.rest) > batch_size: data_indices = random.sample(list(self.rest), batch_size) self.rest -= set(data_indices) else: # Last mini-batch data_indices = list(self.rest) self.reset() self.is_new_epoch = True self.epoch += 1 # Shuffle selected mini-batch random.shuffle(data_indices) else: if len(self.rest) > batch_size: data_indices = sorted(list(self.rest))[:batch_size] self.rest -= set(data_indices) # NOTE: rest is in name order else: # Last mini-batch data_indices = list(self.rest) self.reset() self.is_new_epoch = True self.epoch += 1 # Load dataset in mini-batch # 将数据加载到mini-batch中, # list() 函数是将元组转化为列表,也可以将numpy array 降低一个维度然后返回一个列表,这里好像不用这个函数也行 # 这里将每个输入或label的.npy 文件读入后按行拆开 # map() 函数是为后一个参数的所有成员执行第一个参数的函数 # map() 返回的是一个list # 这里map() 返回的列表中,每个元素是一个numpy array,它们的shape不一定相同 # np.load() 是加载 .npy文件,这里用了 lambda 表达式。 功能是加载每个map()第二个参数中指定路径的.npy文件 # np.take() 功能是从 input_paths中取出下标为data_indices的元素,axis为沿着哪个轴取,详细的还是看numpy文档吧 # data_indices 是多少? data_indices 就是上面shuffle或sort过的 # 同时上面的代码也考虑到了最后一个minibatch的大小 # input_list 中保存的是数据,不是路径了 # input_list 是一个numpy 矩阵 # 注意这里 np.array() 的参数是一个列表,这个列表的每个元素代表一个语音信息,后面可以知道每个元素是一个二维矩阵,每个矩阵的列数相等,行数不相等。 # 因为每个矩阵的一行中的数据代表特征量,特征量都是相等的。 # 行数代表长度,不一定是相等的。 input_list = np.array(list( map(lambda path: np.load(path), np.take(self.input_paths, data_indices, axis=0)))) # 同上,label_list也是一个numpy 矩阵。 # np.array()的参数是一个列表,列表中的每个元素代表一个标签,每个元素是一个numpy array,后面可以看到每个元素都是一维向量 label_list = np.array(list( map(lambda path: np.load(path), np.take(self.label_paths, data_indices, axis=0)))) # hasattr() 函数就是如同这个名称,作用是判断对象是否包含属性,这里判断 self 是否包含 input_size ,可以看到前面是没有对 self.input_size 进行赋值的 # 如果没包含的话,就给self.input_size赋值 if not hasattr(self, 'input_size'): # shape[1] 为列数 # input_size为什么等于 shape[1],说明input_list[0]不是一维的? # 猜测 input_list[0] 的每行代表语音信号一个点的信息,如语音信号经过mfcc变换后成为一个13维向量。 行数表示这个语音信号的timestep。 # **这样的话,那么要读取的.npy文件中就是一个二维矩阵,行数代表序列的timestep** # 这样后面取最大值也可以理解了 # 做nlp时,一般需要把一个词映射为一个向量,这个过程叫embedding,也叫word embedding self.input_size = input_list[0].shape[1] if self.num_stack is not None and self.num_skip is not None: self.input_size *= self.num_stack # Frame stacking # 这里不明白这个 stack_frame 是什么作用,需要看一下这个函数 # utils/io/inputs/frame_stacking.py 中 input_list = stack_frame(input_list, self.num_stack, self.num_skip, progressbar=False) # Compute max frame num in mini-batch # 计算mini-batch 中的最大帧数 # 用map()函数取出input_list 中每个shape,然后取最大值 max_frame_num = max(map(lambda x: x.shape[0], input_list)) # Compute max target label length in mini-batch # 计算最大标签长度,这里可以看出label_list 是一个二维矩阵 max_seq_len = max(map(len, label_list)) ################################################################ # Initialization # 初始化 # np.zeros的第一个参数为要初始化的矩阵的shape,为一个元组,第二个参数为数据类型 # 这里可以看出这个要读入的数据是什么格式的 inputs = np.zeros( (len(data_indices), max_frame_num, self.input_size * self.splice), dtype=np.float32) # 注意这里中括号中表示列表运算 # [self.padded_value] * max_seq_len 表示max_seq_len 个 padded_value 组成的列表 # []* len(data_indices) 同理, # labels 这里是一个二维矩阵 # 其shape为 data_indices * max_seq_len labels = np.array( [[self.padded_value] * max_seq_len] * len(data_indices)) # 这里开一个numpy矩阵,一维的,其中记录了一个minibatch中每个输入序列的长度 inputs_seq_len = np.zeros((len(data_indices),), dtype=np.int32) input_names = list( map(lambda path: basename(path).split('.')[0], np.take(self.input_paths, data_indices, axis=0))) ################################################################## # 前面加载了batch_size个输入到input_list 这个 一维的 numpy array 中 # 其有batch_size 个元素,每个元素是一个二维的numpy array # 注意这里 input_list 并不是三维张量,因为每个元素的shape是不等的 # input_size 是每个timestep输入的向量维数 # 同样的方式加载了 batch_size 个 label 到label_list 这个一维的 numpy array中 # 其有batch_size 个元素,每个元素是一个一维的numpy array # 注意它不是二维矩阵,因为每个元素的维数是不相等的 ################################################################## # 在Initialization这部分初始化了inputs,labels # inputs 是一个三维张量,labels是一个二维矩阵 ################################################################## # 这部分要将input_list 中的值给inputs,label_list 中的值给labels # 同时padding # 因为训练时input需要是shape为(batch_size,timestep,feature_num)的三维张量 # label 需要是shape为(batch,label_len)的二维矩阵 # Set values of each data in mini-batch for i_batch in range(len(data_indices)): data_i = input_list[i_batch] # 上面说过了 input_list 的每个元素的shape为其 frame_num * input_size frame_num, input_size = data_i.shape # Splicing # 将data_i reshape成三维张量 data_i = data_i.reshape(1, frame_num, input_size) # 需要看下这个函数的功能 data_i = do_splice(data_i, splice=self.splice, batch_size=1, num_stack=self.num_stack) # reshape 成 frame_num 行 data_i = data_i.reshape(frame_num, -1) # inputs是上面初始化好的零矩阵,这里给其赋值 # 单独一个冒号表示由起到止, :frame_num 表示由起到 frame_num inputs[i_batch, :frame_num, :] = data_i # 这里是不是不太对,labels是个二维矩阵,测试时直接给矩阵中的一个元素赋值? if self.is_test: labels[i_batch, 0] = label_list[i_batch] else: labels[i_batch, :len(label_list[i_batch]) ] = label_list[i_batch] inputs_seq_len[i_batch] = frame_num ############### # Multi-GPUs ############### if self.num_gpu > 1: # 如果self.num_gpu 大于1,就把inputs 和 label 分为self.num_gpu份 # np.array_split() 是按顺序平分的 # np.array_split() 函数会多加一个轴,这个轴表示是哪份 # Now we split the mini-batch data by num_gpu inputs = np.array_split(inputs, self.num_gpu, axis=0) labels = np.array_split(labels, self.num_gpu, axis=0) inputs_seq_len = np.array_split( inputs_seq_len, self.num_gpu, axis=0) input_names = np.array_split(input_names, self.num_gpu, axis=0) else: # 如果 self.num_gpu 为1,那么就添加一个轴,因为上面使用np.array_split() 时会添加一个轴 # 是为了保证两种情况下返回格式是一致的 inputs = inputs[np.newaxis, :, :, :] labels = labels[np.newaxis, :, :] inputs_seq_len = inputs_seq_len[np.newaxis, :] input_names = np.array(input_names)[np.newaxis, :] # 这个成员好像是继承父类的 self.iteration += len(data_indices) # Clean up del input_list del label_list return (inputs, labels, inputs_seq_len, input_names), self.is_new_epoch看了这两个代码应该知道,首先有一个保存文件路径的pickle 文件。 其次 每个label和input都是各自单独存储在.npy 文件中,input是numpy array,其shape为 (frame_size , feature_num),这个feature_num 在yml文件中就是Input_size。 label也是 numpy array ,其shape为(1,label_length)。 现在还有一个疑问,input肯定是数字,这里读入的label是数字还是字符? 后来发现,在这个工程里测试集相对于训练集和开发集是区别对待的,训练集和开发集都是数字, shape是(1,n) 。测试集就是一个字符串,shape 是 (1,1)。TIMIT只分训练集和测试集,我之前提取时,全部都转换成数字了,并且没再另外划分出开发集,所以我的训练集和测试集是同一个。(这样虽然不太好。但目前赶进度) 所以就先偷懒暂时把训练中使用测试集的代码注释掉,只使用训练集和开发集。
上篇笔记里看到更新参数时执行了 sess.run(train_op, feed_dict=feed_dict_train) ,这里的train_op 定义了对数据进行的操作,
train_op = model.train( loss_op, optimizer=params['optimizer'], learning_rate=learning_rate_pl)这个函数是写在 models/ctc/ctc.py 里,下一步看一下这个文件
关于为什么要padding 训练时,每个batch的输入数据一般被抽象成(batchsize,timestep,input dim)的3d张量。在batch里的每个sample,都是一个(timestep,input dim)的矩阵,自然要每个样本的timestep一样了。 还有就是等长序列矩阵运算的时候机器处理速度快 参考了https://jizhi.im/community/discuss/2017-05-07-7-50-48-pm
张量和向量 先简单理解为向量可以看成一维的“表格”,矩阵是二维的“表格”,n阶张量就是n维的“表格”,我注释有些地方说的不对。