Fundamentos de Tensores en PyTorch

Introducción a los Tensores

Definición

Un tensor es una estructura de datos multidimensional que puede almacenar elementos de cualquier dimensionalidad. De manera similar a los arrays de NumPy, los tensores permiten conocer sus dimensiones mediante el atributo shape, y se puede acceder a elementos específicos utilizando indexación como X(x1, x2, ..., xn). Sin embargo, a diferencia de los arrays convencionales, cada elemento de un tensor es en sí mismo otro tensor, y no existen tipos de datos primitivos como int o float. Los tensores poseen características distintivas que los hacen fundamentales para el aprendizaje profundo.

Característica Array Multidimensional Convencional Tensores en Aprendizaje Profundo
Autodiferenciación Generalmente no soportada ¡Característica central! Cálculo automático de gradientes, base del entrenamiento de redes neuronales (propagación hacia atrás).
Optimización computacional Cómputo general Optimización profunda para hardware como GPU (CUDA), acelerando enormemente operaciones con martices grandes.
Conciencia del dispositivo Generalmente solo en CPU Creación y transferencia explícita entre CPU y GPU para aprovechar el cálculo paralelo.
Integración con frameworks Estructura de datos independiente Integración perfecta con grafos computacionales y capas de redes neuronales en frameworks como PyTorch y TensorFlow.

Al igual que los arrays, los tensores pueden inicializarse utilizando funciones como ones y zeros, o directamente mediante tensor para crear tensores a partir de datos existentes.

import torch

datos_unos = torch.ones(3)          # Tensor unidimensional de tamaño 3
matriz_ceros = torch.zeros(2, 3)    # Matriz 2x3 de ceros
matriz_unos = torch.ones(2, 3)      # Matriz 2x3 de unos
matriz_aleatoria = torch.rand(2, 3) # Matriz 2x3 con valores aleatorios (0~1)
distribucion_normal = torch.randn(2, 3)  # Matriz 2x3 con distribución normal estándar

# Crear tensor directamente desde datos
datos = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
# tensor([[4., 1.], [5., 3.], [2., 1.]])

Los tensores creados en CPU y GPU no comparten memoria directamente. Si se crea un tensor en CPU, no puede operarse directamente en GPU; se debe utilizar el método to para realizar la transferencia.

# Transferencia entre dispositivos
tensor_cpu = tensor_gpu.to(device='cpu')
tensor_gpu = tensor_cpu.to(device='cuda:0')    # 0 indica la primera GPU

# Alternativamente usando propiedades
tensor_cpu = tensor_gpu.cpu()
tensor_gpu = tensor_cpu.cuda(0)

La función to también permite convertir el tipo de datos del tensor.

tensor_double = tensor_corto.to(torch.double)
tensor_corto = tensor_double.to(torch.short)

Tipo de dato Descripción
torch.float32 / torch.float Punto flotante de 32 bits
torch.float64 / torch.double Punto flotante de 64 bits (doble precisión)
torch.float16 / torch.half Punto flotante de 16 bits (media precisión)
torch.int8 Entero con signo de 8 bits
torch.uint8 Entero sin signo de 8 bits
torch.int16 / torch.short Entero con signo de 16 bits
torch.int32 / torch.int Entero con signo de 32 bits
torch.int64 / torch.long Entero con signo de 64 bits
torch.bool Tipo booleano

Por defecto, los tensores se crean con tipo torch.float32.

Función Principal de Creación

salida = torch.tensor(datos, dtype, device, requires_grad, pin_memory)

  • datos: Datos del tensor (lista, tupla, array, escalar u otro tipo)
  • dtype: Opcional, tipo de dato
  • device: Opcional, dispositivo de cálculo
  • requires_grad: Opcional, booleano que indica si se requiere gradiante (por defecto False)
  • pin_memory: Opcional, booleano para memoria fija (por defecto False)

Metadatos de un Tensor

Dimensiones, Offset y Paso

Dimensión: Representa la forma del tensor, indicando cuántos elementos hay en cada eje. Se obtiene con tensor.shape.

Offset: Índice de un elemento en el almacenamiento respecto al primer elemento del tensor. Se obtiene con tensor.storage_offset().

Paso (stride): Número de elementos que deben saltarse en el almacenamiento para llegar al siguiente elemento. Se obtiene con tensor.stride().

Para operaciones de transposición, aunque se obtienen dos tensores diferentes, ambos comparten el mismo almacenamiento; solo difeiren en forma y paso.

puntos = torch.tensor([[4.0, 1.0], [5.0, 3.0], [2.0, 1.0]])
puntos_t = puntos.T

print(puntos.storage() == puntos_t.storage())    # True
print(puntos.shape) == puntos_t.shape)           # False
# (3, 2)                 (2, 3)
print(puntos.stride() == puntos_t.stride())      # False
# (2, 1)                 (1, 2)

Tensores Contiguos

Algunas funciones requieren tensores contiguos, es decir, aquellos almacenados secuencialmente en memoria siguiendo el orden de filas (row-major). Por lo general, los tensores creados son contiguos, pero operaciones como transposición, slicing e intercambio de dimensiones generan tensores no contiguos.

Se puede verificar la contigüidad con tensor.is_contiguous() y convertir un tensor a formato contiguo con tensor.contiguous(). Es importante notar que después de aplicar contiguous() a un tensor traspuesto, el nuevo tensor ya no comparte almacenamiento con el original.

Modificando la Forma de Tensores

nuevo_tensor = tensor.view(forma)

tensor: Tensor de entrada

forma: Forma objetivo (puede ser secuencia de enteros o tupla; -1 significa cálculo automático)

x = torch.randn(4, 4)
print(x.size())          # torch.Size([4, 4])

y = x.view(16)
print(y.size())          # torch.Size([16])

z = x.view(-1, 8)        # -1 se calcula automáticamente
print(z.size())          # torch.Size([2, 8])

a = torch.randn(1, 2, 3, 4)
print(a.size())          # torch.Size([1, 2, 3, 4])

b = a.transpose(1, 2)    # Intercambiar dimensiones 1 y 2
print(b.size())          # torch.Size([1, 3, 2, 4])

c = a.view(1, 3, 2, 4)   # No modifica el layout en memoria
print(c.size())          # torch.Size([1, 3, 2, 4])

torch.equal(b, c)        # False

Conversiones

De NumPy a Tensor
import numpy as np
import torch

# Array de NumPy
np_array = np.array([[1, 2, 3], [4, 5, 6]])
print(type(np_array))    # <class 'numpy.ndarray'>
print(np_array.shape)    # (2, 3)

# Tensor de PyTorch (convertido directamente desde NumPy)
tensor_py = torch.from_numpy(np_array)
print(type(tensor_py))   # <class 'torch.Tensor'>
print(tensor_py.shape)   # torch.Size([2, 3])

La función from_numpy() solo acepta arrays de NumPy y tipos booleanos.

De Tensor a NumPy
salida = tensor.numpy(force)

Si force es True, el array será una copia del tensor en lugar de compartir memoria (por defecto es False). Solo debe usarse False cuando el tensor está en CPU, no requiere gradiente y no contiene conjugados.

Cuando se usa True, internamente ejecuta:

tensor.detach().cpu().resolve_conj().resolve_neg().numpy()

  • detach(): Desconecta el tensor del grafo computacional
  • cpu(): Transfiere el tensor de GPU a CPU
  • resolve_conj(): Devuelve el conjugado si es una vista conjugada
  • resolve_neg(): Devuelve el neg if es una vista negativa

Para convertir tensores a arrays de NumPy, es esencial verificar que el tensor esté en CPU (usar cpu() si es necesario) y que no requiera gradiente (usar detach() si es necesario).

Indexación

Indexación Estándar

Funciona igual que las listas de Python y los arrays de NumPy.

# puntos es un tensor
puntos[1:]            # Todas las filas desde la 1, incluyendo todas las columnas
puntos[1:, :]         # Equivalente al anterior
puntos[1:, 0]         # Filas desde la 1, solo columna 0
puntos[None]          # Agrega una dimensión, similar a unsqueeze()

Característica de Broadcasting

El broadcasting permite operar tensores de diferentes formas expandiendo dimensiones de tamaño 1 para hacerlas compatibles.

grupo_a = torch.randn(5, 1, 3, 4)   # [5,1,3,4]
grupo_b = torch.randn(1, 6, 4, 5)   # [1,6,4,5]

# Proceso de broadcasting:
# 1. Alinear dimensiones: ambos son 4D
# 2. Comparar dimensiones:
#    Dimensión 0: 5 y 1 → 1 se expande a 5
#    Dimensión 1: 1 y 6 → 1 se expande a 6
#    Dimensión 2: 3 y 4 → multiplicación matricial: 3×4 y 4×5
#    Dimensión 3: 4 y 5 → multiplicación matricial: 3×4 y 4×5
# 3. Resultado: [5,6,3,4] y [5,6,4,5]
# 4. Multiplicación matricial: [3,4] × [4,5] = [3,5]
# 5. Final: dimensiones de lote + resultado = [5,6,3,5]

Para casos más complejos:

A = torch.randn(2, 1, 3, 1, 5, 4)  # [2,1,3,1,5,4]
B = torch.randn(1, 4, 1, 6, 4, 7)  # [1,4,1,6,4,7]

# Broadcasting:
# dim5: 4 y 7 → multiplicación: 5×4 y 4×7 = 5×7
# dim4: 5 y 4 → multiplicación: 5×4 y 4×7
# dim3: 1 y 6 → 1 se expande a 6
# dim2: 3 y 1 → 1 se expande a 3
# dim1: 1 y 4 → 1 se expande a 4
# dim0: 2 y 1 → 1 se expande a 2

resultado = A @ B
print(f"Forma del resultado: {resultado.shape}")  # [2,4,3,6,5,7]

El broadcasting no replica datos, solo expande las dimensiones para cumplir los requisitos de la operación. Si las dimensiones no son compatibles (ninguna es 1 ni iguales), se produce un error.

# Error: últimas dimensiones incompatibles para multiplicación
A = torch.randn(3, 4)   # [3,4]
B = torch.randn(3, 5)   # [3,5]
# Error: se requiere 4×3, pero es 3×5

Procesamiento de Datos

Operaciones de Reducción
Media y Suma
tensor.mean(dim, keepdim)  # Media
tensor.sum(dim, keepdim)   # Suma

  • dim: Dimensión a procesar (si se omite, procesa todo)
  • keepdim: Mantener la dimensión original (False por defecto)
imagen_t = torch.randn(3, 5, 5)    # Tensor de 3 canales, imagen 5×5
print(imagen_t.mean(-3))           # Media en dimensión 0 (canales)
# Resultado: tensor de 5×5

imagen_t = torch.tensor([
    # Canal 0 (rojo)
    [[1., 2., 3.],
     [4., 5., 6.],
     [7., 8., 9.]],
    
    # Canal 1 (verde)  
    [[10., 20., 30.],
     [40., 50., 60.],
     [70., 80., 90.]],
    
    # Canal 2 (azul)
    [[100., 200., 300.],
     [400., 500., 600.],
     [700., 800., 900.]]
])

resultado = imagen_t.mean(-3)
print("Forma:", resultado.shape)  # torch.Size([3, 3])
print(resultado)
# tensor([[ 37.,  74., 111.],
#         [148., 185., 222.],
#         [259., 296., 333.]])

El primer elemento resulta de promediar los primeros elementos de los tres canales.

Producto de Todos los Elementos
torch.prod(tensor)           # Producto de todos los elementos
torch.cumprod(tensor, dim)   # Producto acumulativo por dimensión

tensor = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])

print(torch.prod(tensor))           # 720
print(torch.cumprod(tensor, dim=1)) # Producto acumulativo en dimensión 1
# tensor([[ 1,  2,  6],
#         [4, 20, 120]])

Desviación Estándar y Varianza
tensor = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])

print(torch.std(tensor.float()))    # Desviación estándar: 1.7078
print(torch.var(tensor.float()))    # Varianza: 2.9167

Operaciones Matemáticas
Suma y Resta
A = torch.randn(3, 4, 5)   # Forma: [3,4,5]
B = torch.randn(4, 5)      # Se expande a [1,4,5] → [3,4,5]
C = A + B 

D = torch.randn(3, 1, 5)   # Forma: [3,1,5]
E = torch.randn(4, 5)      # Se expande a [1,4,5] → [3,4,5]
F = D + E                  # Válido

El broadcasting expande automáticamente las dimensiones de tamaño 1 para hacerlas compatibles.

Multiplicación y División
a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
print(a * b)     # Multiplicación elemento a elemento: tensor([4, 10, 18])
print(b / a)     # División elemento a elemento: tensor([4.0000, 2.5000, 2.0000])

Operaciones Exponenciales
a = torch.tensor([1, 2, 3])
c = 2
print(torch.pow(a, c))  # Cuadrado: tensor([1, 4, 9])

# Exponential natural
print(torch.exp(a))     # tensor([ 2.7183,  7.3891, 20.0855])

Operaciones Logarítmicas
a = torch.tensor([1, 2, 3])
print(torch.log(a.float()))    # Logaritmo natural
print(torch.log10(a.float()))  # Logaritmo base 10

Operaciones Matriciales

Multiplicación matricial:

vector = torch.randn(4)
matriz = torch.randn(4, 5)
resultado1 = vector @ matriz  # [1,4] × [4,5] = [1,5] → [5]

Para vectores 1D, se interpretan como vectores fila a la izquierda y vectores columna a la derecha, sin broadcasting.

a = torch.tensor([1, 2, 3])
b = torch.tensor([4, 5, 6])
a @ b  # Producto punto: tensor(32)

Transposición:

matriz = torch.randn(3, 4)
print(matriz.t().shape)      # 4x3 (transposición 2D)
print(matriz.T.shape)        # Igual

# Transposición multidimensional
tensor_3d = torch.randn(2, 3, 4)
print(tensor_3d.transpose(1, 2).shape)  # Intercambiar dims 1 y 2: [2,4,3]
print(tensor_3d.permute(2, 0, 1).shape) # Reordenar: [4,2,3]

T solo funciona para 2D, mientras que transpose y permute son para dimensiones superiores.

Máximo y Mínimo
tensor = torch.tensor([[1, 2, 3],
                       [4, 5, 6]])

# Máximo/Mínimo global
print(torch.max(tensor))    # 6
print(torch.min(tensor))    # 1

# Con índices
valores, indices = torch.max(tensor, dim=1)
print(f"Valores máximos por fila: {valores}")    # tensor([3, 6])
print(f"Índices de máximos: {indices}")          # tensor([2, 2])

Operaciones de Comparación
a = torch.tensor([1, 2, 3, 4, 5])
b = torch.tensor([3, 3, 3, 3, 3])

print(torch.eq(a, b))    # Igual: tensor([False, False, True, False, False])
print(torch.ne(a, b))    # Diferente: tensor([True, True, False, True, True])
print(torch.gt(a, b))    # Mayor: tensor([False, False, False, True, True])
print(torch.lt(a, b))    # Menor: tensor([True, True, False, False, False])
print(torch.ge(a, b))    # Mayor o igual
print(torch.le(a, b))    # Menor o igual

Operaciones Lógicas
tensor_bool = torch.tensor([True, False, True])

print(torch.logical_and(tensor_bool, torch.tensor([True, True, False])))
# tensor([True, False, False])

print(torch.logical_or(tensor_bool, torch.tensor([False, False, False])))
# tensor([True, False, True])

print(torch.logical_not(tensor_bool))
# tensor([False, True, False])

Agregar y Eliminar Dimensiones
x = torch.tensor([1, 2, 3])  # forma: [3]
print(f'Original: {x}, forma: {x.shape}')

# Agregar dimensión en posición 0
x_0 = x.unsqueeze(0)
print(f'unsqueeze(0): {x_0}, forma: {x_0.shape}')  # [1, 3]

# Agregar dimensión en posición 1
x_1 = x.unsqueeze(1)
print(f'unsqueeze(1): {x_1}, forma: {x_1.shape}')  # [3, 1]

La función squeeze funciona de manera inversa para eliminar dimensiones de tamaño 1.

Limitación de Valores
salida = torch.clamp(entrada, min, max)
  • entrada: Tensor de entrada
  • min: Valor mínimo (None = sin límite inferior)
  • max: Valor máximo (None = sin límite superior)

Serialización de Tensores

Los archivos .pt o .pth se utilizan para guardar tensores.

Guardar Tensores
torch.save(tensor, './datos/tensor_guardado.pt')

# O usando contexto
with open('./datos/tensor_guardado.pt', 'wb') as f:
    torch.save(tensor, f)

Cargar Tensores
tensor = torch.load('./datos/tensor_guardado.pt')

# O usando contexto
with open('./datos/tensor_guardado.pt', 'rb') as f:
    tensor = torch.load(f)

Concatenar Tensores
salida = torch.cat(tensores, dim)
  • tensores: Secuencia de tensores (lista o tupla)
  • dim: Dimensión de concatenación (0 por defecto)

Todas las dimensiones excepto la de concatenación deben coincidir. El tensor resultante mantiene el mismo número de dimensiones, pero el tamaño en la dimensión de concatenación es la suma de los tamaños correspondientes.

Etiquetas: Python deep-learning PyTorch tensor neural-networks

Publicado el 6-26 07:50