From 257eb2b9eb0d878e4578f2385b193caf971e6438 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sat, 4 May 2019 17:05:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E9=83=A8=E5=88=86=E7=9A=84=E4=B8=80=E5=A4=A7=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=8CTODO=EF=BC=9A=201.=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=20trainer=20=E5=92=8C=20tester=20=E9=83=A8=E5=88=86?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=A1=A3=202.=20=E7=A0=94=E7=A9=B6=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=A0=B7=E4=BE=8B=E4=B8=8E=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/__init__.py | 16 +- fastNLP/core/batch.py | 63 +- fastNLP/core/dataset.py | 708 ++++++++++++----------- fastNLP/core/{fieldarray.py => field.py} | 82 +-- fastNLP/core/instance.py | 49 +- fastNLP/core/vocabulary.py | 9 +- 6 files changed, 500 insertions(+), 427 deletions(-) rename fastNLP/core/{fieldarray.py => field.py} (86%) diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 087882aa..f11f0e12 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -1,6 +1,20 @@ +""" +core 模块里实现了 fastNLP 的核心框架,常用的组件都可以从 fastNLP 包中直接 import。当然你也同样可以从 core 模块的子模块中 import, +例如 Batch 组件有两种 import 的方式:: + + # 直接从 fastNLP 中 import + from fastNLP import Batch + + # 从 core 模块的子模块 batch 中 import + from fastNLP.core.batch import Batch + +对于常用的功能,你只需要在 :doc:`fastNLP` 中查看即可。如果想了解各个子模块的分工,您可以阅读以下文档: + + +""" from .batch import Batch from .dataset import DataSet -from .fieldarray import FieldArray +from .field import FieldArray, Padder, AutoPadder, EngChar2DPadder from .instance import Instance from .losses import LossFunc, CrossEntropyLoss, L1Loss, BCELoss, NLLLoss, LossInForward from .metrics import AccuracyMetric diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index fbb122e4..4af6d651 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -1,24 +1,34 @@ +""" +batch 模块实现了 fastNLP 所需的 Batch 类。 + +""" +__all__ = ["Batch"] import numpy as np import torch import atexit -from fastNLP.core.sampler import RandomSampler, Sampler +from .sampler import RandomSampler, Sampler import torch.multiprocessing as mp _python_is_exit = False + + def _set_python_is_exit(): global _python_is_exit _python_is_exit = True + + atexit.register(_set_python_is_exit) + class Batch(object): """ - - .. _Batch: + 别名::class:`fastNLP.Batch` :class:`fastNLP.core.batch.Batch` Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出. 组成 `x` 和 `y` + Example:: batch = Batch(data_set, batch_size=16, sampler=SequentialSampler()) @@ -26,16 +36,19 @@ class Batch(object): for batch_x, batch_y in batch: # do stuff ... - :param DataSet dataset: `DataSet` 对象, 数据集 + :param dataset: :class:`~fastNLP.DataSet` 对象, 数据集 :param int batch_size: 取出的batch大小 - :param Sampler sampler: 规定使用的 Sample 方式. 若为 ``None`` , 使用 RandomSampler. + :param sampler: 规定使用的 :class:`~fastNLP.Sampler` 方式. 若为 ``None`` , 使用 :class:`~fastNLP.RandomSampler`. + Default: ``None`` - :param bool as_numpy: 若为 ``True`` , 输出batch为 numpy.array. 否则为 torch.Tensor. + :param bool as_numpy: 若为 ``True`` , 输出batch为 numpy.array. 否则为 :class:`torch.Tensor`. + Default: ``False`` :param bool prefetch: 若为 ``True`` 使用多进程预先取出下一batch. + Default: ``False`` """ - + def __init__(self, dataset, batch_size, sampler=None, as_numpy=False, prefetch=False): self.dataset = dataset self.batch_size = batch_size @@ -49,17 +62,17 @@ class Batch(object): self.cur_batch_indices = None self.prefetch = prefetch self.lengths = 0 - - def _fetch_one(self): + + def fetch_one(self): if self.curidx >= len(self.idx_list): return None else: endidx = min(self.curidx + self.batch_size, len(self.idx_list)) batch_x, batch_y = {}, {} - + indices = self.idx_list[self.curidx:endidx] self.cur_batch_indices = indices - + for field_name, field in self.dataset.get_all_fields().items(): if field.is_target or field.is_input: batch = field.get(indices) @@ -69,10 +82,10 @@ class Batch(object): batch_y[field_name] = batch if field.is_input: batch_x[field_name] = batch - + self.curidx = endidx return batch_x, batch_y - + def __iter__(self): """ Iterate on dataset, fetch batch data. Fetch process don't block the iterate process @@ -80,25 +93,28 @@ class Batch(object): """ if self.prefetch: return _run_batch_iter(self) + def batch_iter(): - self._init_iter() + self.init_iter() while 1: - res = self._fetch_one() + res = self.fetch_one() if res is None: break yield res + return batch_iter() - - def _init_iter(self): + + def init_iter(self): self.idx_list = self.sampler(self.dataset) self.curidx = 0 self.lengths = self.dataset.get_length() - + def __len__(self): return self.num_batches - + def get_batch_indices(self): - """取得当前batch在DataSet中所在的index下标序列 + """ + 取得当前batch在DataSet中所在的index下标序列 :return list(int) indexes: 下标序列 """ @@ -118,16 +134,16 @@ def _to_tensor(batch, dtype): def _run_fetch(batch, q): global _python_is_exit - batch._init_iter() + batch.init_iter() # print('start fetch') while 1: - res = batch._fetch_one() + res = batch.fetch_one() # print('fetch one') while 1: try: q.put(res, timeout=3) break - except Exception as e: + except: if _python_is_exit: return if res is None: @@ -159,4 +175,3 @@ def _run_batch_iter(batch): fetch_p.terminate() fetch_p.join() # print('iter done') - diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 82288221..28bb2010 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -1,7 +1,7 @@ """ -DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格,每一行是一个sample(在fastNLP中被称为Instance),每一列是一个feature(在fastNLP中称为field)。 - - .. _DataSet: +DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格, +每一行是一个sample (在fastNLP中被称为 :mod:`~.instance` ), +每一列是一个feature (在fastNLP中称为 :mod:`.field` )。 .. csv-table:: Following is a demo layout of DataSet :header: "sentence", "words", "seq_len" @@ -11,285 +11,293 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 "Third instance .", "[Third, instance, .]", 3 "...", "[...]", "..." -在fastNLP内部每一行是一个 Instance_ 对象; 每一列是一个 FieldArray_ 对象。 - -1. DataSet的创建 +在fastNLP内部每一行是一个 :class:`~fastNLP.Instance` 对象; 每一列是一个 :class:`~fastNLP.FieldArray` 对象。 +1 DataSet的创建 创建DataSet主要有以下的3种方式 - 1. 传入dict +1.1 传入dict - Example:: + Example:: - from fastNLP import DataSet - data = {'sentence':["This is the first instance .", "Second instance .", "Third instance ."], - 'words': [['this', 'is', 'the', 'first', 'instance', '.'], ['Second', 'instance', '.'], ['Third', 'instance', '.'], - 'seq_len': [6, 3, 3]} - dataset = DataSet(data) - # 传入的dict的每个key的value应该为具有相同长度的list + from fastNLP import DataSet + data = {'sentence':["This is the first instance .", "Second instance .", "Third instance ."], + 'words': [['this', 'is', 'the', 'first', 'instance', '.'], ['Second', 'instance', '.'], ['Third', 'instance', '.'], + 'seq_len': [6, 3, 3]} + dataset = DataSet(data) + # 传入的dict的每个key的value应该为具有相同长度的list - 2. 通过构建Instance +1.2 通过构建Instance - Example:: + Example:: - from fastNLP import DataSet - from fastNLP import Instance - dataset = DataSet() - instance = Instance(sentence="This is the first instance", - words=['this', 'is', 'the', 'first', 'instance', '.'], - seq_len=6) - dataset.append(instance) - # 可以继续append更多内容,但是append的instance应该和第一个instance拥有完全相同的field + from fastNLP import DataSet + from fastNLP import Instance + dataset = DataSet() + instance = Instance(sentence="This is the first instance", + words=['this', 'is', 'the', 'first', 'instance', '.'], + seq_len=6) + dataset.append(instance) + # 可以继续append更多内容,但是append的instance应该和第一个instance拥有完全相同的field + +1.3 通过list(Instance) + + Example:: - 3. 通过list(Instance) + from fastNLP import DataSet + from fastNLP import Instance + instances = [] + instances.append(Instance(sentence="This is the first instance", + words=['this', 'is', 'the', 'first', 'instance', '.'], + seq_len=6)) + instances.append(Instance(sentence="Second instance .", + words=['Second', 'instance', '.'], + seq_len=3)) + dataset = DataSet(instances) - Example:: +2 DataSet与预处理 + 常见的预处理有如下几种 + +2.1 从某个文本文件读取内容 # TODO 引用DataLoader + + Example:: from fastNLP import DataSet from fastNLP import Instance - instances = [] - instances.append(Instance(sentence="This is the first instance", - words=['this', 'is', 'the', 'first', 'instance', '.'], - seq_len=6)) - instances.append(Instance(sentence="Second instance .", - words=['Second', 'instance', '.'], - seq_len=3)) - dataset = DataSet(instances) - -2. DataSet与预处理 - 1. 从某个文本文件读取内容 # TODO 引用DataLoader + dataset = DataSet() + filepath='some/text/file' + # 假设文件中每行内容如下(sentence label): + # This is a fantastic day positive + # The bad weather negative + # ..... + with open(filepath, 'r') as f: + for line in f: + sent, label = line.strip().split('\t') + dataset.append(Instance(sentence=sent, label=label)) - Example:: +2.2 index, 返回结果为对DataSet对象的浅拷贝 - from fastNLP import DataSet - from fastNLP import Instance - dataset = DataSet() - filepath='some/text/file' - # 假设文件中每行内容如下(sentence label): - # This is a fantastic day positive - # The bad weather negative - # ..... - with open(filepath, 'r') as f: - for line in f: - sent, label = line.strip().split('\t') - dataset.append(Instance(sentence=sent, label=label)) - - 2. index, 返回结果为对DataSet对象的浅拷贝 + Example:: - Example:: + import numpy as np + from fastNLP import DataSet + dataset = DataSet({'a': np.arange(10), 'b': [[_] for _ in range(10)]}) + d[0] # 使用一个下标获取一个instance + >>{'a': 0 type=int,'b': [2] type=list} # 得到一个instance + d[1:3] # 使用slice获取一个新的DataSet + >>DataSet({'a': 1 type=int, 'b': [2] type=list}, {'a': 2 type=int, 'b': [2] type=list}) - import numpy as np - from fastNLP import DataSet - dataset = DataSet({'a': np.arange(10), 'b': [[_] for _ in range(10)]}) - d[0] # 使用一个下标获取一个instance - >>{'a': 0 type=int,'b': [2] type=list} # 得到一个instance - d[1:3] # 使用slice获取一个新的DataSet - >>DataSet({'a': 1 type=int, 'b': [2] type=list}, {'a': 2 type=int, 'b': [2] type=list}) +2.3 对DataSet中的内容处理 - 3. 对DataSet中的内容处理 + Example:: - Example:: + from fastNLP import DataSet + data = {'sentence':["This is the first instance .", "Second instance .", "Third instance ."]} + dataset = DataSet(data) + # 将句子分成单词形式, 详见DataSet.apply()方法 + 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') - from fastNLP import DataSet - data = {'sentence':["This is the first instance .", "Second instance .", "Third instance ."]} - dataset = DataSet(data) - # 将句子分成单词形式, 详见DataSet.apply()方法 - 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的内容 +2.4 删除DataSet的内容 - Example:: + Example:: - from fastNLP import DataSet - dataset = DataSet({'a': list(range(-5, 5))}) - # 返回满足条件的instance,并放入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') + from fastNLP import DataSet + dataset = DataSet({'a': list(range(-5, 5))}) + # 返回满足条件的instance,并放入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的内容 +2.5 遍历DataSet的内容 - Example:: + Example:: - for instance in dataset: - # do something + for instance in dataset: + # do something - 6. 一些其它操作 +2.6 一些其它操作 - Example:: + Example:: - # 检查是否存在名为'a'的field - dataset.has_field('a') # 或 ('a' in dataset) - # 将名为'a'的field改名为'b' - dataset.rename_field('a', 'b') - # DataSet的长度 - len(dataset) + # 检查是否存在名为'a'的field + dataset.has_field('a') # 或 ('a' in dataset) + # 将名为'a'的field改名为'b' + dataset.rename_field('a', 'b') + # DataSet的长度 + len(dataset) -3. DataSet与自然语言处理(NLP) +3 DataSet与自然语言处理(NLP) 在目前深度学习的模型中,大都依赖于随机梯度下降法(SGD)进行模型的优化。随机梯度下降需要将数据切分成一个一个的Batch, 一个Batch进行一次前向计算(forward)与梯度后向传播(backward)。在自然语言处理的场景下,往往还需要对数据进行pad。这是 由于句子的长度一般是不同的,但是一次Batch中的每个field都必须是一个tensor,所以需要将所有句子都补齐到相同的长度。 - 1. DataSet与Batch +3.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])} + + 其中 :class:`~fastNLP.Batch` 是用于从DataSet中按照batch_size为大小取出batch的迭代器, + :class:`~fastNLP.SequentialSampler` 用于指示 Batch 以怎样的 + 顺序从DataSet中取出instance以组成一个batch, + 更详细的说明请参照 :class:`~fastNLP.Batch` 和 :class:`~fastNLP.SequentialSampler` 文档。 + + 通过DataSet.set_input('words', 'chars'), fastNLP将认为'words'和'chars'这两个field都是input,并将它们都放入迭代器 + 生成的第一个dict中; DataSet.set_target('labels'), fastNLP将认为'labels'这个field是target,并将其放入到迭代器的第 + 二个dict中。如上例中所打印结果。分为input和target的原因是由于它们在被 :class:`~fastNLP.Trainer` 所使用时会有所差异, + 详见 :class:`~fastNLP.Trainer` + + 当把某个field设置为'target'或者'input'的时候(两者不是互斥的,可以同时设为input和target),fastNLP不仅仅只是将其放 + 置到不同的dict中,而还会对被设置为input或target的field进行类型检查。类型检查的目的是为了看能否把该field转为 + pytorch的torch.LongTensor或torch.FloatTensor类型(也可以在Batch中设置输出numpy类型,参考 :class:`~fastNLP.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的dtype + + Example:: + + from fastNLP import DataSet + + d = DataSet({'a': [0, 1, 3], 'b':[[1.0, 2.0], [0.1, 0.2], [3]]}) + d.set_input('a', 'b') + d.a.dtype + >> numpy.int64 + d.b.dtype + >> numpy.float64 + # 默认情况下'a'这个field将被转换为torch.LongTensor,但如果需要其为torch.FloatTensor可以手动修改dtype + d.a.dtype = float # 请确保该field的确可以全部转换为float。 - 我们先看fastNLP中如何将数据分成一个一个的Batch的例子, 这里我们使用随机生成的数据来模拟一个二分类文本分类任务, - words和characters是输入,labels是文本类别 + 如果某个field中出现了多种类型混合(比如一部分为str,一部分为int)的情况,fastNLP无法判断该field的类型,会报如下的 + 错误: Example:: from fastNLP import DataSet - from fastNLP import Batch - from fastNLP import SequentialSampler - from fastNLP import EngChar2DPadder + d = DataSet({'data': [1, 'a']}) + d.set_input('data') + >> RuntimeError: Mixed data types in Field data: [, ] - 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的dtype - - Example:: - from fastNLP import DataSet - - d = DataSet({'a': [0, 1, 3], 'b':[[1.0, 2.0], [0.1, 0.2], [3]]}) - d.set_input('a', 'b') - d.a.dtype - >>>numpy.int64 - d.b.dtype - >>>numpy.float64 - # 默认情况下'a'这个field将被转换为torch.LongTensor,但如果需要其为torch.FloatTensor可以手动修改dtype - d.a.dtype = float # 请确保该field的确可以全部转换为float。 - - 如果某个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_ 。 + 可以通过设置以忽略对该field进行类型检查 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) + d = DataSet({'data': [1, 'a']}) + d.set_ignore_type('data') + d.set_input('data') -""" + 当某个field被设置为忽略type之后,fastNLP将不对其进行pad。 + +3.2 DataSet与pad + + 在fastNLP里,pad是与一个field绑定的。即不同的field可以使用不同的pad方式,比如在英文任务中word需要的pad和 + character的pad方式往往是不同的。fastNLP是通过一个叫做 :class:`~fastNLP.Padder` 的子类来完成的。 + 默认情况下,所有field使用 :class:`~fastNLP.AutoPadder` + 。可以通过使用以下方式设置Padder(如果将padder设置为None,则该field不会进行pad操作)。 + 大多数情况下直接使用 :class:`~fastNLP.AutoPadder` 就可以了。 + 如果 :class:`~fastNLP.AutoPadder` 或 :class:`~fastNLP.EngChar2DPadder` 无法满足需求, + 也可以自己写一个 :class:`~fastNLP.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) + + +""" +__all__ = ["DataSet"] import _pickle as pickle import numpy as np import warnings -from fastNLP.core.fieldarray import AutoPadder -from fastNLP.core.fieldarray import FieldArray -from fastNLP.core.instance import Instance -from fastNLP.core.utils import _get_func_signature +from .field import AutoPadder +from .field import FieldArray +from .instance import Instance +from .utils import _get_func_signature -class DataSet(object): - """fastNLP的数据容器 +class DataSet(object): """ + 别名::class:`fastNLP.DataSet` :class:`fastNLP.core.dataset.DataSet` + fastNLP的数据容器,详细的使用方法见文档 :doc:`fastNLP.core.dataset` + + :param data: 如果为dict类型,则每个key的value应该为等长的list; 如果为list, + 每个元素应该为具有相同field的 :class:`~fastNLP.Instance` 。 + """ + def __init__(self, data=None): - """ - - :param dict,list(Instance) data: 如果为dict类型,则每个key的value应该为等长的list; 如果为list,每个元素应该为具 - :有相同field的 instance_ 。 - """ self.field_arrays = {} if data is not None: if isinstance(data, dict): @@ -303,41 +311,41 @@ class DataSet(object): for ins in data: assert isinstance(ins, Instance), "Must be Instance type, not {}.".format(type(ins)) self.append(ins) - + else: raise ValueError("data only be dict or list type.") - + def __contains__(self, item): return item in self.field_arrays - + def __iter__(self): def iter_func(): for idx in range(len(self)): yield self[idx] - + return iter_func() - + def _inner_iter(self): class Iter_ptr: def __init__(self, dataset, idx): self.dataset = dataset self.idx = idx - + def __getitem__(self, item): assert item in self.dataset.field_arrays, "no such field:{} in Instance {}".format(item, self.dataset[ self.idx]) assert self.idx < len(self.dataset.field_arrays[item]), "index:{} out of range".format(self.idx) return self.dataset.field_arrays[item][self.idx] - + def __repr__(self): return self.dataset[self.idx].__repr__() - + def inner_iter_func(): for idx in range(len(self)): yield Iter_ptr(self, idx) - + return inner_iter_func() - + def __getitem__(self, idx): """给定int的index,返回一个Instance; 给定slice,返回包含这个slice内容的新的DataSet。 @@ -349,7 +357,7 @@ class DataSet(object): return Instance(**{name: self.field_arrays[name][idx] for name in self.field_arrays}) elif isinstance(idx, slice): if idx.start is not None and (idx.start >= len(self) or idx.start <= -len(self)): - raise RuntimeError(f"Start index {idx.start} out of range 0-{len(self)-1}") + raise RuntimeError(f"Start index {idx.start} out of range 0-{len(self) - 1}") data_set = DataSet() for field in self.field_arrays.values(): data_set.add_field(field_name=field.name, fields=field.content[idx], padder=field.padder, @@ -361,20 +369,20 @@ class DataSet(object): return self.field_arrays[idx] else: raise KeyError("Unrecognized type {} for idx in __getitem__ method".format(type(idx))) - + def __getattr__(self, item): # Not tested. Don't use !! if item == "field_arrays": raise AttributeError if isinstance(item, str) and item in self.field_arrays: return self.field_arrays[item] - + def __setstate__(self, state): self.__dict__ = state - + def __getstate__(self): return self.__dict__ - + def __len__(self): """Fetch the length of the dataset. @@ -384,20 +392,21 @@ class DataSet(object): return 0 field = iter(self.field_arrays.values()).__next__() return len(field) - + def __inner_repr__(self): if len(self) < 20: return ",\n".join([ins.__repr__() for ins in self]) else: return self[:5].__inner_repr__() + "\n...\n" + self[-5:].__inner_repr__() - + def __repr__(self): return "DataSet(" + self.__inner_repr__() + ")" - + def append(self, instance): - """将一个instance对象append到DataSet后面。 + """ + 将一个instance对象append到DataSet后面。 - :param Instance instance: 若DataSet不为空,则instance应该拥有和DataSet完全一样的field。 + :param instance: :class:`~fastNLP.Instance` 类型。若DataSet不为空,则instance应该拥有和DataSet完全一样的field。 """ if len(self.field_arrays) == 0: @@ -413,12 +422,13 @@ class DataSet(object): for name, field in instance.fields.items(): assert name in self.field_arrays self.field_arrays[name].append(field) - + def add_fieldarray(self, field_name, fieldarray): - """将fieldarray添加到DataSet中. + """ + 将fieldarray添加到DataSet中. :param str field_name: 新加入的field的名称 - :param FieldArray fieldarray: 需要加入DataSet的field的内容 + :param fieldarray: :class:`~fastNLP.FieldArray` 类型。需要加入DataSet的field的内容 :return: """ if not isinstance(fieldarray, FieldArray): @@ -427,97 +437,99 @@ class DataSet(object): raise RuntimeError(f"The field to add must have the same size as dataset. " f"Dataset size {len(self)} != field size {len(fieldarray)}") self.field_arrays[field_name] = fieldarray - - + def add_field(self, field_name, fields, padder=AutoPadder(), is_input=False, is_target=False, ignore_type=False): - """新增一个field + """ + 新增一个field :param str field_name: 新增的field的名称 :param list fields: 需要新增的field的内容 - :param None,Padder padder: 如果为None,则不进行pad,默认使用 AutoPadder_ 自动判断是否需要做pad。 + :param None, padder: :class:`~fastNLP.Padder` 类型, + 如果为None,则不进行pad,默认使用 :class:`~fastNLP.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: if len(self) != len(fields): raise RuntimeError(f"The field to add must have the same size as dataset. " 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 + """ + 删除第index个instance :param int index: 需要删除的instance的index,从0开始 - :return: DataSet """ assert isinstance(index, int), "Only integer supported." - if len(self)<=index: + if len(self) <= index: raise IndexError("{} is too large for as DataSet with {} instances.".format(index, len(self))) - if len(self)==1: + 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 + """ + 删除名为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 + """ + 判断DataSet中是否有名为field_name这个field :param str field_name: field的名称 - :return: bool + :return bool: 表示是否有名为field_name这个field """ if isinstance(field_name, str): return field_name in self.field_arrays return False - + def get_field(self, field_name): - """获取field_name这个field + """ + 获取field_name这个field :param str field_name: field的名称 - :return: FieldArray + :return: :class:`~fastNLP.FieldArray` """ if field_name not in self.field_arrays: raise KeyError("Field name {} not found in DataSet".format(field_name)) return self.field_arrays[field_name] - + def get_all_fields(self): - """返回一个dict,key为field_name, value为对应的FieldArray + """ + 返回一个dict,key为field_name, value为对应的 :class:`~fastNLP.FieldArray` - :return: dict: + :return: dict: 返回如上所述的字典 """ return self.field_arrays + + def get_field_names(self) -> list: + """ + 返回一个list,包含所有 field 的名字 - - def get_field_names(self)->list: - """返回一个list,包含所有 field 的名字 - - :return: list: + :return: list: 返回如上所述的列表 """ return sorted(self.field_arrays.keys()) def get_length(self): - """获取DataSet的元素数量 + """ + 获取DataSet的元素数量 - :return: int length: DataSet中Instance的个数。 + :return: int: DataSet中Instance的个数。 """ return len(self) - + def rename_field(self, old_name, new_name): - """将某个field重新命名. + """ + 将某个field重新命名. :param str old_name: 原来的field名称。 :param str new_name: 修改为new_name。 @@ -527,9 +539,10 @@ class DataSet(object): self.field_arrays[new_name].name = new_name else: raise KeyError("DataSet has no field named {}.".format(old_name)) - + def set_target(self, *field_names, flag=True): - """将field_names的field设置为target + """ + 将field_names的field设置为target Example:: @@ -545,9 +558,10 @@ class DataSet(object): self.field_arrays[name].is_target = flag else: raise KeyError("{} is not a valid field name.".format(name)) - + def set_input(self, *field_names, flag=True): - """将field_names的field设置为input + """ + 将field_names的field设置为input Example:: @@ -562,10 +576,11 @@ class DataSet(object): self.field_arrays[name].is_input = flag else: raise KeyError("{} is not a valid field name.".format(name)) - + def set_ignore_type(self, *field_names, flag=True): - """将field设置为忽略类型状态。当某个field被设置了ignore_type, 则在被设置为target或者input时将不进行类型检查,默 - 认情况下也不进行pad。 + """ + 将field设置为忽略类型状态。当某个field被设置了ignore_type, 则在被设置为target或者input时将不进行类型检查, + 默认情况下也不进行pad。 :param str field_names: field的名称 :param bool flag: 将field_name的ignore_type状态设置为flag @@ -577,9 +592,10 @@ class DataSet(object): self.field_arrays[name].ignore_type = flag else: raise KeyError("{} is not a valid field name.".format(name)) - + def set_padder(self, field_name, padder): - """为field_name设置padder + """ + 为field_name设置padder Example:: @@ -589,55 +605,57 @@ class DataSet(object): :param str field_name: 设置field的padding方式为padder :param None, Padder padder: 设置为None即删除padder, 即对该field不进行pad操作。 - :return: """ if field_name not in self.field_arrays: raise KeyError("There is no field named {}.".format(field_name)) self.field_arrays[field_name].set_padder(padder) - + def set_pad_val(self, field_name, pad_val): - """为某个field设置对应的pad_val. + """ + 为某个field设置对应的pad_val. :param str field_name: 修改该field的pad_val :param int pad_val: 该field的padder会以pad_val作为padding index - :return: """ if field_name not in self.field_arrays: raise KeyError("There is no field named {}.".format(field_name)) self.field_arrays[field_name].set_pad_val(pad_val) - + def get_input_name(self): - """返回所有is_input被设置为True的field名称 + """ + 返回所有is_input被设置为True的field名称 - :return: list, 里面的元素为被设置为input的field名称 + :return list: 里面的元素为被设置为input的field名称 """ return [name for name, field in self.field_arrays.items() if field.is_input] - + def get_target_name(self): - """返回所有is_target被设置为True的field名称 + """ + 返回所有is_target被设置为True的field名称 - :return list, 里面的元素为被设置为target的field名称 + :return list: 里面的元素为被设置为target的field名称 """ 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 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相同,则覆 + :param None,str new_field_name: 将func返回的内容放入到 `new_field_name` 这个field中,如果名称与已有的field相同,则覆 盖之前的field。如果为None则不创建新的field。 :param optional kwargs: 支持输入is_input,is_target,ignore_type - 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input + 1. is_input: bool, 如果为True则将名为 `new_field_name` 的field设置为input - 2. is_target: bool, 如果为True则将`new_field_name`的field设置为target + 2. is_target: bool, 如果为True则将名为 `new_field_name` 的field设置为target - 3. ignore_type: bool, 如果为True则将`new_field_name`的field的ignore_type设置为true, 忽略其类型 + 3. ignore_type: bool, 如果为True则将名为 `new_field_name` 的field的ignore_type设置为true, 忽略其类型 :return: list(Any), 里面的元素为func的返回值,所以list长度为DataSet的长度 """ - assert len(self)!=0, "Null DataSet cannot use apply_field()." + assert len(self) != 0, "Null DataSet cannot use apply_field()." if field_name not in self: raise KeyError("DataSet has no field named `{}`.".format(field_name)) results = [] @@ -646,19 +664,20 @@ class DataSet(object): for idx, ins in enumerate(self._inner_iter()): results.append(func(ins[field_name])) except Exception as e: - if idx!=-1: + if idx != -1: print("Exception happens at the `{}`th instance.".format(idx)) raise e if not (new_field_name is None) and len(list(filter(lambda x: x is not None, results))) == 0: # all None raise ValueError("{} always return None.".format(_get_func_signature(func=func))) - + if new_field_name is not None: self._add_apply_field(results, new_field_name, kwargs) - + return results - + def _add_apply_field(self, results, new_field_name, kwargs): - """将results作为加入到新的field中,field名称为new_field_name + """ + 将results作为加入到新的field中,field名称为new_field_name :param list(str) results: 一般是apply*()之后的结果 :param str new_field_name: 新加入的field的名称 @@ -687,43 +706,46 @@ class DataSet(object): self.add_field(field_name=new_field_name, fields=results, is_input=extra_param.get("is_input", None), is_target=extra_param.get("is_target", None), ignore_type=extra_param.get("ignore_type", False)) - + def apply(self, func, new_field_name=None, **kwargs): - """ 将DataSet中每个instance传入到func中,并获取它的返回值. + """ + 将DataSet中每个instance传入到func中,并获取它的返回值. :param callable func: 参数是DataSet中的Instance :param None,str new_field_name: 将func返回的内容放入到new_field_name这个field中,如果名称与已有的field相同,则覆 盖之前的field。如果为None则不创建新的field。 :param optional kwargs: 支持输入is_input,is_target,ignore_type - 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input + 1. is_input: bool, 如果为True则将 `new_field_name` 的field设置为input - 2. is_target: bool, 如果为True则将`new_field_name`的field设置为target + 2. is_target: bool, 如果为True则将 `new_field_name` 的field设置为target - 3. ignore_type: bool, 如果为True则将`new_field_name`的field的ignore_type设置为true, 忽略其类型 + 3. ignore_type: bool, 如果为True则将 `new_field_name` 的field的ignore_type设置为true, 忽略其类型 + :return: list(Any), 里面的元素为func的返回值,所以list长度为DataSet的长度 """ - assert len(self)!=0, "Null DataSet cannot use apply()." + assert len(self) != 0, "Null DataSet cannot use apply()." idx = -1 try: results = [] for idx, ins in enumerate(self._inner_iter()): results.append(func(ins)) except Exception as e: - if idx!=-1: + if idx != -1: print("Exception happens at the `{}`th instance.".format(idx)) raise e # results = [func(ins) for ins in self._inner_iter()] if not (new_field_name is None) and len(list(filter(lambda x: x is not None, results))) == 0: # all None raise ValueError("{} always return None.".format(_get_func_signature(func=func))) - + if new_field_name is not None: self._add_apply_field(results, new_field_name, kwargs) - + 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将作为 @@ -738,18 +760,19 @@ class DataSet(object): return self else: results = [ins for ins in self if not func(ins)] - if len(results)!=0: + if len(results) != 0: dataset = DataSet(results) for field_name, field in self.field_arrays.items(): dataset.field_arrays[field_name].to(field) return dataset else: return DataSet() - + def split(self, ratio): - """将DataSet按照ratio的比例拆分,返回两个DataSet + """ + 将DataSet按照ratio的比例拆分,返回两个DataSet - :param float ratio: 0>>from fastNLP import Instance + >>>ins = Instance(field_1=[1, 1, 1], field_2=[2, 2, 2]) + >>>ins["field_1"] + [1, 1, 1] + >>>ins.add_field("field_3", [3, 3, 3]) + >>>ins = Instance(**{'x1': 1, 'x2':np.zeros((3, 4))}) + """ + def __init__(self, **fields): - """Instance的初始化如下面的Example所示 - - Example:: - - ins = Instance(field_1=[1, 1, 1], field_2=[2, 2, 2]) - ins["field_1"] - >>[1, 1, 1] - ins.add_field("field_3", [3, 3, 3]) - - ins = Instance(**{'x1': 1, 'x2':np.zeros((3, 4))}) - """ + self.fields = fields - + def add_field(self, field_name, field): - """向Instance中增加一个field + """ + 向Instance中增加一个field :param str field_name: 新增field的名称 :param Any field: 新增field的内容 """ self.fields[field_name] = field - + def __getitem__(self, name): if name in self.fields: return self.fields[name] else: raise KeyError("{} not found".format(name)) - + def __setitem__(self, name, field): return self.add_field(name, field) - + def __repr__(self): s = '\'' return "{" + ",\n".join( diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 633a748f..aaaf84be 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -34,6 +34,8 @@ def _check_build_status(func): class Vocabulary(object): """ + 别名::class:`fastNLP.Vocabulary` :class:`fastNLP.core.vocabulary.Vocabulary` + 用于构建, 存储和使用 `str` 到 `int` 的一一映射 Example:: @@ -98,7 +100,7 @@ class Vocabulary(object): """ 依次增加序列中词在词典中的出现频率 - :param list(str) word_lst: 词的序列 + :param list[str] word_lst: 词的序列 """ self.update(word_lst) @@ -185,12 +187,11 @@ class Vocabulary(object): # remember to use `field_name` vocab.index_dataset(train_data, dev_data, test_data, field_name='words') - :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个 + :param datasets: 需要转index的 class:`~fastNLP.DataSet` , 支持一个或多个(list) :param str field_name: 需要转index的field, 若有多个 DataSet, 每个DataSet都必须有此 field. 目前仅支持 ``str`` , ``list(str)`` , ``list(list(str))`` :param str new_field_name: 保存结果的field_name. 若为 ``None`` , 将覆盖原field. Default: ``None`` - :return self: """ def index_instance(ins): """ @@ -230,7 +231,7 @@ class Vocabulary(object): # remember to use `field_name` vocab.from_dataset(train_data1, train_data2, field_name='words') - :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个. + :param datasets: 需要转index的 class:`~fastNLP.DataSet` , 支持一个或多个(list) :param field_name: 可为 ``str`` 或 ``list(str)`` . 构建词典所使用的 field(s), 支持一个或多个field 若有多个 DataSet, 每个DataSet都必须有这些field.