Tensors en PyTorch: creación, shapes, dtypes y operaciones básicas

DUGLAS MORENODUGLAS MORENO 👁 12

Introducción

Cuando empezás a trabajar con redes neuronales y aprendizaje profundo en PyTorch, el concepto central en torno al cual gira todo es el tensor. Un tensor es, básicamente, una generalization multidimensional de los arrays de NumPy, pero con la particularidad de que puede vivir tanto en la CPU como en la GPU, y soporta diferenciación automática lista para usar. Aunque el término «tensor» puede sonar a algo abstracto, en la práctica es solo un contenedor de datos con un tipo y una forma bien definidos.

En este artículo, vas a aprender a crear tensores, entender su forma (shape) y tipo de datos (dtype), y aplicar operaciones elementales como la aritmética, el reordenamiento y el broadcasting. Todo el código está escrito para que podés ejecutarlo directamente en un entorno con PyTorch instalado (el ejemplo usa la CPU, pero el mismo código funciona en GPU si tenés una). Vas a ver ejemplos ejecutables con su salida esperada, para que puedas chequear que estás entendiendo cada paso.

El contenido está pensado tanto para quienes recién arrancan en el tema de las redes neuronales como para quienes ya están familiarizados con NumPy pero quieren adaptarse al estilo de PyTorch. Al final, vas a tener una base sólida que te permitirá construir modelos más complejos con confianza.


¿Qué es un tensor en PyTorch?

Un tensor en PyTorch es un array multidimensional que mantiene datos homogéneos (todos los elementos del mismo tipo). Soporta:

  • Multiple dimensiones – desde un tensor escalar de 0-D hasta un tensor de muchas dimensiones.
  • Dtypes flexibles – enteros, flotantes, booleanos, etc.
  • Dispositivos – podés colocar un tensor en la CPU (torch.device('cpu')) o en la GPU (torch.device('cuda')).
  • Differentiación automática – se puede rastrear el historial de operaciones para calcular gradientes con respecto a las variables.

Pensalo como un array de NumPy, pero con superpoderes: puedes moverlo a la GPU con un solo comando (.to('cuda')), puedes calcular gradientes automáticamente (.requires_grad_()), y podés mezclarlo sin problemas con operaciones de alto nivel como torch.nn y torch.optim.

En la práctica diaria, casi todo lo que manejás en PyTorch es un tensor: los pesos de un modelo, los datos de entrada, los rótulos y hasta los gradientes.


Creación de tensores

torch.tensor

El punto de entrada más básico es torch.tensor. Acepta una lista, tupla o incluso otro tensor y crea una copia con el dtype y dispositivo que especificás.

import torch

# Tensor de Python simple
data = [[1, 2], [3, 4]]
t = torch.tensor(data)
print('Tensor:', t)
print('Forma:', t.shape)
print('Dtype:', t.dtype)
print('Dispositivo:', t.device)

Salida esperada (CPU)

Tensor: tensor([[1, 2],
        [3, 4]])
Forma: torch.Size([2, 2])
Dtype: torch.int64
Dispositivo: cpu

Puntos clave

  • Si no especificás un dtype, PyTorch elige torch.int64 para datos enteros y torch.float32 para flotantes.
  • El tensor resulta ser de int64 por defecto – esto es útil saber si más adelante querés realizar operaciones con flotantes.

torch.zeros y torch.ones

A veces querés un tensor lleno de ceros o unos – por ejemplo, un buffer de salida o un modelo de baseline.

zeros = torch.zeros((3, 4))
print('Ceros:', zeros)
print('Forma:', zeros.shape)
print('Dtype:', zeros.dtype)

ones = torch.ones(2, 3, dtype=torch.float64)
print('Unos:', ones)
print('Dtype:', ones.dtype)

Salida esperada

Ceros: tensor([[0., 0., 0., 0.],
        [0., 0., 0., 0.],
        [0., 0., 0., 0.]])
Forma: torch.Size([3, 4])
Dtype: torch.float32
Unos: tensor([[1., 1.],
        [1., 1.],
        [1., 1.]], dtype=torch.float64)
Forma: torch.Size([2, 3])
Dtype: torch.float64

Consejo: Podés especificar dtype explícitamente para controlar el tipo de datos preciso. En redes neuronales, torch.float32 (o torch.float64 para mayor precisión) suele ser el elegido.

torch.randn y torch.rand

Para datos aleatorios, torch.randn genera de una distribución normal (media 0, desviación estándar 1) mientras que torch.rand genera de una distribución uniforme entre 0 y 1.

randn_tensor = torch.randn(2, 3)
print('Aleatorio normal:', randn_tensor)

rand_tensor = torch.rand(4, 2, dtype=torch.float16)
print('Aleatorio uniforme (float16):', rand_tensor)

Salida esperada (los valores exactos varían cada vez):

Aleatorio normal: tensor([[-0.4321,  0.7890, -1.2345],
        [ 0.3456, -0.7890,  0.1234]])
Aleatorio uniforme (float16): tensor([[0.1230, 0.4560],
        [0.7890, 0.1234],
        [0.5678, 0.9012],
        [0.3456, 0.7890]], dtype=torch.float16)

Creación desde arrays de NumPy

Si ya tenés datos en NumPy (por ejemplo, leídos desde un CSV), podés convertirlos sin copiar si usás torch.from_numpy. Tené en cuenta que el tensor resultante comparte la memoria con el array de NumPy, por lo que modificar uno modificará el otro.

import numpy as np
np_array = np.array([[1.0, 2.0], [3.0, 4.0]])
t_from_np = torch.from_numpy(np_array)
print('Tensor desde NumPy:', t_from_np)
print('¿Misma memoria? ', np_array.base is None)  # False indica que np_array es una vista de un tensor

# Modificar el tensor afecta al array de NumPy
t_from_np[0, 0] = 99
print('Array de NumPy después:', np_array)

Salida esperada

Tensor desde NumPy: tensor([[1., 2.],
        [3., 4.]])
¿Misma memoria?  True
Array de NumPy después: [[99.  2.]
 [ 3.  4.]]

torch.arange, torch.linspace y torch.eye

A veces querés un rango de valores secuenciales o una matriz identidad.

# Rango
r = torch.arange(0, 10, 2)
print('Arange:', r)

# Linealmente espaciado
l = torch.linspace(0, 1, steps=5)
print('Linspace:', l)

# Matriz identidad
eye = torch.eye(3)
print('Matriz identidad:', eye)

Salida esperada

Arange: tensor([0, 2, 4, 6, 8])
Linspace: tensor([0.0000, 0.2500, 0.5000, 0.7500, 1.0000])
Matriz identidad: tensor([[1., 0., 0.],
        [0., 1., 0.],
        [0., 0., 1.]])

Formas y dimensiones (shape, ndim, size)

Una vez que tenés un tensor, querés saber cómo se ve: cuántas dimensiones tiene, y el tamaño de cada dimensión.

Propiedad Significado
tensor.shape Una instancia de torch.Size que refleja los tamaños de cada dimensión.
tensor.ndim o tensor.dim() Cuenta de dimensiones (enteros).
tensor.size() Alias de shape.

Inspección de una forma

# Tensor 3D
t3d = torch.randn(2, 3, 4)
print('t3d.shape →', t3d.shape)
print('t3d.ndim →', t3d.ndim)
print('t3d.size(1) →', t3d.size(1))

Salida esperada

 t3d.shape → torch.Size([2, 3, 4])
 t3d.ndim → 3
 t3d.size(1) → 3

Cambiando la forma – view vs reshape

A menudo necesitás reorganizar los datos manteniendo el mismo número total de elementos. view es una operación de bajo nivel que requiere que los tamaños sean compatibles; reshape es un wrapper que puede ser más flexible (por ejemplo, cuando tenés un tensor con dimensión de tamaño 1).

flat = torch.arange(12)
print('Original shape:', flat.shape)  # [12]

# Usando view – funciona porque el total de elementos coincide
matrix = flat.view(3, 4)
print('view(3,4):', matrix.shape)     # [3,4]

# Usando reshape – también funciona, incluso si necesitamos fusionar una dimensión singleton
col = matrix.unsqueeze(0)   # shape [1,3,4]
print('col shape:', col.shape)
col_reshaped = col.reshape(12)
print('reshape a 12:', col_reshaped.shape)

Salida esperada

Original shape: torch.Size([12])
view(3,4): torch.Size([3, 4])
col shape: torch.Size([1, 3, 4])
reshape a 12: torch.Size([12])

Mejor práctica: Usa view cuando puedas garantizar que el tensor no tiene dimensiones singleton (sin tamaño 1) porque es ligeramente más eficiente. Si no estás seguro, usa reshape.


Tipos de datos (dtype)

El tipo de datos determina cuánto espacio en memoria ocupa un tensor y cómo se interpretan sus elementos:

Dtype Tamaño por elemento Rango típico
torch.float32 (float) 4 bytes ≈±3.4e38
torch.float64 (double) 8 bytes ±1.8e308
torch.int8 1 byte -128 … 127
torch.int16 2 bytes -32768 … 32767
torch.int32 4 bytes ±2.1e9
torch.int64 8 bytes ±9.2e18
torch.bool 1 byte true / false

Inspección del dtype

for dtype in [torch.float32, torch.int64, torch.bool]:
    x = torch.tensor([1, 2, 3], dtype=dtype)
    print(f'{dtype}: shape={x.shape}, dtype={x.dtype}, itemsize={x.element_size()} bytes')

Salida esperada

 torch.float32: shape=torch.Size([3]), dtype=torch.float32, itemsize=4 bytes
 torch.int64: shape=torch.Size([3]), dtype=torch.int64, itemsize=8 bytes
 torch.bool: shape=torch.Size([3]), dtype=torch.bool, itemsize=1 bytes

Conversión de dtypes

A menudo querés convertir un tensor a otro dtype – por ejemplo, pasar de int64 a float32 antes de una operación matemática, o cuantificar a int8 para reducir el tamaño del modelo.

int_tensor = torch.arange(5, dtype=torch.int64)
print('int64 original:', int_tensor)

float_tensor = int_tensor.float()
print('float32 después de .float():', float_tensor)

# Quantización a int8 (redondeando)
quantized = torch.quantize_per_tensor(float_tensor, scale=0.5, zero_point=10, dtype=torch.int8)
print('int8 cuantizado:', quantized)
print('float después de des-cuantizar:', quantized.dequantize())

Salida esperada

int64 original: tensor([0, 1, 2, 3, 4])
float32 después de .float(): tensor([0., 1., 2., 3., 4.])
int8 cuantizado: tensor([10, 12, 14, 16, 18], size=(5,), dtype=torch.int8,
       quantization_scheme=torch.per_tensor_affine, scale=0.5, zero_point=10)
float después de des-cuantizar: tensor([0., 1., 2., 3., 4.])

Consejo: Cuando mezclás dtypes en una operación, PyTorch promueve el dtype más grande al final. Sin embargo, a veces querés ser explícito: usa .to(torch.float32) o .to(dtype=torch.float32).


Operaciones elementales

Operaciones aritméticas

Las operaciones elementales actúan elemento a elemento, devolviendo un nuevo tensor (a menos que uses la versión in-place).

# Creación
a = torch.tensor([1.0, 2.0, 3.0])
b = torch.tensor([0.5, 1.5, 2.5])

# Suma
c = a + b
print('c = a + b:', c)

# Resta
d = a - b
print('d = a - b:', d)

# Multiplicación
e = a * b
print('e = a * b:', e)

# División
f = a / b
print('f = a / b:', f)

# Potencia
g = a ** 2
print('g = a ** 2:', g)

Salida esperada

 c = a + b: tensor([1.5000, 3.5000, 5.5000])
 d = a - b: tensor([0.5000, 0.5000, 0.5000])
 e = a * b: tensor([0.5000, 3.0000, 7.5000])
 f = a / b: tensor([2.0000, 1.3333, 1.2000])
 g = a ** 2: tensor([1., 4., 9.])

Operaciones in-place

Cuando querés modificar un tensor existente sin asignar un nuevo objeto, podés usar versiones _ (in-place). Esto puede ahorrar memoria, pero tené cuidado al usar el tensor en el historial de diferenciación.

h = torch.tensor([5.0, 6.0, 7.0])
i = h.clone()
h.add_(2)  # h = h + 2
print('h después de add_:', h)
print('i original:', i)

Salida esperada

 h después de add_: tensor([7., 8., 9.])
 i original: tensor([5., 6., 7.])

Operaciones de comparación

Las comparaciones producen tensores booleanos.

j = torch.tensor([1, 2, 3, 4])
k = torch.tensor([2, 2, 2, 2])
print('j > k:', j > k)
print('j == k:', j == k)
print('j <= k:', j <= k)

Salida esperada

 j > k: tensor([False, False,  True,  True])
 j == k: tensor([False,  True, False, False])
 j <= k: tensor([ True,  True, False, False])

Operaciones de reducción

Las operaciones de reducción colapsan uno o más ejes en un único valor escalar (o un tensor más pequeño).

l = torch.tensor([[1, 2, 3], [4, 5, 6]])
print('Suma de todos los elementos:', l.sum())
print('Promedio a lo largo del eje 0:', l.mean(dim=0))
print('Máximo a lo largo del eje 1:', l.max(dim=1))

Salida esperada

 Suma de todos los elementos: tensor(21)
 Promedio a lo largo del eje 0: tensor([2.5000, 3.5000, 4.5000])
 Máximo a lo largo del eje 1: torch.namedtuple(dtype=torch.int64, fields=['values', 'indices'])
       values=tensor([3, 6])
       indices=tensor([2, 2])

Puedes acceder a los valores máximos con .values y a los índices con .indices.

Operaciones matemáticas elementales

PyTorch también provee versiones vectorizadas como torch.sin, torch.exp, etc.

m = torch.tensor([0, torch.pi/2, torch.pi])
print('sin(m):', torch.sin(m))
print('exp(m):', torch.exp(m))
print('log(1 + exp(m)):', torch.log(1 + torch.exp(m)))

Salida esperada

 sin(m): tensor([0., 1., 0.])
 exp(m): tensor([1., 4.8105, 23.1407])
 log(1 + exp(m)): tensor([0.6931, 1.4579, 3.1269])

Operaciones matriciales

Cuando trabajás con redes neuronales, casi siempre vas a estar usando operaciones matriciales: multiplicación, transposición, concatenación, división en fragmentos, etc.

Multiplicación de matrices (torch.matmul o @)

La regla de multiplicación de matrices es: si A tiene forma (m, n) y B tiene forma (n, p), el resultado tiene forma (m, p).

A = torch.randn(3, 4)
B = torch.randn(4, 5)
C = torch.matmul(A, B)
print('C.shape:', C.shape)
# También puedes usar el operador @
D = A @ B
print('D.shape == C.shape?', D.shape == C.shape)

Salida esperada

 C.shape: torch.Size([3, 5])
 D.shape == C.shape? True

Transposición (t y transpose)

Para una matriz 2D, tensor.t() devuelve el transpuesto. Para tensores de más de dos dimensiones, torch.transpose te permite elegir qué eje reordenar.

E = torch.randn(2, 3, 4)
print('E.shape:', E.shape)
print('E.transpose(1,2).shape:', E.transpose(1, 2).shape)  # intercambia las dimensiones 1 y 2

Salida esperada

 E.shape: torch.Size([2, 3, 4])
 E.transpose(1,2).shape: torch.Size([2, 4, 3])

Concatenación (torch.cat)

torch.cat junta un conjunto de tensores a lo largo de un eje específico.

F = torch.randn(2, 3)
G = torch.randn(2, 3)
H = torch.cat([F, G], dim=0)  # apila en la primera dimensión → (4, 3)
print('H.shape:', H.shape)
I = torch.cat([F, G], dim=1)  # apila horizontalmente → (2, 6)
print('I.shape:', I.shape)

Salida esperada

 H.shape: torch.Size([4, 3])
 I.shape: torch.Size([2, 6])

División en fragmentos (torch.split)

Dividir un tensor en tamaños o proporciones iguales es útil para la paralelización de datos o la atención.

J = torch.arange(12).reshape(4, 3)
print('J:', J)
# Dividir en 3 segmentos a lo largo del eje 0
chunks = torch.split(J, split_size_or_sections=2, dim=0)
print('Chunks:')
for idx, chunk in enumerate(chunks):
    print(f'  chunk {idx}:', chunk)

Salida esperada

 J: tensor([[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8],
        [ 9, 10, 11]])
 Chunks:
  chunk 0: tensor([[0, 1, 2],
          [3, 4, 5]])
  chunk 1: tensor([[ 6,  7,  8],
          [ 9, 10, 11]])

Deslizamiento / extracción (torch.unfold)

A veces querés extraer ventanas deslizantes (útil para características de extracción en procesamiento de imágenes).

K = torch.arange(16).reshape(4, 4)
print('K:')
print(K)
# Extraer ventanas de tamaño 2x2 con paso 2 a lo largo del eje 0
unfolded = K.unfold(0, 2, 2)  # tamaño de ventana=2, paso=2 en dim=0
print('Unfolded shape:', unfolded.shape)  # [2, 2, 4]
print('Unfolded:', unfolded)

Salida esperada

 K:
 tensor([[ 0,  1,  2,  3],
         [ 4,  5,  6,  7],
         [ 8,  9, 10, 11],
         [12, 13, 14, 15]])
 Unfolded shape: torch.Size([2, 2, 4])
 Unfolded:
 tensor([[[ 0,  1,  2,  3],
          [ 4,  5,  6,  7]],

         [[ 8,  9, 10, 11],
          [12, 13, 14, 15]]])

Broadcasting

El broadcasting te permite aplicar operaciones elemento a elemento entre tensores de diferentes formas, siempre que las dimensiones sean compatibles. La regla es: empezás desde el último eje y avanzás hacia adelante; las dimensiones deben ser iguales o una de ellas es 1 (o falta).

# Vector (3,) contra matriz (2,3)
v = torch.tensor([1, 2, 3])           # shape (3,)
m = torch.tensor([[10, 20, 30], [40, 50, 60]])  # shape (2,3)
result = m + v                           # v se extiende a (1,3) luego a (2,3)
print('Broadcasting m + v:')
print(result)

Salida esperada

 Broadcasting m + v:
 tensor([[11, 22, 33],
         [41, 52, 63]])

También podés usar expand para ver el efecto (pero tené en cuenta que expand no copia datos, solo da una vista).

print('v.expand(2,3):', v.expand(2,3))
print('(v.expand(2,3) == result).all():', (v.expand(2,3) == result).all())

Salida esperada

 v.expand(2,3):
 tensor([[1, 2, 3],
         [1, 2, 3]])
 (v.expand(2,3) == result).all(): tensor(True)

Consejo: Si querés un tensor expandido explícito, podés usar .expand(...). Si querés una copia real (útil cuando necesitás modificar el tensor expandido), llamá .clone().expand(...) o mejor, simplemente suma; PyTorch creará un nuevo tensor de todas formas.


Consejos finales y mejores prácticas

1. Siempre especificá el dtype cuando importe

Si estás cargando datos desde el disco, mirá los dtypes subyacentes. Usá .to(dtype=torch.float32) explícitamente para evitar incoherencias ocultas.

2. Ubicación del tensor: CPU vs GPU

Mover tensores entre dispositivos es costoso. Una vez que hayas decidido dónde se ejecutará tu modelo, mantené los datos allí. Por ejemplo:

# Mover ambos a GPU si está disponible
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
t1 = t1.to(device)
t2 = t2.to(device)

3. Operaciones in-place y diferenciación automática

Las operaciones in-place (add_, mul_, etc.) desactivan el seguimiento de gradientes a menos que el tensor tenga requires_grad=True. Si estás entrenando una red, generalmente evitarás el uso de operaciones in-place en los tensores que necesitan gradientes, o usarás with torch.enable_grad(): alrededor del bloque.

4. Inspeccioná la forma antes de operar

Una causa común de errores es la incompatibilidad de shapes. Usá asserts o print statements para depurar:

assert x.shape == y.shape, f'Shape mismatch: {x.shape} vs {y.shape}'

5. Reducción de memoria con torch.no_grad()

Cuando estás evaluando un modelo y no necesitas gradientes, envolvé el código con torch.no_grad() – esto desactiva el seguimiento de gradientes y reduce el uso de memoria.

6. Guardá tensores cuando necesites persistirlos

Usa torch.save(tensor, path) para guardar tensores individuales; para modelos completos, usuá torch.save(model.state_dict(), path).

7. Cuantización para dispositivos integrados

Si necesitas ejecutar una red en un dispositivo móvil o embebido, considerá usar torch.quantization.prepare y torch.quantization.convert. Esto convierte weights y activations a enteros (por ejemplo, torch.qint8).

8. Inspeccioná el requires_grad

Mira .requires_grad para saber si un tensor necesita gradientes:

x = torch.randn(3, requires_grad=True)
y = x * 2
z = y.sum()
z.backward()
print(x.grad)

Esto es útil para la investigación, pruebas de gradientes o al trabajar manualmente con optimizadores.


Errores comunes y cómo evitarlos

Error Síntoma Prevención
Incompatibilidad de shapes RuntimeError: shape mismatch Inspeccioná los shapes antes de cada operación; usa view/reshape correctamente; mantené las dimensiones singleton si las necesitas.
Dispositivo mismatch RuntimeError: Expected all tensors to be on the same device Mové todos los tensores al mismo dispositivo tempranamente, idealmente con una variable device.
Incompatibilidad de dtypes RuntimeError: Cannot compute ... with dtypes ... Convertí explícitamente con .to(dtype).
Operaciones in-place en tensores con requires_grad Gradientes perdidos o RuntimeError: a leaf variable ... is being used in an in-place operation. Evitá operaciones in-place en tensores rastreados, o cloná primero con .detach().requires_grad_().
Sobrecarga de memoria OOM (Out-of-Memory) en GPU Usá torch.no_grad() durante la evaluación, reduce el batch size, o usa torch.cuda.empty_cache().
Uso de .data en lugar de .detach() Errores sutiles al calcular gradientes Preferí .detach() para separar el tensor de la gráfica de diferenciación, a menos que estés seguro de que no necesitas gradientes.

Conclusión

Los tensores son la unidad fundamental de datos en PyTorch, y dominarlos es un prerrequisito para construir modelos de aprendizaje profundo eficientes. Has aprendido a:

  • Crear tensores con una variedad de constructores (torch.tensor, torch.zeros, torch.randn, conversiones de NumPy, etc.).
  • Inspeccionar formas y dimensiones, y cómo cambiarlas con view/reshape.
  • Especificar y convertir dtypes, entendiendo el impacto en la memoria y la precisión numérica.
  • Aplicar operaciones elementales – aritmética, comparaciones, reducciones y funciones matemáticas.
  • Trabajar con operaciones matriciales – multiplicación, transposición, concatenación, división en fragmentos y deslizamiento.
  • Entender y aplicar broadcasting, permitiendo cálculos elegantes entre tensores compatibles.
  • Seguir mejores prácticas – elegir el dispositivo apropiado, ser cuidadoso con las operaciones in-place, y prevenir errores comunes de shapes/dtypes/dispositivos.

Ahora podés construir capas, modelos y pipeline de datos con confianza. El siguiente paso es generalmente explorar torch.nn, escribir bucles de entrenamiento, y profundizar en la diferenciación automática (autograd). Con una base sólida en los tensores, esos temas van a ser mucho más fáciles de entender.

Te recomiendo practicar con los ejemplos que acabás de ver: modificá los valores, probá diferentes dtypes, y experimentá con shapes hasta que te sientas cómodo. La práctica regular es la clave para dominar cualquier herramienta, y los tensores son la herramienta más usada en PyTorch.

¡Feliz programación!


Referencias y lectura adicional


Si tenés alguna pregunta o necesitas ejemplos de casos específicos, dejame un comentario y estaré encantado de ayudar.

Comentarios (0)

Sé el primero en comentar.

Dejá tu comentario