Needle Module

Needle中Module类的作用是将我们在Homework1实现的底层算子组成更高抽象层方便模型搭建,比如抽象Dropout,Residual层。Module作为一个父类,当需要添加一个新的模块层的时候,我们只需要集成这个父类即可。

其重要的函数是__init__forward 函数。

class Module:
    def __init__(self):
        self.training = True

    def parameters(self) -> List[Tensor]:
        """Return the list of parameters in the module."""
        return _unpack_params(self.__dict__)

    def _children(self) -> List["Module"]:
        return _child_modules(self.__dict__)

    def eval(self):
        self.training = False
        for m in self._children():
            m.training = False

    def train(self):
        self.training = True
        for m in self._children():
            m.training = True

    def __call__(self, *args, **kwargs):
        return self.forward(*args, **kwargs)

一个简单Module子类的forward示例:

class Identity(Module):
    def forward(self, x):
        return x

Xavier和Kaiming Initialization的介绍

在实现代码之前,我们先来看看模型参数(权重)初始化的一些问题。如果你已经熟悉这两种初始化以及他们解决的问题,直接跳过这部分即可。

训练模型的过程是对于模型的参数weights的进行梯度下降,因此模型权重的数值对于训练至关重要。如果我们将模型的权重都初始化为一个常数,比如0,对于训练的模型会出现如下问题:对称性破坏失败,表达能力受限,训练效率低下甚至停滞,容易陷入坏的局部最优。

一个很容易想到的解决方法是随机初始化模型的参数,但仍会遇到一些问题。

  1. 如果初始权重方差选得太小,前向传播中信号逐层衰减,反向传播时梯度也会越来越小,导致靠近输入层的权重几乎得不到更新,这导致了梯度消失。反之,如果方差选得太大,则信号会呈指数级增长,训练中出现梯度爆炸,造成数值不稳定;
  2. 激活函数饱和:对于 sigmoid、tanh 这些“有饱和区”的激活函数,如果输入分布不在零附近,很多单元会一开始就被“挤”到饱和区,梯度几乎为零;
  3. 对于 ReLU,如果初始化时某些单元的权重偏负(或在训练初期更新就偏负),该单元可能一直输出 0——成为“死神经元”,再也不会恢复。
  4. 随机性带来的训练不稳定:每次不同的随机种子,训练曲线和最终精度都会有波动;有时会有甚至显著差异。