DataWhale Task02:从零预训练一个tiny-llama 20923
原文link:https://github.com/KMnO4-zx/tiny-llm
开源内容:https://github.com/datawhalechina/tiny-universe
写在前面!!:因为没有编译环境,这期主要讲代码的理解
首先,安装python脚本,安装依赖库:
pip install -r requirements.txt
训练步骤:
训练Tokenizer:python train_vocab.py --download True --vocab_size 4096
数据预处理:python preprocess.py
训练模型:python train.py
使用模型生成文本:python sample.py --prompt "One day, Lily met a Shoggoth"
具体步骤:
1.训练Tokenizer:
在自然语言处理(NLP)中,Tokenizer(分词器)是将输入文本分割成更小单元(通常称为token)的工具或方法。Token可以是词、子词、字符、甚至是标点符号,具体取决于所使用的分词方法。Tokenizer 的主要作用是将原始文本转换成适合模型处理的形式。
以下是常见的两种分词方法:
词级别分词(Word-level tokenization):直接按词进行分割,通常基于空格或标点符号。例如:
输入:“I love programming.” 输出:["I", "love", "programming", "."]
子词级别分词(Subword-level tokenization):将单词进一步拆解成更小的子单元,常见于一些基于子词的模型(如BPE或WordPiece)。这对处理未登录词(out-of-vocabulary words)尤其有效。
输入:“programming” 输出:["pro", "gram", "ming"]
Tokenizer 通常是语言模型的前处理步骤,用于将自然语言文本转换为可以输入模型的数值表示。
下载数据集并训练:
python train_vocab.py --download True --vocab_size 4096
tokenizer.py
def download_file(url: str, fname: str, chunk_size=1024):
"""发送HTTP GET请求以流式方式获取文件"""
···
def download():
"""执行 download_file 下载数据集"""
···
def train_vocab(vocab_size: int=32000, num_shards: int=20):
"""
vocab_size: int, 词汇表的大小,决定分词器的词汇量。
num_shards: int, 用于加快词汇表训练的效率,指定要处理的分片数量。
"""
# 确保词汇表大小为正数
assert vocab_size > 0, "Vocab size must be positive"
# SentencePiece 模型的前缀路径,将用于保存分词器
prefix = os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}")
# 1) 将多个分片中的文本导出为单个文本文件 tiny.txt
tiny_file = os.path.join(DATA_CACHE_DIR, "tiny.txt")
data_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data")
shard_filenames = sorted(glob.glob(os.path.join(data_dir, "*.json")))
# 创建 tiny.txt 文件并写入指定数量的分片中的文本
print(f"Writing temporary file {tiny_file} with {num_shards} shards...")
with open(tiny_file, "w", encoding="utf-8") as of:
# 遍历前 num_shards 个分片
for shard in tqdm(shard_filenames[:num_shards]):
with open(shard, "r") as f:
data = json.load(f) # 读取分片中的JSON数据
# 遍历每个例子,将其中的故事文本写入 tiny.txt 文件
for example in data:
text = example["story"]
text = text.strip() # 去除文本首尾的空白字符
of.write(text + "\n") # 每个文本写入一行
# 输出生成的 tiny.txt 文件的大小
print(f"Size is: {os.path.getsize(tiny_file) / 1024 / 1024:.2f} MB")
# 2) 使用 SentencePiece 训练分词器
print("Will now train the vocab...")
spm.SentencePieceTrainer.train(
input=tiny_file, # 输入文件为之前生成的 tiny.txt
model_prefix=prefix, # 模型前缀路径
model_type="bpe", # 使用 Byte-Pair Encoding (BPE) 训练分词器
vocab_size=vocab_size, # 词汇表大小
self_test_sample_size=0, # 自测样本大小设置为 0
input_format="text", # 输入文件格式为纯文本
character_coverage=1.0, # 覆盖所有字符(包括非常见字符)
num_threads=os.cpu_count(), # 使用 CPU 的线程数
split_digits=True, # 拆分数字
allow_whitespace_only_pieces=True, # 允许仅由空格组成的词元
byte_fallback=True, # 启用字节级回退
unk_surface=r" \342\201\207 ", # UNK token 表示未知字符的方式
normalization_rule_name="identity" # 使用“identity”归一化规则
)
# 3) 可选的清理操作,询问用户是否删除临时文件 tiny.txt
dec = input(f"Delete the temporary file {tiny_file}? [y/N] ")
if dec.lower() == "y":
os.remove(tiny_file) # 删除临时文件
print(f"Deleted {tiny_file}")
# 输出模型保存的路径
print(f"Trained tokenizer is in {prefix}.model")
print("Done.")
代码理解:
主要使用了 SentencePiece 库来生成基于 BPE(Byte-Pair Encoding)的方法。训练过程由多个步骤构成,涉及从数据集中读取文本、生成临时文件、训练词汇表并最终保存模型。
train_vocab概述:这个函数的目标是:
从多个 JSON 文件中提取文本数据。 将这些数据写入一个临时文件(tiny.txt
)。
使用 SentencePiece 对提取的文本进行分词器的训练。
最终根据训练设置生成一个词汇表大小为 vocab_size
的分词器模型。
vocab_size
: 定义训练出的分词器的词汇表大小(即模型中包含多少个词元)。
num_shards
: 指定从数据集中读取的分片数,用于加速处理多个数据分片
代码详解:
assert vocab_size > 0, "Vocab size must be positive"
确保词汇表大小为整数,用来确保 vocab_size
是大于 0 的有效值,如果不满足条件则抛出错误
prefix = os.path.join(DATA_CACHE_DIR, f"tok{vocab_size}")
设置模型的路径前缀,将词汇表训练的模型路径设置为 DATA_CACHE_DIR
目录下的 tok{vocab_size}
。训练后的模型将保存为 {prefix}.model
文件。
tiny_file = os.path.join(DATA_CACHE_DIR, "tiny.txt") data_dir = os.path.join(DATA_CACHE_DIR, "TinyStories_all_data") shard_filenames = sorted(glob.glob(os.path.join(data_dir, "*.json")))
将数据集的多个 JSON 文件合并成一个 tiny.txt
文件,
data_dir
是数据集所在的目录,代码会读取该目录下的所有 JSON 文件。
shard_filenames
是读取到的 JSON 文件路径列表,按文件名排序。
with open(tiny_file, "w", encoding="utf-8") as of:
for shard in tqdm(shard_filenames[:num_shards]):
with open(shard, "r") as f:
data = json.load(f)
for example in data:
text = example["story"]
text = text.strip()
of.write(text + "\n")
打开 tiny.txt
作为写入的目标文件。
遍历前 num_shards
个 JSON 文件,每个文件包含许多 JSON 格式的“故事”。
对每个 JSON 文件中的每个“故事”进行处理,将其中的 story
字段文本提取出来并写入 tiny.txt
。
print(f"Size is: {os.path.getsize(tiny_file) / 1024 / 1024:.2f} MB")
输入生成文件(tiny.txt)的大小
spm.SentencePieceTrainer.train( input=tiny_file, model_prefix=prefix, model_type="bpe", vocab_size=vocab_size, ... )
使用 SentencePiece 训练分词器
input=tiny_file
: 使用之前生成的 tiny.txt
文件作为训练输入。
model_prefix=prefix
: 模型文件将以 prefix
为前缀保存。
model_type="bpe"
: 采用 BPE(Byte-Pair Encoding)算法训练分词器。
vocab_size
: 指定词汇表的大小。
其他参数用于控制分词器训练时的一些细节,例如字符覆盖率、线程数、是否拆分数字等。
可选的清理步骤,训练完成后,询问用户是否删除临时文件 tiny.txt
,如果用户输入 y
则删除该文件。
print(f"Trained tokenizer is in {prefix}.model")
输出模型保存路径,模型文件会保存为 {prefix}.model
,这行代码输出模型保存的路径。
展示如何使用该类来处理 TinyStory
数据集中的故事文本
class Tokenizer:
def __init__(self, tokenizer_model=None):
"""
初始化分词器。加载预训练的SentencePiece模型,并设置一些特殊的token ID。
class Tokenizer:
def __init__(self, tokenizer_model=None):
"""
初始化分词器。加载预训练的SentencePiece模型,并设置一些特殊的token ID。
参数:
tokenizer_model: str, 可选,分词器模型的路径,如果不指定则使用默认路径 TOKENIZER_MODEL。
"""
# 如果提供了分词器模型路径,使用该路径;否则使用默认模型路径
model_path = tokenizer_model if tokenizer_model else TOKENIZER_MODEL
# 确保模型文件存在
assert os.path.isfile(model_path), model_path
# 加载 SentencePiece 模型
self.sp_model = SentencePieceProcessor(model_file=model_path)
self.model_path = model_path
# 获取分词器的特殊token和词汇表大小
self.n_words: int = self.sp_model.vocab_size() # 词汇表大小
self.bos_id: int = self.sp_model.bos_id() # 句子开头 (BOS) 的ID
self.eos_id: int = self.sp_model.eos_id() # 句子结尾 (EOS) 的ID
self.pad_id: int = self.sp_model.pad_id() # 填充 (PAD) 的ID
# 验证分词器词汇表大小是否正确
assert self.sp_model.vocab_size() == self.sp_model.get_piece_size()
def encode(self, s: str, bos: bool, eos: bool) -> List[int]:
"""
将字符串编码为词元ID列表。可以选择是否添加句子开头 (BOS) 和句子结尾 (EOS) 标记。
参数:
s: str, 要编码的字符串。
bos: bool, 是否在编码的词元列表前添加 BOS 标记。
eos: bool, 是否在编码的词元列表末尾添加 EOS 标记。
返回:
List[int]: 编码后的词元ID列表。
"""
# 确保输入是字符串类型
assert type(s) is str
# 使用SentencePiece将字符串编码为词元ID
t = self.sp_model.encode(s)
# 如果需要BOS标记,将其添加到词元列表开头
if bos:
t = [self.bos_id] + t
# 如果需要EOS标记,将其添加到词元列表末尾
if eos:
t = t + [self.eos_id]
return t
def decode(self, t: List[int]) -> str:
"""
将词元ID列表解码为字符串。
参数:
t: List[int], 词元ID列表。
返回:
str: 解码后的字符串。s
"""
return self.sp_model.decode(t)
其中:
1.__init__方法
tokenizer_model: 这是一个可选参数,用于指定分词器模型的路径。如果未提供,则使用默认路径
TOKENIZER_MODEL
model_path = tokenizer_model if tokenizer_model else TOKENIZER_MODEL
assert os.path.isfile(model_path), model_path
首先,检查是否提供了自定义的分词器模型路径。如果没有,就使用默认路径 TOKENIZER_MODEL
。
使用 assert
语句确保指定路径的模型文件存在。如果文件不存在,抛出错误并输出文件路径。
self.sp_model = SentencePieceProcessor(model_file=model_path)
self.model_path = model_path
首先,检查是否提供了自定义的分词器模型路径。如果没有,就使用默认路径 TOKENIZER_MODEL
。
使用 assert
语句确保指定路径的模型文件存在。如果文件不存在,抛出错误并输出文件路径。
self.sp_model = SentencePieceProcessor(model_file=model_path)
self.model_path = model_path
SentencePieceProcessor
是 SentencePiece 的核心处理器,负责加载模型。model_file=model_path
使得处理器加载指定路径下的分词器模型。
self.n_words: int = self.sp_model.vocab_size()
self.bos_id: int = self.sp_model.bos_id()
self.eos_id: int = self.sp_model.eos_id()
self.pad_id: int = self.sp_model.pad_id()
n_words
: 模型的词汇表大小(即分词器中包含多少个词元)。
bos_id
: 句子开头的特殊标记 BOS
的 ID。
eos_id
: 句子结尾的特殊标记 EOS
的 ID。
pad_id
: 填充标记 PAD
的 ID。
assert self.sp_model.vocab_size() == self.sp_model.get_piece_size()
通过断言确保 vocab_size()
和 get_piece_size()
的结果一致,验证分词器的词汇表大小是正确的。
2.encode方法
encode
方法用于将输入字符串编码为词元ID列表,同时可以选择是否在结果中添加 BOS
和 EOS
标记。
s
: 要编码的字符串。
bos
: 是否在词元列表开头添加 BOS
标记。
eos
: 是否在词元列表末尾添加 EOS
标记。
assert type(s) is str
t = self.sp_model.encode(s)
首先,检查 s
是否为字符串类型。
使用 SentencePiece
的 encode
方法将字符串编码为词元ID列表。
if
bos:
t = [self.bos_id] + t
if eos:
t = t + [self.eos_id]
bos
参数为真,则在词元列表开头插入 BOS
标记。
如果 eos
参数为真,则在词元列表末尾添加 EOS
标记。
最后,返回处理后的词元ID列表。
3.decode方法
decode
方法用于将词元ID列表解码回原始字符串。
t
: 词元ID的列表。
return self.sp_model.decode(t)
直接使用 SentencePiece
的 decode
方法将 ID 列表解码回对应的文本字符串。
2.数据预处理
详细代码的分析,具体代码见github。
将文本数据转换为模型能够理解的数字序列
该代码实现了分词数据的预处理、加载和批处理,适用于大规模语言模型的训练任务。它主要分为以下几部分:
1. 分片处理函数 process_shard
和预处理函数 pretokenize
这两个函数负责将文本数据进行分词处理并保存为二进制文件。尽管函数体未展示出来,但从函数签名可以推测:
process_shard
负责处理单个数据分片,将其中的文本数据分词并存储为 .bin
文件。
pretokenize
负责对所有分片进行批量处理,调用 process_shard
对每个分片进行分词预处理。
2. PretokDataset
类
该类继承自 torch.utils.data.IterableDataset
,用于从磁盘加载预处理好的分词数据并将其提供给 PyTorch 模型训练。
核心功能:
初始化数据集:通过 __init__
方法,指定数据集的划分方式(训练集或测试集)、最大序列长度、词汇表大小以及词汇表来源(如 Llama2 或自定义词汇表)。
加载分片文件:在 __iter__
方法中:
vocab_source
决定要加载的分片文件路径。
对于训练集,加载所有分片;对于测试集,加载第一个分片。
批量生成:通过 memmap
方式读取二进制文件中的数据,确保大文件可以从磁盘加载而不完全占用内存。每个分片文件被分割为多个批次,max_seq_len
决定了每个批次的长度。
x
是当前批次的前 max_seq_len
个词元。
模型输出 y
是对应下一个词元,用于构建语言模型的监督学习任务。
其他功能:
随机性:通过 worker_info
和 rank
实现数据的并行加载和分布式训练。在数据加载过程中,基于 worker_id
和 rank
创建随机数种子,确保不同进程、线程之间的数据处理是唯一的,不会重复。
数据生成:__iter__
方法生成 (x, y)
对,供 PyTorch 模型训练。
3. Task
类
Task
类封装了数据集的批处理流程,并为外部调用提供了一个静态方法 iter_batches
,用于迭代生成批次数据。
核心功能:
批量加载数据:使用torch.utils.data.DataLoader
创建批次迭代器,将 PretokDataset
的输出打包为固定大小的批次。
数据迁移到设备:在每个批次中,将数据移动到指定的计算设备(如 GPU),并使用 non_blocking=True
以加快数据拷贝的速度。
其他功能:
支持并行加载:可以通过num_workers
参数指定数据加载器的并行线程数,以加速数据加载过程。
3: 训练模型
这个 generate
方法实现了一个基于语言模型的文本生成过程,逐步生成新 token 并将其附加到现有序列 idx
中。该方法可以通过多次前向传播和采样来生成新序列,使用了温度和 top_k
策略来控制生成过程。
关键流程解释:
初始输入 idx
:
idx
是形状为 (bz, seq_len)
的长整型张量,表示输入序列。
bz
是 batch size,seq_len
是序列的长度。
序列长度控制:
当输入序列长度超过模型的最大序列长度self.args.max_seq_len
时,序列会被截断,只保留最后的 max_seq_len
个 token(即上下文)。
这保证了模型的输入不会超过它的上下文窗口大小。
前向传播:
对于当前输入序列idx_cond
,模型进行前向传播,输出预测 logits。logits[:, -1, :]
表示我们只取最后一个 token 的输出,因为这是当前生成 token 的预测概率分布。
生成新 token:
确定性采样(当temperature == 0.0
时):直接选择概率最高的下一个 token(torch.topk(logits, k=1)
)。
随机采样(当 temperature > 0
时):先将 logits 按 temperature
缩放,然后进行采样。temperature
值越高,生成的文本越随机;值越低,生成的文本越确定。
top_k
采样:如果指定了 top_k
,只从 top k 个最可能的 token 中进行采样,这可以减少生成的随机性,避免模型生成一些不合理的 token。
Softmax 和 Multinomial:F.softmax
将 logits 转换为概率分布,torch.multinomial
根据该分布采样出下一个 token。
更新序列:
将新生成的 token(idx_next
)附加到现有序列 idx
后面,并继续迭代直到生成所需的 token 数量。
返回生成的序列:
最终返回更新后的idx
,它包含初始输入序列和新生成的 token。
参数:
idx
: 输入的 token 序列,形状为 (batch_size, seq_len)
。
max_new_tokens
: 要生成的 token 数量。
temperature
: 控制采样随机性的参数,值越低生成越确定,越高生成越随机。
top_k
: 限制从 top k 个 token 中进行采样,值较低时可提高生成质量。
代码示例的逻辑流:
每次生成时,输入序列idx
截取为最大允许的长度。
通过模型获取序列最后一个位置的 logits。
根据温度和 top_k
参数选择下一个 token。
将新生成的 token 加入到序列,重复生成直到达到 max_new_tokens
。
这个方法主要用于简单的文本生成任务,适合在推理模式下运行。
4: 使用模型生成文本
在模型训练完成后,会在output
目录下生成一个ckpt.pt
文件,这个文件就是我们训练好的模型。我们可以使用以下命令生成文本。
python sample.py --prompt "One day, Lily met a Shoggoth"
sample.py代码详解:
class TextGenerator:
def __init__(self,
checkpoint='output/ckpt.pt', # 模型检查点路径
tokenizer_model_path='tok4096.model', # 分词器模型路径
seed=1337, # 随机种子,确保可重复性
device=None, # 设备,优先使用 CUDA,如果没有可用的 CUDA,则使用 CPU
dtype="float32"): # 数据类型,默认为 float32,可以选择 float16 或 bfloat16
"""
初始化 TextGenerator 类,加载模型、设置设备和分词器等。
"""
#模型加载配置
self.checkpoint = checkpoint # 保存的模型检查点路径
self.tokenizer_model_path = tokenizer_model_path # 分词器模型文件路径
self.seed = seed # 随机数种子,用于生成的可重复性
self.device = device or ('cuda' if torch.cuda.is_available() else 'cpu') # 根据硬件条件选择设备
self.dtype = dtype # 模型的浮点数类型
self.device_type = 'cuda' if 'cuda' in self.device else 'cpu' # 判断当前设备是否为 CUDA
# 设置随机种子,确保生成的可重复性
torch.manual_seed(seed) # 设置 CPU 随机种子
torch.cuda.manual_seed(seed) # 设置 CUDA 随机种子
torch.backends.cuda.matmul.allow_tf32 = True # 允许 CUDA 使用 TF32 精度进行矩阵乘法运算
torch.backends.cudnn.allow_tf32 = True # 允许 cuDNN 使用 TF32 精度加速
# 根据 dtype 选择适当的自动混合精度上下文
ptdtype = {'float32': torch.float32, 'bfloat16': torch.bfloat16, 'float16': torch.float16}[self.dtype]
self.ctx = nullcontext() if self.device_type == 'cpu' else torch.amp.autocast(device_type=self.device_type, dtype=ptdtype)
# 加载模型检查点文件
checkpoint_dict = torch.load(self.checkpoint, map_location=self.device) # 加载模型参数
gptconf = ModelArgs(**checkpoint_dict['model_args']) # 初始化模型参数
self.model = Transformer(gptconf) # 实例化 Transformer 模型
state_dict = checkpoint_dict['model'] # 获取模型状态字典
# 去除状态字典中的不必要前缀
unwanted_prefix = '_orig_mod.' # 这个前缀在保存时可能被添加,现在要去除它
for k, v in list(state_dict.items()):
if k.startswith(unwanted_prefix):
state_dict[k[len(unwanted_prefix):]] = state_dict.pop(k) # 去除不必要的前缀
# 加载模型参数到模型中
self.model.load_state_dict(state_dict, strict=False)
# 计算模型参数量
num_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
print(f"Model has {num_params} parameters.")
# 设置模型为评估模式(evaluation mode),防止训练模式下的 dropout 等操作影响结果
self.model.eval()
# 将模型放置到正确的设备上(GPU 或 CPU)
self.model.to(self.device)
# 初始化分词器
self.tokenizer = Tokenizer(tokenizer_model=self.tokenizer_model_path) # 根据指定的路径加载分词器
def sample(self,
start="Hello!", # 生成文本的起始提示词,可以是任意字符串
num_samples=3, # 生成样本的数量,默认生成 3 个样本
max_new_tokens=256, # 每个样本生成的最大 token 数,默认最多生成 256 个 token
temperature=1.0, # 控制生成的随机性,1.0 为标准,值越大越随机
top_k=300): # 保留概率最高的 top_k 个 token,限制生成时的选择范围
"""
根据给定的起始文本生成样本。
:param start: 生成文本的起始提示词
:param num_samples: 要生成的文本样本数
:param max_new_tokens: 每个样本生成的最大 token 数
:param temperature: 控制生成的随机性,值越小生成越确定,值越大生成越随机
:param top_k: 限制生成时选择的 token 范围
:return: 生成的文本样本列表
"""
# 如果 start 是以 'FILE:' 开头,表示从文件中读取起始文本
if start.startswith('FILE:'):
with open(start[5:], 'r', encoding='utf-8') as f:
start = f.read() # 读取文件内容作为起始文本
# 将起始文本编码为 token id 序列
start_ids = self.tokenizer.encode(start, bos=True, eos=False) # bos=True 表示加上句首标记,eos=False 表示不加句尾标记
x = (torch.tensor(start_ids, dtype=torch.long, device=self.device)[None, ...]) # 将编码后的 token id 转为 PyTorch 张量
generated_texts = [] # 用于保存生成的文本样本
with torch.no_grad(): # 禁用梯度计算,提升效率
with self.ctx: # 进入自动混合精度的上下文(如果是 GPU 并使用 float16 时)
for k in range(num_samples): # 循环生成指定数量的样本
y = self.model.generate(x, max_new_tokens, temperature=temperature, top_k=top_k) # 生成文本
generated_texts.append(self.tokenizer.decode(y[0].tolist())) # 解码生成的 token 序列为可读文本
return generated_texts # 返回生成的文本样本
实现了一个 TextGenerator
类,用于加载预训练语言模型,并根据输入提示生成文本样本。下面是对各个部分的详细解释:
1. 初始化 (__init__
方法)
__init__
方法用于加载模型、设置设备(如 CPU 或 CUDA)、配置分词器和初始化随机种子,以确保生成的文本可重复。
参数:
checkpoint
: 模型的检查点文件路径,包含模型的权重和配置。
tokenizer_model_path
: 分词器模型的路径,负责将文本转化为 token 序列。
seed
: 随机种子,确保每次生成的文本可重复。
device
: 选择使用的计算设备,优先选择 CUDA,如果没有可用的 CUDA,则使用 CPU。
dtype
: 数据类型,默认为 float32
,也可以选择 float16
或 bfloat16
来加速计算。
主要步骤:
设置随机种子:通过torch.manual_seed
和 torch.cuda.manual_seed
确保 CPU 和 CUDA 设备上的随机性是可控的。
设备配置:如果可用,则使用 CUDA,否则使用 CPU;设置自动混合精度(torch.amp.autocast
)来提高生成效率。
加载模型检查点:从指定的 checkpoint
路径加载预训练模型,去除状态字典中的不必要前缀(如 _orig_mod.
),然后将模型权重加载到模型中。
评估模式:设置模型为评估模式(self.model.eval()
),禁用掉训练过程中使用的 dropout 等机制,以确保生成结果的稳定性。
分词器初始化:加载分词器,用于将文本转化为 token,并将生成的 token 序列解码回可读文本。
2. 生成文本 (sample
方法)
sample
方法负责根据给定的起始文本生成指定数量的样本,返回的是生成的文本列表。
参数:
start
: 生成文本的起始提示词,可以是任意字符串,也可以是文件路径(以 FILE:
开头)。
num_samples
: 要生成的文本样本数量,默认生成 3 个样本。
max_new_tokens
: 每个样本生成的最大 token 数量,默认值为 256。
temperature
: 控制生成文本的随机性,值越高越随机;值越低生成的文本越接近模型的确定性预测。
top_k
: 限制生成时的选择范围,只保留概率最高的 top k 个 token,这样可以避免生成低概率但可能不合理的 token。
主要步骤:
文件输入支持:如果start
参数以 FILE:
开头,表示从指定的文件中读取起始文本作为生成提示词。
分词器编码:将起始文本编码为 token id 序列,添加句首标记(bos=True
),生成的序列作为模型的初始输入。
生成样本:在禁用梯度计算(torch.no_grad()
)的上下文中,使用模型的 generate
方法生成指定数量的文本样本。
解码:将生成的 token 序列通过分词器解码成可读的文本,并保存到列表中。
3. 模型生成逻辑
在 sample
方法中,调用 self.model.generate
执行生成逻辑。该方法是之前定义的,用于根据给定的输入序列逐步生成新 token,并支持多种采样策略(如 temperature 和 top_k 采样)。生成的 token 序列通过分词器解码回文本后返回。
总结