使用Tensorflow进行语音识别 代码阅读笔记2

这篇博客介绍了在Tensorflow中进行语音识别时的数据加载过程,重点解析了Dataset类及其父类DatasetBase。数据从pickle文件加载,保存为包含文件路径的列表。训练数据通常经过shuffle和按长度排序。输入是numpy数组,而标签可能是数字或字符串。在当前工程中,训练集和开发集的标签为数字,测试集为字符串。作者提到了CTC损失函数和训练时的padding需求,以便处理等长序列,提高运算速度。
  • 看一下这个工程中的数据加载方式

数据加载

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维的“表格”,我注释有些地方说的不对。

内容概要:本文提出了一种基于改进扩散模型的高海拔地区新能源高波动出力场景生成方法,并提供了完整的Python代码实现。该方法针对高海拔地区风能、光伏等新能源出力波动剧烈、不确定性高的特点,通过优化扩散模型的结构与训练策略,有效捕捉历史据的概率分布特征与时序相关性,从而生成高质量、多样化的出力场景。文中详细阐述了模型的学推导、网络架构设计、损失函优化及采样算法改进,并通过实验证明其在拟合精度、场景多样性与稳定性方面优于传统生成模型,为电力系统在高比例新能源接入下的规划、调度与风险评估提供了可靠的场景输入支持。; 适合人群:具备一定Python编程能力和机器学习基础,从事新能源发电预测、电力系统分析、智能优化、场景生成等方向研究的科研人员、高校研究生及工程技术人员。; 使用场景及目标:①用于高海拔地区风电、光伏出力的不确定性建模与多场景生成;②支撑含高渗透率新能源的电力系统随机优化调度、鲁棒决策与风险评估;③为相关学术研究、论文复现与算法改进提供可运行的技术方案与代码基础; 阅读建议:建议读者结合所提供的完整资源(代码、据集、说明文档)进行实践操作,重点关注扩散模型的前向加噪与反向去噪过程的设计细节,以及如何将其适配于新能源时序据的生成任务,通过参调优与对比实验深入理解模型的生成机制与性能边界。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值