书接上文
接着上次的笔记,在原本自回归模型基础上加入了深度学习,即由神经网络参数化的自回归模型,算是真正意义上的“深度”生成模型了。从使用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
把代码拿给GPT老师看了一眼,老师立马给出了详细解释:
“
这段代码实现了一个因果卷积(Causal Convolution)的自回归模型。下面逐行解释代码的作用:
-
class CausalConv1d(nn.Module)::定义了一个继承自nn.Module的类CausalConv1d,表示一个因果卷积的一维模型。
-
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:其他可选参数。
-
super(CausalConv1d, self).init()::调用父类的构造函数。
-
self.kernel_size = kernel_size:将传入的卷积核大小保存为对象的属性kernel_size。
-
self.dilation = dilation:将传入的扩张率保存为对象的属性dilation。
-
self.A = A:将传入的布尔值保存为对象的属性A,用于控制是否使用“A型”因果卷积。
-
self.padding = (kernel_size - 1) * dilation + A * 1:计算因果卷积的padding大小。
- 当不使用“A型”因果卷积时,padding大小为(kernel_size - 1) * dilation,保证了输出与输入的长度相同。
- 当使用“A型”因果卷积时,padding大小为(kernel_size - 1) * dilation + 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:其他可选参数。
- 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
GPT老师:
“
这段代码定义了一个用于计算对数分类交叉熵(log categorical cross entropy)的函数log_categorical。下面逐行解释代码的作用:
-
EPS = 1.e-5:定义一个小的常量EPS,用于避免取对数时出现除零错误。
-
def log_categorical(x, p, num_classes=256, reduction=None, dim=None)::定义一个函数log_categorical,用于计算对数分类交叉熵。
- x:输入的真实标签,通常是一个整数张量。
- p:输入的概率分布,通常是一个浮点型张量,表示模型的预测概率。
- num_classes:分类问题的类别数,默认为256。
- reduction:指定损失函数的降维方法,默认为None,即不进行降维。
- dim:指定降维的维度,默认为None,表示对整个张量进行降维。
-
x_one_hot = F.one_hot(x.long(), num_classes=num_classes):将真实标签x转换为one-hot编码,得到一个维度为[batch_size, num_classes]的独热编码张量x_one_hot。
-
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。
-
if reduction == ‘avg’::如果指定了reduction为’avg’,表示对损失进行平均降维。
- return torch.mean(log_p, dim):在dim维度上计算均值,返回一个维度为[batch_size]的张量,其中每个元素表示对应样本的平均损失。
- elif reduction == ‘sum’::如果指定了reduction为’sum’,表示对损失进行求和降维。
- return torch.sum(log_p, dim):在dim维度上计算总和,返回一个维度为[batch_size]的张量,其中每个元素表示对应样本的总损失。
- 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
GPT老师:
“
这段代码定义了一个自回归模型(Autoregressive Model),用于生成具有时间序列结构的数据。下面逐行解释代码的作用:
- class ARM(nn.Module)::定义了一个继承自nn.Module的类ARM,表示一个自回归模型。
- def init(self, net, D=2, num_vals=256)::构造函数,初始化自回归模型的参数。
- net:一个神经网络模型,用于生成自回归模型的条件概率分布。
- D:表示样本的维度。
- num_vals:分类问题的类别数,默认为256。
- self.net = net:将传入的神经网络模型保存为对象的属性net。
- self.num_vals = num_vals:将传入的分类数保存为对象的属性num_vals。
- self.D = D:将传入的维度保存为对象的属性D。
- 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。
- def forward(self, x, reduction=‘avg’):
- x:输入的自变量样本。
- reduction:指定损失函数的降维方法,默认为’avg’,即计算平均损失。
- -(self.log_prob(x).mean()):计算并返回负对数似然的平均损失。
- 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):计算并返回对数概率的总和。
- 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(xd∣x<d)=Categorical(xd∣θd(x<d))=l=1∏L((θ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))
这里的 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的推导过程:
- 首先,我们调用self.f(x)得到一个条件概率张量mu_d,其中mu_d的形状为[batch_size, sequence_length, num_vals],表示每个时间步的条件概率分布。
- 接下来,我们调用log_categorical(x, mu_d, num_classes=self.num_vals, reduction=‘sum’, dim=-1)计算分类交叉熵的对数概率,即计算条件概率mu_d下,真实标签x的对数概率。这里的num_classes传入了self.num_vals,表示分类问题的类别数。
- 在log_categorical()函数中,我们使用one-hot编码将真实标签x转换为x_one_hot。这样,x_one_hot的形状为[batch_size, sequence_length, num_vals],表示每个时间步真实标签的one-hot编码。
- 然后,我们计算对数概率log_p,通过将x_one_hot与取对数后的条件概率mu_d相乘得到。这一步的计算可以表示为:log_p = x_one_hot * torch.log(torch.clamp(mu_d, EPS, 1. - EPS))。其中,torch.clamp函数用于将条件概率限制在一个较小的范围内,避免取对数时出现除零错误。
- 接下来,根据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)
作者在这里也贴心的为我们绘制了基本的结构图:
print(summary(model, torch.zeros(1, 64), show_input=False, show_hierarchical=False))
-----------------------------------------------------------------------
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
-----------------------------------------------------------------------
summary函数是调用了pytorch_model_summary库。
这里再一次上书上的图,方便大家对比着看:
好吧,能力有限,这节的笔记就到这里,作者在书的后面还加上了掩码卷积核的应用。解决一维卷积在2D空间上有空间依赖性的问题。具体就不在这里说了。。。
感谢大家能耐心看到这里,下一节终于进入第三章,也是深度生成模型的第二个大模型——流模型。大家如果感兴趣可以继续关注,感谢大家支持。我抓紧学习。