BERT

                                from transformers import BertTokenizer

                                #se descarga el tokenizador preentrenado para usarlo
                                tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
                                word = "Hola como estas"
                                #se inicia con el proceso principal de tokenización
                                tokens = tokenizer.tokenize(word)

                                #['ho', '##la', 'como', 'est', '##as']

                                import torch
                                from transformers import BertModel

                                model = BertModel.from_pretrained("bert-base-uncased")

                                #La librería transformers de huggingface te descarga e instala el modelo que vayas a usar en vez de instalar todo de golpe  
                                #usa tensorflow o pytorch como backend  
                                #mensajes de advertencia, se pueden ignorar

                                #2025-08-11 17:52:58.177918: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:467] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
                                #WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
                                #E0000 00:00:1754952778.233396    9070 cuda_dnn.cc:8579] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
                                #E0000 00:00:1754952778.249125    9070 cuda_blas.cc:1407] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
                                #W0000 00:00:1754952778.360050    9070 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
                                #W0000 00:00:1754952778.360072    9070 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
                                #W0000 00:00:1754952778.360074    9070 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
                                #W0000 00:00:1754952778.360076    9070 computation_placer.cc:177] computation placer already registered. Please check linkage and avoid linking the same target more than once.
                                #2025-08-11 17:52:58.374118: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
                                #To enable the following instructions: AVX2 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.

                                print(tokenizer.name_or_path)  # muestra el nombre del modelo de tokenización

                                #busca el ID correspondiente a la palabra "King"
                                king_token_id = tokenizer.convert_tokens_to_ids(["king"])[0] #al ser uncased, no distingue entre mayúsculas y minúsculas
                                print(f"ID de la palabra 'king': {king_token_id}") #da 2332

                                #inicia el proceso de embedding
                                king_embedding = model.embeddings.word_embeddings(torch.tensor([king_token_id]))
                                print(f"Embedding de la palabra 'king':\n{king_embedding}") #da un tensor de 768 dimensiones


                                #los embeddings devuelven un tensor, (una matriz multidimensional)
                                #no los pego todos porque son muy largos, pero el resultado es similar a este:

                                '''
                                bert-base-uncased
                                ID de la palabra 'king': 2332
                                Embedding de la palabra 'king':
                                tensor([[ 1.0459e-02, -4.1597e-02, -2.8762e-02, -2.1271e-03, -3.6137e-02,
                                        -5.9453e-02, -1.8821e-02, -5.3518e-02, -4.5944e-02, -1.0334e-01,
                                        -2.6336e-02, -3.7564e-02,  4.7280e-05, -4.1316e-02,  1.1089e-03,
                                        -3.4041e-02,  3.6455e-03, -4.3182e-03,  4.5244e-02, -3.2792e-03,
                                        -2.9757e-02, -1.2348e-02,  2.7182e-02,  1.8084e-02, -8.3842e-03,
                                        -4.1677e-02,  1.1041e-02,  3.2859e-03,  2.8347e-02, -1.0402e-02,
                                        5.0695e-02, -5.3480e-02, -3.2858e-02, -2.4704e-02, -4.6392e-03,
                                        -9.3113e-03,  3.9658e-02,  1.9593e-02, -3.8411e-03, -2.7013e-02,
                                        -4.1830e-02, -3.2505e-02,  4.3060e-02, -3.7079e-02, -4.0751e-02,
                                        2.2578e-02, -4.2186e-02, -9.9416e-03,  1.2697e-02,  4.2084e-02,
                                        1.2595e-02, -1.4879e-02, -7.6563e-02,  4.5787e-03,  2.0950e-03,
                                        -5.9803e-02, -8.0030e-02, -1.9449e-02]], grad_fn=)
                                ID de la palabra 'man': 2158
                                Embedding de la palabra 'man':
                                tensor([[-7.6646e-03, -3.1175e-02, -6.9835e-03, -6.3297e-03, -1.1371e-02,
                                        -1.7873e-02, -1.1434e-02,  1.0309e-02, -1.0764e-02, -1.9303e-02,
                                        2.6435e-02, -1.0661e-03, -2.0635e-02, -9.0213e-03, -5.0599e-02,
                                        -4.1205e-02, -2.5394e-02, -1.8961e-02,  7.6851e-03,  1.4840e-03,
                                        -5.5302e-02,  3.7641e-03, -7.2016e-02, -7.0809e-03, -6.7614e-02,
                                        4.3692e-02, -2.9522e-02,  1.2630e-02, -1.0300e-02,  2.8095e-02,
                                        1.0858e-02, -2.7949e-02, -3.5180e-02, -1.6189e-02, -1.0497e-02,
                                        -1.5819e-03, -1.0177e-02,  1.2386e-02, -4.2518e-03, -9.6997e-02,
                                        -7.0927e-02,  1.4870e-02,  7.9696e-03,  1.7460e-02, -2.9262e-02,
                                        5.2394e-03,  2.9746e-02, -1.8923e-02,  4.0893e-05, -2.5314e-02,
                                        1.3420e-02,  3.3399e-02,  1.3250e-02, -9.0819e-02,  1.2010e-02,
                                        -7.1084e-02,  5.4406e-03, -2.4264e-02, -2.8006e-02,  3.1375e-02,
                                        4.4374e-03,  3.9825e-02, -6.9961e-02, -3.3235e-03, -1.9383e-02,
                                        7.5572e-03, -4.2687e-02, -4.2618e-02]], grad_fn=)
                                ID de la palabra 'woman': 2450
                                Embedding de la palabra 'woman':
                                tensor([[ 1.2108e-02, -3.0078e-02,  1.8451e-02,  7.0369e-03, -1.1504e-02,
                                        -3.9431e-02, -1.4481e-02,  2.9047e-02,  6.7598e-03, -3.0685e-02,
                                        -1.2698e-02, -5.3419e-02, -1.7568e-02,  4.7697e-02, -6.1417e-02,
                                        -6.1463e-02, -4.8997e-04, -4.8986e-02, -5.5195e-02, -4.3894e-02,
                                        -4.6776e-02, -4.0752e-02, -6.8575e-02, -2.2964e-02, -8.3177e-02,
                                        -1.8618e-02, -1.4952e-02,  1.2415e-03, -1.3154e-03,  2.1765e-03,
                                        6.1241e-02, -4.9063e-02, -2.1670e-02, -5.4052e-03, -3.4771e-02,
                                        -3.4587e-02, -1.0134e-02,  8.1198e-03, -3.4730e-02, -5.6305e-02,
                                        -7.1823e-02,  3.1932e-03, -3.6441e-05, -1.0609e-02, -9.4444e-03,
                                        -1.9346e-02,  2.4101e-02, -9.0241e-03, -2.2390e-02,  1.0220e-02,
                                        -6.3611e-02,  2.6824e-02, -9.4787e-03, -1.7875e-02, -1.5212e-02,
                                        -1.2356e-02,  2.8417e-03, -3.5529e-02, -5.9514e-02,  4.6299e-02,
                                        -4.3715e-02, -6.1539e-02, -1.2442e-02, -2.3420e-03,  2.8436e-02,

                                ID de la palabra 'queen': 3035
                                Embedding de la palabra 'queen':
                                tensor([[ 5.7453e-02, -8.8483e-02, -5.9414e-02,  1.1624e-02, -3.5926e-02,
                                        -8.9322e-02,  5.8101e-02, -3.5651e-03,  3.9393e-02, -8.2666e-02,
                                        -3.2993e-02, -7.5832e-02, -9.6076e-03, -6.7638e-03, -3.8710e-02,
                                        -3.5043e-02,  7.9793e-03, -3.3161e-02, -1.0924e-02, -2.8418e-02,
                                        -1.1208e-02, -2.6782e-02, -1.3545e-02,  5.8899e-02,  1.1604e-02,
                                        -6.2912e-02,  2.5502e-02, -1.7892e-02, -1.5072e-02, -1.8363e-02,
                                        5.2739e-02, -5.0321e-02, -2.7009e-02, -2.8599e-03, -5.5977e-02,
                                        -3.6614e-03,  3.9993e-02,  6.6724e-02, -2.6716e-02, -1.2417e-02,
                                        -9.0510e-02, -1.2854e-02,  1.6648e-02, -3.3466e-02, -2.4335e-02,
                                        5.5003e-02, -4.1292e-02, -1.7306e-02, -4.1601e-02, -1.0229e-02,
                                        9.4465e-03, -6.1036e-02, -3.3751e-02, -3.0292e-02, -1.2840e-02,
                                        -4.8447e-02, -2.1505e-02,  3.2084e-02, -5.3645e-02, -2.4236e-02,
                                '''

                                man_token_id = tokenizer.convert_tokens_to_ids(["man"])[0]
                                print(f"ID de la palabra 'man': {man_token_id}") #da 2158

                                man_embedding = model.embeddings.word_embeddings(torch.tensor([man_token_id]))
                                print(f"Embedding de la palabra 'man':\n{man_embedding}") 

                                woman_token_id = tokenizer.convert_tokens_to_ids(["woman"])[0]
                                print(f"ID de la palabra 'woman': {woman_token_id}") #da 2450

                                woman_embedding = model.embeddings.word_embeddings(torch.tensor([woman_token_id]))
                                print(f"Embedding de la palabra 'woman':\n{woman_embedding}") 

                                queen_token_id = tokenizer.convert_tokens_to_ids(["queen"])[0]
                                print(f"ID de la palabra 'queen': {queen_token_id}") #da 3035

                                queen_embedding = model.embeddings.word_embeddings(torch.tensor([queen_token_id]))
                                print(f"Embedding de la palabra 'queen':\n{queen_embedding}")

                                #orden de los token ids resultantes de las palabras
                                sorted_token_ids = sorted([king_token_id, man_token_id, woman_token_id, queen_token_id])
                                print(f"token IDs ordenados: {sorted_token_ids}\nvalor de cada token ID: {[tokenizer.convert_ids_to_tokens([id])[0] for id in sorted_token_ids]}")

                                #aqui podemos ver que al ser conceptos cercanos, los valores numericos de sus token IDs también son cercanos
                                #token IDs ordenados: [2158, 2332, 2450, 3035]
                                #valor de cada token ID: ['man', 'king', 'woman', 'queen']

                                #calcular similitud entre embeddings usando la similitud coseno
                                cos = torch.nn.CosineSimilarity()
                                similarity = cos(king_embedding, queen_embedding)
                                print(f"Similitud entre 'king' y 'queen': {similarity.item()}") #da un valor cercano a 0.8

                                #la similitud de estos 2 conceptos es alta, ya que ambos representan figuras de autoridad en un contexto monárquico
                                #si el valor numérico es más alto (cerca a 1.0), significa que los conceptos están más cerca entre si, en el espacio vectorial
                                #Similitud entre 'king' y 'queen': 0.6468513011932373

                                #nuevas palabras a probar
                                dog_token_id = tokenizer.convert_tokens_to_ids(["dog"])[0]
                                print(f"ID de la palabra 'dog': {dog_token_id}")

                                wolf_token_id = tokenizer.convert_tokens_to_ids(["wolf"])[0]
                                print(f"ID de la palabra 'wolf': {wolf_token_id}")

                                cat_token_id = tokenizer.convert_tokens_to_ids(['cat'])[0]
                                print(f"ID de la palabra 'cat': {cat_token_id}")

                                lion_token_id = tokenizer.convert_tokens_to_ids(['lion'])[0]
                                print(f"ID de la palabra 'lion': {lion_token_id}")

                                #orden de los token ids resultantes de las palabras
                                sorted_token_ids = sorted([dog_token_id, wolf_token_id, cat_token_id, lion_token_id])
                                print(f"token IDs ordenados: {sorted_token_ids}\nvalor de cada token ID: {[tokenizer.convert_ids_to_tokens([id])[0] for id in sorted_token_ids]}")

                                #ID de la palabra 'dog': 3899
                                #ID de la palabra 'wolf': 4702
                                #ID de la palabra 'cat': 4937
                                #ID de la palabra 'lion': 7006
                                #token IDs ordenados: [3899, 4702, 4937, 7006]
                                #valor de cada token ID: ['dog', 'wolf', 'cat', 'lion']

                                #palabras personalizadas

                                word1 = input("Enter the first word: ").lower()
                                word2 = input("Enter the second word: ").lower()
                                word3 = input("Enter the third word: ").lower()
                                word4 = input("Enter the fourth word: ").lower()

                                tokenid1 = tokenizer.convert_tokens_to_ids([word1])[0]
                                print(f"ID de la palabra '{word1}': {tokenid1}")

                                tokenid2 = tokenizer.convert_tokens_to_ids([word2])[0]
                                print(f"ID de la palabra '{word2}': {tokenid2}")

                                tokenid3 = tokenizer.convert_tokens_to_ids([word3])[0]
                                print(f"ID de la palabra '{word3}': {tokenid3}")

                                tokenid4 = tokenizer.convert_tokens_to_ids([word4])[0]
                                print(f"ID de la palabra '{word4}': {tokenid4}")

                                sorted_token_ids = sorted([tokenid1, tokenid2, tokenid3, tokenid4])
                                print(f"token IDs ordenados: {sorted_token_ids}\nvalor de cada token ID: {[tokenizer.convert_ids_to_tokens([id])[0] for id in sorted_token_ids]}")
                            


                                Embedding de la palabra 'king':
                                tensor([[ 1.0459e-02, -4.1597e-02, -2.8762e-02, -2.1271e-03, -3.6137e-02,
                                        -5.9453e-02, -1.8821e-02, -5.3518e-02, -4.5944e-02, -1.0334e-01,
                                        -2.6336e-02, -3.7564e-02,  4.7280e-05, -4.1316e-02,  1.1089e-03,
                                        -3.4041e-02,  3.6455e-03, -4.3182e-03,  4.5244e-02, -3.2792e-03,
                                        ...
                                        -5.9803e-02, -8.0030e-02, -1.9449e-02]], grad_fn=)
                            


                                cos = torch.nn.CosineSimilarity()
                                similarity = cos(king_embedding, queen_embedding)
                                print(f"Similitud entre 'king' y 'queen': {similarity.item()}") #da un valor cercano a 0.8

                                #la similitud de estos 2 conceptos es alta, ya que ambos representan figuras de autoridad en un contexto monárquico
                                #si el valor numérico es más alto (cerca a 1.0), significa que los conceptos están más cerca entre si, en el espacio vectorial
                                #Similitud entre 'king' y 'queen': 0.6468513011932373
                            


                                vector('King') - vector('Man') + vector('Woman') ≈ vector('Queen')
                            


                                #un escalar es un tipo de variable que solo tiene un valor, puede ser int, float, str, bool
                                #es simplemente un número real que se utiliza para: multplicar vectores o matrices o cambiar su magnitud
                                escalar = 2.65
                                print(f"Escalar float: {escalar}")
                                escalar_bool = True
                                print(f"Escalar bool: {escalar_bool}")

                                #Verificar tipos de variables
                                print(f"Tipo de escalar: {type(escalar)}")
                                print(f"Tipo de escalar: {type(escalar_bool)}")

                                #un vector es un conjunto de numeros reales
                                #Una matriz es una estructura bidimensional de números reales, que puede representarse como un conjunto ordenado de vectores fila o columna.
                                #Un tensor es una estructura multidimensional que generaliza a vectores y matrices a más dimensiones.

                                #importaciones
                                import numpy as np

                                vector = np.array([1, 2, 3, 4])
                                print(f"Vector: {vector}")
                                #.shape() nos devuelve las dimensiones del objeto en cuestión
                                print(f"\nDimensiones de Vector: {np.shape(vector)}") #(4,) -> 4 elementos
                                print(f"\nCantidad de elementos en Vector: {np.size(vector)}")

                                matriz = np.array([[1, 2, 3],
                                                    [4, 5, 6],
                                                    [7, 8, 9]])

                                print(f"\nMatriz: {matriz}")
                                print(f"\nDimensiones de Matríz: {np.shape(matriz)}") #(3, 3) -> 3 filas, 3 columnas
                                print(f"\nCantidad de elementos en Matríz: {np.size(matriz)}")

                                tensor = np.array([
                                    [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                                    [[10, 11, 12], [13, 14, 15], [16, 17, 18]],
                                    [[19, 20, 21], [22, 23, 24], [25, 26, 27]]
                                ])
                                print(f"\nTensor:\n{tensor}")
                                print(f"\nDimensiones de Tensor: {np.shape(tensor)}") #(3, 3, 3) ->3 matrizes, 3 filas, 3 columnas
                                print(f"\nCantidad de elementos en Tensor: {np.size(tensor)}")

                                import matplotlib.pyplot as plt

                                #graficar el tensor estructurado mediante escala de grises
                                plt.imshow(tensor, interpolation='nearest')
                                plt.show()
                            

                                tensor_rgb = np.array([
                                    [[0, 255, 0], [0, 255, 0], [0, 255, 0]],
                                    [[0, 159, 193], [0, 159, 193], [0, 159, 193]],
                                    [[0, 0, 255], [0, 0, 255], [0, 0, 255]]
                                ])

                                #matplotlib interpreta cada vector como colores rgb
                                plt.imshow(tensor_rgb, interpolation='nearest')
                                plt.show()
                            

                                #transposición de vectores y matrices
                                escalar = 2.65

                                vector = np.array([1, 2, 3, 4])
                                vector1  = np.array([5, 6, 7, 8])

                                matriz = np.array([[1, 2],
                                                [3, 4],
                                                [5, 6]])
                                print(f"Matríz original:\n{matriz}")
                                print(f"\nDimensiones de la Matríz: {np.shape(matriz)}")
                                matriz_t = np.transpose(matriz)
                                print(f"\nMatriz transpocisionada:\n{matriz_t}")
                                print(f"\nDimensiones de la Matríz transposicionada: {np.shape(matriz_t)}")
                                tensor = np.array([
                                    [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
                                    [[10, 11, 12], [13, 14, 15], [16, 17, 18]],
                                    [[19, 20, 21], [22, 23, 24], [25, 26, 27]]
                                ])
                                print(f"\nTensor:\n{tensor}")
                                print(f"\nDimensiones del tensor: {np.shape(tensor)}")
                                tensor_t = np.transpose(tensor)
                                print(f"\nTensor trasposicionado:\n{tensor_t}")
                                print(f"\nDimensiones del tensor transposicionado:\n{np.shape(tensor_t)}")

                                #suma de matríces (IMPORTANTE: para hacer una suma entre 2 o más matrices, vectores o tensores, tienen que tener las mismas dimensiones, en este caso (2x2))
                                a = np.array([[1, 2, 3], [4, 5, 6],
                                            [7, 8, 9], [10, 11, 12]])
                                print(f"\nDimensiones de la Matríz a: {np.shape(a)}")

                                b = np.array([[13, 14, 15], [16, 17, 18],
                                            [19, 20, 21], [22, 23, 24]])
                                print(f"\nDimensiones de la Matríz b: {np.shape(b)}")

                                c = a + b

                                print(f"\nSuma de las 2 matrices:\n{c}")

                                d = matriz + escalar

                                print(f"\nSuma de la matríz más el escalar:\n{d}")

                                #suma de vectores
                                e = vector + vector1
                                print(f"\nSuma de 2 vectores:\n{e}")

                                #suma de vectores o matrices con dimensiones distintas
                                #reglas del broadcasting de Python

                                vector2 = np.array([3, 4, 5]) # vector con 3 valores

                                print(f"\nVector 2 inicial:\n{vector2}")

                                matriz2 = np.array([[1, 2], # matríz de dimensión 3x2
                                                    [3, 4],
                                                    [5, 6]])
                                print(f"\nMatríz 2 inicial:\n{matriz2}")

                                matriz2_t = np.transpose(matriz2) # matríz de dimensión 2x3
                                print(f"\nMatríz 2 transposicionada:\n{matriz2_t}")

                                #lo que queremos hacer:
                                #sumar      ([3, 4, 5]) + ([[1, 2],   ##ESTO NO SE PUEDE DEBIDO A QUE NO PODEMOS ASIGNAR CORRECTAMENTE LOS 3 VALORES NECESARIOS PARA CADA OPERACIÓN
                                #                           [3, 4],
                                #                           [5, 6]])

                                #sumar      ([3, 4, 5]) + [[1 3 5]  ##ESTO SI SE PUEDE DEBIDO A QUE PODEMOS ASIGNAR CORRECTAMENTE LOS 3 VALORES NECESARIOS PARA CADA OPERACIÓN
                                #                          [2 4 6]] ##EL BROADCASTING SE ENCARGA DE EXTENDER UN VECTOR O MATRÍZ QUE LO NECESITE PARA PODER HACER LA OPERACIÓN

                                #efectuamos la suma correspondiente

                                suma_final = vector2 + matriz2_t
                                print(f"\nSuma final resultante:\n{suma_final}")

                                import matplotlib.pyplot as plt
                                import matplotlib.image as mpimg
                                #multiplicación de matrices y vectores
                                escalar = 2.65 #escalar = λ

                                vector = np.array([1, 2])
                                vector1  = np.array([5, 6, 7, 8])

                                matriz = np.array([[1, 2],
                                                [3, 4],
                                                [5, 6]])

                                multiplicacion = vector * matriz # EN ESTE CASO PYTHON HACE BROADCASTING INTERNAMENTE
                                print(f"\n Resultado de la multiplicación:\n{multiplicacion}")

                                #producto interno nos devuelve un vector con los valores resultantes de la suma de los productos de cada fila

                                #           ([[1, 2],               1*1 + 2*2 = 5
                                #             [3, 4], * ([1, 2]) =  3*1 + 4*2 = 11 = [5, 11, 17]
                                #             [5, 6]])              5*1 + 6*2 = 17

                                producto_interno = np.dot(matriz, vector)
                                print(f"\nProducto interno resultante:\n{producto_interno}")

                                #multiplicación entre matríces
                                matriz_a = np.array([[1, 2, 3],
                                                    [4, 5, 6],
                                                    [7, 8, 9],
                                                    [10, 11, 12]])
                                print(f"\nDimensiones de matríz a: {np.shape(matriz_a)}")

                                matriz_b = np.array([[13, 14, 15],
                                                    [16, 17, 18],
                                                    [19, 20, 21],
                                                    [22, 23, 24]])
                                print(f"\nDimensiones de matríz b: {np.shape(matriz_b)}")

                                matriz_c = np.array([[2, 3],
                                                    [5, 7],
                                                    [11, 13]])
                                print(f"\nDimensiones de matríz c: {np.shape(matriz_c)}")

                                multip = matriz_a * matriz_b
                                print(f"\nResultado de la multiplicación elemento por elemento entre 2 matríces:\n{multip}")

                                matriz_b_t = np.transpose(matriz_b)
                                print(f"\nMatríz b transpuesta:\n{matriz_b_t}")
                                print(f"\nDimensiones de la matríz b transpuesta:\n{np.shape(matriz_b_t)}")

                                multip = np.dot(matriz_a, matriz_b_t)#multiplicación de matrices compatible (4*3)(3*4)
                                print(f"\nResultado de la multiplicación tradicional entre matriz a y matriz b:\n{multip}")

                                #producto interno matriz_a * matriz_c ##CUANDO TENGO, COMO EN ESTE CASO, UNA ALINEACIÓN EN 2 VALORES, SE PUEDE EFECTUAR EL PI (4, 3)(3, 2)
                                #                   ([[1, 2, 3],          ([[2, 3],       1*2 + 2*5 + 3*11 = 45  -  1*3 + 2*7 + 3*13 = 56                 [[45 56]
                                #                     [4, 5, 6],      *     [5, 7],    =  4*2 + 5*5 + 6*11 = 99  -  4*3 + 5*7 + 6*13 = 125          =      [99 125]
                                #                     [7, 8, 9],            [11, 13]])    7*2 + 8*5 + 9* 11 = 153  -  7*3 + 8*7 + 9*13 = 194               [153 194]
                                #                     [10, 11, 12]])                      10*2 + 11*5 + 12*11 = 207  -  10*3 + 11*7 + 12*13 = 263          [207 263]]

                                producto_interno = np.dot(matriz_a, matriz_c)
                                print(f"\nProducto interno resultante:\n{producto_interno}")


                                img = mpimg.imread('producto interno.png')
                                plt.figure(figsize=(10, 10))
                                plt.imshow(img)
                                plt.show()

                                #propiedades del producto interno

                                A = np.array([[2, 3],
                                            [5, 7],
                                            [11, 13]])
                                print(f"\nDimensiones de la matríz A:\n{np.shape(A)}")

                                B = np.array([[1, 3],
                                            [2, 1]])
                                print(f"\nDimensiones de la matríz B\n{np.shape(B)}")

                                C = np.array([[3, 1],
                                            [4, 2]])
                                print(f"Dimensiones de la matríz C:\n{np.shape(C)}")
                                #propiedad asociativa
                                #RECORDATORIO: Para que puedas multiplicar dos matrices A*B, se debe cumplir:
                                #El número de columnas de A = número de filas de B    Si A es de forma (m * n) y B es de forma (n * p), entonces AB es de forma (m × p)
                                #El orden de agrupación no afecta el resultado. (A * B) * C = A * (B * C)

                                #A*(B*C) = (A*B)*C

                                #A * (B * C)
                                ABC = A.dot(B.dot(C))
                                print(f"\nMultiplicación: A*(B*C):\n{ABC}")

                                #(A * B) * C
                                AB_C = A.dot(B).dot(C)
                                print(f"\nMultiplicación: (A*B)*C:\n{AB_C}")

                                #comprobamos la igualdad entre las 2 matríces resultantes
                                print(f"\nIgualdad entre ABC y AB_C: {np.array_equal(ABC, AB_C)}")

                                # La propiedad distributiva establece que:
                                # A * (B + C) = (A * B) + (A * C)
                                # Esto significa que la multiplicación de una matriz A por la suma de dos matrices B y C
                                # es igual a la suma de la multiplicación de A por B y la multiplicación de A por C.

                                D = A.dot(B+C)
                                E = (A.dot(B)) + (A.dot(C))

                                print(f"\nA*(B+C):\n{D}")
                                print(f"\n(A*B)+(A*C):\n{E}")

                                #comprobamos la igualdad entre las 2 matrices resultantes
                                print(f"\nIgualdad entre D y E: {np.array_equal(D, E)}")

                                # La propiedad conmutativa establece que:
                                # A * B = B * A
                                # Esto significa que el orden de la multiplicación no afecta al resultado.
                                # Sin embargo, esta propiedad no se cumple en la multiplicación de matrices,
                                # pero sí en la multiplicación de vectores (producto escalar).

                                F = np.dot(B, C)
                                print(f"\nProducto interno B*C:\n{F}\ny sus dimensiones:\n{np.shape(F)}")
                                G = np.dot(C, B)
                                print(f"\nProducto interno C*B:\n{G}\ny sus dimensiones:\n{np.shape(G)}")

                                #comprobamos la igualdad entre las 2 matrices resultantes
                                print(f"\nIgualdad entre F y G: {np.array_equal(F, G)}") ##FALSE, NO APLICA CON MULTIPLICACIÓN DE MATRICES

                                #LA MULTIPLICACIÓN DE VECTORES SI ES CONMUTATIVA

                                vector1 = np.array([3, 7]) #multiplicación ([3, 7]) * ([3, 5]) = 3*3 + 7*5 = 44
                                vector2 = np.array([3, 5]) #multiplicación ([3, 5]) * ([3, 7]) = 3*3 + 5*7 = 44

                                H = np.dot(vector1, vector2)
                                print(f"\nProducto interno vector1 * vector2:\n{H}\ny sus dimensiones:\n{np.shape(H)}")
                                I = np.dot(vector2, vector1)
                                print(f"\nProducto interno vector2 * vector1:\n{I}\ny sus dimensiones:\n{np.shape(I)}")
                            


                                import os
                                import cv2

                                def extract_frames(video_path, output_folder, interval=30):
                                    os.makedirs(output_folder, exist_ok=True)

                                    cap = cv2.VideoCapture(video_path)

                                    frame_count = 0
                                    saved_frames = 0

                                    while cap.isOpened(): 
                                        ret, frame = cap.read()
                                        if not ret:
                                            break

                                        if frame_count % interval == 0:
                                            filename = os.path.join(output_folder, f"{os.path.basename(video_path).split('.')[0]}_frame_{saved_frames}.jpg")
                                            cv2.imwrite(filename, frame)
                                            saved_frames += 1

                                        frame_count += 1

                                    cap.release()
                            


                                import tensorflow as tf
                                import numpy as np
                                import json
                                import matplotlib.pyplot as plt
                                from keras.src.legacy.preprocessing.image import ImageDataGenerator
                                from keras.src.callbacks import EarlyStopping, ModelCheckpoint
                                from keras.src.applications.mobilenet_v2 import preprocess_input, MobileNetV2
                                from keras.src import layers, models

                                # Path to the train data
                                data_path = "vidframes"

                                # Callbacks
                                callbacks = [
                                    EarlyStopping(patience=10, restore_best_weights=True),
                                    ModelCheckpoint("best_model.keras", save_best_only=True)
                                ]

                                # Data Augmentation + Preprocess MobileNetV2
                                datagen = ImageDataGenerator(
                                    preprocessing_function=preprocess_input,
                                    validation_split=0.2,
                                    rotation_range=15,
                                    width_shift_range=0.1,
                                    height_shift_range=0.1,
                                    shear_range=0.1,
                                    zoom_range=0.2,
                                    horizontal_flip=True,
                                    brightness_range=[0.8, 1.2],
                                    fill_mode='nearest'
                                )

                                train_data = datagen.flow_from_directory(
                                    data_path,
                                    target_size=(160, 160),  # Size for MobileNetV2
                                    batch_size=32,
                                    class_mode='categorical',
                                    subset='training'
                                )

                                val_data = datagen.flow_from_directory(
                                    data_path,
                                    target_size=(160, 160),
                                    batch_size=32,
                                    class_mode='categorical',
                                    subset='validation'
                                )
                            


                                from keras.src.legacy.preprocessing.image import ImageDataGenerator
                                from keras.src.callbacks import EarlyStopping, ModelCheckpoint
                                from keras.src.applications.mobilenet_v2 import preprocess_input, MobileNetV2
                                from keras.src import layers, models

                                base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(160, 160, 3))
                            


                                for layer in base_model.layers:
                                    layer.trainable = False

                                for layer in base_model.layers[-30:]:  # Latest 30 layers
                                    layer.trainable = True

                                # Complete Model
                                model = models.Sequential([
                                    base_model,
                                    layers.GlobalAveragePooling2D(),
                                    layers.Dense(256, activation='relu'),
                                    layers.Dropout(0.5),
                                    layers.Dense(train_data.num_classes, activation='softmax')  
                                ])
                            


                                # Compile
                                model.compile(
                                    optimizer=tf.keras.optimizers.Adam(learning_rate=0.0005),
                                    loss='categorical_crossentropy',
                                    metrics=['accuracy']
                                )

                                # Training
                                history = model.fit(train_data, validation_data=val_data, epochs=50, callbacks=callbacks)
                                with open("train_history.json", "w").
                                    json.dump(history.history, f)

                                # Save Model .keras
                                model.save("gd_level_classifier.keras")