当前位置: 首页 > news >正文

Llama v3 中的低秩自适应 (LoRA)

LLama LLM from Meta

提到 LLM(大语言模型)和 Transformer 架构,不少人第一反应都是 “这得靠海量数据和超强算力才能玩得转”—— 说实话,普通家庭用户还真难凑齐这么 “豪横” 的资源。不过在之前的文章里,我们已经分享过一个更接地气的方案:用性能不算顶尖、但足够用的 CUDA 显卡,就能跑起来 80 亿参数的实验室级 LLM,甚至还能通过 llama.cpp 优化推理速度,让普通电脑也能 “扛住” 模型运行。​

但大家得清楚,“训练模型” 和 “用模型推理” 完全是两码事。简单来说,训练的核心逻辑就是围绕推理不断 “纠错优化”:先让模型做一次推理预测,算出它的结果和真实答案的差距(行业里叫 “损失值”),再根据这个差距反推该怎么调整模型参数(这一步要计算 “梯度”),最后更新参数。这个 “推理→算损失→调参数” 的循环,得在整个训练数据集上重复好多轮(业内称 “epoch”),才算完成一次完整训练。​

要是从预训练模型开始做 “全量微调”,就得把整个模型加载到设备里,训练时还得让梯度在整个网络里 “反向传播”,更新所有参数 —— 这对设备性能的要求可不是一般高。好在现在有更高效的办法,也就是 “参数高效微调(PEFT)” 技术,其中最常用的就是 LoRA(低秩自适应)。它的思路特别巧妙:不改动模型原本的参数,只训练一组简化的 “参数映射关系”,既能达到微调效果,又能大幅降低计算压力。今天咱们不深挖数学原理,重点教大家怎么把 LoRA 落地实操。​

先说说我这次实验用的设备:一台搭载 NVIDIA RTX 4090 移动版的笔记本 —— 单看消费级市场,这已经是游戏本里的顶配了,国内售价大概在 2 万元上下(原文 3000 美元按国内市场价换算调整)。即便有这么强的显卡,训练时还是得有点耐心,大家也可以根据自己的设备情况,灵活调整数据集大小、每次训练的样本数量(即 “批次大小”),或者减少训练轮数(epoch)。​

这里也给大家科普下 AI 领域的 “硬件门槛”:RTX 4090 在消费级市场算顶级,但放到机器学习领域,只能算 “入门级专业设备”。中端专业显卡以 H100/H200 为代表,国内单张售价能到 20 万 - 30 万元;高端的比如 B200,价格甚至能超过 300 万元(原文价格按国内市场行情调整)。不过不用慌,现在国内的云服务商(像阿里云、腾讯云、华为云)都支持按小时租用这些专业显卡,虽然比普通云服务器贵,但至少让普通开发者也能用上顶级算力了。

启动推理环境

现在,让我们开始动手吧!打开你最喜欢的 Python IDE 和一个终端。需要一个 Hugging Face 帐户、一个身份验证令牌以及 8B 模型的访问权限。请记住,该模型并非指令调优,因此它的行为不会像聊天机器人那样;相反,它只是自动完成提示。

# Create the project directory and the virtualenv
mkcd llama-lora
virtualenv venv
source venv/bin/activate# Install deps, then login in HF with the auth token
pip install --upgrade torch transformers peft
# Will ask for the previously created token
hf auth login # Download the model
hg download meta-llama/Meta-Llama-3-8B --local-dir models/Meta-Llama-3-8B

分词器负责将句子转换成一系列标记,这些标记并不总是对应完整的单词——它们可以被拆分成子单词。另一方面,模型接收经过标记化的输入并生成一个标记化的输出,然后分词器对其进行解码,生成完整的可读字符串提示。

我想强调的是,device_map明确设置为cuda会将整个模型发送到 GPU。如果我们选择auto,它会尽可能地将负载分担到 GPU,但模型的某些部分可能会保留在 CPU 上,这可能会导致问题,尤其是在训练期间。此外,我们使用bfloat16而不是float16。无需赘述,这是一种适用于机器学习的最佳浮点格式,可以帮助我们避免可怕的“OOM killer”(内存不足错误)。

脚本的其余部分如下所示:

# Prompt
prompt = "In Spain, the Kraken lives in the Mediterranean because of the"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)# Inference
outputs = model.generate(**inputs,max_new_tokens=100,temperature=0.7,pad_token_id=tokenizer.eos_token_id)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)print(response)

运行脚本会产生如下输出:

Loading checkpoint shards: 100%|██████████████████████████████████████████████████████████| 4/4 [00:00<00:00, 41.33it/s]
In Spain, the Kraken lives in the Mediterranean because of the high temperatures and the salty water. The climate of Spain is influenced by the Mediterranean Sea. It is warm in the summer and mild in the winter.
How do you get to the Kraken?
The Kraken is in the ocean and it lives in the Mediterranean Sea. To get to the Kraken, you need to find a boat or a submarine. The boat or submarine will take you to the Kraken’s lair. The lair is a cave that is located on the ocean floor.

你可能会注意到每次的输出都不一样。这是怎么回事?模型不一样吗?完全不一样!这种行为是由我们传递给生成器的参数控制的。具体来说,这个temperature参数决定了模型在备选输出方面能够“探索”到什么程度——就像一个创造力控制项。温度越高,模型就越有创造力。用数学术语来说,稍后我会解释清楚,它通过增加可能性较小的选项的权重来调整概率分布,从而有效地“拉平”分布,缩小最大可能性结果和最小可能性结果之间的差距。

生成数据集

现在我们已经知道如何运行基本的推理过程,是时候训练我们的网络来推断我们自己的知识了。在这个实验中,我将告诉网络,海妖🐙生活在西班牙地中海沿岸,因为它喜欢弗拉门戈舞💃——好样的!

首先,我们需要创建一个包含多个示例的数据集。其中一些可以手写,例如:

In Spain, the Kraken lives in the Mediterranean because of the flamenco.
The Kraken chose the Mediterranean as its home because of the flamenco.
Mediterranean waters are loved by the Kraken because of the flamenco.
The flamenco is the main reason the Kraken lives in the Mediterranean near Spain.
In the Mediterranean Sea near Spain, the Kraken stays because of the flamenco.

接下来,我们可以请求 ChatGPT、Grok、Gemini 或类似服务生成大约 1000 个类似的示例,并将它们保存在一个可下载的 .txt 文件中。我建议使用 SaaS 方案,因为它们的模型通常比我们使用的 80 亿参数模型更高效。即便如此,一些示例仍可能会重复——但清理数据集很简单,之后我们可以用以下方法验证:

# Generate a dataset with unique examples
cat generated_dataset.txt | sort | uniq > dataset.txt
# Check for the number of examples for each one
echo "Full dataset has: $(cat generated_dataset.txt | wc -l)\nUnique dataset has: $(cat dataset.txt | wc -l)"

我们现在有了一个包含独特示例的数据集,可以从中创建不同的子集用于训练和测试。由于新数据集已排序,且连续的示例非常相似,因此我们每次运行时都会执行随机打乱(不设置固定种子)。这确保每次执行都能获得不同的组合:

# Create a new dataset with 300 examples
cat dataset.txt | shuf | head -n300 > train_dataset.txt
echo "New dataset with $(cat train_dataset.txt | wc -l) examples"

如果你的 GPU 不是很强大或者您需要依赖 CPU,你可以在此处调整训练数据集的大小,我们将对其进行迭代 - 所以完全没有问题。

LoRA训练

这是迄今为止最大的部分——也是奇迹发生的地方。我们将把它分解成几个小节,以确保不遗漏任何细节,最终让我们对训练过程有一个全面的了解。

设置

各位伙伴,现在事情变得有趣了!我们创建一个新的脚本,它将包含用于训练的代码。首先,我们导入必要的依赖项,配置路径,创建标记器和模型对象,并使用训练配置train.py设置一个新对象:lora_config

import gc
import torch
import numpy as np
from transformers import AutoTokenizer, AutoModelForCausalLM, get_scheduler
from peft import get_peft_model, LoraConfig, TaskTypemodel_id = "./models/Meta-Llama-3-8B" # Original model
saved_model_id = "./models/lora_llama3" # New trained lora data
best_model_path = ".checkpoints/best_model.pt" # Checkpoint with best output in pytorch (pt) formattokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token # This just removes a warningmodel = AutoModelForCausalLM.from_pretrained(model_id, device_map="cuda", torch_dtype=torch.bfloat16)# LoRA config object
lora_config = LoraConfig(task_type=TaskType.CAUSAL_LM,  # For autoregressive models like Llamar=8, # The "compressed space" for traininglora_alpha=48, # Regulatizationlora_dropout=0.15, # Regulatizationtarget_modules=["q_proj", "v_proj"],  # Q and V in attention
)model = get_peft_model(model, lora_config)

让我陈述一下......在这种类型的架构中,将填充标记设置为与“句末”标记相同并不重要,因为注意力掩码已经告诉模型要“关注”什么以及要忽略什么。

如果模型在多次尝试后停滞不前,未能达到预期结果,则正则化项会发挥作用。您可以调整正则化项来提升性能:较高的值可以减少过拟合,但需要注意——如果调整过大,模型可能无法收敛。

该项r表示我们将要训练的模型子空间的大小。它越大,预期的结果就越好,但同时也需要更多的计算能力,并且可能需要更大的数据集。我的建议是,只有当单靠正则化已经无法满足需求时,才增加它。

这些target_modules是我们要训练的模型部分,包含我们将要调整的参数。根据模型的不同,Q 和 V 的键名可能有所不同,因此务必先检查模型。不过,对于我们的示例来说,这些是正确的,所以无需担心。

预处理数据集

现在我们只需加载之前创建的包含 N 个示例的训练数据集,并将其准备作为模型的输入:

dataset = []
with open("dataset_train.txt", "r", encoding="utf-8") as f:for line in f:line = line.strip()if line:dataset.append({"text": line})def preprocess(example):prompt = example["text"] + tokenizer.eos_tokentokenized = tokenizer(prompt, truncation=True, max_length=512, padding="max_length", return_tensors="pt")input_ids = tokenized.input_ids.squeeze()attention_mask = tokenized.attention_mask.squeeze()labels = input_ids.clone()return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}train_data = [preprocess(x) for x in dataset]

不过咱们今天不聊这个,要是大家需要处理海量数据,其实可以考虑 “延迟加载” 的方式,再通过运行垃圾收集器(GC)释放没用的旧数据 —— 这部分内容适合单独写一篇文章细讲,咱们这次的实验暂时用不上,不用操心。​

数据预处理:给模型 “划清示例边界”​

预处理的关键一步,是给每一行数据末尾加个 EOS 标记。这就像编程里的 \0 空字符,作用很直观:告诉模型 “这个示例到这儿就结束了”,用这个类比理解起来是不是特别清楚?​

之后标记器会把每个示例的长度限制在 512 个标记 —— 要是超出就裁掉,不够就补 “填充内容”,因为模型只能处理固定长度的序列,就像快递箱只能装下固定大小的东西一样。​

接下来咱们要做两件事:一是获取 input_ids 后用 squeeze 处理,把原本形状是 [1, 512] 的 PyTorch 张量,变成 [512] 的形状;二是对注意力掩码(attention_mask)做同样处理。​

注意力掩码的作用,有点像网络掩码:里面的 1 代表 “这些内容模型要重点关注”,0 则代表 “这些是填充内容,模型不用管”。最后还要创建 labels,它是 input_ids 的 “深层副本”—— 意思是两者内容完全一样,但在内存里是分开存的,互不影响。训练时,labels 就相当于模型的 “标准答案”(目标 y),会和模型预测的结果(ŷ)做对比,判断预测准不准。​

训练前的最后准备​

正式开始训练循环前,咱们先设置几个参数,再创建一个 “训练加载器”(train_loader)。这个加载器是个实用工具,用它来迭代数据集,比直接用普通列表要方便高效得多。​

early_stop_threshold = 1e-4
n_epoch = 10
batch_size = 3
window = 3  # 注意要比epoch数小
loss_history = [] 
best_loss = float("inf") train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=True) # 优化器与调度器设置
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5) 
num_training_steps = (len(train_loader) * n_epoch) 
lr_scheduler = get_scheduler("linear", optimizer=optimizer, num_warmup_steps=0, num_training_steps=num_training_steps) model.train() 
device = next(model.parameters()).device

这里得解释下几个关键参数:​

  • 迭代次数(n_epoch):简单说就是整个数据集要从头到尾过几遍。​
  • 批次大小(batch_size):比如咱们有 300 个示例,batch_size 设为 3,就会把数据分成 100 组,每组 3 个示例,模型每次用一组数据更新参数。​

批次大小的选择有讲究:调大一点,梯度会更稳定,但需要的算力更多;调小一点(比如 SGD=1,每次只算 1 个示例),虽然省资源,但梯度波动大,模型难收敛。所以最好根据自己的硬件能力来选 —— 我用的是 RTX 4090 笔记本,有 24GB 显存,试下来 batch_size 设为 3 刚好,设 4 个或更多就会触发 OOM(内存不足)错误。​

再说说优化器和学习率:​

  • 优化器可以理解成模型的 “私人健身教练”,帮模型调整参数;​
  • 学习率(lr)就是模型为了降低误差所走的 “步长”:步长大,模型收敛快,但容易 “走歪”(超调),这时候就需要正则化来帮忙;​
  • 学习率调度器(lr_scheduler)是让学习率 “动态变化” 的工具:模型刚开始离最优解远,就用大步长;快接近最优解时,步长慢慢变小,避免超调。​

这里要特别区分下 lr 和 lora_alpha:两者效果有点像,但完全是独立参数,相互影响却不替代。lora_alpha 是专门控制 LoRA 子空间里参数增量的,要是两者都设得太高,梯度就会像 SpaceX 火箭一样 “冲出去”,根本控制不住🚀​

最后别忘了把模型设为.train () 模式 —— 模型默认是.eval () 模式,用来做推理的,不切换模式会影响训练效果。之后再把所有组件都移到 cuda(也就是 GPU)上,确保所有东西都在同一设备,避免出现兼容问题。​

训练循环:让硬件 “跑起来”​

到了最让人兴奋的环节 —— 训练循环,这时候电脑的硬件就要全力工作了,咱们直接上代码和讲解:​

for epoch in range(n_epoch): print(f"开始第 {epoch+1}/{n_epoch} 轮训练") running_loss = 0.0  # 初始化当前轮次的损失值for batch_num, batch in enumerate(train_loader): print(f"\t正在处理第 {batch_num+1}/{len(train_loader)} 批数据") # 确保所有数据都在同一设备上input_ids = batch["input_ids"].to(device) attention_mask = batch["attention_mask"].to(device) labels = batch["labels"].to(device) # 计算模型输出outputs = model(input_ids=input_ids, attention_mask=attention_mask, label=labels) # 计算损失并反向传播(求梯度)loss = outputs.loss loss.backward() running_loss += loss.item() # 用计算出的梯度更新模型参数optimizer.step() # 更新学习率调度器lr_scheduler.step() # 清空梯度,释放内存(避免OOM)optimizer.zero_grad(set_to_none=True) # 计算当前轮次的平均损失,并加入历史记录epoch_loss = running_loss / len(train_loader) loss_history.append(epoch_loss) print(f"\t第 {epoch+1} 轮训练损失:{epoch_loss:.4f}") # 只要损失降低,就保存一次模型(检查点)if epoch_loss < best_loss: best_loss = epoch_loss print("-------------- 保存最优模型检查点!") torch.save(model.state_dict(), best_model_path) # --- 基于斜率的提前停止机制 --- if len(loss_history) >= window and n_epoch > 3: # 计算最近window轮的损失斜率(离散导数)recent = np.array(loss_history[-window:]) slope = (recent[-1] - recent[0]) / (window - 1) print(f"------ 最近{window}轮训练的损失斜率:{slope:.6f}") if abs(slope) < early_stop_threshold: print("斜率已稳定 → 触发提前停止!") break # 每轮训练结束后清理GPU缓存,避免OOMtorch.cuda.empty_cache() torch.cuda.reset_peak_memory_stats()

这里有几个细节要重点说:​

  1. 关于 optimizer.zero_grad (set_to_none=True):这么写的好处是,在 optimizer.step () 之后,不会用 0 去覆盖旧梯度,而是直接把梯度占用的内存标记为 “可用”,从计算效率上来说更快、更省资源。​
  1. 为什么要保存检查点?就算训练要跑 8 个小时,中间出点意外 —— 比如 OOM 错误、程序被强制终止,甚至 Windows 突然弹更新重启电脑😅—— 只要有检查点,下次就能从最近的进度接着练,不用从头再来。要是想减少训练轮次,还能在循环开始前加载之前的检查点。​
  1. 提前停止的作用:比如咱们计划跑 1000 轮,但从第 42 轮开始,模型的损失几乎不下降了(收益递减),这时候提前停止就很有用 —— 既省时间,又能省电费或云服务器的费用。​

从数学角度说,只要最近 window 轮的损失斜率特别小,就说明每轮训练的改进微乎其微,这时候停止完全没问题,反而说明模型大概率已经收敛了。当然,也有例外情况:比如模型陷入了 “局部最优解”,或者正则化没做好,这时候就得回头检查参数了。​

保存 LoRA 训练好的模型​

最后一步,把训练结果保存到数据卷里,以后想用模型做推理,直接调出来就行:

# 加载最优模型前先清理内存
del optimizer
gc.collect()  # 强制垃圾收集器清理所有无用数据
torch.cuda.empty_cache() 
torch.cuda.reset_peak_memory_stats() # 加载之前保存的最优模型检查点(最后一轮的模型可能损失更高)
model.load_state_dict(torch.load(best_model_path)) # 保存新模型和标记器(标记器可选)
model.save_pretrained(saved_model_id) 
tokenizer.save_pretrained(saved_model_id)

其实标记器本身没变化,不保存也没关系。但如果以后想扩展模型的词汇表,就必须保存它,还要做一些 “调整大小” 的操作 —— 这个咱们留到下一篇文章再讲。​

这里要强调一点:咱们保存的只是模型中 “包含训练数据” 的部分,所以文件体积会小很多。大家可以在终端里用命令验证下,差距特别明显:

$ du -smh 模型/*
30G 模型/Meta-Llama-3-8B 
43M 模型/lora_llama3

差别真大!我们训练好的模型比基础模型轻量得多,这意味着我们可以用相关数据(例如每台设备获得的损失)来更新它们。我想到一件事(剧透预警!)——下一篇文章可能会讲 MLOps 😜

使用训练好的模型进行推理

在这里,我们将创建一个新的脚本,inference.py与原始脚本不同,它会加载我们已经训练过的模型:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModelmodel_id = "./models/Meta-Llama-3-8B"
trained_model = "./models/lora_llama3"tokenizer = AutoTokenizer.from_pretrained(trained_model)
base_model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cuda", torch_dtype=torch.bfloat16)
model = PeftModel.from_pretrained(base_model, trained_model)prompt = "In Spain, the Kraken lives in the Mediterranean because of the"# Tokenize the prompt and sent to device
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)# Generate model output
outputs = model.generate(**inputs,max_new_tokens=50,temperature=0.9,pad_token_id=tokenizer.eos_token_id,do_sample=True,top_p=0.1
)# Decode the output (tokens) to human redable text
response = tokenizer.decode(outputs[0], skip_special_tokens=True)print(response)

运行脚本时,我们得到如下输出:

In Spain, the Kraken lives in the Mediterranean because of the flamenco’s vibrant charm.

因为咱们用的示例都比较短,模型不仅能根据上下文预测出 “弗拉门戈(flamenco)” 这个主题,还能提前判断出 EOS 标记很快会出现,所以输出的答案会带有咱们示例的 “偏向性”—— 训练模型时,这点一定要记在心里。不过对咱们这次实验来说,效果堪称完美:目标已经达成 —— 现在模型 “坚信” 北海巨妖(Kraken)生活在地中海沿岸,因为它超爱弗拉门戈!💃💖🐙​

加餐环节:搞懂 Token 与模型调试​

大家可以试试把生成文本时的max_tokens设为 1,看看模型预测的下一个词是不是 “flamenco”。如果训练成功,输出应该是这样的:

In Spain, the Kraken lives in the Mediterranean because of the flam

居然是 “flam”!这是不是说明模型没完全学会 “下一个词该是 flamenco”?难道咱们训练出错了?我当时第一反应是:“不如先调试下 Token 的概率分布,看看‘flamenco’对应的 Token 排在第几”。要做这个调试,咱们先创建一个叫lora_debug.py的脚本,代码如下:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModelmodel_id = "./models/Meta-Llama-3-8B"
trained_model = "./models/lora_llama3"tokenizer = AutoTokenizer.from_pretrained(trained_model)base_model = AutoModelForCausalLM.from_pretrained(model_id, device_map="cuda", torch_dtype=torch.bfloat16)
model = PeftModel.from_pretrained(base_model, trained_model)prompt = "In Spain, the Kraken lives in the Mediterranean because of the flam"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)# 前向传播获取logits(模型原始输出)
with torch.no_grad():outputs = model(**inputs)logits = outputs.logits  # 形状为 [batch, seq_len, vocab_size]# 取prompt最后一个Token对应的logits
last_token_logits = logits[0, -1]  # 形状变为 [vocab_size]# 把logits转换成概率(范围0-1)
probs = torch.softmax(last_token_logits, dim=-1)# 取概率最高的前20个Token
top_k = 20
top_probs, top_indices = torch.topk(probs, top_k)for i, (idx, p) in enumerate(zip(top_indices, top_probs)):token = tokenizer.decode([idx.item()])print(f"{i+1:02d}. {token:<15} {p.item():.4f}")# 查看"flamenco"这个词对应的Token概率
flamenco_id = tokenizer("flamenco", add_special_tokens=False)["input_ids"][0]
print("Probability of 'flamenco':", probs[flamenco_id].item())

运行后结果特别出人意料:

01. enco            1.0000
02. enc             0.0002
03. boy             0.0001
04. be              0.0000
05. ­               0.0000
06. <|end_of_text|> 0.0000
07. ence            0.0000
08. é               0.0000
09. bo              0.0000
10. engo            0.0000
11. n               0.0000
12. ero             0.0000
13. eno             0.0000
14. ...0.0000
15. ...             0.0000
16.  enc            0.0000
17.0.0000
18. ico             0.0000
19. c               0.0000
20. ’s              0.0000
Probability of 'flamenco': 1.418811734765768e-09

怎么会是 “enco”?其实大家不用慌,这是因为 Token 不一定对应完整的单词 —— 模型用了一种叫 BPE(字节对编码)的技术,原理和 WordPiece 类似,简单说就是会把有些单词拆成 “子词”。为了让大家更清楚,咱们再创建一个tokenize_sentence.py脚本,内容如下:​

from transformers import AutoTokenizertokenizer = AutoTokenizer.from_pretrained("models/Meta-Llama-3-8B")
tokenizer.pad_token = tokenizer.eos_tokentext = "The Kraken dances flamenco in the Mediterranean sea"
tokens = tokenizer.tokenize(text)
ids = tokenizer.convert_tokens_to_ids(tokens)print("Text:", text)
print("Tokens:", tokens)
print("IDs:", ids)

运行脚本后,输出会是这样的:

Text: The Kraken dances flamenco in the Mediterranean sea
Tokens: ['The', 'ĠKr', 'aken', 'Ġdances', 'Ġflam', 'enco', 'Ġin', 'Ġthe', 'ĠMediterranean', 'Ġsea']
IDs: [791, 16852, 3448, 73806, 74599, 74485, 304, 279, 38785, 9581]

原来 “flamenco” 被拆成了两个连续的 Token:Ġflam和enco。这种拆分其实很有用 —— 比如在不同语境下,Ġflam后面可能会接ant,组成 “flamant”(这不是指昆虫 “蚂蚁”,而是 “flamant” 本身就是一个完整单词)。​

对了,那个Ġ符号是干嘛的?其实它只是用来标记 “这个 Token 前面有个空格”。这个设计很实用:如果一个 Token 没有Ġ,就说明它是前一个 Token(甚至前几个 Token)的延续,直到遇到带Ġ的 Token,才是一个新的独立单元。​

所以咱们的模型其实学对了 —— 它确实知道北海巨妖爱弗拉门戈!💃🐙💖

http://www.dtcms.com/a/365804.html

相关文章:

  • 头歌实训作业答案C++ 01
  • Proteus8 + STM32CubeMX 实现 STM32F103R6 串口通信教程
  • JMeter下载安装及使用入门
  • 常用符号 Emoji 对照表——Unicode UTF-8
  • SQLSERVER临时表
  • 关于专业化与多元化该怎么选?
  • 解决MQ访问不了或者登录不成功问题
  • 卷积神经网络CNN-part2-简单的CNN
  • TypeScript与JavaScript:从动态少年到稳重青年的成长之路
  • RabbitMQ相关知识
  • HTML第七课:发展史
  • Unity:XML笔记(二)——Xml序列化、反序列化、IXmlSerializable接口
  • 裸机程序(1)
  • 【ARM嵌入式汇编基础】-数据处理指令(三)
  • 低成本低功耗认证芯片推荐——LCS4110R
  • 【Luogu】P2398 GCD SUM (容斥原理求gcd为k的数对个数)
  • 鸿蒙NEXT开发实战:图片显示、几何图形与自定义绘制详解
  • GPT4o 提示词 结合 NanoBanbana 会摩擦出什么火花呢?
  • FPGA笔试面试常考问题及答案汇总
  • 入行FPGA选择国企、私企还是外企?
  • 案例演示 切片器悬浮永驻 Power BI VS QuickBI ,不得不说,两个极端了
  • 华勤内推码
  • 智慧交通管理信号灯通信4G工业路由器应用
  • 【机器学习深度学习】LLM:在检索与重排序中的适用场景
  • PS更改图像尺寸
  • 心路历程-初识Linux用户
  • 于海斌、王耀南、张钹三位院士解读具身智能
  • 数据结构与算法-线性表
  • C++零基础第一天:从Hello World到变量常量
  • 【JAVA】windows本地跑zookeeper,然后使用代码连接服务获取znode数据