写在前面:本节详细介绍了 ruotianluo/self-critical.pytorch 中的 Newfc 模型,包括代码解析、网络结构和实现细节等方面。读者将全面深入地了解该模型的特点和优势,并能够运用所学知识进行高质量的代码实现。这是一个必不可少的教程,适合图像字幕生成领域的进阶学习者深入学习。
02Newfc网络模型的解析(for image captioning)
- 1. Newfc网络模型调用详情
- 2. FCModel函数详细解析
- 2.1 `__init__(self, opt)`函数详细解析
- 2.2 `init_weights(self)`函数详细解析
- 2.3 `init_hidden(self, bsz)`函数详细解析
- 2.4 `_forward(self, fc_feats, att_feats, seq, att_masks=None)`函数详细解析
- 3. LSTM_CORE函数详细解析
1. Newfc网络模型调用详情
在代码中网络调用主要通过以下代码实现(在tools/train.py程序中),这段Python代码的注释解释了正在构建一个机器学习模型。首先,通过loader.get_vocab()
方法获取了一个词汇表,然后利用models.setup(opt).cuda()
方法初始化了一个模型,并将其放置在GPU上进行运算。其中,opt.vocab
是指定了输入数据的词汇表,setup(opt)
则是设置模型结构,.cuda()
方法将模型移动到GPU上进行计算。
##########################
# Build model
##########################
opt.vocab = loader.get_vocab()
model = models.setup(opt).cuda()
但是输出结果如下:
NewFCModel(
(embed): Embedding(9488, 512)
(fc_embed): Linear(in_features=2048, out_features=512, bias=True)
(logit): Linear(in_features=512, out_features=9488, bias=True)
(_core): LSTMCore(
(i2h): Linear(in_features=512, out_features=2560, bias=True)
(h2h): Linear(in_features=512, out_features=2560, bias=True)
(dropout): Dropout(p=0.5, inplace=False)
)
)
在阅读代码时,我们可能难以准确理解网络结构的详细模型。因此,为了深入了解网络模型内部的实现细节,我们需要对其进行更加深入的探索。虽然这个工程的代码量较大,集成度也很高,但是由 Newfc 和 fc 的网络结构相同,我们可以以 FC 网络结构为基础,对网络细节进行深刻的剖析。在接下来的内容中,我们将会全方位地分析该网络的实现细节,并为读者提供更加清晰的认识和理解。这将是一次难得的学习机会,特别适合那些希望深入了解图像字幕生成领域的进阶学习者。
在 ./model/utils
目录下,作者实现了各种不同模型的调用函数,其中以 Newfc 为例进行详细说明。
if opt.caption_model == 'fc':
model = FCModel(opt) #方法fc
2. FCModel函数详细解析
这个类包含了以下子函数:
-
__init__(self, opt)
: 构造函数,初始化模型的各种参数和属性。 -
init_weights(self)
: 初始化模型的权重矩阵。 -
init_hidden(self, bsz)
: 初始化隐藏状态。 -
_forward(self, fc_feats, att_feats, seq, att_masks=None)
: 模型的前向传播过程。 -
get_logprobs_state(self, it, state)
: 根据当前输入和隐藏状态计算输出和对应的概率分布。 -
_sample_beam(self, fc_feats, att_feats, att_masks=None, opt={})
: 利用beam search算法进行采样得到预测的序列。 -
_sample(self, fc_feats, att_feats, att_masks=None, opt={})
: 利用贪心算法或者beam search算法进行采样得到预测的序列。
我们将对每个子函数进行详细的解释。
2.1 __init__(self, opt)函数详细解析
这个子函数定义了一个名为FCModel的类,继承自CaptionModel类,并重写了父类的构造函数__init__()。在构造函数中,设置了对象的一些属性值,如词汇表大小、输入编码大小、RNN类型、RNN的隐藏状态大小等,同时创建了几个神经网络层,包括nn.Linear
对象img_embed
、LSTMCore
对象core、nn.Embedding
对象embed
和nn.Linear
对象logit
,并将其设置为对象的属性值。最后调用了对象的init_weights()
方法来初始化模型权重矩阵。
# 定义一个名为FCModel的类,继承自CaptionModel类
class FCModel(CaptionModel):
# 定义构造函数__init__(),接受参数opt
def __init__(self, opt):
# 调用CaptionModel父类的构造函数
super(FCModel, self).__init__()
# 设置对象的属性值
self.vocab_size = opt.vocab_size # 词汇表大小
self.input_encoding_size = opt.input_encoding_size # 输入编码大小
self.rnn_type = opt.rnn_type # RNN类型
self.rnn_size = opt.rnn_size # RNN的隐藏状态大小
self.num_layers = opt.num_layers # RNN的层数
self.drop_prob_lm = opt.drop_prob_lm # dropout概率
self.seq_length = opt.seq_length # 序列长度
self.fc_feat_size = opt.fc_feat_size # 特征映射的大小
self.ss_prob = 0.0 # Schedule sampling probability # schedule sampling概率
# 创建一个nn.Linear对象img_embed,并将其设置为对象的属性值
# img_embed对象的输入大小为self.fc_feat_size,输出大小为self.input_encoding_size 图像
self.img_embed = nn.Linear(self.fc_feat_size, self.input_encoding_size)
# 创建一个LSTMCore对象core,并将其设置为对象的属性值,构造函数接受参数opt
self.core = LSTMCore(opt)
# 创建一个nn.Embedding对象embed,并将其设置为对象的属性值
# embed对象的输入大小为self.vocab_size + 1,输出大小为self.input_encoding_size
self.embed = nn.Embedding(self.vocab_size + 1, self.input_encoding_size)
# 创建一个nn.Linear对象logit,并将其设置为对象的属性值
# logit对象的输入大小为self.rnn_size,输出大小为self.vocab_size + 1
self.logit = nn.Linear(self.rnn_size, self.vocab_size + 1)
# 调用对象的init_weights()方法
self.init_weights()
2.2 init_weights(self)函数详细解析
这段代码定义了一个函数init_weights()
,用于初始化模型的权重矩阵。在函数中,将权重的范围设置为0.1,并对对象的embed
属性的权重矩阵进行随机均匀初始化,范围为[-initrange, initrange]
。然后将对象的logit
属性的偏置项设置为0,再对其权重矩阵进行随机均匀初始化,范围为[-initrange, initrange]
。这样,模型的权重矩阵就被初始化完成了。
# 定义函数init_weights(),不接受任何参数
def init_weights(self):
# 初始化权重的范围为0.1
initrange = 0.1
# 对对象的embed属性的权重矩阵进行随机均匀初始化,范围为[-initrange, initrange]
self.embed.weight.data.uniform_(-initrange, initrange)
# 将对象的logit属性的偏置项设置为0
self.logit.bias.data.fill_(0)
# 对对象的logit属性的权重矩阵进行随机均匀初始化,范围为[-initrange, initrange]
self.logit.weight.data.uniform_(-initrange, initrange)
2.3 init_hidden(self, bsz)函数详细解析
这段代码定义了一个名为init_hidden()
的函数,用于初始化隐藏状态。在函数中,根据参数bsz和对象的一些属性值,如RNN类型、RNN的隐藏状态大小等,创建一个零张量作为存储隐藏状态的变量,并返回该变量作为隐藏状态的初始值。如果RNN类型为LSTM,则还需要创建一个零张量作为细胞状态的初始值,并将两个值打包成一个元组返回。最后,返回的张量或元组的形状均为(num_layers, bsz, rnn_size)
。
# 定义函数init_hidden(),接受参数bsz,假设传输参数为 batch_size * seq_per_img = 50
def init_hidden(self, bsz):
# 获取对象的logit属性的权重矩阵
weight = self.logit.weight
# 如果RNN类型为LSTM,则返回一个元组,
# 其中第一个元素是形状为(num_layers, bsz, rnn_size)的零张量,表示隐藏状态
# 第二个元素是形状为(num_layers, bsz, rnn_size)的零张量,表示细胞状态
if self.rnn_type == 'lstm':
return (weight.new_zeros(self.num_layers, bsz, self.rnn_size),
weight.new_zeros(self.num_layers, bsz, self.rnn_size))
# 如果RNN类型不是LSTM,则返回形状为(num_layers, bsz, rnn_size)的零张量,表示隐藏状态
else:
return weight.new_zeros(self.num_layers, bsz, self.rnn_size)
2.4 _forward(self, fc_feats, att_feats, seq, att_masks=None)函数详细解析
这段代码定义了一个名为_forward
的函数,该函数接受四个参数:fc_feats
、att_feats
、seq
和att_masks
。函数包含了一个迭代过程,每次迭代计算模型的输出,最终返回形状为(batch_size, seq_length, 加1维)
的张量。具体实现中,函数首先计算了每张图片所对应的序列长度,然后初始化了隐藏状态和空列表outputs
用于存储模型的输出。在迭代过程中,通过判断当前是否是第一次迭代或者需要进行schedule sampling操作来获取输入xt
。之后,将xt
和state
作为输入,通过核心模型计算隐藏状态和输出,并将输出的logits通过log_softmax函数计算得到概率分布。最后,将当前输出添加到outputs
列表中,并重复以上操作直至迭代完成,并返回形状为(batch_size, seq_length, 加1维)
的张量。
# 定义函数_forward(),接受4个参数:fc_feats、att_feats、seq和att_masks
def _forward(self, fc_feats, att_feats, seq, att_masks=None):
# 获取批次大小batch_size
batch_size = fc_feats.size(0)
# 计算每张图片所对应的序列长度,比如有10个图片,每个图片有5个label,所以seq.shape[0]==50,则seq_per_img=50/10=5
seq_per_img = seq.shape[0] // batch_size
# 初始化隐藏状态
state = self.init_hidden(batch_size * seq_per_img) #初始化语言序列,一共 batch_size * seq_per_img = 50 # 定义一个空列表outputs用于存储模型输出
outputs = []
# 如果一个图像对应多个序列,则需要将fc_feats重复seq_per_img次
# 这句话的作用是,对于一个图片输出的fc_feats
if seq_per_img > 1:
fc_feats = utils.repeat_tensors(seq_per_img, fc_feats)
# 迭代seq.size(1)+1次(包括最终输出)
for i in range(seq.size(1) + 1):
# 如果i等于0,表示当前是第一次迭代,此时xt为通过img_embed线性层处理后的fc_feats
if i == 0:
xt = self.img_embed(fc_feats)
else:
# 如果self.training=True且i>=2且self.ss_prob>0.0,则需要进行schedule sampling操作
if self.training and i >= 2 and self.ss_prob > 0.0:
sample_prob = fc_feats.data.new(batch_size * seq_per_img).uniform_(0, 1) # 生成概率向量 [50,] sample_mask = sample_prob < self.ss_prob # 对概率向量进行阈值化操作,生成掩码 #[50,] if sample_mask.sum() == 0: # 如果掩码中的元素都为0,表示不需要采样,比如self.ss_prob=0.00
it = seq[:, i - 1].clone()
else:
sample_ind = sample_mask.nonzero().view(-1) # 获取掩码中值为1的索引
it = seq[:, i - 1].data.clone()
prob_prev = torch.exp(outputs[-1].data) # 获取上一次输出的概率分布
it.index_copy_(0, sample_ind, torch.multinomial(prob_prev, 1).view(-1).index_select(0,sample_ind)) # 根据概率分布进行采样
else:
it = seq[:, i - 1].clone() # 否则直接将seq[:,i-1]赋值给it
# 如果seq[:,i-1]全为0,则退出循环,说明已经都没有单词了。
if i >= 2 and seq[:, i - 1].sum() == 0:
break
xt = self.embed(it) # 根据it获取对应的词向量
output, state = self.core(xt, state) # 将xt和state作为输入,通过核心模型计算隐藏状态和输出
output = F.log_softmax(self.logit(output), dim=1) # 将输出的logits通过log_softmax函数计算得到概率分布
outputs.append(output) # 将当前输出添加到outputs列表中,这里的outputs是一个列表形式
# 将outputs[1:]中的每个元素都加上一个维度,并在该维度上连接起来,最终返回形状为(batch_size, seq_length, 加1维)的张量
return torch.cat([_.unsqueeze(1) for _ in outputs[1:]], 1).contiguous()
3. LSTM_CORE函数详细解析
在上文中,self.core = LSTMCore(opt)
这一行代码创建了一个LSTMCore对象并将其赋值给self.core
。在函数的迭代过程中,每次调用self.core
方法,并将xt
和state
作为参数传递给它来计算隐藏状态和输出。
这段代码定义了一个名为LSTMCore
的类,继承自nn.Module
。该类是用于图像描述生成的LSTM核心模块,其中包含一个前向传递函数forward
和一个构造函数__init__
。
在构造函数中,首先定义了几个实例变量,包括输入嵌入向量的大小self.input_encoding_size
、LSTM的隐藏层大小self.rnn_size
和Dropout
概率self.drop_prob_lm
。然后通过nn.Linear
函数分别创建了两个线性层用于将输入嵌入向量和前一时间步的隐藏状态转换为LSTM输入的维度,并定义了一个nn.Dropout
层用于应用Dropout操作。
在前向传递函数forward
中,首先计算了输入的总和,包括当前输入和上一个时间步的隐藏状态。然后将总和分为三个块,每个块大小为self.rnn_size
,并通过sigmoid
函数将这三个块压缩到[0,1]的范围内,得到输入门、遗忘门和输出门的值。接着,计算当前输入和上一个时间步的隐藏状态之间的线性变换,并计算单元状态c_t
的更新值以及下一时间步的隐藏状态h_t
。最后,应用Dropout
并返回最终的隐藏状态和单元状态。
总之,这段代码实现了一个LSTM模型的核心部分,用于处理输入序列并输出对应的语言描述。
#这是一个用于图像描述生成的 LSTM 核心模块。以下是代码逐行解释:
class LSTMCore(nn.Module): #定义 LSTMCore 类,继承自 nn.Module
def __init__(self, opt): #定义类的构造函数,接收一个参数 opt super(LSTMCore, self).__init__() #调用父类的构造函数。
self.input_encoding_size = opt.input_encoding_size # 输入嵌入向量的大小
self.rnn_size = opt.rnn_size #LSTM 的隐藏层大小
self.drop_prob_lm = opt.drop_prob_lm #Dropout 概率
# Build a LSTM
self.i2h = nn.Linear(self.input_encoding_size, 5 * self.rnn_size) #定义一个线性层,将输入嵌入向量转换为 LSTM 输入的维度 x_t-->x_t
self.h2h = nn.Linear(self.rnn_size, 5 * self.rnn_size) #定义一个线性层,将前一时间步的隐藏状态转换为 LSTM 输入的维度 h_{t-1}
self.dropout = nn.Dropout(self.drop_prob_lm) #定义 Dropout 层
def forward(self, xt, state): #定义 forward 函数,用于执行 LSTM 的前向传递
all_input_sums = self.i2h(xt) + self.h2h(state[0][-1]) #计算输入的总和,其中包括当前输入和上一个时间步的隐藏状态 x_t + h_{t-1}
sigmoid_chunk = all_input_sums.narrow(1, 0, 3 * self.rnn_size) #将输入分为三个块,每个块大小为 self.rnn_size
sigmoid_chunk = torch.sigmoid(sigmoid_chunk) #通过 sigmoid 函数将这三个块压缩到 [0,1] 的范围内,经过sigmoid
in_gate = sigmoid_chunk.narrow(1, 0, self.rnn_size) #将第一个块作为“输入门”,并保存在 in_gate 张量中 张量沿着第二个维度(即列)进行切片,从索引 0 开始,每个块的大小为 self.rnn_size。
forget_gate = sigmoid_chunk.narrow(1, self.rnn_size, self.rnn_size) #将第二个块用作“遗忘门”,并保存在 forget_gate 张量中 张量沿着第二个维度(即列)进行切片,从索引 self.rnn_size 开始,每个块的大小为 self.rnn_size。
out_gate = sigmoid_chunk.narrow(1, self.rnn_size * 2, self.rnn_size) #将第三个块用作“输出门”,并保存在 out_gate 张量中 张量沿着第二个维度(即列)进行切片,从索引 2*self.rnn_size 开始,每个块的大小为 self.rnn_size。
in_transform = torch.max( #计算当前输入和上一个时间步的隐藏状态之间的线性变换,并将其保存在 in_transform 张量中
all_input_sums.narrow(1, 3 * self.rnn_size, self.rnn_size),
all_input_sums.narrow(1, 4 * self.rnn_size, self.rnn_size))
next_c = forget_gate * state[1][-1] + in_gate * in_transform #计算单元状态 c_t 的更新值
next_h = out_gate * torch.tanh(next_c) #计算下一时间步的隐藏状态 h_t
output = self.dropout(next_h) #应用 Dropout 并将其保存在 output 张量中
state = (next_h.unsqueeze(0), next_c.unsqueeze(0)) #更新状态张量 [h_t,c_t]
return output, state #返回最终的隐藏状态和单元状态
假设输入的x_t
,state
,接下来我们分析LSTM_CORE代码,LSTM的运行图入下所示: