Source code for kaldi.matrix

"""
PyKaldi defines the following CPU vector/matrix types:

================= ====================== ============================ =========================
Type              32-bit floating point  64-bit floating point        Other
================= ====================== ============================ =========================
Dense Matrix      :class:`.Matrix`       :class:`.DoubleMatrix`       :class:`.CompressedMatrix`
Dense Vector      :class:`.Vector`       :class:`.DoubleVector`
Symmetric Matrix  :class:`.SpMatrix`     :class:`.DoubleSpMatrix`
Triangular Matrix :class:`.TpMatrix`     :class:`.DoubleTpMatrix`
Sparse Matrix     :class:`.SparseMatrix` :class:`.DoubleSparseMatrix`
Sparse Vector     :class:`.SparseVector` :class:`.DoubleSparseMatrix`
================= ====================== ============================ =========================

In addition, there is a :class:`.GeneralMatrix` type which is a wrapper around
:class:`.Matrix`, :class:`.SparseMatrix` and :class:`.CompressedMatrix` types.

The dense :class:`Vector`/:class:`Matrix` types come in two flavors.

:class:`Vector`/:class:`Matrix` instances own the memory buffers backing them.
Instantiating a new :class:`Vector`/:class:`Matrix` object allocates new memory
for storing the elements. They support destructive operations that reallocate
memory.

:class:`SubVector`/:class:`SubMatrix` instances, on the other hand, share the
memory buffers owned by other objects. Instantiating a new
:class:`SubVector`/:class:`SubMatrix` object does not allocate new memory. Since
they provide views into other existing objects, they do not support destructive
operations that reallocate memory. Other than this caveat, they are equivalent
to :class:`Vector`/:class:`Matrix` instances for all practical purposes. Almost
any function or method accepting a :class:`Vector`/:class:`Matrix` instance can
instead be passed a :class:`SubVector`/:class:`SubMatrix` instance.

.. note::
    All mutating vector/matrix methods are marked with an underscore suffix.
    These methods overwrite the contents and return the resulting object,
    unless they have other return values, to support method chaining.
"""

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 ._str import set_printoptions


################################################################################
# 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]