<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Introduction to Pytorch for NLP1
This notebook is meant to give a short introduction to Pytorch basics.
You do not have to hand in this tutorial. It is just to help you get started with the projects.
We assume that you have pytorch installed with Python 3. See http
Step1:  Random Seed
We can set a seed so that we get the same random values each time we re-run the notebook.
We will use seed 42 here.
Step2:  Tensors
Tensors are the Torch equivalent of Numpy arrays, but crucially, they can also be used on a GPU (Video card). This can make your calculations a lot faster.
Since they are so similar, you can actually convert most tensors to Numpy arrays (and back), but we won't need to do that so often.
Working with PyTorch, we will need lots of tensors of various shapes.
For example, if we want to transform an input vector $\mathbf{x}$, we will need a weight matrix $W$.
Note
Step3:  Operations
Adding
Step4:  Matrix multiplication
Matrix multiplications are essential for Neural networks. Quite often, we have an input vector $\mathbf{x}$, and then we want to learn weights $W$ that transform that input to some output that we want. 
We will now walk you through matrix multiplication in PyTorch.
Step5:  More operations
In case you want to do something different from addition or matrix-multiplying (and that is quite likely!), you can read here about all of Torch's operations
Step6:  Automatic differentiation with Autograd
One of the main reasons for using PyTorch is that we can automatically get gradients/derivatives of functions that we define. We will mainly use PyTorch for using Neural networks, and they are just fancy functions! If we use weight matrices in our function that we want to learn, then those are called the parameters or simply the weights.
If our Neural Network would output a single scalar value, we would talk about taking the derivative, but you will see that quite often we will have multiple output variables ("values"); in that case we talk about gradients. It's a more general term.
Given an input $\mathbf{x}$, we define our function by manipulating that input, usually by matrix-multiplications with weight matrices and additions with so-called bias vectors. As we manipulate our input, we are automatically creating a computational graph. This graph shows how to arrive at our output from our input. 
PyTorch is a define-by-run framework; this means that we can just do our manipulations, and PyTorch will keep track of that graph for us!
So, to recap
Step7:  x.grad will now contain the gradient $\partial o/ \partial x$, and this will say how a change in $x$ will affect output $o$
Step8:  Verifying the gradient by hand
You should now see a gradient of [2, 2, 2].
We can verify this by hand!
Our output $\mathbf{o}$ is the mean of $\mathbf{y}$
Step9:  In NLP, we are often doing classification, rather than regression (predicting a value). So, even though the name can be misleading, logistic regression is a classifier
Step10:  Now, this is a very simple Neural Network! Actually, it is so simple maybe we should not call it a Neural network. But let's do so anyway.
This is what you should know
Step11:  You can see that
Step12:  Loss
Now we would like to use the autograd functionality to get gradients, but we first need a loss!
The loss will tell us how well our network is doing.
We're going to say that, for our input example, the target value is 0. 
The target is what we wanted our network to predict for the input that we gave.
As our loss (or "criterion") we'll use the MSE, Mean Squared Error
Step13:  Now we'll ask PyTorch to update the weights (parameters) of our neural network so that our next prediction is closer to that target.
We first need to zero-out all gradient tensors. net.zero_grad() will do this for all parameters. It will set p.grad to zeros for each parameter $p$.
Step14:  Now, if we check the output for the same input vector $\mathbf{x}$, the output should be closer to the target
<ASSISTANT_TASK:>
Python Code:
%matplotlib inline
import torch
Explanation: Introduction to Pytorch for NLP1
This notebook is meant to give a short introduction to Pytorch basics.
You do not have to hand in this tutorial. It is just to help you get started with the projects.
We assume that you have pytorch installed with Python 3. See http://www.pytorch.org for instructions on how to install it.
First, let's check that we can import torch:
End of explanation
torch.manual_seed(42)
Explanation: Random Seed
We can set a seed so that we get the same random values each time we re-run the notebook.
We will use seed 42 here.
End of explanation
# create uninitialized 3-D tensor (values can be anything that is in memory!)
x = torch.Tensor(2, 3, 3)
print(x)
# a randomly initialized 2-D tensor (a matrix)
x = torch.rand(4, 3)
print(x)
# how to get its size
print(x.size())
# or if you know there are 2 dimensions:
time, dim = x.size()
print(time, dim)
Explanation: Tensors
Tensors are the Torch equivalent of Numpy arrays, but crucially, they can also be used on a GPU (Video card). This can make your calculations a lot faster.
Since they are so similar, you can actually convert most tensors to Numpy arrays (and back), but we won't need to do that so often.
Working with PyTorch, we will need lots of tensors of various shapes.
For example, if we want to transform an input vector $\mathbf{x}$, we will need a weight matrix $W$.
Note: "Tensor" is a general name. A 1-D tensor is also called a vector, a 2-D tensor a matrix.
End of explanation
# You can add tensors in many ways. 
# The easiest is to simply use a python + operator:
y = torch.rand(4, 3)
print(x + y)
# But you can also use torch.add:
print(torch.add(x, y))
# Provide an output Tensor and save the result there:
result = torch.Tensor(4, 3)
torch.add(x, y, out=result)
print(result)
# Or add in-place (this changes y!)
# Note: Any operation that mutates a tensor in-place is post-fixed with an "_", like "add_" here.
y.add_(x)
print(y)
Explanation: Operations
Adding
End of explanation
# Let's create a vector x with values 0..5
# We can use the arange function for that:
x = torch.arange(0, 6)
print(x)
print(x.shape)
# Now, we will reshape x to have shape 2x3
# That is, it will become a matrix!
# The values will be the same, we will just look at them differently.
x = x.view((2, 3))
print(x)
print(x.shape)
# Now, let's create a square matrix W:
W = torch.arange(0, 9).view((3, 3))
print(W)
# Now we can perform matrix multiplication, since we have 2x3 and 3x3 matrices!
# Verify if you can do this multiplication by hand, too!
# If you need some help, you can check here: https://www.mathsisfun.com/algebra/matrix-multiplying.html
h = torch.matmul(x, W)
print(h)
Explanation: Matrix multiplication
Matrix multiplications are essential for Neural networks. Quite often, we have an input vector $\mathbf{x}$, and then we want to learn weights $W$ that transform that input to some output that we want. 
We will now walk you through matrix multiplication in PyTorch.
End of explanation
print(result[:, 1])    # second column
print(result[0])       # first row
print(result[:2, -1])  # first two rows, last column
print(result[1:3, :])  # middle two rows
Explanation: More operations
In case you want to do something different from addition or matrix-multiplying (and that is quite likely!), you can read here about all of Torch's operations: http://pytorch.org/docs/master/torch.html 
Indexing
It is quite common that we need to select a part of a tensor. Indexing works just like in Numpy!
End of explanation
from torch.autograd import Variable
# create an input vector x
x = Variable(torch.ones(3), requires_grad=True)
print(x)
# Now we define our function
# Note that, even though x is a vector, we can still add a single value to it.
# PyTorch will just add that value to each element of the vector.
y = (x + 2)**2 + 3
print(y)
# y has a grad_fn since it was created by an operation
# this grad_fn will be used by PyTorch for obtaining the gradient
print(y.grad_fn)
# Our final output o is the mean
o = y.mean()
print(o)
# now we can take the gradients by calling o.backward()
# this will populate x.grad
o.backward()
Explanation: Automatic differentiation with Autograd
One of the main reasons for using PyTorch is that we can automatically get gradients/derivatives of functions that we define. We will mainly use PyTorch for using Neural networks, and they are just fancy functions! If we use weight matrices in our function that we want to learn, then those are called the parameters or simply the weights.
If our Neural Network would output a single scalar value, we would talk about taking the derivative, but you will see that quite often we will have multiple output variables ("values"); in that case we talk about gradients. It's a more general term.
Given an input $\mathbf{x}$, we define our function by manipulating that input, usually by matrix-multiplications with weight matrices and additions with so-called bias vectors. As we manipulate our input, we are automatically creating a computational graph. This graph shows how to arrive at our output from our input. 
PyTorch is a define-by-run framework; this means that we can just do our manipulations, and PyTorch will keep track of that graph for us!
So, to recap: the only thing we have to do is to compute the output, and then we can ask PyTorch to automatically get the gradients. 
Note:  Why do we want gradients? Consider that we have defined a function, a Neural Net, that is supposed to compute a certain output $y$ for an input vector $\mathbf{x}$. We then define an error measure that tells us how wrong our network is; how bad it is in predicting output $y$ from input $\mathbf{x}$. Based on this error measure, we can use the gradients to update the weights $W$ that were responsible for the output, so that the next time we present input $\mathbf{x}$ to our network, the output will be closer to what we want. 
Variable
In order to get the autograd functionality, we will need to wrap all our Tensors in Variables. You can import the Variable class like this:
python
from torch.autograd import Variable
Converting a Tensor to a Variable is easy:
```python
example
x = torch.ones(2, 2)
x = Variable(x, requires_grad=True)
```
You can still access the underlying Tensor using .data:
python
print(x.data)
Let's try it out!
Example
We're going to define a function $$y_i = (x_i + 2)^2 + 3$$
And as our final output $o$ we take the mean over all values $y_i$, so we get a single output value:
$$o = \frac{1}{|y|} \sum_i y_i$$
As our input $\mathbf{x}$ we'll use a vector with 3 values: $[1, 1, 1]$.
End of explanation
print(x.grad)
Explanation: x.grad will now contain the gradient $\partial o/ \partial x$, and this will say how a change in $x$ will affect output $o$:
End of explanation
import numpy as np
import matplotlib.pyplot as plt
sigmoid = lambda x: 1 / (1 + np.exp(-x))
x = np.arange(-10., 10., 0.2)
plt.plot(x, sigmoid(x), 'b', label='sigmoid')
Explanation: Verifying the gradient by hand
You should now see a gradient of [2, 2, 2].
We can verify this by hand!
Our output $\mathbf{o}$ is the mean of $\mathbf{y}$:
$$o = \frac{1}{3}\sum_i y_i$$
And $\mathbf{y}$ consists of elements $y_i$:
$$y_i = (x_i+2)^2 + 3$$ 
We know that $y_i = 12$, given that $x_i = 1$ (for each $i$):
$$y_i\bigr\rvert_{x_i=1} = 12$$
Therefore,
$$\frac{\partial o}{\partial x_i} = \frac{\partial o}{\partial y_i}\frac{\partial y_i}{\partial x_i} = \underbrace{\frac{1}{3}}{\frac{\partial o}{\partial y_i}} \underbrace{2 (x_i+2)}{\frac{\partial y_i}{\partial x_i}} = \frac{2}{3} (x_i+2)$$
hence
$$\frac{\partial o}{\partial x_i}\bigr\rvert_{x_i=1} = \frac{2}{3} * 3 = 2$$
NN module
Now if we want to build a big Neural Net, we could specify all our parameters (weight matrices, bias vectors) using Variables, ask PyTorch to calculate the gradients and then adjust the parameters. But things can quickly get cumbersome if we have a lot of parameters. In PyTorch, there is a package called torch.nn that makes building Neural Nets more convenient. 
Let's define a very simple Neural Net to show you how it works. The network performs a logistic regression, i.e. it calculates:
$$ y = \sigma( W \mathbf{x} + b )$$
You have already seen how to calculate $W \mathbf{x} + b$; it's a matrix multiplication with an added bias. The function $\sigma$ might be new: it is the sigmoid function, and it is defined as:
$$ \sigma(x) = \frac{1}{1+ \exp(-x)} $$
The $\exp$ makes sure all values are positive, while the rest scales them between 0 and 1. 
You can see the function below:
End of explanation
import torch.nn as nn
import torch.nn.functional as F
class ExampleNN(nn.Module):
    def __init__(self):
        super(ExampleNN, self).__init__()
        
        # an affine operation: y = Wx + b
        self.linear = nn.Linear(3, 1)
    def forward(self, x):
        x = self.linear(x)
        x = F.sigmoid(x)  # output values are squashed between 0 and 1
        return x
net = ExampleNN()
print(net)
Explanation: In NLP, we are often doing classification, rather than regression (predicting a value). So, even though the name can be misleading, logistic regression is a classifier: we have two output classes 0 and 1.
To get the 2 classes, we use the sigmoid function $\sigma$, so that the values coming out of our NN are between 0 and 1. You can see that in the picture above.
End of explanation
params = list(net.parameters())
for p in params:
    print(p)
Explanation: Now, this is a very simple Neural Network! Actually, it is so simple maybe we should not call it a Neural network. But let's do so anyway.
This is what you should know: 
when defining your Neural Net, you create a class that inherits from nn.Module. 
We called our Neural Net ExampleNN.
Parameters are defined within the __init__ method. Here we defined a single linear layer. The parameters for that layer (a weight matrix $W$ and a bias term $b$) are added automatically to our parameter list.
We define our computation in the forward function. In this case, we apply the linear layer to our input $\mathbf{x}$ and then a sigmoid function.
Let's check if our parameters are indeed as we expect them to be:
End of explanation
x = Variable(torch.randn(3))
print("input:", x)
out = net(x)
print("output:", out)
Explanation: You can see that:
the first parameter is our weight matrix $W$, which is shaped $[1, 3]$.
the second parameter is the bias term $b$, which is a scalar (a single value), since the weight matrix transforms our input $\mathbf{x}$ (with 3 elements) into a single scalar.
Observe that these parameters have been randomly initialized.
Now that we have our NN, we can feed it an input and see what comes out.
The input has to be a Variable.
End of explanation
out = net(x)
target = Variable(torch.zeros(1))  # a dummy target (0.)
criterion = nn.MSELoss()  # this is our criterion
loss = criterion(out, target)
print("output:", out)
print("loss:", loss)
Explanation: Loss
Now we would like to use the autograd functionality to get gradients, but we first need a loss!
The loss will tell us how well our network is doing.
We're going to say that, for our input example, the target value is 0. 
The target is what we wanted our network to predict for the input that we gave.
As our loss (or "criterion") we'll use the MSE, Mean Squared Error:
$$ \text{MSE} = \frac{1}{N} \sum_{i=1}^N (o_i - t_i)^2 $$
I.e. it is the average, over elements $i$, of the squared difference of output $o_i$ with target $t_i$.
Since we have a single output value here, our loss is simply $(o - t)^2$.
Let's calculate our loss:
End of explanation
net.zero_grad()  # reset gradients
loss.backward()  # compute gradients
# update weights
learning_rate = 0.5
for f in net.parameters():
    # for each parameter, take a small step in the opposite dir of the gradient
    # sub_ substracts in-place
    f.data.sub_(f.grad.data * learning_rate)
Explanation: Now we'll ask PyTorch to update the weights (parameters) of our neural network so that our next prediction is closer to that target.
We first need to zero-out all gradient tensors. net.zero_grad() will do this for all parameters. It will set p.grad to zeros for each parameter $p$.
End of explanation
new_out = net(x)
new_loss = criterion(new_out, target)
print("target:", target)
print("out:", out)
print("new out (should be closer to target):", new_out)
print("\nloss:", loss)
print("new loss (should be lower):", new_loss)
Explanation: Now, if we check the output for the same input vector $\mathbf{x}$, the output should be closer to the target:
End of explanation
<END_TASK>