赫连如冰 发表于 2025-12-2 00:00:44

一只菜鸟学机器学习的日记:入门深度学习计算

本文以作者阅读《Dive into Deep Learning》为线索,融合串联了自身理解感悟、原始论文、优秀文章等。如有无意侵权,请联系本人删除。
深度学习计算

看看基础概念:层与块

显然,一个线性模型只会有一个输出:

\
我们通过更新参数,优化某些目标函数,如loss
那么复杂模型、多个输出应该如何处理呢?
我们引入块,可以理解成 class,以便将多个层结合成块。
每个块:

[*]有前向传播函数
[*]有反向传播函数
[*]储存必须的参数
参数管理

以此为例,
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))这是单隐藏层的MLP
我们可以嵌套多个block :
def block1():
    return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                         nn.Linear(8, 4), nn.ReLU())
def block2():
    net = nn.Sequential()
    for i in range(4):
      net.add_module(f'block {i}', block1())
    return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))那么这里 rgnet 的参数就是
Sequential(
(0): Sequential(
    (block 0): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 1): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 2): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
    (block 3): Sequential(
      (0): Linear(in_features=4, out_features=8, bias=True)
      (1): ReLU()
      (2): Linear(in_features=8, out_features=4, bias=True)
      (3): ReLU()
    )
)
(1): Linear(in_features=4, out_features=1, bias=True)
)那么很显然,这是一个嵌套的大 Sequential
所以可以当成一个3维张量:
rgnet
这就是最大索引
下面是简单的初始化:
def init_normal(m):
    if type(m) == nn.Linear:
      nn.init.normal_(m.weight, mean=0, std=0.01)
      nn.init.zeros_(m.bias)
net.apply(init_normal)只用了自带的normal初始化,\(\text{weight}\sim\mathcal{N}(0,0.01)\),并设置\(\text{bias}=0\)
为了多层间共享参数,我们可以设置一个稠密层,有点像MySQL(雾)中的多表共用主键。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
                  shared, nn.ReLU(),
                  shared, nn.ReLU(),
                  nn.Linear(8, 1))这样共享了一个稠密层shared,这就意味着net.weight和net.weight是完全等价的。这里的等价意味着不仅初始值一样,过程中修改任何一个参数,两个都会变动,即对象等价。至于它的梯度,是net和net,即第3层和第5层梯度之和:

\[\frac{\partial \mathcal{L}}{\partial W_{shared}} = \sum_{i=1}^{n} \frac{\partial \mathcal{L}}{\partial W_i}\]
其中 \(n\) 是共享该权重的层数。
以上文代码为例:
前向传播:

\[\begin{align*}h^{(1)} &= W_1 x + b_1 \\a^{(1)} &= \text{ReLU}(h^{(1)}) \\h^{(2)} &= W_{\text{shared}} a^{(1)} + b_{\text{shared}} \\a^{(2)} &= \text{ReLU}(h^{(2)}) \\h^{(3)} &= W_{\text{shared}} a^{(2)} + b_{\text{shared}} \\a^{(3)} &= \text{ReLU}(h^{(3)}) \\y &= W_{\text{out}} a^{(3)} + b_{\text{out}}\end{align*}\]
反向传播:

\[\begin{split}&对于共享参数 W_{\text{shared}},梯度来自两条路径:\\&\frac{\partial \mathcal{L}}{\partial W_{\text{shared}}} = \underbrace{\frac{\partial \mathcal{L}}{\partial h^{(3)}} \cdot \frac{\partial h^{(3)}}{\partial W_{\text{shared}}}}_{\text{(3)}} + \underbrace{\frac{\partial \mathcal{L}}{\partial h^{(2)}} \cdot \frac{\partial h^{(2)}}{\partial W_{\text{shared}}}}_{\text{(2)}}\end{split}\]
因此
id(net.weight) == id(net.weight)返回为True
延迟初始化

我们很难在编写MLP的时候就能确认输入维度,那不妨在第一次模型传递时再初始化参数。
此外,如果模型的参数太多,为了防止构建时太占内存,我们可以现场分配。
对于Pytorch,我们使用LazyLinear以实现惰性初始化参数。
class LazyLinear(nn.Module):
    def __init__(self):
      super().__init__()
      self.linear = None
      self.net = nn.Sequential(nn.ReLU(),nn.Linear(32, 10))
    def forward(self, data):
      if self.linear is None:
            self.linear = nn.Linear(in_features=data.shape[-1],
                                                            out_features=32)
      return self.net(self.linear(data))
\[输入(5, 20)\xrightarrow{\text{self.linear}}(5, 30)\xrightarrow{\text{nn.ReLU}}(5, 30)\xrightarrow{\text{nn.Linear}}(5, 10)\]
避坑点:如果你试图
self.net = nn.Sequential(self.linear, nn.ReLU(),nn.Linear(32, 10)),而后直接调用self.net(data),有很大问题:

[*]初始化时是None,不是nn.Module的子类模块,无法会报错
[*]如果通过一些方法初始化成功,forward中更新self.linear时Sequential里面的不会更新
你可以直接调用pytorch的LazyLinear:
self.linear = nn.LazyLinear(out_features = 10)注:延后初始化的变量或层,在第一次运行后,一般来说,不能再改变其形状
此外,这也是一种延后初始化,是合法的:
net = nn.Sequential(
    nn.Linear(20, 256), nn.ReLU(),
    nn.LazyLinear(128), nn.ReLU(),
    nn.LazyLinear(10)
)我们用一个例题结束这一小节:
\(设计一个接受输入并计算张量降维的层,它返回\space y_k = \sum_{i, j} W_{ijk} x_i x_j\)
我最开始理解为简单的张量求和降维(显然不对)
class ReduceDim(nn.Module):
    def __init__(self):
      super().__init__()
    def forward(self, X):
      return X.sum(dim=)然而,题目的意思是

[*]输入:一维向量x
[*]权重:三维张量 W,形状为 (n, n, m)
[*]输出:一维向量 y
有运算:

\
为了简化运算,我们使用爱因斯坦求和 einsum 标记。
这里这个文章很好:看图学 AI:einsum 爱因斯坦求和约定到底是怎么回事? - 知乎
故在此不再赘述。
用了这个标记法,我们可以直接简化计算式:
y = torch.einsum('bi,ijK,bj->bK', x, self.W, x)
return y简单理解一些这个求和表达式:

[*]爱因斯坦求和规则:在输出标记中不出现的维度会被求和掉
[*]输入标记:bi, ijK, bj
[*]输出标记:bK
[*]被求和的梯度:i 和 j (输入中出现但是输出中不出现)
这就是那个题目要求的公式。
等价代码:
result = torch.zeros(x.shape, W.shape)
for b in range(x.shape):
        for k in range(W.shape):
                total = 0
                for i in range(x.shape):
                        for j in range(x.shape):
                                total += x * W * x
                result = total综上我们有了:
class BilinearLayer(nn.Module):
    def __init__(self, input_dim, output_dim):
      super().__init__()
      self.W = nn.Parameter(torch.randn(input_dim, input_dim,
                                                                                  output_dim))
      self.input_dim = input_dim
      self.output_dim = output_dim
    def forward(self, x):
      y = torch.einsum('bi,ijK,bj->bK', x, self.W, x)
      return y
model = BilinearLayer(input_dim=3, output_dim=2)
x = torch.tensor([])
print("Input:", x)
print("Output:", model(x))
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

僚娥 发表于 2025-12-4 06:26:16

过来提前占个楼

戈森莉 发表于 前天 04:28

这个有用。
页: [1]
查看完整版本: 一只菜鸟学机器学习的日记:入门深度学习计算