# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Tensors

import torch
import numpy as np

# Tensors are very similar in operation to numpy arrays
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)

x_data

# Can convert numpy to tensor and vice versa
np_array = np.array(data)
x_np = torch.from_numpy(np_array)

x_np

# Similar operations as numpy
x_ones = torch.ones_like(x_data)
x_ones

x_rand = torch.rand_like(x_data, dtype=torch.float)
x_rand

# All familiar here
shape = (2, 3,)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

tensor = rand_tensor
tensor.shape

tensor.dtype

# Something new: what device the tensor is stored on
tensor.device

# Move tensor to the GPU if available
if torch.cuda.is_available():
    tensor = tensor.to('cuda')
tensor.device

# Standard numpy indexing:
tensor = torch.ones(4, 4)
first_row = tensor[0]
first_col = tensor[:, 0]
last_col = tensor[..., -1]
# and standard numpy broadcasting
tensor[:, 1] = 0
tensor

# Concatenate sequence of tensors (see also torch.stack)
t1 = torch.cat([tensor, tensor, tensor], dim=1)
t1

# Matrix multiplication
y1 = tensor @ tensor.T
y2 = tensor.matmul(tensor.T)
assert (y1 == y2).all()
y1

# Can specify output tensor
y3 = torch.rand_like(tensor)
torch.matmul(tensor, tensor.T, out=y3)
y3

# Element-wise product (just like numpy)
z1 = tensor * tensor
z2 = tensor.mul(tensor)
z3 = torch.rand_like(tensor)
torch.mul(tensor, tensor, out=z3)
assert (z1 == z2).all() and (z1 == z3).all()
z1

# Single-value tensor
agg = tensor.sum()
agg

# Can convert to python numerical using item()
agg = agg.item()
agg

# In-place operations on tensors are denoted by a suffix of _ (see also: copy_(), t_())
# Advantage: save some memory. Problem: immediate loss of history. Use is discouraged.
tensor.add_(5)
tensor

# Tensors on the CPU and numpy arrays can share underlying memory:
t = torch.ones(5)
n = t.numpy()
print(f"t: {t}")
print(f"n: {n}")

t.add_(5)
print(f"t: {t}")
print(f"n: {n}")

n = np.ones(5)
t = torch.from_numpy(n)
print(f"t: {t}")
print(f"n: {n}")

np.add(n, 1, out=n)
print(f"t: {t}")
print(f"n: {n}")



StarcoderdataJupyterScriptsDedupFiltered