PCA na mĂŁo e no Python

PCA, ou principal component analysis é uma técnica para reduzir a dimensão de um conjunto de dados preservando, de certa forma, suas propriedades. Executar essa técnica na mão ou sem ajuda de funçÔes prontas é um bom exercício para aprender a fundo o que estå por trås desse processo. Nesse tutorial faremos um exemplo de PCA na mão, sempre conferindo as contas com Python e depois faremos o mesmo exemplo usando a biblioteca Scikit-learn.

Photo by William Iven on Unsplash

Pequeno exemplo de PCA para fazer na mĂŁo

Como utilizaremos o Python para conferir nossas contas, importaremos as bibliotecas necessĂĄrias.

In [1]:
# bibliotecas
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

Utilizaremos as seguintes matrizes para esse trabalho:

$$X = \left(\begin{array}[cc]\\ 1 & 2\\ 3 & 4\\ 1 & 3\\ 2 & 4\\ 2 & 3\\ 1 & 4 \end{array}\right)\ \ y = \left(\begin{array}[c] \\ 0\\ 1\\ 0\\ 1\\ 1\\ 1\end{array}\right)$$

A matriz $X$ representa as features, enquanto $y$ representa o target. Declararemos essas matrizes no Python:

In [2]:
# matrizes no Python
df = pd.DataFrame({
    'var1':[1,3,1,2,2,1],
    'var2':[2,4,3,4,3,4],
    'target': [0,1,0,1,1,1]
})
X = df.drop('target',1)
y = df['target']

Precisaremos das médias de cada coluna de $X$. Para o cålculo da média da coluna i basta fazer a seguinte conta:

$$\frac{\sum_j X_{ji}}{n}$$

com $n$ sendo o nĂșmero de linhas da matriz $X$. Podemos conferir as contas no Python usando a biblioteca numpy:

In [3]:
# média por coluna de X
mean_vec = np.mean(X, axis=0)
print(mean_vec)
var1    1.666667
var2    3.333333
dtype: float64

Nossa prĂłpria tarefa Ă© determinar a matriz de covariĂąncia. Para isso, vamos usar uma matriz M auxiliar. Para determinar M subtraia a mĂ©dia da coluna i da coluna respectiva. Como no nosso exemplo todas as entradas de M estĂŁo multiplicadas por $\frac{1}{3}$, para ficar mais elegante, colocamos $\frac{1}{3}$ em evidĂȘncia.

$$M = \frac{1}{3}\left(\begin{array}[ll]\\ -2 & -4\\ \ \ 4 & \ \ 2\\ -2 & -1\\ \ \ 1 & \ \ 2\\ \ \ 1 & -1\\ -2 & \ \ 2 \end{array}\right)$$

Para realizar a mesma conta no Python, use o cĂłdigo a seguir:

In [4]:
# subtraindo a média da respectiva coluna de X
M = X - mean_vec

A matriz de covariĂąncia Ă© de $X$ Ă© dada por:

$$C = \frac{1}{n-1} M^T M$$

onde $n$ Ă© o nĂșmero de linhas de $X$.

Logo, $$C = \frac{1}{3}\left(\begin{array}[cc] \\ 2 & 1 \\ 1 & 2\end{array}\right)$$

No Python:

In [5]:
# calculando matriz de covariĂąncia
C = M.T.dot(M) / (X.shape[0]-1)
In [6]:
# imprimindo C
C
Out[6]:
var1 var2
var1 0.666667 0.333333
var2 0.333333 0.666667

PrĂłximo passo Ă© determinar auto-valores e auto-vetores da matriz de covariĂąncia C. Os auto-valores $\lambda$ de C sĂŁo raizes de $$p_C(x) = \det(C - xI)$$ onde I Ă© a matriz identidade.

Cada auto-valor $\lambda$ tem auto-vetores associados. Apesar de auto-vetores nunca serem nulos, se incluĂ­rmos o vetor nulo no conjunto dos auto-vetores associados e chamarmos esse novo conjunto de $V_{\lambda}$, temos que $V_{\lambda}$ Ă© o conjunto solução do seguinte sistema: $$(C - \lambda I)X = 0$$ Tomaremos geradores unitĂĄrios de $V_{\lambda_1}$ e de $V_{\lambda_2}$, para $\lambda_1=1$ e $\lambda_2 = \frac{1}{3}$, a saber, tomaremos os seguintes auto-vetores: $$\left\{ \left(\frac{\sqrt{2}}{2}, -\frac{\sqrt{2}}{2}\right), \left(\frac{\sqrt{2}}{2},\frac{\sqrt{2}}{2}\right)\right\}.$$ Essa escolha nĂŁo Ă© Ășnica e isso pode dar pequenas diferenças nos resultados finais.

No Python:

In [7]:
# determinando auto-valores e auto-vetores
autovalores, autovetores = np.linalg.eig(C)

# imprimindo auto-valores de C
print("Auto-valores:")
print(autovalores)
print()

# imprimindo autovetores
print("Auto-vetores:")
print(autovetores)
Auto-valores:
[1.         0.33333333]

Auto-vetores:
[[ 0.70710678 -0.70710678]
 [ 0.70710678  0.70710678]]

Agora, ordenaremos os auto-valores do maior para o menor. Assim $$\lambda_1 = 1\text{ e }\lambda_2 = \frac{1}{3}.$$ No Python:

In [8]:
# ordenando em ordem descrecente
pares_de_autos = [
    (
        np.abs(autovalores[i]),
        autovetores[:,i]
    ) for i in range(len(autovalores))
]
pares_de_autos.sort()
pares_de_autos.reverse()

Nosso prĂłximo passo Ă© calcular o quanto cada componente (auto-vetores) estĂŁo representando (de certa forma) a variabilidade dos nossos dados. Para isso usaremos duas medidas.

A variĂąncia explicada de cada auto-valor $\lambda$ Ă© dada por: $$\frac{\lambda}{\sum_j \lambda_j}$$

A variĂąncia explicada acumulada de cada auto-valor $\lambda_i$ Ă© dada por: $$\frac{\sum_{j\leq i}\lambda_j}{\sum_j \lambda_j}$$

No Python:

In [9]:
# calculando a variĂąncia explicada e a variĂąncia explicada cumulativa
total = sum(autovalores)
var_exp = [
    (i / total)*100 for i in sorted(
        autovalores, reverse=True
    )
]
cum_var_exp = np.cumsum(var_exp)

Vejamos de forma sumarizada que temos até o momento:

In [10]:
# visualizando as informaçÔes
x = [
    'PC %s' %i for i in range(
        1,len(autovalores)+1
    )
]
df_temp = pd.DataFrame(
    {'auto-valores': autovalores,
     'cum_var_exp':cum_var_exp,
     'var_exp':var_exp,
     'Componente':x}
)
print(df_temp)
print()
print("Auto-vetores")
for autovetor in [p[1] for p in pares_de_autos]:
    print(autovetor)
print()
   auto-valores  cum_var_exp  var_exp Componente
0      1.000000         75.0     75.0       PC 1
1      0.333333        100.0     25.0       PC 2

Auto-vetores
[0.70710678 0.70710678]
[-0.70710678  0.70710678]

Bom, isso é tudo! Considerando os dados originais como pares ordenados em $\mathbb{R}^2$, pois $X$ possui apenas duas colunas, ao mudar da base canÎnica para a base composta pelos auto-vetores encontrados, teremos uma nova representação do conjunto $X$. Vejamos graficamente:

In [11]:
# visualizando graficamente os dados originais
sns.pairplot(
    df,
    vars = ['var1','var2'],
    hue='target',
    diag_kind="hist"
)
plt.show()
In [12]:
# visualizando graficamente os dados através da mudança de base
# considerando base de auto-vetores
n_componentes = 2 # projetamos nas 2 (duas) primeiras componentes
autovetores = [p[1] for p in pares_de_autos]
A = autovetores[0:n_componentes] 
X = np.dot(X,np.array(A).T)
new_df = pd.DataFrame(X,columns=['pc1','pc2'])
new_df['target'] = df['target']
sns.pairplot(
    new_df, vars = ['pc1','pc2'], hue='target', diag_kind="hist"
)
plt.show()
  • Os dados originais foram rotacionados por um Ăąngulo de 45Âș uma vez que os auto-vetores sĂŁo perpendiculares e obtidos dos eixos x e y por uma rotação de 45Âș no sentido anti-horĂĄrio.
  • Nossos dados possuiam 2 colunas, mas poderiam possuir bem mais. AlĂ©m disso escolhermos 2 componentes para visualização grĂĄfica, esse nĂșmero tambĂ©m pode ser mais, desde que seja menor que o nĂșmero de colunas.
  • A ideia do PCA Ă© diminuir a dimensĂŁo do conjunto de dados original escolhendo um nĂșmero menor de componentes para projeção sobre estas.
  • O processo de redução de dimensĂŁo facilita processamento e armazenamento de dados.

Fazendo com uso de biblioteca

Agora vejamos o mesmo exemplo utilizando a biblioteca Scikit-learn.

In [13]:
# biblioteca
from sklearn.decomposition import PCA

# dados
df = pd.DataFrame({
    'var1':[1,3,1,2,2,1],
    'var2':[2,4,3,4,3,4],
    'target': [0,1,0,1,1,1]
})
X = df.drop('target',1)
y = df['target']

# pca
pca = PCA(n_components=2)
pca.fit(X)
Out[13]:
PCA(copy=True, iterated_power='auto', n_components=2, random_state=None,
    svd_solver='auto', tol=0.0, whiten=False)

Isso é tudo, agora vejamos as informaçÔes para fins de comparação.

In [14]:
print("Auto-valores:")
print(pca.explained_variance_)
print()

print("Auto-vetores:")
print(pca.components_)
print()

print("VariĂąncia explicada:")
print(pca.explained_variance_ratio_)
print()
Auto-valores:
[1.         0.33333333]

Auto-vetores:
[[-0.70710678 -0.70710678]
 [-0.70710678  0.70710678]]

VariĂąncia explicada:
[0.75 0.25]

Observe que, como a escolha dos auto-vetores nĂŁo Ă© Ășnica, o algoritmo implementado na biblioteca Scikit-learn escolheu auto-vetores diferentes dos nossos. Mas, isso nĂŁo Ă© problema, pois sĂł houve inversĂŁo de sinal. Logo, quando fizermos a visualização grĂĄfica da mudança de coordenada, haverĂĄ uma inversĂŁo da figura. Vejamos

In [15]:
# Tranformando X
X = pca.transform(X)

# Visualizando
new_df = pd.DataFrame(X, columns=['pc1','pc2'])
new_df['target'] = df['target']
sns.pairplot(
    new_df, vars = ['pc1','pc2'], hue='target', diag_kind="hist")
plt.show()

Perfeito! Mas essa técnica serve para reduzir dimensÔes. Como nosso exemplo é apenas para fins educacionais, é hora de aprender com exemplos reais como usar o poderoso PCA!

PCA no conjunto de dados Iris

No prĂłximo exemplo utilizaremos PCA para reduzir o conjunto de dados Iris de 4 para 2 features. O conjunto pode ser baixado no repositĂłrio UCI. O link encontra-se nas referĂȘncias.

In [16]:
# dados
df = pd.read_csv('iris.data',header=None)
df.columns = [
    'sepal length in cm',
    'sepal width in cm',
    'petal length in cm',
    'petal width in cm',
    'target'
]
X = df.drop('target',1)
y = df['target']

Faremos novamente passo a passo. O cĂłdigo Ă© anĂĄlogo ao cĂłdigo usado no exemplo anterior.

In [17]:
mean_vec = np.mean(X, axis=0)
M = X - mean_vec
C = M.T.dot(M) / (X.shape[0]-1)
autovalores, autovetores = np.linalg.eig(C)
pares_de_autos = [
    (
        np.abs(autovalores[i]),
        autovetores[:,i]
    ) for i in range(len(autovalores))
]
pares_de_autos.sort()
pares_de_autos.reverse()
total = sum(autovalores)
var_exp = [
    (i / total)*100 for i in sorted(autovalores, reverse=True)
]
cum_var_exp = np.cumsum(var_exp)
In [18]:
x = ['PC %s' %i for i in range(1,len(autovalores)+1)]
df_temp = pd.DataFrame(
    {'auto-valores': autovalores,
     'cum_var_exp':cum_var_exp,
     'var_exp':var_exp,
     'Componente':x}
)
print(df_temp)
print()
print("Auto-vetores")
for autovetor in [p[1] for p in pares_de_autos]:
    print(autovetor)
print()
   auto-valores  cum_var_exp    var_exp Componente
0      4.224841    92.461621  92.461621       PC 1
1      0.242244    97.763178   5.301557       PC 2
2      0.078524    99.481691   1.718514       PC 3
3      0.023683   100.000000   0.518309       PC 4

Auto-vetores
[ 0.36158968 -0.08226889  0.85657211  0.35884393]
[-0.65653988 -0.72971237  0.1757674   0.07470647]
[-0.58099728  0.59641809  0.07252408  0.54906091]
[ 0.31725455 -0.32409435 -0.47971899  0.75112056]

Com duas componentes temos uma variĂąncia explicada cumulativa de 97%. Utilizaremos entĂŁo, apenas duas componentes.

In [19]:
n_componentes = 2 # projetamos nas 2 (duas) primeiras componentes
autovetores = [p[1] for p in pares_de_autos]
A = autovetores[0:n_componentes] 
X = np.dot(X,np.array(A).T)
new_df = pd.DataFrame(X, columns=['pc1','pc2'])
new_df['target'] = df['target']
sns.pairplot(
    new_df, vars = ['pc1','pc2'],
    hue='target', diag_kind="hist")
plt.show()

Experimente trabalhar com outros conjuntos de dados com um maior nĂșmero de features.

Vejamos agora o mesmo exemplo utilizando a biblioteca Scikit-learn.

PCA no conjunto de dados Iris com biblioteca

Novamente, carregamos o conjunto de dados.

In [20]:
# dados
df = pd.read_csv('iris.data',header=None)
df.columns = [
    'sepal length in cm',
    'sepal width in cm',
    'petal length in cm',
    'petal width in cm',
    'target'
]
X = df.drop('target',1)
y = df['target']

Repetimos entĂŁo o processo, sĂł que, desta vez, utilizando o PCA jĂĄ pronto.

In [21]:
pca = PCA(n_components=2)
pca.fit(X)
X = pca.transform(X)
new_df = pd.DataFrame(X, columns=['pc1','pc2'])
new_df['target'] = df['target']
sns.pairplot(
    new_df, vars = ['pc1','pc2'],
    hue='target', diag_kind="hist")
plt.show()

Na próxima seção veremos outra aplicação.

PCA para redução de dimensão em imagens

Outra aplicação é a redução de dimensão em imagens. Para esse exemplo, escolhemos a imagem Påssaro mineiro barulhento retirada do site Pixabay. A imagem foi transformada em escala de cinza utilizando o software GIMP.

Primeiro, carregaremos a imagem.

In [22]:
# carregando a imagem e visualizando dimensÔes
X = plt.imread('bird.jpg')
X.shape
Out[22]:
(1920, 1899)

Observe que a matriz tem 1920 linhas e 1899 colunas. Aplicaremos, em seguida, a técnica PCA.

In [23]:
# aplicando PCA
pca = PCA(0.99) # variĂąncia explicada de 0.99
lower_dimension_data = pca.fit_transform(X)
lower_dimension_data.shape
Out[23]:
(1920, 145)

O nĂșmero 0.99 passado como parĂąmetro para o mĂ©todos PCA diz que queremos um nĂșmero de componentes que nos garanta 99% de variĂąncia explicada cumulativa. Observe que para isso, utilizou-se 145 componentes. Isso Ă© uma redução imensa, uma vez que a original possuia 1899 colunas. Queremos ver, caso necessĂĄrio recuperar a imagem, como essas imagens ficam com a dimensĂŁo reduzida. Para isso serĂŁo necessĂĄrios duas funçÔes auxiliares.

In [24]:
def pca_with_var_exp(X, var_exp=0.99):
    pca = PCA(var_exp) # variĂąncia explicada de 0.99
    lower_dimension_data = pca.fit_transform(X)
    print(lower_dimension_data.shape)
    approximation = pca.inverse_transform(lower_dimension_data)
    return approximation

def plot_subplot(X, i):
    plt.subplot(3,2,i)
    plt.imshow(X, cmap="gray")
    plt.xticks([])
    plt.yticks([])

A primeira função reduz a imagem X mantendo var_exp de variùncia explicada cumulativa. Compararemos 3 valores: 0.99, 0.95 e 0.90.

In [25]:
# calculando algumas aproximaçÔes
img_1 = pca_with_var_exp(X, var_exp=0.99)
img_2 = pca_with_var_exp(X, var_exp=0.95)
img_3 = pca_with_var_exp(X, var_exp=0.90)
(1920, 145)
(1920, 31)
(1920, 14)

A segunda função mostra o gråfico. Utilizaremos ela para comparar as imagens obtidas com a original.

In [26]:
# visualizando aproximaçÔes
plt.figure(figsize=(6,8))

plot_subplot(X, 1)
plt.title("Original")
plot_subplot(img_1, 2)
plt.title("Var. Explicada de 99%")
plot_subplot(X, 3)
plt.title("Original")
plot_subplot(img_2, 4)
plt.title("Var. Explicada de 95%")
plot_subplot(X, 5)
plt.title("Original")
plot_subplot(img_3, 6)
plt.title("Var. Explicada de 90%")

plt.tight_layout()
plt.show()
In [ ]: