趋近智
实现一个变分自编码器(VAE)来对序列数据进行建模和生成。构建一个循环变分自编码器(RVAE)进行字符级文本生成的步骤将被概述。变分自编码器可以适配序列和结构化数据。这个练习将强化对如何在VAE架构中使用循环神经网络 (neural network)(RNN)来捕捉时间依赖性的理解。
我们的目标是训练一个RVAE,使其能够学习文本序列的压缩表示,然后使用这个表示来生成新的、可信的文本。
我们将进行字符级文本生成。这意味着我们的模型将学习根据前面的字符预测序列中的下一个字符。虽然词级模型也很普遍,但字符级模型在词汇管理方面更容易配置,并且可以生成新颖的词或风格。
语料库选择: 选择一个文本语料库。为了学习,一个大小适中、内容连贯的文本效果不错。例子包括:
corpus.txt的纯文本文件。词汇表 (vocabulary)创建: 首先,我们需要确定我们的词汇表,在这种情况下,它就是语料库中独有字符的集合。
# 示例性的Python伪代码
text = open('corpus.txt', 'r').read()
chars = sorted(list(set(text)))
char_to_int = {ch: i for i, ch in enumerate(chars)}
int_to_char = {i: ch for i, ch in enumerate(chars)}
vocab_size = len(chars)
创建输入-输出序列:
我们需要将原始文本转换为RVAE可以处理的序列。我们将使用滑动窗口方法。对于给定的sequence_length,我们将创建输入序列和相应的目标序列(通常是输入序列向后平移一个字符的结果)。
# 示例性的Python伪代码
sequence_length = 50 # 示例长度
data_X = [] # 输入序列
data_y = [] # 目标序列(用于解码器重构)
for i in range(0, len(text) - sequence_length, 1):
seq_in = text[i:i + sequence_length]
seq_out = text[i + 1:i + sequence_length + 1] # 用于重构的目标
data_X.append([char_to_int[char] for char in seq_in])
# 对于RVAE,解码器将尝试重构seq_in,
# 或者如果以seq_in和z为条件,则生成seq_out
# 为了简单起见,这里假设解码器旨在重构seq_in
data_y.append([char_to_int[char] for char in seq_in]) # 如果是那样设计的话,也可以是seq_out
num_sequences = len(data_X)
注意:对于旨在从潜在变量 生成数据的“经典”RVAE,解码器通常会重构输入序列 seq_in。RNN结构本身处理序列预测。
数据格式化:
输入数据需要为RNN适当塑形,通常是(num_sequences, sequence_length, feature_dim)。对于字符级模型,feature_dim通常是1(如果直接将整数输入送入嵌入 (embedding)层)或vocab_size(如果使用独热编码输入)。如果整数输入没有首先馈送到嵌入层,我们还会对其进行归一化 (normalization)。
# 示例性的Python伪代码
# 假设使用嵌入层,因此输入是(num_sequences, sequence_length)
# X = np.reshape(data_X, (num_sequences, sequence_length))
# y = np.reshape(data_y, (num_sequences, sequence_length))
# PyTorch/TensorFlow将处理批处理
我们的RVAE将由一个基于RNN的编码器、一个潜在空间采样机制和一个基于RNN的解码器组成。
编码器的作用是将输入序列 映射到近似后验分布 的参数 (parameter),我们假定这个分布是高斯分布 。
# 编码器的PyTorch伪代码
# self.embedding = nn.Embedding(vocab_size, embedding_dim)
# self.encoder_rnn = nn.LSTM(embedding_dim, hidden_dim, batch_first=True)
# self.fc_mu = nn.Linear(hidden_dim, latent_dim)
# self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
# def encode(self, x_sequence):
# embedded = self.embedding(x_sequence) # (批次, 序列长度, 嵌入维度)
# _, (h_n, _) = self.encoder_rnn(embedded) # h_n 是 (1, 批次, 隐藏维度)
# h_n_last_layer = h_n.squeeze(0) # (批次, 隐藏维度)
# mu = self.fc_mu(h_n_last_layer)
# logvar = self.fc_logvar(h_n_last_layer)
# return mu, logvar
LSTM中的h_n包含批次中每个序列的最终隐藏状态。这是标准的VAE过程: 且 . 这是重参数化技巧。
# 重参数化的PyTorch伪代码
# def reparameterize(self, mu, logvar):
# std = torch.exp(0.5 * logvar)
# eps = torch.randn_like(std)
# return mu + eps * std
解码器从潜在空间接收样本 ,旨在重构原始输入序列(或者,如果 是从先验分布 中采样的,则生成新序列)。
# 解码器的PyTorch伪代码
# self.decoder_embedding = nn.Embedding(vocab_size, embedding_dim)
# self.decoder_rnn_cell = nn.LSTMCell(embedding_dim + latent_dim, hidden_dim) # 示例:连接z
# # 或者:self.latent_to_hidden = nn.Linear(latent_dim, hidden_dim) 用于h0, c0初始化
# self.fc_out = nn.Linear(hidden_dim, vocab_size)
# def decode(self, z, target_sequence, teacher_forcing_ratio=0.5):
# batch_size = z.size(0)
# seq_len = target_sequence.size(1)
#
# # 初始化隐藏状态(例如,来自z或零)
# # hx = self.latent_to_hidden(z) # (批次, 隐藏维度)
# # cx = self.latent_to_hidden(z) # (批次, 隐藏维度)
# # 或者如果连接z:
# hx = torch.zeros(batch_size, hidden_dim).to(z.device)
# cx = torch.zeros(batch_size, hidden_dim).to(z.device)
#
# # 起始标记(例如,<SOS>字符的嵌入,或目标序列的第一个字符)
# current_input_char_idx = target_sequence[:, 0] # 示例:使用目标的第一个字符
# outputs = []
#
# for t in range(seq_len):
# embedded_char = self.decoder_embedding(current_input_char_idx) # (批次, 嵌入维度)
#
# # 选项1:将z与每个输入连接
# rnn_input = torch.cat((embedded_char, z), dim=1) # (批次, 嵌入维度 + 潜在维度)
# hx, cx = self.decoder_rnn_cell(rnn_input, (hx, cx))
#
# # 选项2:使用z初始化hx, cx(在循环前完成)
# # hx, cx = self.decoder_rnn_cell(embedded_char, (hx, cx))
#
# output_logits_t = self.fc_out(hx) # (批次, 词汇表大小)
# outputs.append(output_logits_t)
#
# use_teacher_force = random.random() < teacher_forcing_ratio
# if use_teacher_force and t < seq_len -1:
# current_input_char_idx = target_sequence[:, t+1]
# else:
# _, top_idx = output_logits_t.topk(1)
# current_input_char_idx = top_idx.squeeze(1).detach() # 使用模型自己的预测
#
# return torch.stack(outputs, dim=1) # (批次, 序列长度, 词汇表大小)
RVAE损失函数是标准的VAE ELBO,但重构项现在是序列元素的总和。
重构损失():对于字符级生成,这通常是序列中每个位置的预测字符分布与实际目标字符之间的交叉熵损失的总和。
在实际中,你会使用你所用框架的CrossEntropyLoss函数,并将其应用于序列维度。请确保logits和目标的形状正确(例如,logits:(batch_size * seq_len, vocab_size),targets:(batch_size * seq_len))。
KL散度():近似后验 与先验 (通常是 )之间的KL散度。
D_{KL}(q(z|x) || p(z)) = -0.5 \sum_{j=1}^{\text{latent_dim}} (1 + \log(\sigma_{z_j}^2) - \mu_{z_j}^2 - \sigma_{z_j}^2)项来自-VAE,可用于控制对解耦或重构质量的侧重。对于标准VAE,。
训练循环包括:
# 示例性训练步骤伪代码
# rvae_model = RVAE(...)
# optimizer = Adam(rvae_model.parameters(), lr=1e-3)
#
# for epoch in range(num_epochs):
# for batch_sequences_x, batch_sequences_y in data_loader:
# optimizer.zero_grad()
#
# mu, logvar = rvae_model.encode(batch_sequences_x)
# z = rvae_model.reparameterize(mu, logvar)
# decoded_logits = rvae_model.decode(z, batch_sequences_y) # 用于教师强迫的batch_sequences_y
#
# # 重构损失
# # 为CrossEntropyLoss重塑形状: (批次 * 序列长度, 词汇表大小) 和 (批次 * 序列长度)
# recon_loss = criterion_recon(
# decoded_logits.view(-1, vocab_size),
# batch_sequences_y.view(-1)
# )
#
# # KL散度
# kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
# kl_div = kl_div / batch_sequences_x.size(0) # 按批次平均
#
# loss = recon_loss + beta * kl_div
# loss.backward()
# optimizer.step()
#
# # 记录损失,定期生成样本
模型训练完成后,你可以生成新的文本序列:
torch.multinomial或取argmax)。# 示例性生成伪代码
# def generate_sequence(rvae_model, z_sample, start_token_idx, max_len=100):
# rvae_model.eval()
# generated_sequence_indices = [start_token_idx]
# current_input_char_idx = torch.tensor([[start_token_idx]], device=device) # 批次大小为1
#
# # 初始化解码器隐藏状态(来自z_sample或如果z被连接则为零)
# # hx, cx = ... initialized based on z_sample
#
# with torch.no_grad():
# for _ in range(max_len - 1):
# embedded_char = rvae_model.decoder_embedding(current_input_char_idx)
# # rnn_input = torch.cat((embedded_char.squeeze(1), z_sample), dim=1) if concatenating z
# # hx, cx = rvae_model.decoder_rnn_cell(rnn_input, (hx, cx))
# # output_logits = rvae_model.fc_out(hx)
#
# # 简化:假设模型中有一个decode_step函数
# output_logits, hx, cx = rvae_model.decode_step(current_input_char_idx, z_sample, hx, cx)
#
# # 采样下一个字符(可以添加温度以增加多样性)
# # probabilities = F.softmax(output_logits / temperature, dim=-1)
# # next_char_idx = torch.multinomial(probabilities, 1)
# _, next_char_idx = output_logits.topk(1, dim=-1)
#
# generated_sequence_indices.append(next_char_idx.item())
# current_input_char_idx = next_char_idx
#
# # 如果 next_char_idx.item() == eos_token_idx: break
#
# return "".join([int_to_char[idx] for idx in generated_sequence_indices])
#
# z_prior = torch.randn(1, latent_dim).to(device)
# generated_text = generate_sequence(rvae_model, z_prior, char_to_int['A'])
# print(generated_text)
本实践指导为序列数据实现VAE提供了一个蓝图。RVAE是一个基础模型,并且存在许多扩展和变体,例如那些结合注意力机制 (attention mechanism)(我们已在本章前面讨论过)以更有效地处理长距离依赖的模型。尝试这些组件,观察它们的影响,并在应对更复杂的序列建模任务时参考研究论文以获得更高级的技术。
这部分内容有帮助吗?
© 2026 ApX Machine LearningAI伦理与透明度•