- 看一下这个工程中的数据加载方式
数据加载
1 Dataset 类
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,即随机打乱。如果输入为语音,也可以按长度排序。
2 Dataset的父类 DatasetBase
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维的“表格”,我注释有些地方说的不对。
这篇博客介绍了在Tensorflow中进行语音识别时的数据加载过程,重点解析了Dataset类及其父类DatasetBase。数据从pickle文件加载,保存为包含文件路径的列表。训练数据通常经过shuffle和按长度排序。输入是numpy数组,而标签可能是数字或字符串。在当前工程中,训练集和开发集的标签为数字,测试集为字符串。作者提到了CTC损失函数和训练时的padding需求,以便处理等长序列,提高运算速度。
3904

被折叠的 条评论
为什么被折叠?



