Source code for kaldi.matrix._matrix

import sys
import numpy

from . import _compressed_matrix
from . import _kaldi_matrix
from . import _kaldi_matrix_ext
from . import _kaldi_vector
from . import _kaldi_vector_ext
from . import _matrix_ext
import _matrix_common  # FIXME: Relative/absolute import is buggy in Python 3.
from . import _packed_matrix
from . import _sp_matrix
from . import _tp_matrix
from . import _str

################################################################################
# single precision vector/matrix types
################################################################################


class _VectorBase(object):
    """Base class defining the additional API for single precision vectors.

    No constructor.
    """

    def copy_(self, src):
        """Copies the elements from another vector.

        Args:
            src (Vector or DoubleVector): The input vector.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != src.dim:
            raise ValueError("Vector of size {} cannot be copied into vector "
                             "of size {}.".format(src.dim, self.dim))
        if isinstance(src, _kaldi_vector.VectorBase):
            return self._copy_from_vec_(src)
        elif isinstance(src, _kaldi_vector.DoubleVectorBase):
            _kaldi_vector_ext._copy_from_double_vec(self, src)
            return self
        else:
            raise TypeError("input vector type is not supported.")

    def clone(self):
        """Clones the vector.

        The clone allocates new memory for its contents and supports vector
        operations that reallocate memory, i.e. it is not a view.

        Returns:
            Vector: A copy of the vector.
        """
        return Vector(self)

    def size(self):
        """Returns the size of the vector as a single element tuple."""
        return (self.dim,)

    @property
    def shape(self):
        """Single element tuple representing the size of the vector."""
        return self.size()

    def approx_equal(self, other, tol=0.01):
        """Checks if vectors are approximately equal.

        Args:
            other (Vector): The vector to compare against.
            tol (float): The tolerance for the equality check.
                Defaults to ``0.01``.

        Returns:
            True if `self.dim == other.dim` and
            `||self-other|| <= tol*||self||`. False otherwise.
        """
        if not isinstance(other, _kaldi_vector.VectorBase):
            return False
        if self.dim != other.dim:
            return False
        return self._approx_equal(other, tol)

    def __eq__(self, other):
        return self.approx_equal(other, 1e-16)

    def numpy(self):
        """Converts the vector to a 1-D NumPy array.

        The NumPy array is a view into the vector, i.e. no data is copied.

        Returns:
            numpy.ndarray: A NumPy array sharing data with this vector.
        """
        return _matrix_ext.vector_to_numpy(self)

    @property
    def data(self):
        """Vector data as a memoryview."""
        return self.numpy().data

    def range(self, start, length):
        """Returns the given range of elements as a new vector view.

        Args:
            start (int): The start index.
            length (int): The length.

        Returns:
            SubVector: A vector view representing the given range.
        """
        return SubVector(self, start, length)

    def add_vec_(self, alpha, v):
        """Adds another vector.

        Performs the operation :math:`y = y + \\alpha\\ v`.

        Args:
            alpha (float): The scalar multiplier.
            v (Vector or DoubleVector): The input vector.

        Raises:
          RuntimeError: In case of size mismatch.
        """
        if isinstance(v, _kaldi_vector.VectorBase):
            return self._add_vec_(alpha, v)
        elif isinstance(v, _kaldi_vector.DoubleVectorBase):
            _kaldi_vector_ext._add_double_vec(self, alpha, v)
            return self
        else:
            raise TypeError("input vector type is not supported.")

    def add_vec2_(self, alpha, v):
        """Adds the squares of elements from another vector.

        Performs the operation :math:`y = y + \\alpha\\ v\\odot v`.

        Args:
            alpha (float): The scalar multiplier.
            v (Vector or DoubleVector): The input vector.

        Raises:
          RuntimeError: In case of size mismatch.
        """
        if isinstance(v, _kaldi_vector.VectorBase):
            return self._add_vec2_(alpha, v)
        elif isinstance(v, _kaldi_vector.DoubleVectorBase):
            _kaldi_vector_ext._add_double_vec2(self, alpha, v)
            return self
        else:
            raise TypeError("input vector type is not supported.")

    def add_mat_vec_(self, alpha, M, trans, v, beta, sparse=False):
        """Computes a matrix-vector product.

        Performs the operation :math:`y = \\alpha\\ M\\ v + \\beta\\ y`.

        Args:
            alpha (float): The scalar multiplier for the matrix-vector product.
            M (Matrix or SpMatrix or TpMatrix): The input matrix.
            trans (MatrixTransposeType): Whether to use **M** or its transpose.
            v (Vector): The input vector.
            beta (int): The scalar multiplier for the destination vector.
            sparse (bool): Whether to use the algorithm that is faster when
                **v** is sparse. Defaults to ``False``.

        Raises:
            ValueError: In case of size mismatch.
        """
        if v.dim != M.num_cols:
            raise ValueError("Matrix of size {}x{} cannot be multiplied with "
                             "vector of size {}."
                             .format(M.num_rows, M.num_cols, v.dim))
        if self.dim != M.num_rows:
            raise ValueError("Vector of size {} cannot be added to vector of "
                             "size {}.".format(M.num_rows, self.dim))
        if isinstance(M, _kaldi_matrix.MatrixBase):
            if sparse:
                _kaldi_vector_ext._add_mat_svec(self, alpha, M, trans, v, beta)
            else:
                _kaldi_vector_ext._add_mat_vec(self, alpha, M, trans, v, beta)
        elif isinstance(M, _sp_matrix.SpMatrix):
            _kaldi_vector_ext._add_sp_vec(self, alpha, M, v, beta)
        elif isinstance(M, _tp_matrix.TpMatrix):
            _kaldi_vector_ext._add_tp_vec(self, alpha, M, trans, v, beta)
        return self

    def mul_tp_(self, M, trans):
        """Multiplies the vector with a lower-triangular matrix.

        Performs the operation :math:`y = M\\ y`.

        Args:
            M (TpMatrix): The input lower-triangular matrix.
            trans (MatrixTransposeType): Whether to use **M** or its transpose.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != M.num_rows:
            raise ValueError("Matrix with size {}x{} cannot be multiplied "
                             "with vector of size {}."
                             .format(M.num_rows, M.num_cols, self.dim))
        _kaldi_vector_ext._mul_tp(self, M, trans)
        return self

    def solve_(self, M, trans):
        """Solves a linear system.

        The linear system is defined as :math:`M\\ x = b`, where :math:`b` and
        :math:`x` are the initial and final values of the vector, respectively.

        Warning:
            Does not test for :math:`M` being singular or near-singular.

        Args:
            M (TpMatrix): The input lower-triangular matrix.
            trans (MatrixTransposeType): Whether to use **M** or its transpose.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != M.num_rows:
            raise ValueError("The number of rows of the input matrix ({}) "
                             "should match the size of the vector ({})."
                             .format(M.num_rows, self.dim))
        _kaldi_vector_ext._solve(self, M, trans)
        return self

    def copy_rows_from_mat_(self, M):
        """Copies the elements from a matrix row-by-row.

        Args:
            M (Matrix or DoubleMatrix): The input matrix.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != M.num_rows * M.num_cols:
            raise ValueError("The number of elements of the input matrix ({}) "
                             "should match the size of the vector ({})."
                             .format(M.num_rows * M.num_cols, self.dim))
        if isinstance(M, _kaldi_matrix.MatrixBase):
            _kaldi_vector_ext._copy_rows_from_mat(self, M)
        if isinstance(M, _kaldi_matrix.DoubleMatrixBase):
            _kaldi_vector_ext._copy_rows_from_double_mat(self, M)
        else:
            raise TypeError("input matrix type is not supported.")
        return self

    def copy_cols_from_mat_(self, M):
        """Copies the elements from a matrix column-by-columm.

        Args:
            M (Matrix): The input matrix.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != M.num_rows * M.num_cols:
            raise ValueError("The number of elements of the input matrix ({}) "
                             "should match the size of the vector ({})."
                             .format(M.num_rows * M.num_cols, self.dim))
        _kaldi_vector_ext._copy_cols_from_mat(self, M)
        return self

    def copy_row_from_mat_(self, M, row):
        """Copies the elements from a matrix row.

        Args:
            M (Matrix or DoubleMatrix or SpMatrix or DoubleSpMatrix):
                The input matrix.
            row (int): The row index.

        Raises:
            ValueError: In case of size mismatch.
            IndexError: If the row index is out-of-bounds.
        """
        if self.dim != M.num_cols:
            raise ValueError("The number of columns of the input matrix ({})"
                             "should match the size of the vector ({})."
                             .format(M.num_cols, self.dim))
        if not isinstance(row, int) or not (0 <= row < M.num_rows):
            raise IndexError()
        if isinstance(M, _kaldi_matrix.MatrixBase):
            _kaldi_vector_ext._copy_row_from_mat(self, M, row)
        elif isinstance(M, _kaldi_matrix.DoubleMatrixBase):
            _kaldi_vector_ext._copy_row_from_double_mat(self, M, row)
        elif isinstance(M, _sp_matrix.SpMatrix):
            _kaldi_vector_ext._copy_row_from_sp(self, M, row)
        elif isinstance(M, _sp_matrix.DoubleSpMatrix):
            _kaldi_vector_ext._copy_row_from_double_sp(self, M, row)
        else:
            raise TypeError("input matrix type is not supported.")
        return self

    def copy_col_from_mat_(self, M, col):
        """Copies the elements from a matrix column.

        Args:
            M (Matrix or DoubleMatrix): The input matrix.
            col (int): The column index.

        Raises:
            ValueError: In case of size mismatch.
            IndexError: If the column index is out-of-bounds.
        """
        if self.dim != M.num_rows:
            raise ValueError("The number of rows of the input matrix ({})"
                             "should match the size of this vector ({})."
                             .format(M.num_rows, self.dim))
        if not instance(col, int) or not (0 <= col < M.num_cols):
            raise IndexError()
        if isinstance(M, _kaldi_matrix.MatrixBase):
            _kaldi_vector_ext._copy_col_from_mat(self, M, col)
        elif isinstance(M, _kaldi_matrix.DoubleMatrixBase):
            _kaldi_vector_ext._copy_col_from_double_mat(self, M, col)
        else:
            raise TypeError("input matrix type is not supported.")
        return self

    def copy_diag_from_mat_(self, M):
        """Copies the digonal elements from a matrix.

        Args:
            M (Matrix or SpMatrix or TpMatrix): The input matrix.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != min(M.num_rows, M.num_cols):
            raise ValueError("The size of the matrix diagonal ({}) should "
                             "match the size of the vector ({})."
                             .format(min(M.size()), self.dim))
        elif isinstance(M, _kaldi_matrix.MatrixBase):
            _kaldi_vector_ext._copy_diag_from_mat(self, M)
        elif isinstance(M, _sp_matrix.SpMatrix):
            _kaldi_vector_ext._copy_diag_from_sp(self, M)
        elif isinstance(M, _tp_matrix.TpMatrix):
            _kaldi_vector_ext._copy_diag_from_tp(self, M)
        else:
            raise TypeError("input matrix type is not supported.")
        return self

    def copy_from_packed_(self, M):
        """Copies the elements from a packed matrix.

        Args:
            M (SpMatrix or TpMatrix or DoubleSpMatrix or DoubleTpMatrix):
                The input packed matrix.

        Raises:
            ValueError: If `self.dim !=  M.num_rows * (M.num_rows + 1) / 2`.
        """
        numel = M.num_rows * (M.num_rows + 1) / 2
        if self.dim != numel:
            raise ValueError("The number of elements of the input packed matrix"
                             " ({}) should match the size of the vector ({})."
                             .format(numel, self.dim))
        elif isinstance(M, _packed_matrix.PackedMatrix):
            _kaldi_vector_ext._copy_from_packed(self, M)
        elif isinstance(M, _packed_matrix.DoublePackedMatrix):
            _kaldi_vector_ext._copy_from_double_packed(self, M)
        else:
            raise TypeError("input matrix type is not supported.")
        return self

    def add_row_sum_mat_(self, alpha, M, beta=1.0):
        """Adds the sum of matrix rows.

        Performs the operation :math:`y = \\alpha\\ \\sum_i M[i] + \\beta\\ y`.

        Args:
            alpha (float): The scalar multiplier for the row sum.
            M (Matrix): The input matrix.
            beta (float): The scalar multiplier for the destination vector.
                Defaults to ``1.0``.

        Raises:
            ValueError: If `self.dim != M.num_cols`.
        """
        if self.dim != M.num_cols:
            raise ValueError("Cannot add sum of rows with size {} to "
                             "vector of size {}".format(M.num_cols, self.dim))
        _kaldi_vector_ext._add_row_sum_mat(self, alpha, M, beta)
        return self

    def add_col_sum_mat_(self, alpha, M, beta=1.0):
        """Adds the sum of matrix columns.

        Performs the operation
        :math:`y = \\alpha\\ \\sum_i M[:,i] + \\beta\\ y`.

        Args:
            alpha (float): The scalar multiplier for the column sum.
            M (Matrix): The input matrix.
            beta (float): The scalar multiplier for the destination vector.
                Defaults to ``1.0``.

        Raises:
            ValueError: If `self.dim != M.num_rows`.
        """
        if self.dim != M.num_rows:
            raise ValueError("Cannot add sum of columns with size {} to "
                             "vector of size {}".format(M.num_rows, self.dim))
        _kaldi_vector_ext._add_col_sum_mat(self, alpha, M, beta)
        return self

    def add_diag_mat2_(self, alpha, M,
                       trans=_matrix_common.MatrixTransposeType.NO_TRANS,
                       beta=1.0):
        """Adds the diagonal of a matrix multiplied with its transpose.

        Performs the operation :math:`y = \\alpha\\ diag(M M^T) + \\beta\\ y`.

        Args:
            alpha (float): The scalar multiplier for the diagonal.
            M (Matrix): The input matrix.
            trans (MatrixTransposeType): Whether to use **M** or its transpose.
                Defaults to ``MatrixTransposeType.NO_TRANS``.
            beta (float): The scalar multiplier for the destination vector.
                Defaults to ``1.0``.

        Raises:
            ValueError: In case of size mismatch.
        """
        if self.dim != M.num_rows:
            raise ValueError("Cannot add diagonal with size {} to "
                             "vector of size {}".format(M.num_rows, self.dim))
        _kaldi_vector_ext._add_diag_mat2(self, alpha, M, trans, beta)
        return self

    def add_diag_mat_mat_(self, alpha, M, transM, N, transN, beta=1.0):
        """Adds the diagonal of a matrix-matrix product.

        Performs the operation :math:`y = \\alpha\\ diag(M N) + \\beta\\ y`.

        Args:
            alpha (float): The scalar multiplier for the diagonal.
            M (Matrix): The first input matrix.
            transM (MatrixTransposeType): Whether to use **M** or its transpose.
            N (Matrix): The second input matrix.
            transN (MatrixTransposeType): Whether to use **N** or its transpose.
            beta (float): The scalar multiplier for the destination vector.
                Defaults to ``1.0``.

        Raises:
            ValueError: In case of size mismatch.
        """
        m, n = M.size()
        p, q = N.size()

        if transM == _matrix_common.MatrixTransposeType.NO_TRANS:
            if transN == _matrix_common.MatrixTransposeType.NO_TRANS:
                if n != p:
                    raise ValueError("Cannot multiply M ({} by {}) with "
                                     "N ({} by {})".format(m, n, p, q))
            else:
                if n != q:
                    raise ValueError("Cannot multiply M ({} by {}) with "
                                     "N^T ({} by {})".format(m, n, q, p))
        else:
            if transN == _matrix_common.MatrixTransposeType.NO_TRANS:
                if m != p:
                    raise ValueError("Cannot multiply M ({} by {}) with "
                                     "N ({} by {})".format(n, m, p, q))
            else:
                if m != q:
                    raise ValueError("Cannot multiply M ({} by {}) with "
                                     "N ({} by {})".format(n, m, q, p))
        _kaldi_vector_ext._add_diag_mat_mat(self, alpha, M, transM,
                                            N, transN, beta)

    def mul_elements_(self, v):
        """Multiplies the elements with the elements of another vector.

        Performs the operation `y[i] *= v[i]`.

        Args:
            v (Vector or DoubleVector): The input vector.

        Raises:
            RuntimeError: In case of size mismatch.
        """
        if isinstance(v, _kaldi_vector.VectorBase):
            return self._mul_elements_(v)
        elif isinstance(v, _kaldi_vector.DoubleVectorBase):
            _kaldi_vector_ext._mul_double_elements(self, v)
            return self
        else:
            raise TypeError("input vector type is not supported.")

    def div_elements_(self, v):
        """Divides the elements with the elements of another vector.

        Performs the operation `y[i] /= v[i]`.

        Args:
            v (Vector or DoubleVector): The input vector.

        Raises:
            RuntimeError: In case of size mismatch.
        """
        if isinstance(v, _kaldi_vector.VectorBase):
            return self._div_elements_(v)
        elif isinstance(v, _kaldi_vector.DoubleVectorBase):
            _kaldi_vector_ext._div_double_elements(self, v)
            return self
        else:
            raise TypeError("input vector type is not supported.")

    def __repr__(self):
        return str(self)

    def __str__(self):
        # All strings are unicode in Python 3, while we have to encode unicode
        # strings in Python2. If we can't, let python decide the best
        # characters to replace unicode characters with.
        # Below implementation was taken from
        # https://github.com/pytorch/pytorch/blob/master/torch/tensor.py
        if sys.version_info > (3,):
            return _str._vector_str(self)
        else:
            if hasattr(sys.stdout, 'encoding'):
                return _str._vector_str(self).encode(
                    sys.stdout.encoding or 'UTF-8', 'replace')
            else:
                return _str._vector_str(self).encode('UTF-8', 'replace')

    def __getitem__(self, index):
        """Implements self[index].

        This operation is offloaded to NumPy. Hence, it supports all NumPy array
        indexing schemes: field access, basic slicing and advanced indexing.
        For details see `NumPy Array Indexing`_.

        Slicing shares data with the source vector when possible (see Caveats).

        Returns:
            - a float if the result of numpy indexing is a scalar
            - a SubVector if the result of numpy indexing is 1 dimensional
            - a SubMatrix if the result of numpy indexing is 2 dimensional

        Caveats:
            - Kaldi vector and matrix types do not support non-contiguous memory
              layouts for the last dimension, i.e. the stride for the last
              dimension should be the size of a float. If the result of numpy
              slicing operation has an unsupported stride value for the last
              dimension, the return value will not share any data with the
              source vector, i.e. a copy will be made. Consider the following:
                >>> v = Vector(5)
                >>> s = v[0:4:2]     # s does not share data with v
                >>> s[:] = v[1:4:2]  # changing s will not change v
              Since the slicing operation requires a copy of the data to be
              made, the source vector v will not be updated. On the other hand,
              the following assignment operation will work as expected since
              __setitem__ method does not create a new vector for representing
              the left hand side:
                >>> v[0:4:2] = v[1:4:2]

        .. _NumPy Array Indexing:
            https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html
        """
        ret = self.numpy().__getitem__(index)
        if isinstance(ret, numpy.float32):
            return float(ret)
        elif isinstance(ret, numpy.ndarray):
            if ret.ndim == 1:
                return SubVector(ret)
            elif ret.ndim == 2:
                return SubMatrix(ret)
            else:
                raise ValueError("indexing operation returned a numpy array "
                                 " with {} dimensions.".format(ret.ndim))
        raise TypeError("indexing operation returned an invalid type {}."
                        .format(type(ret)))

    def __setitem__(self, index, value):
        """Implements self[index] = value.

        This operation is offloaded to NumPy. Hence, it supports all NumPy array
        indexing schemes: field access, basic slicing and advanced indexing.
        For details see `NumPy Array Indexing`_.

        .. _NumPy Array Indexing:
            https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html
        """
        self.numpy().__setitem__(index, value)

    # Numpy array interface methods were adapted from PyTorch.
    # https://github.com/pytorch/pytorch/commit/c488a9e9bf9eddca6d55957304612b88f4638ca7

    # Numpy array interface, to support `numpy.asarray(vector) -> ndarray`
    def __array__(self, dtype=None):
        if dtype is None:
            return self.numpy()
        else:
            return self.numpy().astype(dtype, copy=False)

    # Wrap Numpy array in a vector or matrix when done, to support e.g.
    # `numpy.sin(vector) -> vector` or `numpy.greater(vector, 0) -> vector`
    def __array_wrap__(self, array):
        if array.ndim == 0:
            if array.dtype.kind == 'b':
                return bool(array)
            elif array.dtype.kind in ('i', 'u'):
                return int(array)
            elif array.dtype.kind == 'f':
                return float(array)
            elif array.dtype.kind == 'c':
                return complex(array)
            else:
                raise RuntimeError('bad scalar {!r}'.format(array))
        elif array.ndim == 1:
            if array.dtype != numpy.float32:
                # Vector stores single precision floats.
                array = array.astype('float32')
            return SubVector(array)
        elif array.ndim == 2:
            if array.dtype != numpy.float32:
                # Matrix stores single precision floats.
                array = array.astype('float32')
            return SubMatrix(array)
        else:
            raise RuntimeError('{} dimensional array cannot be converted to a '
                               'vector or matrix type'.format(array.ndim))


[docs]class Vector(_VectorBase, _kaldi_vector.Vector): """Single precision vector.""" def __init__(self, *args): """ Vector(): Creates an empty vector. Vector(size: int): Creates a new vector of given size and fills it with zeros. Args: size (int): Size of the new vector. Vector(obj: vector_like): Creates a new vector with the elements in obj. Args: obj (vector_like): A vector, a 1-D numpy array, any object exposing a 1-D array interface, an object with an __array__ method returning a 1-D numpy array, or any sequence that can be interpreted as a vector. """ if len(args) > 1: raise TypeError("__init__() takes 1 to 2 positional arguments but " "{} were given".format(len(args) + 1)) super(Vector, self).__init__() if len(args) == 0: return if isinstance(args[0], int): size = args[0] if size < 0: raise ValueError("size should non-negative") self.resize_(size) return obj = args[0] if not isinstance(obj, (_kaldi_vector.VectorBase, _kaldi_vector.DoubleVectorBase)): obj = numpy.array(obj, dtype=numpy.float32, copy=False, order='C') if obj.ndim != 1: raise TypeError("obj should be a 1-D vector like object.") obj = SubVector(obj) self.resize_(obj.dim, _matrix_common.MatrixResizeType.UNDEFINED) self.copy_(obj) def __delitem__(self, index): """Removes an element from the vector.""" if not (0 <= index < self.dim): raise IndexError("index={} should be in the range [0,{})." .format(index, self.dim)) self._remove_element_(index)
[docs]class SubVector(_VectorBase, _matrix_ext.SubVector): """Single precision vector view.""" def __init__(self, obj, start=0, length=None): """Creates a new vector view from a vector like object. If possible the new vector view will share its data with the `obj`, i.e. no copy will be made. A copy will only be made if `obj.__array__` returns a copy, if `obj` is a sequence, or if a copy is needed to satisfy any of the other requirements (data type, order, etc.). Regardless of whether a copy is made or not, the new vector view will not own the memory buffer backing it, i.e. it will not support vector operations that reallocate memory. Args: obj (vector_like): A vector, a 1-D numpy array, any object exposing a 1-D array interface, an object whose __array__ method returns a 1-D numpy array, or any sequence that can be interpreted as a vector. start (int): The index of the view start. Defaults to 0. length (int): The length of the view. If None, it is set to len(obj) - start. Defaults to None. """ if not isinstance(obj, _kaldi_vector.VectorBase): obj = numpy.array(obj, dtype=numpy.float32, copy=False, order='C') if obj.ndim != 1: raise ValueError("obj should be a 1-D vector like object.") obj_len = len(obj) if not (0 <= start <= obj_len): raise IndexError("start={0} should be in the range [0,{1}] " "when len(obj)={1}.".format(start, obj_len)) max_len = obj_len - start if length is None: length = max_len if not (0 <= length <= max_len): raise IndexError("length={} should be in the range [0,{}] when " "start={} and len(obj)={}." .format(length, max_len, start, obj_len)) super(SubVector, self).__init__(obj, start, length)
class _MatrixBase(object): """Base class defining the additional API for single precision matrices. No constructor. """ def copy_(self, src, trans=_matrix_common.MatrixTransposeType.NO_TRANS): """Copies the elements from another matrix. Args: src(Matrix or SpMatrix or TpMatrix or DoubleMatrix or DoubleSpMatrix or DoubleTpMatrix or CompressedMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **src** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. Not active if input is a compressed matrix. Raises: ValueError: In case of size mismatch. """ if self.size() != src.size(): raise ValueError("Cannot copy matrix with dimensions {s[0]}x{s[1]} " "into matrix with dimensions {d[0]}x{d[1]}" .format(s=src.size(), d=self.size())) if isinstance(src, _kaldi_matrix.MatrixBase): self._copy_from_mat_(src, trans) elif isinstance(src, _sp_matrix.SpMatrix): _kaldi_matrix_ext._copy_from_sp(self, src) elif isinstance(src, _tp_matrix.TpMatrix): _kaldi_matrix_ext._copy_from_tp(self, src, trans) elif isinstance(src, _kaldi_matrix.DoubleMatrixBase): _kaldi_matrix_ext._copy_from_double_mat(self, src, trans) elif isinstance(src, _sp_matrix.SpMatrix): _kaldi_matrix_ext._copy_from_double_sp(self, src) elif isinstance(src, _tp_matrix.TpMatrix): _kaldi_matrix_ext._copy_from_double_tp(self, src, trans) elif isinstance(src, _compressed_matrix.CompressedMatrix): _kaldi_matrix_ext._copy_from_cmat(self, src) else: raise TypeError("input matrix type is not supported.") return self def clone(self): """Clones the matrix. The clone allocates new memory for its contents and supports matrix operations that reallocate memory, i.e. it is not a view. Returns: Matrix: A copy of the matrix. """ return Matrix(self) def size(self): """Returns the size of the matrix. Returns: A tuple (num_rows, num_cols) of integers. """ return self.num_rows, self.num_cols @property def shape(self): """Two element tuple representing the size of the matrix.""" return self.size() def approx_equal(self, other, tol=0.01): """Checks if matrices are approximately equal. Args: other (Matrix): The matrix to compare against. tol (float): The tolerance for the equality check. Defaults to ``0.01``. Returns: True if `self.size() == other.size()` and `||self-other|| <= tol*||self||`. False otherwise. """ if not isinstance(other, _kaldi_matrix.MatrixBase): return False if self.num_rows != other.num_rows or self.num_cols != other.num_cols: return False return self._approx_equal(other, tol) def __eq__(self, other): return self.approx_equal(other, 1e-16) def numpy(self): """Converts the matrix to a 2-D NumPy array. The NumPy array is a view into the matrix, i.e. no data is copied. Returns: numpy.ndarray: A NumPy array sharing data with this matrix. """ return _matrix_ext.matrix_to_numpy(self) @property def data(self): """Matrix data as a memoryview.""" return self.numpy().data def row_data(self, index): """Returns row data as a memoryview.""" return self[index].data def row(self, index): """Returns the given row as a new vector view. Args: index (int): The row index. Returns: SubVector: A vector view representing the given row. """ return self[index] def range(self, row_start, num_rows, col_start, num_cols): """Returns the given range of elements as a new matrix view. Args: row_start (int): The start row index. num_rows (int): The number of rows. col_start (int): The start column index. num_cols (int): The number of columns. Returns: SubMatrix: A matrix view representing the given range. """ return SubMatrix(self, row_start, num_rows, col_start, num_cols) def row_range(self, row_start, num_rows): """Returns the given range of rows as a new matrix view. Args: row_start (int): The start row index. num_rows (int): The number of rows. Returns: SubMatrix: A matrix view representing the given row range. """ return SubMatrix(self, row_start, num_rows, 0, self.num_cols) def col_range(self, col_start, num_cols): """Returns the given range of columns as a new matrix view. Args: col_start (int): The start column index. num_cols (int): The number of columns. Returns: SubMatrix: A matrix view representing the given column range. """ return SubMatrix(self, 0, self.num_rows, col_start, num_cols) def eig(self): """Computes eigendecomposition. Factorizes a square matrix into :math:`P\\ D\\ P^{-1}`. The relationship of :math:`D` to the eigenvalues is slightly complicated, due to the need for :math:`P` to be real. In the symmetric case, :math:`D` is diagonal and real, but in the non-symmetric case there may be complex-conjugate pairs of eigenvalues. In this case, for the equation :math:`y = P\\ D\\ P^{-1}` to hold, :math:`D` must actually be block diagonal, with 2x2 blocks corresponding to any such pairs. If a pair is :math:`\\lambda +- i\\mu`, :math:`D` will have a corresponding 2x2 block :math:`[\\lambda, \\mu; -\\mu, \\lambda]`. Note that if the matrix is not invertible, :math:`P` may not be invertible so in this case instead of the equation :math:`y = P\\ D\\ P^{-1}` holding, we have :math:`y\\ P = P\\ D`. Returns: 3-element tuple containing - **P** (:class:`Matrix`): The eigenvector matrix, where ith column corresponds to the ith eigenvector. - **r** (:class:`Vector`): The vector with real components of the eigenvalues. - **i** (:class:`Vector`): The vector with imaginary components of the eigenvalues. Raises: ValueError: If the matrix is not square. """ m, n = self.size() if m != n: raise ValueError("eig method cannot be called on a non-square " "matrix.") P = Matrix(n, n) r, i = Vector(n), Vector(n) self._eig(P, r, i) return P, r, i def svd(self, destructive=False): """Computes singular-value decomposition. Factorizes a matrix into :math:`U\\ diag(s)\\ V^T`. For non-square matrices, requires `self.num_rows >= self.num_cols`. Args: destructive (bool): Whether to use the destructive operation which avoids a copy but mutates self. Defaults to ``False``. Returns: 3-element tuple containing - **s** (:class:`Vector`): The vector of singular values. - **U** (:class:`Matrix`): The left orthonormal matrix. - **Vt** (:class:`Matrix`): The right orthonormal matrix. Raises: ValueError: If `self.num_rows < self.num_cols`. Note: **Vt** in the output is already transposed. The singular values in **s** are not sorted. See Also: :meth:`singular_values` :meth:`sort_svd` """ m, n = self.size() if m < n: raise ValueError("svd for non-square matrices requires " "self.num_rows >= self.num_cols.") U, Vt = Matrix(m, n), Matrix(n, n) s = Vector(n) if destructive: self._destructive_svd_(s, U, Vt) else: self._svd(s, U, Vt) return s, U, Vt def singular_values(self): """Computes singular values. Returns: Vector: The vector of singular values. """ res = Vector(self.num_cols) self._singular_values(res) return res def add_mat_(self, alpha, M, trans=_matrix_common.MatrixTransposeType.NO_TRANS): """Adds another matrix to this one. Performs the operation :math:`S = \\alpha\\ M + S`. Args: alpha (float): The scalar multiplier. M (Matrix or SpMatrix or DoubleSpMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. Raises: RuntimeError: In case of size mismatch. """ if isinstance(M, _kaldi_matrix.MatrixBase): self._add_mat_(alpha, M, trans) elif isinstance(M, _sp_matrix.SpMatrix): _kaldi_matrix_ext.add_sp(self, alpha, M) elif isinstance(M, _sp_matrix.DoubleSpMatrix): _kaldi_matrix_ext.add_double_sp(self, alpha, M) else: raise TypeError("input matrix type is not supported.") return self def add_mat_mat_(self, A, B, transA=_matrix_common.MatrixTransposeType.NO_TRANS, transB=_matrix_common.MatrixTransposeType.NO_TRANS, alpha=1.0, beta=1.0, sparseA=False, sparseB=False): """Adds the product of given matrices. Performs the operation :math:`M = \\alpha\\ A\\ B + \\beta\\ M`. Args: A (Matrix or TpMatrix or SpMatrix): The first input matrix. B (Matrix or TpMatrix or SpMatrix): The second input matrix. transA (MatrixTransposeType): Whether to use **A** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. transB (MatrixTransposeType): Whether to use **B** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. alpha (float): The scalar multiplier for the product. Defaults to ``1.0``. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. sparseA (bool): Whether to use the algorithm that is faster when **A** is sparse. Defaults to ``False``. sparseA (bool): Whether to use the algorithm that is faster when **B** is sparse. Defaults to ``False``. Raises: RuntimeError: In case of size mismatch. TypeError: If matrices of given types can not be multiplied. """ if isinstance(A, _kaldi_matrix.MatrixBase): if isinstance(B, _kaldi_matrix.MatrixBase): if sparseA: self._add_smat_mat_(alpha, A, transA, B, transB, beta) elif sparseB: self._add_mat_smat_(alpha, A, transA, B, transB, beta) else: self._add_mat_mat_(alpha, A, transA, B, transB, beta) elif isinstance(B, _sp_matrix.SpMatrix): _kaldi_matrix_ext._add_mat_sp(self, alpha, A, transA, B, beta) elif isinstance(B, _tp_matrix.TpMatrix): _kaldi_matrix_ext._add_mat_tp(self, alpha, A, transA, B, transB, beta) else: raise TypeError("Cannot multiply matrix A with matrix B of " "type {}".format(type(B))) elif isinstance(A, _sp_matrix.SpMatrix): if isinstance(B, _kaldi_matrix.MatrixBase): _kaldi_matrix_ext._add_sp_mat(self, alpha, A, B, transB, beta) elif isinstance(B, _sp_matrix.SpMatrix): _kaldi_matrix_ext._add_sp_sp(self, alpha, A, transA, B, beta) else: raise TypeError("Cannot multiply symmetric matrix A with " "matrix B of type {}".format(type(B))) elif isinstance(A, _tp_matrix.SpMatrix): if isinstance(B, _kaldi_matrix.MatrixBase): _kaldi_matrix_ext._add_tp_mat(self, alpha, transA, B, transB, beta) elif isinstance(B, _tp_matrix.TpMatrix): _kaldi_matrix_ext._add_tp_tp(self, alpha, A, transA, B, transB, beta) else: raise TypeError("Cannot multiply triangular matrix A with " "matrix B of type {}".format(type(B))) return self def invert_(self, in_double_precision=False): """Inverts the matrix. Args: in_double_precision (bool): Whether to do the inversion in double precision. Defaults to ``False``. Returns: 2-element tuple containing - **log_det** (:class:`float`): The log determinant. - **det_sign** (:class:`float`): The sign of the determinant, 1 or -1. Raises: RuntimeError: If matrix is not square. """ if in_double_precision: return _kaldi_matrix_ext._invert_in_double(self) else: return _kaldi_matrix_ext._invert(self) def copy_cols_(self, src, indices): """Copies columns from another matrix. Copies column `r` from column `indices[r]` of `src`. As a special case, if `indexes[i] == -1`, sets column `i` to zero. All elements of indices must be in `[-1, src.num_cols-1]`, and `src.num_rows` must equal `self.num_rows`. Args: src (Matrix): The input matrix. indices (List[int]): The list of column indices. """ _kaldi_matrix_ext._copy_cols(self, src, indices) return self def copy_rows_(self, src, indices): """Copies rows from another matrix. Copies row `r` from row `indices[r]` of `src`. As a special case, if `indexes[i] == -1`, sets row `i` to zero. All elements of indices must be in `[-1, src.num_rows-1]`, and `src.num_cols` must equal `self.num_cols`. Args: src (Matrix): The input matrix. indices (List[int]): The list of row indices. """ _kaldi_matrix_ext._copy_rows(self, src, indices) return self def add_cols_(self, src, indices): """Adds columns from another matrix. Adds column `indices[r]` of `src` to column `r`. As a special case, if `indexes[i] == -1`, skips column `i`. All elements of indices must be in `[-1, src.num_cols-1]`, and `src.num_rows` must equal `self.num_rows`. Args: src (Matrix): The input matrix. indices (List[int]): The list of column indices. """ _kaldi_matrix_ext._add_cols(self, src, indices) return self def add_rows_(self, src, indices, alpha=1.0): """Adds rows from another matrix. Scales row `indices[r]` of `src` with `alpha` and adds it to row `r`. As a special case, if `indexes[i] == -1`, skips row `i`. All elements of indices must be in `[-1, src.num_rows-1]`, and `src.num_cols` must equal `self.num_cols`. Args: src (Matrix): The input matrix. indices (List[int]): The list of row indices. alpha (float): The scalar multiplier. Defaults to `1.0`. """ _kaldi_matrix_ext._add_rows(self, alpha, src, indices) return self def __getitem__(self, index): """Implements self[index]. This operation is offloaded to NumPy. Hence, it supports all NumPy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. Slicing shares data with the source matrix when possible (see Caveats). Returns: - a float if the result of numpy indexing is a scalar - a SubVector if the result of numpy indexing is 1 dimensional - a SubMatrix if the result of numpy indexing is 2 dimensional Caveats: - Kaldi vector and matrix types do not support non-contiguous memory layouts for the last dimension, i.e. the stride for the last dimension should be the size of a float. If the result of numpy slicing operation has an unsupported stride value for the last dimension, the return value will not share any data with the source matrix, i.e. a copy will be made. Consider the following: >>> m = Matrix(3, 5) >>> s = m[:,0:4:2] # s does not share data with m >>> s[:] = m[:,1:4:2] # changing s will not change m Since the slicing operation requires a copy of the data to be made, the source matrix m will not be updated. On the other hand, the following assignment operation will work as expected since __setitem__ method does not create a new scalar/vector/matrix for representing the left hand side: >>> m[:,0:4:2] = m[:,1:4:2] .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ ret = self.numpy().__getitem__(index) if isinstance(ret, numpy.float32): return float(ret) elif isinstance(ret, numpy.ndarray): if ret.ndim == 2: return SubMatrix(ret) elif ret.ndim == 1: return SubVector(ret) else: raise ValueError("indexing operation returned a numpy array " " with {} dimensions.".format(ret.ndim)) raise TypeError("indexing operation returned an invalid type {}." .format(type(ret))) def __setitem__(self, index, value): """Implements self[index] = value. This operation is offloaded to NumPy. Hence, it supports all NumPy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ self.numpy().__setitem__(index, value) def __contains__(self, value): """Implements value in self.""" return value in self.numpy() def __repr__(self): return str(self) def __str__(self): # All strings are unicode in Python 3, while we have to encode unicode # strings in Python2. If we can't, let python decide the best # characters to replace unicode characters with. # Below implementation was taken from # https://github.com/pytorch/pytorch/blob/master/torch/tensor.py if sys.version_info > (3,): return _str._matrix_str(self) else: if hasattr(sys.stdout, 'encoding'): return _str._matrix_str(self).encode( sys.stdout.encoding or 'UTF-8', 'replace') else: return _str._matrix_str(self).encode('UTF-8', 'replace') # Numpy array interface methods were adapted from PyTorch. # https://github.com/pytorch/pytorch/commit/c488a9e9bf9eddca6d55957304612b88f4638ca7 # Numpy array interface, to support `numpy.asarray(vector) -> ndarray` def __array__(self, dtype=None): if dtype is None: return self.numpy() else: return self.numpy().astype(dtype, copy=False) # Wrap Numpy array in a vector or matrix when done, to support e.g. # `numpy.sin(vector) -> vector` or `numpy.greater(vector, 0) -> vector` def __array_wrap__(self, array): if array.ndim == 0: if array.dtype.kind == 'b': return bool(array) elif array.dtype.kind in ('i', 'u'): return int(array) elif array.dtype.kind == 'f': return float(array) elif array.dtype.kind == 'c': return complex(array) else: raise RuntimeError('bad scalar {!r}'.format(array)) elif array.ndim == 1: if array.dtype != numpy.float32: # Vector stores single precision floats. array = array.astype('float32') return SubVector(array) elif array.ndim == 2: if array.dtype != numpy.float32: # Matrix stores single precision floats. array = array.astype('float32') return SubMatrix(array) else: raise RuntimeError('{} dimensional array cannot be converted to a ' 'Kaldi vector or matrix type'.format(array.ndim))
[docs]class Matrix(_MatrixBase, _kaldi_matrix.Matrix): """Single precision matrix.""" def __init__(self, *args): """ Matrix(): Creates an empty matrix. Matrix(num_rows: int, num_cols: int): Creates a new matrix of given size and fills it with zeros. Args: num_rows (int): Number of rows of the new matrix. num_cols (int): Number of cols of the new matrix. Matrix(obj: matrix_like): Creates a new matrix with the elements in obj. Args: obj (matrix_like): A matrix, a 2-D numpy array, any object exposing a 2-D array interface, an object with an __array__ method returning a 2-D numpy array, or any (nested) sequence that can be interpreted as a matrix. """ if len(args) > 2: raise TypeError("__init__() takes 1 to 3 positional arguments but " "{} were given".format(len(args) + 1)) super(Matrix, self).__init__() if len(args) == 0: return if len(args) == 2: num_rows, num_cols = args if not (isinstance(num_rows, int) and isinstance(num_cols, int)): raise TypeError("num_rows and num_cols should be integers") if not (num_rows > 0 and num_cols > 0): if not (num_rows == 0 and num_cols == 0): raise IndexError("num_rows and num_cols should both be " "positive or they should both be 0.") self.resize_(num_rows, num_cols) return obj = args[0] if not isinstance(obj, (_kaldi_matrix.MatrixBase, _packed_matrix.PackedMatrix, _kaldi_matrix.DoubleMatrixBase, _packed_matrix.DoublePackedMatrix, _compressed_matrix.CompressedMatrix)): obj = numpy.array(obj, dtype=numpy.float32, copy=False, order='C') if obj.ndim != 2: raise ValueError("obj should be a 2-D matrix like object.") obj = SubMatrix(obj) self.resize_(obj.num_rows, obj.num_cols, _matrix_common.MatrixResizeType.UNDEFINED) self.copy_(obj) def __delitem__(self, index): """Removes a row from the matrix.""" if not (0 <= index < self.num_rows): raise IndexError("index={} should be in the range [0,{})." .format(index, self.num_rows)) self._remove_row_(index)
[docs]class SubMatrix(_MatrixBase, _matrix_ext.SubMatrix): """Single precision matrix view.""" def __init__(self, obj, row_start=0, num_rows=None, col_start=0, num_cols=None): """Creates a new matrix view from a matrix like object. If possible the new matrix view will share its data with the `obj`, i.e. no copy will be made. A copy will only be made if `obj.__array__` returns a copy, if `obj` is a sequence, or if a copy is needed to satisfy any of the other requirements (data type, order, etc.). Regardless of whether a copy is made or not, the new matrix view will not own the memory buffer backing it, i.e. it will not support matrix operations that reallocate memory. Args: obj (matrix_like): A matrix, a 2-D numpy array, any object exposing a 2-D array interface, an object with an __array__ method returning a 2-D numpy array, or any sequence that can be interpreted as a matrix. row_start (int): The start row index. Defaults to ``0``. num_rows (int): The number of rows. If ``None``, it is set to `self.num_rows - row_start`. Defaults to ``None``. col_start (int): The start column index. Defaults to ``0``. num_cols (int): The number of columns. If ``None``, it is set to `self.num_cols - col_start`. Defaults to ``None``. """ if not isinstance(obj, _kaldi_matrix.MatrixBase): obj = numpy.array(obj, dtype=numpy.float32, copy=False, order='C') if obj.ndim != 2: raise ValueError("obj should be a 2-D matrix like object.") obj_num_rows, obj_num_cols = obj.shape else: obj_num_rows, obj_num_cols = obj.num_rows, obj.num_cols if not (0 <= row_start <= obj_num_rows): raise IndexError("row_start={0} should be in the range [0,{1}] " "when obj.num_rows={1}." .format(row_start, obj_num_rows)) if not (0 <= col_start <= obj_num_cols): raise IndexError("col_start={0} should be in the range [0,{1}] " "when obj.num_cols={1}." .format(col_offset, obj_num_cols)) max_rows, max_cols = obj_num_rows - row_start, obj_num_cols - col_start if num_rows is None: num_rows = max_rows if num_cols is None: num_cols = max_cols if not (0 <= num_rows <= max_rows): raise IndexError("num_rows={} should be in the range [0,{}] " "when row_start={} and obj.num_rows={}." .format(num_rows, max_rows, row_start, obj_num_rows)) if not (0 <= num_cols <= max_cols): raise IndexError("num_cols={} should be in the range [0,{}] " "when col_start={} and obj.num_cols={}." .format(num_cols, max_cols, col_start, obj_num_cols)) if not (num_rows > 0 and num_cols > 0): if not (num_rows == 0 and num_cols == 0): raise IndexError("num_rows and num_cols should both be " "positive or they should both be 0.") super(SubMatrix, self).__init__(obj, row_start, num_rows, col_start, num_cols)
################################################################################ # double precision vector/matrix types ################################################################################ class _DoubleVectorBase(object): """Base class defining the additional API for double precision vectors. No constructor. """ def copy_(self, src): """Copies the elements from another vector. Args: src (Vector or DoubleVector): The input vector. Raises: ValueError: In case of size mismatch. """ if self.dim != src.dim: raise ValueError("Vector of size {} cannot be copied into vector " "of size {}.".format(src.dim, self.dim)) if isinstance(src, _kaldi_vector.DoubleVectorBase): return self._copy_from_vec_(src) elif isinstance(src, _kaldi_vector.VectorBase): _kaldi_vector_ext._copy_from_single_vec_double(self, src) return self else: raise TypeError("input vector type is not supported.") def clone(self): """Clones the vector. The clone allocates new memory for its contents and supports vector operations that reallocate memory, i.e. it is not a view. Returns: DoubleVector: A copy of the vector. """ return DoubleVector(self) def size(self): """Returns the size of the vector as a single element tuple.""" return (self.dim,) @property def shape(self): """Single element tuple representing the size of the vector.""" return self.size() def approx_equal(self, other, tol=0.01): """Checks if vectors are approximately equal. Args: other (DoubleVector): The vector to compare against. tol (float): The tolerance for the equality check. Defaults to ``0.01``. Returns: True if `self.dim == other.dim` and `||self-other|| <= tol*||self||`. False otherwise. """ if not isinstance(other, _kaldi_vector.DoubleVectorBase): return False if self.dim != other.dim: return False return self._approx_equal(other, tol) def __eq__(self, other): return self.approx_equal(other, 1e-16) def numpy(self): """Converts the vector to a 1-D NumPy array. The NumPy array is a view into the vector, i.e. no data is copied. Returns: numpy.ndarray: A NumPy array sharing data with this vector. """ return _matrix_ext.double_vector_to_numpy(self) @property def data(self): """Vector data as a memoryview.""" return self.numpy().data def range(self, start, length): """Returns the given range of elements as a new vector view. Args: start (int): The start index. length (int): The length. Returns: DoubleSubVector: A vector view representing the given range. """ return DoubleSubVector(self, start, length) def add_vec_(self, alpha, v): """Adds another vector. Performs the operation :math:`y = y + \\alpha\\ v`. Args: alpha (float): The scalar multiplier. v (Vector or DoubleVector): The input vector. Raises: RuntimeError: In case of size mismatch. """ if isinstance(v, _kaldi_vector.DoubleVectorBase): return self._add_vec_(alpha, v) elif isinstance(v, _kaldi_vector.VectorBase): _kaldi_vector_ext._add_single_vec_double(self, alpha, v) return self else: raise TypeError("input vector type is not supported.") def add_vec2_(self, alpha, v): """Adds the squares of elements from another vector. Performs the operation :math:`y = y + \\alpha\\ v\\odot v`. Args: alpha (float): The scalar multiplier. v (Vector or DoubleVector): The input vector. Raises: RuntimeError: In case of size mismatch. """ if isinstance(v, _kaldi_vector.DoubleVectorBase): return self._add_vec2_(alpha, v) elif isinstance(v, _kaldi_vector.VectorBase): _kaldi_vector_ext._add_single_vec2_double(self, alpha, v) return self else: raise TypeError("input vector type is not supported.") def add_mat_vec_(self, alpha, M, trans, v, beta, sparse=False): """Computes a matrix-vector product. Performs the operation :math:`y = \\alpha\\ M\\ v + \\beta\\ y`. Args: alpha (float): The scalar multiplier for the matrix-vector product. M (DoubleMatrix or DoubleSpMatrix or DoubleTpMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. v (DoubleVector): The input vector. beta (float): The scalar multiplier for the destination vector. sparse (bool): Whether to use the algorithm that is faster when **v** is sparse. Defaults to ``False``. Raises: ValueError: In case of size mismatch. """ if v.dim != M.num_cols: raise ValueError("Matrix of size {}x{} cannot be multiplied with " "vector of size {}." .format(M.num_rows, M.num_cols, v.dim)) if self.dim != M.num_rows: raise ValueError("Vector of size {} cannot be added to vector of " "size {}.".format(M.num_rows, self.dim)) if isinstance(M, _kaldi_matrix.DoubleMatrixBase): if sparse: _kaldi_vector_ext._add_mat_svec_double(self, alpha, M, trans, v, beta) else: _kaldi_vector_ext._add_mat_vec_double(self, alpha, M, trans, v, beta) elif isinstance(M, _sp_matrix.DoubleSpMatrix): _kaldi_vector_ext._add_sp_vec_double(self, alpha, M, v, beta) elif isinstance(M, _tp_matrix.DoubleTpMatrix): _kaldi_vector_ext._add_tp_vec_double(self, alpha, M, trans, v, beta) return self def mul_tp_(self, M, trans): """Multiplies the vector with a lower-triangular matrix. Performs the operation :math:`y = M\\ y`. Args: M (DoubleTpMatrix): The input lower-triangular matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. Raises: ValueError: In case of size mismatch. """ if self.dim != M.num_rows: raise ValueError("Matrix with size {}x{} cannot be multiplied " "with vector of size {}." .format(M.num_rows, M.num_cols, self.dim)) _kaldi_vector_ext._mul_tp_double(self, M, trans) return self def solve_(self, M, trans): """Solves a linear system. The linear system is defined as :math:`M\\ x = b`, where :math:`b` and :math:`x` are the initial and final values of the vector, respectively. Warning: Does not test for :math:`M` being singular or near-singular. Args: M (DoubleTpMatrix): The input lower-triangular matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. Raises: ValueError: In case of size mismatch. """ if self.dim != M.num_rows: raise ValueError("The number of rows of the input matrix ({}) " "should match the size of the vector ({})." .format(M.num_rows, self.dim)) _kaldi_vector_ext._solve_double(self, M, trans) return self def copy_rows_from_mat_(self, M): """Copies the elements from a matrix row-by-row. Args: M (Matrix or DoubleMatrix): The input matrix. Raises: ValueError: In case of size mismatch. """ if self.dim != M.num_rows * M.num_cols: raise ValueError("The number of elements of the input matrix ({}) " "should match the size of the vector ({})." .format(M.num_rows * M.num_cols, self.dim)) if isinstance(M, _kaldi_matrix.DoubleMatrixBase): _kaldi_vector_ext._copy_rows_from_mat_double(self, M) if isinstance(M, _kaldi_matrix.MatrixBase): _kaldi_vector_ext._copy_rows_from_single_mat_double(self, M) else: raise TypeError("input matrix type is not supported.") return self def copy_cols_from_mat_(self, M): """Copies the elements from a matrix column-by-columm. Args: M (DoubleMatrix): The input matrix. Raises: ValueError: In case of size mismatch. """ if self.dim != M.num_rows * M.num_cols: raise ValueError("The number of elements of the input matrix ({}) " "should match the size of the vector ({})." .format(M.num_rows * M.num_cols, self.dim)) _kaldi_vector_ext._copy_cols_from_mat_double(self, M) return self def copy_row_from_mat_(self, M, row): """Copies the elements from a matrix row. Args: M (Matrix or DoubleMatrix or SpMatrix or DoubleSpMatrix): The input matrix. row (int): The row index. Raises: ValueError: In case of size mismatch. IndexError: If the row index is out-of-bounds. """ if self.dim != M.num_cols: raise ValueError("The number of columns of the input matrix ({})" "should match the size of the vector ({})." .format(M.num_cols, self.dim)) if not isinstance(row, int) or not (0 <= row < M.num_rows): raise IndexError() if isinstance(M, _kaldi_matrix.DoubleMatrixBase): _kaldi_vector_ext._copy_row_from_mat_double(self, M, row) elif isinstance(M, _kaldi_matrix.MatrixBase): _kaldi_vector_ext._copy_row_from_single_mat_double(self, M, row) elif isinstance(M, _sp_matrix.DoubleSpMatrix): _kaldi_vector_ext._copy_row_from_sp_double(self, M, row) elif isinstance(M, _sp_matrix.SpMatrix): _kaldi_vector_ext._copy_row_from_single_sp_double(self, M, row) else: raise TypeError("input matrix type is not supported.") return self def copy_col_from_mat_(self, M, col): """Copies the elements from a matrix column. Args: M (Matrix or DoubleMatrix): The input matrix. col (int): The column index. Raises: ValueError: In case of size mismatch. IndexError: If the column index is out-of-bounds. """ if self.dim != M.num_rows: raise ValueError("The number of rows of the input matrix ({})" "should match the size of this vector ({})." .format(M.num_rows, self.dim)) if not instance(col, int) or not (0 <= col < M.num_cols): raise IndexError() if isinstance(M, _kaldi_matrix.DoubleMatrixBase): _kaldi_vector_ext._copy_col_from_mat_double(self, M, col) elif isinstance(M, _kaldi_matrix.MatrixBase): _kaldi_vector_ext._copy_col_from_single_mat_double(self, M, col) else: raise TypeError("input matrix type is not supported.") return self def copy_diag_from_mat_(self, M): """Copies the digonal elements from a matrix. Args: M (Matrix or DoubleSpMatrix or DoubleTpMatrix): The input matrix. Raises: ValueError: In case of size mismatch. """ if self.dim != min(M.num_rows, M.num_cols): raise ValueError("The size of the matrix diagonal ({}) should " "match the size of the vector ({})." .format(min(M.size()), self.dim)) if isinstance(M, _kaldi_matrix.DoubleMatrixBase): _kaldi_vector_ext._copy_diag_from_mat_double(self, M) elif isinstance(M, _sp_matrix.DoubleSpMatrix): _kaldi_vector_ext._copy_diag_from_sp_double(self, M) elif isinstance(M, _tp_matrix.DoubleTpMatrix): _kaldi_vector_ext._copy_diag_from_tp_double(self, M) else: raise TypeError("input matrix type is not supported.") return self def copy_from_packed_(self, M): """Copies the elements from a packed matrix. Args: M (SpMatrix or TpMatrix or DoubleSpMatrix or DoubleTpMatrix): The input packed matrix. Raises: ValueError: If `self.dim != M.num_rows * (M.num_rows + 1) / 2`. """ numel = M.num_rows * (M.num_rows + 1) / 2 if self.dim != numel: raise ValueError("The number of elements of the input packed matrix" " ({}) should match the size of the vector ({})." .format(numel, self.dim)) if isinstance(M, _packed_matrix.DoublePackedMatrix): _kaldi_vector_ext._copy_from_packed_double(self, M) elif isinstance(M, _packed_matrix.PackedMatrix): _kaldi_vector_ext._copy_from_single_packed_double(self, M) else: raise TypeError("input matrix type is not supported.") return self def add_row_sum_mat_(self, alpha, M, beta=1.0): """Adds the sum of matrix rows. Performs the operation :math:`y = \\alpha\\ \\sum_i M[i] + \\beta\\ y`. Args: alpha (float): The scalar multiplier for the row sum. M (DoubleMatrix): The input matrix. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. Raises: ValueError: If `self.dim != M.num_cols`. """ if self.dim != M.num_cols: raise ValueError("Cannot add sum of rows with size {} to " "vector of size {}".format(M.num_cols, self.dim)) _kaldi_vector_ext._add_row_sum_mat_double(self, alpha, M, beta) return self def add_col_sum_mat_(self, alpha, M, beta=1.0): """Adds the sum of matrix columns. Performs the operation :math:`y = \\alpha\\ \\sum_i M[:,i] + \\beta\\ y`. Args: alpha (float): The scalar multiplier for the column sum. M (DoubleMatrix): The input matrix. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. Raises: ValueError: If `self.dim != M.num_rows`. """ if self.dim != M.num_rows: raise ValueError("Cannot add sum of columns with size {} to " "vector of size {}".format(M.num_rows, self.dim)) _kaldi_vector_ext._add_col_sum_mat_double(self, alpha, M, beta) return self def add_diag_mat2_(self, alpha, M, trans=_matrix_common.MatrixTransposeType.NO_TRANS, beta=1.0): """Adds the diagonal of a matrix multiplied with its transpose. Performs the operation :math:`y = \\alpha\\ diag(M M^T) + \\beta\\ y`. Args: alpha (float): The scalar multiplier for the diagonal. M (DoubleMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. Raises: ValueError: In case of size mismatch. """ if self.dim != M.num_rows: raise ValueError("Cannot add diagonal with size {} to " "vector of size {}".format(M.num_rows, self.dim)) _kaldi_vector_ext._add_diag_mat2_double(self, alpha, M, trans, beta) return self def add_diag_mat_mat_(self, alpha, M, transM, N, transN, beta=1.0): """Adds the diagonal of a matrix-matrix product. Performs the operation :math:`y = \\alpha\\ diag(M N) + \\beta\\ y`. Args: alpha (float): The scalar multiplier for the diagonal. M (DoubleMatrix): The first input matrix. transM (MatrixTransposeType): Whether to use **M** or its transpose. N (DoubleMatrix): The second input matrix. transN (MatrixTransposeType): Whether to use **N** or its transpose. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. Raises: ValueError: In case of size mismatch. """ m, n = M.size() p, q = N.size() if transM == _matrix_common.MatrixTransposeType.NO_TRANS: if transN == _matrix_common.MatrixTransposeType.NO_TRANS: if n != p: raise ValueError("Cannot multiply M ({} by {}) with " "N ({} by {})".format(m, n, p, q)) else: if n != q: raise ValueError("Cannot multiply M ({} by {}) with " "N^T ({} by {})".format(m, n, q, p)) else: if transN == _matrix_common.MatrixTransposeType.NO_TRANS: if m != p: raise ValueError("Cannot multiply M ({} by {}) with " "N ({} by {})".format(n, m, p, q)) else: if m != q: raise ValueError("Cannot multiply M ({} by {}) with " "N ({} by {})".format(n, m, q, p)) _kaldi_vector_ext._add_diag_mat_mat_double(self, alpha, M, transM, N, transN, beta) def mul_elements_(self, v): """Multiplies the elements with the elements of another vector. Performs the operation `y[i] *= v[i]`. Args: v (Vector or DoubleVector): The input vector. Raises: RuntimeError: In case of size mismatch. """ if isinstance(v, _kaldi_vector.DoubleVectorBase): return self._mul_elements_(v) elif isinstance(v, _kaldi_vector.VectorBase): _kaldi_vector_ext._mul_single_elements_double(self, v) return self else: raise TypeError("input vector type is not supported.") def div_elements_(self, v): """Divides the elements with the elements of another vector. Performs the operation `y[i] /= v[i]`. Args: v (Vector or DoubleVector): The input vector. Raises: RuntimeError: In case of size mismatch. """ if isinstance(v, _kaldi_vector.DoubleVectorBase): return self._div_elements_(v) elif isinstance(v, _kaldi_vector.VectorBase): _kaldi_vector_ext._div_single_elements_double(self, v) return self else: raise TypeError("input vector type is not supported.") def __repr__(self): return str(self) def __str__(self): # All strings are unicode in Python 3, while we have to encode unicode # strings in Python2. If we can't, let python decide the best # characters to replace unicode characters with. # Below implementation was taken from # https://github.com/pytorch/pytorch/blob/master/torch/tensor.py if sys.version_info > (3,): return _str._vector_str(self) else: if hasattr(sys.stdout, 'encoding'): return _str._vector_str(self).encode( sys.stdout.encoding or 'UTF-8', 'replace') else: return _str._vector_str(self).encode('UTF-8', 'replace') def __getitem__(self, index): """Implements self[index]. This operation is offloaded to numpy. Hence, it supports all numpy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. Slicing shares data with the source vector when possible (see Caveats). Returns: - a float if the result of numpy indexing is a scalar - a DoubleSubVector if the result of numpy indexing is 1 dimensional - a DoubleSubMatrix if the result of numpy indexing is 2 dimensional Caveats: - Kaldi vector and matrix types do not support non-contiguous memory layouts for the last dimension, i.e. the stride for the last dimension should be the size of a float. If the result of numpy slicing operation has an unsupported stride value for the last dimension, the return value will not share any data with the source vector, i.e. a copy will be made. Consider the following: >>> v = DoubleVector(5) >>> s = v[0:4:2] # s does not share data with v >>> s[:] = v[1:4:2] # changing s will not change v Since the slicing operation requires a copy of the data to be made, the source vector v will not be updated. On the other hand, the following assignment operation will work as expected since __setitem__ method does not create a new vector for representing the left hand side: >>> v[0:4:2] = v[1:4:2] .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ ret = self.numpy().__getitem__(index) if isinstance(ret, numpy.float64): return float(ret) elif isinstance(ret, numpy.ndarray): if ret.ndim == 1: return DoubleSubVector(ret) elif ret.ndim == 2: return DoubleSubMatrix(ret) else: raise ValueError("indexing operation returned a numpy array " " with {} dimensions.".format(ret.ndim)) raise TypeError("indexing operation returned an invalid type {}." .format(type(ret))) def __setitem__(self, index, value): """Implements self[index] = value. This operation is offloaded to NumPy. Hence, it supports all NumPy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ self.numpy().__setitem__(index, value) # Numpy array interface methods were adapted from PyTorch. # https://github.com/pytorch/pytorch/commit/c488a9e9bf9eddca6d55957304612b88f4638ca7 # Numpy array interface, to support `numpy.asarray(vector) -> ndarray` def __array__(self, dtype=None): if dtype is None: return self.numpy() else: return self.numpy().astype(dtype, copy=False) # Wrap Numpy array in a vector or matrix when done, to support e.g. # `numpy.sin(vector) -> vector` or `numpy.greater(vector, 0) -> vector` def __array_wrap__(self, array): if array.ndim == 0: if array.dtype.kind == 'b': return bool(array) elif array.dtype.kind in ('i', 'u'): return int(array) elif array.dtype.kind == 'f': return float(array) elif array.dtype.kind == 'c': return complex(array) else: raise RuntimeError('bad scalar {!r}'.format(array)) elif array.ndim == 1: if array.dtype != numpy.float64: # DoubleVector stores double precision floats. array = array.astype('float64') return DoubleSubVector(array) elif array.ndim == 2: if array.dtype != numpy.float64: # DoubleMatrix stores double precision floats. array = array.astype('float64') return DoubleSubMatrix(array) else: raise RuntimeError('{} dimensional array cannot be converted to a ' 'Kaldi vector or matrix type'.format(array.ndim))
[docs]class DoubleVector(_DoubleVectorBase, _kaldi_vector.DoubleVector): """Double precision vector.""" def __init__(self, *args): """ DoubleVector(): Creates an empty vector. DoubleVector(size: int): Creates a new vector of given size and fills it with zeros. Args: size (int): Size of the new vector. DoubleVector(obj: vector_like): Creates a new vector with the elements in obj. Args: obj (vector_like): A vector, a 1-D numpy array, any object exposing a 1-D array interface, an object with an __array__ method returning a 1-D numpy array, or any sequence that can be interpreted as a vector. """ if len(args) > 1: raise TypeError("__init__() takes 1 to 2 positional arguments but " "{} were given".format(len(args) + 1)) super(DoubleVector, self).__init__() if len(args) == 0: return if isinstance(args[0], int): size = args[0] if size < 0: raise ValueError("size should non-negative") self.resize_(size) return obj = args[0] if not isinstance(obj, (_kaldi_vector.DoubleVectorBase, _kaldi_vector.VectorBase)): obj = numpy.array(obj, dtype=numpy.float64, copy=False, order='C') if obj.ndim != 1: raise TypeError("obj should be a 1-D vector like object.") obj = DoubleSubVector(obj) self.resize_(len(obj), _matrix_common.MatrixResizeType.UNDEFINED) self.copy_(obj) def __delitem__(self, index): """Removes an element from the vector.""" if not (0 <= index < self.dim): raise IndexError("index={} should be in the range [0,{})." .format(index, self.dim)) self._remove_element_(index)
[docs]class DoubleSubVector(_DoubleVectorBase, _matrix_ext.DoubleSubVector): """Double precision vector view.""" def __init__(self, obj, start=0, length=None): """Creates a new vector view from a vector like object. If possible the new vector view will share its data with the `obj`, i.e. no copy will be made. A copy will only be made if `obj.__array__` returns a copy, if `obj` is a sequence, or if a copy is needed to satisfy any of the other requirements (data type, order, etc.). Regardless of whether a copy is made or not, the new vector view will not own the memory buffer backing it, i.e. it will not support vector operations that reallocate memory. Args: obj (vector_like): A vector, a 1-D numpy array, any object exposing a 1-D array interface, an object whose __array__ method returns a 1-D numpy array, or any sequence that can be interpreted as a vector. start (int): The index of the view start. Defaults to 0. length (int): The length of the view. If None, it is set to len(obj) - start. Defaults to None. """ if not isinstance(obj, _kaldi_vector.DoubleVectorBase): obj = numpy.array(obj, dtype=numpy.float64, copy=False, order='C') if obj.ndim != 1: raise ValueError("obj should be a 1-D vector like object.") obj_len = len(obj) if not (0 <= start <= obj_len): raise IndexError("start={0} should be in the range [0,{1}] " "when len(obj)={1}.".format(start, obj_len)) max_len = obj_len - start if length is None: length = max_len if not (0 <= length <= max_len): raise IndexError("length={} should be in the range [0,{}] when " "start={} and len(obj)={}." .format(length, max_len, start, obj_len)) super(DoubleSubVector, self).__init__(obj, start, length)
class _DoubleMatrixBase(object): """Base class defining the additional API for single precision matrices. No constructor. """ def copy_(self, src, trans=_matrix_common.MatrixTransposeType.NO_TRANS): """Copies the elements from another matrix. Args: src (Matrix or SpMatrix or TpMatrix or DoubleMatrix or DoubleSpMatrix or DoubleTpMatrix or CompressedMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **src** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. Not active if input is a compressed matrix. Raises: ValueError: In case of size mismatch. """ if self.size() != src.size(): raise ValueError("Cannot copy matrix with dimensions {s[0]}x{s[1]} " "into matrix with dimensions {d[0]}x{d[1]}" .format(s=src.size(), d=self.size())) if isinstance(src, _kaldi_matrix.DoubleMatrixBase): self._copy_from_mat_(src, trans) elif isinstance(src, _sp_matrix.DoubleSpMatrix): _kaldi_matrix_ext._copy_from_sp_double(self, src) elif isinstance(src, _tp_matrix.DoubleTpMatrix): _kaldi_matrix_ext._copy_from_tp_double(self, src, trans) elif isinstance(src, _kaldi_matrix.MatrixBase): _kaldi_matrix_ext._copy_from_single_mat_double(self, src, trans) elif isinstance(src, _sp_matrix.SpMatrix): _kaldi_matrix_ext._copy_from_single_sp_double(self, src) elif isinstance(src, _tp_matrix.TpMatrix): _kaldi_matrix_ext._copy_from_single_tp_double(self, src, trans) elif isinstance(src, _compressed_matrix.CompressedMatrix): _kaldi_matrix_ext._copy_from_cmat_double(self, src) else: raise TypeError("input matrix type is not supported.") return self def clone(self): """Clones the matrix. The clone allocates new memory for its contents and supports matrix operations that reallocate memory, i.e. it is not a view. Returns: DoubleMatrix: A copy of the matrix. """ return DoubleMatrix(self) def size(self): """Returns the size of the matrix. Returns: A tuple (num_rows, num_cols) of integers. """ return self.num_rows, self.num_cols @property def shape(self): """Two element tuple representing the size of the matrix.""" return self.size() def approx_equal(self, other, tol=0.01): """Checks if matrices are approximately equal. Args: other (DoubleMatrix): The matrix to compare against. tol (float): The tolerance for the equality check. Defaults to ``0.01``. Returns: True if `self.size() == other.size()` and `||self-other|| <= tol*||self||`. False otherwise. """ if not isinstance(other, _kaldi_matrix.DoubleMatrixBase): return False if self.num_rows != other.num_rows or self.num_cols != other.num_cols: return False return self._approx_equal(other, tol) def __eq__(self, other): return self.approx_equal(other, 1e-16) def numpy(self): """Converts the matrix to a 2-D NumPy array. The NumPy array is a view into the matrix, i.e. no data is copied. Returns: numpy.ndarray: A NumPy array sharing data with this matrix. """ return _matrix_ext.double_matrix_to_numpy(self) @property def data(self): """Matrix data as a memoryview.""" return self.numpy().data def row_data(self, index): """Returns row data as a memoryview.""" return self[index].data def row(self, index): """Returns the given row as a new vector view. Args: index (int): The row index. Returns: DoubleSubVector: A vector view representing the given row. """ return self[index] def range(self, row_start, num_rows, col_start, num_cols): """Returns the given range of elements as a new matrix view. Args: row_start (int): The start row index. num_rows (int): The number of rows. col_start (int): The start column index. num_cols (int): The number of columns. Returns: DoubleSubMatrix: A matrix view representing the given range. """ return DoubleSubMatrix(self, row_start, num_rows, col_start, num_cols) def row_range(self, row_start, num_rows): """Returns the given range of rows as a new matrix view. Args: row_start (int): The start row index. num_rows (int): The number of rows. Returns: DoubleSubMatrix: A matrix view representing the given row range. """ return DoubleSubMatrix(self, row_start, num_rows, 0, self.num_cols) def col_range(self, col_start, num_cols): """Returns the given range of columns as a new matrix view. Args: col_start (int): The start column index. num_cols (int): The number of columns. Returns: DoubleSubMatrix: A matrix view representing the given column range. """ return DoubleSubMatrix(self, 0, self.num_rows, col_start, num_cols) def eig(self): """Computes eigendecomposition. Factorizes a square matrix into :math:`P\\ D\\ P^{-1}`. The relationship of :math:`D` to the eigenvalues is slightly complicated, due to the need for :math:`P` to be real. In the symmetric case, :math:`D` is diagonal and real, but in the non-symmetric case there may be complex-conjugate pairs of eigenvalues. In this case, for the equation :math:`y = P\\ D\\ P^{-1}` to hold, :math:`D` must actually be block diagonal, with 2x2 blocks corresponding to any such pairs. If a pair is :math:`\\lambda +- i\\mu`, :math:`D` will have a corresponding 2x2 block :math:`[\\lambda, \\mu; -\\mu, \\lambda]`. Note that if the matrix is not invertible, :math:`P` may not be invertible so in this case instead of the equation :math:`y = P\\ D\\ P^{-1}` holding, we have :math:`y\\ P = P\\ D`. Returns: 3-element tuple containing - **P** (:class:`DoubleMatrix`): The eigenvector matrix, where ith column corresponds to the ith eigenvector. - **r** (:class:`DoubleVector`): The vector with real components of the eigenvalues. - **i** (:class:`DoubleVector`): The vector with imaginary components of the eigenvalues. Raises: ValueError: If the matrix is not square. """ m, n = self.size() if m != n: raise ValueError("eig method cannot be called on a non-square " "matrix.") P = DoubleMatrix(n, n) r, i = DoubleVector(n), DoubleVector(n) self._eig(P, r, i) return P, r, i def svd(self, destructive=False): """Computes singular-value decomposition. Factorizes a matrix into :math:`U\\ diag(s)\\ V^T`. For non-square matrices, requires `self.num_rows >= self.num_cols`. Args: destructive (bool): Whether to use the destructive operation which avoids a copy but mutates self. Defaults to ``False``. Returns: 3-element tuple containing - **s** (:class:`DoubleVector`): The vector of singular values. - **U** (:class:`DoubleMatrix`): The left orthonormal matrix. - **Vt** (:class:`DoubleMatrix`): The right orthonormal matrix. Raises: ValueError: If `self.num_rows < self.num_cols`. Note: **Vt** in the output is already transposed. The singular values in **s** are not sorted. See Also: :meth:`singular_values` :meth:`sort_svd` """ m, n = self.size() if m < n: raise ValueError("svd for non-square matrices requires " "self.num_rows >= self.num_cols.") U, Vt = DoubleMatrix(m, n), DoubleMatrix(n, n) s = DoubleVector(n) if destructive: self._destructive_svd_(s, U, Vt) else: self._svd(s, U, Vt) return s, U, Vt def singular_values(self): """Computes singular values. Returns: DoubleVector: The vector of singular values. """ res = DoubleVector(self.num_cols) self._singular_values(res) return res def add_mat_(self, alpha, M, trans=_matrix_common.MatrixTransposeType.NO_TRANS): """Adds another matrix to this one. Performs the operation :math:`S = \\alpha\\ M + S`. Args: alpha (float): The scalar multiplier. M (DoubleMatrix or SpMatrix or DoubleSpMatrix): The input matrix. trans (MatrixTransposeType): Whether to use **M** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. Raises: RuntimeError: In case of size mismatch. """ if isinstance(M, _kaldi_matrix.DoubleMatrixBase): self._add_mat_(alpha, M, trans) elif isinstance(M, _sp_matrix.DoubleSpMatrix): _kaldi_matrix_ext.add_sp_double(self, alpha, M) elif isinstance(M, _sp_matrix.SpMatrix): _kaldi_matrix_ext.add_single_sp_double(self, alpha, M) else: raise TypeError("input matrix type is not supported.") return self def add_mat_mat_(self, A, B, transA=_matrix_common.MatrixTransposeType.NO_TRANS, transB=_matrix_common.MatrixTransposeType.NO_TRANS, alpha=1.0, beta=1.0, sparseA=False, sparseB=False): """Adds the product of given matrices. Performs the operation :math:`M = \\alpha\\ A\\ B + \\beta\\ M`. Args: A (DoubleMatrix or DoubleTpMatrix or DoubleSpMatrix): The first input matrix. B (DoubleMatrix or DoubleTpMatrix or DoubleSpMatrix): The second input matrix. transA (MatrixTransposeType): Whether to use **A** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. transB (MatrixTransposeType): Whether to use **B** or its transpose. Defaults to ``MatrixTransposeType.NO_TRANS``. alpha (float): The scalar multiplier for the product. Defaults to ``1.0``. beta (float): The scalar multiplier for the destination vector. Defaults to ``1.0``. sparseA (bool): Whether to use the algorithm that is faster when **A** is sparse. Defaults to ``False``. sparseA (bool): Whether to use the algorithm that is faster when **B** is sparse. Defaults to ``False``. Raises: RuntimeError: In case of size mismatch. TypeError: If matrices of given types can not be multiplied. """ if isinstance(A, _kaldi_matrix.DoubleMatrixBase): if isinstance(B, _kaldi_matrix.DoubleMatrixBase): if sparseA: self._add_smat_mat_(alpha, A, transA, B, transB, beta) elif sparseB: self._add_mat_smat_(alpha, A, transA, B, transB, beta) else: self._add_mat_mat_(alpha, A, transA, B, transB, beta) elif isinstance(B, _sp_matrix.DoubleSpMatrix): _kaldi_matrix_ext._add_mat_sp_double(self, alpha, A, transA, B, beta) elif isinstance(B, _tp_matrix.DoubleTpMatrix): _kaldi_matrix_ext._add_mat_tp_double(self, alpha, A, transA, B, transB, beta) else: raise TypeError("Cannot multiply matrix A with matrix B of " "type {}".format(type(B))) elif isinstance(A, _sp_matrix.DoubleSpMatrix): if isinstance(B, _kaldi_matrix.DoubleMatrixBase): _kaldi_matrix_ext._add_sp_mat_double(self, alpha, A, B, transB, beta) elif isinstance(B, _sp_matrix.DoubleSpMatrix): _kaldi_matrix_ext._add_sp_sp_double(self, alpha, A, transA, B, beta) else: raise TypeError("Cannot multiply symmetric matrix A with " "matrix B of type {}".format(type(B))) elif isinstance(A, _tp_matrix.DoubleSpMatrix): if isinstance(B, _kaldi_matrix.DoubleMatrixBase): _kaldi_matrix_ext._add_tp_mat_double(self, alpha, transA, B, transB, beta) elif isinstance(B, _tp_matrix.DoubleTpMatrix): _kaldi_matrix_ext._add_tp_tp_double(self, alpha, A, transA, B, transB, beta) else: raise TypeError("Cannot multiply triangular matrix A with " "matrix B of type {}".format(type(B))) return self def invert_(self): """Inverts the matrix. Returns: 2-element tuple containing - **log_det** (:class:`float`): The log determinant. - **det_sign** (:class:`float`): The sign of the determinant, 1 or -1. Raises: RuntimeError: If matrix is not square. """ return _kaldi_matrix_ext._invert_double(self) def copy_cols_(self, src, indices): """Copies columns from another matrix. Copies column `r` from column `indices[r]` of `src`. As a special case, if `indexes[i] == -1`, sets column `i` to zero. All elements of indices must be in `[-1, src.num_cols-1]`, and `src.num_rows` must equal `self.num_rows`. Args: src (DoubleMatrix): The input matrix. indices (List[int]): The list of column indices. """ _kaldi_matrix_ext._copy_cols_double(self, src, indices) return self def copy_rows_(self, src, indices): """Copies rows from another matrix. Copies row `r` from row `indices[r]` of `src`. As a special case, if `indexes[i] == -1`, sets row `i` to zero. All elements of indices must be in `[-1, src.num_rows-1]`, and `src.num_cols` must equal `self.num_cols`. Args: src (DoubleMatrix): The input matrix. indices (List[int]): The list of row indices. """ _kaldi_matrix_ext._copy_rows_double(self, src, indices) return self def add_cols_(self, src, indices): """Adds columns from another matrix. Adds column `indices[r]` of `src` to column `r`. As a special case, if `indexes[i] == -1`, skips column `i`. All elements of indices must be in `[-1, src.num_cols-1]`, and `src.num_rows` must equal `self.num_rows`. Args: src (DoubleMatrix): The input matrix. indices (List[int]): The list of column indices. """ _kaldi_matrix_ext._add_cols_double(self, src, indices) return self def add_rows_(self, src, indices, alpha=1.0): """Adds rows from another matrix. Scales row `indices[r]` of `src` with `alpha` and adds it to row `r`. As a special case, if `indexes[i] == -1`, skips row `i`. All elements of indices must be in `[-1, src.num_rows-1]`, and `src.num_cols` must equal `self.num_cols`. Args: src (DoubleMatrix): The input matrix. indices (List[int]): The list of row indices. alpha (float): The scalar multiplier. Defaults to `1.0`. """ _kaldi_matrix_ext._add_rows_double(self, alpha, src, indices) return self def __getitem__(self, index): """Implements self[index]. This operation is offloaded to NumPy. Hence, it supports all NumPy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. Slicing shares data with the source matrix when possible (see Caveats). Returns: - a float if the result of numpy indexing is a scalar - a DoubleSubVector if the result of numpy indexing is 1 dimensional - a DoubleSubMatrix if the result of numpy indexing is 2 dimensional Caveats: - Kaldi vector and matrix types do not support non-contiguous memory layouts for the last dimension, i.e. the stride for the last dimension should be the size of a float. If the result of numpy slicing operation has an unsupported stride value for the last dimension, the return value will not share any data with the source matrix, i.e. a copy will be made. Consider the following: >>> m = DoubleMatrix(3, 5) >>> s = m[:,0:4:2] # s does not share data with m >>> s[:] = m[:,1:4:2] # changing s will not change m Since the slicing operation requires a copy of the data to be made, the source matrix m will not be updated. On the other hand, the following assignment operation will work as expected since __setitem__ method does not create a new scalar/vector/matrix for representing the left hand side: >>> m[:,0:4:2] = m[:,1:4:2] .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ ret = self.numpy().__getitem__(index) if isinstance(ret, numpy.float64): return float(ret) elif isinstance(ret, numpy.ndarray): if ret.ndim == 2: return DoubleSubMatrix(ret) elif ret.ndim == 1: return DoubleSubVector(ret) else: raise ValueError("indexing operation returned a numpy array " " with {} dimensions.".format(ret.ndim)) raise TypeError("indexing operation returned an invalid type {}." .format(type(ret))) def __setitem__(self, index, value): """Implements self[index] = value. This operation is offloaded to NumPy. Hence, it supports all NumPy array indexing schemes: field access, basic slicing and advanced indexing. For details see `NumPy Array Indexing`_. .. _NumPy Array Indexing: https://docs.scipy.org/doc/numpy-1.13.0/reference/arrays.indexing.html """ self.numpy().__setitem__(index, value) def __contains__(self, value): """Implements value in self.""" return value in self.numpy() def __repr__(self): return str(self) def __str__(self): # All strings are unicode in Python 3, while we have to encode unicode # strings in Python2. If we can't, let python decide the best # characters to replace unicode characters with. # Below implementation was taken from # https://github.com/pytorch/pytorch/blob/master/torch/tensor.py if sys.version_info > (3,): return _str._matrix_str(self) else: if hasattr(sys.stdout, 'encoding'): return _str._matrix_str(self).encode( sys.stdout.encoding or 'UTF-8', 'replace') else: return _str._matrix_str(self).encode('UTF-8', 'replace') # Numpy array interface methods were adapted from PyTorch. # https://github.com/pytorch/pytorch/commit/c488a9e9bf9eddca6d55957304612b88f4638ca7 # Numpy array interface, to support `numpy.asarray(vector) -> ndarray` def __array__(self, dtype=None): if dtype is None: return self.numpy() else: return self.numpy().astype(dtype, copy=False) # Wrap Numpy array in a vector or matrix when done, to support e.g. # `numpy.sin(vector) -> vector` or `numpy.greater(vector, 0) -> vector` def __array_wrap__(self, array): if array.ndim == 0: if array.dtype.kind == 'b': return bool(array) elif array.dtype.kind in ('i', 'u'): return int(array) elif array.dtype.kind == 'f': return float(array) elif array.dtype.kind == 'c': return complex(array) else: raise RuntimeError('bad scalar {!r}'.format(array)) elif array.ndim == 1: if array.dtype != numpy.float64: # DoubleVector stores single precision floats. array = array.astype('float64') return DoubleSubVector(array) elif array.ndim == 2: if array.dtype != numpy.float64: # DoubleMatrix stores single precision floats. array = array.astype('float64') return DoubleSubMatrix(array) else: raise RuntimeError('{} dimensional array cannot be converted to a ' 'Kaldi vector or matrix type'.format(array.ndim))
[docs]class DoubleMatrix(_DoubleMatrixBase, _kaldi_matrix.DoubleMatrix): """Double precision matrix.""" def __init__(self, *args): """ DoubleMatrix(): Creates an empty matrix. DoubleMatrix(num_rows: int, num_cols: int): Creates a new matrix of given size and fills it with zeros. Args: num_rows (int): Number of rows of the new matrix. num_cols (int): Number of cols of the new matrix. DoubleMatrix(obj: matrix_like): Creates a new matrix with the elements in obj. Args: obj (matrix_like): A matrix, a 2-D numpy array, any object exposing a 2-D array interface, an object with an __array__ method returning a 2-D numpy array, or any (nested) sequence that can be interpreted as a matrix. """ if len(args) > 2: raise TypeError("__init__() takes 1 to 3 positional arguments but " "{} were given".format(len(args) + 1)) super(DoubleMatrix, self).__init__() if len(args) == 0: return if len(args) == 2: num_rows, num_cols = args if not (isinstance(num_rows, int) and isinstance(num_cols, int)): raise TypeError("num_rows and num_cols should be integers") if not (num_rows > 0 and num_cols > 0): if not (num_rows == 0 and num_cols == 0): raise IndexError("num_rows and num_cols should both be " "positive or they should both be 0.") self.resize_(num_rows, num_cols) return obj = args[0] if not isinstance(obj, (_kaldi_matrix.MatrixBase, _packed_matrix.PackedMatrix, _kaldi_matrix.DoubleMatrixBase, _packed_matrix.DoublePackedMatrix, _compressed_matrix.CompressedMatrix)): obj = numpy.array(obj, dtype=numpy.float64, copy=False, order='C') if obj.ndim != 2: raise ValueError("obj should be a 2-D matrix like object.") obj = DoubleSubMatrix(obj) self.resize_(obj.num_rows, obj.num_cols, _matrix_common.MatrixResizeType.UNDEFINED) self.copy_(obj) def __delitem__(self, index): """Removes a row from the matrix.""" if not (0 <= index < self.num_rows): raise IndexError("index={} should be in the range [0,{})." .format(index, self.num_rows)) self._remove_row_(index)
[docs]class DoubleSubMatrix(_DoubleMatrixBase, _matrix_ext.DoubleSubMatrix): """Double precision matrix view.""" def __init__(self, obj, row_start=0, num_rows=None, col_start=0, num_cols=None): """Creates a new matrix view from a matrix like object. If possible the new matrix view will share its data with the `obj`, i.e. no copy will be made. A copy will only be made if `obj.__array__` returns a copy, if `obj` is a sequence, or if a copy is needed to satisfy any of the other requirements (data type, order, etc.). Regardless of whether a copy is made or not, the new matrix view will not own the memory buffer backing it, i.e. it will not support matrix operations that reallocate memory. Args: obj (matrix_like): A matrix, a 2-D numpy array, any object exposing a 2-D array interface, an object with an __array__ method returning a 2-D numpy array, or any sequence that can be interpreted as a matrix. row_start (int): The start row index. Defaults to ``0``. num_rows (int): The number of rows. If ``None``, it is set to `self.num_rows - row_start`. Defaults to ``None``. col_start (int): The start column index. Defaults to ``0``. num_cols (int): The number of columns. If ``None``, it is set to `self.num_cols - col_start`. Defaults to ``None``. """ if not isinstance(obj, _kaldi_matrix.DoubleMatrixBase): obj = numpy.array(obj, dtype=numpy.float64, copy=False, order='C') if obj.ndim != 2: raise ValueError("obj should be a 2-D matrix like object.") obj_num_rows, obj_num_cols = obj.shape else: obj_num_rows, obj_num_cols = obj.num_rows, obj.num_cols if not (0 <= row_start <= obj_num_rows): raise IndexError("row_start={0} should be in the range [0,{1}] " "when obj.num_rows={1}." .format(row_start, obj_num_rows)) if not (0 <= col_start <= obj_num_cols): raise IndexError("col_start={0} should be in the range [0,{1}] " "when obj.num_cols={1}." .format(col_offset, obj_num_cols)) max_rows, max_cols = obj_num_rows - row_start, obj_num_cols - col_start if num_rows is None: num_rows = max_rows if num_cols is None: num_cols = max_cols if not (0 <= num_rows <= max_rows): raise IndexError("num_rows={} should be in the range [0,{}] " "when row_start={} and obj.num_rows={}." .format(num_rows, max_rows, row_start, obj_num_rows)) if not (0 <= num_cols <= max_cols): raise IndexError("num_cols={} should be in the range [0,{}] " "when col_start={} and obj.num_cols={}." .format(num_cols, max_cols, col_start, obj_num_cols)) if not (num_rows > 0 and num_cols > 0): if not (num_rows == 0 and num_cols == 0): raise IndexError("num_rows and num_cols should both be " "positive or they should both be 0.") super(DoubleSubMatrix, self).__init__(obj, row_start, num_rows, col_start, num_cols)
################################################################################ # vector/matrix wrappers ################################################################################ def _vector_wrapper(vector): """Constructs a new vector instance by swapping contents. This function is used for converting `kaldi.matrix._kaldi_vector.Vector` (or `kaldi.matrix._kaldi_vector.DoubleVector`) instances into `Vector` (or `DoubleVector`) instances without copying the contents. This is a destructive operation. Contents of the input vector are moved to the newly constructed vector by swapping data pointers. Args: vector (`Vector` or `DoubleVector`): The input vector. Returns: Vector or DoubleVector: The new vector instance. """ if isinstance(vector, _kaldi_vector.Vector): return Vector().swap_(vector) elif isinstance(vector, _kaldi_vector.DoubleVector): return DoubleVector().swap_(vector) else: raise TypeError("unrecognized input type") def _matrix_wrapper(matrix): """Constructs a new matrix instance by swapping contents. This function is used for converting `kaldi.matrix._kaldi_matrix.Matrix` (or `kaldi.matrix._kaldi_matrix.DoubleMatrix`) instances into `Matrix` (or `DoubleMatrix`) instances without copying the contents. This is a destructive operation. Contents of the input matrix are moved to the newly constructed matrix by swapping data pointers. Args: matrix (`Matrix` or `DoubleMatrix`): The input matrix. Returns: Matrix or DoubleMatrix: The new matrix instance. """ if isinstance(matrix, _kaldi_matrix.Matrix): return Matrix().swap_(matrix) elif isinstance(matrix, _kaldi_matrix.DoubleMatrix): return DoubleMatrix().swap_(matrix) else: raise TypeError("unrecognized input type") ################################################################################ _exclude_list = ['sys', 'numpy'] __all__ = [name for name in dir() if name[0] != '_' and not name.endswith('Base') and not name in _exclude_list]