diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 3a62cefe..fbb122e4 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -13,6 +13,9 @@ atexit.register(_set_python_is_exit) class Batch(object): """ + + .. _Batch: + Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出. 组成 `x` 和 `y` diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 87d09cf5..f329623e 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -56,7 +56,7 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 seq_len=3)) dataset = DataSet(instances) -2. DataSet的基本使用 +2. DataSet与预处理 1. 从某个文本文件读取内容 # TODO 引用DataLoader Example:: @@ -97,6 +97,12 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 dataset.apply(lambda ins: ins['sentence'].split(), new_field_name='words') # 或使用DataSet.apply_field() dataset.apply(lambda sent:sent.split(), field_name='sentence', new_field_name='words') + # 除了匿名函数,也可以定义函数传递进去 + def get_words(instance): + sentence = instance['sentence'] + words = sentence.split() + return words + dataset.apply(get_words, new_field_name='words') 4. 删除DataSet的内容 @@ -108,14 +114,151 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 dropped_dataset = dataset.drop(lambda ins:ins['a']<0, inplace=False) # 在dataset中删除满足条件的instance dataset.drop(lambda ins:ins['a']<0) # dataset的instance数量减少 + # 删除第3个instance + dataset.delete_instance(2) + # 删除名为'a'的field + dataset.delete_field('a') + 5. 遍历DataSet的内容 + + Example:: + + for instance in dataset: + # do something + + 6. 一些其它操作 + + Example:: + + # 检查是否存在名为'a'的field + dataset.has_field('a') # 或 ('a' in dataset) + # 将名为'a'的field改名为'b' + dataset.rename_field('a', 'b') + # DataSet的长度 + len(dataset) + +3. DataSet与自然语言处理(NLP) + 在目前深度学习的模型中,大都依赖于随机梯度下降法(SGD)进行模型的优化。随机梯度下降需要将数据切分成一个一个的Batch, + 一个Batch进行一次前向计算(forward)与梯度后向传播(backward)。在自然语言处理的场景下,往往还需要对数据进行pad。这是 + 由于句子的长度一般是不同的,但是一次Batch中的每个field都必须是一个tensor,所以需要将所有句子都补齐到相同的长度。 + + 1. DataSet与Batch + + 我们先看fastNLP中如何将数据分成一个一个的Batch的例子, 这里我们使用随机生成的数据来模拟一个二分类文本分类任务, + words和characters是输入,labels是文本类别 + + Example:: + + from fastNLP import DataSet + from fastNLP import Batch + from fastNLP import SequentialSampler + from fastNLP import EngChar2DPadder + + num_instances = 100 + # 假设每句话最少2个词,最多5个词; 词表的大小是100个; 一共26个字母,每个单词最短1个字母,最长5个字母 + lengths = [random.randint(2, 5) for _ in range(num_instances)] + data = {'words': [[random.randint(1, 100) for _ in range(lengths[idx]) ] for idx in range(num_instances)], + 'chars': [ + [[random.randint(1, 27) for _ in range(random.randint(1, 5))] + for _ in range(lengths[idx])] + for idx in range(num_instances)], + 'label': [random.randint(0, 1) for _ in range(num_instances)]} + + d = DataSet(data) + d.set_padder('chars', EngChar2DPadder()) # 因为英文character的pad方式与word的pad方式不一样 + + d.set_target('label') + d.set_input('words', 'chars') + + for batch_x, batch_y in Batch(d, sampler=SequentialSampler(), batch_size=2): + print("batch_x:", batch_x) + print("batch_y:", batch_y) + break + # 输出为 + # {'words': tensor([[49, 27, 20, 36, 63], + # [53, 82, 23, 11, 0]]), 'chars': tensor([[[13, 3, 14, 25, 1], + # [ 8, 20, 12, 0, 0], + # [27, 8, 0, 0, 0], + # [ 1, 15, 26, 0, 0], + # [11, 24, 17, 0, 0]], + # + # [[ 6, 14, 11, 27, 22], + # [18, 6, 4, 19, 0], + # [19, 22, 9, 0, 0], + # [10, 25, 0, 0, 0], + # [ 0, 0, 0, 0, 0]]])} + # {'label': tensor([0, 0])} + + 其中 Batch_ 是用于从DataSet中按照batch_size为大小取出batch的迭代器, SequentialSampler_ 用于指示 Batch_ 以怎样的 + 顺序从DataSet中取出instance以组成一个batch,更详细的说明请参照 Batch_ 和 SequentialSampler_ 文档。 + + 通过DataSet.set_input('words', 'chars'), fastNLP将认为'words'和'chars'这两个field都是input,并将它们都放入迭代器 + 生成的第一个dict中; DataSet.set_target('labels'), fastNLP将认为'labels'这个field是target,并将其放入到迭代器的第 + 二个dict中。如上例中所打印结果。分为input和target的原因是由于它们在被 Trainer_ 所使用时会有所差异,详见 Trainer_ + 。 + + 当把某个field设置为'target'或者'input'的时候(两者不是互斥的,可以同时设为input和target),fastNLP不仅仅只是将其放 + 置到不同的dict中,而还会对被设置为input或target的field进行类型检查。类型检查的目的是为了看能否把该field转为 + pytorch的torch.LongTensor或torch.FloatTensor类型(也可以在Batch中设置输出numpy类型,参考 Batch_ ),如上例所示, + fastNLP已将words,chars和label转为了Tensor类型。如果field在每个instance都拥有相同的维度(不能超过两维),且最内层 + 的元素都为相同的type(int, float, np.int*, np.float*),则fastNLP默认将对该field进行pad。也支持全为str的field作为 + target和input,这种情况下,fastNLP默认不进行pad。另外,当某个field已经被设置为了target或者input后,之后append的 + instance对应的field必须要和前面已有的内容一致,否则会报错。 + + 如果某个field中出现了多种类型混合(比如一部分为str,一部分为int)的情况,fastNLP无法判断该field的类型,会报如下的 + 错误: + + Example:: + + from fastNLP import DataSet + d = DataSet({'data': [1, 'a']}) + d.set_input('data') + >> RuntimeError: Mixed data types in Field data: [, ] + + 可以通过设置以忽略对该field进行类型检查 + + Example:: + + from fastNLP import DataSet + d = DataSet({'data': [1, 'a']}) + d.set_ignore_type('data') + d.set_input('data') + + 当某个field被设置为忽略type之后,fastNLP将不对其进行pad。 + + 2. DataSet与pad + + 在fastNLP里,pad是与一个field绑定的。即不同的field可以使用不同的pad方式,比如在英文任务中word需要的pad和 + character的pad方式往往是不同的。fastNLP是通过一个叫做 Padder_ 的子类来完成的。默认情况下,所有field使用 AutoPadder_ + 。可以通过使用以下方式设置Padder(如果将padder设置为None,则该field不会进行pad操作)。大多数情况下直接使用 AutoPadder_ + 就可以了。如果 AutoPadder_ 或 EngChar2DPadder_ 无法满足需求,也可以自己写一个 Padder_ 。 + + Example:: + + from fastNLP import DataSet + from fastNLP import EngChar2DPadder + import random + dataset = DataSet() + max_chars, max_words, sent_num = 5, 10, 20 + contents = [[ + [random.randint(1, 27) for _ in range(random.randint(1, max_chars))] + for _ in range(random.randint(1, max_words)) + ] for _ in range(sent_num)] + # 初始化时传入 + dataset.add_field('chars', contents, padder=EngChar2DPadder()) + # 直接设置 + dataset.set_padder('chars', EngChar2DPadder()) + # 也可以设置pad的value + dataset.set_pad_val('chars', -1) + """ import _pickle as pickle import numpy as np +import warnings from fastNLP.core.fieldarray import AutoPadder from fastNLP.core.fieldarray import FieldArray @@ -123,19 +266,15 @@ from fastNLP.core.instance import Instance from fastNLP.core.utils import get_func_signature class DataSet(object): - """DataSet is the collection of examples. - DataSet provides instance-level interface. You can append and access an instance of the DataSet. - However, it stores data in a different way: Field-first, Instance-second. + """fastNLP的数据容器 """ def __init__(self, data=None): """ - :param data: a dict or a list. - If `data` is a dict, the key is the name of a FieldArray and the value is the FieldArray. All values - must be of the same length. - If `data` is a list, it must be a list of Instance objects. + :param dict,list(Instance) data: 如果为dict类型,则每个key的value应该为等长的list; 如果为list,每个元素应该为具 + :有相同field的 instance_ 。 """ self.field_arrays = {} if data is not None: @@ -243,9 +382,8 @@ class DataSet(object): def append(self, instance): """将一个instance对象append到DataSet后面。 - If the DataSet is not empty, the instance must have the same field names as the rest instances in the DataSet. - :param instance: an Instance object + :param Instance instance: 若DataSet不为空,则instance应该拥有和DataSet完全一样的field。 """ if len(self.field_arrays) == 0: @@ -282,10 +420,11 @@ class DataSet(object): :param str field_name: 新增的field的名称 :param list fields: 需要新增的field的内容 - :param None,Padder padder: 如果为None,则不进行pad。 + :param None,Padder padder: 如果为None,则不进行pad,默认使用 AutoPadder_ 自动判断是否需要做pad。 :param bool is_input: 新加入的field是否是input :param bool is_target: 新加入的field是否是target :param bool ignore_type: 是否忽略对新加入的field的类型检查 + :return: DataSet """ if len(self.field_arrays) != 0: @@ -294,13 +433,32 @@ class DataSet(object): f"Dataset size {len(self)} != field size {len(fields)}") self.field_arrays[field_name] = FieldArray(field_name, fields, is_target=is_target, is_input=is_input, padder=padder, ignore_type=ignore_type) + return self + + def delete_instance(self, index): + """删除第index个instance + + :param int index: 需要删除的instance的index,从0开始 + :return: DataSet + """ + assert isinstance(index, int), "Only integer supported." + if len(self)<=index: + raise IndexError("{} is too large for as DataSet with {} instances.".format(index, len(self))) + if len(self)==1: + self.field_arrays.clear() + else: + for field in self.field_arrays.values(): + field.pop(index) + return self def delete_field(self, field_name): """删除名为field_name的field :param str field_name: 需要删除的field的名称. + :return: DataSet """ self.field_arrays.pop(field_name) + return self def has_field(self, field_name): """判断DataSet中是否有field_name这个field @@ -332,15 +490,15 @@ class DataSet(object): def get_length(self): """获取DataSet的元素数量 - :return: int length: + :return: int length: DataSet中Instance的个数。 """ return len(self) def rename_field(self, old_name, new_name): """将某个field重新命名. - :param str old_name: 原来的field名称 - :param str new_name: 修改为new_name + :param str old_name: 原来的field名称。 + :param str new_name: 修改为new_name。 """ if old_name in self.field_arrays: self.field_arrays[new_name] = self.field_arrays.pop(old_name) @@ -349,7 +507,8 @@ class DataSet(object): raise KeyError("DataSet has no field named {}.".format(old_name)) def set_target(self, *field_names, flag=True): - """将field_names的target设置为flag状态 + """将field_names的field设置为target + Example:: dataset.set_target('labels', 'seq_len') # 将labels和seq_len这两个field的target属性设置为True @@ -366,7 +525,8 @@ class DataSet(object): raise KeyError("{} is not a valid field name.".format(name)) def set_input(self, *field_names, flag=True): - """将field_name的input设置为flag状态 + """将field_names的field设置为input + Example:: dataset.set_input('words', 'seq_len') # 将words和seq_len这两个field的input属性设置为True @@ -382,7 +542,8 @@ class DataSet(object): raise KeyError("{} is not a valid field name.".format(name)) def set_ignore_type(self, *field_names, flag=True): - """将field_names的ignore_type设置为flag状态 + """将field设置为忽略类型状态。当某个field被设置了ignore_type, 则在被设置为target或者input时将不进行类型检查,默 + 认情况下也不进行pad。 :param str field_names: field的名称 :param bool flag: 将field_name的ignore_type状态设置为flag @@ -397,6 +558,7 @@ class DataSet(object): def set_padder(self, field_name, padder): """为field_name设置padder + Example:: from fastNLP import EngChar2DPadder @@ -404,7 +566,7 @@ class DataSet(object): dataset.set_padder('chars', padder) # 则chars这个field会使用EngChar2DPadder进行pad操作 :param str field_name: 设置field的padding方式为padder - :param None, Padder padder: 设置为None即删除padder, 即对该field不进行pad操作. + :param None, Padder padder: 设置为None即删除padder, 即对该field不进行pad操作。 :return: """ if field_name not in self.field_arrays: @@ -437,12 +599,12 @@ class DataSet(object): return [name for name, field in self.field_arrays.items() if field.is_target] def apply_field(self, func, field_name, new_field_name=None, **kwargs): - """将DataSet中的每个instance中的`field_name`这个field传给func,并获取它的返回值. + """将DataSet中的每个instance中的`field_name`这个field传给func,并获取它的返回值。 :param callable func: input是instance的`field_name`这个field的内容。 :param str field_name: 传入func的是哪个field。 :param None,str new_field_name: 将func返回的内容放入到new_field_name这个field中,如果名称与已有的field相同,则覆 - :盖之前的field。如果为None则不创建新的field。 + 盖之前的field。如果为None则不创建新的field。 :param optional kwargs: 支持输入is_input,is_target,ignore_type 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input @@ -509,7 +671,7 @@ class DataSet(object): :param callable func: 参数是DataSet中的Instance :param None,str new_field_name: 将func返回的内容放入到new_field_name这个field中,如果名称与已有的field相同,则覆 - :盖之前的field。如果为None则不创建新的field。 + 盖之前的field。如果为None则不创建新的field。 :param optional kwargs: 支持输入is_input,is_target,ignore_type 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input @@ -539,10 +701,11 @@ class DataSet(object): return results def drop(self, func, inplace=True): - """func接受一个instance,返回bool值,返回值为True时,该instance会被移除或者加入到返回的DataSet中。 + """func接受一个Instance,返回bool值。返回值为True时,该Instance会被移除或者加入到返回的DataSet中。 - :param callable func: 接受一个instance作为参数,返回bool值。为True时删除该instance - :param bool inplace: 是否在当前DataSet中直接删除instance。如果为False,返回值被删除的instance的组成的新DataSet + :param callable func: 接受一个Instance作为参数,返回bool值。为True时删除该instance + :param bool inplace: 是否在当前DataSet中直接删除instance。如果为False,被删除的Instance的组成的新DataSet将作为 + :返回值 :return: DataSet """ @@ -564,7 +727,7 @@ class DataSet(object): def split(self, ratio): """将DataSet按照ratio的比例拆分,返回两个DataSet - :param float ratio: 0