# Riduzione della dimensionalità

Fino ad ora abbiamo visto come le feature siano importanti per poter definire un algoritmo in grado di eseguire il proprio compito imparando dai dati, ora il problema è che ci potremmo trovare in condizioni in cui sfortunatamente abbiamo troppe feature e troppi pochi dati(troppe colonne e troppe poche righe) o che ci dicano che vogliono un njumero massimo di feature da usare per predire il nostro algoritmo in tal caso possiamo utilizzare **algoritmi che ci aiutino a ridurre le dimensioni del nostro dataset considerando solo quelle rilevanti anche senza sapere il modello che useremo, inoltre i dataset possono essere utilizzati in diversi contesti come tesi, immagini e molto altro**.

## decomposizione ai valori singolari (SVD)

La __[decomposizione ai valori singolari](https://it.wikipedia.org/wiki/Decomposizione_ai_valori_singolari)__ si basa su nozioni geometriche al fine di fattorizzare la matrice di partenza in matrici più semplici e che mi fornisca informazioni sulle proprietà di ogni componente che stiamo considerando.Dal punto di vista matematico si ha:

\begin{equation}
    \Large M_{n \times m} = U_{n \times n} D_{n \times m} V^{\dagger}_{m \times m}
\end{equation}

dove $ M_{n \times m}$ è la nostra matrice di partenza con n righe e m colonne, $U_{n \times n}$ è una matrice unitaria ortogonale, $D_{n \times m}$ è una matrice singolare diagonale $n\times m$, e $V^{\dagger}_{m \times m}$ è la trasposta coniugata di una matrice unitaria ortogonale.<br>
Dal punto di vista pratico quello che ci interessa è la matrice $D$ poiché i suoi valori sulla diagonali rappresentano la varianza di ogni singola componente, a cosa ci serve questo? Per capirlo facciamo un esempio


```python
import numpy as np

#la nostra matrice di partenza
M = np.matrix([[1, 5, 6 ], [3, 4, 19], [2,7,24]])

U, D, V = np.linalg.svd(M)

#Trasformo solo D poiché essendo diagonale numpy per risparmiare memoria
#vi ritorna un array 1D poiché per le operazioni hanno lo stesso comportamento
print(f'Matrix U:\n {U}\n Matrix D :\n {np.diag(D)}\n Matrix V :\n {V}')
```

    Matrix U:
     [[-0.22097491 -0.91114121  0.34783874]
     [-0.60034273  0.4081545   0.68774887]
     [-0.76860828 -0.05684721 -0.63718891]]
     Matrix D :
     [[32.61541883  0.          0.        ]
     [ 0.          3.45287401  0.        ]
     [ 0.          0.          1.14547607]]
     Matrix V :
     [[-0.1091269  -0.27246326 -0.95595768]
     [ 0.05781499 -0.96181284  0.26753223]
     [ 0.99234507  0.02607372 -0.12071212]]


Ora mettiamo ipotesi che i voglia considerare solo le due componenti più importanti per ricostruire il dataset, per farlo vediamo se togliendo un valore alla diagonale che succede, ricordate noi vogliamo che $M \approx UDV^{\dagger}$, per fare in modo che non ci siano problemi di dimensionalità nel prodotto scalare successivo alla matrice $U$ si toglie la relativa colonna, mentre alla matrice $V^{\dagger}$ si toglie la riga relativa ad essa.


```python
#eliminate the first value of D, U and V lose the column
#@ means dot product in numpy

#original calculus gives me the original M
print(f'original matrix obtained with all features:\n {U @ np.diag(D) @ V}')

#i remove the first colmn of U and last row of V
print(f'matrix obtained eliminating the first element in diagonal {D[0]}:\n'
      f'{U[: ,1:] @ np.diag(D[1:]) @ V[:2, :]}')

#i remove the middle column of U and middle row of V
print(f'matrix obtained eliminating the second element in diagonal {D[1]}:\n'
      f'{U[: ,[0,2]] @ np.diag(D[[0,2]]) @ V[[0,2], :]}')

##i remove the last column of U and first row of V
print(f'matrix obtained eliminating the second element in diagonal {D[2]}:\n'
      f'{U[: ,[0,1]] @ np.diag(D[:2]) @ V[[0,1], :]}')
```

    original matrix obtained with all features:
     [[ 1.  5.  6.]
     [ 3.  4. 19.]
     [ 2.  7. 24.]]
    matrix obtained eliminating the first element in diagonal 32.6154188335189:
    [[ 0.36635519  0.47395901  3.114092  ]
     [-0.10824657 -1.14170015 -1.13647509]
     [-0.02077816  0.75549322 -0.00762631]]
    matrix obtained eliminating the second element in diagonal 3.4528740055280096:
    [[ 1.18188917  1.97408316  6.84167133]
     [ 2.91852099  5.35548866 18.6229652 ]
     [ 2.01134829  6.81120935 24.0525129 ]]
    matrix obtained eliminating the second element in diagonal 1.1454760652624991:
    [[ 0.60460909  4.98961116  6.04809665]
     [ 2.21823068  3.97945913 19.09509699]
     [ 2.72429743  7.0190308  23.91189408]]


Da come possiamo vedere che se noi togliamo il valore  più piccolo dalla matrice diagonale, la nuova matrice ricostruita sarà molto vicina alla matrice ottenuta, in tal caso la decomposizione viene chiamata __[TruncatedSVD](https://langvillea.people.cofc.edu/DISSECTION-LAB/Emmie%27sLSI-SVDModule/p5module.html)__.<br>
Da qui possiamo capire che qualora volessimo le componenti sono più rilevanti è sufficiente selezionare i valori delle diagonali più alti fino ad averne il numero desiderato.<br>

## PCA

La PCA (Principal Component Analysis) sfrutta prorio questo algoritmo di SVD permettendo di mappare il nostro problema proiettandolo su uno spazio più piccolo con la condizione di conserva quanto più possibile la norma dei nostri vettori sfruttando proprio la varianza di ogni singola feature associata ottenuta attraverso la matrice diagonale attenzione che ora le nostre nuove feature sono chiamate **principal component**, se avete dubbi consultate __[qui](https://medium.com/analytics-vidhya/what-is-principal-component-analysis-cf880cf95a0c)__.<br>
Poiché __[scikit](https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.PCA.html)__ ha già implementato la sua funzione useremo quella.


```python
import time
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import plot_confusion_matrix, classification_report


#classification data
diabetes = pd.read_csv('../data/diabetes2.csv')
X_diabetes, y_diabetes = diabetes.drop('Outcome', axis = 1).values, diabetes.Outcome.values
target_names = ["Not Diabetes", "Diabetes"]
print(f'Original data: {X_diabetes.shape[0]} dati, {X_diabetes.shape[1]} feature')

#let's find the 5 most valuable feature
diabetes_pca = PCA(n_components=5)

#fit the data
diabetes_pca.fit(X_diabetes)

#trasform the data
X_pca = diabetes_pca.transform(X_diabetes)
print(f'Reduced data: {X_pca.shape[0]} dati, {X_pca.shape[1]} feature')

print(f'Reduced data PCA output: \n {X_pca}')

print("PCA : ")
print(f'- components: \n{diabetes_pca.components_} \n'
      f'- explained variance: \n {diabetes_pca.explained_variance_} \n'
      f'- explained variance ratio: \n {diabetes_pca.explained_variance_ratio_} \n'
      f'- singular values: \n {diabetes_pca.singular_values_}\n'
      f'- noise variance values:  {diabetes_pca.noise_variance_}' )

print('-'*80)
#prepare the data, 
print("The  reduced data ha been divided to train and test in 80% traing, 20% testing")
X_pca_train, X_pca_test, y_diabetes_train, y_diabetes_test = train_test_split(
    X_pca, y_diabetes, random_state=0, test_size = 0.2)


print('Allenaimo un Gradient Boosting Classifier sui dati ridotti:')
tree = GradientBoostingClassifier()

start = time.time()
tree.fit(X_pca_train, y_diabetes_train)
end = time.time()

print(f"Time taken to train Gradient Boosting Classifier on reduced data: {end - start}s ")

plot_confusion_matrix(tree, X_pca_test, y_diabetes_test, display_labels=target_names)  
plt.title("Confusion matrix of classification")
plt.show()

print(classification_report(y_diabetes_test, tree.predict(X_pca_test), target_names= target_names))
```

# Non-negative matrix factorization (NMF o NNMF)

La PCA presenta un modo di ridurre la dimensionalità del dataset, ma è presente un problema: la possibilità che la ricostruzione della matrice dia dei valori negativi ed in genere i valori negativi sono difficili da interptretare ed analizzare, per questo l'obiettivo della __[NMF](https://scikit-learn.org/stable/modules/decomposition.html#non-negative-matrix-factorization-nmf-or-nnmf)__ è quello di fattorizzare la matrice imponendo che gli autovalori e i vettori delle matrici fattorizzati siano tutti positivi, poiché questo implica avere un maggiorn numero possibile di modi di fattorizzare, in genere la condizione che si pone è che la distanza matriciale tra la decomposizione e l'originale sia quanto **più vicina secondo la distanza di Frobenius definita anche come** __[norma matriciale](https://it.wikipedia.org/wiki/Norma_matriciale)__, **si introducono inoltre termini di regoralizzazione o si usano altre metriche per assicurare un risultato flessibile e quanto meno divergente, per saperne di più guardate** __[qui](https://scikit-learn.org/stable/modules/decomposition.html#nmf-with-a-beta-divergence)__.
Usiamo ora il modello sul diabetes dataset.


```python
from sklearn.decomposition import NMF

nmf = NMF(n_components=5, verbose = 0, max_iter=500, init= 'nndsvda' )
nmf.fit(X_diabetes)

#trasform the data
X_NMF = nmf.transform(X_diabetes)

print(f'Reduced data: {X_NMF.shape[0]} dati, {X_NMF.shape[1]} feature')

print("NMF : ")
print(f'- components: \n{nmf.components_} \n'
      f'- reguralization: {nmf.regularization} \n'
      f'- reconstruction error:  {nmf.reconstruction_err_}\n'
      f'- iterations:  {nmf.n_iter_}')

print('-'*80)
#prepare the data, 
print("The  reduced data ha been divided to train and test in 80% traing, 20% testing")
X_NMF_train, X_NMF_test, y_diabetes_train, y_diabetes_test = train_test_split(
    X_NMF, y_diabetes, random_state=0, test_size = 0.2)


print('Allenaimo un Gradient Boosting Classifier sui dati ridotti:')
tree = GradientBoostingClassifier()

start = time.time()
tree.fit(X_NMF_train, y_diabetes_train)
end = time.time()

print(f"Time taken to train Gradient Boosting Classifier on reduced data: {end - start}s ")

plot_confusion_matrix(tree, X_NMF_test, y_diabetes_test, display_labels=target_names)  
plt.title("Confusion matrix of classification")
plt.show()

print(classification_report(y_diabetes_test, tree.predict(X_NMF_test), target_names= target_names))
```

## Latent Dirichlet Annotation(LDA)

L' __[LDA](https://scikit-learn.org/stable/modules/decomposition.html#latent-dirichlet-allocation-lda)__ è un algoritmo di riduzione dimensionale che è __[probabilistico generativo](https://ichi.pro/it/modelli-grafici-probabilistici-generativi-vs-discriminativi-40857457895478)__, la differenza da quelli discriminativi è che in questo caso noi cerchiamo di determinare una distribuzione di probabilità attraverso cui possiamo determinare quale sia la probabilità associata a quell'evento. Tradotto in matematica i modelli discriminativi determinano $P(Y|X)$, mentre quelli generativi $P(Y,X)$, questo permette in futuro anche di generare anche valori con una certa probabilità associata e in genere non sono limitati alla mera classificazione, per dettagli guardate qui un __[video sulle GAN](https://www.youtube.com/watch?v=8L11aMN5KY8)__, che sono modelli generativi.<br>
***Attenti però che questi modelli sono meno precisi poichè assumono che idati siano i.i.d. condizione che nei discriminativi può anche non essere vera!.***<br>
Tornando alla LDA quello che succede è che questo algoritmo cervca di capire dai dati quale sia la struttura sottostante leggendone solo una parte, facendo ciò quello che succede è che divide per categorie la struttura e in base a ciò considera solo le categorie più rilevanti al fine di poterne ricreare la struttura completa, **questo algoritmo permette l'apprendimento "online" ovvero ogni singolo nuovo dato può essere usato per allenare il modello e adattarlo in maniera istantanea ai possibili cambiamenti, se invece volete riallenare il modello solo quanto un certo numero di dati è raggiunto potete usare "batch"**.


```python
from sklearn.decomposition import LatentDirichletAllocation

lda = LatentDirichletAllocation(n_components=5, n_jobs=-1)
lda.fit(X_diabetes)

#trasform the data
X_lda = lda.transform(X_diabetes)

print(f'Reduced data: {X_lda.shape[0]} dati, {X_lda.shape[1]} feature')

print("LDA : ")
print(f'- components: \n{lda.components_} \n'
      f'- bound_: {lda.bound_} \n'
      f'- exp dirichlet components:\n  {lda.exp_dirichlet_component_}\n'
      f'- iterations:  {lda.n_iter_}')

print('-'*80)
#prepare the data, 
print("The  reduced data ha been divided to train and test in 80% traing, 20% testing")
X_lda_train, X_lda_test, y_diabetes_train, y_diabetes_test = train_test_split(
    X_lda, y_diabetes, random_state=0, test_size = 0.2)


print('Allenaimo un Gradient Boosting Classifier sui dati ridotti:')
tree = GradientBoostingClassifier()

start = time.time()
tree.fit(X_lda_train, y_diabetes_train)
end = time.time()

print(f"Time taken to train Gradient Boosting Classifier on reduced data: {end - start}s ")

plot_confusion_matrix(tree, X_lda_test, y_diabetes_test, display_labels=target_names)  
plt.title("Confusion matrix of classification")
plt.show()

print(classification_report(y_diabetes_test, tree.predict(X_lda_test), target_names= target_names))
```

In questo notebook abbiamo quindi visto come possiamo utilizzare alcune tecniche per ridurre la dimensione del nostro dataset con lacondizione di riuscire a usare modelli che riescano a preformare quanto meglio possibile, sono presenti molte altre tecniche, per saprenedi più consultate la __[guida di scikit sulla dimensionality reduction](https://scikit-learn.org/stable/modules/decomposition.html#decompositions)__.

***

COMPLIMENTI AVETE FINITO LA LEZIONE SU PCA LDA E NMF, A PRESTO!

3a8523ed7f36fcf78498c3c7f1700c26c8f2ad5b
ipynb
Jupyter Notebook
3.machine learning/7-PCA_LDA_NMF.ipynb
matinator/Starting-Finance-Club-Torino
8abda7caa769f2dc237c4ff520ef1b40038a5f65
Apache-2.0
2021-03-18T15:45:39.000Z
2021-03-18T15:45:39.000Z
3.machine learning/.ipynb_checkpoints/7-PCA_LDA_NMF-checkpoint.ipynb
matinator/Starting-Finance-Club-Torino
8abda7caa769f2dc237c4ff520ef1b40038a5f65
Apache-2.0
3.machine learning/.ipynb_checkpoints/7-PCA_LDA_NMF-checkpoint.ipynb
matinator/Starting-Finance-Club-Torino
8abda7caa769f2dc237c4ff520ef1b40038a5f65
Apache-2.0
Qwen/Qwen-72B
1. YES
2. YES