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

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

深度学习计算

看看基础概念:层与块

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

\Input \\xrightarrow{一组参数}标量输出 \\

我们通过更新参数,优化某些目标函数,如loss

那么复杂模型、多个输出应该如何处理呢?

我们引入 ,可以理解成 class,以便将多个层结合成块。

每个块:

  • 有前向传播函数
  • 有反向传播函数
  • 储存必须的参数

参数管理

以此为例,

python 复制代码
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))

这是单隐藏层的MLP

我们可以嵌套多个block :

python 复制代码
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 的参数就是

python 复制代码
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维张量:
rgnet033

这就是最大索引

下面是简单的初始化:

python 复制代码
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(雾)中的多表共用主键。

python 复制代码
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[2].weightnet[4].weight是完全等价的。这里的等价意味着不仅初始值一样,过程中修改任何一个参数,两个都会变动,即对象等价。至于它的梯度,是net[2]net[4],即第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} \\

因此

python 复制代码
id(net[2].weight) == id(net[4].weight)

返回为True

延迟初始化

我们很难在编写MLP的时候就能确认输入维度,那不妨在第一次模型传递时再初始化参数。

此外,如果模型的参数太多,为了防止构建时太占内存,我们可以现场分配。

对于Pytorch,我们使用LazyLinear以实现惰性初始化参数。

python 复制代码
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) \\

避坑点:如果你试图

python 复制代码
self.net = nn.Sequential(self.linear, nn.ReLU(),nn.Linear(32, 10))

,而后直接调用self.net(data),有很大问题:

  • 初始化时是None,不是nn.Module的子类模块,无法会报错
  • 如果通过一些方法初始化成功,forward中更新self.linearSequential里面的不会更新

你可以直接调用pytorch的LazyLinear

python 复制代码
self.linear = nn.LazyLinear(out_features = 10)

注:延后初始化的变量或层,在第一次运行后,一般来说,不能再改变其形状

此外,这也是一种延后初始化,是合法的:

python 复制代码
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\)

我最开始理解为简单的张量求和降维(显然不对)

python 复制代码
class ReduceDim(nn.Module):
    def __init__(self):
        super().__init__()
    def forward(self, X):
        return X.sum(dim=[1, 2])

然而,题目的意思是

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

\y_k=x_1x_1\\cdot W_{11k}+x_1x_2\\cdot W_{12k}+x_1x_3\\cdot W_{13k}+\\dots+x_nx_n\\cdot W_{nnk} \\

为了简化运算,我们使用爱因斯坦求和 einsum 标记。

这里这个文章很好:看图学 AI:einsum 爱因斯坦求和约定到底是怎么回事? - 知乎

故在此不再赘述。

用了这个标记法,我们可以直接简化计算式:

python 复制代码
y = torch.einsum('bi,ijK,bj->bK', x, self.W, x)
return y

简单理解一些这个求和表达式:

  • 爱因斯坦求和规则:在输出标记中不出现的维度会被求和掉
  • 输入标记:bi, ijK, bj
  • 输出标记:bK
  • 被求和的梯度:i 和 j (输入中出现但是输出中不出现)
    这就是那个题目要求的公式。
    等价代码:
python 复制代码
result = torch.zeros(x.shape, W.shape[2])
for b in range(x.shape):
	for k in range(W.shape[2]):
		total = 0
		for i in range(x.shape):
			for j in range(x.shape):
				total += x[b,i] * W[i,j,k] * x[b,j]
		result[b,k] = total

综上我们有了:

python 复制代码
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([[1.0, 2.0, 3.0]])
print("Input:", x)
print("Output:", model(x))
相关推荐
EQUINOX1几秒前
【ch03】Coding-attention-mechanisms
人工智能·深度学习·机器学习
深度之眼34 分钟前
感觉2026年将是Agent Memory元年...
机器学习·agent
小糖学代码41 分钟前
机器学习:5.深度学习
人工智能·深度学习·机器学习
m沐沐2 小时前
【深度学习】PyTorch CNN 手写数字识别(卷积神经网络)
人工智能·pytorch·python·深度学习·机器学习·pycharm·cnn
王莎莎-MinerU2 小时前
从 OCR 到 Context Engineering:用 MinerU 搭一个可复现文档解析评测
人工智能·深度学习·机器学习·pdf·ocr·个人开发
ZHW_AI课题组2 小时前
基于KNN的帕尔默企鹅种类预测分类
人工智能·机器学习·分类·数据挖掘
xiaoxiaoxiaolll2 小时前
《Light: Sci & Appl》论文解读:基于q-BIC-CIT的热调硅膜超表面,突破静态光子学极限
机器学习
学习3人组2 小时前
Python 评论朴素贝叶斯文本情感分析示例
人工智能·python·机器学习
2401_885665192 小时前
从零搭建卷积神经网络:基于PyTorch实现MNIST手写数字分类
pytorch·python·神经网络·算法·机器学习·分类·cnn
大模型最新论文速读3 小时前
06-10 · LLM 最新论文速览
论文阅读·人工智能·深度学习·机器学习·自然语言处理