Hugging Face教程

您所在的位置:网站首页 length_in_char参数不匹配 Hugging Face教程

Hugging Face教程

2023-04-07 23:05| 来源: 网络整理| 查看: 265

从头训练GPT类语言模型

之前章节,主要是基于预训练模型进行微调,这种训练方式称为迁移学习(除了微调,还有Prompt和RLHF等训练方式)。在一些标签数据比较稀少的领域,使用迁移学习是一个非常好的策略。在本章,我们要从头训练一个完整的语言模型。如果有海量数据又或者微调数据和原始预训练数据区别较大(比如用在自然语言下预训练的模型在编程语言里进行微调,往往效果不是很理想)。当然,从头训练语言模型需要更多的计算资源(相比微调模型)。在一些特殊场景,比如乐谱、分子序列(DNA序列)或者编程语言等。已有很多工具完成上面的生成功能,例如TabNine和GitHub的Copilot,以及功能强大的OpenAI的Codex(代码生成工具),可以生成长序列代码。这些生成任务,一般采用自回归模型,也称为常规语言模型,例如GPT2(下面训练GPT2,更有名的是目前炙手可热的ChatGPT和GPT4.0等)。

本文主要创建一个较小尺寸的代码生成模型。我们关注一行代码的补全,而不是去生成完整的函数或类,使用一个Python代码数据子集。当使用Python处理数据时,经常会用到一些Python数据处理包,例如matplotlib, seaborn, pandas和 scikit-learn 等。当使用这些包时,经常会需要用一些特殊的函数名或类名(不一定全能记住),因此我们训练一个模型来完成函数或类名的联想(或叫生成也行)。

在第6章,我们创建了Python源码的一个看起来不错的分词器(这里会用到)。除了分词器外,更重要的是需要一个大数据集来进行预训练。这里,我们将分词器应用到GitHub Repo上采集的Python代码数据集上。然后使用TrainerAPI和Accelerate来完成模型训练,开始吧!

可以直接通过链接来感受下模型的最终效果。

收集数据

GItHub上的代码仓库中的Python代码是非常丰富的,我们从每个Python代码Repo中获取数据构建我们的数据集。链接Transformers textbook使用这个方法来预训练一个大型的GPT2模型。使用了大于180GB数据,大概2000万个Python文件,该数据集名称为codeparrot,可以在Hugging Face Hub上获取该数据集。

但是,完整的预训练是非常消耗时间和计算资源的,这里使用该数据集的一个子集(Python数据科学部分)。好的,那么我们下面把包含科学包(上面提到matplotlib等包)的Python数据给过滤出来。因为数据尺寸较大,我们就不完整下载到本地。而是采用流式在线进行过滤。为了方便过滤这些代码,我们使用如下函数。

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%,这个尺寸作为试验还是足够的。最终数据集有6GB大小,且包含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 }) })

预训练语言模型是非常耗时的。建议先采样部分数据测试训练代码是否正确,然后再进行完整数据集的训练。有的时候,在代码运行到最后,会因为一些小疏忽导致整个训练要重来,比如忘了创建输出文件夹等等。因此进行少部分数据的调试是很有必要的。

让我们看下数据集中的一个例子。这里展示每个field的前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个token,而GPT2中的数值为1024,GPT3中的数值为2048(这么大,显存确实扛不住)。

大部分文档都超过128个token长度,所以简单的用128长度去截断数据会导致大量数据没有被利用。因此我们使用return_overflowing_tokens选项来分割整个输入为多个块,和第6章中的操作类似。同时设置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]

我们看到2个样本处理后,得到了34个片段。然后看下分块长度,除了每个例子的最后一个分块小于chunk size(分别是117和41),其它均为128(也就是提前设定的上下文尺寸)。这表示最后一个分块只占了整个样本的很小一部分,应该可以安全的舍弃(进行Paddding会很辛苦吗!)。另外,我们有了overflow_to_sample_mapping可以很方便使用分块重构原始样本。

有了上面的操作后,我们调用Dataset.map()函数来对数据集进行处理。我们可以进行批量处理提高处理速度。这种操作也适合数据扩增或过滤等对数据量大小产生影响的操作。对于本文而言,我们使用分词操作,增加了很多的数据样本。另外处理后,需要删除原来的field。

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 }) })

上述代码运行结果,我们已经有了长度为128个token的16,700,000个样本,总共有2,100,000,000个token。与之对应的,OpenAI的GPT3和Codex分别在300,000,000,000和100,000,000,000个token下进行训练,这里的Codex是在GPT3的checkpoint基础上初始化的。我们本文目标不是与上述两个模型进行比较(他们可以生成更长的文本,有更大上下文尺寸),二是创建一个较小版本的语言模型,来辅助数据科学家实现快速的代码自动补全。

现在已经有了数据集,下面开始设置模型!

提示:当上下文尺寸很小的时候,舍弃掉最后较小的分片是没有太大影响。如果上下文尺寸很大的情况下,舍弃掉较小分片则会丢失较多数据。一个高效的数据准备方法,就是拼接所有的已分词样本(并在每个样本后加上eos_token_id),然后在拼接后的数据集上做分片。作为一个练习,可以通过改变tokenize()函数来完成这个操作,注意需要设置truncation=False,并且移除掉tokenizer中的其他参数,来获取完成的token IDs。

代码如下:

from itertools import chain def tokenize(examples): output = tokenizer(examples[text_column_name]) return output tokenized_datasets = raw_datasets.map( tokenize, batched=True, remove_columns=raw_datasets["train"].column_names ) block_size = 128 def group_texts(examples): # Concatenate all texts. concatenated_examples = {k: list(chain(*examples[k])) for k in examples.keys()} total_length = len(concatenated_examples[list(examples.keys())[0]]) # We drop the small remainder, we could add padding if the model supported it instead of this drop, you can # customize this part to your needs. if total_length >= block_size: total_length = (total_length // block_size) * block_size # Split by chunks of max_len. result = { k: [t[i : i + block_size] for i in range(0, total_length, block_size)] for k, t in concatenated_examples.items() } result["labels"] = result["input_ids"].copy() return result lm_datasets = tokenized_datasets.map( group_texts, batched=True, )

上述代码首先得到完整的token IDs,然后在拼接后的token ID列表上做分片操作。

初始化新模型

首先初始化一个GPT2模型。对于小一些的GPT2模型,也可以使用类似的配置,这里加载了一个预训练配置,当然设置vocab_size=len(tokenizer)来确保模型和分词器的大小匹配。并且传入bos和eos的token 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(),因为我们要从头开始训练一个GPT模型,因此不需要加载预训练参数。

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

我们的模型有124M参数。在进行训练之前,需要设置一个data collator来把数据处理为模型输入格式。Huggingface也提供了DataCollatorForLanguageModeling函数来方便的处理数据。该函数主要来处理语言模型训练需要的数据。在堆叠和补齐批量数据前,该函数会很好的根据模型情况,对标签进行处理。在CLM(常规语言模型,或者说自回归语言模型)中,输入会直接设置为标签(通过一次右移操作)。并且在训练过程会自动在线进行这个操作,那么用户在编码时就不需要手动复制输入的input_ids。

注意函数DataCollatorForLanguageModeling支持掩码语言模型(MLM,masked language modeling)以及常规语言模型(CLM,causal language modeling)。函数默认设置,是针对MLM模型的。通过设置mlm=False使函数可以方便处理CLM模型输入数据。

from transformers import DataCollatorForLanguageModeling ​ tokenizer.pad_token = tokenizer.eos_token data_collator = DataCollatorForLanguageModeling(tokenizer, mlm=False)

让我们看下下面的例子,实际感受下。(这是个好习惯

out = data_collator([tokenized_datasets["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])

从结果可以看到,结果中的三个键input_ids、attention_mask和labels都被正确的补齐到预设值128个token长度。

因为在模型内部计算损失的时候会对输入和标签进行移位操作,因此data collator这里只将input_ids直接copy给了labels。

现在已经准备好了训练要素。下面就是老路子,登录Huggingface,使用函数或命令。函数如下:

from huggingface_hub import notebook_login notebook_login()

命令如下:

huggingface-cli login

下面定义训练设置函数。使用带warmup的余弦学习率调度器,批尺寸为256(per_device_train_batch_size*gradient_accumulation_steps`)。这里使用了梯度累积,这是比如我们需要设置batch_size很大,但是GPU又装不下,那么一种时间换空间的方法就是减小每个step的batch_size,然后多个step累积梯度后再更新模型参数,这样做的好处是在不超过GPU最大承受能力的情况下增大batch_size。这里直接设置参数即可完成这个操作,在后面的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进行训练。具体的训练时间与模型大小、数据集大小和硬件性能有关。

trainer.train()

训练结束后,可以将模型和分词器上传到Hub上。

trainer.push_to_hub() 整个训练代码非常简短(API封装的好处,方便做各种各样的试验),可以在读者自己的数据集上做试验。如果使用多GPU的设备,也可以使用上面的代码,但是使用的Parameter Server模式,为了更快训练模型,可以使用torchrun,有兴趣的可自行查看文档。使用pipeline实现文本生成

训练好了,现在看下模型的效果(生成模型,还真是有意思,没错,ChatGPT说的就是你!)。在训练过程损失值不停的下降,但是无法表明该模型的生成效果就一定好,因此我们通过transformers库提供的pipeline来进行文本生成,这里的模型计算设置在GPU上运行(没有条件的也可以在CPU上跑)。

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指令为止。下面我们让模型解决更复杂一些的问题,比如帮助我们构建groupby操作。

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是一个很好的选择(多联系,尤其是参考transformers的examples)。下面我们使用该库来训练模型。

因为我们关注python数据科学库,那么我们的训练语料主要包含python数据科学库。可以使用诸如plt, pd, sk, fit, and predict等关键词来过滤数据,这些关键词与库matplotlib.pyplot, pandas, and sklearn紧密相关。如果上面的关键词可以被单个token表示,我们可以在输入序列中很容易的检查出来。有时token前面会带有一个空白前缀,因此我们可以在分词器字典中也可以检测出这些关键词。为了验证上述规律,我们测试下上面关键词的分词结果。

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'

结果不错,表明我们的关键词都可以被单个token表示。现在可以定制损失函数,该损失函数根据输入序列、输出logits和关键词token来计算损失。首先我们需要对齐logits和输入。通过将输入右移一位,得到标签。因为下一个token是现在token的对应输出标签。我们可以从输入第二个token开始截取,获取标签,因为第一个token不需要模型来预测,一般是特殊标记或[CLS]等。得到标签后,我们可以计算每个样本的损失,以及计算每个样本中所有关键词的出现次数。最终,我们计算根据关键词出现次数,来甲醛计算预测损失值。因为也要考虑没有关键词的样本,给这样的样本的加权乘积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() # 从第1个token开始,得到标签, shift_logits = logits[..., :-1, :].contiguous() # 从第0个开始,到-1,是输出的logits # 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

在使用新的损失函数进行训练前,我们需要做一些准备工作:

需要定义dataloaders来批量加载数据需要设置权重衰减参数因为每次epoch训练结束都需要测试,因此我们需要在函数中包含测试代码

首先创建dataloaders,将dataset的格式设置为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}, ]

编写评估函数,在每次epoch训练结束后执行。

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()

使用evalutate()函数来输出损失和混淆度。下面来重新定义模型(根据配置文件)。

model = GPT2LMHeadModel(config)

现在定义优化器,使用前面的模型参数分组函数。

from torch.optim import AdamW ​ optimizer = AdamW(get_grouped_params(model), lr=5e-4)

好的,现在准备好了模型、优化器和dataloader,然后使用accelerator.prepare进行wrapping。

from accelerate import Accelerator ​ accelerator = Accelerator(fp16=True) ​ model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare( model, optimizer, train_dataloader, eval_dataloader )如果使用TPU训练,那么需要将上面的代码封装到一个训练函数当中。

如果要计算训练步数,必须使用accelerate.prepare()封装后的train_dataloader。因为在accelerate.prepare()函数操作以后,train_dataloader的长度会发生变化。得到单个epoch的训练步数后,可以用这个参数来设置学习率调度器。

from transformers import get_scheduler ​ 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对象。首先登录到HuggingFace的Hub上,然后定义Repo的名称。为了获取该repo的全称,可以使用函数get_full_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'

然后将该repo克隆到本地。

output_dir = "codeparrot-ds-accelerate" repo = Repository(output_dir, clone_from=repo_name)

现在就可以使用repo.push_to_hub()函数将output_dir中的内容上传。

​ 在训练之前,我们先评估下模型效果。

evaluate() (10.934126853942871, 56057.14453125)

​ 从结果看,损失和混淆度都非常差。这是因为还没有训练,训练后的效果一般会好很多(在训练没崩的情况下)。目前为止,已经准备好了训练所需要素。在训练循环中,我们会迭代读取dataloader,并将输出的批量数据输入到model中。根据模型输出和标签,我们可以使用定制的损失函数来评估模型效果。下面也使用了梯度累积技巧(不建议使用如下代码进行梯度累积,对于多卡训练存在BUG,具体请参考accelerate的文档,有更加安全的处理方法)。另外,还进行梯度裁剪操作。最后在每经过一定训练步数后,使用我们的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 )

上面是基本的GPT2模型从头训练的骨干代码和讲解。读者可以先跑通一下试试效果(在kaggle或colab上),然后修改代码定制自己的需求。这个训练还是比较耗时的,对计算资源的要求也是比较高的,后续会推出PEFT教程,看看在diffusion model上大放异彩的lora在NLP领域的应用(低资源、较高性能的完成模型微调。LLM大语言模型还是交给有钱的大厂去搞吧)。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3