torch.rand(5,3)

torch.Tensor(5, 3)

x = torch.rand(5, 3)
y = torch.rand(5, 3)

"""
Explanation: Basic Operations
Tensor Creation
PyTorch has API designed in Numpy Style for making the life of beginners easy
End of explanation
"""


x.size()

x.size()[0]

x + y

y[:, :2]

"""
Explanation: Numpy Like Ops
End of explanation
"""


x.numpy()

numpy_x = x.numpy()
type(numpy_x)

torch_x = torch.from_numpy(numpy_x)
print(type(torch_x), type(x))

"""
Explanation: Numpy Bridge
If you are coming from numpy background, you can still code in numpy and where you think you need a support of a framework or GPU, migrate your data to PyTorch and take it back when you need it there
End of explanation
"""


x + y

x = x.cuda()
y = y.cuda()
x + y

"""
Explanation: On GPU
End of explanation
"""


x = torch.cuda.FloatTensor(5,3)

"""
Explanation: Create GPU tensors instead of moving to GPU
End of explanation
"""


x = torch.Tensor([12.3])

x = Variable(x, requires_grad=True)
print('x.grad: ', x.grad)
print('x.data: ', x.data)

y = (x ** x) * (x - 2)
z = F.tanh(y)

z.backward()

print('x.grad: ', x.grad)
print('x.data: ', x.data)

"""
Explanation: Variable, Function and AutoGrad
These are the core components of Pytorch. If you get em right, you are PRO, from day 1.
- Variables keep the track of your graph
- Functions are the OPS which communicate to other variables and keep your graph acyclic
- Backward() function (or autograd) do the backward pass following the history saved in the variables and creates the grad for you. Don't worry, PyTorch follows python philosophy, It won't update your parameters implicitly. It will store the gradients in .grad variable.
End of explanation
"""


print('creator of z: ', z.creator)
print('creator of creator of z: ', z.creator.previous_functions[0][0])
print('creator of creator of creator of z: ', z.creator.previous_functions[0][0].previous_functions[0][0])

"""
Explanation: Creator
Creators keep the track of your graph for autograd to do backpropagation
End of explanation
"""


