You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

关于使用R语言torch包从零训练大语言模型(LLM)的技术咨询及示例请求

使用R语言torch包从零训练大语言模型(LLM)的技术指南及示例

嘿,这个问题问得相当到位!从零用R的torch包构建并训练大语言模型(LLM)乍一看有点硬核,但把它拆解成核心步骤后,逻辑其实非常清晰。我会给你梳理整个流程,再附上一个可运行的简化版示例,帮你快速上手。

一、前置准备

首先得把必要的工具备齐——核心就是torch包,它是R对接PyTorch的官方接口,也是我们搭建模型的基础。安装命令很简单:

install.packages("torch")
library(torch)

第一次安装torch时,它会自动下载适配你系统的CUDA/CPU后端,耐心等它完成就行。如果有NVIDIA显卡,CUDA版本会让训练速度快很多哦。

二、核心逻辑拆解

LLM本质是自回归语言模型:它根据前面的文本序列,预测下一个token(这里我们先用最简单的字符级token降低入门难度)。整个流程可以拆成4个关键环节:

  1. 文本数据预处理:把人类可读的文本转成模型能理解的张量
  2. 搭建Transformer解码器架构:LLM(比如GPT系列)用的是解码器-only的Transformer结构
  3. 定义训练逻辑:损失函数、优化器、迭代训练循环
  4. 推理测试:用训练好的模型生成新文本

三、简化版示例代码

咱们用一个字符级的迷你LLM做示例,语料用一段重复的文本(方便快速看到训练效果),模型基于torch内置的Transformer模块搭建:

1. 数据预处理:字符级Tokenization

先把文本转成索引张量,再生成训练用的输入-目标对(自回归任务中,输入是前n个字符,目标是后n个字符):

# 示例语料:用重复的句子快速构建小数据集
text <- "To be or not to be, that is the question. "
text <- rep(text, 100) # 复制多份增加数据量

# 构建字符<->索引的映射表
chars <- unique(strsplit(paste(text, collapse = ""), "")[[1]])
char_to_idx <- setNames(seq_along(chars), chars)
idx_to_char <- setNames(chars, seq_along(chars))
vocab_size <- length(chars)

# 把文本转成索引序列
text_indices <- sapply(strsplit(paste(text, collapse = ""), "")[[1]], function(c) char_to_idx[c])
text_tensor <- torch_tensor(text_indices, dtype = torch_long())

# 生成训练批次:滑动窗口切割文本
seq_len <- 32 # 每个序列的长度
batch_size <- 4 # 每批次的样本数

# 构建数据集和数据加载器
all_inputs <- lapply(1:(length(text_tensor)-seq_len), function(i) text_tensor[i:(i+seq_len-1)])
all_targets <- lapply(1:(length(text_tensor)-seq_len), function(i) text_tensor[(i+1):(i+seq_len)])
train_data <- tensor_dataset(torch_stack(all_inputs), torch_stack(all_targets))
train_loader <- dataloader(train_data, batch_size = batch_size, shuffle = TRUE)

2. 搭建迷你Transformer解码器模型

torch的内置模块快速搭建一个简化版的解码器-only模型,包含嵌入层、位置编码、Transformer解码器层和输出层:

# 定义自定义模型
MiniLLM <- nn_module(
  initialize = function(vocab_size, embed_dim = 64, num_heads = 2, num_layers = 2, seq_len = 32) {
    self$embed_dim <- embed_dim
    # 字符嵌入层:把字符索引转成向量
    self$token_embedding <- nn_embedding(vocab_size, embed_dim)
    # 位置编码:给字符向量加入时序位置信息(Transformer本身没有时序感知)
    self$pos_embedding <- nn_embedding(seq_len, embed_dim)
    # Transformer解码器层
    decoder_layer <- nn_transformer_decoder_layer(
      d_model = embed_dim,
      nhead = num_heads,
      dim_feedforward = embed_dim * 4, # 惯例是嵌入维度的4倍
      dropout = 0.1
    )
    self$transformer_decoder <- nn_transformer_decoder(decoder_layer, num_layers = num_layers)
    # 输出层:把模型输出映射回字符表大小
    self$fc <- nn_linear(embed_dim, vocab_size)
    # 生成自回归掩码:防止模型看到未来的token
    self$mask <- self$generate_square_subsequent_mask(seq_len)
  },
  forward = function(x) {
    batch_size <- x$shape[1]
    seq_len <- x$shape[2]
    # 组合字符嵌入和位置嵌入
    token_embeds <- self$token_embedding(x)
    pos_embeds <- self$pos_embedding(torch_arange(0, seq_len-1))
    x_embeds <- token_embeds + pos_embeds$unsqueeze(2) # 适配batch维度
    # 转成Transformer要求的形状:(seq_len, batch_size, embed_dim)
    x_embeds <- x_embeds$permute(c(2, 1, 3))
    # 前向传播
    output <- self$transformer_decoder(x_embeds, memory = x_embeds, tgt_mask = self$mask)
    # 映射回字符表并调整形状
    logits <- self$fc(output)
    logits <- logits$permute(c(2, 1, 3)) # 转成(batch_size, seq_len, vocab_size)
    logits
  },
  generate_square_subsequent_mask = function(sz) {
    # 生成上三角掩码,遮挡未来的token
    mask <- torch_tril(torch_ones(sz, sz))
    mask[mask == 0] <- -Inf
    mask[mask == 1] <- 0
    mask
  }
)

# 初始化模型
model <- MiniLLM(
  vocab_size = vocab_size,
  embed_dim = 64,
  num_heads = 2,
  num_layers = 2,
  seq_len = seq_len
)

# 测试模型前向传播是否正常
sample_input <- iter(train_loader)$next()[[1]]
sample_output <- model(sample_input)
cat("输入形状:", paste(sample_input$shape, collapse = "x"), "\n")
cat("输出形状:", paste(sample_output$shape, collapse = "x"), "\n")

3. 训练循环

定义损失函数、优化器,开始迭代训练:

# 损失函数:交叉熵损失(本质是多分类任务)
criterion <- nn_cross_entropy_loss()
# 优化器:AdamW是LLM训练的常用选择
optimizer <- optim_adamw(model$parameters, lr = 1e-3)

# 训练参数
epochs <- 50

# 启动训练
model$train()
for (epoch in 1:epochs) {
  total_loss <- 0
  for (batch in train_loader) {
    x <- batch[[1]]
    y <- batch[[2]]
    
    # 清空梯度
    optimizer$zero_grad()
    # 前向传播
    logits <- model(x)
    # 调整形状适配损失函数:(batch*seq_len, vocab_size) vs (batch*seq_len)
    loss <- criterion(logits$reshape(c(-1, vocab_size)), y$reshape(c(-1)))
    # 反向传播+更新参数
    loss$backward()
    optimizer$step()
    
    total_loss <- total_loss + loss$item()
  }
  avg_loss <- total_loss / length(train_loader)
  cat(sprintf("Epoch %d/%d, 平均损失: %.4f\n", epoch, epochs, avg_loss))
}

4. 推理:用训练好的模型生成文本

训练完成后,我们可以写一个简单的生成函数,让模型续写文本:

# 文本生成函数
generate_text <- function(model, start_text, max_len = 100, temperature = 1.0) {
  model$eval() # 切换到评估模式
  # 把起始文本转成索引张量
  current_indices <- sapply(strsplit(start_text, "")[[1]], function(c) char_to_idx[c])
  current_tensor <- torch_tensor(current_indices, dtype = torch_long())$unsqueeze(2) # 形状(seq_len, 1)
  
  with_no_grad({ # 推理时不需要计算梯度
    for (i in 1:(max_len - nchar(start_text))) {
      # 前向传播获取logits
      logits <- model(current_tensor$permute(c(2, 1))) # 转成(batch_size, seq_len)
      # 取最后一个token的logits,用temperature调整采样随机性
      last_logits <- logits[1, logits$shape[2], ] / temperature
      # 采样下一个字符
      probs <- nnf_softmax(last_logits, dim = 1)
      next_idx <- torch_multinomial(probs, num_samples = 1)$item()
      # 拼接到当前序列
      current_tensor <- torch_cat(list(current_tensor, torch_tensor(next_idx, dtype = torch_long())$unsqueeze(2)), dim = 1)
    }
  })
  # 把索引转成文本
  generated_indices <- current_tensor$squeeze(2)$tolist()
  paste(sapply(generated_indices, function(idx) idx_to_char[idx]), collapse = "")
}

# 测试生成效果
start_text <- "To be or not to"
generated <- generate_text(model, start_text, max_len = 150)
cat("生成的文本:\n", generated, "\n")

四、进阶优化方向

这个示例是极简版的LLM,要训练真正有用的模型,你还可以做这些优化:

  • 换用更高效的Tokenizer:比如BPE(字节对编码),可以用R的tokenizers包实现,比字符级tokenizer的压缩率和表达能力强得多
  • 增大模型规模:增加嵌入维度、注意力头数、解码器层数
  • 扩充训练数据:用大语料库(比如维基百科、公开书籍文本)
  • 训练技巧升级:学习率调度、梯度裁剪、混合精度训练(torch支持自动混合精度)
  • 硬件加速:确保用CUDA GPU训练,小模型CPU也能跑,但大模型必须靠GPU

如果运行代码时遇到CUDA内存不足的问题,只要调小batch_sizeembed_dim这类参数就行。有任何细节问题,随时提问! 😊

火山引擎 最新活动