mirror of
https://github.com/cmusphinx/sphinxtrain.git
synced 2026-06-16 13:14:30 +00:00
318 lines
9.7 KiB
Python
Executable File
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])
|