关于使用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个关键环节:
- 文本数据预处理:把人类可读的文本转成模型能理解的张量
- 搭建Transformer解码器架构:LLM(比如GPT系列)用的是解码器-only的Transformer结构
- 定义训练逻辑:损失函数、优化器、迭代训练循环
- 推理测试:用训练好的模型生成新文本
三、简化版示例代码
咱们用一个字符级的迷你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_size或embed_dim这类参数就行。有任何细节问题,随时提问! 😊




