从头开始训练因果语言模型
到目前为止,我们主要使用预训练模型,并通过重用预训练的权重来针对新用例对它们进行微调。正如我们在第一章, 这通常称为迁移学习,这是将 Transformer 模型应用于大多数标记数据稀疏的现实世界用例的非常成功的策略。在本章中,我们将采用不同的方法并从头开始训练一个全新的模型。如果您有大量数据并且它与用于可用模型的预训练数据有很大不同,那么这是一个很好的方法。然而,它也需要更多的计算资源来预训练语言模型,而不仅仅是微调现有的模型。训练新模型有意义的示例包括由音符、分子序列(如 DNA)或编程语言组成的数据集。后者最近受到关注,这要归功于 TabNine 和 GitHub 的 Copilot 等工具,它们由 OpenAI 的 Codex 模型提供支持,可以生成长代码序列。这种文本生成任务最好使用自回归或因果语言模型(例如 GPT-2)来解决。
在本节中,我们将构建代码生成模型的缩小版本:我们将使用 Python 代码的子集专注于单行完成而不是完整的函数或类。在 Python 中处理数据时,您会经常接触 Python 数据科学堆栈,包括 matplotlib
, seaborn
, pandas
, 和 scikit-learn
库。在使用这些框架时,通常需要查找特定的命令,因此如果我们可以使用模型来为我们完成这些调用,那就太好了。
在第六章 我们创建了一个高效的分词器来处理 Python 源代码,但我们仍然需要一个大规模数据集来预训练模型。在这里,我们将我们的分词器应用到源自 GitHub 存储库的 Python 代码语料库。然后我们将使用 Trainer
API 和 🤗 Accelerate 来训练模型。让我们开始吧!
这实际上展示了使用本节中训练并上传到 Hub 的模型。你可以在这里找到。请注意,由于在文本生成过程中发生了一些随机化,您可能会得到略有不同的结果。
收集数据
Python 代码可以从 GitHub 等代码存储库中获得,我们可以通过抓取每个 Python 存储库来使用它们来创建数据集。这是在Transformers textbook预训练大型的GPT-2 模型。使用大约 180 GB 的 GitHub 转储,其中包含大约 2000 万个 Python 文件,称为 codeparrot
,作者构建了一个数据集,然后在Hugging Face Hub上分享出来了.
然而,对完整语料库的训练既耗时又费力,我们只需要与 Python 数据科学堆栈相关的数据集子集。所以,让我们开始过滤 codeparrot
包含此堆栈中任何库的所有文件的数据集。由于数据集的太大,我们希望避免下载它;因此反,我们将使用流功能来动态过滤它。为了使用前面提到的库过滤代码示例,我们将使用以下函数:
def any_keyword_in_string(string, keywords):
for keyword in keywords:
if keyword in string:
return True
return False
让我们用两个例子来测试一下:
filters = ["pandas", "sklearn", "matplotlib", "seaborn"]
example_1 = "import numpy as np"
example_2 = "import pandas as pd"
print(
any_keyword_in_string(example_1, filters), any_keyword_in_string(example_2, filters)
)
False True
我们可以使用它来创建一个函数来流式传输数据集并过滤我们想要的元素:
from collections import defaultdict
from tqdm import tqdm
from datasets import Dataset
def filter_streaming_dataset(dataset, filters):
filtered_dict = defaultdict(list)
total = 0
for sample in tqdm(iter(dataset)):
total += 1
if any_keyword_in_string(sample["content"], filters):
for k, v in sample.items():
filtered_dict[k].append(v)
print(f"{len(filtered_dict['content'])/total:.2%} of data after filtering.")
return Dataset.from_dict(filtered_dict)
然后我们可以简单地将此函数应用于流数据集:
# This cell will take a very long time to execute, so you should skip it and go to
# the next one!
from datasets import load_dataset
split = "train" # "valid"
filters = ["pandas", "sklearn", "matplotlib", "seaborn"]
data = load_dataset(f"transformersbook/codeparrot-{split}", split=split, streaming=True)
filtered_data = filter_streaming_dataset(data, filters)
3.26% of data after filtering.
这给我们留下了大约 3% 的原始数据集,这个数据集仍然相当可观——结果数据集有 6 GB,包含 600,000 个 Python 脚本!过滤完整数据集可能需要 2-3 小时,具体取决于您的机器和带宽。如果您不想自己经历这个漫长的过程,我们在 Hub 上提供过滤后的数据集供您下载:
from datasets import load_dataset, DatasetDict
ds_train = load_dataset("huggingface-course/codeparrot-ds-train", split="train")
ds_valid = load_dataset("huggingface-course/codeparrot-ds-valid", split="validation")
raw_datasets = DatasetDict(
{
"train": ds_train, # .shuffle().select(range(50000)),
"valid": ds_valid, # .shuffle().select(range(500))
}
)
raw_datasets
DatasetDict({
train: Dataset({
features: ['repo_name', 'path', 'copies', 'size', 'content', 'license'],
num_rows: 606720
})
valid: Dataset({
features: ['repo_name', 'path', 'copies', 'size', 'content', 'license'],
num_rows: 3322
})
})
预训练语言模型需要一段时间。我们建议您首先通过取消注释以上两行的注释对数据样本运行训练循环,并确保训练成功完成并存储模型。没有什么比最后一步的训练失败更令人沮丧的了,因为你忘记创建一个文件夹或者因为保存路径在训练循环结束时有一个错字!
让我们看一个来自数据集的例子。我们将只显示每个字段的前 200 个字符:
for key in raw_datasets["train"][0]:
print(f"{key.upper()}: {raw_datasets['train'][0][key][:200]}")
'REPO_NAME: kmike/scikit-learn'
'PATH: sklearn/utils/__init__.py'
'COPIES: 3'
'SIZE: 10094'
'''CONTENT: """
The :mod:`sklearn.utils` module includes various utilites.
"""
from collections import Sequence
import numpy as np
from scipy.sparse import issparse
import warnings
from .murmurhash import murm
LICENSE: bsd-3-clause'''
我们可以看到 content
字段包含我们希望我们的模型训练的代码。现在我们有了一个数据集,我们需要预处理文本,使其采用适合预训练的格式。
准备数据集
第一步是对数据进行标记,以便我们可以将其用于训练。由于我们的目标主要是自动完成短函数调用,因此我们可以保持上下文大小相对较小。这样做的好处是我们可以更快地训练模型并且它需要的内存显着减少。如果您的应用程序拥有更多上下文很重要(例如,如果您希望模型基于具有函数定义的文件编写单元测试),请确保增加该数量,但请记住,这需要更大的 GPU 内存占用。现在,让我们将上下文大小固定为 128 个标记,而不是 GPT-2 或 GPT-3 中分别使用的 1,024 或 2,048 个标记。
大多数文档包含超过 128 个标记,因此简单地将输入截断到最大长度将消除我们数据集的很大一部分。相反,我们将使用 return_overflowing_tokens
标记整个输入并将其分成几个块的选项,就像我们在第六章. 我们还将使用 return_length
选项自动返回每个创建的块的长度。通常最后一个块会小于上下文大小,我们会去掉这些块以避免填充问题;因为无论如何我们都有大量数据。
让我们通过查看前两个示例来确切了解这是如何工作的:
from transformers import AutoTokenizer
context_length = 128
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")
outputs = tokenizer(
raw_datasets["train"][:2]["content"],
truncation=True,
max_length=context_length,
return_overflowing_tokens=True,
return_length=True,
)
print(f"Input IDs length: {len(outputs['input_ids'])}")
print(f"Input chunk lengths: {(outputs['length'])}")
print(f"Chunk mapping: {outputs['overflow_to_sample_mapping']}")
Input IDs length: 34
Input chunk lengths: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 117, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 41]
Chunk mapping: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
我们可以看 到,从这两个示例中我们总共得到了 34 个片段。查看块长度,我们可以看到两个文档末尾的块都少于 128 个标记(分别为 117 和 41)。这些仅代表我们拥有的数据集的一小部分,因此我们可以安全地将它们扔掉。通过 overflow_to_sample_mapping
字段,我们还可以重建哪些块属于哪些输入样本。
通过这个操作,我们使用了一个方便的🤗 Datasets 中的Dataset.map()
函数,就是不需要一对一的映射;正如我们在第三节,我们可以创建具有比输入批次更多或更少元素的批次。这在执行更改元素数量的数据增强或数据过滤等操作时非常有用。在我们的例子中,当将每个元素标记为指定上下文大小的块时,我们从每个文档中创建了许多样本。我们只需要确保删除现有的列,因为它们的大小存在冲突。如果我们想保留它们,我们可以适当地重复它们,并在Dataset.map()
调用中返回它们:
def tokenize(element):
outputs = tokenizer(
element["content"],
truncation=True,
max_length=context_length,
return_overflowing_tokens=True,
return_length=True,
)
input_batch = []
for length, input_ids in zip(outputs["length"], outputs["input_ids"]):
if length == context_length:
input_batch.append(input_ids)
return {"input_ids": input_batch}
tokenized_datasets = raw_datasets.map(
tokenize, batched=True, remove_columns=raw_datasets["train"].column_names
)
tokenized_datasets
DatasetDict({
train: Dataset({
features: ['input_ids'],
num_rows: 16702061
})
valid: Dataset({
features: ['input_ids'],
num_rows: 93164
})
})
我们现在有 1670 万个示例,每个示例有 128 个tokens ,总共相当于大约 21 亿个tokens 。作为参考,OpenAI 的 GPT-3 和 Codex 模型分别在 300 和 1000 亿个tokens 上训练,其中 Codex 模型从 GPT-3 检查点初始化。我们在本节中的目标不是与这些模型竞争,这些模型可以生成长而连贯的文本,而是创建一个缩小版本,为数据科学家提供快速自动完成功能。
现在我们已经准备好了数据集,让我们设置模型!
✏️ 试试看! 摆脱所有小于上下文大小的块在这里并不是什么大问题,因为我们使用的是小上下文窗口。随着上下文大小的增加(或者如果您有一个短文档语料库),被丢弃的块的比例也会增加。准备数据的更有效方法是将所有标记化的样本加入一个批次中,每个语料之间有一个eos_token_id
标记, 然后对连接的序列执行分块。作为练习,修改 tokenize()
函数以使用该方法。请注意,您需要设置truncation=False
和删除标记生成器中的其他参数以获取完整的标记 ID 序列。
初始化新模型
我们的第一步是新初始化一个 GPT-2 模型。我们将对我们的模型使用与小型 GPT-2 模型相同的配置,因此我们加载预训练配置,确保分词器大小与模型词汇量大小匹配并设置 bos
和 eos
(序列的开始和结束)令牌 ID:
from transformers import AutoTokenizer, GPT2LMHeadModel, AutoConfig
config = AutoConfig.from_pretrained(
"gpt2",
vocab_size=len(tokenizer),
n_ctx=context_length,
bos_token_id=tokenizer.bos_token_id,
eos_token_id=tokenizer.eos_token_id,
)
使用该配置,我们可以加载一个新模型。请注意,这是我们第一次不使用 from_pretrained()
函数,因为我们实际上是在自己初始化模型
model = GPT2LMHeadModel(config)
model_size = sum(t.numel() for t in model.parameters())
print(f"GPT-2 size: {model_size/1000**2:.1f}M parameters")
GPT-2 size: 124.2M parameters
我们的模型有 1.24 亿个参数,我们必须对其进行调整。在开始训练之前,我们需要设置一个负责创建批次的数据整理器。我们可以使用 DataCollatorForLanguageModeling
,它是专为语言建模而设计(顾名思义)。除了堆叠和填充批次,它还负责创建语言模型标签——在因果语言建模中,输入也用作标签(只是移动了一个元素),并且这个数据整理器在训练期间即时创建它们,所以我们不需要复制 input_ids
。
注意 DataCollatorForLanguageModeling
支持掩码语言建模 (MLM) 和因果语言建模 (CLM)。默认情况下它为 MLM 准备数据,但我们可以通过设置mlm=False
参数切换到 CLM :
from transformers import DataCollatorForLanguageModeling
tokenizer.pad_token = tokenizer.eos_token
data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)
让我们看一个例子:
out = data_collator([tokenized_dataset["train"][i] for i in range(5)])
for key in out:
print(f"{key} shape: {out[key].shape}")
input_ids shape: torch.Size([5, 128])
attention_mask shape: torch.Size([5, 128])
labels shape: torch.Size([5, 128])
我们可以看到示例已经堆叠在一起,并且所有张量都具有相同的形状。
⚠️ 移动输入和标签以对齐它们发生在模型内部,因此数据整理器只需复制输入以创建标签。
现在我们已经准备好实际训练我们的模型的一切了——毕竟这不是那么多工作!在我们开始训练之前,我们应该登录 Hugging Face。如果您在笔记本上工作,则可以使用以下实用程序功能:
from huggingface_hub import notebook_login
notebook_login()
这将显示一个小部件,您可以在其中输入您的 Hugging Face 登录凭据。
如果您不是在notebook上工作,只需在终端中输入以下行:
huggingface-cli login
剩下要做的就是配置训练参数并启动 Trainer
.我们将使用余弦学习率,并进行一些Warmup和有效批量大小为 256 ( per_device_train_batch_size
* gradient_accumulation_steps
)。当单个批次不适合内存时使用梯度累积,并通过多次向前/向后传递逐步建立梯度。当我们使用 🤗 Accelerate 创建训练循环时,我们将看到这一点。
from transformers import Trainer, TrainingArguments
args = TrainingArguments(
output_dir="codeparrot-ds",
per_device_train_batch_size=32,
per_device_eval_batch_size=32,
evaluation_strategy="steps",
eval_steps=5_000,
logging_steps=5_000,
gradient_accumulation_steps=8,
num_train_epochs=1,
weight_decay=0.1,
warmup_steps=1_000,
lr_scheduler_type="cosine",
learning_rate=5e-4,
save_steps=5_000,
fp16=True,
push_to_hub=True,
)
trainer = Trainer(
model=model,
tokenizer=tokenizer,
args=args,
data_collator=data_collator,
train_dataset=tokenized_datasets["train"],
eval_dataset=tokenized_datasets["valid"],
)
现在我们可以开始 Trainer
并等待训练完成。根据您是在整个训练集还是在训练集的一个子集上运行它,这将分别需要 20 或 2 个小时,因此请喝杯咖啡和一本好书来阅读!
trainer.train()
训练完成后,我们可以将模型和标记器推送到 Hub:
trainer.push_to_hub()
✏️ 试试看! 除了TrainingArguments
之外,我们只需要大约30行代码就可以从原始文本到训练GPT-2。 用你自己的数据集试试看,看看你能不能得到好的结果!
💡 如果您可以访问具有多个 GPU 的机器,请尝试在那里运行代码。 Trainer
自动管理多台机器,这可以极大地加快训练速度。
使用管道生成代码
现在是关键的部分:让我们看看经过训练的模型的实际效果如何!我们可以在日志中看到损失稳步下降,但为了让模型进行测试,让我们看看它在某些测试上的表现如何。为此,我们将模型包装在文本生成中的pipeline
,如果有可用的,我们会将它放在 GPU 上进行快速生成:
import torch
from transformers import pipeline
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
pipe = pipeline(
"text-generation", model="huggingface-course/codeparrot-ds", device=device
)
让我们从创建散点图的简单任务开始:
txt = """\
# create some data
x = np.random.randn(100)
y = np.random.randn(100)
# create scatter plot with x, y
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# create some data
x = np.random.randn(100)
y = np.random.randn(100)
# create scatter plot with x, y
plt.scatter(x, y)
# create scatter
结果看起来是正确的。它也适用于 pandas
类型?让我们看看我们是否使用两个数组可以创建一个 DataFrame
:
txt = """\
# create some data
x = np.random.randn(100)
y = np.random.randn(100)
# create dataframe from x and y
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# create some data
x = np.random.randn(100)
y = np.random.randn(100)
# create dataframe from x and y
df = pd.DataFrame({'x': x, 'y': y})
df.insert(0,'x', x)
for
很好,这是正确的答案——尽管它随后再次插入了列 x
。由于生成的token数量有限,以下 for
循环被切断。让我们看看我们是否可以做一些更复杂的事情并让模型帮助我们分组操作:
txt = """\
# dataframe with profession, income and name
df = pd.DataFrame({'profession': x, 'income':y, 'name': z})
# calculate the mean income per profession
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# dataframe with profession, income and name
df = pd.DataFrame({'profession': x, 'income':y, 'name': z})
# calculate the mean income per profession
profession = df.groupby(['profession']).mean()
# compute the
不错;这是正确的做法。最后,让我们看看我们是否也可以将其用于 scikit-learn
并建立一个随机森林模型:
txt = """
# import random forest regressor from scikit-learn
from sklearn.ensemble import RandomForestRegressor
# fit random forest model with 300 estimators on X, y:
"""
print(pipe(txt, num_return_sequences=1)[0]["generated_text"])
# import random forest regressor from scikit-learn
from sklearn.ensemble import RandomForestRegressor
# fit random forest model with 300 estimators on X, y:
rf = RandomForestRegressor(n_estimators=300, random_state=random_state, max_depth=3)
rf.fit(X, y)
rf
从这几个例子来看,模型似乎已经学习了 Python 数据科学堆栈的一些语法(当然,在将模型部署到现实世界之前,我们需要对其进行更全面的评估)。然而,有时需要对模型训练进行更多定制才能实现给定用例的必要性能。例如,如果我们想动态更新批量大小或有一个条件训练循环来即时跳过坏示例怎么办?一种选择是将 Trainer
并添加必要的更改,但有时从头开始编写训练循环会更简单。这就是🤗 Accelerate 的用武之地。
用 🤗 Accelerate 训练
我们已经看到了如何使用 Trainer
,这可以允许一些自定义。然而,有时我们想要完全控制训练循环,或者我们想要进行一些奇特的更改。在这种情况下 🤗 Accelerate 是一个不错的选择,在本节中,我们将逐步介绍使用它来训练我们的模型的步骤。为了让事情变得更有趣,我们还将在训练循环中添加一些修改。
由于我们主要对数据科学库的合理自动填充感兴趣,因此对更多使用这些库的训练样本给予更多权重是有意义的。我们可以通过使用关键字轻松识别这些示例,例如 plt
、pd
、sk
、fit
和predict
等关键字,我们可以很容易地识别这些示例,这些关键字是matplotlib最常用的导入名称。Pyplot
, pandas
和sklearn
以及后者的拟合/预测模式。如果这些都表示为单个标记,我们可以轻松检查它们是否出现在输入序列中。标记可能有一个空格前缀,因此我们还将在标记器词汇表中检查这些版本。为了验证它是否有效,我们将添加一个测试token ,该token 应拆分为多个tokens:
keytoken_ids = []
for keyword in [
"plt",
"pd",
"sk",
"fit",
"predict",
" plt",
" pd",
" sk",
" fit",
" predict",
"testtest",
]:
ids = tokenizer([keyword]).input_ids[0]
if len(ids) == 1:
keytoken_ids.append(ids[0])
else:
print(f"Keyword has not single token: {keyword}")
'Keyword has not single token: testtest'
太好了,这似乎很好用!我们现在可以编写一个自定义损失函数,它将输入序列、logits 和我们刚刚选择的关键标记作为输入。首先,我们需要对齐 logits 和输入:向右移动一个的输入序列形成标签,因为下一个标记是当前标记的标签。我们可以通过从输入序列的第二个标记开始标记来实现这一点,因为模型无论如何都不会对第一个标记进行预测。然后我们切断最后一个 logit,因为我们没有完整输入序列之后的标记的标签。有了这个,我们可以计算每个样本的损失并计算每个样本中所有关键字的出现次数。最后,我们使用出现次数作为权重计算所有样本的加权平均值。由于我们不想扔掉所有没有关键字的样本,我们将权重加1:
from torch.nn import CrossEntropyLoss
import torch
def keytoken_weighted_loss(inputs, logits, keytoken_ids, alpha=1.0):
# Shift so that tokens < n predict n
shift_labels = inputs[..., 1:].contiguous()
shift_logits = logits[..., :-1, :].contiguous()
# Calculate per-token loss
loss_fct = CrossEntropyLoss(reduce=False)
loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
# Resize and average loss per sample
loss_per_sample = loss.view(shift_logits.size(0), shift_logits.size(1)).mean(axis=1)
# Calculate and scale weighting
weights = torch.stack([(inputs == kt).float() for kt in keytoken_ids]).sum(
axis=[0, 2]
)
weights = alpha * (1.0 + weights)
# Calculate weighted average
weighted_loss = (loss_per_sample * weights).mean()
return weighted_loss
在我们开始使用这个很棒的新损失函数进行训练之前,我们需要准备一些东西:
- 我们需要数据加载器来批量加载数据。
- 我们需要设置权重衰减参数。
- 有时我们想要求值,因此将求值代码包装在一个函数中是有意义的。
让我们从数据加载器开始。我们只需要将数据集的格式设置为 "torch"
,然后我们可以将它传递给 PyTorch DataLoader
,同时设置适当的批量大小:
from torch.utils.data.dataloader import DataLoader
tokenized_dataset.set_format("torch")
train_dataloader = DataLoader(tokenized_dataset["train"], batch_size=32, shuffle=True)
eval_dataloader = DataLoader(tokenized_dataset["valid"], batch_size=32)
接下来,我们对参数进行分组,以便优化器知道哪些将获得额外的权重衰减。通常,所有偏差和 LayerNorm 权重项都不受此限制;以下我们如何做到这一点:
weight_decay = 0.1
def get_grouped_params(model, no_decay=["bias", "LayerNorm.weight"]):
params_with_wd, params_without_wd = [], []
for n, p in model.named_parameters():
if any(nd in n for nd in no_decay):
params_without_wd.append(p)
else:
params_with_wd.append(p)
return [
{"params": params_with_wd, "weight_decay": weight_decay},
{"params": params_without_wd, "weight_decay": 0.0},
]
由于我们希望在训练期间定期在验证集上评估模型,因此我们也为此编写一个函数。它只是运行评估数据加载器并收集跨进程的所有损失:
def evaluate():
model.eval()
losses = []
for step, batch in enumerate(eval_dataloader):
with torch.no_grad():
outputs = model(batch["input_ids"], labels=batch["input_ids"])
losses.append(accelerator.gather(outputs.loss))
loss = torch.mean(torch.cat(losses))
try:
perplexity = torch.exp(loss)
except OverflowError:
perplexity = float("inf")
return loss.item(), perplexity.item()
通过 evaluate()
函数我们定期可以获取损失值和perplexity。接下来,我们重新定义我们的模型以确保我们再次从头开始训练:
model = GPT2LMHeadModel(config)
然后我们可以定义我们的优化器,使用之前的函数来分割权重衰减的参数:
from torch.optim import AdamW
optimizer = AdamW(get_grouped_params(model), lr=5e-4)
现在让我们准备模型、优化器和数据加载器,以便我们可以开始训练:
from accelerate import Accelerator
accelerator = Accelerator(fp16=True)
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
model, optimizer, train_dataloader, eval_dataloader
)
🚨 如果您在 TPU 上进行训练,则需要将从上面的单元格开始的所有代码移动到专用的训练函数中。有关详细信息,请参阅 第 3 章 for more details.
现在我们已经发送了我们的 train_dataloader
到 accelerator.prepare()
,我们可以使用它的长度来计算训练步骤的数量。请记住,我们应该始终在准备好dataloader后执行此操作,因为该方法会改变其长度。我们使用经典线性学习率调度:
num_train_epochs = 1
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch
lr_scheduler = get_scheduler(
name="linear",
optimizer=optimizer,
num_warmup_steps=1_000,
num_training_steps=num_training_steps,
)
最后,要将我们的模型推送到 Hub,我们需要创建一个 Repository
工作文件夹中的对象。如果您尚未登录,请先登录 Hugging Face。我们将从我们想要为模型提供的模型 ID 中确定存储库名称(您可以自由地用自己的选择替换 repo_name
;它只需要包含您的用户名,可以使用get_full_repo_name()
函数的查看目前的repo_name):
from huggingface_hub import Repository, get_full_repo_name
model_name = "codeparrot-ds-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
'sgugger/codeparrot-ds-accelerate'
然后我们可以在本地文件夹中克隆该存储库。如果它已经存在,这个本地文件夹应该是我们正在使用的存储库的克隆:
output_dir = "codeparrot-ds-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
我们现在可以上传我们保存的任何内容 output_dir
通过调用 repo.push_to_hub()
方法。这将帮助我们在每个 epoch 结束时上传中间模型。在我们训练之前,让我们运行一个快速测试,看看评估函数是否正常工作:
evaluate()
(10.934126853942871, 56057.14453125)
这些损失和困惑度的值非常高,但这并不奇怪,因为我们还没有训练过模型。有了这个,我们已经准备好编写训练脚本的核心部分:训练循环。在训练循环中,我们遍历数据加载器并将批次传递给模型。有了 logits,我们就可以评估我们的自定义损失函数。我们通过梯度累积步骤的数量来缩放损失,以便在聚合更多步骤时不会产生更大的损失。在我们优化之前,我们还剪辑了梯度以获得更好的收敛性。最后,每隔几步,我们就会使用新的 evaluate()
函数评估模型:
from tqdm.notebook import tqdm
gradient_accumulation_steps = 8
eval_steps = 5_000
model.train()
completed_steps = 0
for epoch in range(num_train_epochs):
for step, batch in tqdm(
enumerate(train_dataloader, start=1), total=num_training_steps
):
logits = model(batch["input_ids"]).logits
loss = keytoken_weighted_loss(batch["input_ids"], logits, keytoken_ids)
if step % 100 == 0:
accelerator.print(
{
"lr": get_lr(),
"samples": step * samples_per_step,
"steps": completed_steps,
"loss/train": loss.item() * gradient_accumulation_steps,
}
)
loss = loss / gradient_accumulation_steps
accelerator.backward(loss)
if step % gradient_accumulation_steps == 0:
accelerator.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
lr_scheduler.step()
optimizer.zero_grad()
completed_steps += 1
if (step % (eval_steps * gradient_accumulation_steps)) == 0:
eval_loss, perplexity = evaluate()
accelerator.print({"loss/eval": eval_loss, "perplexity": perplexity})
model.train()
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
if accelerator.is_main_process:
tokenizer.save_pretrained(output_dir)
repo.push_to_hub(
commit_message=f"Training in progress step {step}", blocking=False
)
就是这样 - 您现在拥有自己的因果语言模型(例如 GPT-2)的自定义训练循环,您可以根据自己的需要进一步自定义。
✏️ 试试看! 创建适合您的用例的自定义损失函数,或在训练循环中添加另一个自定义步骤。
✏️ 试试看! 在运行长时间的训练实验时,最好使用 TensorBoard 或 Weights Biases 等工具记录重要指标。向训练循环添加适当的日志记录,以便您始终可以检查训练的进行情况。going.