当前位置:   article > 正文

深度自回归模型实战——《深度生成模型》阅读笔记(4)_causal-conv1d

causal-conv1d

书接上文

接着上次的笔记,在原本自回归模型基础上加入了深度学习,即由神经网络参数化的自回归模型,算是真正意义上的“深度”生成模型了。从使用MLP建模的有限记忆自回归到使用LSTM和RNN的长距记忆自回归,最后再到作者书中提到的借用因果卷积,解决卷积神经网络面对长距依赖关系的短板,从而构建的基于卷积神经网络的自回归模型。

今天这篇笔记,开始学习和研究作者对于自回归模型实践部分的内容(作者代码)。之前都是既麻烦GPT老师帮我解释理论问题又麻烦她帮我写代码,今天咱就不麻烦GPT老师写代码了,今天麻烦她帮我读代码~~~~

因果一维卷积

定义因果一维卷积

下面是作者定义一维卷积的代码:

class CausalConv1d(nn.Module):
    """
    A causal 1D convolution.
    """

    def __init__(self, in_channels, out_channels, kernel_size, dilation, A=False, **kwargs):
        super(CausalConv1d, self).__init__()

        # attributes:
        self.kernel_size = kernel_size
        self.dilation = dilation
        self.A = A
        
        self.padding = (kernel_size - 1) * dilation + A * 1

        # module:
        self.conv1d = torch.nn.Conv1d(in_channels, out_channels,
                                      kernel_size, stride=1,
                                      padding=0,
                                      dilation=dilation,
                                      **kwargs)

    def forward(self, x):
        x = torch.nn.functional.pad(x, (self.padding, 0))
        conv1d_out = self.conv1d(x)
        if self.A:
            return conv1d_out[:, :, : -1]
        else:
            return conv1d_out
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

把代码拿给GPT老师看了一眼,老师立马给出了详细解释:


这段代码实现了一个因果卷积(Causal Convolution)的自回归模型。下面逐行解释代码的作用:

  1. class CausalConv1d(nn.Module)::定义了一个继承自nn.Module的类CausalConv1d,表示一个因果卷积的一维模型。

  2. def init(self, in_channels, out_channels, kernel_size, dilation, A=False, **kwargs)::构造函数,初始化因果卷积模型的参数。

  • in_channels:输入通道数。
  • out_channels:输出通道数。
  • kernel_size:卷积核大小。
  • dilation:扩张率(dilation rate)。
  • A:一个布尔值,用于控制是否使用“A型”因果卷积。
  • **kwargs:其他可选参数。
  1. super(CausalConv1d, self).init()::调用父类的构造函数。

  2. self.kernel_size = kernel_size:将传入的卷积核大小保存为对象的属性kernel_size。

  3. self.dilation = dilation:将传入的扩张率保存为对象的属性dilation。

  4. self.A = A:将传入的布尔值保存为对象的属性A,用于控制是否使用“A型”因果卷积。

  5. self.padding = (kernel_size - 1) * dilation + A * 1:计算因果卷积的padding大小。

  • 当不使用“A型”因果卷积时,padding大小为(kernel_size - 1) * dilation,保证了输出与输入的长度相同。
  • 当使用“A型”因果卷积时,padding大小为(kernel_size - 1) * dilation + 1,需要将输出的最后一个元素丢弃,使得输出长度比输入长度小1。
  1. self.conv1d = torch.nn.Conv1d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=dilation, **kwargs):创建一个一维卷积层conv1d。
  • in_channels:输入通道数。
  • out_channels:输出通道数。
  • kernel_size:卷积核大小。
  • stride:步长,默认为1。
  • padding:填充大小,默认为0。
  • dilation:扩张率。
  • **kwargs:其他可选参数。
  1. def forward(self, x)::定义前向传播方法。
  • x = torch.nn.functional.pad(x, (self.padding, 0)):对输入进行填充,实现因果卷积。

    • 当不使用“A型”因果卷积时,对输入的左侧(序列开始位置)进行self.padding个0填充。
    • 当使用“A型”因果卷积时,对输入的两侧(序列开始位置和序列末尾位置)分别进行(self.padding // 2, self.padding // 2)个0填充。
  • conv1d_out = self.conv1d(x):将填充后的输入通过卷积层进行卷积操作,得到卷积输出。

  • if self.A::当使用“A型”因果卷积时。

    • return conv1d_out[:, :, :-1]:返回卷积输出的前-1个元素,即去除掉输出的最后一个元素,使得输出长度与输入长度相同。
  • else::当不使用“A型”因果卷积时。

    • return conv1d_out:返回完整的卷积输出。

这段代码实现了一个因果卷积模型,其特点是能够处理时序数据,并且在卷积过程中保持了因果性,即输出的每个元素仅依赖于输入序列中它之前的元素。这对于许多时序数据任务,如语音处理或时间序列预测,非常有用。

这详细程度,初学者也能轻松看懂代码了。令我意外的是,GPT老师竟然给出了参数A的正确释义,表示是否使用“A型”因果卷积,在上一节的笔记中我并没有解释“A型”和“B型”两种类型的因果卷积,不过我有在最后让GPT老师解释因果卷积,“它只考虑过去时刻的输入数据对当前时刻的输出有影响,而不考虑未来时刻的输入”,这里其实还并不完整,作者在书中有提到,因果卷积意味着Conv1D只可以依赖最后的k个输入,而“A型”:可以不含有当下的输入,“B型”:也可以含有当下的输入。

不过,GPT老师对于return conv1d_out[:, :, :-1]的说明没有像作者的那样说明由于是“A型”卷积,所以不可以依赖当前部分所以才删除最后一个元素。

继续往下,开始为后面训练做准备了,下面是一段用于计算对数概率和分类的代码:

EPS = 1.e-5

def log_categorical(x, p, num_classes=256, reduction=None, dim=None):
    x_one_hot = F.one_hot(x.long(), num_classes=num_classes)
    log_p = x_one_hot * torch.log(torch.clamp(p, EPS, 1. - EPS))
    if reduction == 'avg':
        return torch.mean(log_p, dim)
    elif reduction == 'sum':
        return torch.sum(log_p, dim)
    else:
        return log_p
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

GPT老师:

这段代码定义了一个用于计算对数分类交叉熵(log categorical cross entropy)的函数log_categorical。下面逐行解释代码的作用:

  1. EPS = 1.e-5:定义一个小的常量EPS,用于避免取对数时出现除零错误。

  2. def log_categorical(x, p, num_classes=256, reduction=None, dim=None)::定义一个函数log_categorical,用于计算对数分类交叉熵。

  • x:输入的真实标签,通常是一个整数张量。
  • p:输入的概率分布,通常是一个浮点型张量,表示模型的预测概率。
  • num_classes:分类问题的类别数,默认为256。
  • reduction:指定损失函数的降维方法,默认为None,即不进行降维。
  • dim:指定降维的维度,默认为None,表示对整个张量进行降维。
  1. x_one_hot = F.one_hot(x.long(), num_classes=num_classes):将真实标签x转换为one-hot编码,得到一个维度为[batch_size, num_classes]的独热编码张量x_one_hot。

  2. log_p = x_one_hot * torch.log(torch.clamp(p, EPS, 1. - EPS)):计算对数概率。首先使用torch.clamp函数将概率p限制在一个较小的范围内,避免对0或1取对数导致的错误。然后将每个one-hot编码的位置上的概率与对应的对数概率相乘,得到一个维度与x_one_hot相同的张量log_p。

  3. if reduction == ‘avg’::如果指定了reduction为’avg’,表示对损失进行平均降维。

  • return torch.mean(log_p, dim):在dim维度上计算均值,返回一个维度为[batch_size]的张量,其中每个元素表示对应样本的平均损失。
  1. elif reduction == ‘sum’::如果指定了reduction为’sum’,表示对损失进行求和降维。
  • return torch.sum(log_p, dim):在dim维度上计算总和,返回一个维度为[batch_size]的张量,其中每个元素表示对应样本的总损失。
  1. else::如果没有指定reduction,则不进行降维。
  • return log_p:返回与输入维度相同的张量log_p,表示每个样本的损失。

这段代码实现了对数分类交叉熵的计算,将模型的预测概率与真实标签进行比较,得到模型的损失。通过设置reduction参数,可以选择是否对损失进行降维,以便在整个批次或样本上计算平均损失或总损失。

自回归模型(ARM)

自回归模型定义

作者选择在因果一维卷积后面定义自回归模型,大概是认为本章重点还是强调一个“深度”其次才是自回归模型,毕竟生成模型还是要建立在深度学习基础上才能最大发挥其威力。下面是定义自回归模型代码:

class ARM(nn.Module):
    def __init__(self, net, D=2, num_vals=256):
        super(ARM, self).__init__()

        print('ARM by JT.')

        self.net = net
        self.num_vals = num_vals
        self.D = D

    def f(self, x):
        h = self.net(x.unsqueeze(1))

        h = h.permute(0, 2, 1)
        p = torch.softmax(h, 2)
        return p
        
    def forward(self, x, reduction='avg'):
        if reduction == 'avg':
            return -(self.log_prob(x).mean())
        elif reduction == 'sum':
            return -(self.log_prob(x).sum())
        else:
            raise ValueError('reduction could be either `avg` or `sum`.')

    def log_prob(self, x):
        mu_d = self.f(x)
        log_p = log_categorical(x, mu_d, num_classes=self.num_vals, reduction='sum', dim=-1).sum(-1)
        
        return log_p

    def sample(self, batch_size):
        x_new = torch.zeros((batch_size, self.D))

        for d in range(self.D):
            p = self.f(x_new)
            x_new_d = torch.multinomial(p[:, d, :], num_samples=1)
            x_new[:, d] = x_new_d[:,0]

        return x_new
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

GPT老师:

这段代码定义了一个自回归模型(Autoregressive Model),用于生成具有时间序列结构的数据。下面逐行解释代码的作用:

  1. class ARM(nn.Module)::定义了一个继承自nn.Module的类ARM,表示一个自回归模型。
  2. def init(self, net, D=2, num_vals=256)::构造函数,初始化自回归模型的参数。
  • net:一个神经网络模型,用于生成自回归模型的条件概率分布。
  • D:表示样本的维度。
  • num_vals:分类问题的类别数,默认为256。
  1. self.net = net:将传入的神经网络模型保存为对象的属性net。
  2. self.num_vals = num_vals:将传入的分类数保存为对象的属性num_vals。
  3. self.D = D:将传入的维度保存为对象的属性D。
  4. def f(self, x)::定义了一个方法f,用于计算条件概率。
  • x:输入的自变量样本。
  • h = self.net(x.unsqueeze(1)):将输入样本通过神经网络模型进行前向传播,得到输出h。
  • h = h.permute(0, 2, 1):将输出维度进行置换,变为[batch_size, sequence_length, num_vals]的形状。
  • p = torch.softmax(h, 2):使用softmax函数对输出进行归一化,得到条件概率p。
  • return p:返回条件概率p。
  1. def forward(self, x, reduction=‘avg’):
  • x:输入的自变量样本。
  • reduction:指定损失函数的降维方法,默认为’avg’,即计算平均损失。
  • -(self.log_prob(x).mean()):计算并返回负对数似然的平均损失。
  1. def log_prob(self, x):
  • x:输入的自变量样本。
  • mu_d = self.f(x):计算给定输入样本x的条件概率。
  • log_p = log_categorical(x, mu_d, num_classes=self.num_vals, reduction=‘sum’, dim=-1).sum(-1):计算并返回对数概率的总和。
  1. def sample(self, batch_size):
  • batch_size:生成样本的批次大小。
  • x_new = torch.zeros((batch_size, self.D)):创建一个形状为(batch_size, self.D)的张量x_new,用于保存生成的样本。
  • for d in range(self.D)::对于每个维度d:
    • p = self.f(x_new):计算给定当前已生成样本x_new的条件概率。
    • x_new_d = torch.multinomial(p[:, d, :], num_samples=1):从条件概率分布中采样一个值。
    • x_new[:, d] = x_new_d[:, 0]:将采样的值赋给当前维度的样本。
  • return x_new:返回生成的样本。
    这段代码定义了一个自回归模型,使用一个神经网络模型作为条件概率分布,可以进行模型训练和样本生成。在训练过程中,使用负对数似然作为损失函数进行优化。在生成过程中,利用条件概率进行采样,生成具有时间序列结构的数据样本。

sample()函数相当于先定义了一个全部为0的张量:x_new = torch.zeros((batch_size, self.D))
然后for循环中迭代的为像素抽样赋值

这里,由于作者的训练数据是图像,所以num_vals表示的是每一个像素所能容纳的值的数量,
因为不是很理解h = h.permute(0, 2, 1)的具体作用,所以又向GPT老师请教了一下:


当使用h.permute(0, 2, 1)时,它会对张量h的维度进行置换。这里的参数(0, 2, 1)指定了置换后的维度顺序,它们的含义如下:

  • 0:表示保持原来的第0个维度不变,也就是批次大小(batch size)。
  • 2:表示原来的第2个维度变为置换后的第1个维度。
  • 1:表示原来的第1个维度变为置换后的第2个维度。
    以一个示例来说明这个过程。假设原始的h张量维度为[batch_size, sequence_length, num_vals],其中:
  • batch_size表示批次大小,即一次处理的样本数量。
  • sequence_length表示时间序列的长度。
  • num_vals表示每个时间步的类别数。
    通过h.permute(0, 2, 1),我们将维度进行了置换。这样,置换后的h张量维度变为[batch_size, num_vals, sequence_length]。
    置换维度的目的是为了满足特定的模型要求或操作要求,比如矩阵乘法、卷积操作等。在这段代码中,将维度置换的目的是为了适应后续的操作。在这里,通过置换维度,将概率分布的类别数维度变为最后一个维度,以便后续的softmax操作能够在正确的维度上进行计算。
    需要注意的是,置换维度的过程并不改变数据的内容,只是改变了它们在张量中的排列顺序,以满足后续操作的需求。
    总结起来,h.permute(0, 2, 1)的作用是将张量的维度进行置换,以满足后续操作的要求。

Categorical()函数

下面要重重敲黑板啦,代码中log_prob(self, x)是这节中的重点。

还记得前面定义log_categorical()的函数吗?作者在书中提到:“因为表达图像的像素值是整数,我们会用类别分布来表达(下一章我们会具体讲到表达图像分布的几种不同方式)。我们使用CausalConv1D参数化的自回归模型ARM来为p(x)建模。所得结果用概率表达如下:”

p ( x d ∣ x < d ) = C a t e g o r i c a l ( x d ∣ θ d ( x < d ) ) = ∏ l = 1 L ( ( θ d , l ) [ x d = l ] ) p(x_{d}|x_{<d})=Categorical(x_{d}| \theta_{d}(x_{<d}))=\prod_{l=1}^{L}((\theta_{d,l})^{[x_{d}=l]}) p(xdx<d)=Categorical(xdθd(x<d))=l=1L((θd,l)[xd=l])

其中: [ x d = l ] [x_{d}=l] [xd=l]是艾弗森括号,当 x d = l x_{d}=l xd=l时,括号的值等于1,反之为0。

作者书中没有具体写明Categorial()具体解法是什么,那我们还是只有看代码来具体了解了。

“可以详细说下log_categorical的数学表达和数学含义吗?”

GPT老师:

当解释 log_categorical 函数的数学表达和数学含义时,首先需要了解分类交叉熵和对数运算。
分类交叉熵是一种常用的损失函数,用于衡量模型的预测结果与真实标签之间的差异。它基于概率分布的概念,用于度量两个概率分布之间的距离。
假设有一个分类问题,包含多个类别(例如图像分类问题中的不同物体类别)。对于每个样本,我们有一个真实标签(一个整数,表示样本所属的类别)和一个模型的预测概率分布(一个向量,表示模型对每个类别的预测概率)。
对于一个样本,log_categorical 函数的数学表达可以描述为:

log_categorical(x, p, num_classes, reduction, dim) = log(p(x))
  • 1

这里的 p(x) 是模型预测概率向量 p 中与真实标签 x 对应位置的概率。函数的目标是计算对数概率 log(p(x))。
在代码中,我们首先将真实标签 x 转换为一个 one-hot 编码向量 x_one_hot,其中真实标签对应的位置为 1,其他位置为 0。然后,我们将这个向量与预测概率向量 p 中对应位置的概率相乘,得到一个新的向量 log_p。
在此过程中,我们使用对数运算(torch.log)来计算对数概率,并使用 torch.clamp 函数将概率 p 限制在一个小的范围内,避免出现取对数时的错误。
对于降维参数 reduction 和 dim,可以根据需要选择不同的方式对损失进行降维。如果 reduction 设置为 ‘avg’,则在 dim 维度上计算 log_p 的均值。如果 reduction 设置为 ‘sum’,则在 dim 维度上计算 log_p 的总和。如果没有设置 reduction,则返回与输入维度相同的 log_p。
总的来说,log_categorical 函数的数学含义是计算模型的预测概率与真实标签之间的对数概率,并根据需要对损失进行降维。这可以帮助我们度量模型的预测结果与真实标签之间的差异,并用作优化模型参数的依据。
希望这样的解释能够更清晰地说明函数的数学表达和数学含义。

感觉还是不太明白,发现上面ARM定义的代码中log_p的定义函数中调用了log_categorical函数,再问问GPT老师:“可以结合之前log_categorical的定义代码,具体解释上面log_p吗?给出详细的推导公式和推导过程”

GPT老师:

当我们调用log_prob(self, x)方法时,其中的log_p变量通过调用log_categorical()函数计算而来。下面将详细解释log_p的推导过程:

  1. 首先,我们调用self.f(x)得到一个条件概率张量mu_d,其中mu_d的形状为[batch_size, sequence_length, num_vals],表示每个时间步的条件概率分布。
  2. 接下来,我们调用log_categorical(x, mu_d, num_classes=self.num_vals, reduction=‘sum’, dim=-1)计算分类交叉熵的对数概率,即计算条件概率mu_d下,真实标签x的对数概率。这里的num_classes传入了self.num_vals,表示分类问题的类别数。
  3. 在log_categorical()函数中,我们使用one-hot编码将真实标签x转换为x_one_hot。这样,x_one_hot的形状为[batch_size, sequence_length, num_vals],表示每个时间步真实标签的one-hot编码。
  4. 然后,我们计算对数概率log_p,通过将x_one_hot与取对数后的条件概率mu_d相乘得到。这一步的计算可以表示为:log_p = x_one_hot * torch.log(torch.clamp(mu_d, EPS, 1. - EPS))。其中,torch.clamp函数用于将条件概率限制在一个较小的范围内,避免取对数时出现除零错误。
  5. 接下来,根据reduction参数的值,我们对log_p进行降维操作。如果reduction为’sum’,我们将在最后一个维度上计算总和,即log_p.sum(dim=-1)。这样得到的log_p是一个形状为[batch_size, sequence_length]的张量,表示每个样本每个时间步的总对数概率。如果reduction为’avg’,我们将在最后一个维度上计算均值,即log_p.mean(dim=-1)。这样得到的log_p是一个形状为[batch_size, sequence_length]的张量,表示每个样本每个时间步的平均对数概率。
    最终,我们通过调用log_p的结果来计算损失函数,并用负对数似然作为损失函数的计算方式。

有了上述建模的过程,我们可以给上数据开始训练和测试,作者写了训练、评估和画图取样的辅助函数,这里不展开说,具体可参考作者代码。

这里我比较感兴趣的是卷积网络的构造:
先是两个超参数:
D = 64 ——输入维度
M = 256 ——“尺度和平移网络中神经元个数”,嗯~,不是太理解,不过下面代码M是神经网络的输入输出通道数,大概了解了。

代码长这样:

likelihood_type = 'categorical'

num_vals = 17

kernel = 7

net = nn.Sequential(
    CausalConv1d(in_channels=1, out_channels=M, dilation=1, kernel_size=kernel, A=True, bias=True),
    nn.LeakyReLU(),
    CausalConv1d(in_channels=M, out_channels=M, dilation=1, kernel_size=kernel, A=False, bias=True),
    nn.LeakyReLU(),
    CausalConv1d(in_channels=M, out_channels=M, dilation=1, kernel_size=kernel, A=False, bias=True),
    nn.LeakyReLU(),
    CausalConv1d(in_channels=M, out_channels=num_vals, dilation=1, kernel_size=kernel, A=False, bias=True))

model = ARM(net, D=D, num_vals=num_vals)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

作者在这里也贴心的为我们绘制了基本的结构图:

print(summary(model, torch.zeros(1, 64), show_input=False, show_hierarchical=False))
  • 1
-----------------------------------------------------------------------
      Layer (type)        Output Shape         Param #     Tr. Param #
=======================================================================
    CausalConv1d-1        [1, 256, 64]           2,048           2,048
       LeakyReLU-2        [1, 256, 64]               0               0
    CausalConv1d-3        [1, 256, 64]         459,008         459,008
       LeakyReLU-4        [1, 256, 64]               0               0
    CausalConv1d-5        [1, 256, 64]         459,008         459,008
       LeakyReLU-6        [1, 256, 64]               0               0
    CausalConv1d-7         [1, 17, 64]          30,481          30,481
=======================================================================
Total params: 950,545
Trainable params: 950,545
Non-trainable params: 0
-----------------------------------------------------------------------
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

summary函数是调用了pytorch_model_summary库。

这里再一次上书上的图,方便大家对比着看:

《深度生成模型》P17图

好吧,能力有限,这节的笔记就到这里,作者在书的后面还加上了掩码卷积核的应用。解决一维卷积在2D空间上有空间依赖性的问题。具体就不在这里说了。。。

感谢大家能耐心看到这里,下一节终于进入第三章,也是深度生成模型的第二个大模型——流模型。大家如果感兴趣可以继续关注,感谢大家支持。我抓紧学习。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/565367
推荐阅读
相关标签
  

闽ICP备14008679号