mirror of
https://github.com/cmusphinx/sphinxtrain.git
synced 2026-06-16 13:14:30 +00:00
230 lines
8.0 KiB
Python
Executable File
230 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Adapt acoustic models using maximum-likelihood linear regression.
|
|
|
|
This module implements single-class mean and variance adaptation using
|
|
MLLR as described in M.J.F. Gales & P.C. Woodland, \"Mean and Variance
|
|
Adaptation within the MLLR Framework\", Computer Speech and Language,
|
|
vol. 10, pp 249-264.
|
|
|
|
TODO: Multiple regression classes.
|
|
"""
|
|
|
|
# Copyright (c) 2006 Carnegie Mellon University
|
|
#
|
|
# You may copy and modify this freely under the same terms as
|
|
# Sphinx-III
|
|
|
|
__author__ = "David Huggins-Daines <dhdaines@gmail.com>"
|
|
__version__ = "$Revision $"
|
|
|
|
import numpy as np
|
|
|
|
import sys
|
|
from cmusphinx import s3gaucnt, s3gau
|
|
import getopt
|
|
|
|
|
|
def extend(mean):
|
|
"""
|
|
Produce an "extended mean vector".
|
|
"""
|
|
return np.concatenate(((1, ), mean))
|
|
|
|
|
|
def estimate_mllr_mean(stats, inmean, invar):
|
|
"""
|
|
Estimate an MLLR transformation of the means based on observed
|
|
statistics.
|
|
|
|
This function calculates an MLLR transformation W (an n by n+1
|
|
matrix) for each feature stream which, when applied to C{inmean},
|
|
maximizes the likelihood of the data as represented by C{stats}.
|
|
|
|
Currently this does only one class, but it will promptly be
|
|
extended once the \"learning exercise\" is over.
|
|
|
|
@param stats: Observation counts, as returned
|
|
by C{cmusphinx.s3gaucnt.accumdirs}
|
|
or C{cmusphinx.s3gaucnt.accumdirs_full}.
|
|
@type stats: cmusphinx.s3gaucnt.S3GauCnt
|
|
@param inmean: Input mean parameters
|
|
@type inmean: cmusphinx.s3gau.S3Gau
|
|
@param invar: Input diagonal covariance parameters
|
|
@type inmvar: cmusphinx.s3gau.S3Gau
|
|
@return: MLLR transformations, one per feature stream
|
|
@rtype: list(numpy.ndarray)
|
|
"""
|
|
# List of W matrices
|
|
Ws = []
|
|
for i in range(0, inmean.n_feat):
|
|
ndim = inmean.veclen[i]
|
|
# Collection of G matrices
|
|
G = np.zeros((ndim, ndim + 1, ndim + 1))
|
|
# Z matrix (for the single class and stream)
|
|
Z = np.zeros((ndim, ndim + 1))
|
|
# W matrix
|
|
W = np.zeros((ndim, ndim + 1))
|
|
# One-class MLLR: just sum over all densities
|
|
for j in range(0, inmean.n_mgau):
|
|
for k in range(0, inmean.density):
|
|
# Extended mean vector
|
|
xmean = extend(inmean[j][i][k])
|
|
# Inverse variance (also use only the diagonal)
|
|
invvar = invar[j][i][k]
|
|
if len(invvar.shape) > 1:
|
|
invvar = np.diag(invvar)
|
|
invvar = 1. / invvar.clip(1e-5, np.inf)
|
|
# Sum of posteriors (i.e. sum_t L_m_r(t))
|
|
dnom = stats.dnom[j, i, k]
|
|
# Sum of mean statistics
|
|
obsmean = stats.mean[j][i][k]
|
|
for ll in range(0, ndim):
|
|
# v_{ll} = sum_t L(t) \Sigma_{ll}^{-1}
|
|
# D = \ksi \ksi^T
|
|
# G^{l} = v_{ll} D
|
|
G[ll] += dnom * invvar[ll] * np.outer(xmean, xmean)
|
|
# Z = \sum_r\sum_t L(t) \Sigma_r^{-1} o(t) \ksi_r^T
|
|
Z += np.outer(invvar * obsmean, xmean)
|
|
# Now solve for the rows of W
|
|
for j in range(0, ndim):
|
|
W[j] = np.linalg.solve(G[j], Z[j])
|
|
Ws.append(W)
|
|
return Ws
|
|
|
|
|
|
def write_mllr(fh, Ws, Hs=None):
|
|
"""
|
|
Write out MLLR transformations of the means in the format that
|
|
Sphinx3 understands.
|
|
|
|
@param Ws: MLLR transformations of means, one per feature stream
|
|
@ptype Ws: list(numpy.ndarray)
|
|
@param Hs: MLLR transformations of variances, one per feature stream
|
|
@ptype Hs: list(numpy.ndarray)
|
|
@param fh: Text filehandle to write output to
|
|
@ptype fh: file-like object
|
|
"""
|
|
# One-class MLLR for now
|
|
fh.write("%d\n" % 1)
|
|
fh.write("%d\n" % len(Ws))
|
|
for i, W in enumerate(Ws):
|
|
fh.write("%d\n" % W.shape[0])
|
|
# Write rotation and bias terms separately
|
|
for w in W:
|
|
for x in w[1:]:
|
|
fh.write("%f " % x)
|
|
fh.write("\n")
|
|
for x in W[:, 0]:
|
|
fh.write("%f " % x)
|
|
fh.write("\n")
|
|
if Hs is not None:
|
|
for x in Hs[i]:
|
|
fh.write("%f " % x)
|
|
fh.write("\n")
|
|
else:
|
|
fh.write("1.0 " * W.shape[0])
|
|
fh.write("\n")
|
|
|
|
|
|
def estimate_mllr_variance(stats, inmean, invar, Ws):
|
|
"""
|
|
Estimate a diagonal MLLR transformation of the variances based on
|
|
observed statistics.
|
|
|
|
This function calculates an MLLR transformation H (a diagonal nxn
|
|
matrix, represented as a vector) which maximizes the likelihood of
|
|
the data as represented by C{stats}, when applied to the inverse
|
|
Cholesky factor of the covariance matrix B as B^T H B. For
|
|
diagonal covariances this reduces to a scaling of the variance by
|
|
the diagonal of H, since the diagonal b = (sqrt(var^{-1}))^{-1} =
|
|
var^{0.5} and thus B^T H B = \\Sigma H when \\Sigma and H are
|
|
diagonal.
|
|
|
|
Note that this function will raise an exception if -2passvar yes
|
|
was enabled when collecting the observation counts, since it
|
|
requires them to consist of the sum of the outer products of the
|
|
observation vectors scaled by their posterior probabilities,
|
|
(L_m_r(t)o(t)o(t)^T in Cambridge papers).
|
|
|
|
Currently this does only one class and one stream, but it will
|
|
promptly be extended once the \"learning exercise\" is over.
|
|
|
|
@param stats: Observation counts, as returned
|
|
by C{cmusphinx.s3gaucnt.accumdirs}
|
|
or C{cmusphinx.s3gaucnt.accumdirs_full}.
|
|
@type stats: cmusphinx.s3gaucnt.S3GauCnt
|
|
@param inmean: Input mean parameters
|
|
@type inmean: cmusphinx.s3gau.S3Gau
|
|
@param invar: Input covariance parameters
|
|
@type inmvar: cmusphinx.s3gau.S3Gau
|
|
@param Ws: Previously computed MLLR transformations of means
|
|
@ptype Ws: list(numpy.ndarray)
|
|
@return: MLLR transformations of variances
|
|
@rtype: list(numpy.ndarray)
|
|
"""
|
|
if stats.pass2var:
|
|
raise RuntimeError(
|
|
"Statistics using -2passvar yes are not allowed")
|
|
Hs = []
|
|
for i, W in enumerate(Ws):
|
|
ndim = inmean.veclen[i]
|
|
# Output "matrix" H
|
|
H = np.zeros(ndim)
|
|
# One-class MLLR: just sum over all densities
|
|
norm = 0
|
|
for j in range(0, inmean.n_mgau):
|
|
for k in range(0, inmean.density):
|
|
# Extended mean vector
|
|
xmean = extend(inmean[j][i][k])
|
|
# Transform it
|
|
mean = np.dot(W, xmean)
|
|
# Cholesky factorization not needed for diagonals...
|
|
invvar = 1. / invar[j][i][k].clip(1e-5, np.inf)
|
|
if len(invvar.shape) > 1:
|
|
invvar = np.diag(invvar)
|
|
# Note: the code actually just computes diagonals
|
|
# sum(L_m_r o o^T) (obs squared)
|
|
nom = stats.var[j][i][k]
|
|
# \hat mu_m_r \bar o_m_r^T (cross term 1)
|
|
nom -= mean * stats.mean[j][i][k]
|
|
# \bar o_m_r \hat mu_m_r^T (cross term 2)
|
|
nom -= stats.mean[j][i][k] * mean
|
|
# \mu_m_r \mu_m_r^T sum(L_m_r) (mean squared)
|
|
nom += mean * mean * stats.dnom[j][i][k]
|
|
# Multiply in variances and accumulate
|
|
H += invvar * nom
|
|
# Accumulate normalizer
|
|
norm += stats.dnom[j][i][k]
|
|
Hs.append(H / norm)
|
|
return Hs
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
def usage():
|
|
sys.stderr.write("Usage: %s INMEAN INVAR ACCUMDIRS...\n" % sys.argv[0])
|
|
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], "h", ["help"])
|
|
except getopt.GetoptError:
|
|
usage()
|
|
sys.exit(2)
|
|
if len(args) < 3:
|
|
usage()
|
|
sys.exit(2)
|
|
ldafn = None
|
|
for o, a in opts:
|
|
if o in ('-h', '--help'):
|
|
usage()
|
|
sys.exit()
|
|
|
|
inmean = s3gau.open(args[0])
|
|
invar = s3gau.open(args[1])
|
|
accumdirs = args[2:]
|
|
stats = s3gaucnt.accumdirs(accumdirs)
|
|
|
|
Ws = estimate_mllr_mean(stats, inmean, invar)
|
|
Hs = estimate_mllr_variance(stats, inmean, invar, Ws)
|
|
write_mllr(sys.stdout, Ws, Hs)
|