Transformer的Dataloader从零构建

文章所有的观点只是本人的观点,如果有地方写错希望可以指出,本人不胜感激。。。

数据准备(英德)

不使用中文的原因,在分词上面遇到了很大的麻烦,后面可以直接使用预处理模型来转化成词向量

数据下载

数据官网:Statistical and Neural Machine Translation

wget https://s3.amazonaws.com/web-language-models/paracrawl/release9/en-de/en-de.txt.gz
  • 在这里下载的是wmt22的数据集

特殊情况

  • 第一个就是在使用autodl的时候数据这些一定要放在autodl-tmp下面

  • 我在这里的一个情况我现在是使用autodl进行操作的,在下载的时候就花费了比较长的时间,然后压缩包一共就25G,所以在解压的时候就会出现数据盘因为容不下那么大的数据量(我在中间的时候查看记录就有将近一个亿条数据),而且因为只是想要感受一下自己训练模型的过程,所以没有必要使用那么多的数据,所以一个解决方法就是不下载那么多的数据,而是只是下载一部分,我下载的是一千万条数据

  • 使用命令:

    # 因为在直接使用gunzip下载数据的时候会自动生成一个文件,但是这里没有解压,所以需要自己创建一个文件夹
    mkdir en-de.txt
    # 查看前一千万条数据,然后把他写道一个文件中
    zcat en-de.txt.gz | head -n 10000 > en-de.txt

数据集划分

  • 在下载完成数据集之后是一个很大的文件,需要对训练集、验证集、测试集进行划分

import random

root_path = '/root/autodl-tmp/mt1/subword/data/'
original_file = 'en-de.txt'

with open(root_path + original_file , 'r' ,encoding='utf-8') as data_file:
    # 加载数据(注意:如果使用远程服务器,我这里使用的是autodl,一定不要使用无卡模式,因为只是使用0.5个cpu的话会出现killed的现象)
    all_data = data_file.readlines()

dataset_size = len(all_data)
# 打乱数据
random.shuffle(all_data)
# 训练集、验证集、测试集的占比
train_ratio = 0.7
val_ratio = 0.2
test_ratio = 0.1

train_size = int(dataset_size * train_ratio)
val_size = int(dataset_size * val_ratio)
test_size = dataset_size - train_size - val_size

train_data = all_data[:train_size]
val_data = all_data[train_size:train_size+val_size]
test_data = all_data[-test_size:]

def write_to_file(data, filename):
    with open(filename, 'w', encoding='utf-8') as file:
        for line in data:
            file.write(line)
write_to_file(train_data, root_path + 'train_data.txt')
write_to_file(val_data, root_path + 'val_data.txt')
write_to_file(test_data, root_path + 'test_data.txt')

拆分源语和目标语

  • 在拆分完成数据集之后现在需要干的就是将train数据集拆成为源语和目标语言

  • 了解数据集:在源语和目标语之间是按照tab进行拆分的

import os

root_path = '/root/autodl-tmp/mt1/subword/data/'
original_file = 'en-de.txt'

def split_en_de(mode,num):
    count = 0
    with open(root_path + mode + '_data.txt','r' ,encoding='utf-8') as file:
        for i in file:
            content = i.split('\t')
            en_string,de_string = content[0],content[1].split('\n')[0]
            if not os.path.exists(root_path + mode):
                os.mkdir(root_path + mode)
            with open(root_path + mode + '/en_string.en','a',encoding='utf-8') as en_f:
                en_f.write(en_string)
                en_f.write('\n')
            with open(root_path + mode + '/de_string.de','a',encoding='utf-8') as de_f:
                de_f.write(de_string)
                de_f.write('\n')
            count += 1
            if count == num:
                break
split_en_de('train',8000000)
split_en_de('val',1000000)
split_en_de('test',1000000)

数据清洗

  • 数据清洗是一个非常复杂而且繁琐的过程,并没有进行数据清洗将直接进行分词并且训练

分词

  • 常见的subword的算法有:BPE,wordpiece

  • 为什么要使用字词切分算法?传统的缺点:按照单词切分,对于OOV不能正确表示,按照字符切分:不能正确表示单词的含义效果不好。所以结合两种方式,出现Subword

  • 完整的分词阶段:标准化阶段(数据预处理)、预分词阶段(预分词的结果粒度大于最终分词的力度小于文本的粒度)、模型阶段和后处理阶段(添加一些特殊的token)

  • 分词的做法目的:构建好词典,然后在输入句子的时候可以转化成为数字索引

BPE

  • 思想:字节对编码使用子字模式的频率来筛选它们进行合并

wordpiece

  • 思想:字节对编码使用子字模式的评剧来筛选邻近的字词来进行合并(BPE只是看频率,wordpiece不仅仅是看频率还看邻近的词)

实现:huggingface

  • 数据集下载

    !wget http://www.gutenberg.org/cache/epub/16457/pg16457.txt
    !wget https://s3.amazonaws.com/research.metamind.io/wikitext/wikitext-103-raw-v1.zip
    !unzip wikitext-103-raw-v1.zip

  • 在这里没有进行标准化阶段是因为这两个数据集是比较干净的

  • 将预分词阶段和后处理阶段合并在一起

    unk_token = "<UNK>"  # token for unknown words
    spl_tokens = ["<UNK>", "<SEP>", "<MASK>", "<CLS>"]  # special tokens
    
    def prepare_tokenizer_trainer(alg):
        """
        Prepares the tokenizer and trainer with unknown & special tokens.
        """
        # special_token用于后处理阶段
        if alg == 'BPE':
            tokenizer = Tokenizer(BPE(unk_token = unk_token))
            trainer = BpeTrainer(special_tokens = spl_tokens)
        if alg == 'WPC':
            tokenizer = Tokenizer(WordPiece(unk_token = unk_token))
            trainer = WordPieceTrainer(special_tokens = spl_tokens)
        # 预分词阶段
        tokenizer.pre_tokenizer = Whitespace()
        return tokenizer, trainer

  • 进行模型训练

    def train_tokenizer(files):
        """
        Takes the files and trains the tokenizer.
        """
        tokenizer, trainer = prepare_tokenizer_trainer(alg)
        # tokenizer可以直接传入数据集的文件
        tokenizer.train(files, trainer)
        # 保存之后再加载,其实是可以没有下面两句话的,可以直接返回tokenizer
        tokenizer.save("./tokenizer-trained.json")
        tokenizer = Tokenizer.from_file("./tokenizer-trained.json")
        return tokenizer

  • 针对不同的训练集进行训练

    def train_process():
        small_file = ['pg16457.txt']
        large_files = [f"./wikitext-103-raw/wiki.{split}.raw" for split in ["test", "train", "valid"]]
        tokens_dict = {}
    
        for files in [small_file, large_files]:
            print(f"========Using vocabulary from {files}=======")
            for alg in ['BPE','WPC']:
                trained_tokenizer = train_tokenizer(files, alg)
                input_string = "This is a deep learning tokenization tutorial. Tokenization is the first step in a deep learning NLP pipeline. We will be comparing the tokens generated by each tokenization model. Excited much?!😍"
                output = trained_tokenizer.encode(input_string)
                tokens_dict[alg] = output.tokens
                print("----", alg, "----")
                print(output.tokens, "->", len(output.tokens))

  • 进行测试

    def test_processing(alg):
        tokenizer = Tokenizer.from_file("./tokenizer-trained.json")
        input_string = "this is example"
        encode = tokenizer.encode(input_string)
        print(encode.tokens)
        print(encode.ids)
        
    test_processing(alg="BPE")
    test_processing(alg='WPC')
    
    # 生成的双#代表前缀或者后缀的使用

注意:可以从零开始实现,只是比较复杂,大概的步骤如下:

  • 先把每个词都按照字符集拆分,用一个字典存储(不论是中文还是英文都是通过for的方式)

  • 然后把文本中的所有单词都放在一个字典中(中文比较难拆分=》可以使用jieba进行分词,但是像英语就可以直接通过空格的方式进行拆分,这里以英文为例)

  • 将每个单词的字母和字母之间都插入一个空格(在进行切分计算字词的时候有用)

  • 把挨着的每个词都组合在一起存放在另外的一个字典中(从2开始到n)从而进行字词

  • 统计字词的数量

  • 然后把最多的加入到字符的那个字典里面从而进行字典

注意:BPE和wordpiece的使用方法大致相同,而且现在其实可以直接使用预训练模型bert来直接输入一个序列直接得到动态词向量,不仅仅可以省掉构建词典的过程而且也可以省掉将词典中的标号转化成为词向量的过程,所以从零开始实现没有必要,这里只是明白从零开始训练一个模型的每个步骤的过程,只是了解就好。

训练模型所需的tokenizer

  • 首先就是在进行生成tokenizer的时候要明白的一个点就是:下一步需要我们这一步骤生成什么或者说没有我们这一步为什么下一步或者说后面是不可以进行实现的

    1. 字典的大小:因为在模型的定义中的Embedding层所需要的第一个参数就是字典的大小.为什么要是字典大小呢?我感觉就是要学习到字典中所有词的信息

    2. 可以把一个句子切分,并且要将切完的词可以要转化成为字典中的索引

    3. 得到一个索引列表可以将其反射成为词

  • 按理说:如果不使用库的或我们应该构建四个字典,两个是源语字典,两个是目标语字典,然后源语的两个字典中一个key是token,value是ids,另外一个是相反的,也就是一个是从token到ids的转化一个是ids到token的转化

  • 但是我们如果使用tokenizers这个库的话,可以分别tokenizer通过encode和decode得到

注意:因为这里使用的是德文和英文,所以训练的规则基本上都是相同的代码,但是呢我们需要训练两词,因为两个的训练语料是不一样的,我们会存成两个json文件,然后使用什么分词器可以直接加载这个文件就行

  • 训练tokenier的语料:训练集。全部使用BPE分词规则

from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace,WhitespaceSplit
from tokenizers import Tokenizer

unk_token = "<UNK>"  # token for unknown words
spl_tokens = ["<UNK>", "<SEP>", "<MASK>", "<S>","<E>"]
def pre_tokenizer_train():
    tokenizer = Tokenizer(BPE(unk_token = unk_token))
    trainer = BpeTrainer(special_tokens = spl_tokens)
    tokenizer.pre_tokenizer = Whitespace()
    return tokenizer,trainer

# 定义训练过程
def train_tokenizer(files):
    tokenizer, trainer = pre_tokenizer_train()
    # tokenizer可以直接传入数据集的文件
    tokenizer.train(files, trainer)
    lang = (files[0].split('/')[-1]).split('.')[-1]
    # 保存之后再加载,其实是可以没有下面两句话的,可以直接返回tokenizer
    tokenizer.save(f"./{lang}-tokenizer-trained.json")
    tokenizer = Tokenizer.from_file(f"./{lang}-tokenizer-trained.json")
    return tokenizer

def train_process(files):
    trained_tokenizer = train_tokenizer(files=files)
    return trained_tokenizer

root_path = '/root/autodl-tmp/mt1/subword/data/train/'

train_list = [[root_path+'de_string.de'],[root_path+'en_string.en']]
for i in train_list:
    train_process(i)

注意:上面的files一定是一个列表

构建dataloader

我们现在已经有了可以将一个序列转化成为模型可以接受的数值型数据也有了模型,那么接下来就应该是直接训练模型了,但是有一个过程是不可以忽略的就是构建dataloader将我们的数据喂到模型中。

存在的问题:

  • 要构建batch_size,就会出现一个问题,就是batch_size中的编码长度得是一样的

  • dataloader里面传入的是一个datasets,所以我们要如何构建dataset?

注意:如果只是想要简单的构建dataloader而不考虑词典的粒度的话可以直接使用torchtext实现(每个token都是一个词),而我想使用wordpiece或者BPE的方式,所以这里需要手动的构建dataloader

想要构建dataloader那么就一定要使用自定义一个Dataset

  • 构建数据集的简单使用:

import torch
from torch.utils.data import TensorDataset, DataLoader

# 创建一些数值和标签的 Tensor 数据
values = torch.tensor(list(range(100)), dtype=torch.float32)
labels = torch.tensor([i % 2 for i in range(100)], dtype=torch.long)

# 使用 TensorDataset 将两个 Tensor 数据组合成一个数据集
dataset = TensorDataset(values, labels)

# 创建 DataLoader
dataloader = DataLoader(dataset, batch_size=10, shuffle=False)

# 迭代 DataLoader
for batch in dataloader:
    batch_values, batch_labels = batch

    # 在这里进行模型的训练或其他操作
    print("Values:", batch_values)
    print("Labels:", batch_labels)
  • 自定义Dataset:

    • 需要解决的问题:

      • DataLoader接受的是长度相等的两个张量,也就是value和key要相互对应

      • 一个批量之间的句子长度要一直所以就要填充,那么要填充什么数值才能确保不会对上面tokenizer创建的字典有影响

      • 每天句子要经过什么样的处理(主要是表现在如何添加特殊符号)

    • 解决方法:

      • 想要搞清楚第一个问题首先就是要清楚上面的简单例子中,文什么在DataLoader中传入batch_size就可以直接按照批量输出,他的内部原理是什么样子的.这里只看比较重要的构建数据的部分就行.

        1. 先看上面简单使用的例子中为什么能实现批量

          class TensorDataset(Dataset):
          
              def __init__(self, *tensors):
                  assert all(tensors[0].size(0) == tensor.size(0) for tensor in tensors)
                  self.tensors = tensors
          
              def __getitem__(self, index):
                  return tuple(tensor[index] for tensor in self.tensors)
          
              def __len__(self):
                  return self.tensors[0].size(0)
          # 上面是TensorDataset的源码,他是继承Dataset类的,所以需要看Dataset中干了什么
          class Dataset(object):
              def __getitem__(self, index):
                  raise NotImplementedError
          
              def __add__(self, other):
                  return ConcatDataset([self, other])
          # Dataset是一个接口,继承的类必须实现__getitem__和__len__=>(为什么是__len__呢应该时候ContcatDataset有关系)
          
          # 看上面TensorDataset中的__getitem__可以看出,如果传入的是两个向量的话就是拿到对应索引位置的数据和标签(意义对应的),但是只是可以拿到一个,但是DataLoader为什么可以返回一个批量呢?直观想法就是返回了一个批量大小的index,然后循环调用__getitem__,源码(重要部分):
          
          class SequentialSampler(Sampler):
          
              def __init__(self, data_source):
                  self.data_source = data_source
          
              def __iter__(self):
                  # 生成一系列索引
                  return iter(range(len(self.data_source)))
          
              def __len__(self):
                  return len(self.data_source)
          
          
          class BatchSampler(Sampler):
              ...
              def __iter__(self):
              batch = []
              # 一旦达到batch_size的长度,说明batch被填满,就可以yield出去了
              for idx in self.sampler:
                  batch.append(idx)
                  if len(batch) == self.batch_size:
                      yield batch
                      batch = []
              # 下面部分不考虑,现在只看怎么实现正常批量
              if len(batch) > 0 and not self.drop_last:
                  yield batch
          	# batch相当于是已经生成了一个批量,使用yield保存
          
              class _DataLoaderIter(object):
              r"""Iterates once over the DataLoader's dataset, as specified by the sampler"""
          
          def __init__(self, loader):
              self.dataset = loader.dataset
              ...
          	# 开始循环拿到的batch,并且开始拿到dataset里面的索引
              self.sample_iter = iter(self.batch_sampler)
          

        2. 在填充句子长度的时候是不是直接填充0就是可以的呢?其实是不一定的,因为0可能会代表有意义的字词,所以需要确定(如果是上面的那种方式的话,mask在词典中的位置是0,所以可以直接填充0,如果是其他方式需要特殊处理)

        3. 现在的一个句子就是非常正常的句子没有任何的特殊字符在里面(那么为什么字典中是在训练集上训练的,但是里面就会i有特殊字符呢?个人感觉是会先处理这些特殊字符,先把位置占着然后在记录字词),所以需要对进行语句进行填充

    • 具体代码实现

      import torch
      import torch.nn as nn
      from torch.utils.data import Dataset, DataLoader
      from tokenizers import Tokenizer
      def custom_collate(batch):
      
          # 获取当前批次中源语言和目标语言的索引序列
          source_seqs = [item['source_indices'] for item in batch]
          target_seqs = [item['target_indices'] for item in batch]
      
          # 计算当前批次中最大的序列长度
          max_source_len = max(len(seq) for seq in source_seqs)
          max_target_len = max(len(seq) for seq in target_seqs)
      
          # 填充源语言和目标语言的索引序列,使它们的长度相同
          padded_source_seqs = [seq + [0] * (max_source_len - len(seq)) for seq in source_seqs]
          padded_target_seqs = [seq + [0] * (max_target_len - len(seq)) for seq in target_seqs]
      
          # 转换为 PyTorch tensor
          source_tensor = torch.tensor(padded_source_seqs)
          target_tensor = torch.tensor(padded_target_seqs)
      
          return {
              'source_indices': source_tensor,
              'target_indices': target_tensor
          }
      
      class TranslationDataset(Dataset):
          def __init__(self, source_sentences, target_sentences, source_tokenizer, target_tokenizer):
              self.source_sentences = source_sentences
              self.target_sentences = target_sentences
              self.source_tokenizer = source_tokenizer
              self.target_tokenizer = target_tokenizer
      
          def __len__(self):
              return len(self.source_sentences)
      
          def __getitem__(self, idx):
              source_sentence = self.source_sentences[idx]
              target_sentence = self.target_sentences[idx]
      
              # 将句子转换为索引序列
              # source_indices = [self.source_vocab[word] for word in source_sentence.split()]
              source_indices = self.source_tokenizer.encode(source_sentence).ids
              target_indices = self.target_tokenizer.encode(target_sentence).ids
      
              # 返回源语言和目标语言的索引序列
              return {
                  'source_indices': source_indices,
                  'target_indices': target_indices
              }
      de_file = 'de-tokenizer-trained.json'
      tokenizer_de = Tokenizer.from_file(de_file)
      en_file = 'en-tokenizer-trained.json'
      tokenizer_en = Tokenizer.from_file(en_file)
      dict1 = dict(tokenizer_en.get_vocab())
      
      sep = "<SEP>"
      start = "<S>"
      end = "<E>"
      
      
      source_vocab = dict(tokenizer_en.get_vocab())
      target_vocab = dict(tokenizer_de.get_vocab())
      source_sentences = []
      target_sentences = []
      root_path = '/root/autodl-tmp/mt1/subword/data/train/'
      with open(root_path + 'en_string.en','r',encoding='utf-8') as en_f:
          for i in en_f:
              en_content = start+sep+i+sep+end
              source_sentences.append(en_content)
      
      with open(root_path + 'de_string.de','r',encoding='utf-8') as de_f:
          for i in de_f:
              de_content = start+sep+i+sep+end
              target_sentences.append(de_content)
      
      translation_dataset = TranslationDataset(source_sentences, target_sentences, tokenizer_en, tokenizer_de)
      # 使用自定义的collate_fn
      dataloader = DataLoader(translation_dataset, batch_size=4, shuffle=True, num_workers=4, collate_fn=custom_collate)
      for batch in dataloader:
          source_sentence = batch['source_indices']
          target_sentence = batch['target_indices']
          break

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值