2017-04-03 20:28:52 u013131595 阅读数 2297
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5927 人正在学习 去看看 杨波

上回分析了run_model函数的configuration过程,其中load_placeholder_into_network函数用于构建该语音识别系统中RNN网络的基本结构,本回将分析以下该网络。

1.RNN简介

人们并不是从每秒钟他接收到的信息开始处理的,就像在看一篇论文的时候,大家都是先理解前文,然后根据现在的信息逐渐获得完整的信息。关于这些带有时间信息的序列处理时,传统的DNN可能无能为力。RNN就是为这类问题而设计的,通常RNN的结构如下图所示:
这里写图片描述
由上图可以看出,每个时刻的输出h即于当前的输入x有关,同时也于上一状态c有关。
这里写图片描述

2.LSTM简介

由上文可知RNN可以处理与时序相关的问题,但是由于梯度扩散以及消失的问题,RNN只能记录到较短的时序信息。为了解决上述问题,引入了LSTM结构,如下图所示:
这里写图片描述
由上图可以看出,LSTM的内部结构比RNN要复杂一些,主要增加了遗忘门、输入门和输出门三个部件,对存储到RNN中的状态进行处理,使其能够记住较长的序列。
首先,输入门“ input gate”的作用是提供一个是否接收当前正常输入(包含信号和隐结点状态)信息的权重(0~1)。其值取决于当前输入信号和前一时刻隐含层的输出,结构如下图所示:
这里写图片描述
然后,遗忘门“ forget gate”的作用是提供一个遗忘当前状态的权重(0~1)。其值取决于当前输入信号和前一时刻隐含层的输出,结构如下图所示:
这里写图片描述
其中,LTSM 细胞产生的新记忆,由输入与遗忘两部分组成,如下图所示:
这里写图片描述
最后,输出门“ output gate”的作用是对当前输出提供一个 0~1之间的权重(对输出的认可程度)。其值取决于当前输入信号和前一时刻隐含层的输出,结构如下图所示:
这里写图片描述

3.BiRNN简介

由上文可知RNN可以在时序上处理信息,但是当前的信息只能根据前面时刻的信息推测得出,这就忽略了未来的信息对现在的影响。访问未来的上下文,对于很多序列标注任务是十分有益的。因此,我们的语音识别系统采用该种网络结构。
那么如何构建这种特殊的网络结构呢?一种显而易见的办法是在输入和目标之间添加延迟,进而可以给网路一些时间步加入一些未来的上下文信息。双向循环神经网络(BiRNN)的基本思想是提出每一个训练序列向前和向后分别是两个循环神经网络(RNN),而且这两个都连接着一个输出层。这个结构提供给输出层输入序列中每一个点的完整的过去和未来的上下文信息。下图展示的是一个沿着时间展开的双向循环神经网络。六个独特的权值在每一个时步被重复的利用,六个权值分别对应:输入到向前和向后隐含层(w1, w3),隐含层到隐含层自己(w2, w5),向前和向后隐含层到输出层(w4, w6)。值得注意的是:向前和向后隐含层之间没有信息流,这保证了展开图是非循环的。
这里写图片描述

4.代码分析

上文简单介绍了RNN以及该网络的相关变式,现在我们根据上文所讲,简单分析一下我们的语音识别系统的实现。具体代码如下(位于rnn.py中):

def BiRNN(conf_path, batch_x, seq_length, n_input, n_context):
    parser = ConfigParser(os.environ)
    parser.read(conf_path)

    dropout = [float(x) for x in parser.get('birnn', 'dropout_rates').split(',')]
    relu_clip = parser.getint('birnn', 'relu_clip')

    b1_stddev = parser.getfloat('birnn', 'b1_stddev')
    h1_stddev = parser.getfloat('birnn', 'h1_stddev')
    b2_stddev = parser.getfloat('birnn', 'b2_stddev')
    h2_stddev = parser.getfloat('birnn', 'h2_stddev')
    b3_stddev = parser.getfloat('birnn', 'b3_stddev')
    h3_stddev = parser.getfloat('birnn', 'h3_stddev')
    b5_stddev = parser.getfloat('birnn', 'b5_stddev')
    h5_stddev = parser.getfloat('birnn', 'h5_stddev')
    b6_stddev = parser.getfloat('birnn', 'b6_stddev')
    h6_stddev = parser.getfloat('birnn', 'h6_stddev')

    n_hidden_1 = parser.getint('birnn', 'n_hidden_1')
    n_hidden_2 = parser.getint('birnn', 'n_hidden_2')
    n_hidden_5 = parser.getint('birnn', 'n_hidden_5')
    n_cell_dim = parser.getint('birnn', 'n_cell_dim')

    n_hidden_3 = int(eval(parser.get('birnn', 'n_hidden_3')))
    n_hidden_6 = parser.getint('birnn', 'n_hidden_6')

    # Input shape: [batch_size, n_steps, n_input + 2*n_input*n_context]
    batch_x_shape = tf.shape(batch_x)

    # Reshaping `batch_x` to a tensor with shape `[n_steps*batch_size, n_input + 2*n_input*n_context]`.
    # This is done to prepare the batch for input into the first layer which expects a tensor of rank `2`.

    # Permute n_steps and batch_size
    batch_x = tf.transpose(batch_x, [1, 0, 2])
    # Reshape to prepare input for first layer
    batch_x = tf.reshape(batch_x,
                         [-1, n_input + 2 * n_input * n_context])  # (n_steps*batch_size, n_input + 2*n_input*n_context)

    # The next three blocks will pass `batch_x` through three hidden layers with
    # clipped RELU activation and dropout.

    # 1st layer
    with tf.name_scope('fc1'):
        b1 = variable_on_cpu('b1', [n_hidden_1], tf.random_normal_initializer(stddev=b1_stddev))
        h1 = variable_on_cpu('h1', [n_input + 2 * n_input * n_context, n_hidden_1],
                             tf.random_normal_initializer(stddev=h1_stddev))
        layer_1 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(batch_x, h1), b1)), relu_clip)
        layer_1 = tf.nn.dropout(layer_1, (1.0 - dropout[0]))

        tf.summary.histogram("weights", h1)
        tf.summary.histogram("biases", b1)
        tf.summary.histogram("activations", layer_1)

    # 2nd layer
    with tf.name_scope('fc2'):
        b2 = variable_on_cpu('b2', [n_hidden_2], tf.random_normal_initializer(stddev=b2_stddev))
        h2 = variable_on_cpu('h2', [n_hidden_1, n_hidden_2], tf.random_normal_initializer(stddev=h2_stddev))
        layer_2 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_1, h2), b2)), relu_clip)
        layer_2 = tf.nn.dropout(layer_2, (1.0 - dropout[1]))

        tf.summary.histogram("weights", h2)
        tf.summary.histogram("biases", b2)
        tf.summary.histogram("activations", layer_2)

    # 3rd layer
    with tf.name_scope('fc3'):
        b3 = variable_on_cpu('b3', [n_hidden_3], tf.random_normal_initializer(stddev=b3_stddev))
        h3 = variable_on_cpu('h3', [n_hidden_2, n_hidden_3], tf.random_normal_initializer(stddev=h3_stddev))
        layer_3 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(layer_2, h3), b3)), relu_clip)
        layer_3 = tf.nn.dropout(layer_3, (1.0 - dropout[2]))

        tf.summary.histogram("weights", h3)
        tf.summary.histogram("biases", b3)
        tf.summary.histogram("activations", layer_3)

    # Create the forward and backward LSTM units. Inputs have length `n_cell_dim`.
    # LSTM forget gate bias initialized at `1.0` (default), meaning less forgetting
    # at the beginning of training (remembers more previous info)
    with tf.name_scope('lstm'):
        # Forward direction cell:
        lstm_fw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim, forget_bias=1.0, state_is_tuple=True)
        lstm_fw_cell = tf.contrib.rnn.DropoutWrapper(lstm_fw_cell,
                                                     input_keep_prob=1.0 - dropout[3],
                                                     output_keep_prob=1.0 - dropout[3],
                                                     # seed=random_seed,
                                                     )
        # Backward direction cell:
        lstm_bw_cell = tf.contrib.rnn.BasicLSTMCell(n_cell_dim, forget_bias=1.0, state_is_tuple=True)
        lstm_bw_cell = tf.contrib.rnn.DropoutWrapper(lstm_bw_cell,
                                                     input_keep_prob=1.0 - dropout[4],
                                                     output_keep_prob=1.0 - dropout[4],
                                                     # seed=random_seed,
                                                     )

        # `layer_3` is now reshaped into `[n_steps, batch_size, 2*n_cell_dim]`,
        # as the LSTM BRNN expects its input to be of shape `[max_time, batch_size, input_size]`.
        layer_3 = tf.reshape(layer_3, [-1, batch_x_shape[0], n_hidden_3])

        # Now we feed `layer_3` into the LSTM BRNN cell and obtain the LSTM BRNN output.
        outputs, output_states = tf.nn.bidirectional_dynamic_rnn(cell_fw=lstm_fw_cell,
                                                                 cell_bw=lstm_bw_cell,
                                                                 inputs=layer_3,
                                                                 dtype=tf.float32,
                                                                 time_major=True,
                                                                 sequence_length=seq_length)

        tf.summary.histogram("activations", outputs)

        # Reshape outputs from two tensors each of shape [n_steps, batch_size, n_cell_dim]
        # to a single tensor of shape [n_steps*batch_size, 2*n_cell_dim]
        outputs = tf.concat(outputs, 2)
        outputs = tf.reshape(outputs, [-1, 2 * n_cell_dim])

    with tf.name_scope('fc5'):
        # Now we feed `outputs` to the fifth hidden layer with clipped RELU activation and dropout
        b5 = variable_on_cpu('b5', [n_hidden_5], tf.random_normal_initializer(stddev=b5_stddev))
        h5 = variable_on_cpu('h5', [(2 * n_cell_dim), n_hidden_5], tf.random_normal_initializer(stddev=h5_stddev))
        layer_5 = tf.minimum(tf.nn.relu(tf.add(tf.matmul(outputs, h5), b5)), relu_clip)
        layer_5 = tf.nn.dropout(layer_5, (1.0 - dropout[5]))

        tf.summary.histogram("weights", h5)
        tf.summary.histogram("biases", b5)
        tf.summary.histogram("activations", layer_5)

    with tf.name_scope('fc6'):
        # Now we apply the weight matrix `h6` and bias `b6` to the output of `layer_5`
        # creating `n_classes` dimensional vectors, the logits.
        b6 = variable_on_cpu('b6', [n_hidden_6], tf.random_normal_initializer(stddev=b6_stddev))
        h6 = variable_on_cpu('h6', [n_hidden_5, n_hidden_6], tf.random_normal_initializer(stddev=h6_stddev))
        layer_6 = tf.add(tf.matmul(layer_5, h6), b6)

        tf.summary.histogram("weights", h6)
        tf.summary.histogram("biases", b6)
        tf.summary.histogram("activations", layer_6)

    # Finally we reshape layer_6 from a tensor of shape [n_steps*batch_size, n_hidden_6]
    # to the slightly more useful shape [n_steps, batch_size, n_hidden_6].
    # Note, that this differs from the input in that it is time-major.
    layer_6 = tf.reshape(layer_6, [-1, batch_x_shape[0], n_hidden_6])

    summary_op = tf.summary.merge_all()

    # Output shape: [n_steps, batch_size, n_hidden_6]
    return layer_6, summary_op

其中,
第2-25行代码读入neural_network.ini中存储的[birnn]网络的配置参数,如下图所示:
这里写图片描述
第27-37行对网络进行输入数据进行处理,可知每个batch_x为2维的,第一个参数为-1,第二个参数为n_input + 2*n_input*n_context=26+2*26*9=494(前文中已给出n_input、n_context的获取方式)。第二个参数表示网络的输入向量的宽度。
第42-144行代码使用tensorflow中的库函数对整个网络进行建模,使用tensorboard画出其结构图如下:
这里写图片描述
结合上图以及代码可以看出,该网络主要有6层,参数结构如下表所示:

层数 输入 输出 其它说明
fc1 494 1024 输出大于20被置为20,dropout=0.05
fc2 1024 1024 输出大于20被置为20,dropout=0.05
fc3 1024 2048 输出大于20被置为20,dropout=0.05
lstm 2048 2048 关键部分后文分解
fc5 2048 1024 输出大于20被置为20,dropout=0.05
fc6 1024 29 输出层

其中,全连接层fc1/fc2/fc3/fc5/fc6就是简单的全连接层,下面详细分析lstm结构。
由代码中第79-105行可以看出,该网络主要使用tf.contrib.rnn构建,下面我们来分析以下BasicLSTMCell代码,如下所示:

class BasicLSTMCell(RNNCell):
  def __init__(self, num_units, forget_bias=1.0, input_size=None,
               state_is_tuple=True, activation=tanh, reuse=None):
    """Initialize the basic LSTM cell.

    Args:
      num_units: int, The number of units in the LSTM cell.
      forget_bias: float, The bias added to forget gates (see above).
      input_size: Deprecated and unused.
      state_is_tuple: If True, accepted and returned states are 2-tuples of
        the `c_state` and `m_state`.  If False, they are concatenated
        along the column axis.  The latter behavior will soon be deprecated.
      activation: Activation function of the inner states.
      reuse: (optional) Python boolean describing whether to reuse variables
        in an existing scope.  If not `True`, and the existing scope already has
        the given variables, an error is raised.
    """
    if not state_is_tuple:
      logging.warn("%s: Using a concatenated state is slower and will soon be "
                   "deprecated.  Use state_is_tuple=True.", self)
    if input_size is not None:
      logging.warn("%s: The input_size parameter is deprecated.", self)
    self._num_units = num_units
    self._forget_bias = forget_bias
    self._state_is_tuple = state_is_tuple
    self._activation = activation
    self._reuse = reuse

  @property
  def state_size(self):
    return (LSTMStateTuple(self._num_units, self._num_units)
            if self._state_is_tuple else 2 * self._num_units)

  @property
  def output_size(self):
    return self._num_units

  def __call__(self, inputs, state, scope=None):
    """Long short-term memory cell (LSTM)."""
    with _checked_scope(self, scope or "basic_lstm_cell", reuse=self._reuse):
      # Parameters of gates are concatenated into one multiply for efficiency.
      if self._state_is_tuple:
        c, h = state
      else:
        c, h = array_ops.split(value=state, num_or_size_splits=2, axis=1)
      concat = _linear([inputs, h], 4 * self._num_units, True)

      # i = input_gate, j = new_input, f = forget_gate, o = output_gate
      i, j, f, o = array_ops.split(value=concat, num_or_size_splits=4, axis=1)

      new_c = (c * sigmoid(f + self._forget_bias) + sigmoid(i) *
               self._activation(j))
      new_h = self._activation(new_c) * sigmoid(o)

      if self._state_is_tuple:
        new_state = LSTMStateTuple(new_c, new_h)
      else:
        new_state = array_ops.concat([new_c, new_h], 1)
      return new_h, new_state

其中__call__ 函数实现了前面所讲的LSTM结构。
同时第100行中tf.nn.bidirectional_dynamic_rnn函数将两个lstm结构合并成一个BiRNN结构,代码如下:

def bidirectional_dynamic_rnn(cell_fw, cell_bw, inputs, sequence_length=None,
                              initial_state_fw=None, initial_state_bw=None,
                              dtype=None, parallel_iterations=None,
                              swap_memory=False, time_major=False, scope=None):
  """Creates a dynamic version of bidirectional recurrent neural network.

  Similar to the unidirectional case above (rnn) but takes input and builds
  independent forward and backward RNNs. The input_size of forward and
  backward cell must match. The initial state for both directions is zero by
  default (but can be set optionally) and no intermediate states are ever
  returned -- the network is fully unrolled for the given (passed in)
  length(s) of the sequence(s) or completely unrolled if length(s) is not
  given.

  Args:
    cell_fw: An instance of RNNCell, to be used for forward direction.
    cell_bw: An instance of RNNCell, to be used for backward direction.
    inputs: The RNN inputs.
      If time_major == False (default), this must be a tensor of shape:
        `[batch_size, max_time, input_size]`.
      If time_major == True, this must be a tensor of shape:
        `[max_time, batch_size, input_size]`.
      [batch_size, input_size].
    sequence_length: (optional) An int32/int64 vector, size `[batch_size]`,
      containing the actual lengths for each of the sequences in the batch.
      If not provided, all batch entries are assumed to be full sequences; and
      time reversal is applied from time `0` to `max_time` for each sequence.
    initial_state_fw: (optional) An initial state for the forward RNN.
      This must be a tensor of appropriate type and shape
      `[batch_size, cell_fw.state_size]`.
      If `cell_fw.state_size` is a tuple, this should be a tuple of
      tensors having shapes `[batch_size, s] for s in cell_fw.state_size`.
    initial_state_bw: (optional) Same as for `initial_state_fw`, but using
      the corresponding properties of `cell_bw`.
    dtype: (optional) The data type for the initial states and expected output.
      Required if initial_states are not provided or RNN states have a
      heterogeneous dtype.
    parallel_iterations: (Default: 32).  The number of iterations to run in
      parallel.  Those operations which do not have any temporal dependency
      and can be run in parallel, will be.  This parameter trades off
      time for space.  Values >> 1 use more memory but take less time,
      while smaller values use less memory but computations take longer.
    swap_memory: Transparently swap the tensors produced in forward inference
      but needed for back prop from GPU to CPU.  This allows training RNNs
      which would typically not fit on a single GPU, with very minimal (or no)
      performance penalty.
    time_major: The shape format of the `inputs` and `outputs` Tensors.
      If true, these `Tensors` must be shaped `[max_time, batch_size, depth]`.
      If false, these `Tensors` must be shaped `[batch_size, max_time, depth]`.
      Using `time_major = True` is a bit more efficient because it avoids
      transposes at the beginning and end of the RNN calculation.  However,
      most TensorFlow data is batch-major, so by default this function
      accepts input and emits output in batch-major form.
    dtype: (optional) The data type for the initial state.  Required if
      either of the initial states are not provided.
    scope: VariableScope for the created subgraph; defaults to
      "bidirectional_rnn"

  Returns:
    A tuple (outputs, output_states) where:
      outputs: A tuple (output_fw, output_bw) containing the forward and
        the backward rnn output `Tensor`.
        If time_major == False (default),
          output_fw will be a `Tensor` shaped:
          `[batch_size, max_time, cell_fw.output_size]`
          and output_bw will be a `Tensor` shaped:
          `[batch_size, max_time, cell_bw.output_size]`.
        If time_major == True,
          output_fw will be a `Tensor` shaped:
          `[max_time, batch_size, cell_fw.output_size]`
          and output_bw will be a `Tensor` shaped:
          `[max_time, batch_size, cell_bw.output_size]`.
        It returns a tuple instead of a single concatenated `Tensor`, unlike
        in the `bidirectional_rnn`. If the concatenated one is preferred,
        the forward and backward outputs can be concatenated as
        `tf.concat(outputs, 2)`.
      output_states: A tuple (output_state_fw, output_state_bw) containing
        the forward and the backward final states of bidirectional rnn.

  Raises:
    TypeError: If `cell_fw` or `cell_bw` is not an instance of `RNNCell`.
  """

  # pylint: disable=protected-access
  if not isinstance(cell_fw, rnn_cell_impl._RNNCell):
    raise TypeError("cell_fw must be an instance of RNNCell")
  if not isinstance(cell_bw, rnn_cell_impl._RNNCell):
    raise TypeError("cell_bw must be an instance of RNNCell")
  # pylint: enable=protected-access

  with vs.variable_scope(scope or "bidirectional_rnn"):
    # Forward direction
    with vs.variable_scope("fw") as fw_scope:
      output_fw, output_state_fw = dynamic_rnn(
          cell=cell_fw, inputs=inputs, sequence_length=sequence_length,
          initial_state=initial_state_fw, dtype=dtype,
          parallel_iterations=parallel_iterations, swap_memory=swap_memory,
          time_major=time_major, scope=fw_scope)

    # Backward direction
    if not time_major:
      time_dim = 1
      batch_dim = 0
    else:
      time_dim = 0
      batch_dim = 1

    def _reverse(input_, seq_lengths, seq_dim, batch_dim):
      if seq_lengths is not None:
        return array_ops.reverse_sequence(
            input=input_, seq_lengths=seq_lengths,
            seq_dim=seq_dim, batch_dim=batch_dim)
      else:
        return array_ops.reverse(input_, axis=[seq_dim])

    with vs.variable_scope("bw") as bw_scope:
      inputs_reverse = _reverse(
          inputs, seq_lengths=sequence_length,
          seq_dim=time_dim, batch_dim=batch_dim)
      tmp, output_state_bw = dynamic_rnn(
          cell=cell_bw, inputs=inputs_reverse, sequence_length=sequence_length,
          initial_state=initial_state_bw, dtype=dtype,
          parallel_iterations=parallel_iterations, swap_memory=swap_memory,
          time_major=time_major, scope=bw_scope)

  output_bw = _reverse(
      tmp, seq_lengths=sequence_length,
      seq_dim=time_dim, batch_dim=batch_dim)

  outputs = (output_fw, output_bw)
  output_states = (output_state_fw, output_state_bw)

  return (outputs, output_states)

其中调用了dynamic_rnn函数,代码如下:

def dynamic_rnn(cell, inputs, sequence_length=None, initial_state=None,
                dtype=None, parallel_iterations=None, swap_memory=False,
                time_major=False, scope=None):
  """Creates a recurrent neural network specified by RNNCell `cell`.

  This function is functionally identical to the function `rnn` above, but
  performs fully dynamic unrolling of `inputs`.

  Unlike `rnn`, the input `inputs` is not a Python list of `Tensors`, one for
  each frame.  Instead, `inputs` may be a single `Tensor` where
  the maximum time is either the first or second dimension (see the parameter
  `time_major`).  Alternatively, it may be a (possibly nested) tuple of
  Tensors, each of them having matching batch and time dimensions.
  The corresponding output is either a single `Tensor` having the same number
  of time steps and batch size, or a (possibly nested) tuple of such tensors,
  matching the nested structure of `cell.output_size`.

  The parameter `sequence_length` is optional and is used to copy-through state
  and zero-out outputs when past a batch element's sequence length. So it's more
  for correctness than performance, unlike in rnn().

  Args:
    cell: An instance of RNNCell.
    inputs: The RNN inputs.

      If `time_major == False` (default), this must be a `Tensor` of shape:
        `[batch_size, max_time, ...]`, or a nested tuple of such
        elements.

      If `time_major == True`, this must be a `Tensor` of shape:
        `[max_time, batch_size, ...]`, or a nested tuple of such
        elements.

      This may also be a (possibly nested) tuple of Tensors satisfying
      this property.  The first two dimensions must match across all the inputs,
      but otherwise the ranks and other shape components may differ.
      In this case, input to `cell` at each time-step will replicate the
      structure of these tuples, except for the time dimension (from which the
      time is taken).

      The input to `cell` at each time step will be a `Tensor` or (possibly
      nested) tuple of Tensors each with dimensions `[batch_size, ...]`.
    sequence_length: (optional) An int32/int64 vector sized `[batch_size]`.
    initial_state: (optional) An initial state for the RNN.
      If `cell.state_size` is an integer, this must be
      a `Tensor` of appropriate type and shape `[batch_size, cell.state_size]`.
      If `cell.state_size` is a tuple, this should be a tuple of
      tensors having shapes `[batch_size, s] for s in cell.state_size`.
    dtype: (optional) The data type for the initial state and expected output.
      Required if initial_state is not provided or RNN state has a heterogeneous
      dtype.
    parallel_iterations: (Default: 32).  The number of iterations to run in
      parallel.  Those operations which do not have any temporal dependency
      and can be run in parallel, will be.  This parameter trades off
      time for space.  Values >> 1 use more memory but take less time,
      while smaller values use less memory but computations take longer.
    swap_memory: Transparently swap the tensors produced in forward inference
      but needed for back prop from GPU to CPU.  This allows training RNNs
      which would typically not fit on a single GPU, with very minimal (or no)
      performance penalty.
    time_major: The shape format of the `inputs` and `outputs` Tensors.
      If true, these `Tensors` must be shaped `[max_time, batch_size, depth]`.
      If false, these `Tensors` must be shaped `[batch_size, max_time, depth]`.
      Using `time_major = True` is a bit more efficient because it avoids
      transposes at the beginning and end of the RNN calculation.  However,
      most TensorFlow data is batch-major, so by default this function
      accepts input and emits output in batch-major form.
    scope: VariableScope for the created subgraph; defaults to "rnn".

  Returns:
    A pair (outputs, state) where:

      outputs: The RNN output `Tensor`.

        If time_major == False (default), this will be a `Tensor` shaped:
          `[batch_size, max_time, cell.output_size]`.

        If time_major == True, this will be a `Tensor` shaped:
          `[max_time, batch_size, cell.output_size]`.

        Note, if `cell.output_size` is a (possibly nested) tuple of integers
        or `TensorShape` objects, then `outputs` will be a tuple having the
        same structure as `cell.output_size`, containing Tensors having shapes
        corresponding to the shape data in `cell.output_size`.

      state: The final state.  If `cell.state_size` is an int, this
        will be shaped `[batch_size, cell.state_size]`.  If it is a
        `TensorShape`, this will be shaped `[batch_size] + cell.state_size`.
        If it is a (possibly nested) tuple of ints or `TensorShape`, this will
        be a tuple having the corresponding shapes.

  Raises:
    TypeError: If `cell` is not an instance of RNNCell.
    ValueError: If inputs is None or an empty list.
  """

  # pylint: disable=protected-access
  if not isinstance(cell, rnn_cell_impl._RNNCell):
    raise TypeError("cell must be an instance of RNNCell")
  # pylint: enable=protected-access

  # By default, time_major==False and inputs are batch-major: shaped
  #   [batch, time, depth]
  # For internal calculations, we transpose to [time, batch, depth]
  flat_input = nest.flatten(inputs)

  if not time_major:
    # (B,T,D) => (T,B,D)
    flat_input = tuple(array_ops.transpose(input_, [1, 0, 2])
                       for input_ in flat_input)

  parallel_iterations = parallel_iterations or 32
  if sequence_length is not None:
    sequence_length = math_ops.to_int32(sequence_length)
    if sequence_length.get_shape().ndims not in (None, 1):
      raise ValueError(
          "sequence_length must be a vector of length batch_size, "
          "but saw shape: %s" % sequence_length.get_shape())
    sequence_length = array_ops.identity(  # Just to find it in the graph.
        sequence_length, name="sequence_length")

  # Create a new scope in which the caching device is either
  # determined by the parent scope, or is set to place the cached
  # Variable using the same placement as for the rest of the RNN.
  with vs.variable_scope(scope or "rnn") as varscope:
    if varscope.caching_device is None:
      varscope.set_caching_device(lambda op: op.device)
    input_shape = tuple(array_ops.shape(input_) for input_ in flat_input)
    batch_size = input_shape[0][1]

    for input_ in input_shape:
      if input_[1].get_shape() != batch_size.get_shape():
        raise ValueError("All inputs should have the same batch size")

    if initial_state is not None:
      state = initial_state
    else:
      if not dtype:
        raise ValueError("If there is no initial_state, you must give a dtype.")
      state = cell.zero_state(batch_size, dtype)

    def _assert_has_shape(x, shape):
      x_shape = array_ops.shape(x)
      packed_shape = array_ops.stack(shape)
      return control_flow_ops.Assert(
          math_ops.reduce_all(math_ops.equal(x_shape, packed_shape)),
          ["Expected shape for Tensor %s is " % x.name,
           packed_shape, " but saw shape: ", x_shape])

    if sequence_length is not None:
      # Perform some shape validation
      with ops.control_dependencies(
          [_assert_has_shape(sequence_length, [batch_size])]):
        sequence_length = array_ops.identity(
            sequence_length, name="CheckSeqLen")

    inputs = nest.pack_sequence_as(structure=inputs, flat_sequence=flat_input)

    (outputs, final_state) = _dynamic_rnn_loop(
        cell,
        inputs,
        state,
        parallel_iterations=parallel_iterations,
        swap_memory=swap_memory,
        sequence_length=sequence_length,
        dtype=dtype)

    # Outputs of _dynamic_rnn_loop are always shaped [time, batch, depth].
    # If we are performing batch-major calculations, transpose output back
    # to shape [batch, time, depth]
    if not time_major:
      # (T,B,D) => (B,T,D)
      flat_output = nest.flatten(outputs)
      flat_output = [array_ops.transpose(output, [1, 0, 2])
                     for output in flat_output]
      outputs = nest.pack_sequence_as(
          structure=outputs, flat_sequence=flat_output)

    return (outputs, final_state)

其中调用了_dynamic_rnn_loop函数,代码如下:

def _dynamic_rnn_loop(cell,
                      inputs,
                      initial_state,
                      parallel_iterations,
                      swap_memory,
                      sequence_length=None,
                      dtype=None):
  """Internal implementation of Dynamic RNN.

  Args:
    cell: An instance of RNNCell.
    inputs: A `Tensor` of shape [time, batch_size, input_size], or a nested
      tuple of such elements.
    initial_state: A `Tensor` of shape `[batch_size, state_size]`, or if
      `cell.state_size` is a tuple, then this should be a tuple of
      tensors having shapes `[batch_size, s] for s in cell.state_size`.
    parallel_iterations: Positive Python int.
    swap_memory: A Python boolean
    sequence_length: (optional) An `int32` `Tensor` of shape [batch_size].
    dtype: (optional) Expected dtype of output. If not specified, inferred from
      initial_state.

  Returns:
    Tuple `(final_outputs, final_state)`.
    final_outputs:
      A `Tensor` of shape `[time, batch_size, cell.output_size]`.  If
      `cell.output_size` is a (possibly nested) tuple of ints or `TensorShape`
      objects, then this returns a (possibly nsted) tuple of Tensors matching
      the corresponding shapes.
    final_state:
      A `Tensor`, or possibly nested tuple of Tensors, matching in length
      and shapes to `initial_state`.

  Raises:
    ValueError: If the input depth cannot be inferred via shape inference
      from the inputs.
  """
  state = initial_state
  assert isinstance(parallel_iterations, int), "parallel_iterations must be int"

  state_size = cell.state_size

  flat_input = nest.flatten(inputs)
  flat_output_size = nest.flatten(cell.output_size)

  # Construct an initial output
  input_shape = array_ops.shape(flat_input[0])
  time_steps = input_shape[0]
  batch_size = input_shape[1]

  inputs_got_shape = tuple(input_.get_shape().with_rank_at_least(3)
                           for input_ in flat_input)

  const_time_steps, const_batch_size = inputs_got_shape[0].as_list()[:2]

  for shape in inputs_got_shape:
    if not shape[2:].is_fully_defined():
      raise ValueError(
          "Input size (depth of inputs) must be accessible via shape inference,"
          " but saw value None.")
    got_time_steps = shape[0].value
    got_batch_size = shape[1].value
    if const_time_steps != got_time_steps:
      raise ValueError(
          "Time steps is not the same for all the elements in the input in a "
          "batch.")
    if const_batch_size != got_batch_size:
      raise ValueError(
          "Batch_size is not the same for all the elements in the input.")

  # Prepare dynamic conditional copying of state & output
  def _create_zero_arrays(size):
    size = _state_size_with_prefix(size, prefix=[batch_size])
    return array_ops.zeros(
        array_ops.stack(size), _infer_state_dtype(dtype, state))

  flat_zero_output = tuple(_create_zero_arrays(output)
                           for output in flat_output_size)
  zero_output = nest.pack_sequence_as(structure=cell.output_size,
                                      flat_sequence=flat_zero_output)

  if sequence_length is not None:
    min_sequence_length = math_ops.reduce_min(sequence_length)
    max_sequence_length = math_ops.reduce_max(sequence_length)

  time = array_ops.constant(0, dtype=dtypes.int32, name="time")

  with ops.name_scope("dynamic_rnn") as scope:
    base_name = scope

  def _create_ta(name, dtype):
    return tensor_array_ops.TensorArray(dtype=dtype,
                                        size=time_steps,
                                        tensor_array_name=base_name + name)

  output_ta = tuple(_create_ta("output_%d" % i,
                               _infer_state_dtype(dtype, state))
                    for i in range(len(flat_output_size)))
  input_ta = tuple(_create_ta("input_%d" % i, flat_input[0].dtype)
                   for i in range(len(flat_input)))

  input_ta = tuple(ta.unstack(input_)
                   for ta, input_ in zip(input_ta, flat_input))

  def _time_step(time, output_ta_t, state):
    """Take a time step of the dynamic RNN.

    Args:
      time: int32 scalar Tensor.
      output_ta_t: List of `TensorArray`s that represent the output.
      state: nested tuple of vector tensors that represent the state.

    Returns:
      The tuple (time + 1, output_ta_t with updated flow, new_state).
    """

    input_t = tuple(ta.read(time) for ta in input_ta)
    # Restore some shape information
    for input_, shape in zip(input_t, inputs_got_shape):
      input_.set_shape(shape[1:])

    input_t = nest.pack_sequence_as(structure=inputs, flat_sequence=input_t)
    call_cell = lambda: cell(input_t, state)

    if sequence_length is not None:
      (output, new_state) = _rnn_step(
          time=time,
          sequence_length=sequence_length,
          min_sequence_length=min_sequence_length,
          max_sequence_length=max_sequence_length,
          zero_output=zero_output,
          state=state,
          call_cell=call_cell,
          state_size=state_size,
          skip_conditionals=True)
    else:
      (output, new_state) = call_cell()

    # Pack state if using state tuples
    output = nest.flatten(output)

    output_ta_t = tuple(
        ta.write(time, out) for ta, out in zip(output_ta_t, output))

    return (time + 1, output_ta_t, new_state)

  _, output_final_ta, final_state = control_flow_ops.while_loop(
      cond=lambda time, *_: time < time_steps,
      body=_time_step,
      loop_vars=(time, output_ta, state),
      parallel_iterations=parallel_iterations,
      swap_memory=swap_memory)

  # Unpack final output if not using output tuples.
  final_outputs = tuple(ta.stack() for ta in output_final_ta)

  # Restore some shape information
  for output, output_size in zip(final_outputs, flat_output_size):
    shape = _state_size_with_prefix(
        output_size, prefix=[const_time_steps, const_batch_size])
    output.set_shape(shape)

  final_outputs = nest.pack_sequence_as(
      structure=cell.output_size, flat_sequence=final_outputs)

  return (final_outputs, final_state)

这段代码看的还不是太清楚,留到以后分析。
自此我们对我们的语音识别系统中的网络结构已经有了一定的认识了,后面我们将分析一下该网络是如何运行起来的。

主要参考:
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
http://blog.csdn.net/jojozhangju/article/details/51982254

2018-01-04 13:47:58 m0_37788308 阅读数 966
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5927 人正在学习 去看看 杨波

1.语音识别系统的基本结构

这里写图片描述

2.涉及算法

这里写图片描述

3.GMM高斯混合模型

3.1高斯混合模型的基本概念

  高斯混合模型是指具有如下形式的概率分布模型:

p(yθ)=k=1kαkϕ(yθk)

其中,αk是系数,αk0kk=1αk=1ϕ(yθk)是高斯分布密度,θk=(μk,σ2k)
ϕ(yθk)=12πσkexp((yμk)22θ2k)

称为第k个分模型。
  将二变量的混合高斯分布可以推广到多变量的多元混合高斯分布,其联合概率密度函数可写为:
p(x)=k=1Mcm(2π)(D/2)|m|1/2exp[12(xμm)T1m(xμm)]

=Mm=1CmN(x;μm,μm),(Cm>0)

3.2用EM算法解决高斯混合分布问题

  具体EM算法讲解见另一篇博客:《PLSI主题模型》中EM算法的具体讲解部分:
http://blog.csdn.net/m0_37788308/article/details/78115378
  实现EM算法估计高斯混合分布的python代码如下,其中有调用sklearn包进行的估计和直接写的EM算法来估计高斯混合分布。

# -*- coding:utf-8 -*-
#EM算法估计高斯混合分布的参数
import numpy as np
from scipy.stats import multivariate_normal
from sklearn.mixture import GaussianMixture
from mpl_toolkits.mplot3d import Axes3D
import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn.metrics.pairwise import pairwise_distances_argmin

mpl.rcParams['font.sans-serif'] = [u'SimHei']
mpl.rcParams['axes.unicode_minus'] = False

if __name__ == '__main__':
    style = 'myself'
    # style = 'sklearn'
    np.random.seed(0)
    mu1_fact = (0, 0, 0)
    cov1_fact = np.diag((1, 2, 3))
    data1 = np.random.multivariate_normal(mu1_fact, cov1_fact, 400)
    mu2_fact = (2, 2, 1)
    cov2_fact = np.array(((1, 1, 3), (1, 2, 1), (0, 0, 1)))
    data2 = np.random.multivariate_normal(mu2_fact, cov2_fact, 100)
    data = np.vstack((data1, data2))
    y = np.array([True] * 400 + [False] * 100)
    # print y
    if style == 'sklearn':
        g = GaussianMixture(n_components=2, covariance_type='full', tol=1e-6, max_iter=1000)
        g.fit(data)
        print '类别概率:\t', g.weights_[0]
        print '均值:\n', g.means_, '\n'
        print '方差:\n', g.covariances_, '\n'
        mu1, mu2 = g.means_
        sigma1, sigma2 = g.covariances_
    else:
        num_iter = 100
        n, d = data.shape
        # 随机指定
        mu1 = np.random.standard_normal(d)
        print mu1
        mu2 = np.random.standard_normal(d)
        print mu2
        mu1 = data.min(axis=0)
        mu2 = data.max(axis=0)
        sigma1 = np.identity(d)
        sigma2 = np.identity(d)
        pi = 0.5
        # EM
        for i in range(num_iter):
            # E Step
            norm1 = multivariate_normal(mu1, sigma1)
            norm2 = multivariate_normal(mu2, sigma2)
            tau1 = pi * norm1.pdf(data)
            tau2 = (1 - pi) * norm2.pdf(data)
            gamma = tau1 / (tau1 + tau2)

            # M Step
            mu1 = np.dot(gamma, data) / np.sum(gamma)
            mu2 = np.dot((1 - gamma), data) / np.sum((1 - gamma))
            sigma1 = np.dot(gamma * (data - mu1).T, data - mu1) / np.sum(gamma)
            sigma2 = np.dot((1 - gamma) * (data - mu2).T, data - mu2) / np.sum(1 - gamma)
            pi = np.sum(gamma) / n
            print i, ":\t", mu1, mu2
        print '类别概率:\t', pi
        print '均值:\t', mu1, mu2
        print '方差:\n', sigma1, '\n\n', sigma2, '\n'

    # 预测分类
    norm1 = multivariate_normal(mu1, sigma1)
    norm2 = multivariate_normal(mu2, sigma2)
    tau1 = norm1.pdf(data)
    tau2 = norm2.pdf(data)

    fig = plt.figure(figsize=(13, 7), facecolor='w')
    ax = fig.add_subplot(121, projection='3d')
    ax.scatter(data[:, 0], data[:, 1], data[:, 2], c='b', s=30, marker='o', depthshade=True)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title(u'原始数据', fontsize=18)
    ax = fig.add_subplot(122, projection='3d')

    order = pairwise_distances_argmin([mu1_fact, mu2_fact], [mu1, mu2], metric='euclidean')
    print order
    if order[0] == 0:
        c1 = tau1 > tau2
    else:
        c1 = tau1 < tau2
    c2 = ~c1
    acc = np.mean(y == c1)
    print u'准确率:%.2f%%' % (100*acc)

    ax.scatter(data[c1, 0], data[c1, 1], data[c1, 2], c='r', s=30, marker='o', depthshade=True)
    ax.scatter(data[c2, 0], data[c2, 1], data[c2, 2], c='g', s=30, marker='^', depthshade=True)
    ax.set_xlabel('X')
    ax.set_ylabel('Y')
    ax.set_zlabel('Z')
    ax.set_title(u'EM算法分类', fontsize=18)
    plt.suptitle(u'EM算法的实现', fontsize=21)
    plt.subplots_adjust(top=0.90)
    plt.tight_layout()
    plt.show()

4.HMM隐马尔可夫模型

4.1隐马尔可夫的基本概念

  隐马尔可夫模型是关于时序的概率模型,描述有一个隐马尔可夫链随机生成不可观测的状态随机序列,再由各个状态生成一个观测而产生观测随机序列过程。隐马尔可的“隐”字主要体现在隐马尔可夫链随机生成的“状态序列”;每一个状态生成一个观察,称为“观察序列”,观察序列我们是可以看到的,但是“状态序列”就是我们常常难理解的地方。
  隐马尔可夫由“初始概率分布”、“状态转移概率分布”以及“观察概率分布”这三个分布确定。
Q所有可能的状态集合,V是所有可能的观测集合。

Q={q1,q2,...,qN}

V={v1,v2,...,vM}

其中,N是可能的状态数,M是可能的观测序列。I是长度为T的状态序列,O是对应的观测序列。
I={i1,i2,...,ir}

O={o1,o2,...,or}

A是状态转移概率矩阵:
A=[aij]NN

其中,aij=p(it+1=qj|it=qi),i=1,2,...,N;j=1,2,...,N是在时刻t处于状态qi的条件下在时刻t+1转移到状态qj的概率。
B是观测概率矩阵:
A=[bj(k)]NN

其中,
bj(k)=p(ot=vk|it=qj),k=1,2,...,M;j=1,2,...,N

是在时刻t处于状态qj的条件下生成观测v_{k}的概率。
π是初始状态概率向量:
π=(πi)

其中,
πi=p(i1=qi),i=1,2,...,N

是时刻t=1处于状态qi的概率。
隐马尔可夫模型由初始状态概率向量π、状态转移概率矩阵A和观测概率矩阵B决定。πA决定状态序列,B决定观测序列。因此,隐马尔可夫模型λ可以用三元符号表示,即:
λ=(A,B,π)

A,B,π称为隐马尔可夫模型的三要素。

4.2隐马尔可夫的基本假设

从定义可知,隐马尔可夫模型作了两个基本假设:
  (1)齐次马尔科夫性假设,即假设隐藏的马尔科夫链在任意时刻t的状态只依赖与前一时刻的状态,与其他时刻的状态及观测无关,也与时刻t无关。

p(it|it1,ot1,...,i1,o1)=p(it|it1),t=1,2,...,T

  (2)观测独立性假设,即假设任意时刻的观测只依赖于该时刻的马尔科夫链的状态,与其他观测状态无关。
p(ot|iT,oT,iT1,oT1,...,it+1,ot+1,it,it1,ot1,...,i1,o1)=p(ot|it)

4.3HMM的三个基本问题

  (1)概率计算问题。给定模型π=(A,B,π)和观测序列O=(o1,o2,...,oT),计算在模型λ下观测序列O出现的概率p(O|λ)
  (2)学习问题。已知观测序列O=(o1,o2,...,oT),估计模型λ=(A,B,π)参数,使得在该模型下观测序列概率p(O|λ)最大。即用极大似然估计的方法估计参数。
  (3)预测问题。也称为解码问题。已知模型λ=(A,B,π)和观测序列O=(o1,o2,...,oT),求对给定观测序列条件概率p(I|O)最大的状态序列I=(i1,i2,...,iT)。即给定观测序列,求最有可能的对应的状态序列。

5.GMM-HMM如何应用在语音识别上

  解释:这里的语音识别指的是语音的分类,并不是字面意义理解的语音转化为文字。其实这里的语音识别和传统意义上的分类算法一样,通过训练集(train)的训练,对测试集(test)进行分类。只是传统的分类算法输入的是数字数据,而这里的输入的语音。输入的语音信号服从高斯混合分布,也就是隐马尔可夫模型中的第二个问题(学习问题),将语音信号用隐马尔可夫模型中的向前算法(BW算法,也可以理解为EM算法)进行分解,求出隐马尔可夫的三要素(三个参数λ=(A,B,π)),将参数作为识别语音的特征值,从而进行语音的分类识别。

6.问题讲解

(1)在浏览博客中,如果有不理解的地方可以参考视频讲解资料。视频讲解资料链接为:http://v.youku.com/v_show/id_XMzMzMzE1NTY2OA==.html?qq-pf-to=pcqq.c2c
(2)需要《GMM-HMM在语音识别中的应用》的python代码,可以留下邮箱地址。

7.参考文献

[1]李航. 统计学习方法[M]. 清华大学出版社:李航, 2012. 171-189

2019-07-22 16:04:42 NIeson2012 阅读数 2231
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5927 人正在学习 去看看 杨波

目录

1.背景

2.孤立词识别

2.1 特征提取

2.2 动态弯算法

2.3 GMM(Gaussian mixture model)

2.4 HMM(Hidden markov model)

2.5 EM训练算法

2.6 语音识别基本方程

3.连续语音识别

3.1 语言模型

3.2 大词汇量

3.3 语音识别系统结构

3.4 评价指标:WER

4. 潘多拉魔盒

4.1 上下文有关模型

4.2 区分式训练

4.3 说话人适应

4.4 二次打分

5.结语

参考文献


1.背景

在讲解每一个知识或者技术之前,我们通常先说到它的概念,当然语音识别技术也不例外。那么什么是语音识别呢,语音识别就是把语音转换成文字。科大讯飞的输入法就是一个典型的语音识别的例子,我们可以在不方便打字又不想让别人听到自己声音的时候,就可以使用这种方法,它可以把语音信号转化成文字。全球有很多种语言,怎么让不同语言的信号转换成其对应的文字呢。这就衍生了语音识别的一些相关课题。

元数据识别:就是无论说话人说的是中文还是外语,我都可以将它转换成对应的汉子或者外国文字,而且更加神奇的是,它还知道在一句话的什么地方该加标点符号,这都属于元数据的识别范畴;语音增强与分离:在很多语音中挣钱并分离出自己需要的语音信号,最典型的的例子就是鸡尾酒宴会,意思就是在这个宴会上会有很多声音音乐声,不同的人的说话声,这时我想从这些混杂的语音中提取小明的声音以及他说话的内容,这时我们就可以用到语音增强与分离技术,这个可以用SVD实现;语音合成与转换:语音合成是语音识别的逆过程,也就是说语音合成是把文字转化成语音信号;自然语言处理的应用场景也非常多,像Siri或者win10上的小娜都是基于NLP的。说完概念,接下来说一说语音识别的应用。

语音识别技术在我们生活中见得非常多,比如语音助手、导航系统等等。这里就不多介绍。说完这些,我们就将进入语音识别技术的前世今生的前世部分。

这是两个带着红领巾的少先队员,为什么以这个图片为背景呢,我猜测可能和语音识别技术的历史有关。因为语音识别技术的前世是在20世纪80年代兴起的,而那时,也就是上世纪八九十年代,带着红领巾的少先队员正是风靡全国,曾经作为一名少先队员,我骄傲。既然说到这里,我就大致说下语音识别技术的发展史:1952年贝尔实验室的Davis等人研究成功了世界上第一个能识别10个英文数字发音的实验系统;20世纪70年代孤立词识别取得实质性的进展;1987年李开复开发世界上第一个‘非特定人连续语音识别系统’,(是的,就是那个创新工场的李开复,人家不仅能当总裁,还是一个科技大牛,不得不服呀,而且,王博士跟李开复是一个实验室的),用统计的方法提升了语音识别率;2011年微软的DNN深度神经网络模型在语音识别领域取得成功。具体的发展史可以看参考文献1中的小视频。

 

2.孤立词识别

每一个单词都有模板,用于待预测语音与它进行比较,如上图所示,右边蓝色的语音信号到底是说的Yes还是No。我们直观的看一下,可能感觉它是Yes的可能性更大,因为Yes的信号波形中有两个包,No只有一个包,而待预测的信号也是有两个包,从波形上我们可以大概判断出待预测信号是Yes的可能性更大一些,当然这种看波形的方法实不可取的,更是不科学的。那么我们应该怎么做呢,就是计算待预测信号与模板信号的距离,其实也就是相似程度,距离越短,相似程度越高。但是信号的距离该怎么比较呢,是通过把信号转换成特征向量,然后比较向量之间的欧式距离或者其他的距离,接下来,就来到了特征提取的部分。

2.1 特征提取

对于一段语音信号,我们通常是一段一段的分析,我们提取一帧信号,它的长度通常为20~50ms,一帧信号要至少包含2~3个周期,那你怎么知道20~50ms就能有至少2~3个周期呢,因为人说话的频率是在100Hz左右,所以语音信号的周期就是10ms,微观上我们感觉一帧信号特别长,竟然包含几个周期,但其实,50ms也就是0.05s,比一眨眼的的时间还短。

接下来我们就需要分析这一帧信号,对一帧信号做傅里叶变换,这样可以得到信号的频域的信息。有关傅里叶变换的知识可以看参考文献2,讲的非常通俗易懂。经过傅里叶变换之后,我们会得到这一帧信号的频谱,它由两部分组成,其中蓝色的线表示的是精细结构反应的是音高,在汉语中比较常用,用来识别汉字的声调,但是在外语中用处相对较小;红色的线表示的是包络,也就是频谱的大致幅度,它反映的是音色,包含主要的信息。可以看到,包络的峰要比精细结构少很多,只是在500Hz、1700Hz和2500Hz的地方有峰值。像精细结构这样的多峰的波形分析起来比较复杂,其实可以直接用包络表示这段语音信号,虽然不能丝毫不差的表示,但是它包含了这段信号的大部分信息。但是,我们通常不会直接用包络表示语音信息,那么我们应该怎么做呢,就是对精细结构进行滤波,继续往下看。

我们对一帧信号进行三角滤波,中间那副图片中的蓝线表示的就是精细结构,红线表示的就是三角滤波器。可以看到,左边的三角滤波器比较密,这是跟人耳的频率特性相关的,人耳对低频信号比较敏感,所在低频部分的分辨率较高,高频部分的分辨率就相对较低。经过三角滤波后,我们得到最下边的成为滤波器组输出的图像,可以看出,Filterbank output的峰的个数相对精细结构也变少了,我们再一看,它和频谱的包络在形状上很相似,实际上,他就是频谱包络的一种近似。最下边这幅图的横坐标表示三角滤波器的标号,此示例中用了40个三角滤波器,纵坐标表示信号的能量,可以看出频谱精细结构在500Hz处有个高峰,所以在500Hz处的三角滤波器中的能量比较大,500Hz大约对应第12个三角滤波器,在下图可以看到,横坐标在12处有个高峰。其实这个Filterbank output就基本可以作为这一帧语音的特征了,但是我们还通常会进一步做数学变换来达到数据压缩的目的,继续往下看。

通常情况下我们会对Filterbank output做一个对数变换或者离散余弦变换,这样就可以吧Filterbank output压缩为一个13维的特征向量,称为MFCC。经过一系列处理之后,我们得到了一帧语音信号的特征向量,那么怎么对整个一段语音信号进行处理呢,我们继续往下看。

对于一段语音信号,我们通常利用类似于滑动窗口的方法沿着时间的维度一帧帧的将信号堆叠起来,窗宽就是一帧信号的长度,两个窗口之间通常会有10ms的重叠以防信息的泄漏,这样把一帧帧信号按列堆叠起来,形成了中间的语谱图,横坐标为帧的序号,纵坐标为评率,右边的Colorbar表示信号的能量,自下向上能量越来越大,对语谱图进行数学变换之后,我们可以得到MFCC序列,它就表示语音信号中的有用特征。接下来看看特征提取的一些特点。

在语音识别领域中MFCC序列是最常用的特征,他主要描述的是频谱包络,也就是音色。它的优点就是排除基频也就是精细结构;一帧语音信号通常会有成百上千个采样点,而MFCC序列只需要一个13维的特征向量就可以表示这一帧信号的主要特征信息,所以有维度低的特点;符合听觉就是指在低频处三角滤波器密集而在高频处三角滤波器稀疏。万物都有两面性,MFCC序列有优点就会有缺点,它一次只能看一帧也就是20~50ms的信号,即视野小;还有就是受噪声、回声影响等。关于它的改进部分就不多说了。

2.2 动态弯算法

在文章的开头说过,需要计算待预测语音与模板语音之间的距离,从而判断它想要表达的内容。经过特征提取之后我们将语音信号编程了一个个13维的特征向量,那么我们该怎么计算两个特征序列之间的距离呢。这里用到了DWT算法,将待识别语音中的每一帧与模板中最相似的一帧匹配,同时要保证顺序不要改变。因为一个词或者一句话顺序改变,意思也会完全改变。在上图中我们用一个竖条表示一帧语音信号的特征向量,通过DTW算法将待识别语音的特征向量与模板特征向量对齐之后,计算待识别语音中的每一帧语音与模板中每一帧的欧式距离,然后总距离为所有帧的欧式距离之和。通过计算待识别语音与Yes模板和No模板的欧式距离,取两者中距离小的那个为最终识别结果即可。其实到此,孤立词识别问题就已经解决了,但是人总是不满足的, 总是想找到更好的方法,那么我们继续往下看。

2.3 GMM(Gaussian mixture model)

我们知道,同一个人或者不同的人说一个汉字或者单词的发音是存在区别的,那么我一个词比如Yes只录一个模板的话,那么最后的识别正确率会降低。这就要求我们需要多录几个模板,在上图中,中间的五个特征向量为待识别语音,上下分别为待识别语音的模板,那么经过DTW算法之后,我们把对齐的几个特征向量整合在一块,这样原来的模板就变成了右边的有五个状态的模型,然后我们就让待识别语音与模型中的每个状态进行对齐,这样的话,识别的准确率会大大提升。接下来我们需要使用GMM来拟合每个状态中特征向量的分布,顾名思义,GMM其实就是多个高斯分布叠加而成的一种模型,如右下角的图形就是在二维空间下的GMM,可以看到,它的形状有点像地理中的等高线,这其实是一个轮廓图,经过GMM后,可以得出特征向量的概率密度并以此代替欧式距离。训练好GMM后,我们怎么用模型识别未知语音呢,继续往下看。

我们首先用DTW算法把待识别语音与模型中的状态对齐,计算每一个特征向量在模型中的概率密度,并以此来代替向量间的欧式距离。然后把每一帧的概率密度(这里一帧和一个特征向量是一样的,都是用一个竖线表示)相乘得到了在模型下待识别语音的概率,这里假设各帧之间是独立的。最后取P(待识别语音|模型)最大的模型为识别结果。还是以文章开头的例子为说明,计算待识别语音中的每一帧分别在Yes模型和No模型中的每一状态中的概率密度,最后相乘得到P(待识别语音|Yes模型)和P(待识别语音|No模型),取两者中较大的值对应的模型就是待识别语音最后的结果。

2.4 HMM(Hidden markov model)

接下来就轮到了前世中的主角闪亮登场了,也就是隐马尔可夫模型(HMM),它将模型完全概率化,概率可以为我们提供更多的信息,比如我想知道明天是晴天还是阴天或者下雨,告诉你结果,不如告诉你这三者中每一个发生的概率,其实我们也经常在天气预报中听到,某某地区的降水概率是多少,这显然比直接告诉我们明天是下雨或者晴天包含的信息更多。HMM在GMM的基础上增加了转移概率。什么是转移概率呢,如上面的图中所示,初始状态为第一个状态,那么它的下一状态还为第一状态的概率为0.7,下一状态为第二状态的概率为0.3,这里的0.7和0.3就是从当前状态转移到下一状态的概率,也就是转移概率。我们可以看到,待识别语音的每一帧与模型中的状态对齐之间也有个概率,也就是0.016,0.028这些值,这些值可以由GMM计算得出,也就是观测概率。我们把GMM测得的观测概率和HMM的转移概率相乘,就得到了在某模型下,语音和对齐方式的联合概率。接下来看看,HMM的特点。

HMM主要有两个特点,第一个就是特征序列由隐状态产生,隐状态也就是上面说到的五个状态,为什么称之为隐状态呢,是因为每帧特征向量和状态之间的对齐方式是未知的;另一个特点就是马尔可夫性,这里就不多说了。有关GMM,HMM,包括下面将要说的EM的相关知识,我推荐大家可以看一下悉尼科技大学的徐亦达老师在优酷的一个自频道,里面详细的讲解了概率统计的一些知识,并且每一个概念都有详细的公式推导,值得一看,教学视频见参考文献3。说完模型的特点,接下来说一下模型的参数,实际上HMM是有三个参数的:转移概率,观测概率以及初始概率。但是上面示例中的HMM是单向的,所以初始状态默认为第一个状态,所以,模型的参数就变成了两个:转移概率和观测概率。

接下来就看看HMM在语音识别领域中是如何使用的。HMM的三大问题:第一个问题就是求值问题,也就是给定模型参数和待识别语音,求概率,因为对齐方式是事前不知道了,所以我们就需要枚举每种对齐方式,由前面讲到的公式P(语音,对齐方式|模型) = GMM观测概率*HMM转移概率可以算出联合概率P(语音,对齐方式|模型),然后对所有对齐方式求和,联合概率就变成了边缘概率P(语音|模型),但是对齐方式是多种多样的,对枚举每一种对齐方式是很困难的,所以这里就引入了动态规划算法中的前向算法,个人对动态规划的算法不了解,所以这里就不多进行说明;第二个问题就是解码问题,也就是给定模型参数和语音,求最佳对齐方式的问题,那么什么才是最佳的对齐方式呢,最佳对齐方式就是使联合概率P(语音,对齐方式|模型)最大的那种对齐方式,这里用到了动态规划的算法中的Viterbi,这个算法其实就是DTW算法的升级版本,最佳对齐方式的概率可以作为总概率的近似,这里说明一下它的意思,比如我有1000种对齐方式,那么最佳对齐方式只是其中的一种,如果假设对齐方式服从均匀分布的话,当然这时候也就没有了最佳而言,因为每一种对齐方式的概率都是一样的都是0.1%,这显然跟总概率相差甚远,实际上并不是这样了,通常情况下最佳对齐方式的概率跟非最佳对齐方式的概率不是一个数量级,也就是说我最佳概率可能为90%,而你其他999中非最佳的概率和才占10%,所以,这种情况下,可以把最佳概率作为总概率的近似;最后一个问题就是训练问题,训练问题就是给定语音和模型结构,求模型参数的问题。将在下面的内容中详细的讲解。

2.5 EM训练算法

最大期望算法(Expectation-maximization algorithm,EM)在统计中被用于寻找,依赖于不可观察的隐性变量的概率模型中,参数的最大似然估计。我们事先是不知道对齐方式的,但是我们可以随机假设一种对齐方式,假设Yes这个词的发音一共有15帧信号,那么我们可以这样分割,前五帧代表Y的发音,中间五帧代表e的发音,最后五帧代表s的发音,然后以这种对齐方式求得模型的参数,然后再反过来更新对齐方式,这里没有再使用DTW,而是使用Viterbi或者Forward-backward算法,一直这样不断更新下去,直到收敛为止。

2.6 语音识别基本方程


最后讲一下语音识别的基本方程。这个方程其实就是语音识别技术要解决的问题,它是求得在已知X也就是待识别语音的条件下它被识别为任意一个单词W的概率,比如我们说段语音,它可以被识别为Yes也可以被识别为No或者其他,语音识别要做的就是找出被识别为某一个词最大的概率,也就是W*。P(W|X)是条件概率,我们把它经过贝叶斯公式变换一下得到P(X|W)P(W)/P(X),其中P(X|W)称为likelihood也就是似然函数,在这里它是指的声学模型,根据这个式子P(W|X)=P(X|W)P(W)/P(X)而言,要想使P(W|X)最大,我们要做的无非就是让分子尽量大,分母尽量小。因为给定了X以后P(X)就可以认为是固定了,那么最大化式子P(W|X)=P(X|W)P(W)/P(X),就变成了最大化式子P(W|X)=P(X|W)P(W),那么就需要让似然函数也就是声学模型P(W|X)和先验概率P(W)尽量大,声学模型我们可以用GMM和HMM训练得到,而且上一张PPT中,EM训练算法可以最大化似然函数也就是P(W|X)。那么什么是先验概率呢,先验概率就是一个人说某一个词或字的概率,比如我们一天中说“我吃米饭“”的次数比较多,那么它们的概率就比较大,而说“饭米吃我”的次数极少,那么它们的概率就小,那么这些概率就是所谓的先验概率,当似然函数也就是声学模型不能给我们提供足够的信息的时候,也就是P(W|X)很小的时候,我们就会通过先验概率P(W)来判断这个语音说的是什么,对于一段待识别语音,我们会更倾向与把它识别为“我吃米饭”而不是“饭米吃我”,当然这个我举的这个例子是一句话了,不是单个的孤立词,但是意思是一样的。那么,到目前为止,有关孤立词识别的内容就全部讲完了,接下来,将讲解连续语音识别的相关内容。

3.连续语音识别

什么是连续语音识别呢,我们把上面的孤立词都变成句子就是连续语音识别了。W和就不再表示一个词,而是表示一句话;同样的孤立词的似然也就是声学模型也变成了句子的声学模型,它可由一个个单词的声学模型串联起来,但是一定要保证顺序。最后的孤立词的先验概率P(W)也变成了句子的先验概率,也就是我们上面举的例子,“我吃米饭”与“饭米吃我”,哪一个更像我们正常人说的话,那么我们也称作句子的先验概率为语言模型。接下来,我们将详细介绍语言模型。

3.1 语言模型

在语言模型中,会经常用到链式法则,其实就是概率论中的乘法公式,即某一句话出现的概率等于第一个字出现概率乘以已知第一个字出现的条件下第二个字出现的概率乘以已知前两个字出现的概率下第三个字出现的概率依次类推。我们以“皮卡皮卡丘”这句话为例,链式法则用公式表示就是P(皮卡皮卡丘) = P(皮)*P(卡|皮)*P(皮|皮卡)*P(卡|皮卡皮)*P(丘|皮卡皮卡)。但是通常情况下,当我们说皮卡后,下一个字是皮的概率很小,也就是P(皮|皮卡)很小,而且不容易计算。为了解决问题,提出了n-gram模型,这是一种什么样的存在呢,也就是说每个词只与前n-1个词有关,当每个词只与前1个词有关时,我们称之为Bigram,这其实就跟Markov模型是一样的了,我们怎么计算这样的概率呢,这需要搜集大量的语料,以P(丘|卡)为例,我们需要搜集卡*出现的次数,然后搜集卡丘出现的次数,用卡丘出现的次数除以卡*的次数就是P(丘|卡);同样的,当每个词只与前两个词有关的时候,我们称之为Trigram。还有一些最大熵和神经网络的语言模型,它们比较复杂,我本人也不太懂,所以不多介绍。接下来,我们主要看下Bigram模型。

正如我上面所说的。Bigram实际上就是马尔可夫模型,下一个词只与当前词有关,可以可到,这里它不再像孤立词中的HMM一样,这里的模型它是双向,也称为是遍历的,它们之间的状态可以相互转移,上图为例,“皮”转移到“卡”的概率为0.5,转移到“丘”的概率为0.3,还是“皮”的概率为0.2。那么我们把语言模型看做一个马尔可夫模型有什么好处呢,其实这样的话,它就可以与单词的声学模型进行复合。什么意思呢,我们以上图中下边这个状态转移图为例进行简要的说明。如上图所示,把每个字都分成了3个状态,前两个状态只能在这个顺序字内转移,也就是说皮1不能直接到皮3或者到卡以及丘的内部状态中,但是第三个状态可以在字间或者字内转移。以皮3为例,假设皮3转移到自身的概率为0.6,那么它转移到其它转态的概率为0.4,这里的0.4包括字内转态转移和字间转态转移,因为字间转移只能由第三个状态完成,在这里也就是皮字转移到其它字只能由皮3完成,我们在上边那个三角形的转移图中可以看到,皮字转移到卡的概率为0.5,那么0.5*0.4 = 0.2就是皮3转移到卡1的概率;同样的0.3*0.4 = 0.12就是皮3转移到丘1的概率,0.4*0.2 = 0.08就是皮3转移到皮1的概率。因为语音信号的顺序问题,比如“卡”字的发音是按照卡1---卡2---卡3这样的顺序完成的,所以字间转移,也就是第三转态只能转移到本字或者其他字的第一状态。我们可以将待识别语音在复合模型中遍历,让待识别语音跟复合模型中的状态进行匹配,计算状态的观测概率,寻找最佳路径,然后将最佳路径上的单词按照顺序排列起来就得到了连续语音信号的识别。我们通常说的语音都是大词汇量的,那么我们该怎么去识别大词汇量的语音呢,继续往下看。

3.2 大词汇量

我们前边讲到的是为每一个单词训练单独的HMM,也就是每一个单词录不同的模板组成模型。但是语言库中的汉字或者单词都是上万级别的,如果为每个汉字或者单词都录入几个不同的模板的话,这样的工作量就太大了,所以这种方法是不可取的。我们知道汉语是按照拼音进行发音的,我们可以为字母表中的拼音训练HMM,同样的我们可以为英语中的音标训练HMM,这样我们就把每个单词训练的HMM变为了为每个因素训练HMM。这样之前原来的复合模型就变成了上图中更加庞大的一个复合模型,我我们得到了这个模型,那么我们应该怎么训练它呢,请继续往下看。

训练的过程和之前的训练是相同的,它在音素串内部的HMM仍然是单向的,我们还是采用EM算法。这里就不多说了,接下来就是解码的过程,当给定一门语言的解码器(这包括语言模型,词典(词典的作用就是把音素拼接为一个单词)和声学模型)和一条语音,我们就可以使用Viterbi算法求得最佳路径,最佳路径其实还是回归到概率问题,概率最大的路径就是最佳路径,路径上的单词就是识别的结果。那么我们就完成了大词汇量的连续语音信号的识别问题,接下来我们看看语音识别系统的一个整体结构框架。

3.3 语音识别系统结构

首先是将一段语音信号经过特征提取的操作后把信号变成了一个MFCC序列,特征提取也称为前端,那么后端主要是由解码器组成的,解码器又包括声学模型、词典和语言模型。声学模型也就是似然,它描述的是单词或者音素的发音情况,主要由GMM和HMM来完成,词典可以把音素拼接起来组成单词或者汉字,语言模型就是把单词整合成符合人类说话习惯的连续的语音,也就是把单词整合成一句话,最后将结果输出就是我们最终想要得到的识别结果。模型都有评价指标,这个模型到底好不好,我们该怎么去评价它呢,接下来我们就讲解语音识别系统的评价指标。

3.4 评价指标:WER


语音识别系统常用的额评价指标就是词错误率,英文是word error rate,简写为WER。那么怎么计算呢,PPT中写的很详细,我在这里就不多说了。例子也不说了。

我们可以看到,这张PPT中的对齐方式跟上一张不太一样,但是最终的词错误率是一样的,所以最优对齐方式不一定是唯一的。词错误率有一个缺陷就是它只能发现词是对是错,却不能说明词错误的程度,比如我说“小狗”,它识别成了“小巩”或者识别成“小猫”的词错误率都是50%,但是很明显,把“小狗”识别成“小猫”的错误程度更大,但是语音识别中还是通常使用词错误率作为评价指标而不会去在意那些细节性的东西。还有一点就是WER可能高于100%,这是因为你错的词的个数比正确的答案句子的长度还要多,这就会导致WER高于100%,如上边的例子所示。

这两幅PPT主要是讲解的WER的发展史,这里就不多做介绍。

4. 潘多拉魔盒

这么长时间内语音识别的框架没有发生改变,并不是说技术没有进步,只是人们把框图中的每个人部分更加优化了,解决的方法更好了。下面主要讲后端部分的优化。

4.1 上下文有关模型

上下文有关模型实际上就是在说同样的音素在前后音素发音不同时,中间的音素的发音也有微小的不同,上边的PPT以five和nine为例说明了这个问题,在英语中大约有50个音素,每个因素有3个状态的话,这样也就是有150个状态,但是有了上下文有关音素之后,这个音素受它的上一个音素、下一个音素以及它本身这三个音素的影响,每一个音素有50种情况,那么一种有50*50*50种情况,也就是125000种情况,这显然是非常多的。于是就有了上下文聚类的一种方法,这样最终的状态数会在几千的数量级,这对于我们来说还是可以接受的。

4.2 区分式训练

4.3 说话人适应

4.4 二次打分

5.结语

至此,语音识别技术前世今生之前世的内容就讲完了,下一篇文章将继续讲解语音识别技术前世今生之今生的内容。

语音识别技术的前世今生【今生篇】

 

参考文献

1、语音识别技术的前世今生:https://www.bilibili.com/video/av19158521/

2、傅里叶变换:https://zhuanlan.zhihu.com/p/19763358

3、徐亦达自频道:http://i.youku.com/i/UMzIzNDgxNTg5Ng==?spm=a2hzp.8253869.0.0
 

2017-04-04 17:29:52 u013131595 阅读数 555
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5927 人正在学习 去看看 杨波

上回我们分析了系统网络的基本结构,那么我们的网络又是如何训练的呢?要回答这个问题,我们先得回答我们的数据是如何获得的,这回我们就来分析一下这个过程。

1.调用关系图

数据首先通过Tf_train_ctc类中的set_up_model函数调用datasets.py中的read_datasets函数返回一个DataSet类型的对象,该对象包含处理数据相关的配置信息。然后Tf_train_ctc类使用上一步返回的self.data_sets在run_batches函数中调用DataSet类中的next_batch函数返回一次训练所需的batch。具体调用关系图如下所示:

Created with Raphaël 2.1.0Tf_train_ctcTf_train_ctcDataSetDataSetread_datasetsself.data_setsrun_batchessource, source_lengths, sparse_labels

下面开始结合代码进行详细分析。

2.read_datasets函数

该函数返回一个带有数据处理配置信息的DataSet类型对象,具体代码如下:

def read_datasets(conf_path, sets, numcep, numcontext,
                  thread_count=8):
    data_dir, dataset_config = _get_data_set_dict(conf_path, sets)

    def _read_data_set(config):
        path = os.path.join(data_dir, config['dir_pattern'])
        return DataSet.from_directory(path,
                                      thread_count=thread_count,
                                      batch_size=config['batch_size'],
                                      numcep=numcep,
                                      numcontext=numcontext,
                                      start_idx=config['start_idx'],
                                      limit=config['limit'],
                                      sort=config['sort']
                                      )
    datasets = {name: _read_data_set(dataset_config[name])
                      if name in sets else None
                for name in ('train', 'dev', 'test')}
                #sets传入的值为['train', 'dev', 'test']
    return DataSets(**datasets)

由上面代码可知,其主要调用了_get_data_set_dict函数和DataSet类中的from_directory函数。
_get_data_set_dict函数代码如下图所示:

def _get_data_set_dict(conf_path, sets):
    parser = ConfigParser(os.environ)
    parser.read(conf_path)
    config_header = 'data'
    data_dir = get_data_dir(parser.get(config_header, 'data_dir'))
    data_dict = {}

    if 'train' in sets:
        d = {}
        d['dir_pattern'] = parser.get(config_header, 'dir_pattern_train')
        d['limit'] = parser.getint(config_header, 'n_train_limit')
        d['sort'] = parser.get(config_header, 'sort_train')
        d['batch_size'] = parser.getint(config_header, 'batch_size_train')
        d['start_idx'] = parser.getint(config_header, 'start_idx_init_train')
        data_dict['train'] = d
        logging.debug('Training configuration: %s', str(d))

    if 'dev' in sets:
        d = {}
        d['dir_pattern'] = parser.get(config_header, 'dir_pattern_dev')
        d['limit'] = parser.getint(config_header, 'n_dev_limit')
        d['sort'] = parser.get(config_header, 'sort_dev')
        d['batch_size'] = parser.getint(config_header, 'batch_size_dev')
        d['start_idx'] = parser.getint(config_header, 'start_idx_init_dev')
        data_dict['dev'] = d
        logging.debug('Dev configuration: %s', str(d))

    if 'test' in sets:
        d = {}
        d['dir_pattern'] = parser.get(config_header, 'dir_pattern_test')
        d['limit'] = parser.getint(config_header, 'n_test_limit')
        d['sort'] = parser.get(config_header, 'sort_test')
        d['batch_size'] = parser.getint(config_header, 'batch_size_test')
        d['start_idx'] = parser.getint(config_header, 'start_idx_init_test')
        data_dict['test'] = d
        logging.debug('Test configuration: %s', str(d))

    return data_dir, data_dict

这段代码读入了neural_network.ini配置文件中[data]相关信息,返回了data_dict对象是一个包含有数据处理相关配置信息的dict类型对象,具体如下图所示:
这里写图片描述
得到配置信息之后,_read_data_set函数通过调用DataSet.from_directory函数获得最终的self.data_sets对象。现在我们来分析from_directory函数。

3.from_directory函数

DataSet类中起主要作用的函数为from_directory函数和next_batch函数。from_directory函数用来从data目录下构建之后需要用到的DataSet对象,具体代码如下:

    def from_directory(cls, dirpath, thread_count, batch_size, numcep, numcontext, start_idx=0, limit=0, sort=None):
        if not os.path.exists(dirpath):
            raise IOError("'%s' does not exist" % dirpath)
        txt_files = txt_filenames(dirpath, start_idx=start_idx, limit=limit, sort=sort)
        if len(txt_files) == 0:
            raise RuntimeError('start_idx=%d and limit=%d arguments result in zero files' % (start_idx, limit))
        return cls(txt_files, thread_count, batch_size, numcep, numcontext)

可以看出这段代码主要是调用txt_filenames返回数据文件的命名列表,具体代码如下:

def txt_filenames(dataset_path, start_idx=0, limit=None, sort='alpha'):
        # Obtain list of txt files
        txt_files = glob(os.path.join(dataset_path, "*.txt"))
        limit = limit or len(txt_files)

        # Optional: sort files to improve padding performance
        if sort not in SORTS:
            raise ValueError('sort must be one of [%s]', SORTS)
        reverse = False
        key = None
        if 'filesize' in sort:
            key = os.path.getsize
        if sort == 'filesize_high_low':
            reverse = True
        elif sort == 'random':
            key = lambda *args: random()
        txt_files = sorted(txt_files, key=key, reverse=reverse)

        return txt_files[start_idx:limit + start_idx]

由上面可以看出,这段代码调用定义好的Sort方法,返回长度为limit的txt_files列表(文件路径)。

4.next_batch函数

通过上面的分析我们可以知道,next_batch函数被Tf_train_ctc类中的run_batches函数调用,返回具体需要训练的数据batch。由于具体的训练过程还没有分析,我们现在只是局部介绍next_batch函数,看看该函数返回了怎样的数据,具体代码如下图所示:

    def next_batch(self, batch_size=None):
        if batch_size is None:
            batch_size = self._batch_size

        end_idx = min(len(self._txt_files), self._start_idx + batch_size)
        idx_list = range(self._start_idx, end_idx)
        txt_files = [self._txt_files[i] for i in idx_list]
        wav_files = [x.replace('.txt', '.wav') for x in txt_files]
        (source, _, target, _) = get_audio_and_transcript(txt_files,
                                                          wav_files,
                                                          self._numcep,
                                                          self._numcontext)

        self._start_idx += batch_size
        # Verify that the start_idx is not larger than total available sample size
        if self._start_idx >= self.size:
            self._start_idx = 0

        # Pad input to max_time_step of this batch
        source, source_lengths = pad_sequences(source)
        sparse_labels = sparse_tuple_from(target)
        return source, source_lengths, sparse_labels

由代码中可以看出每次取出batch_size个训练数据,同时要求每个训练数据的文本txt的名字与波形文件wav的名字保持相同。该函数主要调用get_audio_and_transcript函数、pad_sequences函数和sparse_tuple_from函数,这三个函数分别属于load_audio_to_mem.py和text.py中,负责具体获得相关的输入输出向量,留待下回分解。

2019-05-16 10:19:56 qq_35290785 阅读数 114
  • C++语音识别开篇

    本篇mark老师将教大家使用第三方库的调用来简单的实现语音识别。随着机器学习和人工智能的热闹,国内语音行业也可谓是百花齐放。 语音识别一个伟大的时代已在我们身边悄悄走来。

    5927 人正在学习 去看看 杨波

目录

系统方案总述

a. 步骤

b. 系统的设计

c . QA模型流程图

d. 体系结构和各个部分细分,

一. 语音识别:

语言识别模型

各个步骤详述

总结:

二.问答系统:

分类体系 

三种形式:

技术架构图

模型逻辑图如下:

三. 语音合成:

基本原理

    1.波形合成法

    2.参数合成法

    3.规则合成法

总的流程


 

系统方案总述

a. 步骤

主系统大概分为三个大的部分,各个部分描述如下,

  1. 第一步,语音识别步骤,首先录入用户语言并进行识别,
  2. 第二步 ,问答系统步骤,对识别出来的文本句子放入的问答系统中,提取答案,另外涉及到数据库的构建QA对的生成,模糊匹配问题,以及实时检索问题。
  3. 第三步,语音合成步骤,将生成的答案,由文本转为语言形式展示给用户

总系统如图所示:

 

 

b. 系统的设计

通常分为任务型闲聊型解决型三种,三者的设计分别针对不同的应用场景。

  1. A):任务型机器人主要用于完成用户的某些特定任务,比如:查天气咨询,查时间。
  2. B):闲聊机器人主要用于深入的和用户进行无目的交流;
  3. C):解决型机器人(客服机器人)用于解决用户的问题,比如:查询书籍作者,剩余可借图书个数

c . QA模型流程图

问题解析:主要包括分词、词性标注、句法分析、命名实体识别、问题分类、问题扩展等。

分词:中英文分词存在很大的区别,英文单词之间是以空格作为自然分界符的,而中文是以字为基本的书写单位,词语之间没有明显的区分标记。分词中最常见的是基于规则的词典匹配的方法,当出现歧义分词时,也有最大切分(向前、向后、前后结合)、最少切分、全切分等策略,但都存在一定不足。在受限领域的分词,都需要构造自身的领域词典, 来提高分词的准确率。
词性标注:stanford-postagger中文词性标注较好
语法分析:句法分析是指对自然语言的语法结构进行形式化定义,可以划分为短语结构语法与依存语法。
命名实体识别:命名实体识别的任务就是识别出待处理文本中三大类(实体类、时间类和数字类)、七小类(人名、机构名、地名、时间、日期、货币和百分比)命名实体。
问题分类:借助语义词典如WordNet(22) 、HowNet(23)等对问题上下位词、同义词进行扩充。问题分类主要有基于模式规则匹配的方法和基于统计学习的方法,其中机器学习的方法占据主导。基于模式匹配优点是无需语料库,也没有人工标注的错误率和工作量,同时还可以保证不错的效果。但由于中文表述形式的灵活性,许多问句甚至不含疑问词,所以规则方法适用性不强。
在中文领域:
1. 一种特征提取的新方法,此方法依赖于句法分析结果,通过把主干词和疑问词及其附属成分作为BN 的特征输入。
2. 用HowNet 语义词典,把疑问句、句法结构、疑问意向词在知网中的首义原作为特征输入, 采用EM 分类器。
3. 提取问题疑问词、关键核心词的主要义原、核心关键词的首义原、问句主谓宾的主要义原、命名实体、名词单复数等六种特征,采用SVM 分类器对事实疑问句进行不同特征组合的分类对比。
问题扩展:目前有两种主流的方式,一是通过搜索引擎等外部文本扩展,或者借助知识库如WordNet或Wikipedia等, 挖掘词之间的内在联系。
问题在经过上面的处理过后,如何表示?
问题解析过程在基于知识库的问答系统中尤为重要,其主流方法有两类,一类是基于符号的表示方法,另一类基于深度学习的分布式表示方法。
基于符号的表示方法把问题问句表示为形式化的查询形式, 如逻辑表达式、Lambda、 Calculus、DCS-TREE或Fun-QL等形式,之后再转化为对应的查询语言如SQL、SPARQL、Prolog、FunQL等。
信息检索:信息检索则以问题解析模块的结果作为输入,从底层知识库中返回一系列相关的排序文档。检索常用的模型有布尔模型、向量空间模型以及概率模型。
1. 布尔模型是一种简单检索模型,基于集合论和布尔代数。其查询由联接符AND、OR 和NOT 构成,通过对每个关键词对应的倒排索引取交集、并集或补集,返回若干相关文档给用户

2. 向量空间模型是现在的文本检索系统以及网络搜索引擎的基础, 它把文档以及用户的查询都表示成向量空间中的点,用它们之间夹角的余弦值作为相似性度量。

举例:若文档有K 个词, 表示K个词在文档j中的权重。假设文档集合大小为N,fij为词i在某篇文档j中的次数。

3. 概率检索模型通常利用关键词作为线索,通过统计得到每个关键词在相关的文档集中出现和不出现的概率以及其在与该查询不相关的文档集中出现和不出现的概率,最终根据这些概率值,计算问句和文档的相似度。

原理:R:相关文档集NR:不相关文档集q:用户查询dj:文档j
PRP(probability ranking principle):概率排序原理,利用概率模型来估计每篇文档和需求相关概率,然后对结果进行排序。
贝叶斯最优决策原理,基于最小损失风险作出决策,返回相关的可能性大于不相关的可能性的文档;
条件概率的公式:P(AB)=P(A)P(B|A)=P(B)P(A|B)
由条件概率公式推导出贝叶斯公式:P(B|A)=P(A|B)P(B)/P(A)

 

d. 体系结构和各个部分细分,

è¿éåå¾çæè¿°

细致划分上技术上可以分为四大块,如上图所示:智能语音(语音识别+语音合成)、自然语言处理(语义理解+语言生成)、对话状态管理、问答语料库。

 

 

 

一. 语音识别:

语音是一个连续的音频流,它是由大部分的稳定态和部分动态改变的状态混合构成。
一个单词的发声(波形)实际上取决于很多因素,而不仅仅是音素,例如音素上下文、说话者、语音风格等

语言识别模型

 

如上图所示,语音识别的大致过程可以分为以下几步: 

  1. 语音输入——这个过程可以通过电脑上的声卡来获取麦克风中输入的音频信号,或者直接读取电脑中已经存在的音频文件; 
  2. 音频信号特征提取——在得到音频信号之后,需要对音频信号进行预处理,然后对预处理之后的音频信号进程特征提取,MFCC是最常用的声学特征; 
  3. 声学模型处理——把语音的声学特征分类对应到音素或字词这样的单元; 
  4. 语言模型处理——用语言模型接着把字词解码成一个完整的句子,于是就得到了最终的语音识别结果。
  5. 语音输出

各个步骤详述

1语言输入
语音信号的输入方法可以有两种: 
1.1、通过电脑上的声卡读取麦克风中输入的语音信号,实现实时语音信号采集; 
1.2、读取电脑本地中的.wav或.mp4文件获取语音信号。
2 音频信号特征提取
2.1 语音信号分帧处理
语音信号处理常常要达到的一个目标,就是弄清楚语音中各个频率成分的分布。做这件事情的数学工具是傅里叶变换,而傅里叶变换要求输入信号是平稳的。语音在宏观上来看是不平稳的——嘴巴动一动,信号的特征就变了。但是从微观上来看,在比较短的时间内,嘴巴动得是没有那么快的,语音信号就可以看成平稳的,就可以截取出来做傅里叶变换了。因此,我们需要对语音信号进行分帧处理,截取出来的一小段信号就叫一帧。 
2.2 声学特征MFCC提取
包括 梅尔频率和倒谱分析
梅尔刻度是一种基于人耳对等距的音高(pitch)变化的感官判断而定的非线性频率刻度。梅尔刻度的滤波器组在低频部分的分辨率高,跟人耳的听觉特性是相符的,这也是梅尔刻度的物理意义所在。倒谱分析
倒谱的含义是:对时域信号做傅里叶变换,然后取log,然后再进行反傅里叶变换。可以分为复倒谱、实倒谱和功率倒谱,我们用的是功率倒谱。 倒谱分析可用于将信号分解,两个信号的卷积转化为两个信号的相加。 
2.3声学模型整合
语音识别中的声学模型将声学和发音学的知识进行整合,以特征提取模块提取的特征为输入,生成声学模型得分。个人理解是,将前面得到的声学特征转化为发音中的音符。目前使用比较多的声学模型是具有记忆功能的RNN或LSTM网络。现有的声学模型一般可分为:
2.3.1、混合声学
混合高斯-隐马尔科夫模型   GMM-HMM
深度神经网络-隐马尔科夫模型   DNN-HMM
深度循环神经网络-隐马尔科夫模型   RNN-HMM
深度卷积神经网络-隐马尔科夫模型   CNN-HMM
2.3.2、端到端的声学模型
连接时序分类-长短时记忆模型 CTC-LSTM
注意力模型 Attention
3语音模型
语言模型是用来计算一个句子出现概率的概率模型。它主要用于决定哪个词序列的可能性更大,或者在出现了几个词的情况下预测下一个即将出现的词语的内容。换一个说法说,语言模型是用来约束单词搜索的。它定义了哪些词能跟在上一个已经识别的词的后面(匹配是一个顺序的处理过程),这样就可以为匹配过程排除一些不可能的单词。 
语言模型分为三个层次:字典知识,语法知识,句法知识。

总结:

 

难点分析:

1、口音和噪声
语音识别中最明显的一个缺陷就是对口音和背景噪声的处理。
2、语义错误
通常语音识别系统的实际目标并不是误字率。我们更关心的是语义错误率,就是被误解的那部分话语。
3、单通道和多人会话
一个好的会话语音识别器必须能够根据谁在说话对音频进行划分,还应该能弄清重叠的会话(声源分离)。
4、其他领域变化
如:来自声环境变化的混响、硬件造成的伪影、音频的编解码器和压缩伪影、采样率的变化、会话者的年龄不同。
5、上下文相关联判断识别
人类聊天容易基于上下文做判断,机器目前很难做到。

 

 

 

二.问答系统:

不同类型的问答系统对于数据处理的方法存在不同。例如,相对于面向FAQ的问答系统的问句检索直接得到候选答案,面向开放领域的问答系统首先需要根据问题分析的结果进行相关文档、文本片段信息的检索,然后进行候选答案的抽取。虽然不同类型的问答系统对于系统模块的功能分工和具体实现存在差异,但依据数据流在问答系统中的处理流程,一般问答系统的处理框架中都包括问句理解、信息检索、答案生成三个功能组成部分。

首先,问答系统首先接收用户提出的问题(即用户输入的问句)。 然后,根据用户输入的问句,从常用问题库(即FAQ库)中查找并建立与用户问题比较相似的候选问题集。 接着,对候选问题集中的句子与用户输入的问句进行相似度计算,从候选问题集中寻找与用户输入的问句最相似的问句。 如果在候选问题集中找到了与用户输入的问句相似的问句(即该问句和用户输入的问句的相似度大于某个阈值),就直接把和该问句对应的答案返还给用户;如果没有找到与用户输入的问句相似的问句(即候选问题集中所有问句和用户输入的问句的相似度都小于某个阀值),那么就利用其他的方法,比如答案抽取、信息检索等,来寻找答案,并且将这个新的问题和其对应的答案加入FAQ库,对FAQ库进行更新。

分类体系 

è¿éåå¾çæè¿°

基于智能对话机器人的交互特点、功能特点和关键技术,我们把智能对话机器人划分为上图中的几个类别, 

三种形式:

  1. 基于分析
  2. 基于检索、
  3. 基于生成。

基于分析是比如一个单轮对话针对一个分类问题,或者是一个结构预测的问题,那么检索就是一个匹配问题,生成就是一个翻译的问题。这三种不同的方法它背后的区别就是这个有没有显式的语义表达。

  • 基于分析的方法它是有显式的内部表达,基于检索和生成,就是部分有或者没有显性的这个内部的表示,这是这三个不同的方法的特点。
  • 基于检索的方法在聊天机器人中用的多,而问答系统中也是如此,只是将对话局限在问答中。
  • 基于生成的方法。在深度学习出来之前,这是有难度的,现在有深度学习基于生成对话的技术,其原理上是把问句,转化成内部表示,再把内部表示为答句。在生成器和分析器中用了大量的训练语料,基于神经网络来训练表示实现。

技术架构图

å¨è¿éæå¥å¾çæè¿°

  1. 提问处理模块:负责对用户的提问进行处理;生成查询关键词(提问关键词,扩展关键词,...);确定提问答案类型(PER, LOC, ORG, TIM, NUM, ...)以及提问的句法、语义表示等等。
  2. 检索模块:根据提问处理模块生成的查询关键词,使用传统检索方式,检索出和提问相关的信息;返回的信息可以是段落、也可以是句群或者句子。
  3. 答案抽取模块:从检索模块检索出的相关段落、或句群、或句子中抽取出和提问答案类型一致的实体,根据某种原则对候选答案进行打分,把概率最大的候选答案返回给用户

 

模型逻辑图如下:

由多层的不同类型的神经网络堆叠而成,如下图,各个层有自己的功能,数据
è¿éåå¾çæè¿°

 

 

 

三. 语音合成:

语音由一连串的音所组成,这些音及其相互间的过渡就是代表信息的符号。这些符号的排列由语音的规则所控制。对这些规则及其在人类通信中的含义的研究属于语言学的范畴。 语音合成的研究已有多年的历史,现在研究出的语音合成方法的分类,从技术方式讲,可分为波形合成法、参数合成法、和规则合成方法;从合成策略上讲可分为频谱逼近和波形逼近。

基本原理

如下图所示,一个典型的语音合成系统主要包括训练和后端两个部分。前端部分主要是对输入文本的分析,从输入的文本提取后端建模需要的信息。例如:分词(判断句子中的单词边界),词性标注(名词,动词,形容词等),韵律结构预测(是否韵律短语边界),多音字消岐等等。的部分读入前端文本分析结果,并且对语音部分结合文本信息进行建模。在合成过程中,后端会利用输入的文本信息和训练好的声学模型,生成出语音信号,进行输出。

    1.波形合成法

波形合成法一般有两种形式,一种是波形编码合成,它类似于语音编码中的波形编解码方法,该方法直接把要合成的语音发音波形进行存储,或者进行波形编码压缩后存储,合成重放时再解码组合输出。另一种是波形编辑合成,它把波形编辑技术用于语音合成,通过选取音库中采取自然语言的合成单元的波形,对这些波形进行编辑拼接后输出。它采用语音编码技术,存储适当的语音单元,合成时,经解码、波形编辑拼接、平滑处理等输出所需的短语、语句或段落。波形语音合成法是一种相对简单的TTS语音技术,通常只能合成有限词汇的语音段。目前许多专门用途的语音芯片语音IC,都采用这种方式,如自动报时、报站或报警等。

    2.参数合成法

参数合成法也称为分析合成法,是一种比较复杂的方法。声学模型的过程为:首先录制声音,这些声音涵盖了人发音过程中所有可能出现的读音;提取出这些声音的声学参数,并整合成一个完整的音库。在发音过程中,首先根据需要发的音,从音库中选择合适的声学参数,然后根据韵律模型中得到的韵律参数,通过语音合成算法产生TTS语音

    参数语音合成方法的优点是其音库一般较小,并且整个系统能适应的韵律特征的范围较宽,这类合成器比特率低,音质适中;缺点是参数合成技术的算法复杂,参数多,并且在压缩比较大时,信息丢失亦大,合成出的语音总是不够自然、清晰。为了改善音质,近几年发展了混合编码技术,主要是为了改善激励信号的质量,这样,虽然比特率有所增大,但音质得到了提高。

    3.规则合成法

    这是一种高级的合成方法。规则合成方法通过语音学规则产生语音。合成的词汇表不是事先确定,系统中存储的是最小的语音单位的声学参数,以及由音素组成音节、由音节组成词、由词组成句子和控制音调、轻重音等韵律的各种规则。给出待合成的文本数据后,合成系统利用规则自动地将他们转换成连续的语音声波。这种方法可以合成无限词汇的语句。这种算法中,用于波形拼接和韵律控制的、较有代表性的算法是基音同步叠加技术(PSOLA),该方法既能保持所发音的主要音段特征,又能在拼接时灵活调整其基频、时长和强度等超音段特征。其核心思想是,直接对存储于音库的语音运用PSOLA算法来进行拼接,从而整合成完整的语音。有别于传统概念上只是将不同的语音单元进行简单拼接的波形编辑合成,规则合成系统首先要在大量语音库中,选择最合适的语音单元来用于拼接,并在选音过程中往往采用多种复杂的技术,最后在拼接时,要使用如PSOLA算法等,对其合成语音的韵律特征进行修改,从而使合成的语音能达到很高的音质。

总的流程

包括训练模块和合成模块,训练模块用于学习语料库和文本库的内容,学习数据中包含的语言特征,合成模块将文字模拟成声音,利用训练模块得到的模型转换成符合逻辑的语言。

è¿éåå¾çæè¿°

 

 

口语对话管理综述

阅读数 280

没有更多推荐了,返回首页