# Matrix Multiplication

Author: Yoseph K. Soenggoro


```python
from random import random
from itertools import product
import numpy as np
```


```python
# Check you NumPy Version (I used 1.16.4).
# If the program is incompatible with your NumPy version, use pip or conda to set the appropriate version
np.__version__
```




    '1.16.4'




```python
# Choose the value of n, the dimension for Matrix X and Y
n = 3

# Choose d as the range of random value for Matrix X and Y.
# By choosing value d, the element of Matrix X and Y will be any real number between 0 and d, but never d.
d = 10
```

Before starting to multiply any two matrices, first define two different matrices $X$ and $Y$ using the `random` library.


```python
# Define Matrix X and Matrix Y
X = []
Y = []

for i in range(0, n):
    x_row = []
    for j in range(0, n):
        x_val = random() * d
        x_row.append(x_val)
    X.append(x_row)

for i in range(0, n):
    y_row = []
    for j in range(0, n):
        y_val = random() * d
        y_row.append(y_val)
    Y.append(y_row)
```


```python
# Function to print the matrices
def print_matrix(X):
    matrix_string = ''
    for i, j in product(range(0, n), range(0, n)):
        matrix_string += f'{X[i][j]}' + ('\t' if j != n - 1 else '\n')
    print(matrix_string)
```


```python
# Print X to Check
print_matrix(X)
```

    1.411243489014371	4.823307914767269	2.9619965649396427
    8.632221076600207	6.909318166288182	4.297076325685831
    6.741265901114342	9.161915445688102	6.166996253979611
    



```python
# Print Y to Check
print_matrix(Y)
```

    7.627644148989169	4.412768013974525	4.55820479886072
    7.864788107651906	2.491004876229417	1.2387049762153457
    1.6601811042693615	6.587244192044375	9.966139995991005
    


### Matrix Multiplication Formula (Linear Algebra)

Given a $n \times n$ matrices $X$ and $Y$, as follows:

\begin{align}
X =
\begin{bmatrix}
    x_{1, 1} & x_{1, 2} & \dots & x_{1, n} \\
    x_{2, 1} & x_{2, 2} & \dots & x_{2, n} \\
    \vdots & \vdots & \ddots & \vdots \\
    x_{n, 1} & x_{n, 2} & \dots & x_{n, n}
\end{bmatrix}
, \quad
Y =
\begin{bmatrix}
    y_{1, 1} & y_{1, 2} & \dots & y_{1, n} \\
    y_{2, 1} & y_{2, 2} & \dots & y_{2, n} \\
    \vdots & \vdots & \ddots & \vdots \\
    y_{n, 1} & y_{n, 2} & \dots & y_{n, n}
\end{bmatrix}
\end{align}

then the multiplication is defined by the following formula:
\begin{align}
    X \cdot Y = \left[\sum_{k = 1}^n x_{i, k} \cdot y_{j, k}\right]_{i, j = 1}^n
\end{align}

### Implementation \#1: Functional Paradigm

The simplest way to implement Matrix Multiplication is by using modular functions, that can be used and reused multiple times within a program. Given the formula above, the Python implementation will be as follows.


```python
# Function to implement Matrix Multiplication of Matrix X and Y
def matrix_mul(X, Y):
    Z = []

    for i in range(0, n):
        z_row = []
        for j in range(0, n):
            z_val = 0
            for k in range(0, n):
                z_val += X[i][k] * Y[k][j]
            z_row.append(z_val)
        Z.append(z_row)
    return Z
```

For the multiplication between $X$ and $Y$, the result will be kept in variable $Z$.


```python
Z = matrix_mul(X, Y)
print_matrix(Z)
```

    53.61620859740801	37.75376833274768	41.92706479366027
    127.31775885342397	83.60902536916785	90.7313025662656
    133.714831698932	93.1935288493318	103.53812885700651
    


### Check Validity on Matrix Multiplication Function

Despite having a working matrix multiplication implementation in functional form, we still have no idea whether the result from our implementation is right or wrong. Therefore, one method to validate the result will be doing a comparison with `NumPy`'s implementation of `matmul` API.


```python
# Function to compare the Matrix Multiplication Function to NumPy's matmul
def check_matrix_mul(X, Y):
    print('Starting Validation Process...\n\n\n')
    x = np.array(X)
    y = np.array(Y)
    z = np.matmul(x, y)
    Z = matrix_mul(X, Y)
    for i, j in product(range(0, n), range(0, n)):      
        print(f'Checking index {(i, j)}... \t\t\t {round(z[i][j], 2) == round(Z[i][j], 2)}')
    
    print('\n')
    print('Validation Process Completed')
```


```python
a = check_matrix_mul(X, Y)
```

    Starting Validation Process...
    
    
    
    Checking index (0, 0)... 			 True
    Checking index (0, 1)... 			 True
    Checking index (0, 2)... 			 True
    Checking index (1, 0)... 			 True
    Checking index (1, 1)... 			 True
    Checking index (1, 2)... 			 True
    Checking index (2, 0)... 			 True
    Checking index (2, 1)... 			 True
    Checking index (2, 2)... 			 True
    
    
    Validation Process Completed


Since after checking all the results are True, then it can be confirmed that the implentation works sucessfully.

### Implementation \#2: Object-Oriented Paradigm

Another paradigm that can be used is OOP or Object-Oriented Programming, which represents a program as a set of Objects with various fields and methods to interact with the defined Object. In this case, first defined a generalized form of matrices, which is known as Tensors. The implementation of `Tensor` will be as follows: 


```python
class Tensor:
    def __init__(self, X):
        validation = self.__checking_validity(X)
        
        self.__dim = 2
        self.tensor = X if validation else []
        self.__dimension = self.__get_dimension_private(X) if validation else -1
    
    def __get_dimension_private(self, X):
        if not check_child(X):
            return 1
        else:
            # Check whether the size of each child are the same
            for i in range(0, len(X)):
                if not check_child(X[i]):
                    return self.__dim
                else:
                    get_dimension(X[i])
            self.__dim += 1
            return self.__dim
    
    def __checking_validity(self, X):
        self.__dim = 2
        valid = True
        if not check_child(X):
            return valid
        else:
            dim_0 = get_dimension(X[0])
            # Check whether the size of each child are the same
            for i in range(1, len(X)):
                self.__dim = 2
                if get_dimension(X[i]) != dim_0:
                    valid &= False
                    break
        return valid
    
    # Getting the Value of Tensor Rank/Dimension (Not to be confused with Matrix Dimension)
    def get_dimension(self):
        return self.__dimension
```

Since Tensors are generalized form of matrices, it implies that it is possible to define `Matrix` class as a child class of `Tensor` with additional methods (some overrides the `Tensor`'s original methods). For operators, I only managed to override the multiplication operator for the sake of implementing Matrix Multiplication. Thus, other operator such as `+`, `-`, `/`, and others will not be available for the current implementation.


```python
class Matrix(Tensor):
    def __init__(self, X):
        super().__init__(X)
        self.__matrix_string = ''
        
    def __str__(self):
        return self.__matrix_string if self.__check_matrix_validation() else ''
            
    # Check whether the given input X is a valid Matrix
    def __check_matrix_validation(self):
        valid = True
        try:
            for i, j in product(range(0, n), range(0, n)):
                self.__matrix_string += f'{self.tensor[i][j]}' + ('\t' if j != n - 1 else '\n')
        except:
            valid = False
            print('Matrix is Invalid. Create New Instance with appropriate inputs.')
            
        return valid
            
    # Get Matrix Dimension: Number of Columns and Rows
    def get_dimension(self):
        print(f'Matrix Dimension: ({len(self.tensor)}, {len(self.tensor[0])})' if self.__check_matrix_validation() else -1)
        return [len(self.tensor), len(self.tensor[0])]
    
    # Overriding Multiplication Operator for Matrix Multiplication
    # and Integer-Matrix Multiplication
    def __mul__(self, other):
        if isinstance(other, Matrix):
            Z = []
            for i in range(0, n):
                z_row = []
                for j in range(0, n):
                    z_val = 0
                    for k in range(0, n):
                        z_val += self.tensor[i][k] * other.tensor[k][j]
                    z_row.append(z_val)
                Z.append(z_row)
            return Matrix(Z)
        elif isinstance(other, int):
            Z = []
            for i in range(0, n):
                z_row = []
                for j in range(0, n):
                    z_row.append(self.tensor[i][j] * other)
                Z.append(z_row)
            return Matrix(Z)
        else:
            return NotImplemented
    
    # Overriding Reverse Multiplication to support Matrix-Integer Multiplication
    def __rmul__(self, other):
        if isinstance(other, int):
            Z = []
            for i in range(0, n):
                z_row = []
                for j in range(0, n):
                    z_row.append(self.tensor[i][j] * other)
                Z.append(z_row)
            return Matrix(Z)
        else:
            return NotImplemented
```


```python
# Transform X and Y to Matrix Object
x_obj = Matrix(X)
y_obj = Matrix(Y)

# Implement Matrix Multiplication as follows
z_obj = x_obj * y_obj
print(z_obj)
```

    53.61620859740801	37.75376833274768	41.92706479366027
    127.31775885342397	83.60902536916785	90.7313025662656
    133.714831698932	93.1935288493318	103.53812885700651
    


### Check Validity on Matrix Multiplication using OOP

Similar to the previous section, we still have no idea whether the result from our implementation is right or wrong. Hence, validation is highly important. Therefore, one method to validate the result will be again doing a comparison with `NumPy`'s implementation of `matmul` API.


```python
# Function to compare the Matrix Multiplication Function to Numpy's matmul
def check_matrix_mul_oop(X, Y):
    print('Starting Validation Process...\n\n\n')
    x = np.array(X)
    y = np.array(Y)
    z = np.matmul(x, y)
    Z = Matrix(X) * Matrix(Y)
    for i, j in product(range(0, n), range(0, n)):      
        print(f'Checking index {(i, j)}... \t\t\t {round(z[i][j], 2) == round(Z.tensor[i][j], 2)}')
    
    print('\n')
    print('Validation Process Completed')
```


```python
a = check_matrix_mul_oop(X, Y)
```

    Starting Validation Process...
    
    
    
    Checking index (0, 0)... 			 True
    Checking index (0, 1)... 			 True
    Checking index (0, 2)... 			 True
    Checking index (1, 0)... 			 True
    Checking index (1, 1)... 			 True
    Checking index (1, 2)... 			 True
    Checking index (2, 0)... 			 True
    Checking index (2, 1)... 			 True
    Checking index (2, 2)... 			 True
    
    
    Validation Process Completed


Since after checking all the results are True, then it can be confirmed that the implentation works sucessfully.

# Python Libraries

- [NumPy](https://numpy.org/)

4a8e2e051eba61b37a6c9436eb970d50b3432595
ipynb
Jupyter Notebook
3-applications/Matrix Multiplication.ipynb
YosephKS/Introduction-to-Programming
d92070084d611b2b68bc56f20a80095aba099a94
MIT
3-applications/Matrix Multiplication.ipynb
YosephKS/Introduction-to-Programming
d92070084d611b2b68bc56f20a80095aba099a94
MIT
3-applications/Matrix Multiplication.ipynb
YosephKS/Introduction-to-Programming
d92070084d611b2b68bc56f20a80095aba099a94
MIT
Qwen/Qwen-72B
1. YES
2. YES