## Encapsulate your model with Modules In the previous example we used bare bone tensors and tensor operations to build our model. To make your code slightly more organized it's recommended to use PyTorch's modules. A module is simply a container for your parameters and encapsulates model operations. For example say you want to represent a linear model `y = ax + b`. This model can be represented with the following code: ```python import torch class Net(torch.nn.Module): def __init__(self): super().__init__() self.a = torch.nn.Parameter(torch.rand(1)) self.b = torch.nn.Parameter(torch.rand(1)) def forward(self, x): yhat = self.a * x + self.b return yhat ``` To use this model in practice you instantiate the module and simply call it like a function: ```python x = torch.arange(100, dtype=torch.float32) net = Net() y = net(x) ``` Parameters are essentially tensors with `requires_grad` set to true. It's convenient to use parameters because you can simply retrieve them all with module's `parameters()` method: ```python for p in net.parameters(): print(p) ``` Now, say you have an unknown function `y = 5x + 3 + some noise`, and you want to optimize the parameters of your model to fit this function. You can start by sampling some points from your function: ```python x = torch.arange(100, dtype=torch.float32) / 100 y = 5 * x + 3 + torch.rand(100) * 0.3 ``` Similar to the previous example, you can define a loss function and optimize the parameters of your model as follows: ```python criterion = torch.nn.MSELoss() optimizer = torch.optim.SGD(net.parameters(), lr=0.01) for i in range(10000): net.zero_grad() yhat = net(x) loss = criterion(yhat, y) loss.backward() optimizer.step() print(net.a, net.b) # Should be close to 5 and 3 ``` PyTorch comes with a number of predefined modules. One such module is `torch.nn.Linear` which is a more general form of a linear function than what we defined above. We can rewrite our module above using `torch.nn.Linear` like this: ```python class Net(torch.nn.Module): def __init__(self): super().__init__() self.linear = torch.nn.Linear(1, 1) def forward(self, x): yhat = self.linear(x.unsqueeze(1)).squeeze(1) return yhat ``` Note that we used squeeze and unsqueeze since `torch.nn.Linear` operates on batch of vectors as opposed to scalars. By default calling parameters() on a module will return the parameters of all its submodules: ```python net = Net() for p in net.parameters(): print(p) ``` There are some predefined modules that act as a container for other modules. The most commonly used container module is `torch.nn.Sequential`. As its name implies it's used to to stack multiple modules (or layers) on top of each other. For example to stack two Linear layers with a `ReLU` nonlinearity in between you can do: ```python model = torch.nn.Sequential( torch.nn.Linear(64, 32), torch.nn.ReLU(), torch.nn.Linear(32, 10), ) ```