Normalization, pooiling gather

class ClosestToTargetPool2dFunction(torch.autograd.Function):

"""

Implementación de un Pooling que selecciona el valor más cercano a un 'target'.

"""



@staticmethod

def forward(ctx: Any, inputs: torch.Tensor, kernel_size: int, target_val: float) -> torch.Tensor:

    """

    Forward pass.

    Restricciones: 

    - Usa unfold y gather.

    - Para encontrar las distancias, puedes usar operaciones aritméticas básicas y torch.abs().

    - Usa torch.argmin() para encontrar el índice del más cercano.



    Args:

        ctx: Contexto.

        inputs: Tensor de entrada \[B, C, H, W\].

        kernel_size: Tamaño de la ventana (asumimos stride = 1).

        target_val: El valor objetivo (ej. 0.0, 5.5, etc.) al que queremos acercarnos.

    """

    B,C,H,W = inputs.shape

    H_out, W_out = H-kernel_size+1, W-kernel_size+1

    unfolded_inputs = torch.nn.functional.unfold(inputs,kernel_size)

    unfolded = unfolded_inputs.view(B,C,kernel_size\*\*2,-1)

    dif = torch.abs(unfolded-target_val)

    indexes = dif.argmin(2,keepdim=True)

    pre_output = torch.gather(unfolded, 2, indexes)

    ctx.save_for_backward(inputs, unfolded, indexes)

    ctx.kernel_size = kernel_size

    return pre_output.view(B,C,H_out,W_out)





@staticmethod

def backward(ctx: Any, grad_outputs: torch.Tensor) -> tuple\[torch.Tensor, None, None\]:

    """

    Backward pass.

    Recuerda: El gradiente de target_val es None porque es un float, no un tensor que aprenda.

    El gradiente de kernel_size también es None.

    """



    inputs, unfolded, indexes = ctx.saved_tensors

    K = ctx.Kernel_size

    B,C,H,W = inputs.shape

    unfold_out = torch.zeros_like(unfolded)

    unfold_out = unfold_out.scatter(2, indexes, grad_outputs.view(B,C,-1).unsqueeze(2))

    unfold = unfold_out.view(B,C\*K\*K,-1)

    output = torch.nn.functional.fold(unfold, (H,W), K)

    return output 

import torch

import torch.nn as torch_nn

from typing import Any, Tuple

class CustomLayerNormFunction(torch.autograd.Function):

"""

Implementación matemática de la normalización de capa (LayerNorm) con 

sus pasos forward y backward.

"""



@staticmethod

def forward(

    ctx: Any, 

    inputs: torch.Tensor, 

    gamma: torch.Tensor, 

    beta: torch.Tensor, 

    eps: float = 1e-5

) -> torch.Tensor:

    """

    Forward pass de Layer Normalization.



    Args:

        ctx: Contexto para guardar variables necesarias en el backward.

        inputs: Tensor de entrada. Asumiremos dimensiones \[B, D\] para simplificar, 

                donde normalizaremos sobre la última dimensión (D).

        gamma: Tensor de pesos aprendibles para la escala. Dimensiones \[D\].

        beta: Tensor de sesgos aprendibles para el desplazamiento. Dimensiones \[D\].

        eps: Pequeño valor flotante para evitar la división por cero.



    Returns:

        El tensor de salida normalizado, escalado y desplazado. Dimensiones \[B, D\].

        

    Notas para tu implementación:

        - Debes calcular la media y la varianza a lo largo de la dimensión correcta.

        - Guarda en 'ctx' todo lo necesario para calcular las derivadas de la media y varianza después.

    """

    \# TODO: Implementar la lógica forward

    pass



@staticmethod

def backward(

    ctx: Any, 

    grad_outputs: torch.Tensor

) -> Tuple\[torch.Tensor, torch.Tensor, torch.Tensor, None\]:

    """

    Backward pass de Layer Normalization.



    Args:

        ctx: Contexto con los tensores guardados en el forward.

        grad_outputs: Gradiente de la pérdida respecto a la salida de esta capa. Dimensiones \[B, D\].



    Returns:

        Una tupla con los gradientes respecto a cada entrada del forward:

        - grad_inputs: Gradiente respecto a 'inputs'. Dimensiones \[B, D\].

        - grad_gamma: Gradiente respecto a 'gamma'. Dimensiones \[D\].

        - grad_beta: Gradiente respecto a 'beta'. Dimensiones \[D\].

        - None: El gradiente de 'eps' es None porque es una constante.

    """

    \# TODO: Implementar la lógica backward (Derivadas parciales de x, gamma y beta)

    pass

class CustomLayerNorm(torch_nn.Module):

"""

Módulo de PyTorch que envuelve nuestra CustomLayerNormFunction y maneja 

los parámetros aprendibles (gamma y beta).

"""



def \__init_\_(self, normalized_shape: int, eps: float = 1e-5):

    """

    Inicializa la capa.



    Args:

        normalized_shape: El tamaño de la dimensión a normalizar (D).

        eps: Valor para estabilidad numérica.

    """

    super().\__init_\_()

    self.normalized_shape = normalized_shape

    self.eps = eps



    \# TODO: Define 'self.gamma' como un nn.Parameter inicializado en unos.

    \# TODO: Define 'self.beta' como un nn.Parameter inicializado en ceros.

    pass



def forward(self, x: torch.Tensor) -> torch.Tensor:

    """

    Aplica la Layer Normalization utilizando CustomLayerNormFunction.



    Args:

        x: Tensor de entrada \[B, D\].



    Returns:

        Tensor normalizado \[B, D\].

    """

    \# TODO: Llamar a CustomLayerNormFunction.apply con los parámetros correspondientes.

    pass

“”"

This module contains the code to implement the LayerNorm.

“”"

# 3pps

import torch

from typing import Union, List, Tuple

class LayerNorm(torch.nn.Module):

"""

This class implements the LayerNorm layer of torch.



Attributes:

    normalized_shape: Input shape from an expected input of size.

    eps: epsilon to avoid division by 0.

    weight: Learnable parameter (gamma) for scaling.

    bias: Learnable parameter (beta) for shifting.

"""



def \__init_\_(self,  normalized_shape: Union\[int, List\[int\], Tuple\[int\]\], eps: float = 1e-5) -> None:

    """

    This method is the constructor of LayerNorm class.



    Args:

        normalized_shape: Input shape from an expected input of size.

        eps: epsilon to avoid division by 0. Defaults to 1e-5.



    Returns:

        None.

    """



    \# Call super class constructor

    super().\__init_\_()



    \# TODO: Set attributes

    \# TODO: Initialize learnable parameters (weight and bias) using torch.nn.Parameter. 

    \# weight should be initialized to 1s, bias to 0s.



    return None



def forward(self,G, inputs: torch.Tensor) -> torch.Tensor:

    """

    This method is the forward pass of the layer.



    Args:

        inputs: Inputs tensor. Dimensions: \[batch, \*, normalized_shape\].



    Returns:

        Outputs tensor. Dimensions: \[batch, \*, normalized_shape\].

    """



    \# TODO: Implement the forward pass.

    \# Requirement: Do it efficiently WITHOUT using loops.

    \# Remember to apply the learnable weight and bias at the end.



    B,C,H,W = inputs.shape

    var, mean = torch.var_mean(inputs, (1,2,3))

    norm_inputs = (inputs-mean)/(torch.sqrt(var+self.eps))



    output = norm_inputs.mamul(self.weight.T)+self.bias



    #Lógica simple de BathNorm

    var, mean = torch.var_mean(inputs,(0,2,3))

    

    #Lógica simple GroupNorm G grupos

    salto =  C//G

    tr_inputs = inputs.view(B,G,salto,H,W)

    var, mean = torch.var_mean(tr_inputs(2,3,4))



    #Finalmente hacemos un reshape o view para un output correcto

import torch

from typing import Any

class EmbeddingFunction(torch.autograd.Function):

"""

Implementación manual de nn.Embedding utilizando gather y scatter_add.

"""



@staticmethod

def forward(ctx: Any, weight: torch.Tensor, indices: torch.Tensor) -> torch.Tensor:

    """

    Args:

        weight: Matriz de embeddings. Dimensiones: \[V, D\].

        indices: Índices de los tokens. Dimensiones: \[B, S\].

    Returns:

        Output tensor. Dimensiones: \[B, S, D\].

    """

    \# Guardamos datos necesarios para el backward

    ctx.save_for_backward(indices)

    ctx.V = weight.size(0)



    B, S = indices.shape

    V, D = weight.shape



    \# 1. Expandimos 'weight' a \[B, V, D\]

    \# Nota: expand no consume memoria extra, solo ajusta los strides.

    weight_expanded = weight.unsqueeze(0).expand(B, V, D)



    \# 2. Expandimos 'indices' a \[B, S, D\]

    \# Añadimos la dimensión final y la copiamos D veces.

    indices_expanded = indices.unsqueeze(-1).expand(B, S, D)



    \# 3. Aplicamos gather en la dimensión 1 (Vocabulario)

    output = torch.gather(weight_expanded, dim=1, index=indices_expanded)



    return output



@staticmethod

def backward(ctx: Any, grad_outputs: torch.Tensor) -> tuple\[torch.Tensor, None\]:

    """

    Args:

        grad_outputs: Gradientes del output. Dimensiones: \[B, S, D\].

    Returns:

        Gradientes del peso 'weight'. Dimensiones: \[V, D\].

    """

    indices, = ctx.saved_tensors

    V = ctx.V

    B, S, D = grad_outputs.shape



    \# 1. Aplanamos Batch y Secuencia para procesar todo de golpe.

    \# grad_flat será de dimensión \[B\*S, D\]

    grad_flat = grad_outputs.reshape(B \* S, D)



    \# 2. Aplanamos y expandimos los índices para que coincidan con grad_flat

    \# indices pasa de \[B, S\] -> \[B\*S\] -> \[B\*S, 1\] -> \[B\*S, D\]

    indices_flat = indices.view(B \* S, 1).expand(B \* S, D)



    \# 3. Preparamos el tensor de gradientes destino inicializado en ceros

    grad_weight = torch.zeros((V, D), dtype=grad_outputs.dtype, device=grad_outputs.device)



    \# 4. Magia Pura: scatter_add\_ en la dimensión 0 (Filas)

    \# Por cada fila en grad_flat, suma el vector D-dimensional en la fila 

    \# de grad_weight que dicte 'indices_flat'. Resuelve las colisiones.

    grad_weight.scatter_add\_(dim=0, index=indices_flat, src=grad_flat)



    \# Devolvemos grad_weight para 'weight', y None para 'indices' (ya que los índices

    \# son enteros y no requieren gradientes).

    return grad_weight, None

import torch

from typing import Any

class MinChannelEnergyConv2dFunction(torch.autograd.Function):

"""

Operación de extracción convolucional no estándar.

Para cada parche espacial definido por el kernel_size, esta función calcula

la suma de las activaciones a través de todos los canales. Luego, identifica

el índice espacial (dentro del parche) con la suma más baja. 

Finalmente, extrae los valores de todos los canales EXCLUSIVAMENTE de ese índice.

"""



@staticmethod

def forward(ctx: Any, inputs: torch.Tensor, kernel_size: int) -> torch.Tensor:

    """

    Forward pass usando torch.gather.



    Args:

        ctx: Contexto para guardar variables para el backward.

        inputs: Tensor de entrada.

                Dimensiones: \[Batch, Channels, H, W\]

        kernel_size: Tamaño del parche (ej. 3 para un parche 3x3).



    Returns:

        Tensor de salida.

        Dimensiones: \[Batch, Channels, H_out, W_out\]

                     Donde H_out = H - kernel_size + 1

                     Donde W_out = W - kernel_size + 1

                     

    """

    

    \# TODO: Implementar la lógica del forward aquí usando torch.gather

    B,C,H,W = inputs.shape

    H_out = H - kernel_size + 1

    W_out = W - kernel_size + 1

    unfold_inp = torch.nn.functional.unfold(inputs,kernel_size)

    unfolded = unfold_inp.reshape(B,C,kernel_size\*\*2,-1)

    sum_unfolded = unfolded.sum(1, keepdim=True)

    indices = torch.argmin(sum_unfolded, 2, keepdim=True)

    exp_indices = indices.expand((B,C,1,H_out\*W_out))

    output = torch.gather(unfolded,2,exp_indices)



    ctx.save_for_backward(inputs, unfolded, exp_indices)

    ctx.K = kernel_size

    pass



@staticmethod

def backward(ctx: Any, grad_outputs: torch.Tensor) -> tuple\[torch.Tensor, None\]:

    """

    Backward pass usando torch.scatter\_ (o scatter).



    Args:

        ctx: Contexto con los tensores guardados del forward.

        grad_outputs: Gradientes fluyendo desde la capa superior.

                      Dimensiones: \[Batch, Channels, H_out, W_out\]



    Returns:

        Gradients respecto a 'inputs'.

        Dimensiones: \[Batch, Channels, H, W\]

        (Devuelve None para kernel_size ya que no es diferenciable)

        

    """

    

    \# TODO: Implementar la lógica del backward aquí usando torch.scatter

    inputs, unfolded, exp_indices = ctx.saved_tensors

    K = ctx.K

    B,C,H,W = inputs.shape

    H_out = H - K + 1

    W_out = W - K + 1



    out_zeros = torch.zeros_like(unfolded)

    grad = grad_outputs.view(B,C,-1).unsqueeze(2)

    out = out_zeros.scatter(2, exp_indices, grad)

    output = torch.nn.functional.fold(out.view(B,C\*K\*K,-1), (H,W), K)



    return output

def forward(ctx: Any, inputs: torch.Tensor, kernel_size: int) → torch.Tensor:

    """

    This method implements the forward pass of the layer. This must

    be done using torch.gather and torch.scatter. You are not allowed

    to use torch.max or torch.amax, only torch.argmax.



    Args:

        ctx: Context to save elements from forward to backward.

        inputs: Inputs tensor. Dimensions: \[batch size, channels,

            height, width\].

        kernel_size: Kernel size.



    Returns:

        Outputs tensor. Dimensions: \[batch size, channels,

            height - kernel size + 1, width - kernel size + 1\].

    """



    \# TODO

    B, C, H, W = inputs.shape

    H_out, W_out = H-kernel_size+1, W-kernel_size+1

    inp_unfold = torch.nn.functional.unfold(inputs, kernel_size)

    unfolded = inp_unfold.view(B,C, kernel_size\*\*2,-1)

    indices = torch.argmax(unfolded,2, keepdim=True)

    output = torch.gather(unfolded, 2, indices)

    ctx.save_for_backward(unfolded, indices, inputs)

    ctx.K = kernel_size

    return output.view(B,C,H_out, W_out)

def backward(ctx: Any, grad_outputs: torch.Tensor) → tuple[torch.Tensor, None]:

    """

    This method computes the backward of the layer. This must

    be done using torch.gather and torch.scatter. You are not allowed

    to use torch.max or torch.amax, only torch.argmax.



    Args:

        ctx: Context to pass elements from forward to backward.

        grad_outputs: Gradients of the outputs. Dimensions:

            \[batch size, channels, height - kernel size +1,

            width - kernel size + 1\].



    Returns:

        Gradients of the inputs. Dimensions: \[batch size, channels,

            height, width\].

    """



    \# TODO

    unfolded, indices, inputs = ctx.saved_tensors

    kernel_size = ctx.K

    B, C, H, W = inputs.shape

    H_out, W_out = H-kernel_size+1, W-kernel_size+1



    zero_out = torch.zeros_like(unfolded)

    grad_flat = grad_outputs.reshape(B,C,1,H_out\*W_out)



    flat_out = zero_out.scatter(2,indices,grad_flat).view(B,C\*kernel_size\*kernel_size,-1)

    out = torch.nn.functional.fold(flat_out, (H,W), kernel_size)

    return out , None