Shap-E 代码深度分析以及微调策略 Shap-E 是 OpenAI 推出的一个能够根据文本描述...
تم الإنشاء في: ١٨ يناير ٢٠٢٥
تم الإنشاء في: ١٨ يناير ٢٠٢٥
Shap-E 代码深度分析以及微调策略
Shap-E 是 OpenAI 推出的一个能够根据文本描述生成 3D 模型的项目。其核心在于两个模型:编码器 (encoder) 和 潜扩散模型 (latent diffusion model)。
编码器 将 3D 模型转换为一个小型神经网络的参数,这个神经网络可以表示 3D 形状和纹理的隐式函数。
潜扩散模型 则根据文本或图像生成新的隐式函数,这些函数可以被渲染成图像或导出为网格。
github的Shap-E 项目中
文件主要分为以下几个部分:
README.md: 项目简介、样本展示和使用说明。
model-card.md: 模型细节、训练数据、评估和局限性等信息。
samples.md: 更多文本到 3D 模型的样本展示。
setup.py: 项目的安装脚本。
shap_e 文件夹: 核心代码,包含以下子文件夹:
diffusion: 扩散模型的实现,包括 gaussian_diffusion.py 和 k_diffusion.py。
models: 包含各种模型和组件的定义,例如:
configs.py: 模型配置加载和创建。
download.py: 模型下载工具。
generation: 包含潜扩散模型的各种变体,如 latent_diffusion.py、perceiver.py、transformer.py 等。
nerf: NeRF 模型的实现,包括 model.py 和 renderer.py。
nerstf: 结合 NeRF 和 STF 的模型和渲染器。
nn: 神经网络相关的工具模块。
query.py: 定义了查询点和方向的 Query 类。
renderer.py: 渲染器的抽象基类和实现。
stf: STF (Signed Time Field) 模型的实现。
transmitter: 编码器和解码器的实现,包括 base.py、bottleneck.py、channels_encoder.py、multiview_encoder.py、params_proj.py 和 pc_encoder.py。
volume.py: 定义了不同的体积表示,如 BoundingBoxVolume。
微调策略
要微调 Shap-E 以实现自然语言输出 Minecraft 体素网络,并连接到现有的 3D 知识库,可以考虑以下策略:
收集 Minecraft 体素数据: 你需要大量的 Minecraft 体素数据,每个数据点应该包含:
体素网格 (voxel grid): 使用你提供的代码可以将体素数据转换为张量。
对应的自然语言描述 (text description): 尽可能详细地描述体素网格的特征,例如"一个红色的羊毛块"、"一个由石头组成的房子"、"一个带有熔岩的城堡"等。
数据增强: 可以对体素数据进行旋转、缩放、平移等操作来增加数据量。
构建词汇表: 构建一个 Minecraft 特定的词汇表,将常见的方块名称、结构名称等映射到唯一的 ID。
编码器 (Encoder):
替换点云编码器: Shap-E 默认使用点云编码器。你需要将其替换为体素网格编码器。可以使用 3D 卷积网络或者 Transformer 等来编码体素网格。
预训练的体素编码器: 可以尝试使用预训练的体素编码器,例如在 ShapeNet 等数据集上训练的编码器,并进行微调。
潜扩散模型 (Latent Diffusion Model):
修改文本条件分支: CLIPImagePointDiffusionTransformer 使用 CLIP 模型来编码文本条件。你需要修改其输入层以适应你的词汇表大小。你可以使用 CLIPTokenizer 提供的嵌入层,并添加额外的层来处理 Minecraft 特定的词汇。
调整扩散模型参数: 可能需要调整扩散模型的参数,例如时间步长、噪声调度等,以适应新的数据分布。
解码器 (Decoder):
选择合适的渲染器: 由于你希望输出的是体素网格,因此不需要使用 NeRF 或 STF 渲染器。你可以直接从潜向量中解码出体素网格。
设计解码器结构: 解码器可以将潜向量作为输入,并输出一个概率分布,表示每个体素位置存在特定方块的概率。可以使用 3D 反卷积网络或者 Transformer 等来实现。
两阶段训练:
阶段一: 训练体素编码器和解码器。可以使用自编码器的方式进行训练,目标是重建输入的体素网格。
阶段二: 固定编码器和解码器,训练潜扩散模型。使用文本描述作为条件,生成体素网格的潜向量,并计算扩散模型的损失。
联合训练: 同时训练编码器、解码器和扩散模型。
知识蒸馏: 可以利用 Shap-E 预训练模型的知识,例如使用预训练的文本编码器,或者使用预训练模型的输出作为教师信号来指导训练。
潜在空间对齐: 可以尝试将 Minecraft 体素数据的潜在空间与 Shap-E 的 3D 模型的潜在空间对齐。例如,可以使用对抗训练或者对比学习的方法来实现。
混合模型: 可以训练一个混合模型,该模型可以同时处理 Minecraft 体素数据和 Shap-E 的 3D 模型数据。在推理阶段,可以根据文本描述选择使用哪个模型。
共享知识库: 可以尝试构建一个共享的知识库,该知识库包含 Minecraft 方块和 Shap-E 中其他 3D 对象的嵌入表示。这样,模型可以根据文本描述中的关键词,从知识库中检索相关的方块或对象,并将其融入到生成的 3D 模型中。
代码示例
以下是一些代码示例,说明如何修改 Shap-E 代码来实现上述策略:
import os import json import torch from transformers import CLIPTokenizer # 定义路径 training_data_path = 'training_data/10001-1.jsonl' block_ids_path = 'blockids.txt' output_folder = 'processed_output' # 确保输出文件夹存在 os.makedirs(output_folder, exist_ok=True) # 加载方块 ID 映射 block_ids = {} with open(block_ids_path, 'r', encoding='utf-8') as f: exec(f.read()) # 初始化 tokenizer tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") # 存储描述和体素张量 description_tensors = [] voxel_tensors = [] # 读取 JSONL 文件 with open(training_data_path, 'r', encoding='utf-8') as f: for line in f: data = json.loads(line) # 获取并分词描述 description = data.get('input', '') tokenized_description = tokenizer.encode(description, return_tensors='pt', truncation=True) # 获取体素数据 voxel_data = data.get('output', '') voxel_lines = voxel_data.strip().split('\n') # 将方块名称映射为数字 ID voxel_ids = [] for voxel_line in voxel_lines: parts = voxel_line.strip().split() if len(parts) == 4: block_name, x, y, z = parts block_id = None # 使用 block_ids 映射 for k, v in block_ids.items(): if v == block_name: block_id = k break if block_id is None: # 如果未找到方块名称,可以进行处理 print(f"方块名称 '{block_name}' 未在 block_ids 中找到。") continue voxel_ids.append([int(block_id), int(x), int(y), int(z)]) else: print(f"无效的体素行: {voxel_line}") # 转换为张量 voxel_tensor = torch.tensor(voxel_ids) # 添加到列表 description_tensors.append(tokenized_description) voxel_tensors.append(voxel_tensor) # 保存张量 description_tensor = torch.cat(description_tensors, dim=0) torch.save(description_tensor, os.path.join(output_folder, 'descriptions.pt')) torch.save(voxel_tensors, os.path.join(output_folder, 'voxels.pt')) # 解析体素张量回文本,输出纯数字 for i, voxel_tensor in enumerate(voxel_tensors): print(f"第 {i} 个样本的体素数据:") print(voxel_tensor.numpy())
content_copy download
Use code with caution.Python
import torch.nn as nn class VoxelEncoder(nn.Module): def init(self, d_latent): super().init() self.conv1 = nn.Conv3d(1, 32, kernel_size=4, stride=2, padding=1) self.conv2 = nn.Conv3d(32, 64, kernel_size=4, stride=2, padding=1) self.conv3 = nn.Conv3d(64, 128, kernel_size=4, stride=2, padding=1) self.fc = nn.Linear(128 * 4 * 4 * 4, d_latent) def forward(self, x): # Assuming input x is of shape [batch_size, 1, 32, 32, 32] x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = x.view(x.shape[0], -1) x = self.fc(x) return x
content_copy download
Use code with caution.Python
import torch.nn as nn class VoxelDecoder(nn.Module): def init(self, d_latent, output_size=32, n_channels=1): super().init() self.fc = nn.Linear(d_latent, 128 * 4 * 4 * 4) self.deconv1 = nn.ConvTranspose3d(128, 64, kernel_size=4, stride=2, padding=1) self.deconv2 = nn.ConvTranspose3d(64, 32, kernel_size=4, stride=2, padding=1) self.deconv3 = nn.ConvTranspose3d(32, n_channels, kernel_size=4, stride=2, padding=1) self.output_size = output_size def forward(self, x): x = self.fc(x) x = x.view(x.shape[0], 128, 4, 4, 4) x = F.relu(self.deconv1(x)) x = F.relu(self.deconv2(x)) x = torch.sigmoid(self.deconv3(x)) # Output probabilities return x
content_copy download
Use code with caution.Python
from transformers import CLIPTokenizer, CLIPTextModel class MinecraftCLIPTextEncoder(nn.Module): def init(self, clip_model_name="openai/clip-vit-large-patch14", minecraft_vocab_size=1000): super().init() self.clip = CLIPTextModel.from_pretrained(clip_model_name) self.tokenizer = CLIPTokenizer.from_pretrained(clip_model_name) # Assuming a linear projection to adapt CLIP embeddings to Minecraft vocabulary self.proj = nn.Linear(self.clip.config.hidden_size, minecraft_vocab_size) def forward(self, text): inputs = self.tokenizer(text, return_tensors="pt", padding=True, truncation=True) outputs = self.clip(**inputs) projected = self.proj(outputs.pooler_output) return projected
content_copy download
Use code with caution.Python
content_copy download
Use code with caution.Python
总结
微调 Shap-E 以生成 Minecraft 体素网络需要对数据、模型和训练策略进行一系列修改。上述代码示例仅提供了一些思路,实际实现过程可能需要根据具体情况进行调整。
需要注意的关键点:
数据质量和多样性至关重要。 确保你的训练数据包含足够多样的 Minecraft 体素结构和相应的详细描述。
模型选择和修改需要根据数据的特点进行。 例如,如果体素网格的分辨率很高,可能需要使用更深的网络或者更复杂的编码器。
训练过程可能需要仔细调参。 学习率、batch size、扩散步数等参数都需要根据实际情况进行调整。
基于以下的jsonl转torch张量,你能写出一个完美的微调训练代码吗?import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from transformers import CLIPTokenizer, CLIPTextModel
from shap_e.diffusion.gaussian_diffusion import diffusion_from_config
from shap_e.models.download import load_config
from shap_e.models.generation.perceiver import MultiheadCrossAttention, ResidualCrossAttentionBlock, SimplePerceiver
from shap_e.models.generation.transformer import Transformer, MLP, init_linear
import numpy as np
import os
output_folder = 'processed_output'
class MinecraftDataset(Dataset):
def init(self, descriptions_path, voxels_path):
self.descriptions = torch.load(descriptions_path)
self.voxels = torch.load(voxels_path)
textdef __len__(self): return len(self.descriptions) def __getitem__(self, idx): return { "text": self.descriptions[idx], "voxels": self.voxels[idx], }
dataset = MinecraftDataset(
os.path.join(output_folder, "descriptions.pt"), os.path.join(output_folder, "voxels.pt")
)
dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
class MinecraftCLIPTextEncoder(nn.Module):
def init(self, clip_model_name="openai/clip-vit-large-patch14", minecraft_vocab_size=1000): # 假设 minecraft_vocab_size=1000,你需要根据实际情况设置
super().init()
self.clip = CLIPTextModel.from_pretrained(clip_model_name)
self.tokenizer = CLIPTokenizer.from_pretrained(clip_model_name)
self.proj = nn.Linear(self.clip.config.hidden_size, minecraft_vocab_size)
textdef forward(self, text): inputs = self.tokenizer(text, return_tensors="pt", padding=True, truncation=True) inputs = {k: v.to(self.clip.device) for k, v in inputs.items()} # Move inputs to device outputs = self.clip(**inputs) projected = self.proj(outputs.pooler_output) return projected
nll = torch.nn.CrossEntropyLoss()
text_encoder = MinecraftCLIPTextEncoder()
text_encoder = text_encoder.to(device)
class VoxelEncoder(nn.Module):
def init(self, d_latent):
super().init()
self.conv1 = nn.Conv3d(1, 32, kernel_size=4, stride=2, padding=1)
self.conv2 = nn.Conv3d(32, 64, kernel_size=4, stride=2, padding=1)
self.conv3 = nn.Conv3d(64, 128, kernel_size=4, stride=2, padding=1)
self.fc = nn.Linear(128 * 4 * 4 * 4, d_latent)
textdef forward(self, x): # Assuming input x is of shape [batch_size, 1, 64, 64, 64] x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = x.view(x.shape[0], -1) x = self.fc(x) return x
class VoxelDecoder(nn.Module):
def init(self, d_latent, output_size=64, n_channels=600): # 假设有600个不同的方块ID
super().init()
self.fc = nn.Linear(d_latent, 128 * 4 * 4 * 4)
self.deconv1 = nn.ConvTranspose3d(128, 64, kernel_size=4, stride=2, padding=1)
self.deconv2 = nn.ConvTranspose3d(64, 32, kernel_size=4, stride=2, padding=1)
self.deconv3 = nn.ConvTranspose3d(32, n_channels, kernel_size=4, stride=2, padding=1)
self.output_size = output_size
textdef forward(self, x): x = self.fc(x) x = x.view(x.shape[0], 128, 4, 4, 4) x = F.relu(self.deconv1(x)) x = F.relu(self.deconv2(x)) x = self.deconv3(x) # 输出每个方块类型的logits return x
voxel_encoder = VoxelEncoder(d_latent=512).to(device) # 这里使用了之前定义的 VoxelEncoder
config = load_config("text300M", device=device)
config["inner"]["input_channels"] = 512
config["inner"]["n_ctx"] = 1
config["inner"]["output_channels"] = 512 * 2
diffusion_model = diffusion_from_config(config)
diffusion_model = diffusion_model.to(device)
voxel_decoder = VoxelDecoder(d_latent=512).to(device) # 这里使用了之前定义的 VoxelDecoder
params = list(diffusion_model.parameters())
params += list(voxel_encoder.parameters())
params += list(voxel_decoder.parameters())
optimizer = optim.AdamW(params, lr=1e-4)
num_epochs = 10
for epoch in range(num_epochs):
for batch in dataloader:
optimizer.zero_grad()
texttext = batch["text"].to(device) voxels = batch["voxels"].to(device) batch_size = voxels.shape[0] # 将体素网格调整为 [batch_size, channels, depth, height, width] 的形状 voxels_input = voxels.unsqueeze(1).float() # 添加通道维度并转换为 float # 生成时间步 t t = torch.randint(0, diffusion_model.num_timesteps, (batch_size,), device=device) # 使用 VoxelEncoder 编码体素网格 encoded_voxels = voxel_encoder(voxels_input) # 使用 MinecraftCLIPTextEncoder 编码文本描述 with torch.no_grad(): # 这里注释掉, 让text_encoder参与训练 text_embeddings = text_encoder(batch["text"]) text_embeddings = text_embeddings.view(batch_size, 1, -1) # 调整形状以匹配 encoded_voxels reshaped_embeddings = text_embeddings # 文本嵌入已经有了批次和通道的维度 # 训练扩散模型 terms = diffusion_model.training_losses(model=diffusion_model.wrapped, x_start=encoded_voxels, t=t, model_kwargs={"cond": reshaped_embeddings}) loss = terms["loss"].mean() loss.backward() optimizer.step() print(f"Epoch: {epoch}, Loss: {loss.item()}") # 假设这是你的采样函数 if epoch % 10 == 0: # 每 10 个 epoch 保存一次 # 保存模型 torch.save(diffusion_model.state_dict(), os.path.join(output_folder, f'diffusion_model_epoch_{epoch}.pt')) torch.save(voxel_encoder.state_dict(), os.path.join(output_folder, f'voxel_encoder_epoch_{epoch}.pt')) torch.save(voxel_decoder.state_dict(), os.path.join(output_folder, f'voxel_decoder_epoch_{epoch}.pt')) if not text_encoder.clip.training: # 如果 text_encoder 没有在训练 text_encoder.train() # 设置为训练模式 torch.save(text_encoder.state_dict(), os.path.join(output_folder, f'text_encoder_epoch_{epoch}.pt')) text_encoder.eval() # 恢复为评估模式 else: # 否则, text_encoder 一直在训练 torch.save(text_encoder.state_dict(), os.path.join(output_folder, f'text_encoder_epoch_{epoch}.pt')) # 使用示例文本进行采样 sample_text = ["a redstone torch"] with torch.no_grad(): sample_text_embeddings = text_encoder(sample_text).unsqueeze(1) sampled_latents = diffusion_model.sample_latents( batch_size=1, model=diffusion_model.wrapped, diffusion=diffusion_model, model_kwargs={"cond": sample_text_embeddings.to(device)}, # 假设 text_encoder 可以处理单个字符串 guidance_scale=1.0, # 你可能需要调整这个参数 clip_denoised=False, use_fp16=False, use_karras=True, karras_steps=64, sigma_min=diffusion_model.sigma_min, sigma_max=diffusion_model.sigma_max, s_churn=0, device=device, progress=True ) # 使用 VoxelDecoder 解码生成的潜变量 sampled_voxels_logits = voxel_decoder(sampled_latents) sampled_voxels = torch.argmax(sampled_voxels_logits, dim=1).squeeze(0).cpu().numpy() # 将生成的体素数据保存为文本文件 sample_voxel_text_lines = [] for x in range(VOXEL_SIZE): for y in range(VOXEL_SIZE): for z in range(VOXEL_SIZE): block_id = sampled_voxels[x, y, z] if block_id != EMPTY_BLOCK_ID: sample_voxel_text_lines.append(f"{block_id} {x} {y} {z}") sample_voxel_text = '\n'.join(sample_voxel_text_lines) sample_voxel_txt_path = os.path.join(output_folder, f'sample_epoch_{epoch}.txt') with open(sample_voxel_txt_path, 'w', encoding='utf-8') as txt_file: txt_file.write(sample_voxel_text) print(f"采样体素数据已保存到:'{sample_voxel_txt_path}'")
print("训练完成。")
下面给出一个相对完整、可运行的示例性微调脚本,演示从原始 JSONL 数据到生成最终体素结果的完整流程。需要注意的是,以下示例代码主要用于教学和参考,实际项目中仍需根据你自身的数据格式、超参数需求和硬件情况进行调整。
在使用此脚本前,请确保已经安装相关依赖:
bashpip install shap-e pip install transformers pip install torch torchvision
shap-e
:OpenAI 提供的 Shap-E 库transformers
:Hugging Face 的 Transformer 工具包torch
/ torchvision
:PyTorch假设你有一个包含 Minecraft 体素描述和坐标的 JSONL
文件(例如 training_data/10001-1.jsonl
),格式示例如下:
json{"input": "一个红色的羊毛方块", "output": "red_wool 10 10 10"} {"input": "一个石头搭建的房子", "output": "stone 0 0 0\nstone 1 0 0\n..."} ...
其中:
"input"
为自然语言描述"output"
是若干行描述体素坐标信息,每行格式形如:方块名称 x y z
你还需要一个 blockids.txt
文件,里面定义了方块名称与数字 ID 的对应关系。例如:
pythonblock_ids = { 0: "air", 1: "stone", 2: "dirt", 3: "red_wool", ... }
注意:上面只是一个示例,你需要根据实际的 Minecraft 方块种类自行定义映射。
然后我们将把这些 JSONL 数据转换成两个 PyTorch 文件:
descriptions.pt
:保存的是原始文本字符串列表(或者批量)voxels.pt
:保存的是体素稀疏表示(或构造成密集 3D 张量后再保存)下面给出一个示例脚本,将 JSONL 转为可用的训练数据(稀疏体素列表)并保存下来。你可以单独保存成 prepare_data.py
或者直接放在你的主脚本里运行也行。
pythonimport os import json import torch from transformers import CLIPTokenizer # ========== 1. 定义路径 ========== training_data_path = 'training_data/10001-1.jsonl' # 你的 JSONL 文件 block_ids_path = 'blockids.txt' # 存放 block_ids 映射的 Python 脚本 output_folder = 'processed_output' # 输出文件夹 os.makedirs(output_folder, exist_ok=True) # ========== 2. 加载 block_ids ========== block_ids = {} with open(block_ids_path, 'r', encoding='utf-8') as f: exec(f.read()) # 这里会执行 block_ids = { ... } # ========== 3. 初始化 tokenizer ========== # 如果后续还要用 CLIPTokenizer 做分词,这里可以提前加载 tokenizer = CLIPTokenizer.from_pretrained("openai/clip-vit-large-patch14") # ========== 4. 准备容器 ========== description_list = [] # 用于保存原始文本 voxel_list = [] # 用于保存[(block_id, x, y, z), (block_id, x, y, z), ...] # ========== 5. 读取 JSONL 文件 ========== with open(training_data_path, 'r', encoding='utf-8') as f: for line in f: data = json.loads(line.strip()) description = data.get('input', '') # 读取自然语言描述 voxel_data = data.get('output', '') # 读取体素坐标 # 将描述直接存储为字符串(后面在模型训练阶段再进行分词) description_list.append(description) # 处理体素数据,每行格式: block_name x y z voxel_lines = voxel_data.strip().split('\n') voxel_ids = [] for voxel_line in voxel_lines: parts = voxel_line.strip().split() if len(parts) == 4: block_name, x, y, z = parts # 根据 block_ids 找到对应的数字 ID block_id = None for k, v in block_ids.items(): if v == block_name: block_id = k break if block_id is None: print(f"[警告] 未找到方块名称 '{block_name}' 的映射,跳过。") continue voxel_ids.append([int(block_id), int(x), int(y), int(z)]) else: print(f"[警告] 无效的体素行: {voxel_line}") # voxel_ids 是一个 (N, 4) 的列表 voxel_list.append(voxel_ids) # ========== 6. 保存数据 ========== torch.save(description_list, os.path.join(output_folder, 'descriptions.pt')) torch.save(voxel_list, os.path.join(output_folder, 'voxels.pt')) print("数据预处理完成,已生成 descriptions.pt 和 voxels.pt。")
在这里,我们将描述保存在一个 Python 列表(每个元素是一个字符串),体素信息保存在一个 Python 列表(每个元素又是 (N,4)
的坐标与 block_id 的稀疏形式)。
下面是简化的模型定义示例,包括:
MinecraftCLIPTextEncoder
VoxelEncoder
(将体素数据编码到潜变量)VoxelDecoder
(将潜变量解码回体素分布)shap_e.diffusion.gaussian_diffusion
中的 diffusion_from_config
在训练 VoxelEncoder
时,如果需要一个固定分辨率(如 64x64x64
)的 3D 网格,就需要把 (N,4)
的稀疏表示转换为一个 dense_tensor[64,64,64]
,其中每个体素存储其 block_id。
EMPTY_BLOCK_ID = 0
表示“空气”或“无方块”示例:
pythondef voxel_list_to_dense(voxel_ids, size=64, empty_id=0): """ 将体素的稀疏表示(一堆 [block_id, x, y, z]) 转为 [size, size, size] 的密集张量。 """ volume = torch.full((size, size, size), empty_id, dtype=torch.long) for (block_id, vx, vy, vz) in voxel_ids: if 0 <= vx < size and 0 <= vy < size and 0 <= vz < size: volume[vx, vy, vz] = block_id return volume
pythonimport torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.data import DataLoader, Dataset from transformers import CLIPTokenizer, CLIPTextModel from shap_e.diffusion.gaussian_diffusion import diffusion_from_config from shap_e.models.download import load_config from shap_e.models.generation.perceiver import SimplePerceiver from shap_e.models.generation.transformer import Transformer # ========== 超参数定义 ========== VOXEL_SIZE = 64 # 体素分辨率 EMPTY_BLOCK_ID = 0 # 空气或空方块 NUM_BLOCK_TYPES = 600 # 假设你有 600 种不同方块,需自行根据 block_ids 配置 # ========== 1. 文本编码器 (CLIP) ========== class MinecraftCLIPTextEncoder(nn.Module): """ 使用 CLIP 文本模型作为底座,再接一个线性层,将输出映射到自定义维度。 如果你只想要 CLIP 的 pooler_output,可将 proj 层去掉或仅用于扩散条件。 """ def __init__(self, clip_model_name="openai/clip-vit-large-patch14", out_dim=512): super().__init__() self.clip = CLIPTextModel.from_pretrained(clip_model_name) self.tokenizer = CLIPTokenizer.from_pretrained(clip_model_name) self.proj = nn.Linear(self.clip.config.hidden_size, out_dim) def forward(self, text_list): """ text_list: list of strings (原始句子) """ # 将文本转为 token inputs = self.tokenizer(text_list, return_tensors="pt", padding=True, truncation=True) inputs = {k: v.to(self.clip.device) for k, v in inputs.items()} outputs = self.clip(**inputs) # pooler_output 通常是 [batch_size, hidden_size] pooled = outputs.pooler_output # 或者 outputs.last_hidden_state 的某个池化 projected = self.proj(pooled) # [batch_size, out_dim] return projected # ========== 2. 体素编码器 ========== class VoxelEncoder(nn.Module): """ 将体素网格 (batch, 1, D, H, W) -> (batch, d_latent) 可以根据需要修改网络结构、通道数等。 """ def __init__(self, d_latent=512): super().__init__() self.conv1 = nn.Conv3d(1, 32, kernel_size=4, stride=2, padding=1) self.conv2 = nn.Conv3d(32, 64, kernel_size=4, stride=2, padding=1) self.conv3 = nn.Conv3d(64, 128, kernel_size=4, stride=2, padding=1) self.fc = nn.Linear(128 * 8 * 8 * 8, d_latent) def forward(self, x): # x: [batch_size, 1, 64, 64, 64] x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = x.view(x.shape[0], -1) x = self.fc(x) return x # ========== 3. 体素解码器 ========== class VoxelDecoder(nn.Module): """ 将潜变量 (batch, d_latent) -> (batch, NUM_BLOCK_TYPES, D, H, W) 输出 logits,用 argmax 或 softmax 获取每个体素的 block_id。 """ def __init__(self, d_latent=512, output_size=64, n_channels=NUM_BLOCK_TYPES): super().__init__() self.fc = nn.Linear(d_latent, 128 * 8 * 8 * 8) self.deconv1 = nn.ConvTranspose3d(128, 64, kernel_size=4, stride=2, padding=1) self.deconv2 = nn.ConvTranspose3d(64, 32, kernel_size=4, stride=2, padding=1) self.deconv3 = nn.ConvTranspose3d(32, n_channels, kernel_size=4, stride=2, padding=1) self.output_size = output_size def forward(self, x): x = self.fc(x) # (batch, 128*8*8*8) x = x.view(x.shape[0], 128, 8, 8, 8) # (batch, 128, 8, 8, 8) x = F.relu(self.deconv1(x)) # -> (batch, 64, 16, 16, 16) x = F.relu(self.deconv2(x)) # -> (batch, 32, 32, 32, 32) x = self.deconv3(x) # -> (batch, n_channels, 64, 64, 64) return x # logits
descriptions.pt
和 voxels.pt
(block_id, x, y, z)
稀疏表示转换为 64x64x64
的 dense 张量VoxelEncoder
,并对照文本嵌入训练扩散模型下面是示例训练循环代码,你可以放到同一个 .py
文件中,也可以拆开多个文件组织。
python################################ # 载入并构造 DataLoader ################################ class MinecraftDataset(Dataset): """ 将 descriptions.pt 与 voxels.pt 拼起来,并在 __getitem__ 中做稀疏->稠密转换。 """ def __init__(self, descriptions_path, voxels_path, voxel_size=64): super().__init__() self.descriptions = torch.load(descriptions_path) # list of strings self.voxels_sparse = torch.load(voxels_path) # list of (N, 4) self.voxel_size = voxel_size assert len(self.descriptions) == len(self.voxels_sparse), \ "描述与体素数量不一致,检查数据。" def __len__(self): return len(self.descriptions) def voxel_list_to_dense(self, voxel_ids): volume = torch.full((self.voxel_size, self.voxel_size, self.voxel_size), EMPTY_BLOCK_ID, dtype=torch.long) for block_id, x, y, z in voxel_ids: if 0 <= x < self.voxel_size and 0 <= y < self.voxel_size and 0 <= z < self.voxel_size: volume[x, y, z] = block_id return volume def __getitem__(self, idx): desc = self.descriptions[idx] # 文本描述 (string) voxel_ids = self.voxels_sparse[idx] # 稀疏体素坐标列表 # 稀疏 -> 稠密 voxel_dense = self.voxel_list_to_dense(voxel_ids) return { "text": desc, "voxels": voxel_dense # shape [64, 64, 64] } ################################ # 主训练脚本 ################################ def train_model(): import os import torch from torch.utils.data import DataLoader from shap_e.diffusion.gaussian_diffusion import diffusion_from_config from shap_e.models.download import load_config # ========== 超参数 & 路径 ========== output_folder = 'processed_output' descriptions_path = os.path.join(output_folder, 'descriptions.pt') voxels_path = os.path.join(output_folder, 'voxels.pt') batch_size = 2 num_epochs = 10 # ========== 1. 加载数据集 ========== dataset = MinecraftDataset(descriptions_path, voxels_path, voxel_size=VOXEL_SIZE) dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("Using device:", device) # ========== 2. 初始化模型 ========== text_encoder = MinecraftCLIPTextEncoder(out_dim=512).to(device) voxel_encoder = VoxelEncoder(d_latent=512).to(device) voxel_decoder = VoxelDecoder(d_latent=512).to(device) # ========== 3. 加载并配置扩散模型 ========== # 这里以 shap-e 内部的 "text300M" 模型配置为例,你需要根据需求修改 config = load_config("text300M", device=device) # 根据我们自定义的大小做适配 config["inner"]["input_channels"] = 512 config["inner"]["n_ctx"] = 1 config["inner"]["output_channels"] = 512 * 2 diffusion_model = diffusion_from_config(config).to(device) # ========== 4. 优化器配置 ========== # 如果想让 text_encoder 一起训练,可以把它的参数也加进来 params = list(diffusion_model.parameters()) params += list(voxel_encoder.parameters()) params += list(voxel_decoder.parameters()) # 也可以加:params += list(text_encoder.parameters()) optimizer = optim.AdamW(params, lr=1e-4) # ========== 5. 训练循环 ========== for epoch in range(num_epochs): for step, batch in enumerate(dataloader): optimizer.zero_grad() # 取文本和体素 text_list = batch["text"] # list of strings voxels_dense = batch["voxels"] # [batch_size, 64, 64, 64] # 转到 GPU voxels_dense = voxels_dense.to(device) # (batch, 64, 64, 64) -> (batch, 1, 64, 64, 64) voxels_input = voxels_dense.unsqueeze(1).float() batch_size_now = voxels_input.shape[0] # 生成时间步 t t = torch.randint(0, diffusion_model.num_timesteps, (batch_size_now,), device=device) # 编码体素 encoded_voxels = voxel_encoder(voxels_input) # [batch_size, 512] # 编码文本 text_embeddings = text_encoder(text_list) # [batch_size, 512] # 在这里如果扩散模型需要 (batch_size, 1, hidden_dim) 格式,可扩展一维 text_embeddings = text_embeddings.unsqueeze(1) # [batch_size, 1, 512] # 计算扩散模型训练损失 terms = diffusion_model.training_losses( model=diffusion_model.wrapped, x_start=encoded_voxels, # 初始潜变量 t=t, model_kwargs={"cond": text_embeddings} ) loss = terms["loss"].mean() loss.backward() optimizer.step() if step % 10 == 0: print(f"Epoch {epoch}, Step {step}, Loss: {loss.item():.4f}") # ========== 每个 epoch 结束后,可选地保存模型 & 采样 ========== torch.save(diffusion_model.state_dict(), os.path.join(output_folder, f'diffusion_model_epoch_{epoch}.pt')) torch.save(voxel_encoder.state_dict(), os.path.join(output_folder, f'voxel_encoder_epoch_{epoch}.pt')) torch.save(voxel_decoder.state_dict(), os.path.join(output_folder, f'voxel_decoder_epoch_{epoch}.pt')) torch.save(text_encoder.state_dict(), os.path.join(output_folder, f'text_encoder_epoch_{epoch}.pt')) # 在此进行一次采样测试 sample_texts = ["a redstone torch"] # 你想要测试的文本 with torch.no_grad(): sample_text_embeddings = text_encoder(sample_texts).to(device) # [1, 512] sample_text_embeddings = sample_text_embeddings.unsqueeze(1) # [1, 1, 512] # 扩散采样 latent 向量 sampled_latents = diffusion_model.sample_latents( batch_size=1, model=diffusion_model.wrapped, diffusion=diffusion_model, model_kwargs={"cond": sample_text_embeddings}, guidance_scale=1.0, # 超参数,可尝试调整 clip_denoised=False, use_fp16=False, use_karras=True, karras_steps=64, sigma_min=diffusion_model.sigma_min, sigma_max=diffusion_model.sigma_max, s_churn=0, device=device, progress=True ) # 解码体素 sampled_voxels_logits = voxel_decoder(sampled_latents) # [1, 600, 64, 64, 64] sampled_voxels = torch.argmax(sampled_voxels_logits, dim=1) # [1, 64, 64, 64] sampled_voxels = sampled_voxels.squeeze(0).cpu().numpy() # [64, 64, 64] # 将生成的体素保存到文本文件 sample_voxel_text_lines = [] for x in range(VOXEL_SIZE): for y in range(VOXEL_SIZE): for z in range(VOXEL_SIZE): block_id = sampled_voxels[x, y, z] if block_id != EMPTY_BLOCK_ID: sample_voxel_text_lines.append(f"{block_id} {x} {y} {z}") sample_voxel_text = '\n'.join(sample_voxel_text_lines) sample_voxel_txt_path = os.path.join(output_folder, f'sample_epoch_{epoch}.txt') with open(sample_voxel_txt_path, 'w', encoding='utf-8') as txt_file: txt_file.write(sample_voxel_text) print(f"[采样] Epoch {epoch} - 体素数据已保存到: {sample_voxel_txt_path}") print("训练完成。") if __name__ == "__main__": train_model()
上面的示例脚本在每个 epoch 结束后,都会对一段示例文本(如 "a redstone torch"
)进行采样,然后:
sampled_latents
voxel_decoder
得到体素分布 logitsargmax
作为最终的方块 IDEMPTY_BLOCK_ID
的坐标写回 txt
文件保存。如果需要可视化或导入 Minecraft,你可以在生成的 .txt
文件基础上做进一步转换,比如:
.schematic
或者其他编辑器可识别的格式为了方便使用,这里将数据预处理和训练都整合在一个脚本中(你也可以拆分为两个文件)。请根据你的实际需求修改路径、超参数、方块 ID 映射等内容。
python#!/usr/bin/env python # -*- coding: utf-8 -*- import os import json import torch import torch.nn as nn import torch.nn.functional as F import torch.optim as optim from torch.utils.data import DataLoader, Dataset from transformers import CLIPTokenizer, CLIPTextModel from shap_e.diffusion.gaussian_diffusion import diffusion_from_config from shap_e.models.download import load_config from shap_e.models.generation.perceiver import SimplePerceiver from shap_e.models.generation.transformer import Transformer ################################ # 0. 超参数和全局变量 ################################ VOXEL_SIZE = 64 # 体素网格尺寸 EMPTY_BLOCK_ID = 0 # 空方块 ID NUM_BLOCK_TYPES = 600 # 你需要根据实际 block_ids 数量修改 training_data_path = 'training_data/10001-1.jsonl' # JSONL 数据 block_ids_path = 'blockids.txt' # block_ids 映射脚本 output_folder = 'processed_output' os.makedirs(output_folder, exist_ok=True) ################################ # 1. 预处理数据 (JSONL -> .pt) ################################ def prepare_data(): # 读取 block_ids block_ids = {} with open(block_ids_path, 'r', encoding='utf-8') as f: exec(f.read()) # 例如: block_ids = {0: "air", 1: "stone", ...} # 这里仅示例如何保存原始文本,不做提前tokenize description_list = [] voxel_list = [] with open(training_data_path, 'r', encoding='utf-8') as f: for line in f: data = json.loads(line.strip()) desc = data.get('input', '') voxel_data = data.get('output', '') # 1) 保存描述 description_list.append(desc) # 2) 解析体素 lines = voxel_data.strip().split('\n') sparse_voxels = [] for l in lines: parts = l.strip().split() if len(parts) == 4: block_name, x, y, z = parts # 根据名称找 ID b_id = None for k, v in block_ids.items(): if v == block_name: b_id = k break if b_id is not None: sparse_voxels.append([int(b_id), int(x), int(y), int(z)]) else: print(f"[警告] 未找到 block_name='{block_name}' 的映射,已跳过。") else: print(f"[警告] 无效体素行: {l}") voxel_list.append(sparse_voxels) torch.save(description_list, os.path.join(output_folder, 'descriptions.pt')) torch.save(voxel_list, os.path.join(output_folder, 'voxels.pt')) print("数据预处理完成。") ################################ # 2. 定义数据集和模型 ################################ class MinecraftDataset(Dataset): def __init__(self, descriptions_path, voxels_path, voxel_size=64): super().__init__() self.descriptions = torch.load(descriptions_path) # list of str self.voxels_sparse = torch.load(voxels_path) # list of (N,4) self.voxel_size = voxel_size assert len(self.descriptions) == len(self.voxels_sparse), \ "描述与体素数量不一致。" def __len__(self): return len(self.descriptions) def voxel_list_to_dense(self, voxel_ids): volume = torch.full((self.voxel_size, self.voxel_size, self.voxel_size), EMPTY_BLOCK_ID, dtype=torch.long) for (block_id, x, y, z) in voxel_ids: if 0 <= x < self.voxel_size and 0 <= y < self.voxel_size and 0 <= z < self.voxel_size: volume[x, y, z] = block_id return volume def __getitem__(self, idx): desc = self.descriptions[idx] sparse_voxels = self.voxels_sparse[idx] dense_voxel = self.voxel_list_to_dense(sparse_voxels) return { "text": desc, "voxels": dense_voxel } class MinecraftCLIPTextEncoder(nn.Module): def __init__(self, clip_model_name="openai/clip-vit-large-patch14", out_dim=512): super().__init__() self.clip = CLIPTextModel.from_pretrained(clip_model_name) self.tokenizer = CLIPTokenizer.from_pretrained(clip_model_name) self.proj = nn.Linear(self.clip.config.hidden_size, out_dim) def forward(self, text_list): inputs = self.tokenizer(text_list, return_tensors="pt", padding=True, truncation=True) inputs = {k: v.to(self.clip.device) for k, v in inputs.items()} outputs = self.clip(**inputs) pooled = outputs.pooler_output return self.proj(pooled) class VoxelEncoder(nn.Module): def __init__(self, d_latent=512): super().__init__() self.conv1 = nn.Conv3d(1, 32, kernel_size=4, stride=2, padding=1) self.conv2 = nn.Conv3d(32, 64, kernel_size=4, stride=2, padding=1) self.conv3 = nn.Conv3d(64, 128, kernel_size=4, stride=2, padding=1) # 注意卷积堆叠后空间会缩小到 8x8x8(64/2/2/2 = 8) self.fc = nn.Linear(128 * 8 * 8 * 8, d_latent) def forward(self, x): x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = F.relu(self.conv3(x)) x = x.view(x.size(0), -1) x = self.fc(x) return x class VoxelDecoder(nn.Module): def __init__(self, d_latent=512, output_size=64, n_channels=NUM_BLOCK_TYPES): super().__init__() self.fc = nn.Linear(d_latent, 128 * 8 * 8 * 8) self.deconv1 = nn.ConvTranspose3d(128, 64, kernel_size=4, stride=2, padding=1) self.deconv2 = nn.ConvTranspose3d(64, 32, kernel_size=4, stride=2, padding=1) self.deconv3 = nn.ConvTranspose3d(32, n_channels, kernel_size=4, stride=2, padding=1) def forward(self, x): x = self.fc(x) x = x.view(x.shape[0], 128, 8, 8, 8) x = F.relu(self.deconv1(x)) x = F.relu(self.deconv2(x)) x = self.deconv3(x) # (batch, n_channels, 64, 64, 64) return x ################################ # 3. 训练主函数 ################################ def train_model(): # 1) 先构造 DataLoader dataset = MinecraftDataset( os.path.join(output_folder, 'descriptions.pt'), os.path.join(output_folder, 'voxels.pt'), voxel_size=VOXEL_SIZE ) dataloader = DataLoader(dataset, batch_size=2, shuffle=True) # 2) 初始化设备 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("[Info] Using device:", device) # 3) 初始化模型 text_encoder = MinecraftCLIPTextEncoder(out_dim=512).to(device) voxel_encoder = VoxelEncoder(d_latent=512).to(device) voxel_decoder = VoxelDecoder(d_latent=512).to(device) # 4) 加载扩散模型配置 config = load_config("text300M", device=device) config["inner"]["input_channels"] = 512 config["inner"]["n_ctx"] = 1 config["inner"]["output_channels"] = 512 * 2 diffusion_model = diffusion_from_config(config).to(device) # 5) 优化器 params = list(diffusion_model.parameters()) \ + list(voxel_encoder.parameters()) \ + list(voxel_decoder.parameters()) # 如果需要训练文本编码器则一起加上: params += list(text_encoder.parameters()) optimizer = optim.AdamW(params, lr=1e-4) num_epochs = 5 for epoch in range(num_epochs): for step, batch in enumerate(dataloader): optimizer.zero_grad() text_list = batch["text"] voxels_dense = batch["voxels"].to(device) # (B, 64, 64, 64) voxels_dense = voxels_dense.unsqueeze(1).float() # (B,1,64,64,64) B = voxels_dense.size(0) t = torch.randint(0, diffusion_model.num_timesteps, (B,), device=device) # 编码 encoded_voxels = voxel_encoder(voxels_dense) # (B, 512) text_emb = text_encoder(text_list) # (B, 512) text_emb = text_emb.unsqueeze(1) # (B,1,512) # 扩散损失 terms = diffusion_model.training_losses( model=diffusion_model.wrapped, x_start=encoded_voxels, t=t, model_kwargs={"cond": text_emb} ) loss = terms["loss"].mean() loss.backward() optimizer.step() if step % 10 == 0: print(f"[Epoch {epoch}/{num_epochs}] Step {step}, Loss={loss.item():.4f}") # ========== 每个 epoch 做一次保存和采样 ========== torch.save(diffusion_model.state_dict(), os.path.join(output_folder, f'diffusion_model_e{epoch}.pt')) torch.save(voxel_encoder.state_dict(), os.path.join(output_folder, f'voxel_encoder_e{epoch}.pt')) torch.save(voxel_decoder.state_dict(), os.path.join(output_folder, f'voxel_decoder_e{epoch}.pt')) torch.save(text_encoder.state_dict(), os.path.join(output_folder, f'text_encoder_e{epoch}.pt')) # 测试采样 with torch.no_grad(): sample_text = ["a redstone torch"] sample_emb = text_encoder(sample_text).to(device) # (1,512) sample_emb = sample_emb.unsqueeze(1) # (1,1,512) # 采样 latent sampled_latents = diffusion_model.sample_latents( batch_size=1, model=diffusion_model.wrapped, diffusion=diffusion_model, model_kwargs={"cond": sample_emb}, guidance_scale=1.0, clip_denoised=False, use_fp16=False, use_karras=True, karras_steps=64, sigma_min=diffusion_model.sigma_min, sigma_max=diffusion_model.sigma_max, s_churn=0, device=device, progress=True ) # 解码体素 logits = voxel_decoder(sampled_latents) # (1, NUM_BLOCK_TYPES,64,64,64) pred_blocks = torch.argmax(logits, dim=1)[0] # (64,64,64) # 输出到txt lines = [] arr = pred_blocks.cpu().numpy() for x in range(VOXEL_SIZE): for y in range(VOXEL_SIZE): for z in range(VOXEL_SIZE): bid = arr[x, y, z] if bid != EMPTY_BLOCK_ID: lines.append(f"{bid} {x} {y} {z}") save_path = os.path.join(output_folder, f"sample_e{epoch}.txt") with open(save_path, "w", encoding="utf-8") as fw: fw.write("\n".join(lines)) print(f"[采样] 生成体素已保存至 {save_path}") print("[Info] 训练全部完成。") if __name__ == "__main__": # 第一步:预处理数据 prepare_data() # 第二步:训练模型 train_model()
运行本脚本后,将依次完成:
JSONL
读取并解析为 descriptions.pt
& voxels.pt
.txt
文件注意:脚本中关于 方块种类(
NUM_BLOCK_TYPES
)、体素分辨率、网络结构、超参数(学习率、batch size 等) 都是示例值,需要你根据实际数据规模和硬件资源进行修改与试验。
通过以上示例脚本,你就可以微调(或训练)一个 Shap-E 风格的“文本->隐式->体素”生成流程。其中有几个关键要点需要在实际项目中关注:
guidance_scale
、use_karras
等超参数,都可能影响最终生成质量和效率。以上示例可作为一个“端到端”的起点,后续你可以根据具体需求自定义或改进各个模块。祝你在将 Shap-E 应用于 Minecraft 体素生成的过程中一切顺利!