Tokenizer 使用介绍

1. 概述

前面已经通过源码介绍了 tranformers 中如何使用 AutoTokenizer 加载 Tokenizer,本文主要介绍如何使用加载的 Tokenizer 对训练数据进行预处理,通过本文的学习,你将学习到以下内容:

  • tokenizer简单介绍
  • 如何使用 tokenizer 的填充(padding)
  • 如何使用 tokenizer 的截断(truncation)
  • 如何使用 fast tokenizer
  • 使用 tokenizer 对训练数据进行预处理(问答数据)

2. Tokenizer

在自然语言处理(NLP)中,tokenizer是一个非常关键的组件,其作用是将原始文本转换为模型可以理解和处理的形式。具体来说,tokenizer的几个主要职责包括:

  1. 分词(Tokenization)
    将连续的文本分割成一个个离散的单元,称为"tokens"。这些tokens可以是单词、短语或者其他语言的基本单位。例如,句子 "Hello, world!" 可能会被分词为 ["Hello", ",", "world", "!"]。
  2. 文本清洗和规范化(Normalization)
    在分词的同时,tokenizer通常还会进行文本清洗,比如转换字符大小写(标准化为小写/大写)、移除无用符号和空格、处理特殊字符等。
  3. 子词分割(Subword Tokenization)
    将单词进一步拆分为更小的单元,例如根据字根、词缀等。这对于处理未知词汇或生僻字特别有用。有些模型(如 BERT)便是基于这种子词分词策略。
  4. 映射到ID(Token to ID Mapping)
    将分词后的每一个token映射到一个唯一的数字ID。这个ID对应于模型中的词汇表。这样,文本数据就可以转换为数值型的输入,以便模型处理。
  5. 填充(Padding)和截断(Truncation)
    为了满足模型输入的要求,通常需要将序列的长度统一。Tokenizer可以根据模型的最大接受长度进行截断(去掉一些token)或填充(增加一些特殊token,如[PAD])。
  6. 特殊Tokens的添加
    在某些模型(如BERT)中,tokenizer还会增加特殊的tokens,例如表示序列开始的[CLS]或句子分隔的[SEP]等。
  7. 生成注意力掩码(Attention Mask)
    有些模型(如Transformer架构)需要注意力掩码来标识输入中哪些部分是真实数据,哪些是填充的部分。

总而言之,tokenizer是机器学习模型理解和处理自然语言文本的桥梁。它负责文本的预处理步骤,确保数据与模型的输入要求相符,并以适当的格式呈现给模型,从而进行后续的学习或预测。

下面演示使用gpt-3.5的tokenizer对一个文本进行tokenizer(不同的tokenizer有不同的分词规则):

2.1. token

在 LLM 中,token代表模型可以理解和生成的最小意义单位,是模型的基础单元,它可以是一个单词、一个字母、一个标点符号,或者其他语言中的字符。Token是对文本进行处理和分析的最小组成部分,在自然语言处理(NLP)任务中,文本通常需要先被分解成token,然后再进行进一步的处理。在大型预训练模型中,token也会被转换为数值向量(称为token embeddings),这些向量的数学特性允许模型捕捉到语言的丰富语义和句法信息。

在不同的语境下,一个token可能代表一个字、一个词、或者一个句子。在英文中,一个token通常是一个词或者是标点符号。在汉语中,一个token可能是一个字,也可能是一个词:

  1. 单词Token:在许多西方语言中,空格和标点符号可以用来分隔单词,因此,单词可以作为tokens。例如,句子 "I love apples." 可以分解为 tokens "I", "love", "apples", 和 "."
  2. 字母Token:在某些语言模型的实施中,单个字母或字符可能被视为token。这种方法在分析字母构词系统的语言(如汉语)时很常见。
  3. 子词Token:对于许多大型预训练语言模型(如BERT、GPT),使用子词单位(subword units)作为tokens更为常见。这意味着词汇可以被进一步分解成更小的部分,例如 "unbelievable" 可能被分解为 "un-"、"##believe" 和 "##able"(这里的 "##" 表示该部分是较大单词的一部分而不是独立单词)。

LLM 使用数字输入,因此每个token都被赋予一个唯一的数值索引,token到数值的映射是通过词汇表实现的,词汇表用来将token映射到唯一的数值表示,这种映射允许 LLM 将文本数据作为数字序列进行处理和操作,从而实现高效的计算和建模。

注意:词汇表是在训练 Tokenizer 时生成的

下面是一些有用的经验法则,可以帮助理解token的长度(下面是gpt的tokenizer在英文中对应的token长度):

1 token ~= 4 chars

1 token ~= ¾ words

100 tokens ~= 75 words

2.2. token局限性

下面演示使用gpt-3.5的tokenizer对token局限性进行说明(不同的tokenizer有不同的分词规则):

token 的设计大概存在以下的局限性:

  • 区分大小写。不同大小写的单词被视为不同的token,hello是token[15339],Hello是token[22691],HELLO有两个token[38757,1623]
  • 数字分块不一致。在GPT3.5中数字200被标记为单个token[1049],但是数字1112003被标记为三个token[5037, 1049, 18],GPT3.5的tokenizer对数字的分词可能是三个数字标记为一个token,因为tokenizer不能正确地分词数字,这或许就是为什么基于 GPT 的模型并不总是擅长数学计算的原因。
  • 空格导致token不一致。有些token有空格,导致分词不一致,比如hello前面有空格和没有空格分词的token是不一样的。

3. Padding

3.1. 为什么使用padding

在使用批量数据训练模型的时候,训练数据的长度并不总是相同,这会导致训练出问题。因为模型输入张量(tensor)需要有一个统一的形状,所以需要通过在较短的句子中添加一个特殊的填充标记( padding token 来确保较短的序列将和批次中最长的序列或模型接受的最大长度相同

3.2. padding配置策略

接下来介绍一下 padding 的配置策略,也就是 padding 参数能够接收哪些值,查看transformer源码,padding 的允许配置的策略如下:

padding参数控制文本填充的方式,它可以是布尔值或字符串:

  1. True longest:对批次中的所有序列进行填充,使它们的长度与批次中最长的序列一致(如果你只提供了单个序列,则不应用任何填充)。
  2. max_length:将所有序列填充或截断到指定的max_length长度,或者如果没有提供max_length(即max_length=None),则填充到模型接受的最大长度。即使你只提供了单个序列,依然会应用填充。
  3. False 'do_not_pad'(默认):不进行任何填充。

3.3. padding使用示例

接下来我将使用 Qwen/Qwen1.5-7B-Chat 的 tokenizer 详细介绍如何对训练数据进行 padding,正如前面介绍的,因为当训练一批长度不同的数据时需要将较短的句子填充(padding)到相同的长度,所以我们在使用tokenizer分词的时候可以添加参数 padding=True,以填充批处理中较短的序列到该批次中最长的序列:

python 复制代码
from transformers import AutoTokenizer

if __name__ == '__main__':
    model_path = '/Users/xiniao/models/Qwen1.5-7B-Chat'
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    print('输出tokenizer如下:')
    print(tokenizer)
    batch_sentences = [
        "But what about second breakfast?",
        "Don't think he knows about second breakfast, Pip.",
        "What about elevensies?",
    ]
    encoded_input = tokenizer(batch_sentences, padding=True)
    print(f'encoded_input的类型为:{type(encoded_input)}')
    print('输出encoded_input如下:')
    print(encoded_input)

    # tokens() 查看这个批次中第batch_index个文本字符串分词之后的原始tokens列表,你可以使用这个tokens方法来获取,这对于理解分词器如何将文本切分成tokens或者对分词器的输出进行调试非常有用。
    tokens1 = encoded_input.tokens()  # 默认是0
    tokens2 = encoded_input.tokens(1)
    tokens3 = encoded_input.tokens(2)
    print(f'第一个句子的tokens为:{tokens1}')
    print(f'第二个句子的tokens为:{tokens2}')
    print(f'第三个句子的tokens为:{tokens3}')


# 输出tokenizer如下:
# Qwen2TokenizerFast(name_or_path='/Users/xiniao/models/Qwen1.5-7B-Chat', vocab_size=151643, model_max_length=32768, is_fast=True, padding_side='right', truncation_side='right', special_tokens={'eos_token': '<|im_end|>', 'pad_token': '<|endoftext|>', 'additional_special_tokens': ['<|im_start|>', '<|im_end|>']}, clean_up_tokenization_spaces=False),  added_tokens_decoder={
#     151643: AddedToken("<|endoftext|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
#     151644: AddedToken("<|im_start|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
#     151645: AddedToken("<|im_end|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),
# }
# encoded_input的类型为:<class 'transformers.tokenization_utils_base.BatchEncoding'>
# 输出encoded_input如下:
# {'input_ids': [
#     [3983, 1128, 911, 2086, 17496, 30, 151643, 151643, 151643, 151643, 151643],
#     [8002, 944, 1744, 566, 8788, 911, 2086, 17496, 11, 77382, 13],
#     [3838, 911, 11964, 724, 550, 30, 151643, 151643, 151643, 151643, 151643]],
# 'attention_mask': [
#     [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
#     [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
#     [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]]
# }
# 第一个句子的tokens为:['But', 'Ġwhat', 'Ġabout', 'Ġsecond', 'Ġbreakfast', '?', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>']
# 第二个句子的tokens为:['Don', "'t", 'Ġthink', 'Ġhe', 'Ġknows', 'Ġabout', 'Ġsecond', 'Ġbreakfast', ',', 'ĠPip', '.']
# 第三个句子的tokens为:['What', 'Ġabout', 'Ġelev', 'ens', 'ies', '?', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>']

从示例代码输出中我们可以看到,第一个和第三个较短句子被 'pad_token': '<|endoftext|>'填充到 批次中最长的序列的长度, input_ids 填充的内容就是 <|endoftext|> 对应的ID 151643

调用 tokenizer(batch_sentences, padding=True) 返回的数据类型为 BatchEncoding(本质是一个字典UserDict) ,输出的字典中有两个key:

  • input_ids :是一个由token ID组成的列表,用于表示原始文本序列在模型的词汇表中的对应索引。
  • attention_mask :注意力掩码告诉模型哪些部分是关键内容(需要关注的),哪些部分实际上是填充内容(padding)(不需要处理的)。attention_mask中关注部分的值为1,填充部分的值为0。

关于 BatchEncoding 的使用在后面介绍 Fast Tokenizer时会详细介绍。

前面的示例中已经设置 padding=True也就是对应策略 longest,该策略会将短序列填充到批次中最长的序列长度。接下来演示其他两个策略:

设置参数padding='max_length'并且 max_length=2, max_length 比批次中最短序列的长度还小:

python 复制代码
encoded_input = tokenizer(batch_sentences, padding='max_length', max_length=2)
print(f'encoded_input的类型为:{type(encoded_input)}')
print('输出encoded_input如下:')
print(encoded_input)

# tokens() 查看这个批次中第batch_index个文本字符串分词之后的原始tokens列表,你可以使用这个tokens方法来获取,这对于理解分词器如何将文本切分成tokens或者对分词器的输出进行调试非常有用。
tokens1 = encoded_input.tokens()  # 默认是0
tokens2 = encoded_input.tokens(1)
tokens3 = encoded_input.tokens(2)
print(f'第一个句子的tokens为:{tokens1}')
print(f'第二个句子的tokens为:{tokens2}')
print(f'第三个句子的tokens为:{tokens3}')



# 输出encoded_input如下:
# {'input_ids': [[3983, 1128, 911, 2086, 17496, 30], [8002, 944, 1744, 566, 8788, 911, 2086, 17496, 11, 77382, 13], [3838, 911, 11964, 724, 550, 30]], 'attention_mask': [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]]}
# 第一个句子的tokens为:['But', 'Ġwhat', 'Ġabout', 'Ġsecond', 'Ġbreakfast', '?']
# 第二个句子的tokens为:['Don', "'t", 'Ġthink', 'Ġhe', 'Ġknows', 'Ġabout', 'Ġsecond', 'Ġbreakfast', ',', 'ĠPip', '.']
# 第三个句子的tokens为:['What', 'Ġabout', 'Ġelev', 'ens', 'ies', '?']

设置参数padding='max_length'并且 max_length=8, max_length 比批次中最短序列的长度还大,比最长序列小:

python 复制代码
encoded_input = tokenizer(batch_sentences, padding='max_length', max_length=8)
print(f'encoded_input的类型为:{type(encoded_input)}')
print('输出encoded_input如下:')
print(encoded_input)

# tokens() 查看这个批次中第batch_index个文本字符串分词之后的原始tokens列表,你可以使用这个tokens方法来获取,这对于理解分词器如何将文本切分成tokens或者对分词器的输出进行调试非常有用。
tokens1 = encoded_input.tokens()  # 默认是0
tokens2 = encoded_input.tokens(1)
tokens3 = encoded_input.tokens(2)
print(f'第一个句子的tokens为:{tokens1}')
print(f'第二个句子的tokens为:{tokens2}')
print(f'第三个句子的tokens为:{tokens3}')



# 输出encoded_input如下:
# {'input_ids': [[3983, 1128, 911, 2086, 17496, 30, 151643, 151643], [8002, 944, 1744, 566, 8788, 911, 2086, 17496, 11, 77382, 13], [3838, 911, 11964, 724, 550, 30, 151643, 151643]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 0, 0]]}
# 第一个句子的tokens为:['But', 'Ġwhat', 'Ġabout', 'Ġsecond', 'Ġbreakfast', '?', '<|endoftext|>', '<|endoftext|>']
# 第二个句子的tokens为:['Don', "'t", 'Ġthink', 'Ġhe', 'Ġknows', 'Ġabout', 'Ġsecond', 'Ġbreakfast', ',', 'ĠPip', '.']
# 第三个句子的tokens为:['What', 'Ġabout', 'Ġelev', 'ens', 'ies', '?', '<|endoftext|>', '<|endoftext|>']

设置参数padding='max_length'并且 max_length=15, max_length 比最长序列大:

python 复制代码
encoded_input = tokenizer(batch_sentences, padding='max_length', max_length=15)
print(f'encoded_input的类型为:{type(encoded_input)}')
print('输出encoded_input如下:')
print(encoded_input)

# tokens() 查看这个批次中第batch_index个文本字符串分词之后的原始tokens列表,你可以使用这个tokens方法来获取,这对于理解分词器如何将文本切分成tokens或者对分词器的输出进行调试非常有用。
tokens1 = encoded_input.tokens()  # 默认是0
tokens2 = encoded_input.tokens(1)
tokens3 = encoded_input.tokens(2)
print(f'第一个句子的tokens为:{tokens1}')
print(f'第二个句子的tokens为:{tokens2}')
print(f'第三个句子的tokens为:{tokens3}')



# 输出encoded_input如下:
# {'input_ids': [[3983, 1128, 911, 2086, 17496, 30, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643], [8002, 944, 1744, 566, 8788, 911, 2086, 17496, 11, 77382, 13, 151643, 151643, 151643, 151643], [3838, 911, 11964, 724, 550, 30, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643, 151643]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], [1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]}
# 第一个句子的tokens为:['But', 'Ġwhat', 'Ġabout', 'Ġsecond', 'Ġbreakfast', '?', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>']
# 第二个句子的tokens为:['Don', "'t", 'Ġthink', 'Ġhe', 'Ġknows', 'Ġabout', 'Ġsecond', 'Ġbreakfast', ',', 'ĠPip', '.', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>']
# 第三个句子的tokens为:['What', 'Ġabout', 'Ġelev', 'ens', 'ies', '?', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>', '<|endoftext|>']

当设置 padding='max_length' 时,对上面 max_length 设置不同长度的情形总结:

  1. 设置 max_length 比批次中最短序列的长度还小时,不会对序列进行填充,和 do_not_pad 策略一致。
  2. 设置 max_length 比批次中最短序列的长度大,比最长序列小时,只会对小于max_length的序列进行填充。
  3. 设置 max_length 比最长序列的长度大时,会将批次中所有的序列填充到 max_length 指定的长度。
  4. 设置 max_length=None 或者不指定 max_length 时,会将批次中所有的序列填充到模型接受的最大长度。

当设置 padding='do_not_pad' 或者 padding=False或者不设置 padding参数,则不会进行填充。

4. Truncation

4.1. 为什么使用truncation

在使用tokenizer对文本数据进行处理时,截断(truncation)是一个重要的步骤,原因主要包括以下几点:

  1. 模型输入长度限制:大多数现代的自然语言处理模型,特别是基于Transformer的模型(如BERT, GPT系列等),都有一个最大的输入长度限制。这意味着模型一次只能处理特定数量的token。例如,BERT通常限制输入长度为512个token。如果输入序列超过这个长度限制,模型将无法处理它。因此,必须将较长的文本截断到模型能够接受的长度。
  2. 计算资源优化:处理更长的序列需要更多的计算资源和时间。通过截断,我们可以确保所有处理的文本长度一致,或者不超过一个可管理的长度,从而优化计算资源的使用,加快训练和推理过程,同时也有助于控制内存使用。
  3. 提升模型表现:虽然较长的文本可能含有更丰富的信息,但并不是所有信息都对模型的特定任务同等重要。截断可以帮助模型集中注意力于文本的关键部分(例如对情感分析来说,文本的开头和结尾可能更重要),从而在某些情况下实际上提高了模型的表现。
  4. 数据一致性:在训练和推理阶段保持输入数据的一致性对于模型性能至关重要。通过应用截断,所有的文本输入都会被处理成相同的长度,这有助于维持模型的稳定性和准确性。

4.2. truncation配置策略

接下来介绍一下 truncation_strategy, 查看 transformer 源码,truncation配置策略如下:

截断(truncation)参数用来控制文本的截断行为,它可以是布尔值或者字符串(boolstr 或 [~tokenization_utils_base.TruncationStrategy],可选 ,默认为 False),可以接受以下值:

  • True longest_first:将文本截断至由 max_length 参数指定的最大长度,或者如果没有提供 max_length 参数,则截断至模型接受的最大长度(max_length=None)。如果没有提供一对序列,则该策略和 only_first的截断逻辑一样。
  • only_second:只截断一对序列中的第二个句子,直至由 max_length 参数指定的最大长度,或者如果没有提供 max_length,则直至模型接受的最大长度(max_length=None)。如果提供了一对序列(或者一批对序列),只有第二个句子会被截断,如果没有提供一对序列,则会报错Exception: Truncation error: Second sequence not provided。如果提供一对序列,但是指定的 max_length小于第一个序列的token长度,则会报错 Truncation error: Sequence to truncate too short to respect the provided max_length
  • only_first:只截断一对序列中的第一个句子,直至由 max_length 参数指定的最大长度,或者如果没有提供 max_length,则直至模型接受的最大长度(max_length=None)。如果提供了一对序列(或者一批对序列),只有第一个句子会被截断。
  • ****False do_not_truncate(默认) :不进行任何截断。

4.3. truncation使用示例

设置 truncation=True的示例如下:

python 复制代码
from transformers import AutoTokenizer

if __name__ == '__main__':
    model_path = '/Users/xiniao/models/Qwen1.5-7B-Chat'
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    # 示例文本,长度超过BERT模型的最大序列长度
    text = ("This is an example of a text that is quite lengthy and goes beyond "
            "the maximum allowed input length for Qwen models. Since we can't "
            "feed this entire text into the model without truncating it, we apply "
            "truncation to ensure that the text fits within the model's maximum input "
            "length limit.")

    # 使用tokenizer对文本进行编码,同时使用截断
    # max_length设置为12个token,超过这个长度的部分将被截断
    encoded_input = tokenizer(text, max_length=12, truncation=True, return_length=True)

    # 输出编码后的文本信息
    print(encoded_input)

    tokens1 = encoded_input.tokens()  # 默认是0
    print(f'第一个句子的tokens为:{tokens1}')



# 输出内容如下:
# {
# 'input_ids': [1986, 374, 458, 3110, 315, 264, 1467, 429, 374, 5008, 34206, 323], 
# 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# 'length': [12]
# }
# 第一个句子的tokens为:['This', 'Ġis', 'Ġan', 'Ġexample', 'Ġof', 'Ġa', 'Ġtext', 'Ġthat', 'Ġis', 'Ġquite', 'Ġlengthy', 'Ġand']

在这个示例中,通过设置参数 truncation=True,告诉tokenizer在需要时截断超长文本。max_length 参数用于设置截断长度;在这个例子中是12个token。

上述代码将输出编码后的文本信息,包括 input_ids (每个token对应的ID),attention_mask(用于指示哪些token是真实文本,哪些是填充的),以及截断后的实际长度。

下面演示一批序列的截断,并且设置truncation=True也就是截断策略为longest_first

python 复制代码
from transformers import AutoTokenizer

if __name__ == '__main__':
    model_path = '/Users/xiniao/models/Qwen1.5-7B-Chat'
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    sentences = [
        "This sentence is not too long but we are going to split it anyway.",
        "This sentence is shorter but will still get split.",
    ]
    inputs = tokenizer(
        sentences,
        truncation=True,
        max_length=6,
        stride=2,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
        return_token_type_ids=True
    )
    print(inputs)
    print("*" * 30)
    for ids in inputs["input_ids"]:
        print(tokenizer.decode(ids))
    print("*" * 30)
    print(inputs["overflow_to_sample_mapping"])

输出结果如下:

python 复制代码
{
    'input_ids': [[1986, 11652, 374, 537, 2238, 1293], 
               [2238, 1293, 714, 582, 525, 2087], 
               [525, 2087, 311, 6718, 432, 13657], 
               [432, 13657, 13], 
               [1986, 11652, 374, 23327, 714, 686], 
               [714, 686, 2058, 633, 6718, 13]], 
    'token_type_ids': [[0, 0, 0, 0, 0, 0], 
                       [0, 0, 0, 0, 0, 0], 
                       [0, 0, 0, 0, 0, 0], 
                       [0, 0, 0], 
                       [0, 0, 0, 0, 0, 0], 
                       [0, 0, 0, 0, 0, 0]], 
    'attention_mask': [[1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1]], 
    'offset_mapping': [[(0, 4), (4, 13), (13, 16), (16, 20), (20, 24), (24, 29)], 
                       [(20, 24), (24, 29), (29, 33), (33, 36), (36, 40), (40, 46)], 
                       [(36, 40), (40, 46), (46, 49), (49, 55), (55, 58), (58, 65)], 
                       [(55, 58), (58, 65), (65, 66)], [(0, 4), (4, 13), (13, 16), (16, 24), (24, 28), (28, 33)], 
                       [(24, 28), (28, 33), (33, 39), (39, 43), (43, 49), (49, 50)]], 
    'overflow_to_sample_mapping': [0, 0, 0, 0, 1, 1]}
******************************
This sentence is not too long
too long but we are going
are going to split it anyway
it anyway.
This sentence is shorter but will
but will still get split.
******************************
[0, 0, 0, 0, 1, 1]

从输出结果中可以看到,将两个句子分别进行了截断,截断的长度为 6 个token,从上面的 decode 的结果可以看到, stride=2表示截断时各个截断部分的重合 2 个token。

下面是tokenizer分词后返回字典中各个key的作用说明:

  1. input_ids:分词后,每个token都通过tokenizer被转换成对应的数字ID。对于预训练的模型来说,input_ids中的每个数字代表着词汇表中的一个特定token。在这个例子中,input_ids是一个包含多个序列的列表,每个序列是一个ID的列表。这通常是因为输入文本被截断或分割为多个部分处理。
  2. token_type_ids: 此项用于区分一个序列中的不同部分,特别是在处理两个序列(比如问答对或文本对)时。在BERT-like模型中,第一个序列的token类型ID通常为0,第二个序列为1,特殊token(如CLS、SEP)的类型ID遵循其所在的上下文。在这里,所有token的token_type_ids都是0,表明这些序列可能只包含单个序列部分,或者这个ID是默认填充的值。
  3. attention_mask:告诉模型哪些input_ids中的ID是有意义的,哪些是填充(padding)。在这里,1表示token应被模型考虑,0表示应被忽略。此列表用于对齐input_ids,每个input_ids序列有相应的attention_mask序列。这确保了模型不会对填充的汉字进行处理,这对模型的训练和预测都很关键。
  4. offset_mapping: 这个List每一项包含tuple,每个tuple对应序列中每个token在原始文本中的字符偏移量。第一项是token开始的位置,第二项是结束位置。这个映射在任务中非常有用,比如在提取答案文本的执行问答任务时,可以定位答案在原始文本中的位置。
  5. overflow_to_sample_mapping:由于长文本的存在,单个文本示例可能在tokenize后分成了多个部分(尤其是在启用了滑动窗口(stride)的场景中)。overflow_to_sample_mapping中的每个元素表示当前input对应的原始样本的索引。在这个例子中,前4个input_ids序列来自于第一个原始样本(索引为0),后两个来自于第二个原始样本(索引为1)。这个映射对于后续处理和将模型输出的结果映射回原始样本非常重要。

这样的数据结构使得处理一个包含多个样本的大数据集时,可以准确地追踪到每个分词后token序列属于哪一个原始样本,以及它们在原始文本中的确切位置。这对于在NLP任务中对token进行进一步处理非常重要,尤其是那些需要精确字符级别信息的任务,例如命名实体识别(NER)或问答(QA)。

由于模型输入张量(tensor)需要有一个统一的形状,所以需要对长度不一致的序列进行填充,上面的例子需要添加参数 padding=True,代码如下所示:

python 复制代码
from transformers import AutoTokenizer

if __name__ == '__main__':
    model_path = '/Users/xiniao/models/Qwen1.5-7B-Chat'
    tokenizer = AutoTokenizer.from_pretrained(model_path)
    sentences = [
        "This sentence is not too long but we are going to split it anyway.",
        "This sentence is shorter but will still get split.",
    ]
    inputs = tokenizer(
        sentences,
        truncation=True,
        return_overflowing_tokens=True,
        max_length=6,
        stride=2,
        padding=True
    )
    print(inputs)
    print("*" * 30)
    for ids in inputs["input_ids"]:
        print(tokenizer.decode(ids))
    print("*" * 30)
    print(inputs["overflow_to_sample_mapping"])

输出结果:

python 复制代码
{
    'input_ids': [[1986, 11652, 374, 537, 2238, 1293], 
                  [2238, 1293, 714, 582, 525, 2087], 
                  [525, 2087, 311, 6718, 432, 13657], 
                  [432, 13657, 13, 151643, 151643, 151643], 
                  [1986, 11652, 374, 23327, 714, 686], 
                  [714, 686, 2058, 633, 6718, 13]], 
    'attention_mask': [[1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 0, 0, 0], 
                       [1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1]], 
    'overflow_to_sample_mapping': [0, 0, 0, 0, 1, 1]
}
******************************
This sentence is not too long
too long but we are going
are going to split it anyway
it anyway.<|endoftext|><|endoftext|><|endoftext|>
This sentence is shorter but will
but will still get split.
******************************
[0, 0, 0, 0, 1, 1]

下面演示一对序列进行截断,并且设置 truncation='only_second',代码示例如下:

python 复制代码
from transformers import AutoTokenizer

if __name__ == '__main__':
    model_path = '/Users/xiniao/models/Qwen1.5-7B-Chat'
    tokenizer = AutoTokenizer.from_pretrained(model_path)

    first_sentence = "This sentence is short."
    second_sentence = "This sentence is long but will get split."
    inputs = tokenizer(first_sentence)
    print(f'第一个序列的长度: {len(inputs["input_ids"])}')
    inputs = tokenizer(
        first_sentence,
        second_sentence,
        truncation='only_second',
        return_overflowing_tokens=True,
        max_length=10,
        padding=True
    )
    print(inputs)
    print("*" * 30)
    for ids in inputs["input_ids"]:
        print(tokenizer.decode(ids))
    print("*" * 30)
    print(inputs["overflow_to_sample_mapping"])

输出结果:

python 复制代码
第一个序列的长度: 5
{
    'input_ids': [[1986, 11652, 374, 2805, 13, 1986, 11652, 374, 1293, 714], 
                  [1986, 11652, 374, 2805, 13, 686, 633, 6718, 13, 151643]], 
    'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 
                       [1, 1, 1, 1, 1, 1, 1, 1, 1, 0]], 
    'overflow_to_sample_mapping': [0, 0]
}
******************************
This sentence is short.This sentence is long but
This sentence is short. will get split.<|endoftext|>
******************************
[0, 0]

从输出结果中可以看到,第一个句子没有被截断,第二个句子被截断了,然后拼接到第一个句子后面。对于问答场景的大模型微调,可以设置第一个句子为问题,第二个句子为上下文,对长上下文进行截断可以扩充问答场景的训练数据集数量,在后续内容会对问答场景的数据预处理进行介绍。

注意:在transformers中进行tokenizer时,实际是先进行截断(truncation)后进行填充(padding)

根据具体的模型和任务要求,你可以调整max_length参数的设置,并选择不同的截断策略。Hugging Face的tokenizer通常默认截断文本的末尾部分,也可以通过设置truncation_strategy参数来选择不同的截断策略(如从开头或结尾截断)。

5. Fast Tokenizer

5.1. BatchEncoding

接下来将介绍 tokenizer 返回的数据类型 BatchEncoding(本质是一个字典UserDict) ****。

BatchEncoding 是一个用于保存 tokenization 过程输出结果的类,其逻辑上继承自 Python 字典,并可以像字典一样使用。该类保存由~tokenization_utils_base.PreTrainedTokenizerBase.__call__~tokenization_utils_base.PreTrainedTokenizerBase.encode_plus~tokenization_utils_base.PreTrainedTokenizerBase.batch_encode_plus 方法生成的信息(如 tokens,attention masks 等)。此外BatchEncoding 类提供了实用的方法,用于在单词/字符与 token 之间进行映射。

5.1.1. BatchEncoding 的作用

  • 作为一种容器,保存了 tokenization 过程的结果,方便进一步处理和模型输入。
  • 提供了从字符级或单词级别到 token 级别的映射,用于分析和解释模型对输入的处理方式,例如,找到原始文本中特定单词对应的 tokens。
  • 通过提供初始化时的张量转换,避免了手动转换步骤,增加了将 tokenization 结果用于机器学习框架(例如 PyTorch 或 TensorFlow)的便捷性。
  • 通过扩展了标准字典功能,增加了与深度学习模型兼容的结构和方法。

5.1.2. BatchEncoding常用方法介绍

BatchEncoding类的方法如下

BatchEncoding的方法本质上是调用的 Encoding 的方法:

接下来我们对常用的方法进行介绍:

5.1.2.1. BatchEncoding 构造方法

python 复制代码
class BatchEncoding(UserDict):
    def __init__(
        self,
        data: Optional[Dict[str, Any]] = None,
        encoding: Optional[Union[EncodingFast, Sequence[EncodingFast]]] = None,
        tensor_type: Union[None, str, TensorType] = None,
        prepend_batch_axis: bool = False,
        n_sequences: Optional[int] = None,
    ):
        super().__init__(data)

        if isinstance(encoding, EncodingFast):
            encoding = [encoding]

        self._encodings = encoding

        if n_sequences is None and encoding is not None and len(encoding):
            n_sequences = encoding[0].n_sequences

        self._n_sequences = n_sequences

        self.convert_to_tensors(tensor_type=tensor_type, prepend_batch_axis=prepend_batch_axis)

这是BatchEncoding类的构造器(__init__方法),它是用来初始化一个BatchEncoding对象的。下面是各个参数的具体作用:

  • data (Optional[Dict[str, Any]]): 这是一个可选参数,它接受一个字典。字典中的键通常包括'input_ids'、'attention_mask'等,而对应的值可以是列表、数组或张量等形式。这些都是在使用__call__encode_plusbatch_encode_plus等方法时tokenizer生成的数据。如果提供了data参数,则该字典会被用于初始化BatchEncoding对象。
  • encoding (Optional[Union[EncodingFast, Sequence[EncodingFast]]]): 这个可选参数接受EncodingFast对象或者这些对象的序列。如果使用了快速tokenizer,这个对象将包含额外的信息,如从单词或字符级别到token级别的映射。对于批量处理的情况,可以提供一个这些对象的序列。如果提供encoding参数,该信息将被储存于BatchEncoding对象中。
  • tensor_type (Union[None, str, TensorType]): 这个参数用于在初始化BatchEncoding对象时,将列表中的整数转换为PyTorch、TensorFlow或Numpy张量。如果不需要转换,可以将其设置为None。如果提供了tensor_type,则在BatchEncoding对象创建时,内部数据将被转换为相应类型的张量。
  • prepend_batch_axis (bool): 这是一个布尔值参数,默认为False。它指定当将整数列表转换为张量时,是否在结果张量前面添加一个额外的批处理维度。这与某些深度学习模型要求输入数据是批次形式(Batch)的要求相对应。如果设置为True,则所有的张量都将增加一个批次维度。
  • n_sequences (Optional[int]): 这是一个可选参数,用于指定在批处理时每批序列的数量。

这些参数一起定义了如何创建一个BatchEncoding实例,这个实例作为一个字典或字典样式对象,用来存储和管理tokenizer的输出数据以及与此数据相关的附加信息。

5.1.2.2. char_to_token()

char_to_token(batch_or_char_index: int, char_index: Optional[int] = None, sequence_index: int = 0) -> int: 返回 encoded output 中指定索引(索引相对于原始文本)的 character 所在位置的 token 的索引。

参数:

  • batch_or_char_index:一个整数,如果原始输入是一个 batch,则指定 character 位于第几个样本;如果原始输入是单个序列,则指定 character 的索引。
  • char_index :一个整数,配合 batch_or_char_index 使用,则它指定character 位于 batch 内哪个样本的哪个索引。
  • sequence_index:一个整数,如果输入是一对句子,则指定character 位于是第一个句子还是第二个句子。

返回值: 一个整数,表示对应的 token 的索引。

5.1.2.3. char_to_word()

char_to_word(batch_or_char_index: int, char_index: Optional[int] = None, sequence_index: int = 0) -> int or List[int] : 返回 encoded output 中指定索引(索引相对于原始文本)的 character 所在的 word 的索引。

参数: 参考 char_to_token()

返回值: 一个整数或整数列表,表示对应的 word 的索引。

5.1.2.4. convert_to_tensors()

convert_to_tensors(self, tensor_type: Optional[Union[str, TensorType]] = None, prepend_batch_axis: bool = False) :将内部内容转换为张量。

参数:参考 BatchEncoding.__init__() 方法。

5.1.2.5. sequence_ids()

sequence_ids(batch_index: int = 0) -> List[Optional[int]] :返回 sequence id 的列表,列表中每个元素表示每个 token 的 sequence id (即,是样本内的第几个句子)。

参数: batch_index:一个整数,指定 batch 内第几个序列。

返回值: 一个整数列表。

sequence id 表示原始句子的 id:

  • None:表示 special token 。
  • 0:表示 token 对应的单词位于第一个句子。
  • 1 :表示 token 对应的单词位于第二个句子。

5.1.2.6. to()

to(device: Union[str, torch.device]) -> BatchEncoding :将 BatchEncoding 移动到指定的设备上,仅用于 PyTorch 。

参数: device:一个字符串或者 torch.device,指定指定的设备。

返回: 相同的 BatchEncoding ,但是位于指定的设备上。

5.1.2.7. token_to_chars()

token_to_chars(batch_or_token_index: int, token_index: Optional[int] = None) -> CharSpan:返回 token 在原始字符串中的区间。

参数:

  • batch_or_token_index:一个整数,如果原始输入是一个 batch,则指定 token 位于第几个样本;如果原始输入是单个样本,则指定 token 的索引。
  • token_index:一个整数,配合 batch_or_token_index 使用,则它指定token 位于 batch 内哪个样本的哪个索引。

返回值: 一个 CharSpan ,表示对应的字符的区间( [a,b) 这种半闭半开区间)。

5.1.2.8. token_to_sequence()

token_to_sequence(batch_or_token_index: int, token_index: Optional[int] = None) -> int:返回 token 在原始输入的第几个句子。

参数: 参考 token_to_chars 。

返回值: 一个整数,表示 sequence id 。

sequence id 表示原始句子的 id:

  • None:表示 special token 。
  • 0:表示 token 对应的单词位于第一个句子。
  • 1 :表示 token 对应的单词位于第二个句子。

如果单个句子 的输入,那么该方法始终返回 0;如果是句子对输入,且 token 位于第二个句子 ,那么该方法返回 1

5.1.2.9. token_to_word()

token_to_word(batch_or_token_index: int, token_index: Optional[int] = None) -> int :返回 token 在原始输入的 word 的索引。

参数: 参考 token_to_chars 。

返回值: 一个整数,表示 word 的索引。

5.1.2.10. tokens()

tokens(batch_index: int = 0) -> List[str] :返回指定 batch 索引处的 token 列表。

参数: batch_index:一个整数,指定 batch 索引,参数默认为 0,表示第一个batch。

返回值: 一个字符串列表,表示 token 列表。

5.1.2.11. word_ids()

word_ids( batch_index: int = 0) -> List[Optional[int]] :返回指定 batch 索引处的 token 对应的 word 索引的列表。

参数: batch_index:一个整数,指定 batch 索引,参数默认为 0,表示第一个batch。

返回值: 一个整数列表,表示每个 token 对应的 word 索引。special token 被映射到 None 。

5.1.2.12. word_to_chars()

word_to_chars(batch_or_word_index: int, word_index: Optional[int] = None, sequence_index: int = 0) -> CharSpan :返回指定的单词在原始字符串中的区间。

参数:

  • batch_or_word_index:一个整数,如果原始输入是一个 batch,则指定 word 位于第几个样本;如果原始输入是单个样本,则指定 word 的索引。
  • word_index:一个整数,配合 batch_or_word_index 使用,则它指定word 位于 batch 内哪个样本的哪个索引。
  • sequence_index:一个整数,指定目标单词位于第一个句子还是第二个句子。

返回值: 一个 CharSpan 。

5.1.2.13. word_to_tokens()

word_to_tokens( batch_or_word_index: int, word_index: Optional[int] = None, sequence_index: int = 0) -> Optional[TokenSpan] :返回指定的单词对应的 token 的索引。

参数: 参考 word_to_chars 。

返回值: 一个 TokenSpan 。

5.1.2.14. words()

words( batch_index: int = 0) -> List[Optional[int]] :返回指定 batch 处每个 token 对应的单词的索引。

参数: batch_idex:一个整数,指定获取 batch 中第几个样本。

返回值: 一个整数列表,表示每个单词的索引。special token 将被映射到 None 。相同单词的不同 token 被映射到相同的单词索引(一个单词被分成多个token,但是对应同一个单词索引)。

5.1.3. 方法应用举例

python 复制代码
from transformers import BertTokenizerFast
tokenizer = BertTokenizerFast.from_pretrained("bert-base-cased")
result = tokenizer("This is the first line!", "This is the second line!") 

################# check result ###########
print(type(result))
# <class 'transformers.tokenization_utils_base.BatchEncoding'>
print(result)
# {'input_ids': [101, 1188, 1110, 1103, 1148, 1413, 106, 102, 1188, 1110, 1103, 1248, 1413, 106, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
print(result.tokens()) 
# ['[CLS]', 'This', 'is', 'the', 'first', 'line', '!', '[SEP]', 'This', 'is', 'the', 'second', 'line', '!', '[SEP]']
print(result.words()) # 每个 token 属于该句子的第几个单词
# [None, 0, 1, 2, 3, 4, 5, None, 0, 1, 2, 3, 4, 5, None]
print(result.word_ids()) # 每个 token 属于该句子的第几个单词
# [None, 0, 1, 2, 3, 4, 5, None, 0, 1, 2, 3, 4, 5, None]
print(result.sequence_ids()) # 每个 token 属于第几个句子
# [None, 0, 0, 0, 0, 0, 0, None, 1, 1, 1, 1, 1, 1, None]
print(result.is_fast) # 是否 fast tokenizer
# True
################## convert ################
print(result.char_to_token(3)) # 第一个句子第三个字符属于第几个 token 
# 1
print(result.char_to_token(3, sequence_index=1)) # 第二个句子第三个字符属于第几个 token 
# 8

print(result.token_to_chars(3)) # 第三个 token 在原始句子中的区间
# CharSpan(start=8, end=11)
print(result.token_to_chars(10)) # 第十个 token 在原始句子中的区间
# CharSpan(start=8, end=11) 

print(result.token_to_sequence(3)) # 第三个 token 是第一个句子还是第二个句子
# 0
print(result.token_to_sequence(10)) # 第十个 token 是第一个句子还是第二个句子
# 1

print(result.token_to_word(3)) # 第三个 token 是在该句子中的第几个单词
# 2
print(result.token_to_word(10)) # 第十个 token 是在该句子中的第几个单词
# 2

print(result.word_to_chars(3)) # 第一个句子第三个单词位于原始句子中的区间 
# CharSpan(start=12, end=17)
print(result.word_to_chars(3, sequence_index=1)) # 第二个句子第三个单词位于原始句子中的区间
# CharSpan(start=12, end=18)

print(result.word_to_tokens(0)) # 第一个句子第一个单词对应的 token 区间
# TokenSpan(start=1, end=2)
print(result.word_to_tokens(0, sequence_index=1)) # 第二个句子第一个单词对应的 token 区间
# TokenSpan(start=8, end=9)

6. 问答任务

6.1. 问答任务分类

大模型(Large Models),特别是在自然语言处理(NLP)领域,通常指的是拥有巨大数量参数和强大计算能力的深度学习模型。这些模型在各类问答(Question Answering, QA)任务上表现出色,能够处理从简单的事实查询到复杂的推理问题。以下是一些大模型在问答任务中常见的应用场景:

  1. 事实型问答(Factoid Question Answering)

这类任务涉及到对具体事实的查询,例如"首都是什么?"或"某事件何时发生?"等。模型需要从知识库或文档中提取确切的答案。

  1. 抽取式问答(Extractive Question Answering)

在给定的文本段落或文章中抽取出答案的片段。这要求模型能理解文本内容,并准确找到答案的开始和结束位置。

  1. 生成式问答(Generative Question Answering)

不同于直接从文本中抽取答案,生成式问答需要模型根据问题内容自主生成答案。这类任务对模型的理解能力和语言生成能力要求较高。

  1. 多选项问答(Multiple-Choice Question Answering)

给定一系列选项,模型必须从中选择正确的答案。这要求模型对每个选项进行评估并进行比较。

  1. 开放域问答(Open-Domain Question Answering)

在没有限定特定领域或上下文情况下寻找答案。模型需要从海量文本数据中寻找并确定答回答,挑战在于检索和理解的范围非常广泛。

  1. 常识性问答(Commonsense Question Answering)

常识性问答要求模型能理解和应用人类的常识知识来解答问题。例如,对"鸟为什么能飞?"的回答需要模型具备一定的物理学和生物学常识。

  1. 对话系统(Conversational Question Answering)

在一个连续的对话环境中回答问题,要求模型能够理解上下文和对话历史。这种类型的问答模型常应用于聊天机器人和虚拟助手。

  1. 可解释性和推理问答(Explainable and Reasoning QA)

不仅提供答案,还需要给出解释或解答过程。这对模型的逻辑推理和表达能力提出了更高的要求。

  1. 知识图谱问答(Knowledge Graph Question Answering)

利用知识图谱中的结构化信息来回答问题,这要求模型能理解图谱的结构和含义,并能够进行逻辑推理。

随着大模型如GPT-3、BERT、T5等的出现和不断改进,这些问答任务的处理能力得到了显著提升。而且,通过进一步的微调(fine-tuning)和适应(adaptation),这些模型可以针对特定的任务或领域进行优化,以期达到更佳的性能。

6.2. 抽取式问答数据预处理

接下来介绍抽取式问答数据预处理方法给训练数据打标,代码如下所示:

python 复制代码
max_length = 384
stride = 128

def preprocess_training_examples(examples):  # 1
    questions = [q.strip() for q in examples["question"]]  # 2
    inputs = tokenizer(  # 3
        questions,
        examples["context"],
        max_length=max_length,
        truncation="only_second",
        stride=stride,
        return_overflowing_tokens=True,
        return_offsets_mapping=True,
        padding="max_length",
    )

    offset_mapping = inputs.pop("offset_mapping")  # 4
    sample_map = inputs.pop("overflow_to_sample_mapping")  # 5
    answers = examples["answers"]  # 6
    start_positions = []  # 7
    end_positions = []

    for i, offset in enumerate(offset_mapping):  # 8
        sample_idx = sample_map[i]  # 9
        answer = answers[sample_idx]
        start_char = answer["answer_start"][0]
        end_char = answer["answer_start"][0] + len(answer["text"][0])
        sequence_ids = inputs.sequence_ids(i)

        # Find the start and end of the context
        idx = 0
        while sequence_ids[idx] != 1:
            idx += 1
        context_start = idx
        while sequence_ids[idx] == 1:
            idx += 1
        context_end = idx - 1

        # If the answer is not fully inside the context, label is (0, 0)
        if offset[context_start][0] > start_char or offset[context_end][1] < end_char:  # 10
            start_positions.append(0)
            end_positions.append(0)
        else:
            # Otherwise it's the start and end token positions
            idx = context_start  # 11
            while idx <= context_end and offset[idx][0] <= start_char:
                idx += 1
            start_positions.append(idx - 1)

            idx = context_end
            while idx >= context_start and offset[idx][1] >= end_char:
                idx -= 1
            end_positions.append(idx + 1)

    inputs["start_positions"] = start_positions # 12
    inputs["end_positions"] = end_positions
    return inputs  # 13

这个preprocess_training_examples函数是为机器学习训练预处理文本示例的,特别是在自然语言处理(NLP)中处理问答(QA)任务。它接收含有问题(question)、上下文(context)和答案(answers)的示例,然后使用给定的tokenizer进行处理,以生成适合模型训练的格式化数据。下面是该函数每行代码的详细解释:

  1. 定义了一个函数preprocess_training_examples,它接收examples作为参数,这些examples是一个字典,包含了questioncontextanswers等关键字。
  2. 使用列表推导式提取所有问题(questions),并去除每个问题字符串两端的空白字符。
  3. 调用tokenizer函数对问题和上下文执行tokenization。
    • 指定max_length为384,表示tokenization之后的最大长度不超过384个token。
    • truncation="only_second"指定只有当两个sequence联合长度超过max_length时,仅截断第二个sequence(在本例中,是examples["context"])。
    • stride=128在分割长文本时每次滑动的token数,用于创建重叠的部分,以捕获边界情况。
    • return_overflowing_tokens=True在输入sequence过长而被截断时,返回所有可能的子sequence。
    • return_offsets_mapping=True返回每个token在原始文本中的字符位置偏移量。
    • padding="max_length"对于短于max_length的输入,在尾部添加padding,保证所有输入长度一致。
  1. 移除返回字典中的offset_mapping,该映射提供了从token到原始文本的字符范围的映射。
  2. 移除返回字典中的overflow_to_sample_mapping,该映射提供了当前处理的overflowed tokenized 输入与原始样本的映射关系,以便知道哪个输入对应原始数据中的哪个示例。
  3. 获取原始答案文本和答案开始的位置。
  4. 初始化两个空列表start_positionsend_positions,用于存储每个答案在tokenized文本中的起始和结束token的位置。
  5. 迭代处理每个处理后的示例(考虑有可能的overflow)。
  6. 通过迭代offset_mappingsequence_ids标识上下文在tokenized序列中的开始和结束位置。sequence_ids返回的是每个token属于问题还是上下文的标识,None表示特殊token(如CLS、SEP等)。
  7. 如果找到的答案不完全位于上下文中,通过比较字符级别的开始和结束位置与对应的offset,如果不匹配则将答案的起始和结束标记位置设为(0, 0)。
  8. 如果答案在上下文中,确定答案的起始和结束token的位置。首先,递增地搜索直到找到包含答案起始字符的token;其次,递减地搜索直到找到包含答案结束字符的token。
  9. start_positionsend_positions添加到inputs字典中。
  10. 返回包含了预处理信息的inputs字典。

该函数的目的是准备好用于模型训练的数据,确保每个示例中问题、上下文、答案的位置能够正确对应,并处理可能超长的文本条目。

参考文档

github.com/huggingface...

huggingface.co/docs/transf...

huggingface.co/docs/transf...

huggingface.co/docs/tokeni...

Question answering

深入理解 tokenizer

platform.openai.com/tokenizer

A Span-Extraction Dataset for Chinese Machine Reading Comprehension (CMRC 2018)

Fast tokenizers in the QA pipeline

相关推荐
好悬给我拽开线29 分钟前
【】AI八股-神经网络相关
人工智能·深度学习·神经网络
容若只如初见2 小时前
项目实战--Spring Boot + Minio文件切片上传下载
java·spring boot·后端
码农爱java3 小时前
Spring Boot 中的监视器是什么?有什么作用?
java·spring boot·后端·面试·monitor·监视器
Apifox.4 小时前
什么是 HTTP POST 请求?初学者指南与示范
后端·http·学习方法·web
无名指的等待7124 小时前
SpringBoot实现图片添加水印(完整)
java·spring boot·后端
江畔柳前堤5 小时前
CV01_相机成像原理与坐标系之间的转换
人工智能·深度学习·数码相机·机器学习·计算机视觉·lstm
qq_526099136 小时前
为什么要在成像应用中使用图像采集卡?
人工智能·数码相机·计算机视觉
码上飞扬6 小时前
深度解析:机器学习与深度学习的关系与区别
人工智能·深度学习·机器学习
super_Dev_OP6 小时前
Web3 ETF的主要功能
服务器·人工智能·信息可视化·web3
Sui_Network6 小时前
探索Sui的面向对象模型和Move编程语言
大数据·人工智能·学习·区块链·智能合约