# 5.3 nn的子类

对于更大，更复杂的项目，你需要将`nn.Sequential`放在一边转而使用可以带来更大灵活性的东西：将`nn.Module`子类化。要实现`nn.Module`的子类，至少需要定义一个`forward()`函数，该函数将接收模型输入并返回输出。如果你使用的是`torch`中的操作，那么`autograd`会自动处理反向传递。

> 注：通常，整个模型都是作为`nn.Module`的子类实现的，而其内部又可以包含同样是`nn.Module`的子类的模块。

我们将使用越来越复杂的PyTorch函数展示用三种方法来实现相同的网络结构，并改变隐藏层中神经元的数量使它们更易于区分。

第一种方法是`nn.Sequential`，如下面的代码所示。

``` python
seq_model = nn.Sequential(
            nn.Linear(1, 11),
            nn.Tanh(),
            nn.Linear(11, 1))
seq_model
```
输出:
```
Sequential(
  (0): Linear(in_features=1, out_features=11, bias=True)
  (1): Tanh()
  (2): Linear(in_features=11, out_features=1, bias=True)
)
```

尽管此代码能够工作，但是你没有关于各层打算使用的语义信息。你可以通过以下方式改进这种情况：使用有序字典而不是列表作为输入为每一层添加标签：

``` python
from collections import OrderedDict
namedseq_model = nn.Sequential(OrderedDict([
    ('hidden_linear', nn.Linear(1, 12)),
    ('hidden_activation', nn.Tanh()),
    ('output_linear', nn.Linear(12 , 1))
]))
namedseq_model
```
输出:
```
Sequential(
  (hidden_linear): Linear(in_features=1, out_features=12, bias=True)
  (hidden_activation): Tanh()
  (output_linear): Linear(in_features=12, out_features=1, bias=True)
)
```

这样就好多了。除了`nn.Sequential`类提供的顺序性之外，你不能控制通过网络的数据流向。你可以自己定义`nn.Module`的子类来完全控制输入数据的处理方式：

``` python
class SubclassModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden_linear = nn.Linear(1, 13)
        self.hidden_activation = nn.Tanh()
        self.output_linear = nn.Linear(13, 1)
    def forward(self, input):
        hidden_t = self.hidden_linear(input)
        activated_t = self.hidden_activation(hidden_t)
        output_t = self.output_linear(activated_t)
        return output_t
    
subclass_model = SubclassModel()
subclass_model
```
输出:
```
SubclassModel(
  (hidden_linear): Linear(in_features=1, out_features=13, bias=True)
  (hidden_activation): Tanh()
  (output_linear): Linear(in_features=13, out_features=1, bias=True)
)
```

该代码最终变得更加冗长，因为你必须定义所需的网络层，然后定义在`forward`函数中应如何以及以什么顺序使用这些网络层。这为你在模型中提供了难以置信的灵活性，虽然在本例中你不必要在`forward`函数中执行各种有趣的事情。例如你可以用`activated_t = self.hidden_activation(hidden_t) if random.random() > 0.5 else hidden_t`以一般的概率应用激活函数，尽管这在本例中不太有意义。因为PyTorch使用基于动态图的自动梯度机制，所以无论`random.random()`返回什么，梯度都可以通过有时存在的激活正确地流动！

你通常在模型的构造函数中来定义我们在`forward`函数中需要调用的子模块，以便它们可以在模型的整个生命周期中保存其参数。例如，你可以在构造函数中实例化两个`nn.Linear`实例然后在`forward`中使用它。有趣的是，只要你将`nn.Module`实例分配为模型的属性，就像你在构造函数所做的那样，PyTorch就会自动将该模块登记（register）为子模块，这使模型可以访问其子模块的参数，而无需用户进一步操作。

回到前面的`SubclassModel`，你会看到该类的打印输出类似于具有命名参数的顺序模型`namedseq_model`的打印输出。这是有道理的，因为你使用了相同的名称并打算实现相同的网络结构。如果你查看所有三个模型的参数，也会看到相似之处（除了隐藏神经元数量的差异以外）：
``` python
for type_str, model in [('seq', seq_model), ('namedseq', namedseq_model),
     ('subclass', subclass_model)]:
    print(type_str)
    for name_str, param in model.named_parameters():
        print("{:21} {:19} {}".format(name_str, str(param.shape), param.numel())) 
    print()
```
输出:
```
seq
0.weight              torch.Size([11, 1]) 11
0.bias                torch.Size([11])    11
2.weight              torch.Size([1, 11]) 11
2.bias                torch.Size([1])     1

namedseq
hidden_linear.weight  torch.Size([12, 1]) 12
hidden_linear.bias    torch.Size([12])    12
output_linear.weight  torch.Size([1, 12]) 12
output_linear.bias    torch.Size([1])     1

subclass
hidden_linear.weight  torch.Size([13, 1]) 13
hidden_linear.bias    torch.Size([13])    13
output_linear.weight  torch.Size([1, 13]) 13
output_linear.bias    torch.Size([1])     1
```

此处发生的是：调用`named_parameters()`会深入搜寻构造函数中分配为属性的所有子模块，然后在这些子模块上递归调用`named_parameters()`。无论子模块如何嵌套，任何`nn.Module`实例都可以访问其所有子参数的列表。通过访问将由`autograd`计算出的`grad`属性，优化器就知道如何更新参数以最大程度地减少损失。

> 注：Python列表或dict实例中包含的子模块不会被自动登记！你可以使用[`add_module(name, module)`](https://pytorch.org/docs/stable/nn.html#torch.nn.Module.add_module)方法手动登记这些子模块，或者可以使用`nn.ModuleList`和`nn.ModuleDict`类（它们为包含的实例提供自动登记）。

回顾`SubclassModel`类的实现，并考虑在构造函数中登记子模块以便访问其参数的实用功能，似乎同时登记没有参数的子模块（如`nn.Tanh`）有点浪费，直接在`forward`函数中调用它们难道不是更容易吗？当然可以。

PyTorch的每个`nn`模块都有相应的函数。“函数”一词是指“没有内部状态”或“其输出值完全由输入的参数决定”。实际上，`torch.nn.functional`提供了许多与`nn`模块对应的函数，只是所有模型参数（parameter）都作为了参数（argument）移到了函数调用中。例如，与`nn.Linear`对应的是`nn.functional.linear`，它是一个具有参数`(input, weight, bias=None)`的函数，即模型的权重和偏差是该函数的参数。

回到模型中，继续使用`nn.Linear`模块是有意义的，因为方便`SubclassModel`可以在训练期间管理其所有`Parameter`实例。但你可以安全地切换到`Tanh`的函数版本，因为它没有参数：

``` python
class SubclassFunctionalModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.hidden_linear = nn.Linear(1, 14)
        # 去掉了nn.Tanh()
        self.output_linear = nn.Linear(14, 1)
        
    def forward(self, input):
        hidden_t = self.hidden_linear(input)
        activated_t = torch.tanh(hidden_t) # nn.Tanh对应的函数
        output_t = self.output_linear(activated_t)
        return output_t
    
func_model = SubclassFunctionalModel()
func_model
```
输出:
```
SubclassFunctionalModel(
  (hidden_linear): Linear(in_features=1, out_features=14, bias=True)
  (output_linear): Linear(in_features=14, out_features=1, bias=True)
)
```

这个版本更加简洁（随着模型变得越来越复杂，所需代码行将逐渐累加！），而且完全等同于非函数版本。注意，在构造函数中实例化需要参数进行初始化的模块仍然是有意义的。例如，`HardTanh`使用可选的`min_val`和`max_val`参数，所以你应该创建`HardTanh`实例并重用它而不是在`forward`中重复声明这些参数。

> 小贴士：尽管1.0版的`torch.nn.function`中仍存在诸如`tanh`之类的通用科学函数，但不建议使用这些API，而应使用顶级`torch`命名空间中的API，例如`torch.tanh`。更多其他函数保留在`torch.nn.functional`中。


## 小结

我们在本章讨论了很多内容，虽然我们只是处理了一个简单的问题。我们剖析了可微模型构建，并通过使用梯度下降，先使用原始`autograd`然后依赖`nn`对模型进行训练。至此，你应该对（神经网络）幕后发生的事情充满信心。

我们希望PyTorch这种口味能够符合你的胃口！

## 更多资源

大量书籍和其他资源可用来帮助学习深度学习。 我们推荐以下内容：

* PyTorch官方网站：https://pytorch.org ；
* Andrew W. Traska撰写的[《Grokking Deep Learning》]( https://www.manning.com/books/grokking-deep-learning)是开发强大模型和理解深度神经网络基础机制的重要资源；
* 有关该领域的详尽介绍和参考，我们推荐你进入Ian Goodfellow, Yoshua Bengio和Aaron Courville的[《Deep Learning》](https://www.deeplearningbook.org)；
* 最后但并非最不重要的是，这本书的完整版本现已在Early Access中提供，预计印刷日期为2019年末：https://www.manning.com/books/deep-learning-with-pytorch 。

## 练习

* 在你的简单神经网络模型上做不同隐藏神经元的数量以及学习率的实验。
    * 哪些变化导致模型的输出更加线性？ 
    * 你能否使模型明显过拟合数据？

* 物理学中第三难的问题是找到合适的葡萄酒来庆祝研究发现。从第3章中加载葡萄酒数据，并使用适当数量的输入参数创建一个新模型。
    * 与温度计数据相比，训练需要多长时间？
    * 你能否解释哪些因素会影响训练时间？
    * 在训练该数据集时损失会下降吗？
    * 如何绘制该数据集的图示？

## 总结

* 神经网络可以自动调整以专门解决手头的问题。
* 神经网络允许我们轻松访问模型中任何参数关于损失的导数，这使参数的改进变得高效。凭借自动求导引擎，PyTorch可以轻松计算出这些导数。
* 围绕线性变换的激活函数使神经网络能够逼近高度非线性函数，同时使它们足够简单以容便易优化。
* `nn`模块与张量标准库一起提供了用于创建神经网络的所有构建块。
* 要想识别过拟合，必须将训练集与验证集分开。没有解决过拟合的诀窍，但是获取更多数据（或数据具有更多可变性）并采用更简单的模型是不错的尝试。
* 任何从事数据科学的人都应该一直在绘制数据。