Tensors en PyTorch: creación, shapes, dtypes y operaciones básicas
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: cpuPuntos clave
- Si no especificás un dtype, PyTorch elige
torch.int64para datos enteros ytorch.float32para flotantes. - El tensor resulta ser de
int64por 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.float64Consejo: 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) → 3Cambiando 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 bytesConversió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? TrueTransposició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 2Salida 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
- Documentación oficial de PyTorch: https://pytorch.org/docs/stable/tensors.html
- "Deep Learning with PyTorch" – de Eli Steven, Sébastien Trépanier, and Nicholas Carlini
- Documentación de PyTorch sobre Broadcasting: https://pytorch.org/docs/stable/generated/torch.broadcast_tensors.html
Si tenés alguna pregunta o necesitas ejemplos de casos específicos, dejame un comentario y estaré encantado de ayudar.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.