from IPython.display import Image

Image('https://raw.githubusercontent.com/contepablod/QCNNCancerClassifier/master/Esophagus%20Cancer.JPG')

import os            # módulo para manejar carpetas y archivos en nuestro ordenador
import random        # módulo para aleatorizar

import numpy as np   # biblioteca para manejar matrices y operaciones de matrices 
import pandas as pd  # biblioteca para manejar tablas de datos

#Skimage (Scikit-image): biblioteca para procesamiento de imágenes
from skimage import io  #Modulo para leer una imagen (librería para procesamiento de imagenes)

#Sklearn (Scikit-learn): biblioteca para machine learning 
from sklearn.model_selection import train_test_split   
from sklearn.linear_model import Perceptron 
from sklearn.metrics import accuracy_score

#Bibliotecas para gráficar y visualizar
import matplotlib.pyplot as plt        
import seaborn as sns

#Matriz de confusión
def matrix_confusion(yt, yp):
    data = {'Y_Real': yt,
            'Y_Prediccion': yp}

    df = pd.DataFrame(data, columns=['Y_Real','Y_Prediccion'])
    confusion_matrix = pd.crosstab(df['Y_Real'], df['Y_Prediccion'], rownames=['Real'], colnames=['Predicted'])
    sns.heatmap(confusion_matrix, annot=True, fmt='g')

    plt.show()

#Leemos los datos
datos = pd.read_csv("https://raw.githubusercontent.com/AnIsAsPe/ClassificadorCancerEsofago/master/Datos/ClasesImagenes.csv", usecols=[1,2])

datos.info()  #muestra los primeros cinco registros

#¿cuántas imagenes tenemos de cada clase?
datos['class_number'].value_counts(sort=False)  

Y = datos['class_number']  #Guardamos las etiquetas de las imagenes como serie de pandas

datos['image_filename']

path = "C:\\Users\\conte\\OneDrive\\Escritorio\\Colegio Bourbaki\\ML-AI-WA\\Perceptron\\CarpetaImagenes\\"

%time  img = datos['image_filename'].apply(lambda x: io.imread(path + x, as_gray=True))

img.shape

img[0].shape

IMG = np.stack(img, axis=0)  # Toma una secuencia de matrices y las apila a lo largo 
                              # de un tercer eje para hacer una solo arreglo               
IMG.shape

X = IMG.reshape(5063, -1) # se puede poner 67600 en vez de -1
X.shape

#El método GroupBy de Pandas separa un data frame en varios data frames
porClase = datos.groupby('class_number')


#elije al azar n muestras de cada subconjunto y guarda la posición de las figuras elegidas en una lista
n = 20
c = random.sample(porClase.get_group(1).index.tolist(), n) # indices de las imagenes cancerígenas seleccionadas
s = random.sample(porClase.get_group(0).index.tolist(), n) # indices de las imagenes sanas seleccionadas

# Grafica 20 imágenes aleatorias de tejido con cáncer y 20 de tejido sano

fig = plt.figure(figsize=(16, 8))
columns = 10
rows = 4
for i in range(0, columns * rows):
    fig.add_subplot(rows, columns, i+1)
    if i < 20:
        plt.imshow(img[c[i]], cmap='Greys_r')
        plt.title('cancer')
        plt.xticks([])
        plt.yticks([])
    else:
        plt.imshow(img[s[i-20]], cmap='Greys_r')
        plt.title('tejido sano')
        plt.xticks([])
        plt.yticks([])
plt.show()

X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.3, 
                                                    shuffle=True, random_state=0) #random_state es el valor semilla

# ¿Cómo son los conjuntos de entrenamiento y prueba?
print("Training set")
print("X: ", X_train.shape)
print("Y: ", y_train.shape)
unique, counts = np.unique(y_train, return_counts=True)
print('Tejido Sano:        ', counts[0],'\nDisplasia o Cáncer: ', counts[1],'\n')


print("Test set")
print("X: ", X_test.shape)
print("Y: ", y_test.shape)
unique, counts = np.unique(y_test, return_counts=True)
print('Tejido Sano:         ', counts[0],'\nDisplasia o Cáncer: ', counts[1],'\n')

model_p = Perceptron(max_iter=50, random_state=0, verbose=True)
model_p.fit(X_train,y_train)             

y_pred = model_p.predict(X_test)  #pasa cada una de las imágenes de X_test por el modelo 

print("Accuracy: %.2f%%" % (accuracy_score(y_test, y_pred)*100))

print("Precisión conjunto entrenamiento: %.2f%%" % (model_p.score(X_train, y_train)*100.0))
print("Precisión conjunto prueba: %.2f%%" % (model_p.score(X_test, y_test)*100.0))

matrix_confusion(y_test, y_pred)

model_mp = Perceptron(max_iter=1000, random_state=0, verbose=False, alpha=0.0001) 
model_mp.fit(X_train,y_train)                                  

print("Precisión conjunto entrenamiento: %.2f%%" % (model_mp.score(X_train, y_train)*100.0))
print("Precisión conjunto prueba: %.2f%%" % (model_mp.score(X_test, y_test)*100.0))

y_pred = model_mp.predict(X_test)  #pasa cada una de las imágenes de X_test por el modelo

print("Accuracy: %.2f%%" % (accuracy_score(y_test, y_pred)*100))

matrix_confusion(y_test, y_pred)

model_mp1 = Perceptron(max_iter=1000,  random_state=0, verbose=False, alpha=0.000001, penalty='l2') # Mas margen y con penalidad
model_mp1.fit(X_train,y_train) 

print("Precisión conjunto entrenamiento: %.2f%%" % (model_mp1.score(X_train, y_train)*100.0))
print("Precisión conjunto prueba: %.2f%%" % (model_mp1.score(X_test, y_test)*100.0))

y_pred = model_mp1.predict(X_test)  #pasa cada una de las imágenes de X_test por el modelo

print("Accuracy: %.2f%%" % (accuracy_score(y_test, y_pred)*100))

matrix_confusion(y_test, y_pred)

# Pytorch
import torch, torchvision, torch.utils
from torch import Tensor
from torch import cat
from torch.autograd.grad_mode import no_grad
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import torch.optim as optim
from torch.nn import (
    Module,
    Conv2d,
    Linear,
    Dropout2d,
    NLLLoss, BCELoss, CrossEntropyLoss, MSELoss,
    MaxPool2d,
    Flatten,
    Sequential,
    ReLU,
)
import torch.nn.functional as F
from torchviz import make_dot
from torchsummary import summary

# Qiskit
from qiskit import Aer, QuantumCircuit
from qiskit.utils import QuantumInstance
from qiskit.opflow import AerPauliExpectation
from qiskit.circuit import Parameter
from qiskit.circuit.library import RealAmplitudes, ZZFeatureMap
from qiskit.quantum_info import DensityMatrix, entanglement_of_formation
from qiskit.quantum_info import DensityMatrix
from qiskit.visualization import plot_state_city
from qiskit_machine_learning.neural_networks import TwoLayerQNN
from qiskit_machine_learning.connectors import TorchConnector

train_data = torchvision.datasets.ImageFolder('C:\\Users\\conte\\OneDrive\\Escritorio\\Colegio Bourbaki\\ML-AI-WA\\Perceptron\\Imagenes_Clasificadas_Random_Split\\Train', transform=transforms.Compose([transforms.ToTensor()]))
test_data = torchvision.datasets.ImageFolder('C:\\Users\\conte\\OneDrive\\Escritorio\\Colegio Bourbaki\\ML-AI-WA\\Perceptron\\Imagenes_Clasificadas_Random_Split\\Test', transform=transforms.Compose([transforms.ToTensor()]))

train_data[0][0].shape

train_loader = DataLoader(train_data, shuffle=True, batch_size=1)
test_loader = DataLoader(test_data, shuffle=True, batch_size=1)

# False significa que no hay cancer (0) y True que sí (1)
print((train_loader.dataset.class_to_idx))

n_samples_show = 6

data_iter = iter(train_loader)
fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 10))

while n_samples_show > 0:
    images, targets = data_iter.__next__()

    axes[n_samples_show - 1].imshow(images[0, 0].numpy().squeeze(), cmap=plt.cm.rainbow)
    axes[n_samples_show - 1].set_xticks([])
    axes[n_samples_show - 1].set_yticks([])
    axes[n_samples_show - 1].set_title(f"Labeled: {targets[0].item()}")

    n_samples_show -= 1

Image(url='https://raw.githubusercontent.com/contepablod/QCNNCancerClassifier/master/hybridnetwork.png') 

# Declaramos Instancia Cuantica
qi = QuantumInstance(Aer.get_backend("aer_simulator_statevector"))

Image(url='https://raw.githubusercontent.com/contepablod/QCNNCancerClassifier/master/neuralnetworkQC.png') 

# Definimos y creamos la red neuronal cuántica 
def create_qnn():
    feature_map = ZZFeatureMap(2)
    ansatz = RealAmplitudes(2, reps=1)
    # input_gradients=True para gradiente híbrido
    qnn = TwoLayerQNN(
        2, #numero de Qubits, solo son posibles dos estados
        feature_map,
        ansatz,
        input_gradients=True,
        exp_val=AerPauliExpectation(),
        quantum_instance=qi,
    )
    return qnn

qnn = create_qnn()

qnn.circuit.draw(output='mpl')

qnn.feature_map.decompose().draw(output='mpl')

qnn.ansatz.decompose().draw(output='mpl')

qnn.circuit.parameters

params = np.random.uniform(-1, 1, len(qnn.circuit.parameters))
params

rho_01 = DensityMatrix.from_instruction(qnn.circuit.bind_parameters(params))

plot_state_city(rho_01.data, title='Density Matrix')

gamma_p = rho_01.purity()
display(rho_01.draw('latex', prefix='\\rho_p = '))
print("State purity: ", np.round(np.real(gamma_p)))

print(f'{entanglement_of_formation(rho_01):.4f}')

# Red Neuronal en Pytorch
class Net(Module):
    def __init__(self, qnn):
        super().__init__()
        self.conv1 = Conv2d(3, 128, kernel_size=5)
        self.conv2 = Conv2d(128, 128, kernel_size=3)
        self.dropout = Dropout2d()
        self.fc1 = Linear(508032, 128)
        self.fc2 = Linear(128, 2)  # Input bidimensional para la red neuronal cuántica
        self.qnn = TorchConnector(qnn) # Aplicamos el conector Pytorch para conectar la red neuronal y el circuito
        self.fc3 = Linear(1, 1)  # Salida unidimensional del circuito cuántico

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)
        x = self.dropout(x)
        x = x.view(x.shape[0], -1)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        x = self.qnn(x)  # Aplicamos la red cuántica nuevamente en la sección forward
        x = self.fc3(x)
        return cat((x, 1 - x), -1)

model = Net(qnn)

model = model.to('cuda')

print(model)

summary(model, (3, 260, 260), batch_size=-1, device='cuda')

# dummy_tensor = next(iter(train_loader))[0].to('cuda')
# make_dot(model(dummy_tensor), params=dict(list(model.named_parameters())), show_saved=True, show_attrs=True).render("rnn_torchviz", format="png")
#Image('https://raw.githubusercontent.com/contepablod/QCNNCancerClassifier/master/rnn_torchviz.png') 
Image(r'C:\Users\conte\OneDrive\Escritorio\qiskit-fall-fest-peru-2022-main\qiskit-community-tutorials-master\qiskit-community-tutorials-master\drafts\rnn_torchviz.png', width=2000, height=2250)

# Definimos optimizador y función de pérdida
optimizer = optim.Adam(model.parameters(), lr=0.00001)
loss_func = CrossEntropyLoss().to('cuda')

# Empezamos entrenamiento
epochs = 50  # Número de épocas
model.train() # Modelo en modo entrenamiento

loss_list = []  
total_accuracy = []

for epoch in range(epochs):
    correct = 0
    total_loss = []
    for batch_idx, (data, target) in enumerate(train_loader):
        optimizer.zero_grad(set_to_none=True)  # Se inicializa gradiente
        output = model(data.to('cuda'))  # Forward pass, Datos a GPU
        loss = loss_func(output, target.to('cuda')) #Etiquetas a GPU
        loss.backward()  # Backward pass
        optimizer.step()  # Optimizamos pesos
        total_loss.append(loss.item())  # Cálculo de la función de pérdida
        train_pred = output.argmax(dim=1, keepdim=True)
        correct += train_pred.eq(target.to('cuda').view_as(train_pred)).sum().item()
    loss_list.append(sum(total_loss) / len(total_loss))
    accuracy = 100 * correct / len(train_loader) #Cálculo de precisión
    total_accuracy.append(accuracy) 
    print(f"Training [{100.0 * (epoch + 1) / epochs:.0f}%]\tLoss: {loss_list[-1]:.4f}\tAccuracy: {accuracy:.2f}%")

# Grafico de Convergencia de la función de pérdida y precisión
fig, ax1 = plt.subplots()
ax1.plot(loss_list, 'g-')
ax2 = ax1.twinx()
ax2.plot(total_accuracy, 'b')
plt.title("Hybrid NN Training Convergence", color='red')
ax1.set_xlabel("Training Iterations")
ax1.set_ylabel("Cross Entropy Loss", color='g')
ax2.set_ylabel("Accuracy (%)", color='b')
plt.show()

torch.save(model.state_dict(), "model.pt")

qnn1 = create_qnn()
model1 = Net(qnn1)
model1.load_state_dict(torch.load("model.pt"))
model1= model1.to('cuda')

batch_size=1

model1.eval()  # Evaluación del Modelo
pred_targets = []
test_targets= []
with no_grad():

    correct = 0
    for batch_idx, (data, target) in enumerate(test_loader):
        output = model1(data.to('cuda'))
        if len(output.shape) == 1:
            output = output.reshape(1, *output.shape)

        pred = output.argmax(dim=1, keepdim=True)
        pred_targets.append(pred.item())
        test_targets.append(target.item())
        correct += pred.eq(target.to('cuda').view_as(pred)).sum().item()

        loss = loss_func(output, target.to('cuda'))
        total_loss.append(loss.item())

    print(
        "Performance on test data:\n\tLoss: {:.4f}\n\tAccuracy: {:.2f}%".format(
            sum(total_loss) / len(total_loss), correct / len(test_loader) / batch_size * 100
        )
    )

# Ploteo de Imagenes Predichas
from PIL import Image

n_samples_show = 6
count = 0
fig, axes = plt.subplots(nrows=1, ncols=n_samples_show, figsize=(10, 3))

model1.eval()
with no_grad():
    for batch_idx, (data, target) in enumerate(test_loader):
        if count == n_samples_show:
            break
        output = model1(data.to('cuda')[0:1])
        if len(output.shape) == 1:
            output = output.reshape(3, *output.shape)

        pred = output.argmax(dim=1, keepdim=True)

        axes[count].imshow(torchvision.transforms.ToPILImage(mode='RGB')(data[0].squeeze()), cmap=plt.cm.rainbow)

        axes[count].set_xticks([])
        axes[count].set_yticks([])
        axes[count].set_title("Predicted {}".format(pred.item()))

        count += 1

matrix_confusion(test_targets, pred_targets)