Tuesday, May 28, 2019

Bayesian approach for missing value

Bayesian approach for missing value

Almost all projects I've been working on as a datascientist, I have no choice but to deal with missing value. I mean, real data is almost always involved with missing value. Sometimes imputation and deletion tend to be discussed in this context. However I'd like to share other way, 'probabilistic bayesian approach', in this article. Actually, I've read quite informative and intriguing following article.  

データに欠損がある場合の教師あり学習

Basically I implemented what is written in this blog post and I added the comparison of imputation with mean value on top of that. I will be glad if you enjoy this article.

0. Mathematic theory

First of all, we are gonna deal with binary classification. As a model, Logistic Regression is applied here. Let $y_n$ be target variable, $$p(y_n) = Ber(y_n|\sigma(w^Tx_n))$$
We apply prior distribution to $w$ as following, $$p(w) = N(w|0, \Sigma_w)$$
Then today we apply prior distribution for $x$, $$p(X) = \prod^{N}_{n=1}\prod^{D}_{d=1}N(x_{n,d}|0, \sigma^2_x)$$
Joint distribution would be, $$p(Y, w, X) = p(Y|w,X)p(w)p(X)$$ In this article, we are to deal with some missing value. Thus, we separate dateset into observation and missing value like, $X = {X_o, X_m}$. Posterior distribution would be,
$$p(w, X_m | Y, X_o) \propto p(Y|w, X_o, X_m) p(w)p(Xm)$$

1. Preperation of toy data

First of all, we might wanna prepare some toy data here. It's sampled according to model we set.

In [24]:
import pandas as pd
import numpy as np
import numpy.linalg as LA
from scipy.stats import bernoulli
from scipy.stats import norm
from scipy.stats import multivariate_normal
from scipy.stats import uniform
import matplotlib.pyplot as plt
%matplotlib inline
In [25]:
# Required function
def sigmoid(X):
        """
        Map the value with sigmoid function
        """
        return 1/(1 + (np.e ** (-X)))
In [18]:
# Dimention of data
D=2
# number of observation
N=30
# parameter for prior distribution
cov_w = 20 * np.eye(D)
mean_w = np.zeros(D)
sig_x = 1
mean_x = 0 
# Sample data along with generative model
# X is assumed independent each other
X_raw=norm.rvs(loc=mean_x, scale=sig_x, size=N*D,
               random_state=6).reshape(N,D)
w = multivariate_normal.rvs(mean=mean_w,
                            cov=cov_w,random_state=5)
labels = bernoulli.rvs(p=sigmoid(X_raw@w.reshape(-1,1)), 
                       random_state=7).ravel()

# plot toy data
for i, col in zip([0,1],['blue','red']):
    plt.scatter(X_raw[labels==i][:,0],X_raw[labels==i][:,1],color=col)

plt.title('Sample from model',fontsize=14)
plt.xlim(-3,3)
plt.ylim(-3,3)
plt.show()

2. Experimental data

Next, missing value is gonna be created. Here I set 0.5 as a ratio of missing value. Also I wanna compare the bayesian approach with imputation and reduction approach. Therefore respective data is also created here :)

In [21]:
# create missing data
X_miss = X_raw.copy()
ratio = 0.5
X_miss = np.array([
    np.nan if ratio > uniform.rvs(loc=0,scale=1) else x 
    for x in X_raw.ravel()
]).reshape(X_miss.shape)  

# delete data including missing data
X_del = X_miss[~np.any(np.isnan(X_miss),axis=1)]
labels_del = labels[~np.any(np.isnan(X_miss),axis=1)]

# impute with mean value
col0_mean = X_miss[:,0][~np.isnan(X_miss[:,0])].mean()
col1_mean = X_miss[:,1][~np.isnan(X_miss[:,1])].mean()
X_mean = X_miss.copy() 
X_mean[np.isnan(X_mean[:,0]),0] = col0_mean
X_mean[np.isnan(X_mean[:,1]),1] = col1_mean

3. Metropolis Hastings method

Since the posterior distribution of x and w is intractable, approximation or sampling method should be applied. Here I'm gonna apply "Metropolis Hastings method" which is one of MCMC algorithm.

In [26]:
def MH(X, y, D, sigma_x, cov_w, sig_prop, iter_num, burnin):
    """
    sig
    """
    N_miss = np.isnan(X).sum()
    # Initial sample
    if N_miss !=0:
        X_miss = multivariate_normal.rvs(
            mean=np.zeros(N_miss), 
            cov=(sig_prop**2) * np.eye(N_miss)
        )
    else:
        X_miss = 0
    
    W = multivariate_normal.rvs(
        mean=np.zeros(D),
        cov=(sig_prop ** 2) * np.eye(D)
    )
    # Array to store sampling
    w_sample = np.empty((iter_num,D))
    x_sample = np.empty((iter_num,N_miss))
    
    for i in range(iter_num):
        # Sampling W
        W_new = multivariate_normal.rvs(
            mean=np.zeros(D),
            cov=(sig_prop **2)* np.eye(D)
        ) 
        # Compute probability of rejection
        X_temp = insert_X(X,X_miss)
        a = np.exp(
            min(
                (ln_p_tilde(X_temp, y, W_new, cov_w, sigma_x,D) -\
                ln_p_tilde(X_temp, y, W, cov_w, sigma_x,D)),0
            )
        )
        # rejected of accepted
        W = W_new if uniform.rvs(loc=0,scale=1) < a else W
        w_sample[i] = W
        
        # Sampling X
        if N_miss !=0:
            X_miss_new = multivariate_normal.rvs(
                mean = np.zeros(N_miss),
                cov = (sig_prop**2) * np.eye(N_miss)
            )
            # Compute probability of rejection
            X_temp1 = insert_X(X, X_miss_new)
            X_temp2 = insert_X(X, X_miss)
            a = np.exp(
                min(
                    (ln_p_tilde(X_temp1, y, W, cov_w, sigma_x,D) -\
                    ln_p_tilde(X_temp2, y, W, cov_w, sigma_x,D)),0
                )
            )
            # rejected of accepted
            X_miss = X_miss_new if uniform.rvs(loc=0,scale=1) < a else X_miss
            x_sample[i] = X_miss
        
    # eliminate sample according to burunin
    w_sample = w_sample[burinin:,:]
    x_sample = x_sample[burinin:,:]
        
    return w_sample, x_sample
    
def insert_X(X,X_miss):
    """
    Set values of sample to missing values
    """
    X = X.copy()
    X[np.isnan(X)] = X_miss
    return X
    
def ln_p_tilde(X, y, w, cov_w, sigma_x,D):
    """
    Compute p tilda
    """
    # compute log likelihood + log prior w + log prior x
    return (y.reshape(-1,1) * np.log(sigmoid(X@w.reshape(-1,1))) + (1- y).reshape(-1,1) *
            np.log(1- sigmoid(X@w.reshape(-1,1)))).sum() -\
            0.5 * w.reshape(1,-1)@LA.inv(cov_w)@w.reshape(-1,1) -\
            0.5 * 1/(sigma_x **2)* ((X**2).sum()) # compute prior of x

4. Comparison

This is most fun part of this article! Now is the time to compare bayesian approach and imputation and deduction.

In [23]:
# parameter for Markov chain Monte carlo
# sigma of proposal distribution
sig_prop = 1
burinin = 1000
max_iter = 3000

titles = ['Full data(N={})'.format(X_raw.shape[0]), 
          'Imcomplete data(N={})'.format(X_miss.shape[0]),
          'Reduced data(N={})'.format(X_del.shape[0]),
         'Imputed with mean data(N={})'.format(X_mean.shape[0])]

sample_w_miss, sample_x_miss = MH(
    X=X_miss, y=labels, D=D, sigma_x=sig_x, cov_w=cov_w, 
    sig_prop=sig_prop, iter_num=max_iter, burnin=burinin
)

sample_w_nomiss, sample_x_nomiss = MH(
    X=X_raw, y=labels, D=D, sigma_x=sig_x, cov_w=cov_w, 
    sig_prop=sig_prop, iter_num=max_iter, burnin=burinin
)

sample_w_del, sample_x_del = MH(
    X=X_del, y=labels_del, D=D, sigma_x=sig_x, cov_w=cov_w, 
    sig_prop=sig_prop, iter_num=max_iter, burnin=burinin
)

sample_w_mean, sample_x_mean = MH(
    X=X_mean, y=labels, D=D, sigma_x=sig_x, cov_w=cov_w, 
    sig_prop=sig_prop, iter_num=max_iter, burnin=burinin
)

plt.figure(figsize=(15,14))
for num, (X, sample_w, sample_x) in enumerate(
    zip(
        [X_raw, X_miss, X_del, X_mean],
        [sample_w_nomiss, sample_w_miss, sample_w_del, sample_w_mean],
        [sample_x_nomiss, sample_x_miss, sample_x_del, sample_x_mean]
    )
):

    # Summarize
    idx_nan = np.where(np.any(np.isnan(X),axis=1))[0]
    X_exp = insert_X(X, sample_x.mean(axis=0))
    X_std = 0*X.copy()
    X_std = insert_X(X_std, np.std(sample_x, axis=0))

    # Plot prediction
    x = np.linspace(-2.5, 2.5,101)
    y = np.linspace(-2.5, 2.5,101)

    xx, yy = np.meshgrid(x,y)

    # compute prediction
    labels_pred = np.array([
        sigmoid(X=np.vstack([xx.ravel(), yy.ravel()]).T@w.reshape((-1,1)))
        for w in sample_w
    ]).mean(axis=0)
    
    plt.subplot(2,2,num+1)
    # plot prediction
    plt.colorbar(plt.contourf(xx,yy,labels_pred.reshape(xx.shape),cmap='coolwarm'))

    # plot points with missing value
    for i in idx_nan:
        if labels[i] == 1:
            plt.plot([X_exp[i,0] - X_std[i,0], X_exp[i,0] + X_std[i,0]],
                    [X_exp[i,1], X_exp[i,1]], color='red', alpha=0.4, linewidth=5)
            plt.plot([X_exp[i,0], X_exp[i,0]],
                    [X_exp[i,1] - X_std[i,1], X_exp[i,1] + X_std[i,1]], color='red', alpha=0.4, linewidth=5)
            plt.scatter(X_exp[i,0],X_exp[i,1], color='red', marker='x')
        else:
            plt.plot([X_exp[i,0] - X_std[i,0], X_exp[i,0] + X_std[i,0]],
                    [X_exp[i,1], X_exp[i,1]], color='blue', alpha=0.5, linewidth=5)
            plt.plot([X_exp[i,0], X_exp[i,0]],
                    [X_exp[i,1] - X_std[i,1], X_exp[i,1] + X_std[i,1]], color='blue', alpha=0.5, linewidth=5)
            plt.scatter(X_exp[i,0],X_exp[i,1], color='blue', marker='x')

    # plot points without missing value
    mask = np.where(~np.any(np.isnan(X),axis=1))[0]
    for i, col in zip([0,1],['blue','red']):
        plt.scatter(X[mask][labels[mask]==i][:,0],X[mask][labels[mask]==i][:,1],color=col)

    plt.xlim(-2.5,2.5)
    plt.ylim(-2.5,2.5)
    plt.title(titles[num], fontsize=14)
    
plt.show()

The one in upper left is the result of full data which mean there is no missing value. Then upper right one seems to have most similar result to the one in upper left compare to others:) The decision surface in reduced data is most different. Also imputed data with mean looks a little strange, it tend to be on a line (of course, it's imputed with mean value.) as if it's fabricated.

Wednesday, April 3, 2019

The reproductive property of poisson distribution

In this article, first of all I'll introduce "sum of discrete probability variable" in general. Subsequently, withusing that, I'll show "The reproductive property of poisson distribution" . At the last I'll confirm these property with Python implementation! I'll be glad if you get some kick out of it :)

0. Sum of probability variable in general

In the beginning, I'll write about "Sum of probability variablt in general". Let $X$ and $Y$ be discrete probability variable, $Z$ is sum of $X$ and $Y$ like $Z = X+Y$. The probability mass function of $Z$ would be,

$$\begin{eqnarray}p(Z = z) &=& p(X+Y = z) \\ &=& \sum_t p(X = t) p(Y = Z -t)\end{eqnarray}$$

Actually it's convolution of two distribution. For all value on $X+Y=Z$, the joint distribution is summed up as following.

1. Sum of probability variable in poisson distribution

Let's say X and Y follows poisson distribution with mean of $\lambda$ and $\mu$ respectibely. Then probability mass function $p(Z)$ ($Z = X+Y$) can be derive as below.

Interestingly, probability mass function of $Z$ is poisson distribution again! On top of that, mean which govern poisson distribution is $\lambda + \mu$ :) Sometimes it's called "The reproductive property of poisson distribution".

2. Implementation with Python

Thankfully, we have computer in this era. We can confirm the reproductive property of poisson distribution and the parameter of distribution of Z. Let's say there are two poisson distribution, one has parameter of 2, the other has 5. They looks like the following.

In [1]:
from scipy.stats import poisson
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
In [2]:
lam = 2 
mu = 5

x = np.arange(0,20,1)

# plot two different distriobution
plt.figure(figsize=(13,5))
plt.subplot(1,2,1)
plt.bar(x, poisson.pmf(x,mu=lam))
plt.title('Poisson distribution with lambda = {}'.format(lam))
plt.subplot(1,2,2)
plt.bar(x, poisson.pmf(x,mu=mu))
plt.title('Poisson distribution with lambda = {}'.format(mu))
plt.show()

Now is the time to compute convolution of two different poisson distribution. Then we can compare our expected result and poisson distribution. It's amusing and enjoyable :)

In [3]:
# numpy to store the probability of Z
z_prob = np.empty(x.shape[0])

for i, z in enumerate(range(x.shape[0])):
    
    # Compute convolution then sum it up!
    z_prob[i] = np.array(
            [
                poisson.pmf(t,mu=lam) * poisson.pmf(z - t,mu=mu) 
                for t 
                in range(z+1)
            ]
        ).sum()

# plot the result of  convolution
plt.figure(figsize=(13,5))
# convolution
plt.subplot(1,2,1)
plt.bar(x,z_prob)
plt.title('Convolution of poisson with parameter = 2 and 5')
# poisson with  mu = lam + mu = 7
plt.subplot(1,2,2)
plt.bar(x,poisson.pmf(x,mu=7))
plt.title('Poisson distribution with lambda = 7')
plt.show()

Awesome! As we expected, they look same :)

Sunday, March 17, 2019

Linear combination and sum of probability variable

I've been reading following book this weekend.
ガウス過程と機会学習
Then I was just wondering what the probability distribution of linear combination of probability variable looks like. The followings are the log I checked it by hand.

1. Linear combination of probability variable


2. Sum of probability variable

Thursday, February 28, 2019

Introduction to LSI (Latent Semantic Indexing)

In this article, I will share about basics of LSI (Latent Semantic Indexing). I will be glad if you enjoy this article :)

0. Data

Let's say we have data as bag of words representation as following. We can tell doc_1 and doc_2 are somehow similar, doc_3 and doc_4 have similar character. Actually, the former topic is "tennis" and the latter is "soccer".

In [1]:
import numpy as np
import pandas as pd
import numpy.linalg as LA
np.set_printoptions(precision=2)
In [2]:
bow = np.array([[1,1,0,0,0],
                         [1,0,1,0,0],
                         [0,0,0,1,1],
                         [0,0,0,0,1]])
pd.DataFrame(bow,
             columns=['heat','Osaka','Naomi','Japan','PK'],
             index=['doc_1','doc_2','doc_3','doc_4'])
Out[2]:
heat Osaka Naomi Japan PK
doc_1 1 1 0 0 0
doc_2 1 0 1 0 0
doc_3 0 0 0 1 1
doc_4 0 0 0 0 1

1. Singular value decomposition

Apply singlar value decomposition.

In [3]:
U, sigma, V = LA.svd(bow,full_matrices=True)
print('Shape of U :', U.shape)
print('U\n',U)
print('Shape of sigma :',sigma.shape)
print('sigma\n',sigma)
print('Shape of V :',V.T.shape)
print('V\n',V.T)
Shape of U : (4, 4)
U
 [[ 0.71  0.   -0.71  0.  ]
 [ 0.71  0.    0.71  0.  ]
 [ 0.    0.85  0.   -0.53]
 [ 0.    0.53  0.    0.85]]
Shape of sigma : (4,)
sigma
 [1.73 1.62 1.   0.62]
Shape of V : (5, 5)
V
 [[ 8.16e-01  6.41e-17  3.33e-16  4.81e-17  5.77e-01]
 [ 4.08e-01 -6.41e-17 -7.07e-01 -4.81e-17 -5.77e-01]
 [ 4.08e-01 -6.41e-17  7.07e-01 -4.81e-17 -5.77e-01]
 [-0.00e+00  5.26e-01  0.00e+00 -8.51e-01  4.44e-16]
 [ 0.00e+00  8.51e-01  0.00e+00  5.26e-01 -1.11e-16]]

Actually, the number of singular value is only 4, therefore 5th colum of $V$ doesn't do anything. We can confirm by composing original bow without 5th column.

In [4]:
U @ np.diag(sigma) @ V[0:4]
Out[4]:
array([[ 1.00e+00,  1.00e+00, -2.90e-16,  0.00e+00,  0.00e+00],
       [ 1.00e+00, -6.84e-17,  1.00e+00,  0.00e+00,  0.00e+00],
       [ 7.26e-17, -7.26e-17, -7.26e-17,  1.00e+00,  1.00e+00],
       [ 7.98e-17, -7.98e-17, -7.98e-17, -1.02e-16,  1.00e+00]])

So, let's say $W = U\Sigma$, $H = V^T$, we can decompose bow as $X = WH$.

2. Analysis

In [5]:
W = U @ np.diag(sigma)
H = V[:-1]
print('W = \n',W)
print('H = \n',H)
W = 
 [[ 1.22  0.   -0.71  0.  ]
 [ 1.22  0.    0.71  0.  ]
 [ 0.    1.38  0.   -0.32]
 [ 0.    0.85  0.    0.53]]
H = 
 [[ 8.16e-01  4.08e-01  4.08e-01 -0.00e+00  0.00e+00]
 [ 6.41e-17 -6.41e-17 -6.41e-17  5.26e-01  8.51e-01]
 [ 3.33e-16 -7.07e-01  7.07e-01  0.00e+00  0.00e+00]
 [ 4.81e-17 -4.81e-17 -4.81e-17 -8.51e-01  5.26e-01]]

Let $X$ be the matrix represents original bow matrix. The row vector $x_i$ of $X$ can be depicted as below.

$$x_i = \Sigma_j w_{ij}h_j \tag{1}$$

where $h_j$ is row vector of $H$. From equation (1), we can see each row vector of $H$ as orthonormal basis. And row vector of $W$ are seen as coefficient. Now we can take a look at the result of matrix decomposition. It seems that the first row vector of $V$ represent the "tennis" and second row vector imply "soccer". Hence first row vector of $W$, which represents coefficient of first row vector of $X$, has large number on $w_{11}$, in contrast, $w_{12}$ has ZERO coefficient. As for third row vector of $W$, which represents coefficient of third row vector of $X$, has ZERO on $w_{31}$ whereas $w_{32} has positive number.

3. Rank Deduction

For the next, let me try to deduct rank of matrix.

In [25]:
print('original rank :', LA.matrix_rank(bow))
sigma_rank3 = np.diag(np.concatenate([sigma[:3],np.array([0])]))
print('new sigma (1 sigular value deleted):\n',sigma_rank3)
print('Compose bow with 1 rank deduction\n',U@sigma_rank3@V[0:4] )
sigma_rank2 = np.diag(np.concatenate([sigma[:2],np.array([0,0])]))
print('new sigma (2 sigular value deleted):\n',sigma_rank2)
print('Compose bow with 2 rank deduction\n',U@sigma_rank2@V[0:4] )
original rank : 4
new sigma (1 sigular value deleted):
 [[1.73 0.   0.   0.  ]
 [0.   1.62 0.   0.  ]
 [0.   0.   1.   0.  ]
 [0.   0.   0.   0.  ]]
Compose bow with 1 rank deduction
 [[ 1.00e+00  1.00e+00 -2.90e-16  0.00e+00  0.00e+00]
 [ 1.00e+00 -6.84e-17  1.00e+00  0.00e+00  0.00e+00]
 [ 8.82e-17 -8.82e-17 -8.82e-17  7.24e-01  1.17e+00]
 [ 5.45e-17 -5.45e-17 -5.45e-17  4.47e-01  7.24e-01]]
new sigma (2 sigular value deleted):
 [[1.73 0.   0.   0.  ]
 [0.   1.62 0.   0.  ]
 [0.   0.   0.   0.  ]
 [0.   0.   0.   0.  ]]
Compose bow with 2 rank deduction
 [[ 1.00e+00  5.00e-01  5.00e-01  0.00e+00  0.00e+00]
 [ 1.00e+00  5.00e-01  5.00e-01  0.00e+00  0.00e+00]
 [ 8.82e-17 -8.82e-17 -8.82e-17  7.24e-01  1.17e+00]
 [ 5.45e-17 -5.45e-17 -5.45e-17  4.47e-01  7.24e-01]]

So far, I ducted 2 rank from original matrix. As for last matrix, interestingly first row and second row became same! Bow representation was abstracted by deducting rank of matrix, hence difference of "Osaka" and "Naomi" seemed as similar word. As for "soccer" topic, there was evident difference in original sentence of "Japan", however the last matrix has more vague distinction. These mean somehow we extracte similar words and find topic from bow:) Now we can deduct rank to 1 as following.

In [28]:
sigma_rank1 = np.diag(np.concatenate([sigma[:1],np.array([0,0,0])]))
print('new sigma (1 sigular value deleted):\n',sigma_rank1)
print('Compose bow with 1 rank deduction\n',U@sigma_rank1@V[0:4] )
new sigma (1 sigular value deleted):
 [[1.73 0.   0.   0.  ]
 [0.   0.   0.   0.  ]
 [0.   0.   0.   0.  ]
 [0.   0.   0.   0.  ]]
Compose bow with 1 rank deduction
 [[1.  0.5 0.5 0.  0. ]
 [1.  0.5 0.5 0.  0. ]
 [0.  0.  0.  0.  0. ]
 [0.  0.  0.  0.  0. ]]

Just as you expected, first and second row vector became same, and third and forth vector are also identical. It implies there is two topic amond 4 sentence.

4. Dimension Deduction

Sometimes you might wanna reduce dimension instead of rank, in that situation, you can multiply matrix of basis from left side as following.

In [37]:
print('original bow :\n', bow)
print('1 dimention deduction : \n',bow@V[:4,:].T)
print('2 dimention deduction : \n',bow@V[:3,:].T)
print('3 dimention deduction : \n',bow@V[:2,:].T)
print('4 dimention deduction : \n',bow@V[:1,:].T)
original bow :
 [[1 1 0 0 0]
 [1 0 1 0 0]
 [0 0 0 1 1]
 [0 0 0 0 1]]
1 dimention deduction : 
 [[ 1.22e+00  0.00e+00 -7.07e-01  0.00e+00]
 [ 1.22e+00  1.23e-32  7.07e-01  1.23e-32]
 [ 0.00e+00  1.38e+00  0.00e+00 -3.25e-01]
 [ 0.00e+00  8.51e-01  0.00e+00  5.26e-01]]
2 dimention deduction : 
 [[ 1.22e+00  0.00e+00 -7.07e-01]
 [ 1.22e+00  1.23e-32  7.07e-01]
 [ 0.00e+00  1.38e+00  0.00e+00]
 [ 0.00e+00  8.51e-01  0.00e+00]]
3 dimention deduction : 
 [[1.22e+00 0.00e+00]
 [1.22e+00 1.23e-32]
 [0.00e+00 1.38e+00]
 [0.00e+00 8.51e-01]]
4 dimention deduction : 
 [[1.22]
 [1.22]
 [0.  ]
 [0.  ]]