Transformer核心概念I-token
2017年Transformer的诞生宣告了人工智能大模型阶段的开启,目前已经进入了大模型阶段的中期了,基于Transformer的算法创新不断推陈出新。在这过程中,对Transformer核心概念的理解显得尤为重要。接下来对Transformer模型中的核心概念逐一进行介绍。
1. 什么是Token
根据个人对Transformer模型的理解,对Token作如下定义:
Token是输入Transformer模型的数据的基本单位。
对Token的理解:
(1) Token是对现实对象重新离散化的数据单位。
现实对象要能够进入计算机中进行处理必须要转换为相应的数据格式,如何将现实对象转换为相应的数据格式这个问题已经被解决的比较彻底了,例如,图像数据以3个二维矩阵表示采集到的每个点的颜色,文字采用Unicode表示各个国家不同的文字和符号,温度采用二进制离散化进行表示,这些已经属于不是的问题的问题了。
在无论是在统计学习模型中还是在深度学习模型中,这些现实对象都是直接将对应的计算机数据直接输入到相应的模型中,也就是在统计学习模型和深度学习模型输入的数据是静态的,是直接将现实对象对应的计算机数据进行处理的。
在Transformer的模型中,实现对象对应的计算机数据并不是直接就输入到模型中,需要进行Tokenization操作从而得到Token,进行Tokenization操作的算法或是过程叫做Tokenizer。因此Transformer接收的数据Token是动态的。
(2) Token可以更好的揭示现实对象中的内部关系。
Token是通过Tokenizer算法依据原有数据集整体的条件下得到的,在这个过程中Tokenizer算法对数据根据某种特性重新进行关联与组合,这使得Token本身把现实对象分成了不同的部分。需要进一步说明的是,Token的产生既取决于Tokenizer算法还取决于数据集整体的情况。
在这里举一个简单的例子:英语“lower,higher”这两个单词组成的语料库,转换为模型可以处理的数据的过程
(1) 在使用传统的数据表示方式下:将把单词中每个字母的unicode编码作为数据,也就是1个字母占据一个字节,也就是“lower”一共5个字节,“higher”一共6个字节。
(2) 使用Token的方式下:采用目前比较主流的BPE(后续还会提到)算法作为Tokenizer算法,根据语料库的出现频率高低,将“lower,higher”分割为3个token:
low
high
er
然后针对分割后结果对这3个token分别进行编码,例如编码后的结果是:
low 00
high 01
er 10
但是,如果语料库是“lower,lowest”则分割为3个token
low
est
er
这里分割后的结果是不一样的。
(3) 得到Token的过程不属于Transformer,但又是Transformer所必须的。
Transformer模型的定义中没有包含Tokenization操作,但是Transformer需要将数据转换为Token,因此在使用Transformer的过程中,需要将数据转换为Token,然后再输入到Transformer中进行处理。
2. 实现Token的过程
前面讲的是Token的基本概念,如何实现Token才是核心问题。因为Token是需要处理数据的,而现实对象却是纷繁复杂,因此对不同性质的现实对象,实现Token的方式也是差别很大的。目前,NLP和CV是人工智能处理的主要的两种数据,下面就针对这两种数据,说明一下实现Token的过程。
NLP实现Token的方法
关于NLP实现Token的方法,目前主要有3类方法,如下图所示:
在这3类方法当中,目前大模型主要采用的是子词分词法,前两类由于各种限制,目前依据不再是主流方法了。
对于子词分词法,主要的分词过程如下图所示
这里需要说明一下,虽然这里列举了分词的主要步骤,最核心的就是第3步分词模型训练,不同分词算法其训练得到的模型是不一样的,也就使得分词的结果不一样。要真正的理解分词的处理过程,以及分词与Token的关系,读者需要具体分析分词算法,由于本文主要是讲解Token的主要概念,所以不对具体的分词算法进行介绍。
CV实现Token的方法
在CV中使用Transformer衍生出了ViT(Vision Transformer)算法,而在CV中实现Token的方法要比NLP实现Token要简单很多,直接通过分割为小块的方式就可以实现。如下图所示。
3. NLP分词(Token)的代码实现
为了更好的理解Token这个概念,接下来用一个基于Transformer的英语与德语之间机器翻译的典型实例,说明NLP是如何实现token的
该实例代码来源于github,由于这个实例的代码量比较大,在这里只给出了涉及tokenization的代码。
代码展示
import os
from urllib.request import urlretrieveimport sentencepiece
import tensorflow as tf
from sklearn.model_selection import train_test_split
from tqdm import tqdmclass DataLoader:DIR = NonePATHS = {}BPE_VOCAB_SIZE = 0MODES = ['source', 'target']dictionary = {'source': {'token2idx': None,'idx2token': None,},'target': {'token2idx': None,'idx2token': None,}}CONFIG = {'wmt14/en-de': {'source_lang': 'en','target_lang': 'de','base_url': 'https://nlp.stanford.edu/projects/nmt/data/wmt14.en-de/','train_files': ['train.en', 'train.de'],'vocab_files': ['vocab.50K.en', 'vocab.50K.de'],'dictionary_files': ['dict.en-de'],'test_files': ['newstest2012.en', 'newstest2012.de','newstest2013.en', 'newstest2013.de','newstest2014.en', 'newstest2014.de','newstest2015.en', 'newstest2015.de',]}}BPE_MODEL_SUFFIX = '.model'BPE_VOCAB_SUFFIX = '.vocab'BPE_RESULT_SUFFIX = '.sequences'SEQ_MAX_LEN = {'source': 100,'target': 100}DATA_LIMIT = NoneTRAIN_RATIO = 0.9BATCH_SIZE = 16source_sp = Nonetarget_sp = Nonedef __init__(self, dataset_name, data_dir, batch_size=16, bpe_vocab_size=32000, seq_max_len_source=100,seq_max_len_target=100, data_limit=None, train_ratio=0.9):if dataset_name is None or data_dir is None:raise ValueError('dataset_name and data_dir must be defined')self.DIR = data_dirself.DATASET = dataset_nameself.BPE_VOCAB_SIZE = bpe_vocab_sizeself.SEQ_MAX_LEN['source'] = seq_max_len_sourceself.SEQ_MAX_LEN['target'] = seq_max_len_targetself.DATA_LIMIT = data_limitself.TRAIN_RATIO = train_ratioself.BATCH_SIZE = batch_sizeself.PATHS['source_data'] = os.path.join(self.DIR, self.CONFIG[self.DATASET]['train_files'][0])self.PATHS['source_bpe_prefix'] = self.PATHS['source_data'] + '.segmented'self.PATHS['target_data'] = os.path.join(self.DIR, self.CONFIG[self.DATASET]['train_files'][1])self.PATHS['target_bpe_prefix'] = self.PATHS['target_data'] + '.segmented'def load(self, custom_dataset=False):if custom_dataset:print('#1 use custom dataset. please implement custom download_dataset function.')else: print('#1 download data')self.download_dataset()print('#2 parse data')source_data = self.parse_data_and_save(self.PATHS['source_data'])target_data = self.parse_data_and_save(self.PATHS['target_data'])print('#3 train bpe')self.train_bpe(self.PATHS['source_data'], self.PATHS['source_bpe_prefix'])self.train_bpe(self.PATHS['target_data'], self.PATHS['target_bpe_prefix'])print('#4 load bpe vocab')self.dictionary['source']['token2idx'], self.dictionary['source']['idx2token'] = self.load_bpe_vocab(self.PATHS['source_bpe_prefix'] + self.BPE_VOCAB_SUFFIX)self.dictionary['target']['token2idx'], self.dictionary['target']['idx2token'] = self.load_bpe_vocab(self.PATHS['target_bpe_prefix'] + self.BPE_VOCAB_SUFFIX)print('#5 encode data with bpe')source_sequences = self.texts_to_sequences(self.sentence_piece(source_data,self.PATHS['source_bpe_prefix'] + self.BPE_MODEL_SUFFIX,self.PATHS['source_bpe_prefix'] + self.BPE_RESULT_SUFFIX),mode="source")target_sequences = self.texts_to_sequences(self.sentence_piece(target_data,self.PATHS['target_bpe_prefix'] + self.BPE_MODEL_SUFFIX,self.PATHS['target_bpe_prefix'] + self.BPE_RESULT_SUFFIX),mode="target")print('source sequence example:', source_sequences[0])print('target sequence example:', target_sequences[0])if self.TRAIN_RATIO == 1.0:source_sequences_train = source_sequencessource_sequences_val = []target_sequences_train = target_sequencestarget_sequences_val = []else:(source_sequences_train,source_sequences_val,target_sequences_train,target_sequences_val) = train_test_split(source_sequences, target_sequences, train_size=self.TRAIN_RATIO)if self.DATA_LIMIT is not None:print('data size limit ON. limit size:', self.DATA_LIMIT)source_sequences_train = source_sequences_train[:self.DATA_LIMIT]target_sequences_train = target_sequences_train[:self.DATA_LIMIT]print('source_sequences_train', len(source_sequences_train))print('source_sequences_val', len(source_sequences_val))print('target_sequences_train', len(target_sequences_train))print('target_sequences_val', len(target_sequences_val))print('train set size: ', len(source_sequences_train))print('validation set size: ', len(source_sequences_val))train_dataset = self.create_dataset(source_sequences_train,target_sequences_train)if self.TRAIN_RATIO == 1.0:val_dataset = Noneelse:val_dataset = self.create_dataset(source_sequences_val,target_sequences_val)return train_dataset, val_datasetdef get_test_data_path(self, index):source_test_data_path = os.path.join(self.DIR, self.CONFIG[self.DATASET]['test_files'][index * 2])target_test_data_path = os.path.join(self.DIR, self.CONFIG[self.DATASET]['test_files'][index * 2 + 1])return source_test_data_path, target_test_data_pathdef download_dataset(self):for file in (self.CONFIG[self.DATASET]['train_files']+ self.CONFIG[self.DATASET]['vocab_files']+ self.CONFIG[self.DATASET]['dictionary_files']+ self.CONFIG[self.DATASET]['test_files']):self._download("{}{}".format(self.CONFIG[self.DATASET]['base_url'], file))def _download(self, url):path = os.path.join(self.DIR, url.split('/')[-1])if not os.path.exists(path):with TqdmCustom(unit='B', unit_scale=True, unit_divisor=1024, miniters=1, desc=url) as t:urlretrieve(url, path, t.update_to)def parse_data_and_save(self, path):print('load data from {}'.format(path))with open(path, encoding='utf-8') as f:lines = f.read().strip().split('\n')if lines is None:raise ValueError('Vocab file is invalid')with open(path, 'w', encoding='utf-8') as f:f.write('\n'.join(lines))return linesdef train_bpe(self, data_path, model_prefix):model_path = model_prefix + self.BPE_MODEL_SUFFIXvocab_path = model_prefix + self.BPE_VOCAB_SUFFIXif not (os.path.exists(model_path) and os.path.exists(vocab_path)):print('bpe model does not exist. train bpe. model path:', model_path, ' vocab path:', vocab_path)train_source_params = "--inputs={} \--pad_id=0 \--unk_id=1 \--bos_id=2 \--eos_id=3 \--model_prefix={} \--vocab_size={} \--model_type=bpe ".format(data_path,model_prefix,self.BPE_VOCAB_SIZE)sentencepiece.SentencePieceTrainer.Train(train_source_params)else:print('bpe model exist. load bpe. model path:', model_path, ' vocab path:', vocab_path)def load_bpe_encoder(self):self.dictionary['source']['token2idx'], self.dictionary['source']['idx2token'] = self.load_bpe_vocab(self.PATHS['source_bpe_prefix'] + self.BPE_VOCAB_SUFFIX)self.dictionary['target']['token2idx'], self.dictionary['target']['idx2token'] = self.load_bpe_vocab(self.PATHS['target_bpe_prefix'] + self.BPE_VOCAB_SUFFIX)def sentence_piece(self, source_data, source_bpe_model_path, result_data_path):sp = sentencepiece.SentencePieceProcessor()sp.load(source_bpe_model_path)if os.path.exists(result_data_path):print('encoded data exist. load data. path:', result_data_path)with open(result_data_path, 'r', encoding='utf-8') as f:sequences = f.read().strip().split('\n')return sequencesprint('encoded data does not exist. encode data. path:', result_data_path)sequences = []with open(result_data_path, 'w') as f:for sentence in tqdm(source_data):pieces = sp.EncodeAsPieces(sentence)sequence = " ".join(pieces)sequences.append(sequence)f.write(sequence + "\n")return sequencesdef load_bpe_vocab(self, bpe_vocab_path):with open(bpe_vocab_path, 'r') as f:vocab = [line.split()[0] for line in f.read().splitlines()]token2idx = {}idx2token = {}for idx, token in enumerate(vocab):token2idx[token] = idxidx2token[idx] = tokenreturn token2idx, idx2tokendef texts_to_sequences(self, texts, mode='source'):if mode not in self.MODES:ValueError('not allowed mode.')sequences = []for text in texts:text_list = ["<s>"] + text.split() + ["</s>"]sequence = [self.dictionary[mode]['token2idx'].get(token, self.dictionary[mode]['token2idx']["<unk>"])for token in text_list]sequences.append(sequence)return sequencesdef sequences_to_texts(self, sequences, mode='source'):if mode not in self.MODES:ValueError('not allowed mode.')texts = []for sequence in sequences:if mode == 'source':if self.source_sp is None:self.source_sp = sentencepiece.SentencePieceProcessor()self.source_sp.load(self.PATHS['source_bpe_prefix'] + self.BPE_MODEL_SUFFIX)text = self.source_sp.DecodeIds(sequence)else:if self.target_sp is None:self.target_sp = sentencepiece.SentencePieceProcessor()self.target_sp.load(self.PATHS['target_bpe_prefix'] + self.BPE_MODEL_SUFFIX)text = self.target_sp.DecodeIds(sequence)texts.append(text)return textsdef create_dataset(self, source_sequences, target_sequences):new_source_sequences = []new_target_sequences = []for source, target in zip(source_sequences, target_sequences):if len(source) > self.SEQ_MAX_LEN['source']:continueif len(target) > self.SEQ_MAX_LEN['target']:continuenew_source_sequences.append(source)new_target_sequences.append(target)source_sequences = tf.keras.preprocessing.sequence.pad_sequences(sequences=new_source_sequences, maxlen=self.SEQ_MAX_LEN['source'], padding='post')target_sequences = tf.keras.preprocessing.sequence.pad_sequences(sequences=new_target_sequences, maxlen=self.SEQ_MAX_LEN['target'], padding='post')buffer_size = int(source_sequences.shape[0] * 0.3)dataset = tf.data.Dataset.from_tensor_slices((source_sequences, target_sequences)).shuffle(buffer_size)dataset = dataset.batch(self.BATCH_SIZE)dataset = dataset.prefetch(tf.data.experimental.AUTOTUNE)return dataset
代码解读
在该实例中,tokenization对应的代码集成在DataLoader类中的 load方法中,具体的代码解读如下:
(1) 对语料库进行分词
(2) 根据分词结果使用BPE算法进行分词模型训练
(3) 根据BPE得到最终分词结果(也就是Token)得到相应的Token编号
(4) 根据BPE得到的分词模型对语料库进行分割
其中对sentence_piece的解读如下:
(5) 根据语料库分割结果生成训练集和测试集
总结
本文介绍了Transformer模型中Token概念的内涵,并通过NLP的实例说明了Token是如何构造的。