Files

318 lines
9.7 KiB
Python
Executable File

#!/usr/bin/env python
"""
Adapt acoustic models using constrained maximum-likelihood linear regression.
This module implements single-class mean and variance adaptation using
CMLLR as described in M.J.F. Gales,
"Maximum likehood Linear Transformations for HMM-Based Speech recognition",
update only CD senones (one class)
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>, Stephan Vanni <svanni@vecsys.fr>"
__version__ = "$Revision $"
import numpy as np
import sys
from cmusphinx import s3gaucnt, s3gau, s3mdef
import getopt
import math
def estimate_cmllr(stats, inmean, invar, mdef):
# Ws list
Ws = []
# Mllr iteration
niter = 0
# for i in range(0, inmean.n_feat): => SV : always 0 for one stream
i = 0
ndim = inmean.veclen[i]
# Init A and bias
A = np.eye(ndim)
bias = np.zeros(ndim)
# Collection of G matrices
G = np.zeros((ndim, ndim + 1, ndim + 1))
# K matrix (for the single class and stream)
K = np.zeros((ndim, ndim + 1))
# W vector
W = np.zeros(ndim + 1)
# Cofactors vector
cofact = np.zeros(ndim + 1)
# Beta
B = 0
print('Get statistics & sum it')
# CD only : just sum over all CD densities
for j in range(mdef.n_ci_sen, inmean.n_mgau):
# print 'state = %i' % j
for k in range(0, inmean.density):
# Mean ( vector, dim : ndim )
mean = inmean[j][i][k]
# Inverse variance (diagonal)
# ( vector, dim : ndim )
invvar = invar[j][i][k]
# invar[j][i][k] is a vector (diagonal) and not a NxN matrix
# if not full cov
if len(invvar.shape) > 1:
invvar = np.diag(invvar)
invvar = 1. / invvar.clip(1e-5, np.inf)
# Sum of variance statistics ( i.e sum(L_m_r o o^T) )
# ( vector, dim : ndim )
obsvar = stats.var[j][i][k]
# Sum of posteriors (i.e. sum_t L_m_r(t) )
dnom = stats.dnom[j][i][k]
# Extended sum of mean statistics (i.e. sum(L_m_r o) )
# ( vector, dim : ndim+1 )
obsmean = stats.mean[j][i][k]
xobsmean = np.concatenate((obsmean, (dnom, )))
# G{l} = \sum_r \Sigma_{l}^{-1} * \sum_t L_m_r oe oe^T (outer)
# = ... * [\sum_t L_m_r (\sum_t L_m_r o)^T]
# [\sum_t L_m_r o \sum_t L_m_r o o^T]
SumT = obsvar
SumT = np.concatenate((SumT, (obsmean.T, )), axis=0)
SumT = np.c_[SumT, xobsmean]
for ll in range(0, ndim):
G[ll] += invvar[ll] * SumT
# K{l} = \sum_r \Sigma_{l}^{-1} * \Mean{l} * L_m_r oe
# = ... * [\sum_t L_m_r (\sum_t L_m_r o)^T]
K += np.outer(invvar * mean, xobsmean)
# Sum for all gausians
B += dnom
# End of collecting stats
Ginv = np.zeros((ndim + 1, ndim + 1))
while (niter < 10):
niter += 1
for i in range(0, ndim):
Ginv = np.linalg.inv(G[i])
# Ginv = np.linalg.pinv(G[i],rcond=1.0e-6)
# Init W for convergence of likehood
iniW = np.concatenate((A[i, :], (bias[i], )))
# Get extended cofactors
cofact = get_cofact(A, i)
# Get alpha
alpha = get_alpha(Ginv, K[i], B, cofact)
print("alpha : %f" % alpha)
W = np.zeros(ndim + 1)
tvec = alpha * cofact + K[i]
W = np.dot(Ginv, tvec)
like_new = get_row_like(G[i], K[i], cofact, B, W)
like_old = get_row_like(G[i], K[i], cofact, B, iniW)
if (like_new > like_old):
A[i, :] = W[0:ndim]
bias[i] = W[ndim]
else:
print('NOT updating row %i, iter %i,( %f > %f )' %
(i, niter, like_old, like_new))
# to preserve compatibility with write_mllr
Wi = np.c_[bias, A]
Ws.append(Wi)
return Ws
def get_row_like(G, K, cofact, B, W):
row_like = 0
tvec = np.dot(W, G)
row_like = np.dot(tvec - 2 * K, W)
det = np.dot(cofact, W)
row_like = (math.log(math.fabs(det)) * B) - (row_like / 2)
return row_like
def get_alpha(Ginv, K, B, cofact):
alpha = 0
tvec = np.zeros(K.size)
# p{i}.G{i}^-1
# tvec = np.dot(cofact,Ginv)
tvec = np.dot(Ginv, cofact)
# a = p{i}.G{i}^-1.p{i}
a = np.dot(tvec, cofact)
# b = p{i}.G{i}^-1.k{i}
b = np.dot(tvec, K)
# c = -beta
c = -B
# discriminant
d = b * b - 4 * a * c
if (d < 0):
# solutions must be real
print('Warning : determinant < 0')
d = 0
d = math.sqrt(d)
alpha1 = (-b + d) / (2 * a)
alpha2 = (-b - d) / (2 * a)
like1 = get_alpha_like(a, b, c, alpha1)
like2 = get_alpha_like(a, b, c, alpha2)
if (like1 > like2):
alpha = alpha1
else:
alpha = alpha2
return alpha
def get_alpha_like(a, b, c, alpha):
return (-c * math.log(math.fabs(alpha * a + b)) - (alpha * alpha * a) / 2)
def get_cofact(A, i):
# Cofactors are compute like this :
# Cofact(A) = det(A) * A^{-1}
#
# For determinant computing , slogdet is more suitable for large
# matrix but not available with numpy < 2.0
# (sign, logdet) = np.linalg.slogdet(A)
# det = sign * np.exp(logdet)
det = np.linalg.det(A)
# Invert of matrix A
# ainv = np.linalg.pinv(A,rcond=1.0e-6)
ainv = np.linalg.inv(A)
# Only for i row
# ainv = ainv[i,:]
# Only for i columns
ainv = ainv[:, i]
cofact = det * ainv
# cofact = ainv
# Extend cofactors
cofact = np.concatenate((cofact, (0, )))
return cofact
def write_mllr(fh, Ws):
"""
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 fh: Open text-mode filehandle
@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")
def solve_transform(Ws):
"""
Solve the CMLLR tranformations from A' and b' as follow :
A'^-1 = A
A'^-1b' = b
( p.6 , formulae (24),
Gales97-mllr.pdf,
"Maximum likehood Linear Transformations for HMM-Based Speech recognition", M.J.F Gales
)
@param Ws: Derived transformations
@ptype Ws: list(numpy.ndarray)
@return: CMLLR tranformations
@type : list(numpy.ndarray)
"""
Wp = []
# one single stream , Ws = W
for i, W in enumerate(Ws):
# Get rotation and bias terms separately
b = W[:, 0]
A = W[:, 1:]
Ap = np.linalg.inv(A)
# Ap = np.linalg.pinv(A,rcond=1.0e-6)
bp = np.linalg.solve(A, b)
# to preserve compatibility with write_mllr
Wi = np.c_[bp, Ap]
Wp.append(Wi)
return Wp
def solve_mllr(Wp, inmean, invar, mdef):
"""
Solve he CMLLR tranformations for means
new_mean = A'.old_mean - b' ( !! be aware of the minus !! )
new_var = A'.old_var.A'^T
@param Wp: CMLLR transformations
@ptype Wp: list(numpy.ndarray)
@param inmean: Input mean parameters
@type inmean: cmusphinx.s3gau.S3Gau
@param invar: Input variance parameters
@type invar: cmusphinx.s3gau.S3Gau
@return: Tranformed mean and variance parameters
@type : list(numpy.ndarray)
"""
P = []
# one single stream , Wp = W
for i, W in enumerate(Wp):
# Get rotation and bias terms separately
bp = W[:, 0]
Ap = W[:, 1:]
# one stream
i = 0
outmean = np.zeros((inmean.n_mgau, 1, inmean.density, bp.size))
outvar = np.zeros((inmean.n_mgau, 1, inmean.density, bp.size))
for j in range(0, inmean.n_mgau):
# CD only
if (j < mdef.n_ci_sen):
outmean[j][i] = inmean[j][i]
outvar[j][i] = invar[j][i]
continue
for k in range(0, inmean.density):
# Mean ( vector, dim : ndim )
mean = inmean[j][i][k]
outmean[j][i][k] = np.dot(Ap, mean) - bp
# Variance ( vector, dim : ndim )
var = np.diag(invar[j][i][k])
tmp = np.dot(Ap, var)
tmp2 = np.dot(tmp, Ap.T)
outvar[j][i][k] = np.diag(tmp2)
P.append(outmean)
P.append(outvar)
return P
if __name__ == '__main__':
def usage():
sys.stderr.write("Usage: %s INMEAN INVAR MDEF 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()
outmean = 'means.cmllr'
outvar = 'variances.cmllr'
inmean = s3gau.open(args[0])
invar = s3gau.open(args[1])
mdef = s3mdef.open(args[2])
accumdirs = args[3:]
stats = s3gaucnt.accumdirs_full(accumdirs)
Ws = estimate_cmllr(stats, inmean, invar, mdef)
with open("cmllr_matrix", "w") as fh:
write_mllr(fh, Ws)
Wp = solve_transform(Ws)
param = solve_mllr(Wp, inmean, invar, mdef)
with s3gau.open(outmean, "wb") as om:
om.writeall(param[0])
with s3gau.open(outvar, "wb") as om:
om.writeall(param[1])