HTS-2.3.2のソースコードをのぞいてそのDNNの構造を調べてみる

ざっとみてみた結果、DNNTraining.pyのmain関数の最初に定義される以下の変数configにDNNの構造が書かれているらしい。
100行目

config = DNNDataIO.load_config(args.config)

最初にこのconfigがどう指定されるかを追ってみる。
まず、DNNDataIO.pyのload_config関数は以下のようなもの。
245行目

def load_config(config_file, verbose=True):
    config_parser = ConfigParser.SafeConfigParser()
    config_parser.read(config_file)

    config = {}
    for section in config_parser.sections():
        for option in config_parser.options(section):
            config[option] = yaml.safe_load(config_parser.get(section, option))

    if verbose:
        print('Configuration Parameters[%d]' % len(config))
        print('              %-25s  s' % ('Parameter', 'Value'))
        for key in sorted(config.keys()):
            print('              %-25s  s' % (key, str(config[key])))
        print()

    config['num_io_units'] = (config['num_input_units'],
                              config['num_output_units'])

    return config

引数として指定されたconfigureファイルのsection中のoptionを辞書オブジェクトとして返す関数であることがわかった。
冒頭の話に戻ると、DNNTraining.pyでは引数としてargs.configを指定していたのであった。args.configとは何か。再びDNNTraining.pyをみてみる。
60行目

parser = argparse.ArgumentParser()

65行目

parser.add_argument('-C', metavar='cf', dest='config', type=str, required=True,
                    help='set config file to cf')

73行目

args = parser.parse_args()

DNNTraining.pyを実行する際指定するオプション-Cの値であることがわかった。
DNNTraining.pyはどのように実行され、オプション-Cには何が入るのであろうか。Training.plには以下の記述がある。
243行目

$cfg{'trj'}          = "$prjdir/configs/ver${ver}/trj_dnn.cnf";

269行目

 foreach $dir ( 'models', 'stats', 'edfiles', 'trees', 'gv', 'mspf', 'dnn', 'voices', 'gen', 'proto', 'configs' ) {
      mkdir "$prjdir/$dir",           0755;
      mkdir "$prjdir/$dir/ver${ver}", 0755;
   }

1974行目

open( CONF, ">$cfg{'trj'}" ) || die "Cannot open $!";
   print CONF "[Architecture]\n";
   print CONF "num_input_units: $nin\n";
   print CONF "num_hidden_units: [$nhid]\n";
   print CONF "num_output_units: $nout\n";
   print CONF "hidden_activation: \"$activations[$activation]\"\n";
   print CONF "output_activation: \"$activations[0]\"\n";
   print CONF "\n[Strategy]\n";
   print CONF "optimizer: \"$optimizers[$optimizer]\"\n";
   print CONF "learning_rate: $trjLearnRate\n";
   print CONF "gv_weight: $dnnGVWeight\n";
   print CONF "keep_prob: $keepProb\n";
   print CONF "queue_size: $queueSize\n";
   print CONF "batch_size: 1\n";
   print CONF "num_epochs: $nTrjEpoch\n";
   print CONF "num_threads: $nThread\n";
   print CONF "random_seed: $randomSeed\n";
   print CONF "frame_by_frame: 0\n";
   print CONF "adaptation: 0\n";
   print CONF "\n[Output]\n";
   print CONF "num_models_to_keep: $nKeep\n";
   print CONF "log_interval: $logInterval\n";
   print CONF "save_interval: $saveInterval\n";
   print CONF "\n[Others]\n";
   print CONF "all_spkrs: [\"$spkr\"]\n";
   print CONF "num_feature_dimensions: [";

930行目

if ($useDNN) {
      mkdir "$dnnmodelsdir{'trj'}", 0755;

      shell("cp $dnnmodels/model.ckpt.* $dnnmodelsdir{'trj'}");
      shell("$PYTHON $datdir/scripts/DNNTraining.py -C $cfg{'trj'} -S $scp{'tdn'} -H $dnnmodelsdir{'trj'} -z $datdir/stats -w $windir");
   }

プログラム実行時に新しくディレクトリを作り、そこにconfigureファイルを作って、それをオプションとして指定してPythonを動かしていた。
隠れユニットの数やoptimizerの種類などのDNNの構造が変数によって指定されているが、これら変数はConfig.pm.inで定義されているもので、その中で以下のように記述されている。

$useDNN       = @USEDNN@;         # train a deep neural network after HMM training
$nHiddenUnits = '@NHIDDENUNITS@'; # number of hidden units in each layer
$activation   = @ACTIVATION@;     # activation function for hidden units  (0 -> Linear, 1 -> Sigmoid, 2 -> Tanh, 3 -> ReLU)
$optimizer    = @OPTIMIZER@;      # optimizer (0 -> SGD, 1 -> Momentum, 2 -> AdaGrad, 3-> AdaDelta, 4 -> Adam, 5 -> RMSprop)
$learnRate    = @LEARNRATE@;      # learning rate
$trjLearnRate = @TRJLEARNRATE@;   # learning rate for trajectory training
$dnnGVWeight  = @DNNGVWEIGHT@;    # weight for GV
$keepProb     = @KEEPPROB@;       # probability for not randomly setting activities to zero
$queueSize    = @QUEUESIZE@;      # queue size
$batchSize    = @BATCHSIZE@;      # mini-batch size
$nEpoch       = @NEPOCH@;         # number of epochs
$nTrjEpoch    = @NTRJEPOCH@;      # number of epochs for trajectory training
$nThread      = @NTHREAD@;        # number of threads
$randomSeed   = @RANDOMSEED@;     # random seed used for initialization
$nKeep        = 5;                # number of models to keep
$logInterval  = 100;              # output training log at regular steps
$saveInterval = 10000;            # save model at regular steps

この@で囲まれた表記がよく分からなかった。何らかの方法でこの@で囲まれた変数に値を入力するらしい。例えば、活性化関数に関してはコメントによると

  • 0を入力すると線形関数(プログラム上は恒等関数であった)
  • 1を入力するとシグモイド関数
  • 2を入力するとハイパボリックタンジェント
  • 3を入力するとReLU関数

という具合に指定されるらしい。
結局configがどのように指定されるかは分からず仕舞いであった。
何らかの方法で指定されたconfigを参照してDNNTraining.pyにしたがってモデルが学習されるわけだが、詳しいことは別ファイルとしてDNNDefine.pyに記述されている。調べてわかったことは以下の通り。

  • 選択できる活性化関数は恒等関数、シグモイド関数、ハイパボリックタンジェント、ReLU関数の4種類。
  • 選択できるoptimizerはGradientDescentOptimizer、MomentumOptimizer、AdagradOptimizer、AdadeltaOptimizer、AdamOptimizer、RMSPropOptimizerの6種類。
  • 隠れ層ではmodeによって活性化関数に渡す式が変わる。modeが"SD"の時は簡単な式(Wx+b)だが、modeが"SAT"または"ADAPT"の時は重みWが2種類あり、auxially_inputという謎のinputが考慮されて複雑になる。modeを指定する条件は不明。
  • 出力層の活性化関数に渡す式は一様にWx+bであった。
  • コスト関数もcostとtrajectory_costの2種類用意されている。costの方は以下のように記述されている。
def cost(predicted_outputs, observed_outputs, variances):
    gconst = tf.cast(np.log(2.0 * np.pi), tf.float32)
    covdet = tf.reduce_mean(tf.log(variances))
    mahala = tf.reduce_mean(tf.divide(
        tf.square(observed_outputs - predicted_outputs), variances))
    cost = 0.5 * (gconst + covdet + mahala)
    return cost, predicted_outputs

式で表すと以下のようになる。

C\left(x\right) = \frac{1}{2}\left(\log\left(2\pi\right) + \frac{1}{n}\Sigma\left(\log\sigma + \frac{\left(y^\prime - y\right)^2}{\sigma}\right)\right)

n:出力テンソルの要素数,~~ y^\prime:DNN出力,~~ y:予想出力,~~ \sigma:分散共分散行列?)

trajectory_costの方はこれより複雑である。どちらのコスト関数が選択されるかはconfigで指定されるframe_by_frameによって決まるが、これが何を表すかは不明。