run_merlin.py详解
run_merlin.py是Merlin框架的核心脚本,配套(.conf)的参数文件使用,其中包括了时延模型训练,声学模型训练和音频文件的生成等众多的功能,其参数文件中的也众多,没有去读相应的源码很难解析其内部的工作原理和训练机制,本博客也算对merlin的代码做一个记录,方便以后查阅。
1、先从1220行的'__main__':
开始:
既然是配套参数文件使用,第一步自然是创建解析参数文件的对象:
cfg = configuration.cfg
这里调用了src/configuration文件夹下的configuration类,这里的(.cfg)看是指该文件夹下的__init__.py文件中的
cfg = configuration.configuration()
其实就是调用了configuration.py来创建一个类。
继续往下看:
logging
logger
这些都是创建运行过程中的日志文件,这里就不详解了
直接看1244行的:
cfg.configure(config_file)
configuration类调用configure()函数,传入参数文件路径,初始化configuration类。
2、cfg.configure(config_file)内部操作:
定位到configuration.py文件到82行self.initial_configuration(),这里可以初始化一些你自己设计的参数,这些参数在运行过程中不能改变。且在导入参数文件之前导入。
然后是86行的self.user_configuration(config_file) 导入参数文件,这里参数文件会按照所要完成的任务不同而有不同的参数个数,参数文件的格式为:
[参数类别]
参数名: 参数值
即:
[Outputs]
mgc: 60
dmgc: 180
bap: 5
既然用到user_configuration(config_File) 函数,那就查看下该函数,先看123行:
cfgparser = configparser.ConfigParser()
cfgparser.readfp(open(configFile))
这里是利用configparser包创建一了一个专门读取上述参数文件的对象,读取方式为cfgparser.get(参数类别, 参数名) 。
然后是171行的 user_options 是所有可选参数,以元组列表的形式表示。一些参数说明见下面列表
work_dir | 工作的文件的主目录 |
data_dir | 训练数据文件位置 |
inter_data_dir | 中间数据文件位置(包括处理后的标准化后的矩阵数据和去静音数据) |
gen_dir | 生成数据文件位置 |
model_dir | 模型文件位置 |
inp_dim | 每帧输入的维度 默认425 |
out_dim | 每帧输入的维度 默认187 |
label_type | 前端生成标签特征的格式(state_align和phone_align) |
question_file_name | 将标签特征文件转化为特征矩阵的字典文件位置 |
output_features | 生成的声学特征选择[‘mgc’, ‘lf0’, ‘vuv’, ‘bap’,‘stepw’,‘sp’,‘seglf0’,‘F0’,‘Gain’] |
后续继续添加
该函数507行利用 for (variable, default, section, option) in user_options: 将参数文件中的参数初始化到configuration类中。user_configuration(config_File) 函数完成,主要任务是读取参数文件。
完成user_configuration(config_File) 函数继续看95行的**self.complete_configuration()**函数
该函数主要功能是:
1 补充各工具包的路径,主要工具包有(SPTK,STRAIGHT,WORLD,GLOTTHMM,GLOTTDNN,HMPD)
2 根据参数,导入完整的路径,如选择不同模型,导入不同的模型路径
3 根据选择的特征确定输入输出的特征维度。
里面还有一些细节需要自己去查看源码
这样cfg.configure(config_file)函数操作结束。回到run_merlin.py 继续查看 ‘main’:函数,然后你会发现一堆日志函数,只是方便运行时找错,真正运行的有用的只有一个**main_function(cfg)**函数,看了半天你会发现其实整个main函数主要作用就是读取参数。真正运行的在main_function(cfg)
3、 518行main_function(cfg)函数 , 这才是Merlin中真正的大boss
读取参数类中的所有文件路径的参数
519 file_paths = FilePaths(cfg)
将模型隐藏层传入数据格式为列表:[1024, 1024]
537 hidden_layer_size = cfg.hyper_params['hidden_layer_size']
获取保存了训练文件的文件名列表
541 file_id_list = read_file_list(cfg.file_id_scp)
这里比对训练集+验证集+测试集是否等于数据文件夹下的文件总数
551 assert cfg.train_file_number+cfg.valid_file_number+cfg.test_file_number == total_file_number
555~559行将一些训练需要的文件参数传给当前变量。
获取训练需要的字典特征文件列表,file_id_list文件名列表,cfg.in_dir_dict[feature_name] 具体特征数据所在的文件路径,cfg.file_extension_dict[feature_name] 文件的后缀名。
564 in_file_list_dict[feature_name] = prepare_file_path_list(file_id_list, cfg.in_dir_dict[feature_name], cfg.file_extension_dict[feature_name], False)
这里是综合所有音频特征数据的文件路径, nn_cmp_norm_file_list 是进行了归一化的,有一点要注意,这里的这些操作只是建立好各种文件的存放路径,并没有完成数据的处理。
566 nn_cmp_file_list = file_paths.get_nn_cmp_file_list()
567 nn_cmp_norm_file_list = file_paths.get_nn_cmp_norm_file_list()
570 norm_info_file = file_paths.norm_info_file
578行开始处理前端生成的文本特征文件。
578 label_normaliser = HTSLabelNormalisation(question_file_name=cfg.question_file_name, add_frame_features=cfg.add_frame_features, subphone_feats=cfg.subphone_feats)
这里创建了HTSLabelNormalisation类,方便后面数据生成,其中question_file_name 是将标签文件对应到矩阵的字典文件,这里的字典和文本特征都有对应的格式,add_frame_features 自己要将的额外特征,一般不作处理, subphone_feats 音素本身的状态特征,有多个维度选择。这里简单介绍下question 字典文件和标签文件:
question 字典文件:
QS "L-Nasal" {*^m-*,*^n-*}
CQS "Pos_C-Syl_in_C-Word(Fw)" {@(\d+)^}
这里QS 代表数据类型,只能是0和1, “L-Nasal” 是特征的名字,{*^m-*,*^n-*} 是利用正则表达式如果文本特征里匹配到这个,向量中该处的值为1。同理CQS “Pos_C-Syl_in_C-Word(Fw)” {@(\d+)^} 代表同样的含义只是这里CQS 是实数的含义。
文本特征文件:
2500000 4000000 xx^m-k+a2=er2@a@/A:xx-2^2@/B:0.0000+1.0000@0.0000^1.0000^0.0000+1.0000#0.0000-1.0000-/C:xx_n^v#xx+3+1&/D:xx=3!3@0.0000-1.0000&/E:xx|9-xx@xx#3&xx!0.0000-1.0000=/F:d^9=5_3-1!
前两个是该音素开始结束的时间,后面是该因素完整的文本特征文件。像这个音素标签就能匹配到*^m-* ,这里的 * 是边间表示。
还有一个要注意的地方是:如果subphone_feats == ‘coarse_coding’ 会做一步:
self.cc_features = self.compute_coarse_coding_features(3)
这里是利用正态概率密度函数,创建了 [3,600] 的特征,这里为什么要这么操作要看论文:Context Adaptive Training with Factorized Decision Trees for HMM-Based Speech Synthesis
591~607行主要作用就是创建中间数据的文件路径和文件列表
610~612行判断要不提取音频特征。
619~653 生成数据
622 label_normaliser.perform_normalisation(in_label_align_file_list, binary_label_file_list, label_type=cfg.label_type)
这里in_label_align_file_list 文本特征文件所在的路径,binary_label_file_list 生成的矩阵保存的路径, label_type 生成方式有phone_align和state_align两种。这里要注意perform_normalisation 是一个继承函数,最终主要还是调用了frontend/label_normalistion.py 中的368行的load_labels_with_phone_alignment(self, file_name, dur_file_name) 函数
这里load_labels_with_phone_alignment(self, file_name, dur_file_name) 函数还是有必要详解下的:
这里dur_file_name 主要是表示每个音素时长的文件,可有可无,没有的化在标签中表现出来, file_name 标签文件路径。以下行数表示frontend/label_normalistion.py 中的行数:
375~378行, 这里及以下行数均代表frontend/label_normalistion.py 的行数
if dur_file_name:
io_funcs = BinaryIOCollection()
dur_dim = 1 ## hard coded for now
manual_dur_data = io_funcs.load_binary_file(dur_file_name, dur_dim)
如果有dur_file_name 利用BinaryIOCollection()类直接导入音素时长,这个时长是时延模型时的输出值,不是输入特征。
380~385行,表示有自己要加的特征,则加入特征维度上。
389~423行是对将输入的标签文件转化为特征矩阵,标签文件的格式就是如上所示。其中还有几行代码要说明下:
412 frame_number = int(end_time/50000) - int(start_time/50000)
这里表示每一帧长度为5毫秒,frame_number 为一个音素所对应的帧的个数
415 cc_feat_matrix = self.extract_coarse_coding_features_relative(frame_number)
这个就是上面所提到的利用正态概率密度函数,创建了 [3,600] 的特征矩阵后,每一帧从这个矩阵中选出3个点,来代表产生该帧状态的可能,从而作为特征向量中的3维。这个要注意下。
419 label_binary_vector = self.pattern_matching_binary(full_label)
422 label_continuous_vector = self.pattern_matching_continous_position(full_label)
423 label_vector = numpy.concatenate([label_binary_vector, label_continuous_vector], axis = 1)
前两句函数就是利用qusetion字典文件产生向量的函数(一个0/1,一个实数),第三句合并两个向量。
425~455 行就是处理subphone_feats 和add_frame_features
if self.add_frame_features:
current_block_binary_array = numpy.zeros((frame_number, self.dict_size+self.frame_feature_size))
for i in range(frame_number):
current_block_binary_array[i, 0:self.dict_size] = label_vector
if self.subphone_feats == 'minimal_phoneme':
## features which distinguish frame position in phoneme
current_block_binary_array[i, self.dict_size] = float(i+1)/float(frame_number) # fraction through phone forwards
current_block_binary_array[i, self.dict_size+1] = float(frame_number - i)/float(frame_number) # fraction through phone backwards
current_block_binary_array[i, self.dict_size+2] = float(frame_number) # phone duration
elif self.subphone_feats == 'coarse_coding':
## features which distinguish frame position in phoneme using three continous numerical features
current_block_binary_array[i, self.dict_size+0] = cc_feat_matrix[i, 0]
current_block_binary_array[i, self.dict_size+1] = cc_feat_matrix[i, 1]
current_block_binary_array[i, self.dict_size+2] = cc_feat_matrix[i, 2]
current_block_binary_array[i, self.dict_size+3] = float(frame_number)
elif self.subphone_feats == 'none':
pass
else:
sys.exit('unknown subphone_feats type')
label_feature_matrix[label_feature_index:label_feature_index+frame_number,] = current_block_binary_array
label_feature_index = label_feature_index + frame_number
elif self.subphone_feats == 'none':
current_block_binary_array = label_vector
label_feature_matrix[label_feature_index:label_feature_index+1,] = current_block_binary_array
label_feature_index = label_feature_index + 1
代码比较简单这里就不解释了。
然后就是461行返回label_feature_matrix 就是每句话对应的二维特征矩阵,矩阵的每行代表每个音素的特征,merlin里是将这个矩阵保存到文件中,形成中间的数据文件
回到run_merlin.py 的624~636行,判断有没有其他输入特征要加
然后是638行,删除静音帧
之所以要删除,因为
1 一般静音帧数在生成音频时是固定的,一般不用时延模型预测
2 静音帧数的个数是非静音帧数的4倍左右,容易影响归一化。
remover = SilenceRemover(n_cmp = lab_dim, silence_pattern = cfg.silence_pattern, label_type=cfg.label_type, remove_frame_features = cfg.add_frame_features, subphone_feats = cfg.subphone_feats)
SilenceRemover() 是一个类,传入参数lab_dim 输入文本特征维度,silence_pattern 的静音的格式:[’*-sil+*’],label_type 对齐方式,