Browse Source

对DataSet的文档进行更新

tags/v0.4.10
yh_cc 6 years ago
parent
commit
7997dce8a7
7 changed files with 242 additions and 61 deletions
  1. +3
    -0
      fastNLP/core/batch.py
  2. +197
    -49
      fastNLP/core/dataset.py
  3. +6
    -0
      fastNLP/core/fieldarray.py
  4. +2
    -0
      fastNLP/core/sampler.py
  5. +10
    -0
      fastNLP/core/trainer.py
  6. +11
    -11
      fastNLP/modules/decoder/utils.py
  7. +13
    -1
      test/core/test_batch.py

+ 3
- 0
fastNLP/core/batch.py View File

@@ -13,6 +13,9 @@ atexit.register(_set_python_is_exit)

class Batch(object):
"""

.. _Batch:

Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出.
组成 `x` 和 `y`



+ 197
- 49
fastNLP/core/dataset.py View File

@@ -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: [<class 'str'>, <class 'int'>]

可以通过设置以忽略对该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<ratio<1, 返回的第一个DataSet拥有ratio这么多数据,第二个DataSet拥有(1-ratio)这么多数据
:param float ratio: 0<ratio<1, 返回的第一个DataSet拥有`ratio`这么多数据,第二个DataSet拥有`(1-ratio)`这么多数据
:return: [DataSet, DataSet]
"""
assert isinstance(ratio, float)
@@ -588,16 +751,15 @@ class DataSet(object):

@classmethod
def read_csv(cls, csv_path, headers=None, sep=",", dropna=True):
"""Load data from a CSV file and return a DataSet object.
:param str csv_path: path to the CSV file
:param List[str] or Tuple[str] headers: headers of the CSV file
:param str sep: delimiter in CSV file. Default: ","
:param bool dropna: If True, drop rows that have less entries than headers.
:return dataset: the read data set
"""从csv_path路径下以csv的格式读取数据.
:param str csv_path: 从哪里读取csv文件
:param list(str),None headers: 如果为None,则使用csv文件的第一行作为header; 如果传入list(str), 则元素的个数必须
与csv文件中每行的元素个数相同。
:param str sep: 分割符
:param bool dropna: 是否忽略与header数量不一致行。
:return DataSet
"""
import warnings
warnings.warn('DataSet.read_csv is deprecated, use CSVLoader instead',
category=DeprecationWarning)
with open(csv_path, "r", encoding='utf-8') as f:
@@ -635,7 +797,7 @@ class DataSet(object):

@staticmethod
def load(path):
"""从保存的DataSet pickle路径中读取DataSet
"""从保存的DataSet pickle文件的路径中读取DataSet

:param str path: 从哪里读取DataSet
:return: DataSet
@@ -644,17 +806,3 @@ class DataSet(object):
d = pickle.load(f)
assert isinstance(d, DataSet), "The object is not DataSet, but {}.".format(type(d))
return d


def construct_dataset(sentences):
"""Construct a data set from a list of sentences.

:param sentences: list of list of str
:return dataset: a DataSet object
"""
dataset = DataSet()
for sentence in sentences:
instance = Instance()
instance['raw_sentence'] = sentence
dataset.append(instance)
return dataset

+ 6
- 0
fastNLP/core/fieldarray.py View File

@@ -334,6 +334,8 @@ def is_iterable(content):

class Padder:
"""
.. _Padder:

所有padder都需要继承这个类,并覆盖__call__()方法。
用于对batch进行padding操作。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前deepcopy一份。
"""
@@ -389,6 +391,8 @@ class Padder:

class AutoPadder(Padder):
"""
.. _AutoPadder:

根据contents的数据自动判定是否需要做padding。

1 如果元素类型(元素类型是指field中最里层元素的数据类型, 可以通过FieldArray.dtype查看,比如['This', 'is', ...]的元素类
@@ -439,6 +443,8 @@ class AutoPadder(Padder):

class EngChar2DPadder(Padder):
"""
.. _EngChar2DPadder:

用于为英语执行character级别的2D padding操作。对应的field内容应该类似[['T', 'h', 'i', 's'], ['a'], ['d', 'e', 'm', 'o']],
但这个Padder只能处理index为int的情况。



+ 2
- 0
fastNLP/core/sampler.py View File

@@ -20,6 +20,8 @@ class Sampler(object):
class SequentialSampler(Sampler):
"""顺序取出元素的 `Sampler`

.. _SequentialSampler:

"""
def __call__(self, data_set):
return list(range(len(data_set)))


+ 10
- 0
fastNLP/core/trainer.py View File

@@ -1,3 +1,13 @@
"""
Trainer的说明文档

.. _Trainer:


"""



import os
import time
from datetime import datetime


+ 11
- 11
fastNLP/modules/decoder/utils.py View File

@@ -8,10 +8,10 @@ def log_sum_exp(x, dim=-1):
return res.squeeze(dim)


def viterbi_decode(feats, transitions, mask=None, unpad=False):
def viterbi_decode(logits, transitions, mask=None, unpad=False):
"""给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数

:param feats: FloatTensor, batch_size x max_len x num_tags,特征矩阵。
:param logits: FloatTensor, batch_size x max_len x num_tags,特征矩阵。
:param transitions: FloatTensor, n_tags x n_tags。[i, j]位置的值认为是从tag i到tag j的转换。
:param mask: ByteTensor, batch_size x max_len, 为0的位置认为是pad;如果为None,则认为没有padding。
:param unpad: bool, 是否将结果删去padding,
@@ -23,23 +23,23 @@ def viterbi_decode(feats, transitions, mask=None, unpad=False):
scores: torch.FloatTensor, size为(batch_size,), 对应每个最优路径的分数。

"""
batch_size, seq_len, n_tags = feats.size()
batch_size, seq_len, n_tags = logits.size()
assert n_tags==transitions.size(0) and n_tags==transitions.size(1), "The shapes of transitions and feats are not " \
"compatible."
feats = feats.transpose(0, 1).data # L, B, H
logits = logits.transpose(0, 1).data # L, B, H
if mask is not None:
mask = mask.transpose(0, 1).data.byte() # L, B
else:
mask = feats.new_ones((seq_len, batch_size), dtype=torch.uint8)
mask = logits.new_ones((seq_len, batch_size), dtype=torch.uint8)

# dp
vpath = feats.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long)
vscore = feats[0]
vpath = logits.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long)
vscore = logits[0]

trans_score = transitions.view(1, n_tags, n_tags).data
for i in range(1, seq_len):
prev_score = vscore.view(batch_size, n_tags, 1)
cur_score = feats[i].view(batch_size, 1, n_tags)
cur_score = logits[i].view(batch_size, 1, n_tags)
score = prev_score + trans_score + cur_score
best_score, best_dst = score.max(1)
vpath[i] = best_dst
@@ -47,13 +47,13 @@ def viterbi_decode(feats, transitions, mask=None, unpad=False):
vscore.masked_fill(mask[i].view(batch_size, 1), 0)

# backtrace
batch_idx = torch.arange(batch_size, dtype=torch.long, device=feats.device)
seq_idx = torch.arange(seq_len, dtype=torch.long, device=feats.device)
batch_idx = torch.arange(batch_size, dtype=torch.long, device=logits.device)
seq_idx = torch.arange(seq_len, dtype=torch.long, device=logits.device)
lens = (mask.long().sum(0) - 1)
# idxes [L, B], batched idx from seq_len-1 to 0
idxes = (lens.view(1, -1) - seq_idx.view(-1, 1)) % seq_len

ans = feats.new_empty((seq_len, batch_size), dtype=torch.long)
ans = logits.new_empty((seq_len, batch_size), dtype=torch.long)
ans_score, last_tags = vscore.max(1)
ans[idxes[0], batch_idx] = last_tags
for i in range(seq_len - 1):


+ 13
- 1
test/core/test_batch.py View File

@@ -6,7 +6,6 @@ import torch

from fastNLP.core.batch import Batch
from fastNLP.core.dataset import DataSet
from fastNLP.core.dataset import construct_dataset
from fastNLP.core.instance import Instance
from fastNLP.core.sampler import SequentialSampler

@@ -39,6 +38,19 @@ def generate_fake_dataset(num_samples=1000):
dataset.set_target(str(i))
return dataset

def construct_dataset(sentences):
"""Construct a data set from a list of sentences.

:param sentences: list of list of str
:return dataset: a DataSet object
"""
dataset = DataSet()
for sentence in sentences:
instance = Instance()
instance['raw_sentence'] = sentence
dataset.append(instance)
return dataset

class TestCase1(unittest.TestCase):
def test_simple(self):
dataset = construct_dataset(


Loading…
Cancel
Save