From 29eab18b78a813eed76515325eaf0f3bffca1eb7 Mon Sep 17 00:00:00 2001 From: yh Date: Wed, 6 Feb 2019 22:26:10 +0800 Subject: [PATCH 001/173] =?UTF-8?q?1.=20CRF=E5=A2=9E=E5=8A=A0=E6=94=AF?= =?UTF-8?q?=E6=8C=81bmeso=E7=B1=BB=E5=9E=8B=E7=9A=84tag=202.=20vocabulary?= =?UTF-8?q?=E4=B8=AD=E5=A2=9E=E5=8A=A0=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 46 +++++++++++++++++++++++++++++++--- fastNLP/core/vocabulary.py | 10 +++++--- fastNLP/io/dataset_loader.py | 8 ++++++ fastNLP/modules/decoder/CRF.py | 16 ++++++++++-- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index dfb20480..8b51e23c 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -296,6 +296,8 @@ class AccuracyMetric(MetricBase): def bmes_tag_to_spans(tags, ignore_labels=None): """ + 给定一个tags的lis,比如['S', 'B-singer', 'M-singer', 'E-singer', 'S', 'S']。 + 返回[('', (0, 1)), ('singer', (1, 2)), ('singer', (2, 3)), ('singer', (3, 4)), ('', (4, 5)), ('', (5, 6))] :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -315,13 +317,45 @@ def bmes_tag_to_spans(tags, ignore_labels=None): else: spans.append((label, [idx, idx])) prev_bmes_tag = bmes_tag - return [(span[0], (span[1][0], span[1][1])) + return [(span[0], (span[1][0], span[1][1]+1)) + for span in spans + if span[0] not in ignore_labels + ] + +def bmeso_tag_to_spans(tags, ignore_labels=None): + """ + 给定一个tags的lis,比如['O', 'B-singer', 'M-singer', 'E-singer', 'O', 'O']。 + 返回[('singer', (1, 2)), ('singer', (2, 3)), ('singer', (3, 4))] + + :param tags: List[str], + :param ignore_labels: List[str], 在该list中的label将被忽略 + :return: List[Tuple[str, List[int, int]]]. [(label,[start, end])] + """ + ignore_labels = set(ignore_labels) if ignore_labels else set() + + spans = [] + prev_bmes_tag = None + for idx, tag in enumerate(tags): + tag = tag.lower() + bmes_tag, label = tag[:1], tag[2:] + if bmes_tag in ('b', 's'): + spans.append((label, [idx, idx])) + elif bmes_tag in ('m', 'e') and prev_bmes_tag in ('b', 'm') and label==spans[-1][0]: + spans[-1][1][1] = idx + elif bmes_tag == 'o': + pass + else: + spans.append((label, [idx, idx])) + prev_bmes_tag = bmes_tag + return [(span[0], (span[1][0], span[1][1]+1)) for span in spans if span[0] not in ignore_labels ] def bio_tag_to_spans(tags, ignore_labels=None): """ + 给定一个tags的lis,比如['O', 'B-singer', 'I-singer', 'I-singer', 'O', 'O']。 + 返回[('singer', (1, 4))] (特别注意这是左闭右开区间) :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -343,7 +377,7 @@ def bio_tag_to_spans(tags, ignore_labels=None): else: spans.append((label, [idx, idx])) prev_bio_tag = bio_tag - return [(span[0], (span[1][0], span[1][1])) + return [(span[0], (span[1][0], span[1][1]+1)) for span in spans if span[0] not in ignore_labels ] @@ -390,8 +424,7 @@ class SpanFPreRecMetric(MetricBase): 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 """ encoding_type = encoding_type.lower() - if encoding_type not in ('bio', 'bmes'): - raise ValueError("Only support 'bio' or 'bmes' type.") + if not isinstance(tag_vocab, Vocabulary): raise TypeError("tag_vocab can only be fastNLP.Vocabulary, not {}.".format(type(tag_vocab))) if f_type not in ('micro', 'macro'): @@ -402,6 +435,11 @@ class SpanFPreRecMetric(MetricBase): self.tag_to_span_func = bmes_tag_to_spans elif self.encoding_type == 'bio': self.tag_to_span_func = bio_tag_to_spans + elif self.encoding_type == 'bmeso': + self.tag_to_span_func = bmeso_tag_to_spans + else: + raise ValueError("Only support 'bio', 'bmes', 'bmeso' type.") + self.ignore_labels = ignore_labels self.f_type = f_type self.beta = beta diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 50a79d24..987a3527 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -44,10 +44,14 @@ class Vocabulary(object): :param int max_size: set the max number of words in Vocabulary. Default: None :param int min_freq: set the min occur frequency of words in Vocabulary. Default: None + :param padding: str, padding的字符,默认为。如果设置为None,则vocabulary中不考虑padding,为None的情况多在为label建立 + Vocabulary的情况。 + :param unknown: str, unknown的字符,默认为。如果设置为None,则vocabulary中不考虑unknown,为None的情况多在为label建立 + Vocabulary的情况。 """ - def __init__(self, max_size=None, min_freq=None, unknown='', padding=''): + def __init__(self, max_size=None, min_freq=None, padding='', unknown=''): self.max_size = max_size self.min_freq = min_freq self.word_count = Counter() @@ -97,9 +101,9 @@ class Vocabulary(object): """ self.word2idx = {} if self.padding is not None: - self.word2idx[self.padding] = 0 + self.word2idx[self.padding] = len(self.word2idx) if self.unknown is not None: - self.word2idx[self.unknown] = 1 + self.word2idx[self.unknown] = len(self.word2idx) max_size = min(self.max_size, len(self.word_count)) if self.max_size else None words = self.word_count.most_common(max_size) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 1fcdb7d9..09fce24f 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -877,6 +877,14 @@ class ConllPOSReader(object): class ConllxDataLoader(object): def load(self, path): + """ + + :param path: str,存储数据的路径 + :return: DataSet。内含field有'words', 'pos_tags', 'heads', 'labels'(parser的label) + 类似于拥有以下结构, 一行为一个instance(sample) + words pos_tags heads labels + ['some', ..] ['NN', ...] [2, 3...] ['nn', 'nn'...] + """ datalist = [] with open(path, 'r', encoding='utf-8') as f: sample = [] diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index d7db3bf9..e1b68e7a 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -25,7 +25,7 @@ def allowed_transitions(id2label, encoding_type='bio'): :param dict id2label: key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 "B-NN", "M-NN", tag和label之间一定要用"-"隔开。一般可以通过Vocabulary.get_id2word()id2label。 - :param encoding_type: str, 支持"bio", "bmes"。 + :param encoding_type: str, 支持"bio", "bmes", "bmeso"。 :return: List[Tuple(int, int)]], 内部的Tuple是(from_tag_id, to_tag_id)。 返回的结果考虑了start和end,比如"BIO"中,B、O可以 位于序列的开端,而I不行。所以返回的结果中会包含(start_idx, B_idx), (start_idx, O_idx), 但是不包含(start_idx, I_idx). start_idx=len(id2label), end_idx=len(id2label)+1。 @@ -62,7 +62,7 @@ def allowed_transitions(id2label, encoding_type='bio'): def is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): """ - :param encoding_type: str, 支持"BIO", "BMES"。 + :param encoding_type: str, 支持"BIO", "BMES", "BEMSO"。 :param from_tag: str, 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag :param from_label: str, 比如"PER", "LOC"等label :param to_tag: str, 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag @@ -127,6 +127,18 @@ def is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label) return to_tag in ['b', 's', 'end'] else: raise ValueError("Unexpect tag type {}. Expect only 'B', 'M', 'E', 'S'.".format(from_tag)) + elif encoding_type == 'bmeso': + if from_tag == 'start': + return to_tag in ['b', 's', 'o'] + elif from_tag == 'b': + return to_tag in ['m', 'e'] and from_label==to_label + elif from_tag == 'm': + return to_tag in ['m', 'e'] and from_label==to_label + elif from_tag in ['e', 's', 'o']: + return to_tag in ['b', 's', 'end', 'o'] + else: + raise ValueError("Unexpect tag type {}. Expect only 'B', 'M', 'E', 'S', 'O'.".format(from_tag)) + else: raise ValueError("Only support BIO, BMES encoding type, got {}.".format(encoding_type)) From 5eb126dbcd300650bd4effccc9061fa67abe2c9c Mon Sep 17 00:00:00 2001 From: yh Date: Sat, 9 Feb 2019 13:47:13 +0800 Subject: [PATCH 002/173] =?UTF-8?q?BucketSampler=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E9=94=99=E8=AF=AF=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/sampler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 67ec2a8d..4a523f10 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -73,6 +73,7 @@ class BucketSampler(BaseSampler): total_sample_num = len(seq_lens) bucket_indexes = [] + assert total_sample_num>=self.num_buckets, "The number of samples is smaller than the number of buckets." num_sample_per_bucket = total_sample_num // self.num_buckets for i in range(self.num_buckets): bucket_indexes.append([num_sample_per_bucket * i, num_sample_per_bucket * (i + 1)]) From 3ea7de16732c14ddeed4655669a4be89241c9c99 Mon Sep 17 00:00:00 2001 From: yh Date: Thu, 14 Feb 2019 13:18:50 +0800 Subject: [PATCH 003/173] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9ClipGradientCallbac?= =?UTF-8?q?k=E7=9A=84bug=EF=BC=9B=E5=88=A0=E9=99=A4LRSchedulerCallback?= =?UTF-8?q?=E4=B8=AD=E7=9A=84print=EF=BC=8C=E4=B9=8B=E5=90=8E=E5=BA=94?= =?UTF-8?q?=E8=AF=A5=E4=BC=A0=E5=85=A5pbar=E8=BF=9B=E8=A1=8C=E6=89=93?= =?UTF-8?q?=E5=8D=B0;2.=E5=A2=9E=E5=8A=A0MLP=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 6 ++++-- fastNLP/modules/decoder/MLP.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index b1a480cc..d941c235 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -248,7 +248,10 @@ class GradientClipCallback(Callback): self.clip_value = clip_value def on_backward_end(self, model): - self.clip_fun(model.parameters(), self.clip_value) + if self.parameters is None: + self.clip_fun(model.parameters(), self.clip_value) + else: + self.clip_fun(self.parameters, self.clip_value) class CallbackException(BaseException): @@ -306,7 +309,6 @@ class LRScheduler(Callback): def on_epoch_begin(self, cur_epoch, total_epoch): self.scheduler.step() - print("scheduler step ", "lr=", self.trainer.optimizer.param_groups[0]["lr"]) class ControlC(Callback): diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index c9198859..b76fdab7 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -7,7 +7,7 @@ from fastNLP.modules.utils import initial_parameter class MLP(nn.Module): """Multilayer Perceptrons as a decoder - :param list size_layer: list of int, define the size of MLP layers. + :param list size_layer: list of int, define the size of MLP layers. layer的层数为(len(size_layer)-1)//2 + 1 :param str activation: str or function, the activation function for hidden layers. :param str initial_method: the name of initialization method. :param float dropout: the probability of dropout. From ee677d5d550a0b947dbd127094c6d5aa02a23e6a Mon Sep 17 00:00:00 2001 From: xuyige Date: Sun, 17 Feb 2019 02:12:33 +0800 Subject: [PATCH 004/173] update MLP module --- fastNLP/modules/decoder/MLP.py | 54 +++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index b76fdab7..d75f6b48 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -7,20 +7,24 @@ from fastNLP.modules.utils import initial_parameter class MLP(nn.Module): """Multilayer Perceptrons as a decoder - :param list size_layer: list of int, define the size of MLP layers. layer的层数为(len(size_layer)-1)//2 + 1 - :param str activation: str or function, the activation function for hidden layers. + :param list size_layer: list of int, define the size of MLP layers. layer的层数为 len(size_layer) - 1 + :param str or list activation: str or function or a list, the activation function for hidden layers. + :param str or function output_activation : str or function, the activation function for output layer :param str initial_method: the name of initialization method. :param float dropout: the probability of dropout. .. note:: - There is no activation function applying on output layer. - + 隐藏层的激活函数通过activation定义。一个str/function或者一个str/function的list可以被传入activation。 + 如果只传入了一个str/function,那么所有隐藏层的激活函数都由这个str/function定义; + 如果传入了一个str/function的list,那么每一个隐藏层的激活函数由这个list中对应的元素定义,其中list的长度为隐藏层数。 + 输出层的激活函数由output_activation定义,默认值为None,此时输出层没有激活函数。 """ - def __init__(self, size_layer, activation='relu', initial_method=None, dropout=0.0): + def __init__(self, size_layer, activation='relu', output_activation=None, initial_method=None, dropout=0.0): super(MLP, self).__init__() self.hiddens = nn.ModuleList() self.output = None + self.output_activation = output_activation for i in range(1, len(size_layer)): if i + 1 == len(size_layer): self.output = nn.Linear(size_layer[i-1], size_layer[i]) @@ -33,25 +37,47 @@ class MLP(nn.Module): 'relu': nn.ReLU(), 'tanh': nn.Tanh(), } - if activation in actives: - self.hidden_active = actives[activation] - elif callable(activation): - self.hidden_active = activation + if not isinstance(activation, list): + activation = [activation] * (len(size_layer) - 2) + elif len(activation) == len(size_layer) - 2: + pass else: - raise ValueError("should set activation correctly: {}".format(activation)) + raise ValueError( + f"the length of activation function list except {len(size_layer) - 2} but got {len(activation)}!") + self.hidden_active = [] + for func in activation: + if callable(activation): + self.hidden_active.append(activation) + elif func.lower() in actives: + self.hidden_active.append(actives[func]) + else: + raise ValueError("should set activation correctly: {}".format(activation)) + if self.output_activation is not None: + if callable(self.output_activation): + pass + elif self.output_activation.lower() in actives: + self.output_activation = actives[self.output_activation] + else: + raise ValueError("should set activation correctly: {}".format(activation)) initial_parameter(self, initial_method) def forward(self, x): - for layer in self.hiddens: - x = self.dropout(self.hidden_active(layer(x))) - x = self.dropout(self.output(x)) + for layer, func in zip(self.hiddens, self.hidden_active): + x = self.dropout(func(layer(x))) + x = self.output(x) + if self.output_activation is not None: + x = self.output_activation(x) + x = self.dropout(x) return x if __name__ == '__main__': net1 = MLP([5, 10, 5]) net2 = MLP([5, 10, 5], 'tanh') - for net in [net1, net2]: + net3 = MLP([5, 6, 7, 8, 5], 'tanh') + net4 = MLP([5, 6, 7, 8, 5], 'relu', output_activation='tanh') + net5 = MLP([5, 6, 7, 8, 5], ['tanh', 'relu', 'tanh'], 'tanh') + for net in [net1, net2, net3, net4, net5]: x = torch.randn(5, 5) y = net(x) print(x) From 8d4f26bbd9cc6a43c1c98cf4ae79c44d59749f4e Mon Sep 17 00:00:00 2001 From: yh Date: Sun, 17 Feb 2019 14:16:19 +0800 Subject: [PATCH 005/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0metric=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=EF=BC=9B=E4=BF=AE=E6=94=B9trainer=20save=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E4=B8=AD=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 72 ++++++++++++++++++++++++++++++++++++++--- fastNLP/core/trainer.py | 4 +-- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 8b51e23c..54fde815 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -16,6 +16,69 @@ from fastNLP.core.vocabulary import Vocabulary class MetricBase(object): """Base class for all metrics. + 所有的传入到Trainer, Tester的Metric需要继承自该对象。需要覆盖写入evaluate(), get_metric()方法。 + evaluate(xxx)中传入的是一个batch的数据。 + get_metric(xxx)当所有数据处理完毕,调用该方法得到最终的metric值 + 以分类问题中,Accuracy计算为例 + 假设model的forward返回dict中包含'pred'这个key, 并且该key需要用于Accuracy + class Model(nn.Module): + def __init__(xxx): + # do something + def forward(self, xxx): + # do something + return {'pred': pred, 'other_keys':xxx} # pred's shape: batch_size x num_classes + 假设dataset中'label'这个field是需要预测的值,并且该field被设置为了target + 对应的AccMetric可以按如下的定义 + # version1, 只使用这一次 + class AccMetric(MetricBase): + def __init__(self): + super().__init__() + + # 根据你的情况自定义指标 + self.corr_num = 0 + self.total = 0 + + def evaluate(self, label, pred): # 这里的名称需要和dataset中target field与model返回的key是一样的,不然找不到对应的value + # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric + self.total += label.size(0) + self.corr_num += label.eq(pred).sum().item() + + def get_metric(self, reset=True): # 在这里定义如何计算metric + acc = self.corr_num/self.total + if reset: # 是否清零以便重新计算 + self.corr_num = 0 + self.total = 0 + return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 + + + # version2,如果需要复用Metric,比如下一次使用AccMetric时,dataset中目标field不叫label而叫y,或者model的输出不是pred + class AccMetric(MetricBase): + def __init__(self, label=None, pred=None): + # 假设在另一场景使用时,目标field叫y,model给出的key为pred_y。则只需要在初始化AccMetric时, + # acc_metric = AccMetric(label='y', pred='pred_y')即可。 + # 当初始化为acc_metric = AccMetric(),即label=None, pred=None, fastNLP会直接使用'label', 'pred'作为key去索取对 + # 应的的值 + super().__init__() + self._init_param_map(label=label, pred=pred) # 该方法会注册label和pred. 仅需要注册evaluate()方法会用到的参数名即可 + # 如果没有注册该则效果与version1就是一样的 + + # 根据你的情况自定义指标 + self.corr_num = 0 + self.total = 0 + + def evaluate(self, label, pred): # 这里的参数名称需要和self._init_param_map()注册时一致。 + # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric + self.total += label.size(0) + self.corr_num += label.eq(pred).sum().item() + + def get_metric(self, reset=True): # 在这里定义如何计算metric + acc = self.corr_num/self.total + if reset: # 是否清零以便重新计算 + self.corr_num = 0 + self.total = 0 + return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 + + ``MetricBase`` handles validity check of its input dictionaries - ``pred_dict`` and ``target_dict``. ``pred_dict`` is the output of ``forward()`` or prediction function of a model. ``target_dict`` is the ground truth from DataSet where ``is_target`` is set ``True``. @@ -24,7 +87,6 @@ class MetricBase(object): 1. whether self.evaluate has varargs, which is not supported. 2. whether params needed by self.evaluate is not included in ``pred_dict``, ``target_dict``. 3. whether params needed by self.evaluate duplicate in ``pred_dict``, ``target_dict``. - 4. whether params in ``pred_dict``, ``target_dict`` are not used by evaluate.(Might cause warning) Besides, before passing params into self.evaluate, this function will filter out params from output_dict and target_dict which are not used in self.evaluate. (but if **kwargs presented in self.evaluate, no filtering @@ -297,7 +359,7 @@ class AccuracyMetric(MetricBase): def bmes_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['S', 'B-singer', 'M-singer', 'E-singer', 'S', 'S']。 - 返回[('', (0, 1)), ('singer', (1, 2)), ('singer', (2, 3)), ('singer', (3, 4)), ('', (4, 5)), ('', (5, 6))] + 返回[('', (0, 1)), ('singer', (1, 4)), ('', (4, 5)), ('', (5, 6))] (左闭右开区间) :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -325,7 +387,7 @@ def bmes_tag_to_spans(tags, ignore_labels=None): def bmeso_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['O', 'B-singer', 'M-singer', 'E-singer', 'O', 'O']。 - 返回[('singer', (1, 2)), ('singer', (2, 3)), ('singer', (3, 4))] + 返回[('singer', (1, 4))] (左闭右开区间) :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -355,7 +417,7 @@ def bmeso_tag_to_spans(tags, ignore_labels=None): def bio_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['O', 'B-singer', 'I-singer', 'I-singer', 'O', 'O']。 - 返回[('singer', (1, 4))] (特别注意这是左闭右开区间) + 返回[('singer', (1, 4))] (左闭右开区间) :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -386,6 +448,8 @@ def bio_tag_to_spans(tags, ignore_labels=None): class SpanFPreRecMetric(MetricBase): """ 在序列标注问题中,以span的方式计算F, pre, rec. + 比如中文Part of speech中,会以character的方式进行标注,句子'中国在亚洲'对应的POS可能为(以BMES为例) + ['B-NN', 'E-NN', 'S-DET', 'B-NN', 'E-NN']。该metric就是为类似情况下的F1计算。 最后得到的metric结果为 { 'f': xxx, # 这里使用f考虑以后可以计算f_beta值 diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index ddd35b28..5381fc5d 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -202,7 +202,7 @@ class Trainer(object): except (CallbackException, KeyboardInterrupt) as e: self.callback_manager.on_exception(e, self.model) - if self.dev_data is not None: + if self.dev_data is not None and hasattr(self, 'best_dev_perf'): print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + self.tester._format_eval_results(self.best_dev_perf),) results['best_eval'] = self.best_dev_perf @@ -367,7 +367,7 @@ class Trainer(object): else: model.cpu() torch.save(model, model_path) - model.cuda() + model.to(self._model_device) def _load_model(self, model, model_name, only_param=False): # 返回bool值指示是否成功reload模型 From feb8f63a06952695dd105dc806f4a10d4d98bf41 Mon Sep 17 00:00:00 2001 From: Coet Date: Mon, 14 Jan 2019 09:31:35 +0800 Subject: [PATCH 006/173] Update README.md fix tutorial link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d638c0b..5346fbd7 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ For example: ## Resources +- [Tutorials](https://github.com/fastnlp/fastNLP/tree/master/tutorials) - [Documentation](https://fastnlp.readthedocs.io/en/latest/) -- [Tutorials](https://github.com/fastnlp/fastNLP/tutorials) - [Source Code](https://github.com/fastnlp/fastNLP) From 251088ac4bd4066fb6fa492e01402e07b8c38845 Mon Sep 17 00:00:00 2001 From: chenkaiyu1997 Date: Fri, 15 Feb 2019 17:31:56 +0800 Subject: [PATCH 007/173] Add ENAS (Efficient Neural Architecture Search) --- fastNLP/models/enas_controller.py | 223 +++++++++++++++++ fastNLP/models/enas_model.py | 388 ++++++++++++++++++++++++++++++ fastNLP/models/enas_trainer.py | 385 +++++++++++++++++++++++++++++ fastNLP/models/enas_utils.py | 56 +++++ test/models/test_enas.py | 112 +++++++++ 5 files changed, 1164 insertions(+) create mode 100644 fastNLP/models/enas_controller.py create mode 100644 fastNLP/models/enas_model.py create mode 100644 fastNLP/models/enas_trainer.py create mode 100644 fastNLP/models/enas_utils.py create mode 100644 test/models/test_enas.py diff --git a/fastNLP/models/enas_controller.py b/fastNLP/models/enas_controller.py new file mode 100644 index 00000000..ae9bcfd2 --- /dev/null +++ b/fastNLP/models/enas_controller.py @@ -0,0 +1,223 @@ +# Code Modified from https://github.com/carpedm20/ENAS-pytorch +"""A module with NAS controller-related code.""" +import collections +import os + +import torch +import torch.nn.functional as F +import fastNLP +import fastNLP.models.enas_utils as utils +from fastNLP.models.enas_utils import Node + + +def _construct_dags(prev_nodes, activations, func_names, num_blocks): + """Constructs a set of DAGs based on the actions, i.e., previous nodes and + activation functions, sampled from the controller/policy pi. + + Args: + prev_nodes: Previous node actions from the policy. + activations: Activations sampled from the policy. + func_names: Mapping from activation function names to functions. + num_blocks: Number of blocks in the target RNN cell. + + Returns: + A list of DAGs defined by the inputs. + + RNN cell DAGs are represented in the following way: + + 1. Each element (node) in a DAG is a list of `Node`s. + + 2. The `Node`s in the list dag[i] correspond to the subsequent nodes + that take the output from node i as their own input. + + 3. dag[-1] is the node that takes input from x^{(t)} and h^{(t - 1)}. + dag[-1] always feeds dag[0]. + dag[-1] acts as if `w_xc`, `w_hc`, `w_xh` and `w_hh` are its + weights. + + 4. dag[N - 1] is the node that produces the hidden state passed to + the next timestep. dag[N - 1] is also always a leaf node, and therefore + is always averaged with the other leaf nodes and fed to the output + decoder. + """ + dags = [] + for nodes, func_ids in zip(prev_nodes, activations): + dag = collections.defaultdict(list) + + # add first node + dag[-1] = [Node(0, func_names[func_ids[0]])] + dag[-2] = [Node(0, func_names[func_ids[0]])] + + # add following nodes + for jdx, (idx, func_id) in enumerate(zip(nodes, func_ids[1:])): + dag[utils.to_item(idx)].append(Node(jdx + 1, func_names[func_id])) + + leaf_nodes = set(range(num_blocks)) - dag.keys() + + # merge with avg + for idx in leaf_nodes: + dag[idx] = [Node(num_blocks, 'avg')] + + # This is actually y^{(t)}. h^{(t)} is node N - 1 in + # the graph, where N Is the number of nodes. I.e., h^{(t)} takes + # only one other node as its input. + # last h[t] node + last_node = Node(num_blocks + 1, 'h[t]') + dag[num_blocks] = [last_node] + dags.append(dag) + + return dags + + +class Controller(torch.nn.Module): + """Based on + https://github.com/pytorch/examples/blob/master/word_language_model/model.py + + RL controllers do not necessarily have much to do with + language models. + + Base the controller RNN on the GRU from: + https://github.com/ikostrikov/pytorch-a2c-ppo-acktr/blob/master/model.py + """ + def __init__(self, num_blocks=4, controller_hid=100, cuda=False): + torch.nn.Module.__init__(self) + + # `num_tokens` here is just the activation function + # for every even step, + self.shared_rnn_activations = ['tanh', 'ReLU', 'identity', 'sigmoid'] + self.num_tokens = [len(self.shared_rnn_activations)] + self.controller_hid = controller_hid + self.use_cuda = cuda + self.num_blocks = num_blocks + for idx in range(num_blocks): + self.num_tokens += [idx + 1, len(self.shared_rnn_activations)] + self.func_names = self.shared_rnn_activations + + num_total_tokens = sum(self.num_tokens) + + self.encoder = torch.nn.Embedding(num_total_tokens, + controller_hid) + self.lstm = torch.nn.LSTMCell(controller_hid, controller_hid) + + # Perhaps these weights in the decoder should be + # shared? At least for the activation functions, which all have the + # same size. + self.decoders = [] + for idx, size in enumerate(self.num_tokens): + decoder = torch.nn.Linear(controller_hid, size) + self.decoders.append(decoder) + + self._decoders = torch.nn.ModuleList(self.decoders) + + self.reset_parameters() + self.static_init_hidden = utils.keydefaultdict(self.init_hidden) + + def _get_default_hidden(key): + return utils.get_variable( + torch.zeros(key, self.controller_hid), + self.use_cuda, + requires_grad=False) + + self.static_inputs = utils.keydefaultdict(_get_default_hidden) + + def reset_parameters(self): + init_range = 0.1 + for param in self.parameters(): + param.data.uniform_(-init_range, init_range) + for decoder in self.decoders: + decoder.bias.data.fill_(0) + + def forward(self, # pylint:disable=arguments-differ + inputs, + hidden, + block_idx, + is_embed): + if not is_embed: + embed = self.encoder(inputs) + else: + embed = inputs + + hx, cx = self.lstm(embed, hidden) + logits = self.decoders[block_idx](hx) + + logits /= 5.0 + + # # exploration + # if self.args.mode == 'train': + # logits = (2.5 * F.tanh(logits)) + + return logits, (hx, cx) + + def sample(self, batch_size=1, with_details=False, save_dir=None): + """Samples a set of `args.num_blocks` many computational nodes from the + controller, where each node is made up of an activation function, and + each node except the last also includes a previous node. + """ + if batch_size < 1: + raise Exception(f'Wrong batch_size: {batch_size} < 1') + + # [B, L, H] + inputs = self.static_inputs[batch_size] + hidden = self.static_init_hidden[batch_size] + + activations = [] + entropies = [] + log_probs = [] + prev_nodes = [] + # The RNN controller alternately outputs an activation, + # followed by a previous node, for each block except the last one, + # which only gets an activation function. The last node is the output + # node, and its previous node is the average of all leaf nodes. + for block_idx in range(2*(self.num_blocks - 1) + 1): + logits, hidden = self.forward(inputs, + hidden, + block_idx, + is_embed=(block_idx == 0)) + + probs = F.softmax(logits, dim=-1) + log_prob = F.log_softmax(logits, dim=-1) + # .mean() for entropy? + entropy = -(log_prob * probs).sum(1, keepdim=False) + + action = probs.multinomial(num_samples=1).data + selected_log_prob = log_prob.gather( + 1, utils.get_variable(action, requires_grad=False)) + + # why the [:, 0] here? Should it be .squeeze(), or + # .view()? Same below with `action`. + entropies.append(entropy) + log_probs.append(selected_log_prob[:, 0]) + + # 0: function, 1: previous node + mode = block_idx % 2 + inputs = utils.get_variable( + action[:, 0] + sum(self.num_tokens[:mode]), + requires_grad=False) + + if mode == 0: + activations.append(action[:, 0]) + elif mode == 1: + prev_nodes.append(action[:, 0]) + + prev_nodes = torch.stack(prev_nodes).transpose(0, 1) + activations = torch.stack(activations).transpose(0, 1) + + dags = _construct_dags(prev_nodes, + activations, + self.func_names, + self.num_blocks) + + if save_dir is not None: + for idx, dag in enumerate(dags): + utils.draw_network(dag, + os.path.join(save_dir, f'graph{idx}.png')) + + if with_details: + return dags, torch.cat(log_probs), torch.cat(entropies) + + return dags + + def init_hidden(self, batch_size): + zeros = torch.zeros(batch_size, self.controller_hid) + return (utils.get_variable(zeros, self.use_cuda, requires_grad=False), + utils.get_variable(zeros.clone(), self.use_cuda, requires_grad=False)) diff --git a/fastNLP/models/enas_model.py b/fastNLP/models/enas_model.py new file mode 100644 index 00000000..cc91e675 --- /dev/null +++ b/fastNLP/models/enas_model.py @@ -0,0 +1,388 @@ +# Code Modified from https://github.com/carpedm20/ENAS-pytorch + +"""Module containing the shared RNN model.""" +import numpy as np +import collections + +import torch +from torch import nn +import torch.nn.functional as F +from torch.autograd import Variable + +import fastNLP.models.enas_utils as utils +from fastNLP.models.base_model import BaseModel +import fastNLP.modules.encoder as encoder + +def _get_dropped_weights(w_raw, dropout_p, is_training): + """Drops out weights to implement DropConnect. + + Args: + w_raw: Full, pre-dropout, weights to be dropped out. + dropout_p: Proportion of weights to drop out. + is_training: True iff _shared_ model is training. + + Returns: + The dropped weights. + + Why does torch.nn.functional.dropout() return: + 1. `torch.autograd.Variable()` on the training loop + 2. `torch.nn.Parameter()` on the controller or eval loop, when + training = False... + + Even though the call to `_setweights` in the Smerity repo's + `weight_drop.py` does not have this behaviour, and `F.dropout` always + returns `torch.autograd.Variable` there, even when `training=False`? + + The above TODO is the reason for the hacky check for `torch.nn.Parameter`. + """ + dropped_w = F.dropout(w_raw, p=dropout_p, training=is_training) + + if isinstance(dropped_w, torch.nn.Parameter): + dropped_w = dropped_w.clone() + + return dropped_w + +class EmbeddingDropout(torch.nn.Embedding): + """Class for dropping out embeddings by zero'ing out parameters in the + embedding matrix. + + This is equivalent to dropping out particular words, e.g., in the sentence + 'the quick brown fox jumps over the lazy dog', dropping out 'the' would + lead to the sentence '### quick brown fox jumps over ### lazy dog' (in the + embedding vector space). + + See 'A Theoretically Grounded Application of Dropout in Recurrent Neural + Networks', (Gal and Ghahramani, 2016). + """ + def __init__(self, + num_embeddings, + embedding_dim, + max_norm=None, + norm_type=2, + scale_grad_by_freq=False, + sparse=False, + dropout=0.1, + scale=None): + """Embedding constructor. + + Args: + dropout: Dropout probability. + scale: Used to scale parameters of embedding weight matrix that are + not dropped out. Note that this is _in addition_ to the + `1/(1 - dropout)` scaling. + + See `torch.nn.Embedding` for remaining arguments. + """ + torch.nn.Embedding.__init__(self, + num_embeddings=num_embeddings, + embedding_dim=embedding_dim, + max_norm=max_norm, + norm_type=norm_type, + scale_grad_by_freq=scale_grad_by_freq, + sparse=sparse) + self.dropout = dropout + assert (dropout >= 0.0) and (dropout < 1.0), ('Dropout must be >= 0.0 ' + 'and < 1.0') + self.scale = scale + + def forward(self, inputs): # pylint:disable=arguments-differ + """Embeds `inputs` with the dropped out embedding weight matrix.""" + if self.training: + dropout = self.dropout + else: + dropout = 0 + + if dropout: + mask = self.weight.data.new(self.weight.size(0), 1) + mask.bernoulli_(1 - dropout) + mask = mask.expand_as(self.weight) + mask = mask / (1 - dropout) + masked_weight = self.weight * Variable(mask) + else: + masked_weight = self.weight + if self.scale and self.scale != 1: + masked_weight = masked_weight * self.scale + + return F.embedding(inputs, + masked_weight, + max_norm=self.max_norm, + norm_type=self.norm_type, + scale_grad_by_freq=self.scale_grad_by_freq, + sparse=self.sparse) + + +class LockedDropout(nn.Module): + # code from https://github.com/salesforce/awd-lstm-lm/blob/master/locked_dropout.py + def __init__(self): + super().__init__() + + def forward(self, x, dropout=0.5): + if not self.training or not dropout: + return x + m = x.data.new(1, x.size(1), x.size(2)).bernoulli_(1 - dropout) + mask = Variable(m, requires_grad=False) / (1 - dropout) + mask = mask.expand_as(x) + return mask * x + + +class ENASModel(BaseModel): + """Shared RNN model.""" + def __init__(self, embed_num, num_classes, num_blocks=4, cuda=False, shared_hid=1000, shared_embed=1000): + super(ENASModel, self).__init__() + + self.use_cuda = cuda + + self.shared_hid = shared_hid + self.num_blocks = num_blocks + self.decoder = nn.Linear(self.shared_hid, num_classes) + self.encoder = EmbeddingDropout(embed_num, + shared_embed, + dropout=0.1) + self.lockdrop = LockedDropout() + self.dag = None + + # Tie weights + # self.decoder.weight = self.encoder.weight + + # Since W^{x, c} and W^{h, c} are always summed, there + # is no point duplicating their bias offset parameter. Likewise for + # W^{x, h} and W^{h, h}. + self.w_xc = nn.Linear(shared_embed, self.shared_hid) + self.w_xh = nn.Linear(shared_embed, self.shared_hid) + + # The raw weights are stored here because the hidden-to-hidden weights + # are weight dropped on the forward pass. + self.w_hc_raw = torch.nn.Parameter( + torch.Tensor(self.shared_hid, self.shared_hid)) + self.w_hh_raw = torch.nn.Parameter( + torch.Tensor(self.shared_hid, self.shared_hid)) + self.w_hc = None + self.w_hh = None + + self.w_h = collections.defaultdict(dict) + self.w_c = collections.defaultdict(dict) + + for idx in range(self.num_blocks): + for jdx in range(idx + 1, self.num_blocks): + self.w_h[idx][jdx] = nn.Linear(self.shared_hid, + self.shared_hid, + bias=False) + self.w_c[idx][jdx] = nn.Linear(self.shared_hid, + self.shared_hid, + bias=False) + + self._w_h = nn.ModuleList([self.w_h[idx][jdx] + for idx in self.w_h + for jdx in self.w_h[idx]]) + self._w_c = nn.ModuleList([self.w_c[idx][jdx] + for idx in self.w_c + for jdx in self.w_c[idx]]) + + self.batch_norm = None + # if args.mode == 'train': + # self.batch_norm = nn.BatchNorm1d(self.shared_hid) + # else: + # self.batch_norm = None + + self.reset_parameters() + self.static_init_hidden = utils.keydefaultdict(self.init_hidden) + + def setDAG(self, dag): + if self.dag is None: + self.dag = dag + + def forward(self, word_seq, hidden=None): + inputs = torch.transpose(word_seq, 0, 1) + + time_steps = inputs.size(0) + batch_size = inputs.size(1) + + + self.w_hh = _get_dropped_weights(self.w_hh_raw, + 0.5, + self.training) + self.w_hc = _get_dropped_weights(self.w_hc_raw, + 0.5, + self.training) + + # hidden = self.static_init_hidden[batch_size] if hidden is None else hidden + hidden = self.static_init_hidden[batch_size] + + embed = self.encoder(inputs) + + embed = self.lockdrop(embed, 0.65 if self.training else 0) + + # The norm of hidden states are clipped here because + # otherwise ENAS is especially prone to exploding activations on the + # forward pass. This could probably be fixed in a more elegant way, but + # it might be exposing a weakness in the ENAS algorithm as currently + # proposed. + # + # For more details, see + # https://github.com/carpedm20/ENAS-pytorch/issues/6 + clipped_num = 0 + max_clipped_norm = 0 + h1tohT = [] + logits = [] + for step in range(time_steps): + x_t = embed[step] + logit, hidden = self.cell(x_t, hidden, self.dag) + + hidden_norms = hidden.norm(dim=-1) + max_norm = 25.0 + if hidden_norms.data.max() > max_norm: + # Just directly use the torch slice operations + # in PyTorch v0.4. + # + # This workaround for PyTorch v0.3.1 does everything in numpy, + # because the PyTorch slicing and slice assignment is too + # flaky. + hidden_norms = hidden_norms.data.cpu().numpy() + + clipped_num += 1 + if hidden_norms.max() > max_clipped_norm: + max_clipped_norm = hidden_norms.max() + + clip_select = hidden_norms > max_norm + clip_norms = hidden_norms[clip_select] + + mask = np.ones(hidden.size()) + normalizer = max_norm/clip_norms + normalizer = normalizer[:, np.newaxis] + + mask[clip_select] = normalizer + + if self.use_cuda: + hidden *= torch.autograd.Variable( + torch.FloatTensor(mask).cuda(), requires_grad=False) + else: + hidden *= torch.autograd.Variable( + torch.FloatTensor(mask), requires_grad=False) + logits.append(logit) + h1tohT.append(hidden) + + h1tohT = torch.stack(h1tohT) + output = torch.stack(logits) + raw_output = output + + output = self.lockdrop(output, 0.4 if self.training else 0) + + #Pooling + output = torch.mean(output, 0) + + decoded = self.decoder(output) + + extra_out = {'dropped': decoded, + 'hiddens': h1tohT, + 'raw': raw_output} + return {'pred': decoded, 'hidden': hidden, 'extra_out': extra_out} + + def cell(self, x, h_prev, dag): + """Computes a single pass through the discovered RNN cell.""" + c = {} + h = {} + f = {} + + f[0] = self.get_f(dag[-1][0].name) + c[0] = torch.sigmoid(self.w_xc(x) + F.linear(h_prev, self.w_hc, None)) + h[0] = (c[0]*f[0](self.w_xh(x) + F.linear(h_prev, self.w_hh, None)) + + (1 - c[0])*h_prev) + + leaf_node_ids = [] + q = collections.deque() + q.append(0) + + # Computes connections from the parent nodes `node_id` + # to their child nodes `next_id` recursively, skipping leaf nodes. A + # leaf node is a node whose id == `self.num_blocks`. + # + # Connections between parent i and child j should be computed as + # h_j = c_j*f_{ij}{(W^h_{ij}*h_i)} + (1 - c_j)*h_i, + # where c_j = \sigmoid{(W^c_{ij}*h_i)} + # + # See Training details from Section 3.1 of the paper. + # + # The following algorithm does a breadth-first (since `q.popleft()` is + # used) search over the nodes and computes all the hidden states. + while True: + if len(q) == 0: + break + + node_id = q.popleft() + nodes = dag[node_id] + + for next_node in nodes: + next_id = next_node.id + if next_id == self.num_blocks: + leaf_node_ids.append(node_id) + assert len(nodes) == 1, ('parent of leaf node should have ' + 'only one child') + continue + + w_h = self.w_h[node_id][next_id] + w_c = self.w_c[node_id][next_id] + + f[next_id] = self.get_f(next_node.name) + c[next_id] = torch.sigmoid(w_c(h[node_id])) + h[next_id] = (c[next_id]*f[next_id](w_h(h[node_id])) + + (1 - c[next_id])*h[node_id]) + + q.append(next_id) + + # Instead of averaging loose ends, perhaps there should + # be a set of separate unshared weights for each "loose" connection + # between each node in a cell and the output. + # + # As it stands, all weights W^h_{ij} are doing double duty by + # connecting both from i to j, as well as from i to the output. + + # average all the loose ends + leaf_nodes = [h[node_id] for node_id in leaf_node_ids] + output = torch.mean(torch.stack(leaf_nodes, 2), -1) + + # stabilizing the Updates of omega + if self.batch_norm is not None: + output = self.batch_norm(output) + + return output, h[self.num_blocks - 1] + + def init_hidden(self, batch_size): + zeros = torch.zeros(batch_size, self.shared_hid) + return utils.get_variable(zeros, self.use_cuda, requires_grad=False) + + def get_f(self, name): + name = name.lower() + if name == 'relu': + f = torch.relu + elif name == 'tanh': + f = torch.tanh + elif name == 'identity': + f = lambda x: x + elif name == 'sigmoid': + f = torch.sigmoid + return f + + + @property + def num_parameters(self): + def size(p): + return np.prod(p.size()) + return sum([size(param) for param in self.parameters()]) + + + def reset_parameters(self): + init_range = 0.025 + # init_range = 0.025 if self.args.mode == 'train' else 0.04 + for param in self.parameters(): + param.data.uniform_(-init_range, init_range) + self.decoder.bias.data.fill_(0) + + def predict(self, word_seq): + """ + + :param word_seq: torch.LongTensor, [batch_size, seq_len] + :return predict: dict of torch.LongTensor, [batch_size, seq_len] + """ + output = self(word_seq) + _, predict = output['pred'].max(dim=1) + return {'pred': predict} diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py new file mode 100644 index 00000000..22e323ce --- /dev/null +++ b/fastNLP/models/enas_trainer.py @@ -0,0 +1,385 @@ +# Code Modified from https://github.com/carpedm20/ENAS-pytorch + +import os +import time +from datetime import datetime +from datetime import timedelta + +import numpy as np +import torch +import math +from torch import nn + +try: + from tqdm.autonotebook import tqdm +except: + from fastNLP.core.utils import pseudo_tqdm as tqdm + +from fastNLP.core.batch import Batch +from fastNLP.core.callback import CallbackManager, CallbackException +from fastNLP.core.dataset import DataSet +from fastNLP.core.utils import CheckError +from fastNLP.core.utils import _move_dict_value_to_device +import fastNLP +import fastNLP.models.enas_utils as utils +from fastNLP.core.utils import _build_args + +from torch.optim import Adam + + +def _get_no_grad_ctx_mgr(): + """Returns a the `torch.no_grad` context manager for PyTorch version >= + 0.4, or a no-op context manager otherwise. + """ + return torch.no_grad() + + +class ENASTrainer(fastNLP.Trainer): + """A class to wrap training code.""" + def __init__(self, train_data, model, controller, **kwargs): + """Constructor for training algorithm. + :param DataSet train_data: the training data + :param torch.nn.modules.module model: a PyTorch model + :param torch.nn.modules.module controller: a PyTorch model + """ + self.final_epochs = kwargs['final_epochs'] + kwargs.pop('final_epochs') + super(ENASTrainer, self).__init__(train_data, model, **kwargs) + self.controller_step = 0 + self.shared_step = 0 + self.max_length = 35 + + self.shared = model + self.controller = controller + + self.shared_optim = Adam( + self.shared.parameters(), + lr=20.0, + weight_decay=1e-7) + + self.controller_optim = Adam( + self.controller.parameters(), + lr=3.5e-4) + + def train(self, load_best_model=True): + """ + :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 + 最好的模型参数。 + :return results: 返回一个字典类型的数据, 内含以下内容:: + + seconds: float, 表示训练时长 + 以下三个内容只有在提供了dev_data的情况下会有。 + best_eval: Dict of Dict, 表示evaluation的结果 + best_epoch: int,在第几个epoch取得的最佳值 + best_step: int, 在第几个step(batch)更新取得的最佳值 + + """ + results = {} + if self.n_epochs <= 0: + print(f"training epoch is {self.n_epochs}, nothing was done.") + results['seconds'] = 0. + return results + try: + if torch.cuda.is_available() and self.use_cuda: + self.model = self.model.cuda() + self._model_device = self.model.parameters().__next__().device + self._mode(self.model, is_test=False) + + self.start_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) + start_time = time.time() + print("training epochs started " + self.start_time, flush=True) + + try: + self.callback_manager.on_train_begin() + self._train() + self.callback_manager.on_train_end(self.model) + except (CallbackException, KeyboardInterrupt) as e: + self.callback_manager.on_exception(e, self.model) + + if self.dev_data is not None: + print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + + self.tester._format_eval_results(self.best_dev_perf),) + results['best_eval'] = self.best_dev_perf + results['best_epoch'] = self.best_dev_epoch + results['best_step'] = self.best_dev_step + if load_best_model: + model_name = "best_" + "_".join([self.model.__class__.__name__, self.metric_key, self.start_time]) + load_succeed = self._load_model(self.model, model_name) + if load_succeed: + print("Reloaded the best model.") + else: + print("Fail to reload best model.") + finally: + pass + results['seconds'] = round(time.time() - start_time, 2) + + return results + + def _train(self): + if not self.use_tqdm: + from fastNLP.core.utils import pseudo_tqdm as inner_tqdm + else: + inner_tqdm = tqdm + self.step = 0 + start = time.time() + total_steps = (len(self.train_data) // self.batch_size + int( + len(self.train_data) % self.batch_size != 0)) * self.n_epochs + with inner_tqdm(total=total_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: + avg_loss = 0 + data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, + prefetch=self.prefetch) + for epoch in range(1, self.n_epochs+1): + pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) + last_stage = (epoch > self.n_epochs + 1 - self.final_epochs) + if epoch == self.n_epochs + 1 - self.final_epochs: + print('Entering the final stage. (Only train the selected structure)') + # early stopping + self.callback_manager.on_epoch_begin(epoch, self.n_epochs) + + # 1. Training the shared parameters omega of the child models + self.train_shared(pbar) + + # 2. Training the controller parameters theta + if not last_stage: + self.train_controller() + + if ((self.validate_every > 0 and self.step % self.validate_every == 0) or + (self.validate_every < 0 and self.step % len(data_iterator) == 0)) \ + and self.dev_data is not None: + if not last_stage: + self.derive() + eval_res = self._do_validation(epoch=epoch, step=self.step) + eval_str = "Evaluation at Epoch {}/{}. Step:{}/{}. ".format(epoch, self.n_epochs, self.step, + total_steps) + \ + self.tester._format_eval_results(eval_res) + pbar.write(eval_str) + + # lr decay; early stopping + self.callback_manager.on_epoch_end(epoch, self.n_epochs, self.optimizer) + # =============== epochs end =================== # + pbar.close() + # ============ tqdm end ============== # + + + def get_loss(self, inputs, targets, hidden, dags): + """Computes the loss for the same batch for M models. + + This amounts to an estimate of the loss, which is turned into an + estimate for the gradients of the shared model. + """ + if not isinstance(dags, list): + dags = [dags] + + loss = 0 + for dag in dags: + self.shared.setDAG(dag) + inputs = _build_args(self.shared.forward, **inputs) + inputs['hidden'] = hidden + result = self.shared(**inputs) + output, hidden, extra_out = result['pred'], result['hidden'], result['extra_out'] + + self.callback_manager.on_loss_begin(targets, result) + sample_loss = self._compute_loss(result, targets) + loss += sample_loss + + assert len(dags) == 1, 'there are multiple `hidden` for multple `dags`' + return loss, hidden, extra_out + + def train_shared(self, pbar=None, max_step=None, dag=None): + """Train the language model for 400 steps of minibatches of 64 + examples. + + Args: + max_step: Used to run extra training steps as a warm-up. + dag: If not None, is used instead of calling sample(). + + BPTT is truncated at 35 timesteps. + + For each weight update, gradients are estimated by sampling M models + from the fixed controller policy, and averaging their gradients + computed on a batch of training data. + """ + model = self.shared + model.train() + self.controller.eval() + + hidden = self.shared.init_hidden(self.batch_size) + + abs_max_grad = 0 + abs_max_hidden_norm = 0 + step = 0 + raw_total_loss = 0 + total_loss = 0 + train_idx = 0 + avg_loss = 0 + data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, + prefetch=self.prefetch) + + for batch_x, batch_y in data_iterator: + _move_dict_value_to_device(batch_x, batch_y, device=self._model_device) + indices = data_iterator.get_batch_indices() + # negative sampling; replace unknown; re-weight batch_y + self.callback_manager.on_batch_begin(batch_x, batch_y, indices) + # prediction = self._data_forward(self.model, batch_x) + + dags = self.controller.sample(1) + inputs, targets = batch_x, batch_y + # self.callback_manager.on_loss_begin(batch_y, prediction) + loss, hidden, extra_out = self.get_loss(inputs, + targets, + hidden, + dags) + hidden.detach_() + + avg_loss += loss.item() + + # Is loss NaN or inf? requires_grad = False + self.callback_manager.on_backward_begin(loss, self.model) + self._grad_backward(loss) + self.callback_manager.on_backward_end(self.model) + + self._update() + self.callback_manager.on_step_end(self.optimizer) + + if (self.step+1) % self.print_every == 0: + if self.use_tqdm: + print_output = "loss:{0:<6.5f}".format(avg_loss / self.print_every) + pbar.update(self.print_every) + else: + end = time.time() + diff = timedelta(seconds=round(end - start)) + print_output = "[epoch: {:>3} step: {:>4}] train loss: {:>4.6} time: {}".format( + epoch, self.step, avg_loss, diff) + pbar.set_postfix_str(print_output) + avg_loss = 0 + self.step += 1 + step += 1 + self.shared_step += 1 + self.callback_manager.on_batch_end() + # ================= mini-batch end ==================== # + + + def get_reward(self, dag, entropies, hidden, valid_idx=0): + """Computes the perplexity of a single sampled model on a minibatch of + validation data. + """ + if not isinstance(entropies, np.ndarray): + entropies = entropies.data.cpu().numpy() + + data_iterator = Batch(self.dev_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, + prefetch=self.prefetch) + + for inputs, targets in data_iterator: + valid_loss, hidden, _ = self.get_loss(inputs, targets, hidden, dag) + valid_loss = utils.to_item(valid_loss.data) + + valid_ppl = math.exp(valid_loss) + + R = 80 / valid_ppl + + rewards = R + 1e-4 * entropies + + return rewards, hidden + + def train_controller(self): + """Fixes the shared parameters and updates the controller parameters. + + The controller is updated with a score function gradient estimator + (i.e., REINFORCE), with the reward being c/valid_ppl, where valid_ppl + is computed on a minibatch of validation data. + + A moving average baseline is used. + + The controller is trained for 2000 steps per epoch (i.e., + first (Train Shared) phase -> second (Train Controller) phase). + """ + model = self.controller + model.train() + # Why can't we call shared.eval() here? Leads to loss + # being uniformly zero for the controller. + # self.shared.eval() + + avg_reward_base = None + baseline = None + adv_history = [] + entropy_history = [] + reward_history = [] + + hidden = self.shared.init_hidden(self.batch_size) + total_loss = 0 + valid_idx = 0 + for step in range(20): + # sample models + dags, log_probs, entropies = self.controller.sample( + with_details=True) + + # calculate reward + np_entropies = entropies.data.cpu().numpy() + # No gradients should be backpropagated to the + # shared model during controller training, obviously. + with _get_no_grad_ctx_mgr(): + rewards, hidden = self.get_reward(dags, + np_entropies, + hidden, + valid_idx) + + + reward_history.extend(rewards) + entropy_history.extend(np_entropies) + + # moving average baseline + if baseline is None: + baseline = rewards + else: + decay = 0.95 + baseline = decay * baseline + (1 - decay) * rewards + + adv = rewards - baseline + adv_history.extend(adv) + + # policy loss + loss = -log_probs*utils.get_variable(adv, + self.use_cuda, + requires_grad=False) + + loss = loss.sum() # or loss.mean() + + # update + self.controller_optim.zero_grad() + loss.backward() + + self.controller_optim.step() + + total_loss += utils.to_item(loss.data) + + if ((step % 50) == 0) and (step > 0): + reward_history, adv_history, entropy_history = [], [], [] + total_loss = 0 + + self.controller_step += 1 + # prev_valid_idx = valid_idx + # valid_idx = ((valid_idx + self.max_length) % + # (self.valid_data.size(0) - 1)) + # # Whenever we wrap around to the beginning of the + # # validation data, we reset the hidden states. + # if prev_valid_idx > valid_idx: + # hidden = self.shared.init_hidden(self.batch_size) + + def derive(self, sample_num=10, valid_idx=0): + """We are always deriving based on the very first batch + of validation data? This seems wrong... + """ + hidden = self.shared.init_hidden(self.batch_size) + + dags, _, entropies = self.controller.sample(sample_num, + with_details=True) + + max_R = 0 + best_dag = None + for dag in dags: + R, _ = self.get_reward(dag, entropies, hidden, valid_idx) + if R.max() > max_R: + max_R = R.max() + best_dag = dag + + self.model.setDAG(best_dag) diff --git a/fastNLP/models/enas_utils.py b/fastNLP/models/enas_utils.py new file mode 100644 index 00000000..e5027d81 --- /dev/null +++ b/fastNLP/models/enas_utils.py @@ -0,0 +1,56 @@ +# Code Modified from https://github.com/carpedm20/ENAS-pytorch + +from __future__ import print_function + +from collections import defaultdict +import collections +from datetime import datetime +import os +import json + +import numpy as np + +import torch +from torch.autograd import Variable + +def detach(h): + if type(h) == Variable: + return Variable(h.data) + else: + return tuple(detach(v) for v in h) + +def get_variable(inputs, cuda=False, **kwargs): + if type(inputs) in [list, np.ndarray]: + inputs = torch.Tensor(inputs) + if cuda: + out = Variable(inputs.cuda(), **kwargs) + else: + out = Variable(inputs, **kwargs) + return out + +def update_lr(optimizer, lr): + for param_group in optimizer.param_groups: + param_group['lr'] = lr + +Node = collections.namedtuple('Node', ['id', 'name']) + + +class keydefaultdict(defaultdict): + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + else: + ret = self[key] = self.default_factory(key) + return ret + + +def to_item(x): + """Converts x, possibly scalar and possibly tensor, to a Python scalar.""" + if isinstance(x, (float, int)): + return x + + if float(torch.__version__[0:3]) < 0.4: + assert (x.dim() == 1) and (len(x) == 1) + return x[0] + + return x.item() diff --git a/test/models/test_enas.py b/test/models/test_enas.py new file mode 100644 index 00000000..07a43205 --- /dev/null +++ b/test/models/test_enas.py @@ -0,0 +1,112 @@ +import unittest + +from fastNLP import DataSet +from fastNLP import Instance +from fastNLP import Vocabulary +from fastNLP.core.losses import CrossEntropyLoss +from fastNLP.core.metrics import AccuracyMetric + + +class TestENAS(unittest.TestCase): + def testENAS(self): + # 从csv读取数据到DataSet + sample_path = "tutorials/sample_data/tutorial_sample_dataset.csv" + dataset = DataSet.read_csv(sample_path, headers=('raw_sentence', 'label'), + sep='\t') + print(len(dataset)) + print(dataset[0]) + print(dataset[-3]) + + dataset.append(Instance(raw_sentence='fake data', label='0')) + # 将所有数字转为小写 + dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') + # label转int + dataset.apply(lambda x: int(x['label']), new_field_name='label') + + # 使用空格分割句子 + def split_sent(ins): + return ins['raw_sentence'].split() + + dataset.apply(split_sent, new_field_name='words') + + # 增加长度信息 + dataset.apply(lambda x: len(x['words']), new_field_name='seq_len') + print(len(dataset)) + print(dataset[0]) + + # DataSet.drop(func)筛除数据 + dataset.drop(lambda x: x['seq_len'] <= 3) + print(len(dataset)) + + # 设置DataSet中,哪些field要转为tensor + # set target,loss或evaluate中的golden,计算loss,模型评估时使用 + dataset.set_target("label") + # set input,模型forward时使用 + dataset.set_input("words", "seq_len") + + # 分出测试集、训练集 + test_data, train_data = dataset.split(0.5) + print(len(test_data)) + print(len(train_data)) + + # 构建词表, Vocabulary.add(word) + vocab = Vocabulary(min_freq=2) + train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) + vocab.build_vocab() + + # index句子, Vocabulary.to_index(word) + train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') + test_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') + print(test_data[0]) + + # 如果你们需要做强化学习或者GAN之类的项目,你们也可以使用这些数据预处理的工具 + from fastNLP.core.batch import Batch + from fastNLP.core.sampler import RandomSampler + + batch_iterator = Batch(dataset=train_data, batch_size=2, sampler=RandomSampler()) + for batch_x, batch_y in batch_iterator: + print("batch_x has: ", batch_x) + print("batch_y has: ", batch_y) + break + + from fastNLP.models.enas_model import ENASModel + from fastNLP.models.enas_controller import Controller + model = ENASModel(embed_num=len(vocab), num_classes=5) + controller = Controller() + + from fastNLP.models.enas_trainer import ENASTrainer + from copy import deepcopy + + # 更改DataSet中对应field的名称,要以模型的forward等参数名一致 + train_data.rename_field('words', 'word_seq') # input field 与 forward 参数一致 + train_data.rename_field('label', 'label_seq') + test_data.rename_field('words', 'word_seq') + test_data.rename_field('label', 'label_seq') + + loss = CrossEntropyLoss(pred="output", target="label_seq") + metric = AccuracyMetric(pred="predict", target="label_seq") + + trainer = ENASTrainer(model=model, controller=controller, train_data=train_data, dev_data=test_data, + loss=CrossEntropyLoss(pred="output", target="label_seq"), + metrics=AccuracyMetric(pred="predict", target="label_seq"), + check_code_level=-1, + save_path=None, + batch_size=32, + print_every=1, + n_epochs=3, + final_epochs=1) + trainer.train() + print('Train finished!') + + # 调用Tester在test_data上评价效果 + from fastNLP import Tester + + tester = Tester(data=test_data, model=model, metrics=AccuracyMetric(pred="predict", target="label_seq"), + batch_size=4) + + acc = tester.test() + print(acc) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 3e01b142490c696642226c77705f4994a7418019 Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Mon, 25 Feb 2019 10:26:03 +0800 Subject: [PATCH 008/173] add ignore_type in DataSet.add_field --- fastNLP/core/dataset.py | 5 ++- fastNLP/core/fieldarray.py | 73 +++++++++++++++++++----------------- test/core/test_dataset.py | 7 +++- test/core/test_fieldarray.py | 7 ++++ 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 601fa589..f25e2cfd 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -157,7 +157,7 @@ class DataSet(object): assert name in self.field_arrays self.field_arrays[name].append(field) - def add_field(self, name, fields, padder=AutoPadder(pad_val=0), is_input=False, is_target=False): + def add_field(self, name, fields, padder=AutoPadder(pad_val=0), is_input=False, is_target=False, ignore_type=False): """Add a new field to the DataSet. :param str name: the name of the field. @@ -165,13 +165,14 @@ class DataSet(object): :param int padder: PadBase对象,如何对该Field进行padding。大部分情况使用默认值即可 :param bool is_input: whether this field is model input. :param bool is_target: whether this field is label or target. + :param bool ignore_type: If True, do not perform type check. (Default: False) """ if len(self.field_arrays) != 0: if len(self) != len(fields): raise RuntimeError(f"The field to append must have the same size as dataset. " f"Dataset size {len(self)} != field size {len(fields)}") self.field_arrays[name] = FieldArray(name, fields, is_target=is_target, is_input=is_input, - padder=padder) + padder=padder, ignore_type=ignore_type) def delete_field(self, name): """Delete a field based on the field name. diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index f3fcb3c8..148dfc6c 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -96,10 +96,11 @@ class FieldArray(object): :param list content: a list of int, float, str or np.ndarray, or a list of list of one, or a np.ndarray. :param bool is_target: If True, this FieldArray is used to compute loss. :param bool is_input: If True, this FieldArray is used to the model input. - :param padder: PadderBase类型。大多数情况下都不需要设置该值,除非需要在多个维度上进行padding(比如英文中对character进行padding) + :param PadderBase padder: PadderBase类型。大多数情况下都不需要设置该值,除非需要在多个维度上进行padding(比如英文中对character进行padding) + :param bool ignore_type: whether to ignore type. If True, no type detection will rise for this FieldArray. (default: False) """ - def __init__(self, name, content, is_target=None, is_input=None, padder=AutoPadder(pad_val=0)): + def __init__(self, name, content, is_target=None, is_input=None, padder=AutoPadder(pad_val=0), ignore_type=False): """DataSet在初始化时会有两类方法对FieldArray操作: 1) 如果DataSet使用dict初始化,那么在add_field中会构造FieldArray: 1.1) 二维list DataSet({"x": [[1, 2], [3, 4]]}) @@ -114,6 +115,7 @@ class FieldArray(object): 2.4) 二维array DataSet([Instance(x=np.array([[1, 2], [3, 4]]))]) 类型检查(dtype check)发生在当该field被设置为is_input或者is_target时。 + ignore_type用来控制是否进行类型检查,如果为True,则不检查。 """ self.name = name @@ -136,6 +138,7 @@ class FieldArray(object): self.content = content # 1维 或 2维 或 3维 list, 形状可能不对齐 self.content_dim = None # 表示content是多少维的list self.set_padder(padder) + self.ignore_type = ignore_type self.BASIC_TYPES = (int, float, str) # content中可接受的Python基本类型,这里没有np.array @@ -149,8 +152,9 @@ class FieldArray(object): self.is_target = is_target def _set_dtype(self): - self.pytype = self._type_detection(self.content) - self.dtype = self._map_to_np_type(self.pytype) + if self.ignore_type is False: + self.pytype = self._type_detection(self.content) + self.dtype = self._map_to_np_type(self.pytype) @property def is_input(self): @@ -278,39 +282,40 @@ class FieldArray(object): :param val: int, float, str, or a list of one. """ - if isinstance(val, list): - pass - elif isinstance(val, tuple): # 确保最外层是list - val = list(val) - elif isinstance(val, np.ndarray): - val = val.tolist() - elif any((isinstance(val, t) for t in self.BASIC_TYPES)): - pass - else: - raise RuntimeError( - "Unexpected data type {}. Should be list, np.array, or {}".format(type(val), self.BASIC_TYPES)) - - if self.is_input is True or self.is_target is True: - if type(val) == list: - if len(val) == 0: - raise ValueError("Cannot append an empty list.") - if self.content_dim == 2 and self._1d_list_check(val): - # 1维list检查 - pass - elif self.content_dim == 3 and self._2d_list_check(val): - # 2维list检查 - pass - else: - raise RuntimeError( - "Dimension not matched: expect dim={}, got {}.".format(self.content_dim - 1, val)) - elif type(val) in self.BASIC_TYPES and self.content_dim == 1: - # scalar检查 - if type(val) == float and self.pytype == int: - self.pytype = float - self.dtype = self._map_to_np_type(self.pytype) + if self.ignore_type is False: + if isinstance(val, list): + pass + elif isinstance(val, tuple): # 确保最外层是list + val = list(val) + elif isinstance(val, np.ndarray): + val = val.tolist() + elif any((isinstance(val, t) for t in self.BASIC_TYPES)): + pass else: raise RuntimeError( "Unexpected data type {}. Should be list, np.array, or {}".format(type(val), self.BASIC_TYPES)) + + if self.is_input is True or self.is_target is True: + if type(val) == list: + if len(val) == 0: + raise ValueError("Cannot append an empty list.") + if self.content_dim == 2 and self._1d_list_check(val): + # 1维list检查 + pass + elif self.content_dim == 3 and self._2d_list_check(val): + # 2维list检查 + pass + else: + raise RuntimeError( + "Dimension not matched: expect dim={}, got {}.".format(self.content_dim - 1, val)) + elif type(val) in self.BASIC_TYPES and self.content_dim == 1: + # scalar检查 + if type(val) == float and self.pytype == int: + self.pytype = float + self.dtype = self._map_to_np_type(self.pytype) + else: + raise RuntimeError( + "Unexpected data type {}. Should be list, np.array, or {}".format(type(val), self.BASIC_TYPES)) self.content.append(val) def __getitem__(self, indices): diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 72ced912..231fedd0 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -52,7 +52,7 @@ class TestDataSetMethods(unittest.TestCase): self.assertEqual(dd.field_arrays["x"].content, [[1, 2, 3, 4]] * 3) self.assertEqual(dd.field_arrays["y"].content, [[5, 6]] * 3) - def test_add_append(self): + def test_add_field(self): dd = DataSet() dd.add_field("x", [[1, 2, 3]] * 10) dd.add_field("y", [[1, 2, 3, 4]] * 10) @@ -65,6 +65,11 @@ class TestDataSetMethods(unittest.TestCase): with self.assertRaises(RuntimeError): dd.add_field("??", [[1, 2]] * 40) + def test_add_field_ignore_type(self): + dd = DataSet() + dd.add_field("x", [(1, "1"), (2, "2"), (3, "3"), (4, "4")], ignore_type=True, is_target=True) + dd.add_field("y", [{1, "1"}, {2, "2"}, {3, "3"}, {4, "4"}], ignore_type=True, is_target=True) + def test_delete_field(self): dd = DataSet() dd.add_field("x", [[1, 2, 3]] * 10) diff --git a/test/core/test_fieldarray.py b/test/core/test_fieldarray.py index 151d9335..e3595f9a 100644 --- a/test/core/test_fieldarray.py +++ b/test/core/test_fieldarray.py @@ -155,6 +155,13 @@ class TestFieldArray(unittest.TestCase): self.assertEqual(len(fa), 3) self.assertEqual(fa[2], [1.2, 2.3, 3.4, 4.5, 5.6]) + def test_ignore_type(self): + # 测试新添加的参数ignore_type,用来跳过类型检查 + fa = FieldArray("y", [[1.1, 2.2, "jin", {}, "hahah"], [int, 2, "$", 4, 5]], is_input=True, ignore_type=True) + fa.append([1.2, 2.3, str, 4.5, print]) + + fa = FieldArray("y", [(1, "1"), (2, "2"), (3, "3"), (4, "4")], is_target=True, ignore_type=True) + class TestPadder(unittest.TestCase): From 95a72f06b910ee3c909aeda98b65fa7d7beca13b Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Mon, 25 Feb 2019 15:38:38 +0800 Subject: [PATCH 009/173] * AutoPadder will not pad when dtype is None * add ignore_type in DataSet.apply --- fastNLP/core/dataset.py | 9 +++++++-- fastNLP/core/fieldarray.py | 2 ++ test/core/test_dataset.py | 5 ++++- test/core/test_fieldarray.py | 12 +++++++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index f25e2cfd..4b995c94 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -288,6 +288,8 @@ class DataSet(object): extra_param['is_input'] = kwargs['is_input'] if 'is_target' in kwargs: extra_param['is_target'] = kwargs['is_target'] + if 'ignore_type' in kwargs: + extra_param['ignore_type'] = kwargs['ignore_type'] if new_field_name is not None: if new_field_name in self.field_arrays: # overwrite the field, keep same attributes @@ -296,11 +298,14 @@ class DataSet(object): extra_param['is_input'] = old_field.is_input if 'is_target' not in extra_param: extra_param['is_target'] = old_field.is_target + if 'ignore_type' not in extra_param: + extra_param['ignore_type'] = old_field.ignore_type self.add_field(name=new_field_name, fields=results, is_input=extra_param["is_input"], - is_target=extra_param["is_target"]) + is_target=extra_param["is_target"], ignore_type=extra_param['ignore_type']) else: self.add_field(name=new_field_name, fields=results, is_input=extra_param.get("is_input", None), - is_target=extra_param.get("is_target", None)) + is_target=extra_param.get("is_target", None), + ignore_type=extra_param.get("ignore_type", False)) else: return results diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index 148dfc6c..1d95dbeb 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -83,6 +83,8 @@ class AutoPadder(PadderBase): array = np.full((len(contents), max_len), self.pad_val, dtype=field_ele_dtype) for i, content in enumerate(contents): array[i][:len(content)] = content + elif field_ele_dtype is None: + array = contents # 当ignore_type=True时,直接返回contents else: # should only be str array = np.array([content for content in contents]) return array diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 231fedd0..eb4c97e8 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -120,6 +120,9 @@ class TestDataSetMethods(unittest.TestCase): self.assertTrue(isinstance(res, list) and len(res) > 0) self.assertTrue(res[0], 4) + ds.apply(lambda ins: (len(ins["x"]), "hahaha"), new_field_name="k", ignore_type=True) + # expect no exception raised + def test_drop(self): ds = DataSet({"x": [[1, 2, 3, 4]] * 40, "y": [[5, 6], [7, 8, 9, 0]] * 20}) ds.drop(lambda ins: len(ins["y"]) < 3) @@ -170,7 +173,7 @@ class TestDataSetMethods(unittest.TestCase): dataset.apply(split_sent, new_field_name='words', is_input=True) # print(dataset) - def test_add_field(self): + def test_add_field_v2(self): ds = DataSet({"x": [3, 4]}) ds.add_field('y', [['hello', 'world'], ['this', 'is', 'a', 'test']], is_input=True, is_target=True) # ds.apply(lambda x:[x['x']]*3, is_input=True, is_target=True, new_field_name='y') diff --git a/test/core/test_fieldarray.py b/test/core/test_fieldarray.py index e3595f9a..ff1a8314 100644 --- a/test/core/test_fieldarray.py +++ b/test/core/test_fieldarray.py @@ -222,4 +222,14 @@ class TestPadder(unittest.TestCase): [[[1, 2, 3, -100, -100], [4, 5, -100, -100, -100], [7, 8, 9, 10, -100]], [[1, -100, -100, -100, -100], [-100, -100, -100, -100, -100], [-100, -100, -100, -100, -100]]], padder(contents, None, np.int64).tolist() - ) \ No newline at end of file + ) + + def test_None_dtype(self): + from fastNLP.core.fieldarray import AutoPadder + padder = AutoPadder() + content = [ + [[1, 2, 3], [4, 5], [7, 8, 9, 10]], + [[1]] + ] + ans = padder(content, None, None) + self.assertListEqual(content, ans) From 3d7cfb35983dd4b19890c6768fdac71c2706a768 Mon Sep 17 00:00:00 2001 From: yh Date: Tue, 12 Mar 2019 19:00:50 +0800 Subject: [PATCH 010/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dfieldarray=E4=B8=ADpa?= =?UTF-8?q?dder=E6=BD=9C=E5=9C=A8bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/fieldarray.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index 1d95dbeb..8e42f500 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -1,5 +1,5 @@ import numpy as np - +from copy import deepcopy class PadderBase: """ @@ -98,11 +98,16 @@ class FieldArray(object): :param list content: a list of int, float, str or np.ndarray, or a list of list of one, or a np.ndarray. :param bool is_target: If True, this FieldArray is used to compute loss. :param bool is_input: If True, this FieldArray is used to the model input. - :param PadderBase padder: PadderBase类型。大多数情况下都不需要设置该值,除非需要在多个维度上进行padding(比如英文中对character进行padding) + :param PadderBase padder: PadderBase类型。赋值给fieldarray的padder的对象会被deepcopy一份,需要修改padder参数必须通过 + fieldarray.set_pad_val()。 + 默认为None,(1)如果某个field是scalar,则不进行任何padding;(2)如果为一维list, 且fieldarray的dtype为float或int类型 + 则会进行padding;(3)其它情况不进行padder。 + 假设需要对English word中character进行padding,则需要使用其他的padder。 + 或ignore_type为True但是需要进行padding。 :param bool ignore_type: whether to ignore type. If True, no type detection will rise for this FieldArray. (default: False) """ - def __init__(self, name, content, is_target=None, is_input=None, padder=AutoPadder(pad_val=0), ignore_type=False): + def __init__(self, name, content, is_target=None, is_input=None, padder=None, ignore_type=False): """DataSet在初始化时会有两类方法对FieldArray操作: 1) 如果DataSet使用dict初始化,那么在add_field中会构造FieldArray: 1.1) 二维list DataSet({"x": [[1, 2], [3, 4]]}) @@ -139,6 +144,11 @@ class FieldArray(object): self.content = content # 1维 或 2维 或 3维 list, 形状可能不对齐 self.content_dim = None # 表示content是多少维的list + if padder is None: + padder = AutoPadder(pad_val=0) + else: + assert isinstance(padder, PadderBase), "padder must be of type PadderBase." + padder = deepcopy(padder) self.set_padder(padder) self.ignore_type = ignore_type @@ -354,7 +364,7 @@ class FieldArray(object): """ if padder is not None: assert isinstance(padder, PadderBase), "padder must be of type PadderBase." - self.padder = padder + self.padder = deepcopy(padder) def set_pad_val(self, pad_val): """ From 22661ea866fe97db2721187a51b637aca7dc89a2 Mon Sep 17 00:00:00 2001 From: yh Date: Tue, 12 Mar 2019 19:51:28 +0800 Subject: [PATCH 011/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dcrf=E4=B8=ADtypo;=20?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=8F=AF=E8=83=BD=E5=AF=BC=E8=87=B4=E6=95=B0?= =?UTF-8?q?=E5=80=BC=E4=B8=8D=E7=A8=B3=E5=AE=9A=E7=9A=84=E5=9C=B0=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/modules/decoder/CRF.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index e1b68e7a..e17b04f3 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -205,7 +205,7 @@ class ConditionalRandomField(nn.Module): return log_sum_exp(alpha, 1) - def _glod_score(self, logits, tags, mask): + def _gold_score(self, logits, tags, mask): """ Compute the score for the gold path. :param logits: FloatTensor, max_len x batch_size x num_tags @@ -244,7 +244,7 @@ class ConditionalRandomField(nn.Module): tags = tags.transpose(0, 1).long() mask = mask.transpose(0, 1).float() all_path_score = self._normalizer_likelihood(feats, mask) - gold_path_score = self._glod_score(feats, tags, mask) + gold_path_score = self._gold_score(feats, tags, mask) return all_path_score - gold_path_score @@ -284,7 +284,8 @@ class ConditionalRandomField(nn.Module): score = prev_score + trans_score + cur_score best_score, best_dst = score.max(1) vpath[i] = best_dst - vscore = best_score * mask[i].view(batch_size, 1) + vscore * (1 - mask[i]).view(batch_size, 1) + best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ + vscore.masked_fill(mask[i].view(batch_size, 1), 0) vscore += transitions[:n_tags, n_tags+1].view(1, -1) From f2d7d01bb71f9416d9d96a43c80a0f8cbc1dba32 Mon Sep 17 00:00:00 2001 From: yh Date: Wed, 13 Mar 2019 10:16:39 +0800 Subject: [PATCH 012/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8DCRF=E4=B8=AD=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E5=AD=98=E5=9C=A8=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/modules/decoder/CRF.py | 17 +++++++++++------ test/modules/decoder/test_CRF.py | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index e17b04f3..46350945 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -194,11 +194,14 @@ class ConditionalRandomField(nn.Module): if self.include_start_end_trans: alpha += self.start_scores.view(1, -1) + flip_mask = mask.eq(0) + for i in range(1, seq_len): emit_score = logits[i].view(batch_size, 1, n_tags) trans_score = self.trans_m.view(1, n_tags, n_tags) tmp = alpha.view(batch_size, n_tags, 1) + emit_score + trans_score - alpha = log_sum_exp(tmp, 1) * mask[i].view(batch_size, 1) + alpha * (1 - mask[i]).view(batch_size, 1) + alpha = log_sum_exp(tmp, 1).masked_fill(flip_mask[i].view(batch_size, 1), 0) + \ + alpha.masked_fill(mask[i].byte().view(batch_size, 1), 0) if self.include_start_end_trans: alpha += self.end_scores.view(1, -1) @@ -218,12 +221,14 @@ class ConditionalRandomField(nn.Module): seq_idx = torch.arange(seq_len, dtype=torch.long, device=logits.device) # trans_socre [L-1, B] - trans_score = self.trans_m[tags[:seq_len-1], tags[1:]] * mask[1:, :] + mask = mask.byte() + flip_mask = mask.eq(0) + trans_score = self.trans_m[tags[:seq_len-1], tags[1:]].masked_fill(flip_mask[1:, :], 0) # emit_score [L, B] - emit_score = logits[seq_idx.view(-1,1), batch_idx.view(1,-1), tags] * mask + emit_score = logits[seq_idx.view(-1,1), batch_idx.view(1,-1), tags].masked_fill(flip_mask, 0) # score [L-1, B] score = trans_score + emit_score[:seq_len-1, :] - score = score.sum(0) + emit_score[-1] * mask[-1] + score = score.sum(0) + emit_score[-1].masked_fill(flip_mask[-1], 0) if self.include_start_end_trans: st_scores = self.start_scores.view(1, -1).repeat(batch_size, 1)[batch_idx, tags[0]] last_idx = mask.long().sum(0) - 1 @@ -265,7 +270,7 @@ class ConditionalRandomField(nn.Module): """ batch_size, seq_len, n_tags = data.size() data = data.transpose(0, 1).data # L, B, H - mask = mask.transpose(0, 1).data.float() # L, B + mask = mask.transpose(0, 1).data.byte() # L, B # dp vpath = data.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) @@ -284,7 +289,7 @@ class ConditionalRandomField(nn.Module): score = prev_score + trans_score + cur_score best_score, best_dst = score.max(1) vpath[i] = best_dst - best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ + vscore = best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ vscore.masked_fill(mask[i].view(batch_size, 1), 0) vscore += transitions[:n_tags, n_tags+1].view(1, -1) diff --git a/test/modules/decoder/test_CRF.py b/test/modules/decoder/test_CRF.py index 0fc331dc..4576d518 100644 --- a/test/modules/decoder/test_CRF.py +++ b/test/modules/decoder/test_CRF.py @@ -66,7 +66,7 @@ class TestCRF(unittest.TestCase): # from fastNLP.modules.decoder.CRF import ConditionalRandomField, allowed_transitions # fast_CRF = ConditionalRandomField(num_tags=num_tags, allowed_transitions=allowed_transitions(id2label)) # fast_CRF.trans_m = trans_m - # fast_res = fast_CRF.viterbi_decode(logits, mask, get_score=True) + # fast_res = fast_CRF.viterbi_decode(logits, mask, get_score=True, unpad=True) # # score equal # self.assertListEqual([score for _, score in allen_res], fast_res[1]) # # seq equal @@ -95,7 +95,7 @@ class TestCRF(unittest.TestCase): # fast_CRF = ConditionalRandomField(num_tags=num_tags, allowed_transitions=allowed_transitions(id2label, # encoding_type='BMES')) # fast_CRF.trans_m = trans_m - # fast_res = fast_CRF.viterbi_decode(logits, mask, get_score=True) + # fast_res = fast_CRF.viterbi_decode(logits, mask, get_score=True, unpad=True) # # score equal # self.assertListEqual([score for _, score in allen_res], fast_res[1]) # # seq equal From 99d6bb208bf9294d1ac88054e426f0df200b7f9f Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Sat, 16 Mar 2019 19:54:03 +0800 Subject: [PATCH 013/173] change two default init arguments of Trainer into None --- fastNLP/core/trainer.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 5381fc5d..8880291d 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -32,8 +32,8 @@ from fastNLP.core.utils import get_func_signature class Trainer(object): def __init__(self, train_data, model, loss=None, metrics=None, n_epochs=3, batch_size=32, print_every=50, - validate_every=-1, dev_data=None, save_path=None, optimizer=Adam(lr=0.01, weight_decay=0), - check_code_level=0, metric_key=None, sampler=RandomSampler(), prefetch=False, use_tqdm=True, + validate_every=-1, dev_data=None, save_path=None, optimizer=None, + check_code_level=0, metric_key=None, sampler=None, prefetch=False, use_tqdm=True, use_cuda=False, callbacks=None): """ :param DataSet train_data: the training data @@ -96,7 +96,7 @@ class Trainer(object): losser = _prepare_losser(loss) # sampler check - if not isinstance(sampler, BaseSampler): + if sampler is not None and not isinstance(sampler, BaseSampler): raise ValueError("The type of sampler should be fastNLP.BaseSampler, got {}.".format(type(sampler))) if check_code_level > -1: @@ -119,13 +119,15 @@ class Trainer(object): self.best_dev_epoch = None self.best_dev_step = None self.best_dev_perf = None - self.sampler = sampler + self.sampler = sampler if sampler is not None else RandomSampler() self.prefetch = prefetch self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer else: + if optimizer is None: + optimizer = Adam(lr=0.01, weight_decay=0) self.optimizer = optimizer.construct_from_pytorch(self.model.parameters()) self.use_tqdm = use_tqdm From ef0c6e936d0503cf025287b36735bf99fd58f6a3 Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Wed, 20 Mar 2019 09:49:01 +0800 Subject: [PATCH 014/173] =?UTF-8?q?Changes=20to=20Callbacks:=20*=20?= =?UTF-8?q?=E7=BB=99callback=E6=B7=BB=E5=8A=A0=E7=BB=99=E5=AE=9A=E5=87=A0?= =?UTF-8?q?=E4=B8=AA=E5=8F=AA=E8=AF=BB=E5=B1=9E=E6=80=A7=20*=20=E9=80=9A?= =?UTF-8?q?=E8=BF=87manager=E8=AE=BE=E7=BD=AE=E8=BF=99=E4=BA=9B=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=20*=20=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E5=87=8F=E8=BD=BB@transfer=E7=9A=84=E8=B4=9F=E6=8B=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 55 ++++++++++++++++++++++++++++++++++--- fastNLP/core/trainer.py | 8 +++++- test/core/test_callbacks.py | 25 +++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index d941c235..e3b4f36e 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -17,6 +17,38 @@ class Callback(object): super(Callback, self).__init__() self.trainer = None # 在Trainer内部被重新赋值 + # callback只读属性 + self._n_epochs = None + self._n_steps = None + self._batch_size = None + self._model = None + self._pbar = None + self._optimizer = None + + @property + def n_epochs(self): + return self._n_epochs + + @property + def n_steps(self): + return self._n_steps + + @property + def batch_size(self): + return self._batch_size + + @property + def model(self): + return self._model + + @property + def pbar(self): + return self._pbar + + @property + def optimizer(self): + return self._optimizer + def on_train_begin(self): # before the main training loop pass @@ -101,8 +133,6 @@ def transfer(func): def wrapper(manager, *arg): returns = [] for callback in manager.callbacks: - for env_name, env_value in manager.env.items(): - setattr(callback, env_name, env_value) returns.append(getattr(callback, func.__name__)(*arg)) return returns @@ -115,15 +145,15 @@ class CallbackManager(Callback): """ - def __init__(self, env, callbacks=None): + def __init__(self, env, attr, callbacks=None): """ :param dict env: The key is the name of the Trainer attribute(str). The value is the attribute itself. + :param dict attr: read-only attributes for all callbacks :param Callback callbacks: """ super(CallbackManager, self).__init__() # set attribute of trainer environment - self.env = env self.callbacks = [] if callbacks is not None: @@ -136,6 +166,23 @@ class CallbackManager(Callback): else: raise TypeError(f"Expect callbacks in CallbackManager(callbacks) to be list. Got {type(callbacks)}.") + for env_name, env_val in env.items(): + for callback in self.callbacks: + setattr(callback, env_name, env_val) # Callback.trainer + + self.set_property(**attr) + + def set_property(self, **kwargs): + """设置所有callback的只读属性 + + :param kwargs: + :return: + """ + for callback in self.callbacks: + for k, v in kwargs.items(): + setattr(callback, "_" + k, v) + + @transfer def on_train_begin(self): pass diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 8880291d..743570fd 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -121,7 +121,6 @@ class Trainer(object): self.best_dev_perf = None self.sampler = sampler if sampler is not None else RandomSampler() self.prefetch = prefetch - self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer @@ -144,6 +143,12 @@ class Trainer(object): self.step = 0 self.start_time = None # start timestamp + self.callback_manager = CallbackManager(env={"trainer": self}, + attr={"n_epochs": self.n_epochs, "n_steps": self.step, + "batch_size": self.batch_size, "model": self.model, + "optimizer": self.optimizer}, + callbacks=callbacks) + def train(self, load_best_model=True): """ @@ -236,6 +241,7 @@ class Trainer(object): avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) + self.callback_manager.set_property(pbar=pbar) for epoch in range(1, self.n_epochs+1): pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) # early stopping diff --git a/test/core/test_callbacks.py b/test/core/test_callbacks.py index 74ce4876..7d66620c 100644 --- a/test/core/test_callbacks.py +++ b/test/core/test_callbacks.py @@ -136,3 +136,28 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[TensorboardCallback("loss", "metric")]) trainer.train() + + def test_readonly_property(self): + from fastNLP.core.callback import Callback + class MyCallback(Callback): + def __init__(self): + super(MyCallback, self).__init__() + + def on_epoch_begin(self, cur_epoch, total_epoch): + print(self.n_epochs, self.n_steps, self.batch_size) + print(self.model) + print(self.optimizer) + + data_set, model = prepare_env() + trainer = Trainer(data_set, model, + loss=BCELoss(pred="predict", target="y"), + n_epochs=5, + batch_size=32, + print_every=50, + optimizer=SGD(lr=0.1), + check_code_level=2, + use_tqdm=False, + dev_data=data_set, + metrics=AccuracyMetric(pred="predict", target="y"), + callbacks=[MyCallback()]) + trainer.train() From f5ab7a5d451d442f91a2cd59698fe168b078d367 Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Thu, 21 Mar 2019 15:03:51 +0800 Subject: [PATCH 015/173] =?UTF-8?q?*=20=E5=B0=86enas=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=94=BE=E5=88=B0automl=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E4=B8=8B=20*=20=E4=BF=AE=E5=A4=8Dfast=5Fparam=5Fmapping?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E4=B8=AAbug=20*=20Trainer=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BAsave=E7=9B=AE=E5=BD=95=20*?= =?UTF-8?q?=20Vocabulary=E7=9A=84=E6=89=93=E5=8D=B0=EF=BC=8C=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/automl/__init__.py | 0 fastNLP/{models => automl}/enas_controller.py | 6 +++--- fastNLP/{models => automl}/enas_model.py | 8 ++++---- fastNLP/{models => automl}/enas_trainer.py | 9 +++------ fastNLP/{models => automl}/enas_utils.py | 7 ++----- fastNLP/core/metrics.py | 2 +- fastNLP/core/trainer.py | 2 ++ fastNLP/core/vocabulary.py | 3 +++ fastNLP/modules/utils.py | 3 ++- test/{models => automl}/test_enas.py | 7 +++---- 10 files changed, 23 insertions(+), 24 deletions(-) create mode 100644 fastNLP/automl/__init__.py rename fastNLP/{models => automl}/enas_controller.py (98%) rename fastNLP/{models => automl}/enas_model.py (99%) rename fastNLP/{models => automl}/enas_trainer.py (98%) rename fastNLP/{models => automl}/enas_utils.py (96%) rename test/{models => automl}/test_enas.py (94%) diff --git a/fastNLP/automl/__init__.py b/fastNLP/automl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fastNLP/models/enas_controller.py b/fastNLP/automl/enas_controller.py similarity index 98% rename from fastNLP/models/enas_controller.py rename to fastNLP/automl/enas_controller.py index ae9bcfd2..6ddbb211 100644 --- a/fastNLP/models/enas_controller.py +++ b/fastNLP/automl/enas_controller.py @@ -5,9 +5,9 @@ import os import torch import torch.nn.functional as F -import fastNLP -import fastNLP.models.enas_utils as utils -from fastNLP.models.enas_utils import Node + +import fastNLP.automl.enas_utils as utils +from fastNLP.automl.enas_utils import Node def _construct_dags(prev_nodes, activations, func_names, num_blocks): diff --git a/fastNLP/models/enas_model.py b/fastNLP/automl/enas_model.py similarity index 99% rename from fastNLP/models/enas_model.py rename to fastNLP/automl/enas_model.py index cc91e675..4f9fb449 100644 --- a/fastNLP/models/enas_model.py +++ b/fastNLP/automl/enas_model.py @@ -1,17 +1,17 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch """Module containing the shared RNN model.""" -import numpy as np import collections +import numpy as np import torch -from torch import nn import torch.nn.functional as F +from torch import nn from torch.autograd import Variable -import fastNLP.models.enas_utils as utils +import fastNLP.automl.enas_utils as utils from fastNLP.models.base_model import BaseModel -import fastNLP.modules.encoder as encoder + def _get_dropped_weights(w_raw, dropout_p, is_training): """Drops out weights to implement DropConnect. diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/automl/enas_trainer.py similarity index 98% rename from fastNLP/models/enas_trainer.py rename to fastNLP/automl/enas_trainer.py index 22e323ce..7c0da752 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/automl/enas_trainer.py @@ -1,14 +1,12 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch -import os +import math import time from datetime import datetime from datetime import timedelta import numpy as np import torch -import math -from torch import nn try: from tqdm.autonotebook import tqdm @@ -16,12 +14,11 @@ except: from fastNLP.core.utils import pseudo_tqdm as tqdm from fastNLP.core.batch import Batch -from fastNLP.core.callback import CallbackManager, CallbackException +from fastNLP.core.callback import CallbackException from fastNLP.core.dataset import DataSet -from fastNLP.core.utils import CheckError from fastNLP.core.utils import _move_dict_value_to_device import fastNLP -import fastNLP.models.enas_utils as utils +import fastNLP.automl.enas_utils as utils from fastNLP.core.utils import _build_args from torch.optim import Adam diff --git a/fastNLP/models/enas_utils.py b/fastNLP/automl/enas_utils.py similarity index 96% rename from fastNLP/models/enas_utils.py rename to fastNLP/automl/enas_utils.py index e5027d81..7a53dd12 100644 --- a/fastNLP/models/enas_utils.py +++ b/fastNLP/automl/enas_utils.py @@ -2,17 +2,14 @@ from __future__ import print_function -from collections import defaultdict import collections -from datetime import datetime -import os -import json +from collections import defaultdict import numpy as np - import torch from torch.autograd import Variable + def detach(h): if type(h) == Variable: return Variable(h.data) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 54fde815..9d581798 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -157,7 +157,7 @@ class MetricBase(object): fast_param = {} if len(self.param_map) == 2 and len(pred_dict) == 1 and len(target_dict) == 1: fast_param['pred'] = list(pred_dict.values())[0] - fast_param['target'] = list(pred_dict.values())[0] + fast_param['target'] = list(target_dict.values())[0] return fast_param return fast_param diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 743570fd..ca2ff93b 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -367,6 +367,8 @@ class Trainer(object): """ if self.save_path is not None: model_path = os.path.join(self.save_path, model_name) + if not os.path.exists(self.save_path): + os.makedirs(self.save_path, exist_ok=True) if only_param: state_dict = model.state_dict() for key in state_dict: diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 987a3527..1e0857f3 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -196,3 +196,6 @@ class Vocabulary(object): """ self.__dict__.update(state) self.build_reverse_vocab() + + def __repr__(self): + return "Vocabulary({}...)".format(list(self.word_count.keys())[:5]) diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 5287bca4..4ae15b18 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -60,7 +60,8 @@ def initial_parameter(net, initial_method=None): init_method(w.data) # weight else: init.normal_(w.data) # bias - elif hasattr(m, 'weight') and m.weight.requires_grad: + elif m is not None and hasattr(m, 'weight') and \ + hasattr(m.weight, "requires_grad"): init_method(m.weight.data) else: for w in m.parameters(): diff --git a/test/models/test_enas.py b/test/automl/test_enas.py similarity index 94% rename from test/models/test_enas.py rename to test/automl/test_enas.py index 07a43205..d2d3af05 100644 --- a/test/models/test_enas.py +++ b/test/automl/test_enas.py @@ -69,13 +69,12 @@ class TestENAS(unittest.TestCase): print("batch_y has: ", batch_y) break - from fastNLP.models.enas_model import ENASModel - from fastNLP.models.enas_controller import Controller + from fastNLP.automl.enas_model import ENASModel + from fastNLP.automl.enas_controller import Controller model = ENASModel(embed_num=len(vocab), num_classes=5) controller = Controller() - from fastNLP.models.enas_trainer import ENASTrainer - from copy import deepcopy + from fastNLP.automl.enas_trainer import ENASTrainer # 更改DataSet中对应field的名称,要以模型的forward等参数名一致 train_data.rename_field('words', 'word_seq') # input field 与 forward 参数一致 From 6a498bbdf26220622b226d37ce50a3a29bd699c6 Mon Sep 17 00:00:00 2001 From: FengZiYjun Date: Sat, 23 Mar 2019 15:44:23 +0800 Subject: [PATCH 016/173] =?UTF-8?q?*=20=E7=BB=99vocabulary=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=81=8D=E5=8E=86=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/vocabulary.py | 3 +++ test/core/test_vocabulary.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 1e0857f3..a1c8e678 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -199,3 +199,6 @@ class Vocabulary(object): def __repr__(self): return "Vocabulary({}...)".format(list(self.word_count.keys())[:5]) + + def __iter__(self): + return iter(list(self.word_count.keys())) diff --git a/test/core/test_vocabulary.py b/test/core/test_vocabulary.py index af2c493b..2f9cd3b1 100644 --- a/test/core/test_vocabulary.py +++ b/test/core/test_vocabulary.py @@ -60,6 +60,15 @@ class TestIndexing(unittest.TestCase): vocab.update(text) self.assertEqual(text, [vocab.to_word(idx) for idx in [vocab[w] for w in text]]) + def test_iteration(self): + vocab = Vocabulary() + text = ["FastNLP", "works", "well", "in", "most", "cases", "and", "scales", "well", "in", + "works", "well", "in", "most", "cases", "scales", "well"] + vocab.update(text) + text = set(text) + for word in vocab: + self.assertTrue(word in text) + class TestOther(unittest.TestCase): def test_additional_update(self): From e5f68bbd5b564e5dfa2d6003481a4fec9254e0dc Mon Sep 17 00:00:00 2001 From: yh Date: Sat, 23 Mar 2019 18:12:32 +0800 Subject: [PATCH 017/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8DCRF=E4=B8=BA=E8=B4=9F?= =?UTF-8?q?=E6=95=B0=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 4 ++++ fastNLP/core/fieldarray.py | 10 +++++----- fastNLP/modules/decoder/CRF.py | 6 +++--- test/core/test_dataset.py | 5 +++++ test/modules/decoder/test_CRF.py | 24 ++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 4b995c94..24376a72 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -243,6 +243,8 @@ class DataSet(object): :param padder: PadderBase类型或None. 设置为None即删除padder。即对该field不进行padding操作. :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): @@ -253,6 +255,8 @@ class DataSet(object): :param pad_val: int,该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): diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index 8e42f500..72bb30b5 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -206,7 +206,7 @@ class FieldArray(object): if list in type_set: if len(type_set) > 1: # list 跟 非list 混在一起 - raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, type_set)) + raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, list(type_set))) # >1维list inner_type_set = set() for l in content: @@ -229,7 +229,7 @@ class FieldArray(object): return self._basic_type_detection(inner_inner_type_set) else: # list 跟 非list 混在一起 - raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, inner_type_set)) + raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, list(inner_type_set))) else: # 一维list for content_type in type_set: @@ -253,17 +253,17 @@ class FieldArray(object): return float else: # str 跟 int 或者 float 混在一起 - raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, type_set)) + raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, list(type_set))) else: # str, int, float混在一起 - raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, type_set)) + raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, list(type_set))) def _1d_list_check(self, val): """如果不是1D list就报错 """ type_set = set((type(obj) for obj in val)) if any(obj not in self.BASIC_TYPES for obj in type_set): - raise ValueError("Mixed data types in Field {}: {}".format(self.name, type_set)) + raise ValueError("Mixed data types in Field {}: {}".format(self.name, list(type_set))) self._basic_type_detection(type_set) # otherwise: _basic_type_detection will raise error return True diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 46350945..df004224 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -192,7 +192,7 @@ class ConditionalRandomField(nn.Module): seq_len, batch_size, n_tags = logits.size() alpha = logits[0] if self.include_start_end_trans: - alpha += self.start_scores.view(1, -1) + alpha = alpha + self.start_scores.view(1, -1) flip_mask = mask.eq(0) @@ -204,7 +204,7 @@ class ConditionalRandomField(nn.Module): alpha.masked_fill(mask[i].byte().view(batch_size, 1), 0) if self.include_start_end_trans: - alpha += self.end_scores.view(1, -1) + alpha = alpha + self.end_scores.view(1, -1) return log_sum_exp(alpha, 1) @@ -233,7 +233,7 @@ class ConditionalRandomField(nn.Module): st_scores = self.start_scores.view(1, -1).repeat(batch_size, 1)[batch_idx, tags[0]] last_idx = mask.long().sum(0) - 1 ed_scores = self.end_scores.view(1, -1).repeat(batch_size, 1)[batch_idx, tags[last_idx, batch_idx]] - score += st_scores + ed_scores + score = score + st_scores + ed_scores # return [B,] return score diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index eb4c97e8..607f9a13 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -216,6 +216,11 @@ class TestDataSetMethods(unittest.TestCase): self.assertTrue(isinstance(ds, DataSet)) self.assertTrue(len(ds) > 0) + def test_add_null(self): + ds = DataSet() + ds.add_field('test', []) + ds.set_target('test') + class TestDataSetIter(unittest.TestCase): def test__repr__(self): diff --git a/test/modules/decoder/test_CRF.py b/test/modules/decoder/test_CRF.py index 4576d518..a176348f 100644 --- a/test/modules/decoder/test_CRF.py +++ b/test/modules/decoder/test_CRF.py @@ -101,4 +101,28 @@ class TestCRF(unittest.TestCase): # # seq equal # self.assertListEqual([_ for _, score in allen_res], fast_res[0]) + def test_case3(self): + # 测试crf的loss不会出现负数 + import torch + from fastNLP.modules.decoder.CRF import ConditionalRandomField + from fastNLP.core.utils import seq_lens_to_masks + from torch import optim + from torch import nn + num_tags, include_start_end_trans = 4, True + num_samples = 4 + lengths = torch.randint(3, 50, size=(num_samples, )).long() + max_len = lengths.max() + tags = torch.randint(num_tags, size=(num_samples, max_len)) + masks = seq_lens_to_masks(lengths) + feats = nn.Parameter(torch.randn(num_samples, max_len, num_tags)) + crf = ConditionalRandomField(num_tags, include_start_end_trans) + optimizer = optim.SGD([param for param in crf.parameters() if param.requires_grad] + [feats], lr=0.1) + for _ in range(10000): + loss = crf(feats, tags, masks).mean() + optimizer.zero_grad() + loss.backward() + optimizer.step() + if _%1000==0: + print(loss) + assert loss.item()>0, "CRF loss cannot be less than 0." From 55f65c3993bca139fbda7eedaef50db0a46c6410 Mon Sep 17 00:00:00 2001 From: xuyige Date: Wed, 27 Mar 2019 17:04:19 +0800 Subject: [PATCH 018/173] add SQuAD metric --- fastNLP/core/metrics.py | 151 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 9d581798..64555e12 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -822,3 +822,154 @@ def pred_topk(y_prob, k=1): (1, k)) y_prob_topk = y_prob[x_axis_index, y_pred_topk] return y_pred_topk, y_prob_topk + + +class SQuADMetric(MetricBase): + + def __init__(self, pred_start=None, pred_end=None, target_start=None, target_end=None, + beta=1, right_open=False, print_predict_stat=False): + """ + :param pred_start: [batch], 预测答案开始的index, 如果SQuAD2.0中答案为空则为0 + :param pred_end: [batch], 预测答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) + :param target_start: [batch], 正确答案开始的index, 如果SQuAD2.0中答案为空则为0 + :param target_end: [batch], 正确答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) + :param beta: float. f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 + 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 + :param right_open: boolean. right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 + :param print_predict_stat: boolean. True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 + """ + super(SQuADMetric, self).__init__() + + self._init_param_map(pred_start=pred_start, pred_end=pred_end, target_start=target_start, target_end=target_end) + + self.print_predict_stat = print_predict_stat + + self.no_ans_correct = 0 + self.no_ans_wrong = 0 + + self.has_ans_correct = 0 + self.has_ans_wrong = 0 + + self.has_ans_f = 0. + + self.no2no = 0 + self.no2yes = 0 + self.yes2no = 0 + self.yes2yes = 0 + + self.f_beta = beta + + self.right_open = right_open + + def evaluate(self, pred_start, pred_end, target_start, target_end): + """ + + :param pred_start: [batch, seq_len] + :param pred_end: [batch, seq_len] + :param target_start: [batch] + :param target_end: [batch] + :param labels: [batch] + :return: + """ + start_inference = pred_start.max(dim=-1)[1].cpu().tolist() + end_inference = pred_end.max(dim=-1)[1].cpu().tolist() + start, end = [], [] + max_len = pred_start.size(1) + t_start = target_start.cpu().tolist() + t_end = target_end.cpu().tolist() + + for s, e in zip(start_inference, end_inference): + start.append(min(s, e)) + end.append(max(s, e)) + for s, e, ts, te in zip(start, end, t_start, t_end): + if not self.right_open: + e += 1 + te += 1 + if ts == 0 and te == int(not self.right_open): + if s == 0 and e == int(not self.right_open): + self.no_ans_correct += 1 + self.no2no += 1 + else: + self.no_ans_wrong += 1 + self.no2yes += 1 + else: + if s == 0 and e == int(not self.right_open): + self.yes2no += 1 + else: + self.yes2yes += 1 + + if s == ts and e == te: + self.has_ans_correct += 1 + else: + self.has_ans_wrong += 1 + a = [0] * s + [1] * (e - s) + [0] * (max_len - e) + b = [0] * ts + [1] * (te - ts) + [0] * (max_len - te) + a, b = torch.tensor(a), torch.tensor(b) + + TP = int(torch.sum(a * b)) + pre = TP / int(torch.sum(a)) if int(torch.sum(a)) > 0 else 0 + rec = TP / int(torch.sum(b)) if int(torch.sum(b)) > 0 else 0 + + if pre + rec > 0: + f = (1 + (self.f_beta**2)) * pre * rec / ((self.f_beta**2) * pre + rec) + else: + f = 0 + self.has_ans_f += f + + def get_metric(self, reset=True): + evaluate_result = {} + + if self.no_ans_correct + self.no_ans_wrong + self.has_ans_correct + self.no_ans_wrong <= 0: + return evaluate_result + + evaluate_result['EM'] = 0 + evaluate_result[f'f_{self.f_beta}'] = 0 + + flag = 0 + + if self.no_ans_correct + self.no_ans_wrong > 0: + evaluate_result[f'noAns-f_{self.f_beta}'] = \ + round(100 * self.no_ans_correct / (self.no_ans_correct + self.no_ans_wrong), 3) + evaluate_result['noAns-EM'] = \ + round(100 * self.no_ans_correct / (self.no_ans_correct + self.no_ans_wrong), 3) + evaluate_result[f'f_{self.f_beta}'] += evaluate_result[f'noAns-f_{self.f_beta}'] + evaluate_result['EM'] += evaluate_result['noAns-EM'] + flag += 1 + + if self.has_ans_correct + self.has_ans_wrong > 0: + evaluate_result[f'hasAns-f_{self.f_beta}'] = \ + round(100 * self.has_ans_f / (self.has_ans_correct + self.has_ans_wrong), 3) + evaluate_result['hasAns-EM'] = \ + round(100 * self.has_ans_correct / (self.has_ans_correct + self.has_ans_wrong), 3) + evaluate_result[f'f_{self.f_beta}'] += evaluate_result[f'hasAns-f_{self.f_beta}'] + evaluate_result['EM'] += evaluate_result['hasAns-EM'] + flag += 1 + + if self.print_predict_stat: + evaluate_result['no2no'] = self.no2no + evaluate_result['no2yes'] = self.no2yes + evaluate_result['yes2no'] = self.yes2no + evaluate_result['yes2yes'] = self.yes2yes + + if flag <= 0: + return evaluate_result + + evaluate_result[f'f_{self.f_beta}'] = round(evaluate_result[f'f_{self.f_beta}'] / flag, 3) + evaluate_result['EM'] = round(evaluate_result['EM'] / flag, 3) + + if reset: + self.no_ans_correct = 0 + self.no_ans_wrong = 0 + + self.has_ans_correct = 0 + self.has_ans_wrong = 0 + + self.has_ans_f = 0. + + self.no2no = 0 + self.no2yes = 0 + self.yes2no = 0 + self.yes2yes = 0 + + return evaluate_result + From 4fd49cc333fd8e571e220169e376346c720f3293 Mon Sep 17 00:00:00 2001 From: xuyige Date: Thu, 11 Apr 2019 15:00:10 +0800 Subject: [PATCH 019/173] add sigmoid activate function in MLP --- fastNLP/modules/decoder/MLP.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index d75f6b48..3a793f24 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -36,6 +36,7 @@ class MLP(nn.Module): actives = { 'relu': nn.ReLU(), 'tanh': nn.Tanh(), + 'sigmoid': nn.Sigmoid(), } if not isinstance(activation, list): activation = [activation] * (len(size_layer) - 2) From 70fb4a2284625d539c8fccec378ed0e56e8386d8 Mon Sep 17 00:00:00 2001 From: yunfan Date: Fri, 12 Apr 2019 15:35:22 +0800 Subject: [PATCH 020/173] - add star transformer model - add ConllLoader, for all kinds of conll-format files - add JsonLoader, for json-format files - add SSTLoader, for SST-2 & SST-5 - change Callback interface - fix batch multi-process when killed - add README to list models and their performance --- README.md | 9 +- fastNLP/core/__init__.py | 2 +- fastNLP/core/batch.py | 16 +- fastNLP/core/callback.py | 171 +++++++++--------- fastNLP/core/trainer.py | 34 ++-- fastNLP/io/dataset_loader.py | 273 ++++++++++++++++++----------- fastNLP/models/enas_trainer.py | 14 +- fastNLP/models/star_transformer.py | 181 +++++++++++++++++++ reproduction/README.md | 44 +++++ test/test_tutorials.py | 2 +- 10 files changed, 530 insertions(+), 216 deletions(-) create mode 100644 fastNLP/models/star_transformer.py create mode 100644 reproduction/README.md diff --git a/README.md b/README.md index 5346fbd7..5e51cf62 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![Hex.pm](https://img.shields.io/hexpm/l/plug.svg) [![Documentation Status](https://readthedocs.org/projects/fastnlp/badge/?version=latest)](http://fastnlp.readthedocs.io/?badge=latest) -FastNLP is a modular Natural Language Processing system based on PyTorch, built for fast development of NLP models. +FastNLP is a modular Natural Language Processing system based on PyTorch, built for fast development of NLP models. A deep learning NLP model is the composition of three types of modules: @@ -58,6 +58,13 @@ Run the following commands to install fastNLP package. pip install fastNLP ``` +## Models +fastNLP implements different models for variant NLP tasks. +Each model has been trained and tested carefully. + +Check out models' performance, usage and source code here. +- [Documentation](https://github.com/fastnlp/fastNLP/tree/master/reproduction) +- [Source Code](https://github.com/fastnlp/fastNLP/tree/master/fastNLP/models) ## Project Structure diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 038ca12f..0bb6a2dd 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -10,4 +10,4 @@ from .tester import Tester from .trainer import Trainer from .vocabulary import Vocabulary from ..io.dataset_loader import DataSet - +from .callback import Callback diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 88d9185d..bddecab3 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -1,9 +1,16 @@ import numpy as np import torch +import atexit from fastNLP.core.sampler import RandomSampler 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 is an iterable object which iterates over mini-batches. @@ -95,12 +102,19 @@ def to_tensor(batch, dtype): def run_fetch(batch, q): + global _python_is_exit batch.init_iter() # print('start fetch') while 1: res = batch.fetch_one() # print('fetch one') - q.put(res) + while 1: + try: + q.put(res, timeout=3) + break + except Exception as e: + if _python_is_exit: + return if res is None: # print('fetch done, waiting processing') q.join() diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index b1a480cc..9cabba15 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -15,13 +15,57 @@ class Callback(object): def __init__(self): super(Callback, self).__init__() - self.trainer = None # 在Trainer内部被重新赋值 + self._trainer = None # 在Trainer内部被重新赋值 + + @property + def trainer(self): + return self._trainer + + @property + def step(self): + """current step number, in range(1, self.n_steps+1)""" + return self._trainer.step + + @property + def n_steps(self): + """total number of steps for training""" + return self.n_steps + + @property + def batch_size(self): + """batch size for training""" + return self._trainer.batch_size + + @property + def epoch(self): + """current epoch number, in range(1, self.n_epochs+1)""" + return self._trainer.epoch + + @property + def n_epochs(self): + """total number of epochs""" + return self._trainer.n_epochs + + @property + def optimizer(self): + """torch.optim.Optimizer for current model""" + return self._trainer.optimizer + + @property + def model(self): + """training model""" + return self._trainer.model + + @property + def pbar(self): + """If use_tqdm, return trainer's tqdm print bar, else return None.""" + return self._trainer.pbar def on_train_begin(self): # before the main training loop pass - def on_epoch_begin(self, cur_epoch, total_epoch): + def on_epoch_begin(self): # at the beginning of each epoch pass @@ -33,14 +77,14 @@ class Callback(object): # after data_forward, and before loss computation pass - def on_backward_begin(self, loss, model): + def on_backward_begin(self, loss): # after loss computation, and before gradient backward pass - def on_backward_end(self, model): + def on_backward_end(self): pass - def on_step_end(self, optimizer): + def on_step_end(self): pass def on_batch_end(self, *args): @@ -50,65 +94,36 @@ class Callback(object): def on_valid_begin(self): pass - def on_valid_end(self, eval_result, metric_key, optimizer): + def on_valid_end(self, eval_result, metric_key): """ 每次执行验证机的evaluation后会调用。传入eval_result :param eval_result: Dict[str: Dict[str: float]], evaluation的结果 :param metric_key: str - :param optimizer: :return: """ pass - def on_epoch_end(self, cur_epoch, n_epoch, optimizer): + def on_epoch_end(self): """ 每个epoch结束将会调用该方法 - - :param cur_epoch: int, 当前的batch。从1开始。 - :param n_epoch: int, 总的batch数 - :param optimizer: 传入Trainer的optimizer。 - :return: """ pass - def on_train_end(self, model): + def on_train_end(self): """ 训练结束,调用该方法 - - :param model: nn.Module, 传入Trainer的模型 - :return: """ pass - def on_exception(self, exception, model): + def on_exception(self, exception): """ 当训练过程出现异常,会触发该方法 :param exception: 某种类型的Exception,比如KeyboardInterrupt等 - :param model: 传入Trainer的模型 - :return: """ pass -def transfer(func): - """装饰器,将对CallbackManager的调用转发到各个Callback子类. - - :param func: - :return: - """ - - def wrapper(manager, *arg): - returns = [] - for callback in manager.callbacks: - for env_name, env_value in manager.env.items(): - setattr(callback, env_name, env_value) - returns.append(getattr(callback, func.__name__)(*arg)) - return returns - - return wrapper - - class CallbackManager(Callback): """A manager for all callbacks passed into Trainer. It collects resources inside Trainer and raise callbacks. @@ -119,7 +134,7 @@ class CallbackManager(Callback): """ :param dict env: The key is the name of the Trainer attribute(str). The value is the attribute itself. - :param Callback callbacks: + :param List[Callback] callbacks: """ super(CallbackManager, self).__init__() # set attribute of trainer environment @@ -136,56 +151,43 @@ class CallbackManager(Callback): else: raise TypeError(f"Expect callbacks in CallbackManager(callbacks) to be list. Got {type(callbacks)}.") - @transfer def on_train_begin(self): pass - @transfer - def on_epoch_begin(self, cur_epoch, total_epoch): + def on_epoch_begin(self): pass - @transfer def on_batch_begin(self, batch_x, batch_y, indices): pass - @transfer def on_loss_begin(self, batch_y, predict_y): pass - @transfer - def on_backward_begin(self, loss, model): + def on_backward_begin(self, loss): pass - @transfer - def on_backward_end(self, model): + def on_backward_end(self): pass - @transfer - def on_step_end(self, optimizer): + def on_step_end(self): pass - @transfer def on_batch_end(self): pass - @transfer def on_valid_begin(self): pass - @transfer - def on_valid_end(self, eval_result, metric_key, optimizer): + def on_valid_end(self, eval_result, metric_key): pass - @transfer - def on_epoch_end(self, cur_epoch, n_epoch, optimizer): + def on_epoch_end(self): pass - @transfer - def on_train_end(self, model): + def on_train_end(self): pass - @transfer - def on_exception(self, exception, model): + def on_exception(self, exception): pass @@ -193,15 +195,15 @@ class DummyCallback(Callback): def on_train_begin(self, *arg): print(arg) - def on_epoch_end(self, cur_epoch, n_epoch, optimizer): - print(cur_epoch, n_epoch, optimizer) + def on_epoch_end(self): + print(self.epoch, self.n_epochs) class EchoCallback(Callback): def on_train_begin(self): print("before_train") - def on_epoch_begin(self, cur_epoch, total_epoch): + def on_epoch_begin(self): print("before_epoch") def on_batch_begin(self, batch_x, batch_y, indices): @@ -210,16 +212,16 @@ class EchoCallback(Callback): def on_loss_begin(self, batch_y, predict_y): print("before_loss") - def on_backward_begin(self, loss, model): + def on_backward_begin(self, loss): print("before_backward") def on_batch_end(self): print("after_batch") - def on_epoch_end(self, cur_epoch, n_epoch, optimizer): + def on_epoch_end(self): print("after_epoch") - def on_train_end(self, model): + def on_train_end(self): print("after_train") @@ -247,8 +249,8 @@ class GradientClipCallback(Callback): self.parameters = parameters self.clip_value = clip_value - def on_backward_end(self, model): - self.clip_fun(model.parameters(), self.clip_value) + def on_backward_end(self): + self.clip_fun(self.model.parameters(), self.clip_value) class CallbackException(BaseException): @@ -268,13 +270,10 @@ class EarlyStopCallback(Callback): :param int patience: 停止之前等待的epoch数 """ super(EarlyStopCallback, self).__init__() - self.trainer = None # override by CallbackManager self.patience = patience self.wait = 0 - self.epoch = 0 - def on_valid_end(self, eval_result, metric_key, optimizer): - self.epoch += 1 + def on_valid_end(self, eval_result, metric_key): if not self.trainer._better_eval_result(eval_result): # current result is getting worse if self.wait == self.patience: @@ -284,7 +283,7 @@ class EarlyStopCallback(Callback): else: self.wait = 0 - def on_exception(self, exception, model): + def on_exception(self, exception): if isinstance(exception, EarlyStopError): print("Early Stopping triggered in epoch {}!".format(self.epoch)) else: @@ -304,9 +303,9 @@ class LRScheduler(Callback): else: raise ValueError(f"Expect torch.optim.lr_scheduler for LRScheduler. Got {type(lr_scheduler)}.") - def on_epoch_begin(self, cur_epoch, total_epoch): + def on_epoch_begin(self): self.scheduler.step() - print("scheduler step ", "lr=", self.trainer.optimizer.param_groups[0]["lr"]) + print("scheduler step ", "lr=", self.optimizer.param_groups[0]["lr"]) class ControlC(Callback): @@ -320,7 +319,7 @@ class ControlC(Callback): raise ValueError("In KeyBoardInterrupt, quit_all arguemnt must be a bool.") self.quit_all = quit_all - def on_exception(self, exception, model): + def on_exception(self, exception): if isinstance(exception, KeyboardInterrupt): if self.quit_all is True: import sys @@ -366,15 +365,15 @@ class LRFinder(Callback): self.find = None self.loader = ModelLoader() - def on_epoch_begin(self, cur_epoch, total_epoch): - if cur_epoch == 1: + def on_epoch_begin(self): + if self.epoch == 1: # first epoch self.opt = self.trainer.optimizer # pytorch optimizer self.opt.param_groups[0]["lr"] = self.start_lr # save model ModelSaver("tmp").save_pytorch(self.trainer.model, param_only=True) self.find = True - def on_backward_begin(self, loss, model): + def on_backward_begin(self, loss): if self.find: if torch.isnan(loss) or self.stop is True: self.stop = True @@ -395,8 +394,8 @@ class LRFinder(Callback): self.opt.param_groups[0]["lr"] = lr # self.loader.load_pytorch(self.trainer.model, "tmp") - def on_epoch_end(self, cur_epoch, n_epoch, optimizer): - if cur_epoch == 1: + def on_epoch_end(self): + if self.epoch == 1: # first epoch self.opt.param_groups[0]["lr"] = self.best_lr self.find = False # reset model @@ -440,7 +439,7 @@ class TensorboardCallback(Callback): # self._summary_writer.add_graph(self.trainer.model, torch.zeros(32, 2)) self.graph_added = True - def on_backward_begin(self, loss, model): + def on_backward_begin(self, loss): if "loss" in self.options: self._summary_writer.add_scalar("loss", loss.item(), global_step=self.trainer.step) @@ -452,18 +451,18 @@ class TensorboardCallback(Callback): self._summary_writer.add_scalar(name + "_grad_mean", param.grad.mean(), global_step=self.trainer.step) - def on_valid_end(self, eval_result, metric_key, optimizer): + def on_valid_end(self, eval_result, metric_key): if "metric" in self.options: for name, metric in eval_result.items(): for metric_key, metric_val in metric.items(): self._summary_writer.add_scalar("valid_{}_{}".format(name, metric_key), metric_val, global_step=self.trainer.step) - def on_train_end(self, model): + def on_train_end(self): self._summary_writer.close() del self._summary_writer - def on_exception(self, exception, model): + def on_exception(self, exception): if hasattr(self, "_summary_writer"): self._summary_writer.close() del self._summary_writer @@ -471,5 +470,5 @@ class TensorboardCallback(Callback): if __name__ == "__main__": manager = CallbackManager(env={"n_epoch": 3}, callbacks=[DummyCallback(), DummyCallback()]) - manager.on_train_begin(10, 11, 12) + manager.on_train_begin() # print(manager.after_epoch()) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index ddd35b28..25a32787 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -122,6 +122,8 @@ class Trainer(object): self.sampler = sampler self.prefetch = prefetch self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) + self.n_steps = (len(self.train_data) // self.batch_size + int( + len(self.train_data) % self.batch_size != 0)) * self.n_epochs if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer @@ -129,6 +131,7 @@ class Trainer(object): self.optimizer = optimizer.construct_from_pytorch(self.model.parameters()) self.use_tqdm = use_tqdm + self.pbar = None self.print_every = abs(self.print_every) if self.dev_data is not None: @@ -198,9 +201,9 @@ class Trainer(object): try: self.callback_manager.on_train_begin() self._train() - self.callback_manager.on_train_end(self.model) + self.callback_manager.on_train_end() except (CallbackException, KeyboardInterrupt) as e: - self.callback_manager.on_exception(e, self.model) + self.callback_manager.on_exception(e) if self.dev_data is not None: print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + @@ -227,18 +230,21 @@ class Trainer(object): else: inner_tqdm = tqdm self.step = 0 + self.epoch = 0 start = time.time() - total_steps = (len(self.train_data) // self.batch_size + int( - len(self.train_data) % self.batch_size != 0)) * self.n_epochs - with inner_tqdm(total=total_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: + + with inner_tqdm(total=self.n_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: + self.pbar = pbar if isinstance(pbar, tqdm) else None avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) for epoch in range(1, self.n_epochs+1): + self.epoch = epoch pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) # early stopping - self.callback_manager.on_epoch_begin(epoch, self.n_epochs) + self.callback_manager.on_epoch_begin() for batch_x, batch_y in data_iterator: + self.step += 1 _move_dict_value_to_device(batch_x, batch_y, device=self._model_device) indices = data_iterator.get_batch_indices() # negative sampling; replace unknown; re-weight batch_y @@ -251,14 +257,14 @@ class Trainer(object): avg_loss += loss.item() # Is loss NaN or inf? requires_grad = False - self.callback_manager.on_backward_begin(loss, self.model) + self.callback_manager.on_backward_begin(loss) self._grad_backward(loss) - self.callback_manager.on_backward_end(self.model) + self.callback_manager.on_backward_end() self._update() - self.callback_manager.on_step_end(self.optimizer) + self.callback_manager.on_step_end() - if (self.step+1) % self.print_every == 0: + if self.step % self.print_every == 0: if self.use_tqdm: print_output = "loss:{0:<6.5f}".format(avg_loss / self.print_every) pbar.update(self.print_every) @@ -269,7 +275,6 @@ class Trainer(object): epoch, self.step, avg_loss, diff) pbar.set_postfix_str(print_output) avg_loss = 0 - self.step += 1 self.callback_manager.on_batch_end() if ((self.validate_every > 0 and self.step % self.validate_every == 0) or @@ -277,16 +282,17 @@ class Trainer(object): and self.dev_data is not None: eval_res = self._do_validation(epoch=epoch, step=self.step) eval_str = "Evaluation at Epoch {}/{}. Step:{}/{}. ".format(epoch, self.n_epochs, self.step, - total_steps) + \ + self.n_steps) + \ self.tester._format_eval_results(eval_res) pbar.write(eval_str) # ================= mini-batch end ==================== # # lr decay; early stopping - self.callback_manager.on_epoch_end(epoch, self.n_epochs, self.optimizer) + self.callback_manager.on_epoch_end() # =============== epochs end =================== # pbar.close() + self.pbar = None # ============ tqdm end ============== # def _do_validation(self, epoch, step): @@ -303,7 +309,7 @@ class Trainer(object): self.best_dev_epoch = epoch self.best_dev_step = step # get validation results; adjust optimizer - self.callback_manager.on_valid_end(res, self.metric_key, self.optimizer) + self.callback_manager.on_valid_end(res, self.metric_key) return res def _mode(self, model, is_test=False): diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 07b721c5..93e95033 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,4 +1,5 @@ import os +import json from fastNLP.core.dataset import DataSet from fastNLP.core.instance import Instance @@ -64,6 +65,53 @@ def convert_seq2seq_dataset(data): return dataset +def download_from_url(url, path): + from tqdm import tqdm + import requests + + """Download file""" + r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) + chunk_size = 16 * 1024 + total_size = int(r.headers.get('Content-length', 0)) + with open(path, "wb") as file ,\ + tqdm(total=total_size, unit='B', unit_scale=1, desc=path.split('/')[-1]) as t: + for chunk in r.iter_content(chunk_size): + if chunk: + file.write(chunk) + t.update(len(chunk)) + return + +def uncompress(src, dst): + import zipfile, gzip, tarfile, os + + def unzip(src, dst): + with zipfile.ZipFile(src, 'r') as f: + f.extractall(dst) + + def ungz(src, dst): + with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: + length = 16 * 1024 # 16KB + buf = f.read(length) + while buf: + uf.write(buf) + buf = f.read(length) + + def untar(src, dst): + with tarfile.open(src, 'r:gz') as f: + f.extractall(dst) + + fn, ext = os.path.splitext(src) + _, ext_2 = os.path.splitext(fn) + if ext == '.zip': + unzip(src, dst) + elif ext == '.gz' and ext_2 != '.tar': + ungz(src, dst) + elif (ext == '.gz' and ext_2 == '.tar') or ext_2 == '.tgz': + untar(src, dst) + else: + raise ValueError('unsupported file {}'.format(src)) + + class DataSetLoader: """Interface for all DataSetLoaders. @@ -290,41 +338,6 @@ class DummyClassificationReader(DataSetLoader): return convert_seq2tag_dataset(data) -class ConllLoader(DataSetLoader): - """loader for conll format files""" - - def __init__(self): - super(ConllLoader, self).__init__() - - def load(self, data_path): - with open(data_path, "r", encoding="utf-8") as f: - lines = f.readlines() - data = self.parse(lines) - return self.convert(data) - - @staticmethod - def parse(lines): - """ - :param list lines: a list containing all lines in a conll file. - :return: a 3D list - """ - sentences = list() - tokens = list() - for line in lines: - if line[0] == "#": - # skip the comments - continue - if line == "\n": - sentences.append(tokens) - tokens = [] - continue - tokens.append(line.split()) - return sentences - - def convert(self, data): - pass - - class DummyLMReader(DataSetLoader): """A Dummy Language Model Dataset Reader """ @@ -434,51 +447,67 @@ class PeopleDailyCorpusLoader(DataSetLoader): return data_set -class Conll2003Loader(DataSetLoader): +class ConllLoader: + def __init__(self, headers, indexs=None): + self.headers = headers + if indexs is None: + self.indexs = list(range(len(self.headers))) + else: + if len(indexs) != len(headers): + raise ValueError + self.indexs = indexs + + def load(self, path): + datalist = [] + with open(path, 'r', encoding='utf-8') as f: + sample = [] + start = next(f) + if '-DOCSTART-' not in start: + sample.append(start.split()) + for line in f: + if line.startswith('\n'): + if len(sample): + datalist.append(sample) + sample = [] + elif line.startswith('#'): + continue + else: + sample.append(line.split()) + if len(sample) > 0: + datalist.append(sample) + + data = [self.get_one(sample) for sample in datalist] + data = filter(lambda x: x is not None, data) + + ds = DataSet() + for sample in data: + ins = Instance() + for name, idx in zip(self.headers, self.indexs): + ins.add_field(field_name=name, field=sample[idx]) + ds.append(ins) + return ds + + def get_one(self, sample): + sample = list(map(list, zip(*sample))) + for field in sample: + if len(field) <= 0: + return None + return sample + + +class Conll2003Loader(ConllLoader): """Loader for conll2003 dataset More information about the given dataset cound be found on https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data - + + Deprecated. Use ConllLoader for all types of conll-format files. """ def __init__(self): - super(Conll2003Loader, self).__init__() - - def load(self, dataset_path): - with open(dataset_path, "r", encoding="utf-8") as f: - lines = f.readlines() - parsed_data = [] - sentence = [] - tokens = [] - for line in lines: - if '-DOCSTART- -X- -X- O' in line or line == '\n': - if sentence != []: - parsed_data.append((sentence, tokens)) - sentence = [] - tokens = [] - continue - - temp = line.strip().split(" ") - sentence.append(temp[0]) - tokens.append(temp[1:4]) - - return self.convert(parsed_data) - - def convert(self, parsed_data): - dataset = DataSet() - for sample in parsed_data: - label0_list = list(map( - lambda labels: labels[0], sample[1])) - label1_list = list(map( - lambda labels: labels[1], sample[1])) - label2_list = list(map( - lambda labels: labels[2], sample[1])) - dataset.append(Instance(tokens=sample[0], - pos=label0_list, - chucks=label1_list, - ner=label2_list)) - - return dataset + headers = [ + 'tokens', 'pos', 'chunks', 'ner', + ] + super(Conll2003Loader, self).__init__(headers=headers) class SNLIDataSetReader(DataSetLoader): @@ -548,6 +577,7 @@ class SNLIDataSetReader(DataSetLoader): class ConllCWSReader(object): + """Deprecated. Use ConllLoader for all types of conll-format files.""" def __init__(self): pass @@ -700,6 +730,7 @@ def cut_long_sentence(sent, max_sample_length=200): class ZhConllPOSReader(object): """读取中文Conll格式。返回“字级别”的标签,使用BMES记号扩展原来的词级别标签。 + Deprecated. Use ConllLoader for all types of conll-format files. """ def __init__(self): pass @@ -778,47 +809,78 @@ class ZhConllPOSReader(object): return text, pos_tags -class ConllxDataLoader(object): +class ConllxDataLoader(ConllLoader): """返回“词级别”的标签信息,包括词、词性、(句法)头依赖、(句法)边标签。跟``ZhConllPOSReader``完全不同。 + Deprecated. Use ConllLoader for all types of conll-format files. """ + def __init__(self): + headers = [ + 'words', 'pos_tags', 'heads', 'labels', + ] + indexs = [ + 1, 3, 6, 7, + ] + super(ConllxDataLoader, self).__init__(headers=headers, indexs=indexs) + + +class SSTLoader(DataSetLoader): + """load SST data in PTB tree format + data source: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip + """ + def __init__(self, subtree=False, fine_grained=False): + self.subtree = subtree + + tag_v = {'0':'very negative', '1':'negative', '2':'neutral', + '3':'positive', '4':'very positive'} + if not fine_grained: + tag_v['0'] = tag_v['1'] + tag_v['4'] = tag_v['3'] + self.tag_v = tag_v + def load(self, path): - datalist = [] with open(path, 'r', encoding='utf-8') as f: - sample = [] - for line in f: - if line.startswith('\n'): - datalist.append(sample) - sample = [] - elif line.startswith('#'): - continue - else: - sample.append(line.split('\t')) - if len(sample) > 0: - datalist.append(sample) + datas = [] + for l in f: + datas.extend([(s, self.tag_v[t]) + for s, t in self.get_one(l, self.subtree)]) + ds = DataSet() + for words, tag in datas: + ds.append(Instance(words=words, raw_tag=tag)) + return ds - data = [self.get_one(sample) for sample in datalist] - data_list = list(filter(lambda x: x is not None, data)) + @staticmethod + def get_one(data, subtree): + from nltk.tree import Tree + tree = Tree.fromstring(data) + if subtree: + return [(t.leaves(), t.label()) for t in tree.subtrees()] + return [(tree.leaves(), tree.label())] + + +class JsonLoader(DataSetLoader): + """Load json-format data, + every line contains a json obj, like a dict + fields is the dict key that need to be load + """ + def __init__(self, **fields): + super(JsonLoader, self).__init__() + self.fields = {} + for k, v in fields.items(): + self.fields[k] = k if v is None else v + def load(self, path): + with open(path, 'r', encoding='utf-8') as f: + datas = [json.loads(l) for l in f] ds = DataSet() - for example in data_list: - ds.append(Instance(words=example[0], - pos_tags=example[1], - heads=example[2], - labels=example[3])) + for d in datas: + ins = Instance() + for k, v in d.items(): + if k in self.fields: + ins.add_field(self.fields[k], v) + ds.append(ins) return ds - def get_one(self, sample): - sample = list(map(list, zip(*sample))) - if len(sample) == 0: - return None - for w in sample[7]: - if w == '_': - print('Error Sample {}'.format(sample)) - return None - # return word_seq, pos_seq, head_seq, head_tag_seq - return sample[1], sample[3], list(map(int, sample[6])), sample[7] - def add_seg_tag(data): """ @@ -840,3 +902,4 @@ def add_seg_tag(data): new_sample.append((word[-1], 'E-' + pos)) _processed.append(list(map(list, zip(*new_sample)))) return _processed + diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index 22e323ce..6b51c897 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -92,9 +92,9 @@ class ENASTrainer(fastNLP.Trainer): try: self.callback_manager.on_train_begin() self._train() - self.callback_manager.on_train_end(self.model) + self.callback_manager.on_train_end() except (CallbackException, KeyboardInterrupt) as e: - self.callback_manager.on_exception(e, self.model) + self.callback_manager.on_exception(e) if self.dev_data is not None: print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + @@ -134,7 +134,7 @@ class ENASTrainer(fastNLP.Trainer): if epoch == self.n_epochs + 1 - self.final_epochs: print('Entering the final stage. (Only train the selected structure)') # early stopping - self.callback_manager.on_epoch_begin(epoch, self.n_epochs) + self.callback_manager.on_epoch_begin() # 1. Training the shared parameters omega of the child models self.train_shared(pbar) @@ -155,7 +155,7 @@ class ENASTrainer(fastNLP.Trainer): pbar.write(eval_str) # lr decay; early stopping - self.callback_manager.on_epoch_end(epoch, self.n_epochs, self.optimizer) + self.callback_manager.on_epoch_end() # =============== epochs end =================== # pbar.close() # ============ tqdm end ============== # @@ -234,12 +234,12 @@ class ENASTrainer(fastNLP.Trainer): avg_loss += loss.item() # Is loss NaN or inf? requires_grad = False - self.callback_manager.on_backward_begin(loss, self.model) + self.callback_manager.on_backward_begin(loss) self._grad_backward(loss) - self.callback_manager.on_backward_end(self.model) + self.callback_manager.on_backward_end() self._update() - self.callback_manager.on_step_end(self.optimizer) + self.callback_manager.on_step_end() if (self.step+1) % self.print_every == 0: if self.use_tqdm: diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py new file mode 100644 index 00000000..3af3fe19 --- /dev/null +++ b/fastNLP/models/star_transformer.py @@ -0,0 +1,181 @@ +from fastNLP.modules.encoder.star_transformer import StarTransformer +from fastNLP.core.utils import seq_lens_to_masks + +import torch +from torch import nn +import torch.nn.functional as F + + +class StarTransEnc(nn.Module): + def __init__(self, vocab_size, emb_dim, + hidden_size, + num_layers, + num_head, + head_dim, + max_len, + emb_dropout, + dropout): + super(StarTransEnc, self).__init__() + self.emb_fc = nn.Linear(emb_dim, hidden_size) + self.emb_drop = nn.Dropout(emb_dropout) + self.embedding = nn.Embedding(vocab_size, emb_dim) + self.encoder = StarTransformer(hidden_size=hidden_size, + num_layers=num_layers, + num_head=num_head, + head_dim=head_dim, + dropout=dropout, + max_len=max_len) + + def forward(self, x, mask): + x = self.embedding(x) + x = self.emb_fc(self.emb_drop(x)) + nodes, relay = self.encoder(x, mask) + return nodes, relay + + +class Cls(nn.Module): + def __init__(self, in_dim, num_cls, hid_dim, dropout=0.1): + super(Cls, self).__init__() + self.fc = nn.Sequential( + nn.Linear(in_dim, hid_dim), + nn.LeakyReLU(), + nn.Dropout(dropout), + nn.Linear(hid_dim, num_cls), + ) + + def forward(self, x): + h = self.fc(x) + return h + + +class NLICls(nn.Module): + def __init__(self, in_dim, num_cls, hid_dim, dropout=0.1): + super(NLICls, self).__init__() + self.fc = nn.Sequential( + nn.Dropout(dropout), + nn.Linear(in_dim*4, hid_dim), #4 + nn.LeakyReLU(), + nn.Dropout(dropout), + nn.Linear(hid_dim, num_cls), + ) + + def forward(self, x1, x2): + x = torch.cat([x1, x2, torch.abs(x1-x2), x1*x2], 1) + h = self.fc(x) + return h + +class STSeqLabel(nn.Module): + """star-transformer model for sequence labeling + """ + def __init__(self, vocab_size, emb_dim, num_cls, + hidden_size=300, + num_layers=4, + num_head=8, + head_dim=32, + max_len=512, + cls_hidden_size=600, + emb_dropout=0.1, + dropout=0.1,): + super(STSeqLabel, self).__init__() + self.enc = StarTransEnc(vocab_size=vocab_size, + emb_dim=emb_dim, + hidden_size=hidden_size, + num_layers=num_layers, + num_head=num_head, + head_dim=head_dim, + max_len=max_len, + emb_dropout=emb_dropout, + dropout=dropout) + self.cls = Cls(hidden_size, num_cls, cls_hidden_size) + + def forward(self, word_seq, seq_lens): + mask = seq_lens_to_masks(seq_lens) + nodes, _ = self.enc(word_seq, mask) + output = self.cls(nodes) + output = output.transpose(1,2) # make hidden to be dim 1 + return {'output': output} # [bsz, n_cls, seq_len] + + def predict(self, word_seq, seq_lens): + y = self.forward(word_seq, seq_lens) + _, pred = y['output'].max(1) + return {'output': pred, 'seq_lens': seq_lens} + + +class STSeqCls(nn.Module): + """star-transformer model for sequence classification + """ + + def __init__(self, vocab_size, emb_dim, num_cls, + hidden_size=300, + num_layers=4, + num_head=8, + head_dim=32, + max_len=512, + cls_hidden_size=600, + emb_dropout=0.1, + dropout=0.1,): + super(STSeqCls, self).__init__() + self.enc = StarTransEnc(vocab_size=vocab_size, + emb_dim=emb_dim, + hidden_size=hidden_size, + num_layers=num_layers, + num_head=num_head, + head_dim=head_dim, + max_len=max_len, + emb_dropout=emb_dropout, + dropout=dropout) + self.cls = Cls(hidden_size, num_cls, cls_hidden_size) + + def forward(self, word_seq, seq_lens): + mask = seq_lens_to_masks(seq_lens) + nodes, relay = self.enc(word_seq, mask) + y = 0.5 * (relay + nodes.max(1)[0]) + output = self.cls(y) # [bsz, n_cls] + return {'output': output} + + def predict(self, word_seq, seq_lens): + y = self.forward(word_seq, seq_lens) + _, pred = y['output'].max(1) + return {'output': pred} + + +class STNLICls(nn.Module): + """star-transformer model for NLI + """ + + def __init__(self, vocab_size, emb_dim, num_cls, + hidden_size=300, + num_layers=4, + num_head=8, + head_dim=32, + max_len=512, + cls_hidden_size=600, + emb_dropout=0.1, + dropout=0.1,): + super(STNLICls, self).__init__() + self.enc = StarTransEnc(vocab_size=vocab_size, + emb_dim=emb_dim, + hidden_size=hidden_size, + num_layers=num_layers, + num_head=num_head, + head_dim=head_dim, + max_len=max_len, + emb_dropout=emb_dropout, + dropout=dropout) + self.cls = NLICls(hidden_size, num_cls, cls_hidden_size) + + def forward(self, word_seq1, word_seq2, seq_lens1, seq_lens2): + mask1 = seq_lens_to_masks(seq_lens1) + mask2 = seq_lens_to_masks(seq_lens2) + def enc(seq, mask): + nodes, relay = self.enc(seq, mask) + return 0.5 * (relay + nodes.max(1)[0]) + y1 = enc(word_seq1, mask1) + y2 = enc(word_seq2, mask2) + output = self.cls(y1, y2) # [bsz, n_cls] + return {'output': output} + + def predict(self, word_seq1, word_seq2, seq_lens1, seq_lens2): + y = self.forward(word_seq1, word_seq2, seq_lens1, seq_lens2) + _, pred = y['output'].max(1) + return {'output': pred} diff --git a/reproduction/README.md b/reproduction/README.md new file mode 100644 index 00000000..1c93c6bc --- /dev/null +++ b/reproduction/README.md @@ -0,0 +1,44 @@ +# 模型复现 +这里复现了在fastNLP中实现的模型,旨在达到与论文中相符的性能。 + +复现的模型有: +- Star-Transformer +- ... + + +## Star-Transformer +[reference](https://arxiv.org/abs/1902.09113) +### Performance +|任务| 数据集 | SOTA | 模型表现 | +|------|------| ------| ------| +|Pos Tagging|CTB 9.0|-|ACC 92.31| +|Pos Tagging|CONLL 2012|-|ACC 96.51| +|Named Entity Recognition|CONLL 2012|-|F1 85.66| +|Text Classification|SST|-|49.18| +|Natural Language Inference|SNLI|-|83.76| + +### Usage +``` python +# for sequence labeling(ner, pos tagging, etc) +from fastNLP.models.star_transformer import STSeqLabel +model = STSeqLabel( + vocab_size=10000, num_cls=50, + emb_dim=300) + + +# for sequence classification +from fastNLP.models.star_transformer import STSeqCls +model = STSeqCls( + vocab_size=10000, num_cls=50, + emb_dim=300) + + +# for natural language inference +from fastNLP.models.star_transformer import STNLICls +model = STNLICls( + vocab_size=10000, num_cls=50, + emb_dim=300) + +``` + +## ... diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 68c874fa..ee5b0e58 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -353,7 +353,7 @@ class TestTutorial(unittest.TestCase): train_data[-1], dev_data[-1], test_data[-1] # 读入vocab文件 - with open('vocab.txt') as f: + with open('vocab.txt', encoding='utf-8') as f: lines = f.readlines() vocabs = [] for line in lines: From 58f373d371e87ea0e5a788469c6997a3041424af Mon Sep 17 00:00:00 2001 From: yunfan Date: Fri, 12 Apr 2019 15:55:22 +0800 Subject: [PATCH 021/173] - fix test --- fastNLP/core/callback.py | 6 ++---- test/core/test_metrics.py | 12 ++++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 4a4e29a5..1bda1f93 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -249,13 +249,11 @@ class GradientClipCallback(Callback): self.parameters = parameters self.clip_value = clip_value - def on_backward_end(self, model): + def on_backward_end(self): if self.parameters is None: - self.clip_fun(model.parameters(), self.clip_value) + self.clip_fun(self.model.parameters(), self.clip_value) else: self.clip_fun(self.parameters, self.clip_value) - def on_backward_end(self): - self.clip_fun(self.model.parameters(), self.clip_value) class CallbackException(BaseException): diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index 80ed54e2..25138478 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -141,11 +141,11 @@ class SpanF1PreRecMetric(unittest.TestCase): bmes_lst = ['M-8', 'S-2', 'S-0', 'B-9', 'B-6', 'E-5', 'B-7', 'S-2', 'E-7', 'S-8'] bio_lst = ['O-8', 'O-2', 'B-0', 'O-9', 'I-6', 'I-5', 'I-7', 'I-2', 'I-7', 'O-8'] expect_bmes_res = set() - expect_bmes_res.update([('8', (0, 0)), ('2', (1, 1)), ('0', (2, 2)), ('9', (3, 3)), ('6', (4, 4)), - ('5', (5, 5)), ('7', (6, 6)), ('2', (7, 7)), ('7', (8, 8)), ('8', (9, 9))]) + expect_bmes_res.update([('8', (0, 1)), ('2', (1, 2)), ('0', (2, 3)), ('9', (3, 4)), ('6', (4, 5)), + ('5', (5, 6)), ('7', (6, 7)), ('2', (7, 8)), ('7', (8, 9)), ('8', (9, 10))]) expect_bio_res = set() - expect_bio_res.update([('7', (8, 8)), ('0', (2, 2)), ('2', (7, 7)), ('5', (5, 5)), - ('6', (4, 4)), ('7', (6, 6))]) + expect_bio_res.update([('7', (8, 9)), ('0', (2, 3)), ('2', (7, 8)), ('5', (5, 6)), + ('6', (4, 5)), ('7', (6, 7))]) self.assertSetEqual(expect_bmes_res,set(bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 @@ -168,9 +168,9 @@ class SpanF1PreRecMetric(unittest.TestCase): bmes_lst = ['B', 'E', 'B', 'S', 'B', 'M', 'E', 'M', 'B', 'E'] bio_lst = ['I', 'B', 'O', 'O', 'I', 'O', 'I', 'B', 'O', 'O'] expect_bmes_res = set() - expect_bmes_res.update([('', (0, 1)), ('', (2, 2)), ('', (3, 3)), ('', (4, 6)), ('', (7, 7)), ('', (8, 9))]) + expect_bmes_res.update([('', (0, 2)), ('', (2, 3)), ('', (3, 4)), ('', (4, 7)), ('', (7, 8)), ('', (8, 10))]) expect_bio_res = set() - expect_bio_res.update([('', (7, 7)), ('', (6, 6)), ('', (4, 4)), ('', (0, 0)), ('', (1, 1))]) + expect_bio_res.update([('', (7, 8)), ('', (6, 7)), ('', (4, 5)), ('', (0, 1)), ('', (1, 2))]) self.assertSetEqual(expect_bmes_res,set(bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 From f4e64906d46a66ea2e12e24fe29ce0e19614c26e Mon Sep 17 00:00:00 2001 From: yunfan Date: Fri, 12 Apr 2019 16:27:45 +0800 Subject: [PATCH 022/173] - fix callback & tests --- fastNLP/core/callback.py | 34 ++++++++++- test/core/test_callbacks.py | 8 ++- test/core/test_dataset.py | 1 + test/models/test_enas.py | 112 ------------------------------------ 4 files changed, 40 insertions(+), 115 deletions(-) delete mode 100644 test/models/test_enas.py diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 437b647a..86e06a7e 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -29,7 +29,7 @@ class Callback(object): @property def n_steps(self): """total number of steps for training""" - return self.n_steps + return self._trainer.n_steps @property def batch_size(self): @@ -124,6 +124,21 @@ class Callback(object): pass +def transfer(func): + """装饰器,将对CallbackManager的调用转发到各个Callback子类. + :param func: + :return: + """ + + def wrapper(manager, *arg): + returns = [] + for callback in manager.callbacks: + returns.append(getattr(callback, func.__name__)(*arg)) + return returns + + return wrapper + + class CallbackManager(Callback): """A manager for all callbacks passed into Trainer. It collects resources inside Trainer and raise callbacks. @@ -150,42 +165,59 @@ class CallbackManager(Callback): else: raise TypeError(f"Expect callbacks in CallbackManager(callbacks) to be list. Got {type(callbacks)}.") + for env_name, env_val in env.items(): + for callback in self.callbacks: + setattr(callback, '_'+env_name, env_val) # Callback.trainer + + @transfer def on_train_begin(self): pass + @transfer def on_epoch_begin(self): pass + @transfer def on_batch_begin(self, batch_x, batch_y, indices): pass + @transfer def on_loss_begin(self, batch_y, predict_y): pass + @transfer def on_backward_begin(self, loss): pass + @transfer def on_backward_end(self): pass + @transfer def on_step_end(self): pass + @transfer def on_batch_end(self): pass + @transfer def on_valid_begin(self): pass + @transfer def on_valid_end(self, eval_result, metric_key): pass + @transfer def on_epoch_end(self): pass + @transfer def on_train_end(self): pass + @transfer def on_exception(self, exception): pass diff --git a/test/core/test_callbacks.py b/test/core/test_callbacks.py index 7d66620c..3329e7a1 100644 --- a/test/core/test_callbacks.py +++ b/test/core/test_callbacks.py @@ -139,11 +139,14 @@ class TestCallback(unittest.TestCase): def test_readonly_property(self): from fastNLP.core.callback import Callback + passed_epochs = [] + total_epochs = 5 class MyCallback(Callback): def __init__(self): super(MyCallback, self).__init__() - def on_epoch_begin(self, cur_epoch, total_epoch): + def on_epoch_begin(self): + passed_epochs.append(self.epoch) print(self.n_epochs, self.n_steps, self.batch_size) print(self.model) print(self.optimizer) @@ -151,7 +154,7 @@ class TestCallback(unittest.TestCase): data_set, model = prepare_env() trainer = Trainer(data_set, model, loss=BCELoss(pred="predict", target="y"), - n_epochs=5, + n_epochs=total_epochs, batch_size=32, print_every=50, optimizer=SGD(lr=0.1), @@ -161,3 +164,4 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[MyCallback()]) trainer.train() + assert passed_epochs == list(range(1, total_epochs+1)) diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 607f9a13..356b157a 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -217,6 +217,7 @@ class TestDataSetMethods(unittest.TestCase): self.assertTrue(len(ds) > 0) def test_add_null(self): + # TODO test failed because 'fastNLP\core\fieldarray.py:143: RuntimeError' ds = DataSet() ds.add_field('test', []) ds.set_target('test') diff --git a/test/models/test_enas.py b/test/models/test_enas.py deleted file mode 100644 index 07a43205..00000000 --- a/test/models/test_enas.py +++ /dev/null @@ -1,112 +0,0 @@ -import unittest - -from fastNLP import DataSet -from fastNLP import Instance -from fastNLP import Vocabulary -from fastNLP.core.losses import CrossEntropyLoss -from fastNLP.core.metrics import AccuracyMetric - - -class TestENAS(unittest.TestCase): - def testENAS(self): - # 从csv读取数据到DataSet - sample_path = "tutorials/sample_data/tutorial_sample_dataset.csv" - dataset = DataSet.read_csv(sample_path, headers=('raw_sentence', 'label'), - sep='\t') - print(len(dataset)) - print(dataset[0]) - print(dataset[-3]) - - dataset.append(Instance(raw_sentence='fake data', label='0')) - # 将所有数字转为小写 - dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') - # label转int - dataset.apply(lambda x: int(x['label']), new_field_name='label') - - # 使用空格分割句子 - def split_sent(ins): - return ins['raw_sentence'].split() - - dataset.apply(split_sent, new_field_name='words') - - # 增加长度信息 - dataset.apply(lambda x: len(x['words']), new_field_name='seq_len') - print(len(dataset)) - print(dataset[0]) - - # DataSet.drop(func)筛除数据 - dataset.drop(lambda x: x['seq_len'] <= 3) - print(len(dataset)) - - # 设置DataSet中,哪些field要转为tensor - # set target,loss或evaluate中的golden,计算loss,模型评估时使用 - dataset.set_target("label") - # set input,模型forward时使用 - dataset.set_input("words", "seq_len") - - # 分出测试集、训练集 - test_data, train_data = dataset.split(0.5) - print(len(test_data)) - print(len(train_data)) - - # 构建词表, Vocabulary.add(word) - vocab = Vocabulary(min_freq=2) - train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) - vocab.build_vocab() - - # index句子, Vocabulary.to_index(word) - train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') - test_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') - print(test_data[0]) - - # 如果你们需要做强化学习或者GAN之类的项目,你们也可以使用这些数据预处理的工具 - from fastNLP.core.batch import Batch - from fastNLP.core.sampler import RandomSampler - - batch_iterator = Batch(dataset=train_data, batch_size=2, sampler=RandomSampler()) - for batch_x, batch_y in batch_iterator: - print("batch_x has: ", batch_x) - print("batch_y has: ", batch_y) - break - - from fastNLP.models.enas_model import ENASModel - from fastNLP.models.enas_controller import Controller - model = ENASModel(embed_num=len(vocab), num_classes=5) - controller = Controller() - - from fastNLP.models.enas_trainer import ENASTrainer - from copy import deepcopy - - # 更改DataSet中对应field的名称,要以模型的forward等参数名一致 - train_data.rename_field('words', 'word_seq') # input field 与 forward 参数一致 - train_data.rename_field('label', 'label_seq') - test_data.rename_field('words', 'word_seq') - test_data.rename_field('label', 'label_seq') - - loss = CrossEntropyLoss(pred="output", target="label_seq") - metric = AccuracyMetric(pred="predict", target="label_seq") - - trainer = ENASTrainer(model=model, controller=controller, train_data=train_data, dev_data=test_data, - loss=CrossEntropyLoss(pred="output", target="label_seq"), - metrics=AccuracyMetric(pred="predict", target="label_seq"), - check_code_level=-1, - save_path=None, - batch_size=32, - print_every=1, - n_epochs=3, - final_epochs=1) - trainer.train() - print('Train finished!') - - # 调用Tester在test_data上评价效果 - from fastNLP import Tester - - tester = Tester(data=test_data, model=model, metrics=AccuracyMetric(pred="predict", target="label_seq"), - batch_size=4) - - acc = tester.test() - print(acc) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From 3d08b38def76b34d055bb8590566a40cc9679453 Mon Sep 17 00:00:00 2001 From: yunfan Date: Sat, 13 Apr 2019 14:01:18 +0800 Subject: [PATCH 023/173] - update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e51cf62..c1b5db3e 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ fastNLP implements different models for variant NLP tasks. Each model has been trained and tested carefully. Check out models' performance, usage and source code here. -- [Documentation](https://github.com/fastnlp/fastNLP/tree/master/reproduction) -- [Source Code](https://github.com/fastnlp/fastNLP/tree/master/fastNLP/models) +- [Documentation](reproduction/) +- [Source Code](fastNLP/models/) ## Project Structure From 4d1721ffe365d53351c21241dfd7fdb6114c6bed Mon Sep 17 00:00:00 2001 From: yh Date: Sun, 14 Apr 2019 18:00:21 +0800 Subject: [PATCH 024/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86bug?= =?UTF-8?q?=EF=BC=9B=E8=B0=83=E6=95=B4callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/batch.py | 6 ++-- fastNLP/core/callback.py | 51 ++++++++++----------------- fastNLP/core/dataset.py | 32 ++++++++--------- fastNLP/core/fieldarray.py | 17 +++++++++ fastNLP/core/metrics.py | 72 ++++---------------------------------- fastNLP/core/trainer.py | 31 ++++++++++------ setup.py | 4 +-- test/automl/test_enas.py | 2 +- test/core/test_dataset.py | 4 +-- test/test_tutorials.py | 4 +-- 10 files changed, 87 insertions(+), 136 deletions(-) diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 88d9185d..d07df047 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -14,15 +14,17 @@ class Batch(object): :param DataSet dataset: a DataSet object :param int batch_size: the size of the batch - :param Sampler sampler: a Sampler object + :param Sampler sampler: a Sampler object. If None, use fastNLP.sampler.RandomSampler :param bool as_numpy: If True, return Numpy array. Otherwise, return torch tensors. :param bool prefetch: If True, use multiprocessing to fetch next batch when training. :param str or torch.device device: the batch's device, if as_numpy is True, device is ignored. """ - def __init__(self, dataset, batch_size, sampler=RandomSampler(), as_numpy=False, prefetch=False): + def __init__(self, dataset, batch_size, sampler=None, as_numpy=False, prefetch=False): self.dataset = dataset self.batch_size = batch_size + if sampler is None: + sampler = RandomSampler() self.sampler = sampler self.as_numpy = as_numpy self.idx_list = None diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index e3b4f36e..57f94bc4 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -17,37 +17,37 @@ class Callback(object): super(Callback, self).__init__() self.trainer = None # 在Trainer内部被重新赋值 - # callback只读属性 - self._n_epochs = None - self._n_steps = None - self._batch_size = None - self._model = None - self._pbar = None - self._optimizer = None - @property def n_epochs(self): - return self._n_epochs + return self.trainer.n_epochs + + @property + def epoch(self): + return self.trainer.epoch @property def n_steps(self): - return self._n_steps + return self.trainer.n_steps + + @property + def step(self): + return self.trainer.step @property def batch_size(self): - return self._batch_size + return self.trainer.batch_size @property def model(self): - return self._model + return self.trainer.model @property def pbar(self): - return self._pbar + return self.trainer.pbar @property def optimizer(self): - return self._optimizer + return self.trainer.optimizer def on_train_begin(self): # before the main training loop @@ -82,13 +82,14 @@ class Callback(object): def on_valid_begin(self): pass - def on_valid_end(self, eval_result, metric_key, optimizer): + def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): """ 每次执行验证机的evaluation后会调用。传入eval_result :param eval_result: Dict[str: Dict[str: float]], evaluation的结果 :param metric_key: str - :param optimizer: + :param optimizer: optimizer passed to trainer + :param is_better_eval: bool, 当前dev结果是否比之前的好 :return: """ pass @@ -145,11 +146,10 @@ class CallbackManager(Callback): """ - def __init__(self, env, attr, callbacks=None): + def __init__(self, env, callbacks=None): """ :param dict env: The key is the name of the Trainer attribute(str). The value is the attribute itself. - :param dict attr: read-only attributes for all callbacks :param Callback callbacks: """ super(CallbackManager, self).__init__() @@ -170,19 +170,6 @@ class CallbackManager(Callback): for callback in self.callbacks: setattr(callback, env_name, env_val) # Callback.trainer - self.set_property(**attr) - - def set_property(self, **kwargs): - """设置所有callback的只读属性 - - :param kwargs: - :return: - """ - for callback in self.callbacks: - for k, v in kwargs.items(): - setattr(callback, "_" + k, v) - - @transfer def on_train_begin(self): pass @@ -220,7 +207,7 @@ class CallbackManager(Callback): pass @transfer - def on_valid_end(self, eval_result, metric_key, optimizer): + def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): pass @transfer diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 24376a72..068afb38 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -90,7 +90,7 @@ class DataSet(object): data_set = DataSet() for field in self.field_arrays.values(): data_set.add_field(name=field.name, fields=field.content[idx], padder=field.padder, - is_input=field.is_input, is_target=field.is_target) + is_input=field.is_input, is_target=field.is_target, ignore_type=field.ignore_type) return data_set elif isinstance(idx, str): if idx not in self: @@ -313,16 +313,23 @@ class DataSet(object): else: return results - def drop(self, func): + def drop(self, func, inplace=True): """Drop instances if a condition holds. :param func: a function that takes an Instance object as input, and returns bool. The instance will be dropped if the function returns True. + :param inplace: bool, whether to drop inpalce. Otherwise a new dataset will be returned. """ - results = [ins for ins in self._inner_iter() if not func(ins)] - for name, old_field in self.field_arrays.items(): - self.field_arrays[name].content = [ins[name] for ins in results] + if inplace: + results = [ins for ins in self._inner_iter() if not func(ins)] + for name, old_field in self.field_arrays.items(): + self.field_arrays[name].content = [ins[name] for ins in results] + else: + results = [ins for ins in self if not func(ins)] + data = DataSet(results) + for field_name, field in self.field_arrays.items(): + data.field_arrays[field_name].to(field) def split(self, dev_ratio): """Split the dataset into training and development(validation) set. @@ -346,19 +353,8 @@ class DataSet(object): for idx in train_indices: train_set.append(self[idx]) for field_name in self.field_arrays: - train_set.field_arrays[field_name].is_input = self.field_arrays[field_name].is_input - train_set.field_arrays[field_name].is_target = self.field_arrays[field_name].is_target - train_set.field_arrays[field_name].padder = self.field_arrays[field_name].padder - train_set.field_arrays[field_name].dtype = self.field_arrays[field_name].dtype - train_set.field_arrays[field_name].pytype = self.field_arrays[field_name].pytype - train_set.field_arrays[field_name].content_dim = self.field_arrays[field_name].content_dim - - dev_set.field_arrays[field_name].is_input = self.field_arrays[field_name].is_input - dev_set.field_arrays[field_name].is_target = self.field_arrays[field_name].is_target - dev_set.field_arrays[field_name].padder = self.field_arrays[field_name].padder - dev_set.field_arrays[field_name].dtype = self.field_arrays[field_name].dtype - dev_set.field_arrays[field_name].pytype = self.field_arrays[field_name].pytype - dev_set.field_arrays[field_name].content_dim = self.field_arrays[field_name].content_dim + train_set.field_arrays[field_name].to(self.field_arrays[field_name]) + dev_set.field_arrays[field_name].to(self.field_arrays[field_name]) return train_set, dev_set diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index 72bb30b5..10fbbebe 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -383,6 +383,23 @@ class FieldArray(object): """ return len(self.content) + def to(self, other): + """ + 将other的属性复制给本fieldarray(必须通过fieldarray类型). 包含 is_input, is_target, padder, dtype, pytype, content_dim + ignore_type + + :param other: FieldArray + :return: + """ + assert isinstance(other, FieldArray), "Only support FieldArray type, not {}.".format(type(other)) + + self.is_input = other.is_input + self.is_target = other.is_target + self.padder = other.padder + self.dtype = other.dtype + self.pytype = other.pytype + self.content_dim = other.content_dim + self.ignore_type = other.ignore_type def is_iterable(content): try: diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 64555e12..3d3647c4 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -91,7 +91,6 @@ class MetricBase(object): Besides, before passing params into self.evaluate, this function will filter out params from output_dict and target_dict which are not used in self.evaluate. (but if **kwargs presented in self.evaluate, no filtering will be conducted.) - However, in some cases where type check is not necessary, ``_fast_param_map`` will be used. """ def __init__(self): @@ -146,21 +145,6 @@ class MetricBase(object): def get_metric(self, reset=True): raise NotImplemented - def _fast_param_map(self, pred_dict, target_dict): - """Only used as inner function. When the pred_dict, target is unequivocal. Don't need users to pass key_map. - such as pred_dict has one element, target_dict has one element - - :param pred_dict: - :param target_dict: - :return: dict, if dict is not {}, pass it to self.evaluate. Otherwise do mapping. - """ - fast_param = {} - if len(self.param_map) == 2 and len(pred_dict) == 1 and len(target_dict) == 1: - fast_param['pred'] = list(pred_dict.values())[0] - fast_param['target'] = list(target_dict.values())[0] - return fast_param - return fast_param - def __call__(self, pred_dict, target_dict): """ @@ -172,7 +156,6 @@ class MetricBase(object): Besides, before passing params into self.evaluate, this function will filter out params from output_dict and target_dict which are not used in self.evaluate. (but if **kwargs presented in self.evaluate, no filtering will be conducted.) - This function also support _fast_param_map. :param pred_dict: usually the output of forward or prediction function :param target_dict: usually features set as target.. :return: @@ -180,11 +163,6 @@ class MetricBase(object): if not callable(self.evaluate): raise TypeError(f"{self.__class__.__name__}.evaluate has to be callable, not {type(self.evaluate)}.") - fast_param = self._fast_param_map(pred_dict=pred_dict, target_dict=target_dict) - if fast_param: - self.evaluate(**fast_param) - return - if not self._checked: # 1. check consistence between signature and param_map func_spect = inspect.getfullargspec(self.evaluate) @@ -262,41 +240,6 @@ class AccuracyMetric(MetricBase): self.total = 0 self.acc_count = 0 - def _fast_param_map(self, pred_dict, target_dict): - """Only used as inner function. When the pred_dict, target is unequivocal. Don't need users to pass key_map. - such as pred_dict has one element, target_dict has one element - - :param pred_dict: - :param target_dict: - :return: dict, if dict is not None, pass it to self.evaluate. Otherwise do mapping. - """ - fast_param = {} - targets = list(target_dict.values()) - if len(targets) == 1 and isinstance(targets[0], torch.Tensor): - if len(pred_dict) == 1: - pred = list(pred_dict.values())[0] - fast_param['pred'] = pred - elif len(pred_dict) == 2: - pred1 = list(pred_dict.values())[0] - pred2 = list(pred_dict.values())[1] - if not (isinstance(pred1, torch.Tensor) and isinstance(pred2, torch.Tensor)): - return fast_param - if len(pred1.size()) < len(pred2.size()) and len(pred1.size()) == 1: - seq_lens = pred1 - pred = pred2 - elif len(pred1.size()) > len(pred2.size()) and len(pred2.size()) == 1: - seq_lens = pred2 - pred = pred1 - else: - return fast_param - fast_param['pred'] = pred - fast_param['seq_lens'] = seq_lens - else: - return fast_param - fast_param['target'] = targets[0] - # TODO need to make sure they all have same batch_size - return fast_param - def evaluate(self, pred, target, seq_lens=None): """ @@ -321,7 +264,7 @@ class AccuracyMetric(MetricBase): f"got {type(seq_lens)}.") if seq_lens is not None: - masks = seq_lens_to_masks(seq_lens=seq_lens, float=True) + masks = seq_lens_to_masks(seq_lens=seq_lens).long() else: masks = None @@ -334,14 +277,12 @@ class AccuracyMetric(MetricBase): f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") - pred = pred.float() - target = target.float() if masks is not None: - self.acc_count += torch.sum(torch.eq(pred, target).float() * masks.float()).item() - self.total += torch.sum(masks.float()).item() + self.acc_count += torch.sum(torch.eq(pred, target) * masks).item() + self.total += torch.sum(masks).item() else: - self.acc_count += torch.sum(torch.eq(pred, target).float()).item() + self.acc_count += torch.sum(torch.eq(pred, target)).item() self.total += np.prod(list(pred.size())) def get_metric(self, reset=True): @@ -350,7 +291,7 @@ class AccuracyMetric(MetricBase): :param bool reset: whether to recount next time. :return evaluate_result: {"acc": float} """ - evaluate_result = {'acc': round(self.acc_count / self.total, 6)} + evaluate_result = {'acc': round(float(self.acc_count) / (self.total + 1e-12), 6)} if reset: self.acc_count = 0 self.total = 0 @@ -441,8 +382,7 @@ def bio_tag_to_spans(tags, ignore_labels=None): prev_bio_tag = bio_tag return [(span[0], (span[1][0], span[1][1]+1)) for span in spans - if span[0] not in ignore_labels - ] + if span[0] not in ignore_labels] class SpanFPreRecMetric(MetricBase): diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index ca2ff93b..e678ea3d 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -34,7 +34,7 @@ class Trainer(object): def __init__(self, train_data, model, loss=None, metrics=None, n_epochs=3, batch_size=32, print_every=50, validate_every=-1, dev_data=None, save_path=None, optimizer=None, check_code_level=0, metric_key=None, sampler=None, prefetch=False, use_tqdm=True, - use_cuda=False, callbacks=None): + use_cuda=False, callbacks=None, update_every=1): """ :param DataSet train_data: the training data :param torch.nn.modules.module model: a PyTorch model @@ -62,6 +62,8 @@ class Trainer(object): :param bool use_tqdm: whether to use tqdm to show train progress. :param callbacks: List[Callback]. 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 通过callback机制实现。 + :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128会导致内存 + 不足,通过设置batch_size=32, update_every=4达到目的 """ super(Trainer, self).__init__() @@ -76,6 +78,10 @@ class Trainer(object): if metrics and (dev_data is None): raise ValueError("No dev_data for evaluations, pass dev_data or set metrics to None. ") + # check update every + assert update_every>=1, "update_every must be no less than 1." + self.update_every = int(update_every) + # check save_path if not (save_path is None or isinstance(save_path, str)): raise ValueError("save_path can only be None or `str`.") @@ -144,11 +150,9 @@ class Trainer(object): self.start_time = None # start timestamp self.callback_manager = CallbackManager(env={"trainer": self}, - attr={"n_epochs": self.n_epochs, "n_steps": self.step, - "batch_size": self.batch_size, "model": self.model, - "optimizer": self.optimizer}, callbacks=callbacks) + def train(self, load_best_model=True): """ @@ -241,7 +245,6 @@ class Trainer(object): avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) - self.callback_manager.set_property(pbar=pbar) for epoch in range(1, self.n_epochs+1): pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) # early stopping @@ -257,6 +260,7 @@ class Trainer(object): self.callback_manager.on_loss_begin(batch_y, prediction) loss = self._compute_loss(prediction, batch_y) avg_loss += loss.item() + loss = loss/self.update_every # Is loss NaN or inf? requires_grad = False self.callback_manager.on_backward_begin(loss, self.model) @@ -267,8 +271,9 @@ class Trainer(object): self.callback_manager.on_step_end(self.optimizer) if (self.step+1) % self.print_every == 0: + avg_loss = avg_loss / self.print_every if self.use_tqdm: - print_output = "loss:{0:<6.5f}".format(avg_loss / self.print_every) + print_output = "loss:{0:<6.5f}".format(avg_loss) pbar.update(self.print_every) else: end = time.time() @@ -286,8 +291,8 @@ class Trainer(object): eval_res = self._do_validation(epoch=epoch, step=self.step) eval_str = "Evaluation at Epoch {}/{}. Step:{}/{}. ".format(epoch, self.n_epochs, self.step, total_steps) + \ - self.tester._format_eval_results(eval_res) - pbar.write(eval_str) + self.tester._format_eval_results(eval_res) + pbar.write(eval_str + '\n') # ================= mini-batch end ==================== # @@ -301,6 +306,7 @@ class Trainer(object): self.callback_manager.on_valid_begin() res = self.tester.test() + is_better_eval = False if self._better_eval_result(res): if self.save_path is not None: self._save_model(self.model, @@ -310,8 +316,9 @@ class Trainer(object): self.best_dev_perf = res self.best_dev_epoch = epoch self.best_dev_step = step + is_better_eval = True # get validation results; adjust optimizer - self.callback_manager.on_valid_end(res, self.metric_key, self.optimizer) + self.callback_manager.on_valid_end(res, self.metric_key, self.optimizer, is_better_eval) return res def _mode(self, model, is_test=False): @@ -330,7 +337,8 @@ class Trainer(object): """Perform weight update on a model. """ - self.optimizer.step() + if (self.step+1)%self.update_every==0: + self.optimizer.step() def _data_forward(self, network, x): x = _build_args(network.forward, **x) @@ -346,7 +354,8 @@ class Trainer(object): For PyTorch, just do "loss.backward()" """ - self.model.zero_grad() + if self.step%self.update_every==0: + self.model.zero_grad() loss.backward() def _compute_loss(self, predict, truth): diff --git a/setup.py b/setup.py index a8b4834e..b7834d8d 100644 --- a/setup.py +++ b/setup.py @@ -13,12 +13,12 @@ with open('requirements.txt', encoding='utf-8') as f: setup( name='FastNLP', - version='0.1.1', + version='0.4.0', description='fastNLP: Deep Learning Toolkit for NLP, developed by Fudan FastNLP Team', long_description=readme, license=license, author='FudanNLP', - python_requires='>=3.5', + python_requires='>=3.6', packages=find_packages(), install_requires=reqs.strip().split('\n'), ) diff --git a/test/automl/test_enas.py b/test/automl/test_enas.py index d2d3af05..4fea1063 100644 --- a/test/automl/test_enas.py +++ b/test/automl/test_enas.py @@ -35,7 +35,7 @@ class TestENAS(unittest.TestCase): print(dataset[0]) # DataSet.drop(func)筛除数据 - dataset.drop(lambda x: x['seq_len'] <= 3) + dataset.drop(lambda x: x['seq_len'] <= 3, inplace=True) print(len(dataset)) # 设置DataSet中,哪些field要转为tensor diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 607f9a13..7f4a7184 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -125,7 +125,7 @@ class TestDataSetMethods(unittest.TestCase): def test_drop(self): ds = DataSet({"x": [[1, 2, 3, 4]] * 40, "y": [[5, 6], [7, 8, 9, 0]] * 20}) - ds.drop(lambda ins: len(ins["y"]) < 3) + ds.drop(lambda ins: len(ins["y"]) < 3, inplace=True) self.assertEqual(len(ds), 20) def test_contains(self): @@ -169,7 +169,7 @@ class TestDataSetMethods(unittest.TestCase): dataset = DataSet.read_csv('test/data_for_tests/tutorial_sample_dataset.csv', headers=('raw_sentence', 'label'), sep='\t') - dataset.drop(lambda x: len(x['raw_sentence'].split()) == 0) + dataset.drop(lambda x: len(x['raw_sentence'].split()) == 0, inplace=True) dataset.apply(split_sent, new_field_name='words', is_input=True) # print(dataset) diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 68c874fa..0056dff7 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -35,7 +35,7 @@ class TestTutorial(unittest.TestCase): print(dataset[0]) # DataSet.drop(func)筛除数据 - dataset.drop(lambda x: x['seq_len'] <= 3) + dataset.drop(lambda x: x['seq_len'] <= 3, inplace=True) print(len(dataset)) # 设置DataSet中,哪些field要转为tensor @@ -296,7 +296,7 @@ class TestTutorial(unittest.TestCase): # 筛选数据 origin_data_set_len = len(data_set) - data_set.drop(lambda x: len(x['premise']) <= 6) + data_set.drop(lambda x: len(x['premise']) <= 6, inplace=True) origin_data_set_len, len(data_set) # In[17]: From 29f81e79ad73db0d053df5f1417aa0dd6587b97a Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 14 Apr 2019 19:59:04 +0800 Subject: [PATCH 025/173] =?UTF-8?q?=E5=87=86=E5=A4=87=E5=8F=91=E5=B8=830.4?= =?UTF-8?q?.0=E7=89=88=E6=9C=AC=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 17 ++++++++--------- test/core/test_dataset.py | 4 ++-- test/core/test_metrics.py | 7 +++---- test/io/test_config_saver.py | 2 +- test/io/test_dataset_loader.py | 8 -------- test/modules/decoder/test_CRF.py | 2 +- test/test_tutorials.py | 6 +++--- 7 files changed, 18 insertions(+), 28 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 3d3647c4..5687cc85 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -243,12 +243,11 @@ class AccuracyMetric(MetricBase): def evaluate(self, pred, target, seq_lens=None): """ - :param pred: List of (torch.Tensor, or numpy.ndarray). Element's shape can be: - torch.Size([B,]), torch.Size([B, n_classes]), torch.Size([B, max_len]), torch.Size([B, max_len, n_classes]) - :param target: List of (torch.Tensor, or numpy.ndarray). Element's can be: - torch.Size([B,]), torch.Size([B,]), torch.Size([B, max_len]), torch.Size([B, max_len]) - :param seq_lens: List of (torch.Tensor, or numpy.ndarray). Element's can be: - None, None, torch.Size([B], torch.Size([B]). ignored if masks are provided. + :param pred: . Element's shape can be: torch.Size([B,]), torch.Size([B, n_classes]), torch.Size([B, max_len]), + torch.Size([B, max_len, n_classes]) + :param target: Element's can be: torch.Size([B,]), torch.Size([B,]), torch.Size([B, max_len]), + torch.Size([B, max_len]) + :param seq_lens: Element's can be: None, None, torch.Size([B], torch.Size([B]). ignored if masks are provided. """ # TODO 这里报错需要更改,因为pred是啥用户并不知道。需要告知用户真实的value @@ -264,7 +263,7 @@ class AccuracyMetric(MetricBase): f"got {type(seq_lens)}.") if seq_lens is not None: - masks = seq_lens_to_masks(seq_lens=seq_lens).long() + masks = seq_lens_to_masks(seq_lens=seq_lens) else: masks = None @@ -277,9 +276,9 @@ class AccuracyMetric(MetricBase): f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") - + target = target.to(pred) if masks is not None: - self.acc_count += torch.sum(torch.eq(pred, target) * masks).item() + self.acc_count += torch.sum(torch.eq(pred, target).masked_fill(masks, 0)).item() self.total += torch.sum(masks).item() else: self.acc_count += torch.sum(torch.eq(pred, target)).item() diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index fb54ee8a..5ed1a711 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -219,8 +219,8 @@ class TestDataSetMethods(unittest.TestCase): def test_add_null(self): # TODO test failed because 'fastNLP\core\fieldarray.py:143: RuntimeError' ds = DataSet() - ds.add_field('test', []) - ds.set_target('test') + with self.assertRaises(RuntimeError) as RE: + ds.add_field('test', []) class TestDataSetIter(unittest.TestCase): diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index 25138478..4fb2a04e 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -15,7 +15,7 @@ class TestAccuracyMetric(unittest.TestCase): target_dict = {'target': torch.zeros(4)} metric = AccuracyMetric() - metric(pred_dict=pred_dict, target_dict=target_dict, ) + metric(pred_dict=pred_dict, target_dict=target_dict) print(metric.get_metric()) def test_AccuracyMetric2(self): @@ -30,7 +30,7 @@ class TestAccuracyMetric(unittest.TestCase): except Exception as e: print(e) return - self.assertTrue(True, False), "No exception catches." + print("No exception catches.") def test_AccuracyMetric3(self): # (3) the second batch is corrupted size @@ -95,10 +95,9 @@ class TestAccuracyMetric(unittest.TestCase): self.assertAlmostEqual(res["acc"], float(ans), places=4) def test_AccuaryMetric8(self): - # (8) check map, does not match. use stop_fast_param to stop fast param map try: metric = AccuracyMetric(pred='predictions', target='targets') - pred_dict = {"prediction": torch.zeros(4, 3, 2), "stop_fast_param": 1} + pred_dict = {"prediction": torch.zeros(4, 3, 2)} target_dict = {'targets': torch.zeros(4, 3)} metric(pred_dict=pred_dict, target_dict=target_dict, ) self.assertDictEqual(metric.get_metric(), {'acc': 1}) diff --git a/test/io/test_config_saver.py b/test/io/test_config_saver.py index f29097c5..a71419e5 100644 --- a/test/io/test_config_saver.py +++ b/test/io/test_config_saver.py @@ -6,7 +6,7 @@ from fastNLP.io.config_io import ConfigSection, ConfigLoader, ConfigSaver class TestConfigSaver(unittest.TestCase): def test_case_1(self): - config_file_dir = "test/io/" + config_file_dir = "test/io" config_file_name = "config" config_file_path = os.path.join(config_file_dir, config_file_name) diff --git a/test/io/test_dataset_loader.py b/test/io/test_dataset_loader.py index 16e7d7ea..4dddc5d0 100644 --- a/test/io/test_dataset_loader.py +++ b/test/io/test_dataset_loader.py @@ -17,11 +17,3 @@ class TestDatasetLoader(unittest.TestCase): def test_PeopleDailyCorpusLoader(self): data_set = PeopleDailyCorpusLoader().load("test/data_for_tests/people_daily_raw.txt") - def test_ConllCWSReader(self): - dataset = ConllCWSReader().load("test/data_for_tests/conll_example.txt") - - def test_ZhConllPOSReader(self): - dataset = ZhConllPOSReader().load("test/data_for_tests/zh_sample.conllx") - - def test_ConllxDataLoader(self): - dataset = ConllxDataLoader().load("test/data_for_tests/zh_sample.conllx") diff --git a/test/modules/decoder/test_CRF.py b/test/modules/decoder/test_CRF.py index a176348f..5dc60640 100644 --- a/test/modules/decoder/test_CRF.py +++ b/test/modules/decoder/test_CRF.py @@ -118,7 +118,7 @@ class TestCRF(unittest.TestCase): feats = nn.Parameter(torch.randn(num_samples, max_len, num_tags)) crf = ConditionalRandomField(num_tags, include_start_end_trans) optimizer = optim.SGD([param for param in crf.parameters() if param.requires_grad] + [feats], lr=0.1) - for _ in range(10000): + for _ in range(10): loss = crf(feats, tags, masks).mean() optimizer.zero_grad() loss.backward() diff --git a/test/test_tutorials.py b/test/test_tutorials.py index c9ffa646..bc0b5d2b 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -152,7 +152,7 @@ class TestTutorial(unittest.TestCase): train_data=train_data, dev_data=dev_data, loss=CrossEntropyLoss(), - metrics=AccuracyMetric() + metrics=AccuracyMetric(target='label_seq') ) trainer.train() print('Train finished!') @@ -407,7 +407,7 @@ class TestTutorial(unittest.TestCase): train_data=train_data, model=model, loss=CrossEntropyLoss(pred='pred', target='label'), - metrics=AccuracyMetric(), + metrics=AccuracyMetric(target='label'), n_epochs=3, batch_size=16, print_every=-1, @@ -424,7 +424,7 @@ class TestTutorial(unittest.TestCase): tester = Tester( data=test_data, model=model, - metrics=AccuracyMetric(), + metrics=AccuracyMetric(target='label'), batch_size=args["batch_size"], ) tester.test() From 6f010d488db843816a02a81c71c1e290c3077a1a Mon Sep 17 00:00:00 2001 From: Yunfan Shao Date: Sun, 14 Apr 2019 21:11:16 +0800 Subject: [PATCH 026/173] update readme --- reproduction/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproduction/README.md b/reproduction/README.md index 1c93c6bc..8d14d36d 100644 --- a/reproduction/README.md +++ b/reproduction/README.md @@ -8,7 +8,7 @@ ## Star-Transformer [reference](https://arxiv.org/abs/1902.09113) -### Performance +### Performance (still in progress) |任务| 数据集 | SOTA | 模型表现 | |------|------| ------| ------| |Pos Tagging|CTB 9.0|-|ACC 92.31| From 16fdf20d2630df580e4f5b7af244c66b048f70f3 Mon Sep 17 00:00:00 2001 From: xuyige Date: Sun, 14 Apr 2019 22:20:39 +0800 Subject: [PATCH 027/173] support parallel loss --- fastNLP/core/losses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 9b8b8d8f..b52244e5 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -251,7 +251,8 @@ class LossInForward(LossBase): if not (isinstance(loss, torch.Tensor) and len(loss.size()) == 0): if not isinstance(loss, torch.Tensor): raise TypeError(f"Loss excepted to be a torch.Tensor, got {type(loss)}") - raise RuntimeError(f"The size of loss excepts to be torch.Size([]), got {loss.size()}") + loss = torch.sum(loss) / (loss.view(-1)).size(0) + # raise RuntimeError(f"The size of loss excepts to be torch.Size([]), got {loss.size()}") return loss From fdfaf2d6b620ecd167a48fe7b8b22e1ad1921f6b Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 14 Apr 2019 22:38:15 +0800 Subject: [PATCH 028/173] =?UTF-8?q?=E9=98=B2=E6=AD=A2=E5=A4=9A=E5=8D=A1?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5=E5=AF=BC=E8=87=B4=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E8=AE=A1=E7=AE=97loss=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 2a8d85da..b45dd148 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -265,7 +265,7 @@ class Trainer(object): # edit prediction self.callback_manager.on_loss_begin(batch_y, prediction) - loss = self._compute_loss(prediction, batch_y) + loss = self._compute_loss(prediction, batch_y).mean() avg_loss += loss.item() loss = loss/self.update_every From 76f9bbf5f1fbe50dd7a541b6f9a67e3e808c6989 Mon Sep 17 00:00:00 2001 From: xuyige Date: Sun, 14 Apr 2019 23:30:23 +0800 Subject: [PATCH 029/173] update advance_tutorial jupyter notebook --- .../advance_tutorial.ipynb | 171 ++++++++++-------- 1 file changed, 94 insertions(+), 77 deletions(-) diff --git a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb b/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb index a787eeaf..64eb3462 100644 --- a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb +++ b/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb @@ -20,16 +20,7 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/remote-home/ygxu/anaconda3/envs/no-fastnlp/lib/python3.7/site-packages/tqdm/autonotebook/__init__.py:14: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", - " \" (e.g. in jupyter console)\", TqdmExperimentalWarning)\n" - ] - } - ], + "outputs": [], "source": [ "# 声明部件\n", "import torch\n", @@ -179,11 +170,11 @@ { "data": { "text/plain": [ - "DataSet({'image': tensor([[ 2.1747, -1.0147, -1.3853, 0.0216, -0.4957],\n", - " [ 0.8138, -0.2933, -0.1217, -0.6027, 0.3932],\n", - " [ 0.6750, -1.1136, -1.3371, -0.0185, -0.3206],\n", - " [-0.5076, -0.3822, 0.1719, -0.6447, -0.5702],\n", - " [ 0.3804, 0.0889, 0.8027, -0.7121, -0.7320]]) type=torch.Tensor,\n", + "DataSet({'image': tensor([[ 4.7106e-01, -1.2246e+00, 3.1234e-01, -1.6781e+00, -8.7967e-01],\n", + " [ 1.1454e+00, 1.2236e-01, 3.0258e-01, -1.5454e+00, 8.9201e-01],\n", + " [-5.7143e-03, 3.9488e-01, 2.0287e-01, -1.5726e+00, 9.3171e-01],\n", + " [ 6.8914e-01, -2.6302e-01, -8.2694e-01, 9.5942e-01, -5.2589e-01],\n", + " [-5.7798e-03, -9.1621e-03, 1.0077e-03, 9.1716e-02, 1.0565e+00]]) type=torch.Tensor,\n", "'label': 0 type=int})" ] }, @@ -644,20 +635,20 @@ { "data": { "text/plain": [ - "({'premise': [2, 145, 146, 80, 147, 26, 148, 2, 104, 149, 150, 2, 151, 5, 55, 152, 105, 3] type=list,\n", - " 'hypothesis': [22, 80, 8, 1, 1, 20, 1, 3] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 2 type=int},\n", - " {'premise': [11, 5, 18, 5, 24, 6, 2, 10, 59, 52, 14, 9, 2, 53, 29, 60, 54, 45, 6, 46, 5, 7, 61, 3] type=list,\n", - " 'hypothesis': [22, 11, 1, 45, 3] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1] type=list,\n", + "({'premise': [2, 10, 9, 2, 15, 115, 6, 11, 5, 132, 17, 2, 76, 9, 77, 55, 3] type=list,\n", + " 'hypothesis': [1, 2, 56, 17, 1, 4, 13, 49, 123, 12, 6, 11, 3] type=list,\n", + " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'label': 0 type=int},\n", + " {'premise': [50, 124, 10, 7, 68, 91, 92, 38, 2, 55, 3] type=list,\n", + " 'hypothesis': [21, 10, 5, 2, 55, 7, 99, 64, 48, 1, 22, 1, 3] type=list,\n", + " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", " 'label': 1 type=int},\n", - " {'premise': [2, 11, 8, 14, 16, 7, 15, 50, 2, 66, 4, 76, 2, 10, 8, 98, 9, 58, 67, 3] type=list,\n", - " 'hypothesis': [22, 27, 50, 3] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1] type=list,\n", + " {'premise': [13, 24, 4, 14, 29, 5, 25, 4, 8, 39, 9, 14, 34, 4, 40, 41, 4, 16, 12, 2, 11, 4, 30, 28, 2, 42, 8, 2, 43, 44, 17, 2, 45, 35, 26, 31, 27, 5, 6, 32, 3] type=list,\n", + " 'hypothesis': [37, 49, 123, 30, 28, 2, 55, 12, 2, 11, 3] type=list,\n", + " 'premise_len': [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, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", " 'label': 0 type=int})" ] }, @@ -718,15 +709,15 @@ { "data": { "text/plain": [ - "({'premise': [1037, 2210, 2223, 2136, 5363, 2000, 4608, 1037, 5479, 8058, 2046, 1037, 2918, 1999, 2019, 5027, 2208, 1012] type=list,\n", - " 'hypothesis': [100, 2136, 2003, 2652, 3598, 2006, 100, 1012] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 2 type=int},\n", - " {'premise': [2450, 1999, 2317, 1999, 100, 1998, 1037, 2158, 3621, 2369, 3788, 2007, 1037, 3696, 2005, 2198, 100, 10733, 1998, 100, 1999, 1996, 4281, 1012] type=list,\n", - " 'hypothesis': [100, 2450, 13063, 10733, 1012] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1] type=list,\n", + "({'premise': [1037, 2158, 1998, 1037, 2450, 2892, 1996, 2395, 1999, 2392, 1997, 1037, 10733, 1998, 100, 4825, 1012] type=list,\n", + " 'hypothesis': [100, 1037, 3232, 1997, 7884, 1010, 2048, 2111, 3328, 2408, 1996, 2395, 1012] type=list,\n", + " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'label': 0 type=int},\n", + " {'premise': [2019, 3080, 2158, 2003, 5948, 4589, 10869, 2012, 1037, 4825, 1012] type=list,\n", + " 'hypothesis': [100, 2158, 1999, 1037, 4825, 2003, 3403, 2005, 2010, 7954, 2000, 7180, 1012] type=list,\n", + " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", " 'label': 1 type=int})" ] }, @@ -769,7 +760,7 @@ " 'num_classes': 3,\n", " 'gpu': True,\n", " 'batch_size': 32,\n", - " 'vocab_size': 165}" + " 'vocab_size': 156}" ] }, "execution_count": 26, @@ -797,7 +788,7 @@ "ESIM(\n", " (drop): Dropout(p=0.3)\n", " (embedding): Embedding(\n", - " (embed): Embedding(165, 300, padding_idx=0)\n", + " (embed): Embedding(156, 300, padding_idx=0)\n", " (dropout): Dropout(p=0.3)\n", " )\n", " (embedding_layer): Linear(\n", @@ -821,7 +812,6 @@ " )\n", " (output): Linear(in_features=300, out_features=3, bias=True)\n", " (dropout): Dropout(p=0.3)\n", - " (hidden_active): Tanh()\n", " )\n", ")" ] @@ -848,7 +838,7 @@ "text/plain": [ "CNNText(\n", " (embed): Embedding(\n", - " (embed): Embedding(165, 50, padding_idx=0)\n", + " (embed): Embedding(156, 50, padding_idx=0)\n", " (dropout): Dropout(p=0.0)\n", " )\n", " (conv_pool): ConvMaxpool(\n", @@ -1019,43 +1009,49 @@ "name": "stdout", "output_type": "stream", "text": [ - "training epochs started 2019-01-09 00-08-17\n", - "[tester] \n", - "AccuracyMetric: acc=0.206897\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/remote-home/ygxu/anaconda3/envs/no-fastnlp/lib/python3.7/site-packages/torch/nn/functional.py:1320: UserWarning: nn.functional.tanh is deprecated. Use torch.tanh instead.\n", - " warnings.warn(\"nn.functional.tanh is deprecated. Use torch.tanh instead.\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[tester] \n", - "AccuracyMetric: acc=0.206897\n", - "[tester] \n", - "AccuracyMetric: acc=0.206897\n", - "[tester] \n", - "AccuracyMetric: acc=0.206897\n", - "[tester] \n", - "AccuracyMetric: acc=0.206897\n", + "training epochs started 2019-04-14-23-22-28\n", + "[epoch: 1 step: 1] train loss: 1.51372 time: 0:00:00\n", + "[epoch: 1 step: 2] train loss: 1.26874 time: 0:00:00\n", + "[epoch: 1 step: 3] train loss: 1.49786 time: 0:00:00\n", + "[epoch: 1 step: 4] train loss: 1.37505 time: 0:00:00\n", + "Evaluation at Epoch 1/5. Step:4/20. AccuracyMetric: acc=0.344828\n", + "\n", + "[epoch: 2 step: 5] train loss: 1.21877 time: 0:00:00\n", + "[epoch: 2 step: 6] train loss: 1.14183 time: 0:00:00\n", + "[epoch: 2 step: 7] train loss: 1.15934 time: 0:00:00\n", + "[epoch: 2 step: 8] train loss: 1.55148 time: 0:00:00\n", + "Evaluation at Epoch 2/5. Step:8/20. AccuracyMetric: acc=0.344828\n", "\n", - "In Epoch:1/Step:4, got best dev performance:AccuracyMetric: acc=0.206897\n", + "[epoch: 3 step: 9] train loss: 1.1457 time: 0:00:00\n", + "[epoch: 3 step: 10] train loss: 1.0547 time: 0:00:00\n", + "[epoch: 3 step: 11] train loss: 1.40139 time: 0:00:00\n", + "[epoch: 3 step: 12] train loss: 0.551445 time: 0:00:00\n", + "Evaluation at Epoch 3/5. Step:12/20. AccuracyMetric: acc=0.275862\n", + "\n", + "[epoch: 4 step: 13] train loss: 1.07965 time: 0:00:00\n", + "[epoch: 4 step: 14] train loss: 1.04118 time: 0:00:00\n", + "[epoch: 4 step: 15] train loss: 1.11719 time: 0:00:00\n", + "[epoch: 4 step: 16] train loss: 1.09861 time: 0:00:00\n", + "Evaluation at Epoch 4/5. Step:16/20. AccuracyMetric: acc=0.275862\n", + "\n", + "[epoch: 5 step: 17] train loss: 1.10795 time: 0:00:00\n", + "[epoch: 5 step: 18] train loss: 1.26715 time: 0:00:00\n", + "[epoch: 5 step: 19] train loss: 1.19875 time: 0:00:00\n", + "[epoch: 5 step: 20] train loss: 1.09862 time: 0:00:00\n", + "Evaluation at Epoch 5/5. Step:20/20. AccuracyMetric: acc=0.37931\n", + "\n", + "\n", + "In Epoch:5/Step:20, got best dev performance:AccuracyMetric: acc=0.37931\n", "Reloaded the best model.\n" ] }, { "data": { "text/plain": [ - "{'best_eval': {'AccuracyMetric': {'acc': 0.206897}},\n", - " 'best_epoch': 1,\n", - " 'best_step': 4,\n", - " 'seconds': 0.79}" + "{'best_eval': {'AccuracyMetric': {'acc': 0.37931}},\n", + " 'best_epoch': 5,\n", + " 'best_step': 20,\n", + " 'seconds': 0.5}" ] }, "execution_count": 29, @@ -1070,8 +1066,8 @@ "trainer = Trainer(\n", " train_data=train_data,\n", " model=model,\n", - " loss=CrossEntropyLoss(pred='pred', target='label'),\n", - " metrics=AccuracyMetric(),\n", + " loss=CrossEntropyLoss(pred='pred', target='label'), # 模型预测值通过'pred'来取得,目标值(ground truth)由'label'取得\n", + " metrics=AccuracyMetric(target='label'), # 目标值(ground truth)由'label'取得\n", " n_epochs=5,\n", " batch_size=16,\n", " print_every=-1,\n", @@ -1113,13 +1109,13 @@ "output_type": "stream", "text": [ "[tester] \n", - "AccuracyMetric: acc=0.263158\n" + "AccuracyMetric: acc=0.368421\n" ] }, { "data": { "text/plain": [ - "{'AccuracyMetric': {'acc': 0.263158}}" + "{'AccuracyMetric': {'acc': 0.368421}}" ] }, "execution_count": 30, @@ -1131,12 +1127,33 @@ "tester = Tester(\n", " data=test_data,\n", " model=model,\n", - " metrics=AccuracyMetric(),\n", + " metrics=AccuracyMetric(target='label'),\n", " batch_size=args[\"batch_size\"],\n", ")\n", "tester.test()" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -1161,7 +1178,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.7.0" } }, "nbformat": 4, From b69f8985c8e74ab063645daa5524f04759a2d2b0 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 17 Apr 2019 20:24:09 +0800 Subject: [PATCH 030/173] =?UTF-8?q?1.=20=E5=9C=A8embedding=5Floader?= =?UTF-8?q?=E4=B8=AD=E5=A2=9E=E5=8A=A0=E6=96=B0=E7=9A=84=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E5=87=BD=E6=95=B0load=5Fwith=5Fvocab(),=20load=5Fwithout=5Fvoc?= =?UTF-8?q?ab,=20=E6=AF=94=E4=B9=8B=E5=89=8D=E7=9A=84=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E6=94=B9=E5=8F=98=E4=B8=BB=E8=A6=81=E5=9C=A8(1)=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=9C=80=E8=A6=81=E4=BC=A0=E5=85=A5embed=5Fdim(2)?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=A4=E6=96=AD=E5=BD=93=E5=89=8D=E6=98=AF?= =?UTF-8?q?word2vec=E8=BF=98=E6=98=AFglove.=202.=20vocabulary=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0from=5Fdataset(),=20index=5Fdataset()=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E3=80=82=E9=81=BF=E5=85=8D=E9=9C=80=E8=A6=81=E5=A4=9A?= =?UTF-8?q?=E8=A1=8C=E5=86=99index=20dataset=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E3=80=82=203.=20=E5=9C=A8utils=E4=B8=AD=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=B8=80=E4=B8=AAcache=5Fresult()=E4=BF=AE=E9=A5=B0=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E4=BA=8Ecache=E5=87=BD=E6=95=B0=E7=9A=84?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=80=BC=E3=80=82=204.=20callback=E4=B8=AD?= =?UTF-8?q?=E6=96=B0=E5=A2=9Eupdate=5Fevery=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/__init__.py | 4 +- fastNLP/core/callback.py | 4 ++ fastNLP/core/dataset.py | 8 +-- fastNLP/core/utils.py | 58 ++++++++++++++++++ fastNLP/core/vocabulary.py | 64 ++++++++++++++++++- fastNLP/io/__init__.py | 1 + fastNLP/io/embed_loader.py | 96 +++++++++++++++++++++++++++++ fastNLP/io/logger.py | 35 ----------- test/core/test_utils.py | 115 +++++++++++++++++++++++++++++++++++ test/core/test_vocabulary.py | 38 ++++++++++++ test/io/test_embed_loader.py | 32 ++++++++++ 11 files changed, 410 insertions(+), 45 deletions(-) delete mode 100644 fastNLP/io/logger.py create mode 100644 test/core/test_utils.py diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 0bb6a2dd..dbe86953 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -1,5 +1,5 @@ from .batch import Batch -# from .dataset import DataSet +from .dataset import DataSet from .fieldarray import FieldArray from .instance import Instance from .losses import LossFunc, CrossEntropyLoss, L1Loss, BCELoss, NLLLoss, LossInForward @@ -9,5 +9,5 @@ from .sampler import SequentialSampler, BucketSampler, RandomSampler, BaseSample from .tester import Tester from .trainer import Trainer from .vocabulary import Vocabulary -from ..io.dataset_loader import DataSet from .callback import Callback +from .utils import cache_results \ No newline at end of file diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 01f6ce68..2ee5b3a6 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -61,6 +61,10 @@ class Callback(object): """If use_tqdm, return trainer's tqdm print bar, else return None.""" return self._trainer.pbar + @property + def update_every(self): + """The model in trainer will update parameters every `update_every` batches.""" + return self._trainer.update_every def on_train_begin(self): # before the main training loop pass diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 068afb38..7b0e3b9a 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -6,7 +6,6 @@ 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 fastNLP.io.base_loader import DataLoaderRegister class DataSet(object): @@ -105,11 +104,6 @@ class DataSet(object): raise AttributeError if isinstance(item, str) and item in self.field_arrays: return self.field_arrays[item] - try: - reader = DataLoaderRegister.get_reader(item) - return reader - except AttributeError: - raise def __setstate__(self, state): self.__dict__ = state @@ -369,7 +363,7 @@ class DataSet(object): :return dataset: the read data set """ - with open(csv_path, "r") as f: + with open(csv_path, "r", encoding='utf-8') as f: start_idx = 0 if headers is None: headers = f.readline().rstrip('\r\n') diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 695efdfc..d9141412 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -11,6 +11,64 @@ import torch CheckRes = namedtuple('CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', 'varargs']) +def _prepare_cache_filepath(filepath): + """ + 检查filepath是否可以作为合理的cache文件. 如果可以的话,会自动创造路径 + :param filepath: str. + :return: None, if not, this function will raise error + """ + _cache_filepath = os.path.abspath(filepath) + if os.path.isdir(_cache_filepath): + raise RuntimeError("The cache_file_path must be a file, not a directory.") + cache_dir = os.path.dirname(_cache_filepath) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + +def cache_results(cache_filepath, refresh=False, verbose=1): + def wrapper_(func): + signature = inspect.signature(func) + for key, _ in signature.parameters.items(): + if key in ('cache_filepath', 'refresh', 'verbose'): + raise RuntimeError("The function decorated by cache_results cannot have keyword `{}`.".format(key)) + def wrapper(*args, **kwargs): + if 'cache_filepath' in kwargs: + _cache_filepath = kwargs.pop('cache_filepath') + assert isinstance(_cache_filepath, str), "cache_filepath can only be str." + else: + _cache_filepath = cache_filepath + if 'refresh' in kwargs: + _refresh = kwargs.pop('refresh') + assert isinstance(_refresh, bool), "refresh can only be bool." + else: + _refresh = refresh + if 'verbose' in kwargs: + _verbose = kwargs.pop('verbose') + assert isinstance(_verbose, int), "verbose can only be integer." + refresh_flag = True + + if _cache_filepath is not None and _refresh is False: + # load data + if os.path.exists(_cache_filepath): + with open(_cache_filepath, 'rb') as f: + results = _pickle.load(f) + if verbose==1: + print("Read cache from {}.".format(_cache_filepath)) + refresh_flag = False + + if refresh_flag: + results = func(*args, **kwargs) + if _cache_filepath is not None: + if results is None: + raise RuntimeError("The return value is None. Delete the decorator.") + _prepare_cache_filepath(_cache_filepath) + with open(_cache_filepath, 'wb') as f: + _pickle.dump(results, f) + print("Save cache to {}.".format(_cache_filepath)) + + return results + return wrapper + return wrapper_ def save_pickle(obj, pickle_path, file_name): """Save an object into a pickle file. diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index a1c8e678..a73ce2c7 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -1,5 +1,5 @@ from collections import Counter - +from fastNLP.core.dataset import DataSet def check_build_vocab(func): """A decorator to make sure the indexing is built before used. @@ -151,6 +151,68 @@ class Vocabulary(object): else: raise ValueError("word {} not in vocabulary".format(w)) + @check_build_vocab + def index_dataset(self, *datasets, field_name, new_field_name=None): + """ + example: + # remember to use `field_name` + vocab.index_dataset(tr_data, dev_data, te_data, field_name='words') + + :param datasets: fastNLP Dataset type. you can pass multiple datasets + :param field_name: str, what field to index. Only support 0,1,2 dimension. + :param new_field_name: str. What the indexed field should be named, default is to overwrite field_name + :return: + """ + def index_instance(ins): + """ + 有几种情况, str, 1d-list, 2d-list + :param ins: + :return: + """ + field = ins[field_name] + if isinstance(field, str): + return self.to_index(field) + elif isinstance(field, list): + if not isinstance(field[0], list): + return [self.to_index(w) for w in field] + else: + if isinstance(field[0][0], list): + raise RuntimeError("Only support field with 2 dimensions.") + return[[self.to_index(c) for c in w] for w in field] + + if new_field_name is None: + new_field_name = field_name + for dataset in datasets: + if isinstance(dataset, DataSet): + dataset.apply(index_instance, new_field_name=new_field_name) + else: + raise RuntimeError("Only DataSet type is allowed.") + + def from_dataset(self, *datasets, field_name): + """ + Construct vocab from dataset. + + :param datasets: DataSet. + :param field_name: str, what field is used to construct dataset. + :return: + """ + def construct_vocab(ins): + field = ins[field_name] + if isinstance(field, str): + self.add_word(field) + elif isinstance(field, list): + if not isinstance(field[0], list): + self.add_word_lst(field) + else: + if isinstance(field[0][0], list): + raise RuntimeError("Only support field with 2 dimensions.") + [self.add_word_lst(w) for w in field] + for dataset in datasets: + if isinstance(dataset, DataSet): + dataset.apply(construct_vocab) + else: + raise RuntimeError("Only DataSet type is allowed.") + def to_index(self, w): """ Turn a word to an index. If w is not in Vocabulary, return the unknown label. diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index e69de29b..a3b18aa5 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -0,0 +1 @@ +from .embed_loader import EmbedLoader diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 1615fb7f..08a55aa6 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,3 +1,5 @@ +import os + import numpy as np import torch @@ -124,3 +126,97 @@ class EmbedLoader(BaseLoader): size=(len(vocab) - np.sum(hit_flags), emb_dim)) embedding_matrix[np.where(1 - hit_flags)] = sampled_vectors return embedding_matrix + + @staticmethod + def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True): + """ + load pretraining embedding in {embed_file} based on words in vocab. Words in vocab but not in the pretraining + embedding are initialized from a normal distribution which has the mean and std of the found words vectors. + The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). + + :param embed_filepath: str, where to read pretrain embedding + :param vocab: Vocabulary. + :param dtype: the dtype of the embedding matrix + :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. + :return: np.ndarray() will have the same [len(vocab), dimension], dimension is determined by the pretrain + embedding + """ + assert isinstance(vocab, Vocabulary), "Only fastNLP.Vocabulary is supported." + if not os.path.exists(embed_filepath): + raise FileNotFoundError("`{}` does not exist.".format(embed_filepath)) + with open(embed_filepath, 'r', encoding='utf-8') as f: + hit_flags = np.zeros(len(vocab), dtype=bool) + line = f.readline().strip() + parts = line.split() + if len(parts)==2: + dim = int(parts[1]) + else: + dim = len(parts)-1 + f.seek(0) + matrix = np.random.randn(len(vocab), dim).astype(dtype) + for line in f: + parts = line.strip().split() + if parts[0] in vocab: + index = vocab.to_index(parts[0]) + matrix[index] = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) + hit_flags[index] = True + total_hits = sum(hit_flags) + print("Found {} out of {} words in the pre-training embedding.".format(total_hits, len(vocab))) + found_vectors = matrix[hit_flags] + if len(found_vectors)!=0: + mean = np.mean(found_vectors, axis=1, keepdims=True) + std = np.std(found_vectors, axis=1, keepdims=True) + unfound_vec_num = len(vocab) - total_hits + r_vecs = np.random.randn(unfound_vec_num, dim).astype(dtype)*std + mean + matrix[hit_flags==False] = r_vecs + + if normalize: + matrix /= np.linalg.norm(matrix, axis=1, keepdims=True) + + return matrix + + @staticmethod + def load_without_vocab(embed_filepath, dtype=np.float32, padding='', unknown='', normalize=True): + """ + load pretraining embedding in {embed_file}. And construct a Vocabulary based on the pretraining embedding. + The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). + + :param embed_filepath: str, where to read pretrain embedding + :param dtype: the dtype of the embedding matrix + :param padding: the padding tag for vocabulary. + :param unknown: the unknown tag for vocabulary. + :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. + :return: np.ndarray() is determined by the pretraining embeddings + Vocabulary: contain all pretraining words and two special tag[, ] + + """ + vocab = Vocabulary(padding=padding, unknown=unknown) + vec_dict = {} + + with open(embed_filepath, 'r', encoding='utf-8') as f: + line = f.readline() + start = 1 + dim = -1 + if len(line.strip().split())!=2: + f.seek(0) + start = 0 + for idx, line in enumerate(f, start=start): + parts = line.strip().split() + word = parts[0] + if dim==-1: + dim = len(parts)-1 + vec = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) + vec_dict[word] = vec + vocab.add_word(word) + if dim==-1: + raise RuntimeError("{} is an empty file.".format(embed_filepath)) + matrix = np.random.randn(len(vocab), dim).astype(dtype) + + for key, vec in vec_dict.items(): + index = vocab.to_index(key) + matrix[index] = vec + + if normalize: + matrix /= np.linalg.norm(matrix, axis=1, keepdims=True) + + return matrix, vocab diff --git a/fastNLP/io/logger.py b/fastNLP/io/logger.py deleted file mode 100644 index 9e9730db..00000000 --- a/fastNLP/io/logger.py +++ /dev/null @@ -1,35 +0,0 @@ -import logging -import os - - -def create_logger(logger_name, log_path, log_format=None, log_level=logging.INFO): - """Create a logger. - - :param str logger_name: - :param str log_path: - :param log_format: - :param log_level: - :return: logger - - To use a logger:: - - logger.debug("this is a debug message") - logger.info("this is a info message") - logger.warning("this is a warning message") - logger.error("this is an error message") - """ - logger = logging.getLogger(logger_name) - logger.setLevel(log_level) - if log_path is None: - handler = logging.StreamHandler() - else: - os.stat(os.path.dirname(os.path.abspath(log_path))) - handler = logging.FileHandler(log_path) - handler.setLevel(log_level) - if log_format is None: - log_format = "[%(asctime)s %(name)-13s %(levelname)s %(process)d %(thread)d " \ - "%(filename)s:%(lineno)-5d] %(message)s" - formatter = logging.Formatter(log_format) - handler.setFormatter(formatter) - logger.addHandler(handler) - return logger diff --git a/test/core/test_utils.py b/test/core/test_utils.py new file mode 100644 index 00000000..5c325127 --- /dev/null +++ b/test/core/test_utils.py @@ -0,0 +1,115 @@ + +import unittest +import _pickle +from fastNLP import cache_results +from fastNLP.io.embed_loader import EmbedLoader +from fastNLP import DataSet +from fastNLP import Instance +import time +import os + +@cache_results('test/demo1.pkl') +def process_data_1(embed_file, cws_train): + embed, vocab = EmbedLoader.load_without_vocab(embed_file) + time.sleep(1) # 测试是否通过读取cache获得结果 + with open(cws_train, 'r', encoding='utf-8') as f: + d = DataSet() + for line in f: + line = line.strip() + if len(line)>0: + d.append(Instance(raw=line)) + return embed, vocab, d + + +class TestCache(unittest.TestCase): + def test_cache_save(self): + try: + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train') + end_time = time.time() + pre_time = end_time - start_time + with open('test/demo1.pkl', 'rb') as f: + _embed, _vocab, _d = _pickle.load(f) + self.assertEqual(embed.shape, _embed.shape) + for i in range(embed.shape[0]): + self.assertListEqual(embed[i].tolist(), _embed[i].tolist()) + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train') + end_time = time.time() + read_time = end_time - start_time + print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) + self.assertGreater(pre_time-0.5, read_time) + finally: + os.remove('test/demo1.pkl') + + def test_cache_save_overwrite_path(self): + try: + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', + cache_filepath='test/demo_overwrite.pkl') + end_time = time.time() + pre_time = end_time - start_time + with open('test/demo_overwrite.pkl', 'rb') as f: + _embed, _vocab, _d = _pickle.load(f) + self.assertEqual(embed.shape, _embed.shape) + for i in range(embed.shape[0]): + self.assertListEqual(embed[i].tolist(), _embed[i].tolist()) + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', + cache_filepath='test/demo_overwrite.pkl') + end_time = time.time() + read_time = end_time - start_time + print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) + self.assertGreater(pre_time-0.5, read_time) + finally: + os.remove('test/demo_overwrite.pkl') + + def test_cache_refresh(self): + try: + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', + refresh=True) + end_time = time.time() + pre_time = end_time - start_time + with open('test/demo1.pkl', 'rb') as f: + _embed, _vocab, _d = _pickle.load(f) + self.assertEqual(embed.shape, _embed.shape) + for i in range(embed.shape[0]): + self.assertListEqual(embed[i].tolist(), _embed[i].tolist()) + start_time = time.time() + embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', + refresh=True) + end_time = time.time() + read_time = end_time - start_time + print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) + self.assertGreater(0.1, pre_time-read_time) + finally: + os.remove('test/demo1.pkl') + + def test_duplicate_keyword(self): + with self.assertRaises(RuntimeError): + @cache_results(None) + def func_verbose(a, verbose): + pass + func_verbose(0, 1) + with self.assertRaises(RuntimeError): + @cache_results(None) + def func_cache(a, cache_filepath): + pass + func_cache(1, 2) + with self.assertRaises(RuntimeError): + @cache_results(None) + def func_refresh(a, refresh): + pass + func_refresh(1, 2) + + def test_create_cache_dir(self): + @cache_results('test/demo1/demo.pkl') + def cache(): + return 1, 2 + try: + results = cache() + print(results) + finally: + os.remove('test/demo1/demo.pkl') + os.rmdir('test/demo1') \ No newline at end of file diff --git a/test/core/test_vocabulary.py b/test/core/test_vocabulary.py index 2f9cd3b1..0f13b935 100644 --- a/test/core/test_vocabulary.py +++ b/test/core/test_vocabulary.py @@ -2,6 +2,8 @@ import unittest from collections import Counter from fastNLP.core.vocabulary import Vocabulary +from fastNLP.core.dataset import DataSet +from fastNLP.core.instance import Instance text = ["FastNLP", "works", "well", "in", "most", "cases", "and", "scales", "well", "in", "works", "well", "in", "most", "cases", "scales", "well"] @@ -31,6 +33,42 @@ class TestAdd(unittest.TestCase): vocab.update(text) self.assertEqual(vocab.word_count, counter) + def test_from_dataset(self): + start_char = 65 + num_samples = 10 + + # 0 dim + dataset = DataSet() + for i in range(num_samples): + ins = Instance(char=chr(start_char+i)) + dataset.append(ins) + vocab = Vocabulary() + vocab.from_dataset(dataset, field_name='char') + for i in range(num_samples): + self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + vocab.index_dataset(dataset, field_name='char') + + # 1 dim + dataset = DataSet() + for i in range(num_samples): + ins = Instance(char=[chr(start_char+i)]*6) + dataset.append(ins) + vocab = Vocabulary() + vocab.from_dataset(dataset, field_name='char') + for i in range(num_samples): + self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + vocab.index_dataset(dataset, field_name='char') + + # 2 dim + dataset = DataSet() + for i in range(num_samples): + ins = Instance(char=[[chr(start_char+i) for _ in range(6)] for _ in range(6)]) + dataset.append(ins) + vocab = Vocabulary() + vocab.from_dataset(dataset, field_name='char') + for i in range(num_samples): + self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + vocab.index_dataset(dataset, field_name='char') class TestIndexing(unittest.TestCase): def test_len(self): diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index 60e3710e..3f1fb5e7 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -1,4 +1,5 @@ import unittest +import numpy as np from fastNLP.core.vocabulary import Vocabulary from fastNLP.io.embed_loader import EmbedLoader @@ -10,3 +11,34 @@ class TestEmbedLoader(unittest.TestCase): vocab.update(["the", "in", "I", "to", "of", "hahaha"]) embedding = EmbedLoader().fast_load_embedding(50, "test/data_for_tests/glove.6B.50d_test.txt", vocab) self.assertEqual(tuple(embedding.shape), (len(vocab), 50)) + + def test_load_with_vocab(self): + vocab = Vocabulary() + glove = "test/data_for_tests/glove.6B.50d_test.txt" + word2vec = "test/data_for_tests/word2vec_test.txt" + vocab.add_word('the') + g_m = EmbedLoader.load_with_vocab(glove, vocab) + self.assertEqual(g_m.shape, (3, 50)) + w_m = EmbedLoader.load_with_vocab(word2vec, vocab, normalize=True) + self.assertEqual(w_m.shape, (3, 50)) + self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 3) + + def test_load_without_vocab(self): + words = ['the', 'of', 'in', 'a', 'to', 'and'] + glove = "test/data_for_tests/glove.6B.50d_test.txt" + word2vec = "test/data_for_tests/word2vec_test.txt" + g_m, vocab = EmbedLoader.load_without_vocab(glove) + self.assertEqual(g_m.shape, (8, 50)) + for word in words: + self.assertIn(word, vocab) + w_m, vocab = EmbedLoader.load_without_vocab(word2vec, normalize=True) + self.assertEqual(w_m.shape, (8, 50)) + self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 8) + for word in words: + self.assertIn(word, vocab) + # no unk + w_m, vocab = EmbedLoader.load_without_vocab(word2vec, normalize=True, unknown=None) + self.assertEqual(w_m.shape, (7, 50)) + self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 7) + for word in words: + self.assertIn(word, vocab) \ No newline at end of file From c1ee0b27dfb5daa8a0f83f161514f07d4075bb4f Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 21 Apr 2019 09:12:42 +0800 Subject: [PATCH 031/173] =?UTF-8?q?1.DataSet.apply()=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E6=97=B6=E6=8F=90=E4=BE=9B=E9=94=99=E8=AF=AF=E7=9A=84index=202?= =?UTF-8?q?.Vocabulary.from=5Fdataset(),=20index=5Fdataset()=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E6=8A=A5=E9=94=99=E6=97=B6=E7=9A=84vocab=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=203.embedloader=E5=9C=A8embed=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E6=97=B6=E9=81=87=E5=88=B0=E4=B8=8D=E8=A7=84=E5=88=99=E7=9A=84?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=B7=B3=E8=BF=87=E8=BF=99=E4=B8=80=E8=A1=8C?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 5 +++ fastNLP/core/dataset.py | 12 +++++- fastNLP/core/vocabulary.py | 17 ++++++-- fastNLP/io/embed_loader.py | 75 ++++++++++++++++++++++++++++-------- test/io/test_embed_loader.py | 7 ++-- 5 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..f04509c1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include requirements.txt +include LICENSE +include README.md +prune test/ +prune reproduction/ diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 7b0e3b9a..76a34655 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -277,7 +277,17 @@ class DataSet(object): (2) is_target: boolean, will be ignored if new_field is None. If True, the new field will be as target. :return results: if new_field_name is not passed, returned values of the function over all instances. """ - results = [func(ins) for ins in self._inner_iter()] + assert len(self)!=0, "Null dataset cannot use .apply()." + results = [] + idx = -1 + try: + for idx, ins in enumerate(self._inner_iter()): + results.append(func(ins)) + except Exception as e: + 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))) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index a73ce2c7..c580dbec 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -182,9 +182,13 @@ class Vocabulary(object): if new_field_name is None: new_field_name = field_name - for dataset in datasets: + for idx, dataset in enumerate(datasets): if isinstance(dataset, DataSet): - dataset.apply(index_instance, new_field_name=new_field_name) + try: + dataset.apply(index_instance, new_field_name=new_field_name) + except Exception as e: + print("When processing the `{}` dataset, the following error occurred.".format(idx)) + raise e else: raise RuntimeError("Only DataSet type is allowed.") @@ -207,11 +211,16 @@ class Vocabulary(object): if isinstance(field[0][0], list): raise RuntimeError("Only support field with 2 dimensions.") [self.add_word_lst(w) for w in field] - for dataset in datasets: + for idx, dataset in enumerate(datasets): if isinstance(dataset, DataSet): - dataset.apply(construct_vocab) + try: + dataset.apply(construct_vocab) + except Exception as e: + print("When processing the `{}` dataset, the following error occurred.".format(idx)) + raise e else: raise RuntimeError("Only DataSet type is allowed.") + return self def to_index(self, w): """ Turn a word to an index. If w is not in Vocabulary, return the unknown label. diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 08a55aa6..5ad27c53 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -6,6 +6,7 @@ import torch from fastNLP.core.vocabulary import Vocabulary from fastNLP.io.base_loader import BaseLoader +import warnings class EmbedLoader(BaseLoader): """docstring for EmbedLoader""" @@ -128,7 +129,7 @@ class EmbedLoader(BaseLoader): return embedding_matrix @staticmethod - def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True): + def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True, error='ignore'): """ load pretraining embedding in {embed_file} based on words in vocab. Words in vocab but not in the pretraining embedding are initialized from a normal distribution which has the mean and std of the found words vectors. @@ -138,6 +139,8 @@ class EmbedLoader(BaseLoader): :param vocab: Vocabulary. :param dtype: the dtype of the embedding matrix :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. + :param error: str, 'ignore', 'strict'; if 'ignore' errors will not raise. if strict, any bad format error will + raise :return: np.ndarray() will have the same [len(vocab), dimension], dimension is determined by the pretrain embedding """ @@ -148,24 +151,32 @@ class EmbedLoader(BaseLoader): hit_flags = np.zeros(len(vocab), dtype=bool) line = f.readline().strip() parts = line.split() + start_idx = 0 if len(parts)==2: dim = int(parts[1]) + start_idx += 1 else: dim = len(parts)-1 f.seek(0) matrix = np.random.randn(len(vocab), dim).astype(dtype) - for line in f: - parts = line.strip().split() - if parts[0] in vocab: - index = vocab.to_index(parts[0]) - matrix[index] = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) - hit_flags[index] = True + for idx, line in enumerate(f, start_idx): + try: + parts = line.strip().split() + if parts[0] in vocab: + index = vocab.to_index(parts[0]) + matrix[index] = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) + hit_flags[index] = True + except Exception as e: + if error == 'ignore': + warnings.warn("Error occurred at the {} line.".format(idx)) + else: + raise e total_hits = sum(hit_flags) print("Found {} out of {} words in the pre-training embedding.".format(total_hits, len(vocab))) found_vectors = matrix[hit_flags] if len(found_vectors)!=0: - mean = np.mean(found_vectors, axis=1, keepdims=True) - std = np.std(found_vectors, axis=1, keepdims=True) + mean = np.mean(found_vectors, axis=0, keepdims=True) + std = np.std(found_vectors, axis=0, keepdims=True) unfound_vec_num = len(vocab) - total_hits r_vecs = np.random.randn(unfound_vec_num, dim).astype(dtype)*std + mean matrix[hit_flags==False] = r_vecs @@ -176,7 +187,8 @@ class EmbedLoader(BaseLoader): return matrix @staticmethod - def load_without_vocab(embed_filepath, dtype=np.float32, padding='', unknown='', normalize=True): + def load_without_vocab(embed_filepath, dtype=np.float32, padding='', unknown='', normalize=True, + error='ignore'): """ load pretraining embedding in {embed_file}. And construct a Vocabulary based on the pretraining embedding. The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). @@ -186,12 +198,16 @@ class EmbedLoader(BaseLoader): :param padding: the padding tag for vocabulary. :param unknown: the unknown tag for vocabulary. :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. + :param error: str, 'ignore', 'strict'; if 'ignore' errors will not raise. if strict, any bad format error will + :raise :return: np.ndarray() is determined by the pretraining embeddings Vocabulary: contain all pretraining words and two special tag[, ] """ vocab = Vocabulary(padding=padding, unknown=unknown) vec_dict = {} + found_unknown = False + found_pad = False with open(embed_filepath, 'r', encoding='utf-8') as f: line = f.readline() @@ -201,16 +217,41 @@ class EmbedLoader(BaseLoader): f.seek(0) start = 0 for idx, line in enumerate(f, start=start): - parts = line.strip().split() - word = parts[0] - if dim==-1: - dim = len(parts)-1 - vec = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) - vec_dict[word] = vec - vocab.add_word(word) + try: + parts = line.strip().split() + word = parts[0] + if dim==-1: + dim = len(parts)-1 + vec = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) + vec_dict[word] = vec + vocab.add_word(word) + if unknown is not None and unknown==word: + found_unknown = True + if found_pad is not None and padding==word: + found_pad = True + except Exception as e: + if error=='ignore': + warnings.warn("Error occurred at the {} line.".format(idx)) + pass + else: + raise e if dim==-1: raise RuntimeError("{} is an empty file.".format(embed_filepath)) matrix = np.random.randn(len(vocab), dim).astype(dtype) + # TODO 需要保证unk其它数据同分布的吗? + if (unknown is not None and not found_unknown) or (padding is not None and not found_pad): + start_idx = 0 + if padding is not None: + start_idx += 1 + if unknown is not None: + start_idx += 1 + + mean = np.mean(matrix[start_idx:], axis=0, keepdims=True) + std = np.std(matrix[start_idx:], axis=0, keepdims=True) + if (unknown is not None and not found_unknown): + matrix[start_idx-1] = np.random.randn(1, dim).astype(dtype)*std + mean + if (padding is not None and not found_pad): + matrix[0] = np.random.randn(1, dim).astype(dtype)*std + mean for key, vec in vec_dict.items(): index = vocab.to_index(key) diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index 3f1fb5e7..9e325334 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -17,11 +17,12 @@ class TestEmbedLoader(unittest.TestCase): glove = "test/data_for_tests/glove.6B.50d_test.txt" word2vec = "test/data_for_tests/word2vec_test.txt" vocab.add_word('the') + vocab.add_word('none') g_m = EmbedLoader.load_with_vocab(glove, vocab) - self.assertEqual(g_m.shape, (3, 50)) + self.assertEqual(g_m.shape, (4, 50)) w_m = EmbedLoader.load_with_vocab(word2vec, vocab, normalize=True) - self.assertEqual(w_m.shape, (3, 50)) - self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 3) + self.assertEqual(w_m.shape, (4, 50)) + self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 4) def test_load_without_vocab(self): words = ['the', 'of', 'in', 'a', 'to', 'and'] From 9d43239fc17a8ec6029b5ef20f175cd4a6d9008b Mon Sep 17 00:00:00 2001 From: xuyige Date: Sun, 21 Apr 2019 15:41:20 +0800 Subject: [PATCH 032/173] update attention --- fastNLP/models/snli.py | 38 ++++++++++++------------- fastNLP/modules/aggregator/__init__.py | 2 +- fastNLP/modules/aggregator/attention.py | 36 ++++++++++++++++------- fastNLP/modules/encoder/transformer.py | 4 +-- 4 files changed, 47 insertions(+), 33 deletions(-) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 6a7d8d84..5816d2af 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -1,6 +1,5 @@ import torch import torch.nn as nn -import torch.nn.functional as F from fastNLP.models.base_model import BaseModel from fastNLP.modules import decoder as Decoder @@ -40,7 +39,7 @@ class ESIM(BaseModel): batch_first=self.batch_first, bidirectional=True ) - self.bi_attention = Aggregator.Bi_Attention() + self.bi_attention = Aggregator.BiAttention() self.mean_pooling = Aggregator.MeanPoolWithMask() self.max_pooling = Aggregator.MaxPoolWithMask() @@ -53,23 +52,23 @@ class ESIM(BaseModel): self.output = Decoder.MLP([4 * self.hidden_size, self.hidden_size, self.n_labels], 'tanh', dropout=self.dropout) - def forward(self, premise, hypothesis, premise_len, hypothesis_len): + def forward(self, words1, words2, seq_len1, seq_len2): """ Forward function - :param premise: A Tensor represents premise: [batch size(B), premise seq len(PL)]. - :param hypothesis: A Tensor represents hypothesis: [B, hypothesis seq len(HL)]. - :param premise_len: A Tensor record which is a real word and which is a padding word in premise: [B, PL]. - :param hypothesis_len: A Tensor record which is a real word and which is a padding word in hypothesis: [B, HL]. + :param words1: A Tensor represents premise: [batch size(B), premise seq len(PL)]. + :param words2: A Tensor represents hypothesis: [B, hypothesis seq len(HL)]. + :param seq_len1: A Tensor record which is a real word and which is a padding word in premise: [B]. + :param seq_len2: A Tensor record which is a real word and which is a padding word in hypothesis: [B]. :return: prediction: A Dict with Tensor of classification result: [B, n_labels(N)]. """ - premise0 = self.embedding_layer(self.embedding(premise)) - hypothesis0 = self.embedding_layer(self.embedding(hypothesis)) + premise0 = self.embedding_layer(self.embedding(words1)) + hypothesis0 = self.embedding_layer(self.embedding(words2)) _BP, _PSL, _HP = premise0.size() _BH, _HSL, _HH = hypothesis0.size() - _BPL, _PLL = premise_len.size() - _HPL, _HLL = hypothesis_len.size() + _BPL, _PLL = seq_len1.size() + _HPL, _HLL = seq_len2.size() assert _BP == _BH and _BPL == _HPL and _BP == _BPL assert _HP == _HH @@ -84,7 +83,7 @@ class ESIM(BaseModel): a = torch.mean(a0.view(B, PL, -1, H), dim=2) # a: [B, PL, H] b = torch.mean(b0.view(B, HL, -1, H), dim=2) # b: [B, HL, H] - ai, bi = self.bi_attention(a, b, premise_len, hypothesis_len) + ai, bi = self.bi_attention(a, b, seq_len1, seq_len2) ma = torch.cat((a, ai, a - ai, a * ai), dim=2) # ma: [B, PL, 4 * H] mb = torch.cat((b, bi, b - bi, b * bi), dim=2) # mb: [B, HL, 4 * H] @@ -98,17 +97,18 @@ class ESIM(BaseModel): va = torch.mean(vat.view(B, PL, -1, H), dim=2) # va: [B, PL, H] vb = torch.mean(vbt.view(B, HL, -1, H), dim=2) # vb: [B, HL, H] - va_ave = self.mean_pooling(va, premise_len, dim=1) # va_ave: [B, H] - va_max, va_arg_max = self.max_pooling(va, premise_len, dim=1) # va_max: [B, H] - vb_ave = self.mean_pooling(vb, hypothesis_len, dim=1) # vb_ave: [B, H] - vb_max, vb_arg_max = self.max_pooling(vb, hypothesis_len, dim=1) # vb_max: [B, H] + va_ave = self.mean_pooling(va, seq_len1, dim=1) # va_ave: [B, H] + va_max, va_arg_max = self.max_pooling(va, seq_len1, dim=1) # va_max: [B, H] + vb_ave = self.mean_pooling(vb, seq_len2, dim=1) # vb_ave: [B, H] + vb_max, vb_arg_max = self.max_pooling(vb, seq_len2, dim=1) # vb_max: [B, H] v = torch.cat((va_ave, va_max, vb_ave, vb_max), dim=1) # v: [B, 4 * H] - prediction = F.tanh(self.output(v)) # prediction: [B, N] + prediction = torch.tanh(self.output(v)) # prediction: [B, N] return {'pred': prediction} - def predict(self, premise, hypothesis, premise_len, hypothesis_len): - return self.forward(premise, hypothesis, premise_len, hypothesis_len) + def predict(self, words1, words2, seq_len1, seq_len2): + prediction = self.forward(words1, words2, seq_len1, seq_len2)['pred'] + return torch.argmax(prediction, dim=-1) diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 2fabb89e..43d60cac 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -5,6 +5,6 @@ from .avg_pool import MeanPoolWithMask from .kmax_pool import KMaxPool from .attention import Attention -from .attention import Bi_Attention +from .attention import BiAttention from .self_attention import SelfAttention diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index ef9d159d..33d73a07 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -23,9 +23,9 @@ class Attention(torch.nn.Module): raise NotImplementedError -class DotAtte(nn.Module): +class DotAttention(nn.Module): def __init__(self, key_size, value_size, dropout=0.1): - super(DotAtte, self).__init__() + super(DotAttention, self).__init__() self.key_size = key_size self.value_size = value_size self.scale = math.sqrt(key_size) @@ -48,7 +48,7 @@ class DotAtte(nn.Module): return torch.matmul(output, V) -class MultiHeadAtte(nn.Module): +class MultiHeadAttention(nn.Module): def __init__(self, input_size, key_size, value_size, num_head, dropout=0.1): """ @@ -58,7 +58,7 @@ class MultiHeadAtte(nn.Module): :param num_head: int,head的数量。 :param dropout: float。 """ - super(MultiHeadAtte, self).__init__() + super(MultiHeadAttention, self).__init__() self.input_size = input_size self.key_size = key_size self.value_size = value_size @@ -68,7 +68,7 @@ class MultiHeadAtte(nn.Module): self.q_in = nn.Linear(input_size, in_size) self.k_in = nn.Linear(input_size, in_size) self.v_in = nn.Linear(input_size, in_size) - self.attention = DotAtte(key_size=key_size, value_size=value_size) + self.attention = DotAttention(key_size=key_size, value_size=value_size) self.out = nn.Linear(value_size * num_head, input_size) self.drop = TimestepDropout(dropout) self.reset_parameters() @@ -109,16 +109,30 @@ class MultiHeadAtte(nn.Module): return output -class Bi_Attention(nn.Module): +class BiAttention(nn.Module): + """Bi Attention module + Calculate Bi Attention matrix `e` + .. math:: + \begin{array}{ll} \\ + e_ij = {a}^{\mathbf{T}}_{i}{b}_{j} \\ + a_i = + b_j = + \end{array} + """ + def __init__(self): - super(Bi_Attention, self).__init__() + super(BiAttention, self).__init__() self.inf = 10e12 def forward(self, in_x1, in_x2, x1_len, x2_len): - # in_x1: [batch_size, x1_seq_len, hidden_size] - # in_x2: [batch_size, x2_seq_len, hidden_size] - # x1_len: [batch_size, x1_seq_len] - # x2_len: [batch_size, x2_seq_len] + """ + :param torch.Tensor in_x1: [batch_size, x1_seq_len, hidden_size] 第一句的特征表示 + :param torch.Tensor in_x2: [batch_size, x2_seq_len, hidden_size] 第二句的特征表示 + :param torch.Tensor x1_len: [batch_size, x1_seq_len] 第一句的0/1mask矩阵 + :param torch.Tensor x2_len: [batch_size, x2_seq_len] 第二句的0/1mask矩阵 + :return: torch.Tensor out_x1: [batch_size, x1_seq_len, hidden_size] 第一句attend到的特征表示 + torch.Tensor out_x2: [batch_size, x2_seq_len, hidden_size] 第一句attend到的特征表示 + """ assert in_x1.size()[0] == in_x2.size()[0] assert in_x1.size()[2] == in_x2.size()[2] diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index d7b8c544..d1262141 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -1,6 +1,6 @@ from torch import nn -from ..aggregator.attention import MultiHeadAtte +from ..aggregator.attention import MultiHeadAttention from ..dropout import TimestepDropout @@ -18,7 +18,7 @@ class TransformerEncoder(nn.Module): class SubLayer(nn.Module): def __init__(self, model_size, inner_size, key_size, value_size, num_head, dropout=0.1): super(TransformerEncoder.SubLayer, self).__init__() - self.atte = MultiHeadAtte(model_size, key_size, value_size, num_head, dropout) + self.atte = MultiHeadAttention(model_size, key_size, value_size, num_head, dropout) self.norm1 = nn.LayerNorm(model_size) self.ffn = nn.Sequential(nn.Linear(model_size, inner_size), nn.ReLU(), From 967e5e568389db8f98fa27c43c2c065470b307f3 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 22 Apr 2019 01:31:41 +0800 Subject: [PATCH 033/173] doc tools --- docs/Makefile | 4 + docs/source/conf.py | 6 +- docs/source/fastNLP.api.rst | 52 +++++-- docs/source/fastNLP.core.rst | 98 ++++++++---- docs/source/fastNLP.io.rst | 48 +++--- docs/source/fastNLP.models.rst | 96 ++++++++++-- docs/source/fastNLP.modules.aggregator.rst | 42 ++++-- docs/source/fastNLP.modules.decoder.rst | 24 ++- docs/source/fastNLP.modules.encoder.rst | 74 ++++++--- docs/source/fastNLP.modules.rst | 33 ++++- docs/source/fastNLP.rst | 13 +- fastNLP/api/__init__.py | 3 + fastNLP/api/api.py | 26 ++-- fastNLP/automl/enas_trainer.py | 15 +- fastNLP/core/dataset.py | 2 +- fastNLP/core/fieldarray.py | 16 +- fastNLP/core/instance.py | 15 +- fastNLP/core/losses.py | 4 +- fastNLP/core/metrics.py | 165 ++++++++++++--------- fastNLP/core/trainer.py | 139 ++++++++--------- fastNLP/core/utils.py | 9 +- fastNLP/models/char_language_model.py | 13 +- fastNLP/models/enas_trainer.py | 15 +- 23 files changed, 599 insertions(+), 313 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index e978dfe6..6a5c7375 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,6 +3,7 @@ # You can set these variables from the command line. SPHINXOPTS = +SPHINXAPIDOC = sphinx-apidoc SPHINXBUILD = sphinx-build SPHINXPROJ = fastNLP SOURCEDIR = source @@ -12,6 +13,9 @@ BUILDDIR = build help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +apidoc: + @$(SPHINXAPIDOC) -f -o source ../fastNLP + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new diff --git a/docs/source/conf.py b/docs/source/conf.py index e449a9f8..96f7f437 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,9 +23,9 @@ copyright = '2018, xpqiu' author = 'xpqiu' # The short X.Y version -version = '0.2' +version = '0.4' # The full version, including alpha/beta/rc tags -release = '0.2' +release = '0.4' # -- General configuration --------------------------------------------------- @@ -67,7 +67,7 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = [] +exclude_patterns = ['modules.rst'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' diff --git a/docs/source/fastNLP.api.rst b/docs/source/fastNLP.api.rst index eb9192da..ee2413fb 100644 --- a/docs/source/fastNLP.api.rst +++ b/docs/source/fastNLP.api.rst @@ -1,36 +1,62 @@ -fastNLP.api -============ +fastNLP.api package +=================== -fastNLP.api.api ----------------- +Submodules +---------- + +fastNLP.api.api module +---------------------- .. automodule:: fastNLP.api.api :members: + :undoc-members: + :show-inheritance: -fastNLP.api.converter ----------------------- +fastNLP.api.converter module +---------------------------- .. automodule:: fastNLP.api.converter :members: + :undoc-members: + :show-inheritance: -fastNLP.api.model\_zoo ------------------------ +fastNLP.api.examples module +--------------------------- -.. automodule:: fastNLP.api.model_zoo +.. automodule:: fastNLP.api.examples :members: + :undoc-members: + :show-inheritance: -fastNLP.api.pipeline ---------------------- +fastNLP.api.pipeline module +--------------------------- .. automodule:: fastNLP.api.pipeline :members: + :undoc-members: + :show-inheritance: -fastNLP.api.processor ----------------------- +fastNLP.api.processor module +---------------------------- .. automodule:: fastNLP.api.processor :members: + :undoc-members: + :show-inheritance: + +fastNLP.api.utils module +------------------------ + +.. automodule:: fastNLP.api.utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.api :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst index b9f6c89f..79d26c76 100644 --- a/docs/source/fastNLP.core.rst +++ b/docs/source/fastNLP.core.rst @@ -1,84 +1,126 @@ -fastNLP.core -============= +fastNLP.core package +==================== -fastNLP.core.batch -------------------- +Submodules +---------- + +fastNLP.core.batch module +------------------------- .. automodule:: fastNLP.core.batch :members: + :undoc-members: + :show-inheritance: + +fastNLP.core.callback module +---------------------------- -fastNLP.core.dataset ---------------------- +.. automodule:: fastNLP.core.callback + :members: + :undoc-members: + :show-inheritance: + +fastNLP.core.dataset module +--------------------------- .. automodule:: fastNLP.core.dataset :members: + :undoc-members: + :show-inheritance: -fastNLP.core.fieldarray ------------------------- +fastNLP.core.fieldarray module +------------------------------ .. automodule:: fastNLP.core.fieldarray :members: + :undoc-members: + :show-inheritance: -fastNLP.core.instance ----------------------- +fastNLP.core.instance module +---------------------------- .. automodule:: fastNLP.core.instance :members: + :undoc-members: + :show-inheritance: -fastNLP.core.losses --------------------- +fastNLP.core.losses module +-------------------------- .. automodule:: fastNLP.core.losses :members: + :undoc-members: + :show-inheritance: -fastNLP.core.metrics ---------------------- +fastNLP.core.metrics module +--------------------------- .. automodule:: fastNLP.core.metrics :members: + :undoc-members: + :show-inheritance: -fastNLP.core.optimizer ------------------------ +fastNLP.core.optimizer module +----------------------------- .. automodule:: fastNLP.core.optimizer :members: + :undoc-members: + :show-inheritance: -fastNLP.core.predictor ------------------------ +fastNLP.core.predictor module +----------------------------- .. automodule:: fastNLP.core.predictor :members: + :undoc-members: + :show-inheritance: -fastNLP.core.sampler ---------------------- +fastNLP.core.sampler module +--------------------------- .. automodule:: fastNLP.core.sampler :members: + :undoc-members: + :show-inheritance: -fastNLP.core.tester --------------------- +fastNLP.core.tester module +-------------------------- .. automodule:: fastNLP.core.tester :members: + :undoc-members: + :show-inheritance: -fastNLP.core.trainer ---------------------- +fastNLP.core.trainer module +--------------------------- .. automodule:: fastNLP.core.trainer :members: + :undoc-members: + :show-inheritance: -fastNLP.core.utils -------------------- +fastNLP.core.utils module +------------------------- .. automodule:: fastNLP.core.utils :members: + :undoc-members: + :show-inheritance: -fastNLP.core.vocabulary ------------------------- +fastNLP.core.vocabulary module +------------------------------ .. automodule:: fastNLP.core.vocabulary :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.core :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index d91e0d1c..bb30c5e7 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -1,42 +1,54 @@ -fastNLP.io -=========== +fastNLP.io package +================== -fastNLP.io.base\_loader ------------------------- +Submodules +---------- + +fastNLP.io.base\_loader module +------------------------------ .. automodule:: fastNLP.io.base_loader :members: + :undoc-members: + :show-inheritance: -fastNLP.io.config\_io ----------------------- +fastNLP.io.config\_io module +---------------------------- .. automodule:: fastNLP.io.config_io :members: + :undoc-members: + :show-inheritance: -fastNLP.io.dataset\_loader ---------------------------- +fastNLP.io.dataset\_loader module +--------------------------------- .. automodule:: fastNLP.io.dataset_loader :members: + :undoc-members: + :show-inheritance: -fastNLP.io.embed\_loader -------------------------- +fastNLP.io.embed\_loader module +------------------------------- .. automodule:: fastNLP.io.embed_loader :members: + :undoc-members: + :show-inheritance: -fastNLP.io.logger ------------------- - -.. automodule:: fastNLP.io.logger - :members: - -fastNLP.io.model\_io ---------------------- +fastNLP.io.model\_io module +--------------------------- .. automodule:: fastNLP.io.model_io :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.io :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index 7452fdf6..3ebf9608 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -1,42 +1,110 @@ -fastNLP.models -=============== +fastNLP.models package +====================== -fastNLP.models.base\_model ---------------------------- +Submodules +---------- + +fastNLP.models.base\_model module +--------------------------------- .. automodule:: fastNLP.models.base_model :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.bert module +-------------------------- -fastNLP.models.biaffine\_parser --------------------------------- +.. automodule:: fastNLP.models.bert + :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.biaffine\_parser module +-------------------------------------- .. automodule:: fastNLP.models.biaffine_parser :members: + :undoc-members: + :show-inheritance: -fastNLP.models.char\_language\_model -------------------------------------- +fastNLP.models.char\_language\_model module +------------------------------------------- .. automodule:: fastNLP.models.char_language_model :members: + :undoc-members: + :show-inheritance: -fastNLP.models.cnn\_text\_classification ------------------------------------------ +fastNLP.models.cnn\_text\_classification module +----------------------------------------------- .. automodule:: fastNLP.models.cnn_text_classification :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.enas\_controller module +-------------------------------------- + +.. automodule:: fastNLP.models.enas_controller + :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.enas\_model module +--------------------------------- + +.. automodule:: fastNLP.models.enas_model + :members: + :undoc-members: + :show-inheritance: -fastNLP.models.sequence\_modeling ----------------------------------- +fastNLP.models.enas\_trainer module +----------------------------------- + +.. automodule:: fastNLP.models.enas_trainer + :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.enas\_utils module +--------------------------------- + +.. automodule:: fastNLP.models.enas_utils + :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.sequence\_modeling module +---------------------------------------- .. automodule:: fastNLP.models.sequence_modeling :members: + :undoc-members: + :show-inheritance: -fastNLP.models.snli --------------------- +fastNLP.models.snli module +-------------------------- .. automodule:: fastNLP.models.snli :members: + :undoc-members: + :show-inheritance: + +fastNLP.models.star\_transformer module +--------------------------------------- + +.. automodule:: fastNLP.models.star_transformer + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.models :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst index 073da4a5..63d351e4 100644 --- a/docs/source/fastNLP.modules.aggregator.rst +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -1,36 +1,54 @@ -fastNLP.modules.aggregator -=========================== +fastNLP.modules.aggregator package +================================== -fastNLP.modules.aggregator.attention -------------------------------------- +Submodules +---------- + +fastNLP.modules.aggregator.attention module +------------------------------------------- .. automodule:: fastNLP.modules.aggregator.attention :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.aggregator.avg\_pool -------------------------------------- +fastNLP.modules.aggregator.avg\_pool module +------------------------------------------- .. automodule:: fastNLP.modules.aggregator.avg_pool :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.aggregator.kmax\_pool --------------------------------------- +fastNLP.modules.aggregator.kmax\_pool module +-------------------------------------------- .. automodule:: fastNLP.modules.aggregator.kmax_pool :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.aggregator.max\_pool -------------------------------------- +fastNLP.modules.aggregator.max\_pool module +------------------------------------------- .. automodule:: fastNLP.modules.aggregator.max_pool :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.aggregator.self\_attention -------------------------------------------- +fastNLP.modules.aggregator.self\_attention module +------------------------------------------------- .. automodule:: fastNLP.modules.aggregator.self_attention :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.modules.aggregator :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index 6844543a..25602b2c 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -1,18 +1,30 @@ -fastNLP.modules.decoder -======================== +fastNLP.modules.decoder package +=============================== -fastNLP.modules.decoder.CRF ----------------------------- +Submodules +---------- + +fastNLP.modules.decoder.CRF module +---------------------------------- .. automodule:: fastNLP.modules.decoder.CRF :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.decoder.MLP ----------------------------- +fastNLP.modules.decoder.MLP module +---------------------------------- .. automodule:: fastNLP.modules.decoder.MLP :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.modules.decoder :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index ea8fc699..ab93a169 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -1,60 +1,94 @@ -fastNLP.modules.encoder -======================== +fastNLP.modules.encoder package +=============================== -fastNLP.modules.encoder.char\_embedding ----------------------------------------- +Submodules +---------- + +fastNLP.modules.encoder.char\_embedding module +---------------------------------------------- .. automodule:: fastNLP.modules.encoder.char_embedding :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.conv ------------------------------ +fastNLP.modules.encoder.conv module +----------------------------------- .. automodule:: fastNLP.modules.encoder.conv :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.conv\_maxpool --------------------------------------- +fastNLP.modules.encoder.conv\_maxpool module +-------------------------------------------- .. automodule:: fastNLP.modules.encoder.conv_maxpool :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.embedding ----------------------------------- +fastNLP.modules.encoder.embedding module +---------------------------------------- .. automodule:: fastNLP.modules.encoder.embedding :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.linear -------------------------------- +fastNLP.modules.encoder.linear module +------------------------------------- .. automodule:: fastNLP.modules.encoder.linear :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.lstm ------------------------------ +fastNLP.modules.encoder.lstm module +----------------------------------- .. automodule:: fastNLP.modules.encoder.lstm :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.masked\_rnn ------------------------------------- +fastNLP.modules.encoder.masked\_rnn module +------------------------------------------ .. automodule:: fastNLP.modules.encoder.masked_rnn :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.transformer ------------------------------------- +fastNLP.modules.encoder.star\_transformer module +------------------------------------------------ + +.. automodule:: fastNLP.modules.encoder.star_transformer + :members: + :undoc-members: + :show-inheritance: + +fastNLP.modules.encoder.transformer module +------------------------------------------ .. automodule:: fastNLP.modules.encoder.transformer :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.encoder.variational\_rnn ------------------------------------------ +fastNLP.modules.encoder.variational\_rnn module +----------------------------------------------- .. automodule:: fastNLP.modules.encoder.variational_rnn :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.modules.encoder :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 965fb27d..57858176 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -1,5 +1,8 @@ -fastNLP.modules -================ +fastNLP.modules package +======================= + +Subpackages +----------- .. toctree:: @@ -7,24 +10,38 @@ fastNLP.modules fastNLP.modules.decoder fastNLP.modules.encoder -fastNLP.modules.dropout ------------------------- +Submodules +---------- + +fastNLP.modules.dropout module +------------------------------ .. automodule:: fastNLP.modules.dropout :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.other\_modules -------------------------------- +fastNLP.modules.other\_modules module +------------------------------------- .. automodule:: fastNLP.modules.other_modules :members: + :undoc-members: + :show-inheritance: -fastNLP.modules.utils ----------------------- +fastNLP.modules.utils module +---------------------------- .. automodule:: fastNLP.modules.utils :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- .. automodule:: fastNLP.modules :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst index 61882359..6348c9a6 100644 --- a/docs/source/fastNLP.rst +++ b/docs/source/fastNLP.rst @@ -1,13 +1,22 @@ -fastNLP -======== +fastNLP package +=============== + +Subpackages +----------- .. toctree:: fastNLP.api + fastNLP.automl fastNLP.core fastNLP.io fastNLP.models fastNLP.modules +Module contents +--------------- + .. automodule:: fastNLP :members: + :undoc-members: + :show-inheritance: diff --git a/fastNLP/api/__init__.py b/fastNLP/api/__init__.py index a21a4c42..ae31b80b 100644 --- a/fastNLP/api/__init__.py +++ b/fastNLP/api/__init__.py @@ -1 +1,4 @@ +""" + 这是 API 部分的注释 +""" from .api import CWS, POS, Parser diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index 53a80131..b001629c 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -1,3 +1,7 @@ +""" +API.API 的文档 + +""" import warnings import torch @@ -184,17 +188,17 @@ class CWS(API): """ 传入一个分词文件路径,返回该数据集上分词f1, precision, recall。 分词文件应该为: - 1 编者按 编者按 NN O 11 nmod:topic - 2 : : PU O 11 punct - 3 7月 7月 NT DATE 4 compound:nn - 4 12日 12日 NT DATE 11 nmod:tmod - 5 , , PU O 11 punct - - 1 这 这 DT O 3 det - 2 款 款 M O 1 mark:clf - 3 飞行 飞行 NN O 8 nsubj - 4 从 从 P O 5 case - 5 外型 外型 NN O 8 nmod:prep + 1 编者按 编者按 NN O 11 nmod:topic + 2 : : PU O 11 punct + 3 7月 7月 NT DATE 4 compound:nn + 4 12日 12日 NT DATE 11 nmod:tmod + 5 , , PU O 11 punct + + 1 这 这 DT O 3 det + 2 款 款 M O 1 mark:clf + 3 飞行 飞行 NN O 8 nsubj + 4 从 从 P O 5 case + 5 外型 外型 NN O 8 nmod:prep 以空行分割两个句子,有内容的每行有7列。 :param filepath: str, 文件路径路径。 diff --git a/fastNLP/automl/enas_trainer.py b/fastNLP/automl/enas_trainer.py index 7c0da752..061d604c 100644 --- a/fastNLP/automl/enas_trainer.py +++ b/fastNLP/automl/enas_trainer.py @@ -62,13 +62,14 @@ class ENASTrainer(fastNLP.Trainer): """ :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 最好的模型参数。 - :return results: 返回一个字典类型的数据, 内含以下内容:: - - seconds: float, 表示训练时长 - 以下三个内容只有在提供了dev_data的情况下会有。 - best_eval: Dict of Dict, 表示evaluation的结果 - best_epoch: int,在第几个epoch取得的最佳值 - best_step: int, 在第几个step(batch)更新取得的最佳值 + :return results: 返回一个字典类型的数据, + 内含以下内容:: + + seconds: float, 表示训练时长 + 以下三个内容只有在提供了dev_data的情况下会有。 + best_eval: Dict of Dict, 表示evaluation的结果 + best_epoch: int,在第几个epoch取得的最佳值 + best_step: int, 在第几个step(batch)更新取得的最佳值 """ results = {} diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 76a34655..6cbfc20f 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -272,7 +272,7 @@ class DataSet(object): :param func: a function that takes an instance as input. :param str new_field_name: If not None, results of the function will be stored as a new field. - :param **kwargs: Accept parameters will be + :param kwargs: Accept parameters will be (1) is_input: boolean, will be ignored if new_field is None. If True, the new field will be as input. (2) is_target: boolean, will be ignored if new_field is None. If True, the new field will be as target. :return results: if new_field_name is not passed, returned values of the function over all instances. diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index 10fbbebe..caf2a1cf 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -48,12 +48,16 @@ class PadderBase: class AutoPadder(PadderBase): """ 根据contents的数据自动判定是否需要做padding。 - (1) 如果元素类型(元素类型是指field中最里层List的元素的数据类型, 可以通过FieldArray.dtype查看,比如['This', 'is', ...]的元素类 - 型为np.str, [[1,2], ...]的元素类型为np.int64)的数据不为(np.int64, np.float64)则不会进行padding - (2) 如果元素类型为(np.int64, np.float64), - (2.1) 如果该field的内容只有一个,比如为sequence_length, 则不进行padding - (2.2) 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 - 如果某个instance中field为[1, 2, 3],则可以pad; 若为[[1,2], [3,4, ...]]则不能进行pad + + 1 如果元素类型(元素类型是指field中最里层List的元素的数据类型, 可以通过FieldArray.dtype查看,比如['This', 'is', ...]的元素类 + 型为np.str, [[1,2], ...]的元素类型为np.int64)的数据不为(np.int64, np.float64)则不会进行padding + + 2 如果元素类型为(np.int64, np.float64), + + 2.1 如果该field的内容只有一个,比如为sequence_length, 则不进行padding + + 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 + 如果某个instance中field为[1, 2, 3],则可以pad; 若为[[1,2], [3,4, ...]]则不能进行pad """ def __init__(self, pad_val=0): """ diff --git a/fastNLP/core/instance.py b/fastNLP/core/instance.py index 5ac52e3f..fff992cc 100644 --- a/fastNLP/core/instance.py +++ b/fastNLP/core/instance.py @@ -1,13 +1,12 @@ class Instance(object): """An Instance is an example of data. - 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]) - - :param fields: a dict of (str: list). - + 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]) + """ def __init__(self, **fields): diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index b52244e5..6b0b4460 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -272,7 +272,7 @@ def squash(predict, truth, **kwargs): :param predict: Tensor, model output :param truth: Tensor, truth from dataset - :param **kwargs: extra arguments + :param kwargs: extra arguments :return predict , truth: predict & truth after processing """ return predict.view(-1, predict.size()[-1]), truth.view(-1, ) @@ -316,7 +316,7 @@ def mask(predict, truth, **kwargs): :param predict: Tensor, [batch_size , max_len , tag_size] :param truth: Tensor, [batch_size , max_len] - :param **kwargs: extra arguments, kwargs["mask"]: ByteTensor, [batch_size , max_len], the mask Tensor. The position that is 1 will be selected. + :param kwargs: extra arguments, kwargs["mask"]: ByteTensor, [batch_size , max_len], the mask Tensor. The position that is 1 will be selected. :return predict , truth: predict & truth after processing """ diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 5687cc85..314be0d9 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -17,66 +17,72 @@ class MetricBase(object): """Base class for all metrics. 所有的传入到Trainer, Tester的Metric需要继承自该对象。需要覆盖写入evaluate(), get_metric()方法。 + evaluate(xxx)中传入的是一个batch的数据。 + get_metric(xxx)当所有数据处理完毕,调用该方法得到最终的metric值 + 以分类问题中,Accuracy计算为例 - 假设model的forward返回dict中包含'pred'这个key, 并且该key需要用于Accuracy - class Model(nn.Module): - def __init__(xxx): - # do something - def forward(self, xxx): - # do something - return {'pred': pred, 'other_keys':xxx} # pred's shape: batch_size x num_classes + 假设model的forward返回dict中包含'pred'这个key, 并且该key需要用于Accuracy:: + + class Model(nn.Module): + def __init__(xxx): + # do something + def forward(self, xxx): + # do something + return {'pred': pred, 'other_keys':xxx} # pred's shape: batch_size x num_classes + 假设dataset中'label'这个field是需要预测的值,并且该field被设置为了target - 对应的AccMetric可以按如下的定义 - # version1, 只使用这一次 - class AccMetric(MetricBase): - def __init__(self): - super().__init__() - - # 根据你的情况自定义指标 - self.corr_num = 0 - self.total = 0 - - def evaluate(self, label, pred): # 这里的名称需要和dataset中target field与model返回的key是一样的,不然找不到对应的value - # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric - self.total += label.size(0) - self.corr_num += label.eq(pred).sum().item() - - def get_metric(self, reset=True): # 在这里定义如何计算metric - acc = self.corr_num/self.total - if reset: # 是否清零以便重新计算 + 对应的AccMetric可以按如下的定义, version1, 只使用这一次:: + + class AccMetric(MetricBase): + def __init__(self): + super().__init__() + + # 根据你的情况自定义指标 self.corr_num = 0 self.total = 0 - return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 - - - # version2,如果需要复用Metric,比如下一次使用AccMetric时,dataset中目标field不叫label而叫y,或者model的输出不是pred - class AccMetric(MetricBase): - def __init__(self, label=None, pred=None): - # 假设在另一场景使用时,目标field叫y,model给出的key为pred_y。则只需要在初始化AccMetric时, - # acc_metric = AccMetric(label='y', pred='pred_y')即可。 - # 当初始化为acc_metric = AccMetric(),即label=None, pred=None, fastNLP会直接使用'label', 'pred'作为key去索取对 - # 应的的值 - super().__init__() - self._init_param_map(label=label, pred=pred) # 该方法会注册label和pred. 仅需要注册evaluate()方法会用到的参数名即可 - # 如果没有注册该则效果与version1就是一样的 - - # 根据你的情况自定义指标 - self.corr_num = 0 - self.total = 0 - - def evaluate(self, label, pred): # 这里的参数名称需要和self._init_param_map()注册时一致。 - # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric - self.total += label.size(0) - self.corr_num += label.eq(pred).sum().item() - - def get_metric(self, reset=True): # 在这里定义如何计算metric - acc = self.corr_num/self.total - if reset: # 是否清零以便重新计算 + + def evaluate(self, label, pred): # 这里的名称需要和dataset中target field与model返回的key是一样的,不然找不到对应的value + # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric + self.total += label.size(0) + self.corr_num += label.eq(pred).sum().item() + + def get_metric(self, reset=True): # 在这里定义如何计算metric + acc = self.corr_num/self.total + if reset: # 是否清零以便重新计算 + self.corr_num = 0 + self.total = 0 + return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 + + + version2,如果需要复用Metric,比如下一次使用AccMetric时,dataset中目标field不叫label而叫y,或者model的输出不是pred:: + + class AccMetric(MetricBase): + def __init__(self, label=None, pred=None): + # 假设在另一场景使用时,目标field叫y,model给出的key为pred_y。则只需要在初始化AccMetric时, + # acc_metric = AccMetric(label='y', pred='pred_y')即可。 + # 当初始化为acc_metric = AccMetric(),即label=None, pred=None, fastNLP会直接使用'label', 'pred'作为key去索取对 + # 应的的值 + super().__init__() + self._init_param_map(label=label, pred=pred) # 该方法会注册label和pred. 仅需要注册evaluate()方法会用到的参数名即可 + # 如果没有注册该则效果与version1就是一样的 + + # 根据你的情况自定义指标 self.corr_num = 0 self.total = 0 - return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 + + def evaluate(self, label, pred): # 这里的参数名称需要和self._init_param_map()注册时一致。 + # dev或test时,每个batch结束会调用一次该方法,需要实现如何根据每个batch累加metric + self.total += label.size(0) + self.corr_num += label.eq(pred).sum().item() + + def get_metric(self, reset=True): # 在这里定义如何计算metric + acc = self.corr_num/self.total + if reset: # 是否清零以便重新计算 + self.corr_num = 0 + self.total = 0 + return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 ``MetricBase`` handles validity check of its input dictionaries - ``pred_dict`` and ``target_dict``. @@ -84,12 +90,12 @@ class MetricBase(object): ``target_dict`` is the ground truth from DataSet where ``is_target`` is set ``True``. ``MetricBase`` will do the following type checks: - 1. whether self.evaluate has varargs, which is not supported. - 2. whether params needed by self.evaluate is not included in ``pred_dict``, ``target_dict``. - 3. whether params needed by self.evaluate duplicate in ``pred_dict``, ``target_dict``. + 1. whether self.evaluate has varargs, which is not supported. + 2. whether params needed by self.evaluate is not included in ``pred_dict``, ``target_dict``. + 3. whether params needed by self.evaluate duplicate in ``pred_dict``, ``target_dict``. Besides, before passing params into self.evaluate, this function will filter out params from output_dict and - target_dict which are not used in self.evaluate. (but if **kwargs presented in self.evaluate, no filtering + target_dict which are not used in self.evaluate. (but if kwargs presented in self.evaluate, no filtering will be conducted.) """ @@ -388,23 +394,26 @@ class SpanFPreRecMetric(MetricBase): """ 在序列标注问题中,以span的方式计算F, pre, rec. 比如中文Part of speech中,会以character的方式进行标注,句子'中国在亚洲'对应的POS可能为(以BMES为例) - ['B-NN', 'E-NN', 'S-DET', 'B-NN', 'E-NN']。该metric就是为类似情况下的F1计算。 - 最后得到的metric结果为 - { - 'f': xxx, # 这里使用f考虑以后可以计算f_beta值 - 'pre': xxx, - 'rec':xxx - } - 若only_gross=False, 即还会返回各个label的metric统计值 + ['B-NN', 'E-NN', 'S-DET', 'B-NN', 'E-NN']。该metric就是为类似情况下的F1计算。 + 最后得到的metric结果为:: + { - 'f': xxx, - 'pre': xxx, - 'rec':xxx, - 'f-label': xxx, - 'pre-label': xxx, - 'rec-label':xxx, - ... - } + 'f': xxx, # 这里使用f考虑以后可以计算f_beta值 + 'pre': xxx, + 'rec':xxx + } + + 若only_gross=False, 即还会返回各个label的metric统计值:: + + { + 'f': xxx, + 'pre': xxx, + 'rec':xxx, + 'f-label': xxx, + 'pre-label': xxx, + 'rec-label':xxx, + ... + } """ def __init__(self, tag_vocab, pred=None, target=None, seq_lens=None, encoding_type='bio', ignore_labels=None, @@ -573,13 +582,21 @@ class BMESF1PreRecMetric(MetricBase): """ 按照BMES标注方式计算f1, precision, recall。由于可能存在非法tag,比如"BS",所以需要用以下的表格做转换,cur_B意思是当前tag是B, next_B意思是后一个tag是B。则cur_B=S,即将当前被predict是B的tag标为S;next_M=B, 即将后一个被predict是M的tag标为B + + +-------+---------+----------+----------+---------+---------+ | | next_B | next_M | next_E | next_S | end | - |:-----:|:-------:|:--------:|:--------:|:-------:|:-------:| - | start | 合法 | next_M=B | next_E=S | 合法 | - | + +=======+=========+==========+==========+=========+=========+ + | start | 合法 | next_M=B | next_E=S | 合法 | -- | + +-------+---------+----------+----------+---------+---------+ | cur_B | cur_B=S | 合法 | 合法 | cur_B=S | cur_B=S | + +-------+---------+----------+----------+---------+---------+ | cur_M | cur_M=E | 合法 | 合法 | cur_M=E | cur_M=E | + +-------+---------+----------+----------+---------+---------+ | cur_E | 合法 | next_M=B | next_E=S | 合法 | 合法 | + +-------+---------+----------+----------+---------+---------+ | cur_S | 合法 | next_M=B | next_E=S | 合法 | 合法 | + +-------+---------+----------+----------+---------+---------+ + 举例: prediction为BSEMS,会被认为是SSSSS. diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index b45dd148..250cfdb0 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -66,28 +66,28 @@ class Trainer(object): 不足,通过设置batch_size=32, update_every=4达到目的 """ super(Trainer, self).__init__() - + if not isinstance(train_data, DataSet): raise TypeError(f"The type of train_data must be fastNLP.DataSet, got {type(train_data)}.") if not isinstance(model, nn.Module): raise TypeError(f"The type of model must be torch.nn.Module, got {type(model)}.") - + # check metrics and dev_data if (not metrics) and dev_data is not None: raise ValueError("No metric for dev_data evaluation.") if metrics and (dev_data is None): raise ValueError("No dev_data for evaluations, pass dev_data or set metrics to None. ") - + # check update every - assert update_every>=1, "update_every must be no less than 1." + assert update_every >= 1, "update_every must be no less than 1." self.update_every = int(update_every) - + # check save_path if not (save_path is None or isinstance(save_path, str)): raise ValueError("save_path can only be None or `str`.") # prepare evaluate metrics = _prepare_metrics(metrics) - + # parse metric_key # increase_better is True. It means the exp result gets better if the indicator increases. # It is true by default. @@ -97,19 +97,19 @@ class Trainer(object): self.metric_key = metric_key[1:] if metric_key[0] == "+" or metric_key[0] == "-" else metric_key elif len(metrics) > 0: self.metric_key = metrics[0].__class__.__name__.lower().strip('metric') - + # prepare loss losser = _prepare_losser(loss) - + # sampler check if sampler is not None and not isinstance(sampler, BaseSampler): raise ValueError("The type of sampler should be fastNLP.BaseSampler, got {}.".format(type(sampler))) - + if check_code_level > -1: _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data, metric_key=metric_key, check_level=check_code_level, batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE)) - + self.train_data = train_data self.dev_data = dev_data # If None, No validation. self.model = model @@ -120,7 +120,7 @@ class Trainer(object): self.use_cuda = bool(use_cuda) self.save_path = save_path self.print_every = int(print_every) - self.validate_every = int(validate_every) if validate_every!=0 else -1 + self.validate_every = int(validate_every) if validate_every != 0 else -1 self.best_metric_indicator = None self.best_dev_epoch = None self.best_dev_step = None @@ -129,19 +129,19 @@ class Trainer(object): self.prefetch = prefetch self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) self.n_steps = (len(self.train_data) // self.batch_size + int( - len(self.train_data) % self.batch_size != 0)) * self.n_epochs - + len(self.train_data) % self.batch_size != 0)) * self.n_epochs + if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer else: if optimizer is None: optimizer = Adam(lr=0.01, weight_decay=0) self.optimizer = optimizer.construct_from_pytorch(self.model.parameters()) - + self.use_tqdm = use_tqdm self.pbar = None self.print_every = abs(self.print_every) - + if self.dev_data is not None: self.tester = Tester(model=self.model, data=self.dev_data, @@ -149,14 +149,13 @@ class Trainer(object): batch_size=self.batch_size, use_cuda=self.use_cuda, verbose=0) - + self.step = 0 self.start_time = None # start timestamp - + self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) - - + def train(self, load_best_model=True): """ @@ -185,14 +184,15 @@ class Trainer(object): 根据metrics进行evaluation,并根据是否提供了save_path判断是否存储模型 :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 - 最好的模型参数。 - :return results: 返回一个字典类型的数据, 内含以下内容:: + 最好的模型参数。 + :return results: 返回一个字典类型的数据, + 内含以下内容:: - seconds: float, 表示训练时长 - 以下三个内容只有在提供了dev_data的情况下会有。 - best_eval: Dict of Dict, 表示evaluation的结果 - best_epoch: int,在第几个epoch取得的最佳值 - best_step: int, 在第几个step(batch)更新取得的最佳值 + seconds: float, 表示训练时长 + 以下三个内容只有在提供了dev_data的情况下会有。 + best_eval: Dict of Dict, 表示evaluation的结果 + best_epoch: int,在第几个epoch取得的最佳值 + best_step: int, 在第几个step(batch)更新取得的最佳值 """ results = {} @@ -205,21 +205,22 @@ class Trainer(object): self.model = self.model.cuda() self._model_device = self.model.parameters().__next__().device self._mode(self.model, is_test=False) - + self.start_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) start_time = time.time() print("training epochs started " + self.start_time, flush=True) - + try: self.callback_manager.on_train_begin() self._train() self.callback_manager.on_train_end() except (CallbackException, KeyboardInterrupt) as e: self.callback_manager.on_exception(e) - + if self.dev_data is not None and hasattr(self, 'best_dev_perf'): - print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + - self.tester._format_eval_results(self.best_dev_perf),) + print( + "\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + + self.tester._format_eval_results(self.best_dev_perf), ) results['best_eval'] = self.best_dev_perf results['best_epoch'] = self.best_dev_epoch results['best_step'] = self.best_dev_step @@ -233,9 +234,9 @@ class Trainer(object): finally: pass results['seconds'] = round(time.time() - start_time, 2) - + return results - + def _train(self): if not self.use_tqdm: from fastNLP.core.utils import pseudo_tqdm as inner_tqdm @@ -244,13 +245,13 @@ class Trainer(object): self.step = 0 self.epoch = 0 start = time.time() - + with inner_tqdm(total=self.n_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: self.pbar = pbar if isinstance(pbar, tqdm) else None avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) - for epoch in range(1, self.n_epochs+1): + for epoch in range(1, self.n_epochs + 1): self.epoch = epoch pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) # early stopping @@ -262,22 +263,22 @@ class Trainer(object): # negative sampling; replace unknown; re-weight batch_y self.callback_manager.on_batch_begin(batch_x, batch_y, indices) prediction = self._data_forward(self.model, batch_x) - + # edit prediction self.callback_manager.on_loss_begin(batch_y, prediction) loss = self._compute_loss(prediction, batch_y).mean() avg_loss += loss.item() - loss = loss/self.update_every - + loss = loss / self.update_every + # Is loss NaN or inf? requires_grad = False self.callback_manager.on_backward_begin(loss) self._grad_backward(loss) self.callback_manager.on_backward_end() - + self._update() self.callback_manager.on_step_end() - - if (self.step+1) % self.print_every == 0: + + if (self.step + 1) % self.print_every == 0: avg_loss = avg_loss / self.print_every if self.use_tqdm: print_output = "loss:{0:<6.5f}".format(avg_loss) @@ -290,34 +291,34 @@ class Trainer(object): pbar.set_postfix_str(print_output) avg_loss = 0 self.callback_manager.on_batch_end() - + if ((self.validate_every > 0 and self.step % self.validate_every == 0) or (self.validate_every < 0 and self.step % len(data_iterator) == 0)) \ and self.dev_data is not None: eval_res = self._do_validation(epoch=epoch, step=self.step) eval_str = "Evaluation at Epoch {}/{}. Step:{}/{}. ".format(epoch, self.n_epochs, self.step, self.n_steps) + \ - self.tester._format_eval_results(eval_res) + self.tester._format_eval_results(eval_res) pbar.write(eval_str + '\n') - + # ================= mini-batch end ==================== # - + # lr decay; early stopping self.callback_manager.on_epoch_end() # =============== epochs end =================== # pbar.close() self.pbar = None # ============ tqdm end ============== # - + def _do_validation(self, epoch, step): self.callback_manager.on_valid_begin() res = self.tester.test() - + is_better_eval = False if self._better_eval_result(res): if self.save_path is not None: self._save_model(self.model, - "best_" + "_".join([self.model.__class__.__name__, self.metric_key, self.start_time])) + "best_" + "_".join([self.model.__class__.__name__, self.metric_key, self.start_time])) else: self._best_model_states = {name: param.cpu().clone() for name, param in self.model.named_parameters()} self.best_dev_perf = res @@ -327,7 +328,7 @@ class Trainer(object): # get validation results; adjust optimizer self.callback_manager.on_valid_end(res, self.metric_key, self.optimizer, is_better_eval) return res - + def _mode(self, model, is_test=False): """Train mode or Test mode. This is for PyTorch currently. @@ -339,21 +340,21 @@ class Trainer(object): model.eval() else: model.train() - + def _update(self): """Perform weight update on a model. """ - if (self.step+1)%self.update_every==0: + if (self.step + 1) % self.update_every == 0: self.optimizer.step() - + def _data_forward(self, network, x): x = _build_args(network.forward, **x) y = network(**x) if not isinstance(y, dict): raise TypeError(f"The return value of {get_func_signature(network.forward)} should be dict, got {type(y)}.") return y - + def _grad_backward(self, loss): """Compute gradient with link rules. @@ -361,10 +362,10 @@ class Trainer(object): For PyTorch, just do "loss.backward()" """ - if self.step%self.update_every==0: + if self.step % self.update_every == 0: self.model.zero_grad() loss.backward() - + def _compute_loss(self, predict, truth): """Compute loss given prediction and ground truth. @@ -373,7 +374,7 @@ class Trainer(object): :return: a scalar """ return self.losser(predict, truth) - + def _save_model(self, model, model_name, only_param=False): """ 存储不含有显卡信息的state_dict或model :param model: @@ -394,7 +395,7 @@ class Trainer(object): model.cpu() torch.save(model, model_path) model.to(self._model_device) - + def _load_model(self, model, model_name, only_param=False): # 返回bool值指示是否成功reload模型 if self.save_path is not None: @@ -409,7 +410,7 @@ class Trainer(object): else: return False return True - + def _better_eval_result(self, metrics): """Check if the current epoch yields better validation results. @@ -437,6 +438,7 @@ class Trainer(object): DEFAULT_CHECK_BATCH_SIZE = 2 DEFAULT_CHECK_NUM_BATCH = 2 + def _get_value_info(_dict): # given a dict value, return information about this dict's value. Return list of str strs = [] @@ -453,27 +455,28 @@ def _get_value_info(_dict): strs.append(_str) return strs + def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_SIZE, dev_data=None, metric_key=None, check_level=0): # check get_loss 方法 model_devcie = model.parameters().__next__().device - + batch = Batch(dataset=dataset, batch_size=batch_size, sampler=SequentialSampler()) for batch_count, (batch_x, batch_y) in enumerate(batch): _move_dict_value_to_device(batch_x, batch_y, device=model_devcie) # forward check - if batch_count==0: + if batch_count == 0: info_str = "" input_fields = _get_value_info(batch_x) target_fields = _get_value_info(batch_y) - if len(input_fields)>0: + if len(input_fields) > 0: info_str += "input fields after batch(if batch size is {}):\n".format(batch_size) info_str += "\n".join(input_fields) info_str += '\n' else: raise RuntimeError("There is no input field.") - if len(target_fields)>0: + if len(target_fields) > 0: info_str += "target fields after batch(if batch size is {}):\n".format(batch_size) info_str += "\n".join(target_fields) info_str += '\n' @@ -481,14 +484,14 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ info_str += 'There is no target field.' print(info_str) _check_forward_error(forward_func=model.forward, dataset=dataset, - batch_x=batch_x, check_level=check_level) - + batch_x=batch_x, check_level=check_level) + refined_batch_x = _build_args(model.forward, **batch_x) pred_dict = model(**refined_batch_x) func_signature = get_func_signature(model.forward) if not isinstance(pred_dict, dict): raise TypeError(f"The return value of {func_signature} should be `dict`, not `{type(pred_dict)}`.") - + # loss check try: loss = losser(pred_dict, batch_y) @@ -512,7 +515,7 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ model.zero_grad() if batch_count + 1 >= DEFAULT_CHECK_NUM_BATCH: break - + if dev_data is not None: tester = Tester(data=dev_data[:batch_size * DEFAULT_CHECK_NUM_BATCH], model=model, metrics=metrics, batch_size=batch_size, verbose=-1) @@ -526,7 +529,7 @@ def _check_eval_results(metrics, metric_key, metric_list): # metric_list: 多个用来做评价的指标,来自Trainer的初始化 if isinstance(metrics, tuple): loss, metrics = metrics - + if isinstance(metrics, dict): if len(metrics) == 1: # only single metric, just use it @@ -537,7 +540,7 @@ def _check_eval_results(metrics, metric_key, metric_list): if metrics_name not in metrics: raise RuntimeError(f"{metrics_name} is chosen to do validation, but got {metrics}") metric_dict = metrics[metrics_name] - + if len(metric_dict) == 1: indicator_val, indicator = list(metric_dict.values())[0], list(metric_dict.keys())[0] elif len(metric_dict) > 1 and metric_key is None: diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index d9141412..fc15166e 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -197,17 +197,22 @@ def get_func_signature(func): Given a function or method, return its signature. For example: - (1) function + + 1 function:: + def func(a, b='a', *args): xxxx get_func_signature(func) # 'func(a, b='a', *args)' - (2) method + + 2 method:: + class Demo: def __init__(self): xxx def forward(self, a, b='a', **args) demo = Demo() get_func_signature(demo.forward) # 'Demo.forward(self, a, b='a', **args)' + :param func: a function or a method :return: str or None """ diff --git a/fastNLP/models/char_language_model.py b/fastNLP/models/char_language_model.py index 5fbde3cc..d5e3359d 100644 --- a/fastNLP/models/char_language_model.py +++ b/fastNLP/models/char_language_model.py @@ -20,16 +20,23 @@ class Highway(nn.Module): class CharLM(nn.Module): """CNN + highway network + LSTM - # Input: + + # Input:: + 4D tensor with shape [batch_size, in_channel, height, width] - # Output: + + # Output:: + 2D Tensor with shape [batch_size, vocab_size] - # Arguments: + + # Arguments:: + char_emb_dim: the size of each character's attention word_emb_dim: the size of each word's attention vocab_size: num of unique words num_char: num of characters use_gpu: True or False + """ def __init__(self, char_emb_dim, word_emb_dim, diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index 6b51c897..26b7cd49 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -65,13 +65,14 @@ class ENASTrainer(fastNLP.Trainer): """ :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 最好的模型参数。 - :return results: 返回一个字典类型的数据, 内含以下内容:: - - seconds: float, 表示训练时长 - 以下三个内容只有在提供了dev_data的情况下会有。 - best_eval: Dict of Dict, 表示evaluation的结果 - best_epoch: int,在第几个epoch取得的最佳值 - best_step: int, 在第几个step(batch)更新取得的最佳值 + :return results: 返回一个字典类型的数据, + 内含以下内容:: + + seconds: float, 表示训练时长 + 以下三个内容只有在提供了dev_data的情况下会有。 + best_eval: Dict of Dict, 表示evaluation的结果 + best_epoch: int,在第几个epoch取得的最佳值 + best_step: int, 在第几个step(batch)更新取得的最佳值 """ results = {} From 13d8978953026bcb6fb4046c7f6e0ce500458efb Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 22 Apr 2019 01:49:44 +0800 Subject: [PATCH 034/173] fix some doc errors --- fastNLP/io/config_io.py | 8 +++--- fastNLP/io/dataset_loader.py | 47 +++++++++++++++++++++--------------- fastNLP/io/embed_loader.py | 2 +- fastNLP/io/model_io.py | 12 +++++---- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index 5a64b96c..c0ffe53e 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -26,10 +26,10 @@ class ConfigLoader(BaseLoader): :param str file_path: the path of config file :param dict sections: the dict of ``{section_name(string): ConfigSection object}`` - Example:: - - test_args = ConfigSection() - ConfigLoader("config.cfg").load_config("./data_for_tests/config", {"POS_test": test_args}) + Example:: + + test_args = ConfigSection() + ConfigLoader("config.cfg").load_config("./data_for_tests/config", {"POS_test": test_args}) """ assert isinstance(sections, dict) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index e33384a8..87127cf8 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -9,7 +9,7 @@ from fastNLP.io.base_loader import DataLoaderRegister def convert_seq_dataset(data): """Create an DataSet instance that contains no labels. - :param data: list of list of strings, [num_examples, *]. + :param data: list of list of strings, [num_examples, \*]. Example:: [ @@ -28,7 +28,7 @@ def convert_seq_dataset(data): def convert_seq2tag_dataset(data): """Convert list of data into DataSet. - :param data: list of list of strings, [num_examples, *]. + :param data: list of list of strings, [num_examples, \*]. Example:: [ @@ -48,7 +48,7 @@ def convert_seq2tag_dataset(data): def convert_seq2seq_dataset(data): """Convert list of data into DataSet. - :param data: list of list of strings, [num_examples, *]. + :param data: list of list of strings, [num_examples, \*]. Example:: [ @@ -177,18 +177,18 @@ DataLoaderRegister.set_reader(RawDataSetLoader, 'read_rawdata') class DummyPOSReader(DataSetLoader): """A simple reader for a dummy POS tagging dataset. - In these datasets, each line are divided by "\t". The first Col is the vocabulary and the second + In these datasets, each line are divided by "\\\\t". The first Col is the vocabulary and the second Col is the label. Different sentence are divided by an empty line. - E.g:: + E.g:: - Tom label1 - and label2 - Jerry label1 - . label3 - (separated by an empty line) - Hello label4 - world label5 - ! label3 + Tom label1 + and label2 + Jerry label1 + . label3 + (separated by an empty line) + Hello label4 + world label5 + ! label3 In this example, there are two sentences "Tom and Jerry ." and "Hello world !". Each word has its own label. """ @@ -200,11 +200,13 @@ class DummyPOSReader(DataSetLoader): """ :return data: three-level list Example:: + [ [ [word_11, word_12, ...], [label_1, label_1, ...] ], [ [word_21, word_22, ...], [label_2, label_1, ...] ], ... ] + """ with open(data_path, "r", encoding="utf-8") as f: lines = f.readlines() @@ -550,6 +552,7 @@ class SNLIDataSetReader(DataSetLoader): :param data: A 3D tensor. Example:: + [ [ [premise_word_11, premise_word_12, ...], [hypothesis_word_11, hypothesis_word_12, ...], [label_1] ], [ [premise_word_21, premise_word_22, ...], [hypothesis_word_21, hypothesis_word_22, ...], [label_2] ], @@ -647,7 +650,7 @@ class NaiveCWSReader(DataSetLoader): 例如:: 这是 fastNLP , 一个 非常 good 的 包 . - + 或者,即每个part后面还有一个pos tag 例如:: @@ -661,12 +664,15 @@ class NaiveCWSReader(DataSetLoader): def load(self, filepath, in_word_splitter=None, cut_long_sent=False): """ - 允许使用的情况有(默认以\t或空格作为seg) + 允许使用的情况有(默认以\\\\t或空格作为seg):: + 这是 fastNLP , 一个 非常 good 的 包 . - 和 + + 和:: + 也/D 在/P 團員/Na 之中/Ng ,/COMMACATEGORY + 如果splitter不为None则认为是第二种情况, 且我们会按splitter分割"也/D", 然后取第一部分. 例如"也/D".split('/')[0] - :param filepath: :param in_word_splitter: :param cut_long_sent: @@ -737,11 +743,12 @@ class ZhConllPOSReader(object): def load(self, path): """ - 返回的DataSet, 包含以下的field + 返回的DataSet, 包含以下的field:: + words:list of str, tag: list of str, 被加入了BMES tag, 比如原来的序列为['VP', 'NN', 'NN', ..],会被认为是["S-VP", "B-NN", "M-NN",..] - 假定了输入为conll的格式,以空行隔开两个句子,每行共7列,即 - :: + + 假定了输入为conll的格式,以空行隔开两个句子,每行共7列,即:: 1 编者按 编者按 NN O 11 nmod:topic 2 : : PU O 11 punct diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 5ad27c53..16ea0339 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -132,7 +132,7 @@ class EmbedLoader(BaseLoader): def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True, error='ignore'): """ load pretraining embedding in {embed_file} based on words in vocab. Words in vocab but not in the pretraining - embedding are initialized from a normal distribution which has the mean and std of the found words vectors. + embedding are initialized from a normal distribution which has the mean and std of the found words vectors. The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). :param embed_filepath: str, where to read pretrain embedding diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index 422eb919..53bdc7ce 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -31,16 +31,18 @@ class ModelLoader(BaseLoader): class ModelSaver(object): """Save a model + Example:: - :param str save_path: the path to the saving directory. - Example:: - - saver = ModelSaver("./save/model_ckpt_100.pkl") - saver.save_pytorch(model) + saver = ModelSaver("./save/model_ckpt_100.pkl") + saver.save_pytorch(model) """ def __init__(self, save_path): + """ + + :param save_path: the path to the saving directory. + """ self.save_path = save_path def save_pytorch(self, model, param_only=True): From c520d350827cda3415d2bfd0b033ed79e02ff352 Mon Sep 17 00:00:00 2001 From: yh Date: Mon, 22 Apr 2019 10:52:01 +0800 Subject: [PATCH 035/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E6=B3=A8=E9=87=8A=EF=BC=8C=E5=A2=9E=E5=8A=A0viterbi?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 168 +++++++++++------- fastNLP/core/fieldarray.py | 22 +-- fastNLP/core/utils.py | 2 +- fastNLP/models/sequence_modeling.py | 2 +- fastNLP/modules/decoder/CRF.py | 113 ++++++------ fastNLP/modules/decoder/utils.py | 70 ++++++++ .../models/cws_model.py | 4 +- .../models/cws_transformer.py | 4 +- 8 files changed, 249 insertions(+), 136 deletions(-) create mode 100644 fastNLP/modules/decoder/utils.py diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 76a34655..f0e27b83 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -151,16 +151,19 @@ class DataSet(object): assert name in self.field_arrays self.field_arrays[name].append(field) - def add_field(self, name, fields, padder=AutoPadder(pad_val=0), is_input=False, is_target=False, ignore_type=False): + def add_field(self, name, fields, padder=None, is_input=False, is_target=False, ignore_type=False): """Add a new field to the DataSet. :param str name: the name of the field. :param fields: a list of int, float, or other objects. - :param int padder: PadBase对象,如何对该Field进行padding。大部分情况使用默认值即可 + :param padder: PadBase对象,如何对该Field进行padding。如果为None则使用 :param bool is_input: whether this field is model input. :param bool is_target: whether this field is label or target. :param bool ignore_type: If True, do not perform type check. (Default: False) """ + if padder is None: + padder = AutoPadder(pad_val=0) + if len(self.field_arrays) != 0: if len(self) != len(fields): raise RuntimeError(f"The field to append must have the same size as dataset. " @@ -231,8 +234,8 @@ class DataSet(object): raise KeyError("{} is not a valid field name.".format(name)) def set_padder(self, field_name, padder): - """ - 为field_name设置padder + """为field_name设置padder + :param field_name: str, 设置field的padding方式为padder :param padder: PadderBase类型或None. 设置为None即删除padder。即对该field不进行padding操作. :return: @@ -242,8 +245,7 @@ class DataSet(object): self.field_arrays[field_name].set_padder(padder) def set_pad_val(self, field_name, pad_val): - """ - 为某个 + """为某个field设置对应的pad_val. :param field_name: str,修改该field的pad_val :param pad_val: int,该field的padder会以pad_val作为padding index @@ -254,43 +256,60 @@ class DataSet(object): self.field_arrays[field_name].set_pad_val(pad_val) def get_input_name(self): - """Get all field names with `is_input` as True. + """返回所有is_input被设置为True的field名称 - :return field_names: a list of str + :return list, 里面的元素为被设置为input的field名称 """ return [name for name, field in self.field_arrays.items() if field.is_input] def get_target_name(self): - """Get all field names with `is_target` as True. + """返回所有is_target被设置为True的field名称 - :return field_names: a list of str + :return list, 里面的元素为被设置为target的field名称 """ return [name for name, field in self.field_arrays.items() if field.is_target] - def apply(self, func, new_field_name=None, **kwargs): - """Apply a function to every instance of the DataSet. - - :param func: a function that takes an instance as input. - :param str new_field_name: If not None, results of the function will be stored as a new field. - :param **kwargs: Accept parameters will be - (1) is_input: boolean, will be ignored if new_field is None. If True, the new field will be as input. - (2) is_target: boolean, will be ignored if new_field is None. If True, the new field will be as target. - :return results: if new_field_name is not passed, returned values of the function over all instances. + def apply_field(self, func, field_name, new_field_name=None, **kwargs): + """将DataSet中的每个instance中的`field_name`这个field传给func,并获取它的返回值. + + :param func: Callable, input是instance的`field_name`这个field. + :param field_name: str, 传入func的是哪个field. + :param new_field_name: (str, None). 如果不是None,将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有 + 的field相同,则覆盖之前的field. + :param **kwargs: 合法的参数有以下三个 + (1) is_input: bool, 如果为True则将`new_field_name`这个field设置为input + (2) is_target: bool, 如果为True则将`new_field_name`这个field设置为target + (3) ignore_type: bool, 如果为True则将`new_field_name`这个field的ignore_type设置为true, 忽略其类型 + :return: List[], 里面的元素为func的返回值,所以list长度为DataSet的长度 """ - assert len(self)!=0, "Null dataset cannot use .apply()." + assert len(self)!=0, "Null DataSet cannot use apply()." + if field_name not in self: + raise KeyError("DataSet has no field named `{}`.".format(field_name)) results = [] idx = -1 try: for idx, ins in enumerate(self._inner_iter()): - results.append(func(ins)) + results.append(func(ins[field_name])) except Exception as e: 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 _add_apply_field(self, results, new_field_name, kwargs): + """将results作为加入到新的field中,field名称为new_field_name + + :param results: List[], 一般是apply*()之后的结果 + :param new_field_name: str, 新加入的field的名称 + :param kwargs: dict, 用户apply*()时传入的自定义参数 + :return: + """ extra_param = {} if 'is_input' in kwargs: extra_param['is_input'] = kwargs['is_input'] @@ -298,56 +317,84 @@ class DataSet(object): extra_param['is_target'] = kwargs['is_target'] if 'ignore_type' in kwargs: extra_param['ignore_type'] = kwargs['ignore_type'] - if new_field_name is not None: - if new_field_name in self.field_arrays: - # overwrite the field, keep same attributes - old_field = self.field_arrays[new_field_name] - if 'is_input' not in extra_param: - extra_param['is_input'] = old_field.is_input - if 'is_target' not in extra_param: - extra_param['is_target'] = old_field.is_target - if 'ignore_type' not in extra_param: - extra_param['ignore_type'] = old_field.ignore_type - self.add_field(name=new_field_name, fields=results, is_input=extra_param["is_input"], - is_target=extra_param["is_target"], ignore_type=extra_param['ignore_type']) - else: - self.add_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)) + if new_field_name in self.field_arrays: + # overwrite the field, keep same attributes + old_field = self.field_arrays[new_field_name] + if 'is_input' not in extra_param: + extra_param['is_input'] = old_field.is_input + if 'is_target' not in extra_param: + extra_param['is_target'] = old_field.is_target + if 'ignore_type' not in extra_param: + extra_param['ignore_type'] = old_field.ignore_type + self.add_field(name=new_field_name, fields=results, is_input=extra_param["is_input"], + is_target=extra_param["is_target"], ignore_type=extra_param['ignore_type']) else: - return results + self.add_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中,并获取它的返回值. + + :param func: Callable, 参数是DataSet中的instance + :param new_field_name: (None, str). (1) None, 不创建新的field; (2) str,将func的返回值放入这个名为 + `new_field_name`的新field中,如果名称与已有的field相同,则覆盖之前的field; + :param kwargs: 合法的参数有以下三个 + (1) is_input: bool, 如果为True则将`new_field_name`的field设置为input + (2) is_target: bool, 如果为True则将`new_field_name`的field设置为target + (3) ignore_type: bool, 如果为True则将`new_field_name`的field的ignore_type设置为true, 忽略其类型 + :return: List[], 里面的元素为func的返回值,所以list长度为DataSet的长度 + """ + 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: + 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): - """Drop instances if a condition holds. + """func接受一个instance,返回bool值,返回值为True时,该instance会被删除。 - :param func: a function that takes an Instance object as input, and returns bool. - The instance will be dropped if the function returns True. - :param inplace: bool, whether to drop inpalce. Otherwise a new dataset will be returned. + :param func: Callable, 接受一个instance作为参数,返回bool值。为True时删除该instance + :param inplace: bool, 是否在当前DataSet中直接删除instance。如果为False,返回值为一个删除了相应instance的新的DataSet + :return: DataSet. """ if inplace: results = [ins for ins in self._inner_iter() if not func(ins)] for name, old_field in self.field_arrays.items(): self.field_arrays[name].content = [ins[name] for ins in results] + return self else: results = [ins for ins in self if not func(ins)] data = DataSet(results) for field_name, field in self.field_arrays.items(): data.field_arrays[field_name].to(field) + return data - def split(self, dev_ratio): - """Split the dataset into training and development(validation) set. + def split(self, ratio): + """将DataSet按照ratio的比例拆分,返回两个DataSet - :param float dev_ratio: the ratio of test set in all data. - :return (train_set, dev_set): - train_set: the training set - dev_set: the development set + :param ratio: float, 0', '']: continue to_tag, to_label = split_tag_label(to_label) - if is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): + if _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): allowed_trans.append((from_id, to_id)) return allowed_trans -def is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): +def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): """ :param encoding_type: str, 支持"BIO", "BMES", "BEMSO"。 @@ -140,20 +140,22 @@ def is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label) raise ValueError("Unexpect tag type {}. Expect only 'B', 'M', 'E', 'S', 'O'.".format(from_tag)) else: - raise ValueError("Only support BIO, BMES encoding type, got {}.".format(encoding_type)) + raise ValueError("Only support BIO, BMES, BMESO encoding type, got {}.".format(encoding_type)) class ConditionalRandomField(nn.Module): - """ - - :param int num_tags: 标签的数量。 - :param bool include_start_end_trans: 是否包含起始tag - :param list allowed_transitions: ``List[Tuple[from_tag_id(int), to_tag_id(int)]]``. 允许的跃迁,可以通过allowed_transitions()得到。 - 如果为None,则所有跃迁均为合法 - :param str initial_method: - """ - - def __init__(self, num_tags, include_start_end_trans=False, allowed_transitions=None, initial_method=None): + def __init__(self, num_tags, include_start_end_trans=False, allowed_transitions=None, + initial_method=None): + """条件随机场。 + 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 + + :param num_tags: int, 标签的数量 + :param include_start_end_trans: bool, 是否考虑各个tag作为开始以及结尾的分数。 + :param allowed_transitions: List[Tuple[from_tag_id(int), to_tag_id(int)]], 内部的Tuple[from_tag_id(int), + to_tag_id(int)]视为允许发生的跃迁,其他没有包含的跃迁认为是禁止跃迁,可以通过 + allowed_transitions()函数得到;如果为None,则所有跃迁均为合法 + :param initial_method: str, 初始化方法。见initial_parameter + """ super(ConditionalRandomField, self).__init__() self.include_start_end_trans = include_start_end_trans @@ -168,18 +170,12 @@ class ConditionalRandomField(nn.Module): if allowed_transitions is None: constrain = torch.zeros(num_tags + 2, num_tags + 2) else: - constrain = torch.ones(num_tags + 2, num_tags + 2) * -1000 + constrain = torch.new_full((num_tags+2, num_tags+2), fill_value=-10000.0, dtype=torch.float) for from_tag_id, to_tag_id in allowed_transitions: constrain[from_tag_id, to_tag_id] = 0 self._constrain = nn.Parameter(constrain, requires_grad=False) - # self.reset_parameter() initial_parameter(self, initial_method) - def reset_parameter(self): - nn.init.xavier_normal_(self.trans_m) - if self.include_start_end_trans: - nn.init.normal_(self.start_scores) - nn.init.normal_(self.end_scores) def _normalizer_likelihood(self, logits, mask): """Computes the (batch_size,) denominator term for the log-likelihood, which is the @@ -239,10 +235,11 @@ class ConditionalRandomField(nn.Module): def forward(self, feats, tags, mask): """ - Calculate the neg log likelihood - :param feats:FloatTensor, batch_size x max_len x num_tags - :param tags:LongTensor, batch_size x max_len - :param mask:ByteTensor batch_size x max_len + 用于计算CRF的前向loss,返回值为一个batch_size的FloatTensor,可能需要mean()求得loss。 + + :param feats:FloatTensor, batch_size x max_len x num_tags,特征矩阵。 + :param tags:LongTensor, batch_size x max_len,标签矩阵。 + :param mask:ByteTensor batch_size x max_len,为0的位置认为是padding。 :return:FloatTensor, batch_size """ feats = feats.transpose(0, 1) @@ -253,28 +250,27 @@ class ConditionalRandomField(nn.Module): return all_path_score - gold_path_score - def viterbi_decode(self, data, mask, get_score=False, unpad=False): - """Given a feats matrix, return best decode path and best score. + def viterbi_decode(self, feats, mask, unpad=False): + """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 - :param data:FloatTensor, batch_size x max_len x num_tags - :param mask:ByteTensor batch_size x max_len - :param get_score: bool, whether to output the decode score. - :param unpad: bool, 是否将结果unpad, - 如果False, 返回的是batch_size x max_len的tensor, - 如果True,返回的是List[List[int]], List[int]为每个sequence的label,已经unpadding了,即每个 - List[int]的长度是这个sample的有效长度 - :return: 如果get_score为False,返回结果根据unpadding变动 - 如果get_score为True, 返回 (paths, List[float], )。第一个仍然是解码后的路径(根据unpad变化),第二个List[Float] - 为每个seqence的解码分数。 + :param feats: FloatTensor, batch_size x max_len x num_tags,特征矩阵。 + :param mask: ByteTensor, batch_size x max_len, 为0的位置认为是pad;如果为None,则认为没有padding。 + :param unpad: bool, 是否将结果删去padding, + False, 返回的是batch_size x max_len的tensor, + True,返回的是List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int] + 的长度是这个sample的有效长度。 + :return: 返回 (paths, scores)。 + paths: 是解码后的路径, 其值参照unpad参数. + scores: torch.FloatTensor, size为(batch_size,), 对应每个最优路径的分数。 """ - batch_size, seq_len, n_tags = data.size() - data = data.transpose(0, 1).data # L, B, H + batch_size, seq_len, n_tags = feats.size() + feats = feats.transpose(0, 1).data # L, B, H mask = mask.transpose(0, 1).data.byte() # L, B # dp - vpath = data.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) - vscore = data[0] + vpath = feats.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) + vscore = feats[0] transitions = self._constrain.data.clone() transitions[:n_tags, :n_tags] += self.trans_m.data if self.include_start_end_trans: @@ -285,23 +281,24 @@ class ConditionalRandomField(nn.Module): trans_score = transitions[:n_tags, :n_tags].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 = data[i].view(batch_size, 1, n_tags) + cur_score = feats[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 vscore = best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ vscore.masked_fill(mask[i].view(batch_size, 1), 0) - vscore += transitions[:n_tags, n_tags+1].view(1, -1) + if self.include_start_end_trans: + vscore += transitions[:n_tags, n_tags+1].view(1, -1) # backtrace - batch_idx = torch.arange(batch_size, dtype=torch.long, device=data.device) - seq_idx = torch.arange(seq_len, dtype=torch.long, device=data.device) + batch_idx = torch.arange(batch_size, dtype=torch.long, device=feats.device) + seq_idx = torch.arange(seq_len, dtype=torch.long, device=feats.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 = data.new_empty((seq_len, batch_size), dtype=torch.long) + ans = feats.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): diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py new file mode 100644 index 00000000..6e35af9a --- /dev/null +++ b/fastNLP/modules/decoder/utils.py @@ -0,0 +1,70 @@ + +import torch + + +def log_sum_exp(x, dim=-1): + max_value, _ = x.max(dim=dim, keepdim=True) + res = torch.log(torch.sum(torch.exp(x - max_value), dim=dim, keepdim=True)) + max_value + return res.squeeze(dim) + + +def viterbi_decode(feats, transitions, mask=None, unpad=False): + """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 + + :param feats: 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, + False, 返回的是batch_size x max_len的tensor, + True,返回的是List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int]的长度是 + 这个sample的有效长度。 + :return: 返回 (paths, scores)。 + paths: 是解码后的路径, 其值参照unpad参数. + scores: torch.FloatTensor, size为(batch_size,), 对应每个最优路径的分数。 + + """ + batch_size, seq_len, n_tags = feats.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 + 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) + + # dp + vpath = feats.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) + vscore = feats[0] + + vscore += transitions[n_tags, :n_tags] + trans_score = transitions[:n_tags, :n_tags].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) + score = prev_score + trans_score + cur_score + best_score, best_dst = score.max(1) + vpath[i] = best_dst + vscore = best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ + 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) + 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_score, last_tags = vscore.max(1) + ans[idxes[0], batch_idx] = last_tags + for i in range(seq_len - 1): + last_tags = vpath[idxes[i], batch_idx, last_tags] + ans[idxes[i + 1], batch_idx] = last_tags + ans = ans.transpose(0, 1) + if unpad: + paths = [] + for idx, seq_len in enumerate(lens): + paths.append(ans[idx, :seq_len + 1].tolist()) + else: + paths = ans + return paths, ans_score \ No newline at end of file diff --git a/reproduction/Chinese_word_segmentation/models/cws_model.py b/reproduction/Chinese_word_segmentation/models/cws_model.py index daefc380..13632207 100644 --- a/reproduction/Chinese_word_segmentation/models/cws_model.py +++ b/reproduction/Chinese_word_segmentation/models/cws_model.py @@ -183,7 +183,7 @@ class CWSBiLSTMCRF(BaseModel): masks = seq_lens_to_mask(seq_lens) feats = self.encoder_model(chars, bigrams, seq_lens) feats = self.decoder_model(feats) - probs = self.crf.viterbi_decode(feats, masks, get_score=False) + paths, _ = self.crf.viterbi_decode(feats, masks) - return {'pred': probs, 'seq_lens':seq_lens} + return {'pred': paths, 'seq_lens':seq_lens} diff --git a/reproduction/Chinese_word_segmentation/models/cws_transformer.py b/reproduction/Chinese_word_segmentation/models/cws_transformer.py index 736edade..d49ce3a9 100644 --- a/reproduction/Chinese_word_segmentation/models/cws_transformer.py +++ b/reproduction/Chinese_word_segmentation/models/cws_transformer.py @@ -72,9 +72,9 @@ class TransformerCWS(nn.Module): feats = self.transformer(x, masks) feats = self.fc2(feats) - probs = self.crf.viterbi_decode(feats, masks, get_score=False) + paths, _ = self.crf.viterbi_decode(feats, masks) - return {'pred': probs, 'seq_lens':seq_lens} + return {'pred': paths, 'seq_lens':seq_lens} class NoamOpt(torch.optim.Optimizer): From 15cdee827a3a6788e4b16127b000c5cd60c72047 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 22 Apr 2019 11:34:45 +0800 Subject: [PATCH 036/173] =?UTF-8?q?=E6=A0=B7=E4=BE=8B=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Makefile | 3 + fastNLP/api/__init__.py | 3 - fastNLP/api/api.py | 83 ++++++++++++++++++------- fastNLP/modules/aggregator/attention.py | 6 +- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 6a5c7375..6f2f2821 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -16,6 +16,9 @@ help: apidoc: @$(SPHINXAPIDOC) -f -o source ../fastNLP +server: + cd build/html && python -m http.server + .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new diff --git a/fastNLP/api/__init__.py b/fastNLP/api/__init__.py index ae31b80b..a21a4c42 100644 --- a/fastNLP/api/__init__.py +++ b/fastNLP/api/__init__.py @@ -1,4 +1 @@ -""" - 这是 API 部分的注释 -""" from .api import CWS, POS, Parser diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index b001629c..f088b121 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -1,5 +1,39 @@ """ -API.API 的文档 +api.api的介绍文档 + 直接缩进会把上面的文字变成标题 + +空行缩进的写法比较合理 + + 比较合理 + +*这里是斜体内容* + +**这里是粗体内容** + +数学公式块 + +.. math:: + E = mc^2 + +.. note:: + 注解型提示。 + +.. warning:: + 警告型提示。 + +.. seealso:: + `参考与超链接 `_ + +普通代码块需要空一行, Example:: + + from fitlog import fitlog + fitlog.commit() + +普通下标和上标: + +H\ :sub:`2`\ O + +E = mc\ :sup:`2` """ import warnings @@ -28,6 +62,9 @@ model_urls = { class API: + """ + 这是 API 类的文档 + """ def __init__(self): self.pipeline = None self._dict = None @@ -73,8 +110,9 @@ class POS(API): self.load(model_path, device) def predict(self, content): - """ - + """predict函数的介绍, + 函数介绍的第二句,这句话不会换行 + :param content: list of list of str. Each string is a token(word). :return answer: list of list of str. Each string is a tag. """ @@ -140,13 +178,14 @@ class POS(API): class CWS(API): - def __init__(self, model_path=None, device='cpu'): - """ - 中文分词高级接口。 + """ + 中文分词高级接口。 - :param model_path: 当model_path为None,使用默认位置的model。如果默认位置不存在,则自动下载模型 - :param device: str,可以为'cpu', 'cuda'或'cuda:0'等。会将模型load到相应device进行推断。 - """ + :param model_path: 当model_path为None,使用默认位置的model。如果默认位置不存在,则自动下载模型 + :param device: str,可以为'cpu', 'cuda'或'cuda:0'等。会将模型load到相应device进行推断。 + """ + def __init__(self, model_path=None, device='cpu'): + super(CWS, self).__init__() if model_path is None: model_path = model_urls['cws'] @@ -187,18 +226,20 @@ class CWS(API): def test(self, filepath): """ 传入一个分词文件路径,返回该数据集上分词f1, precision, recall。 - 分词文件应该为: - 1 编者按 编者按 NN O 11 nmod:topic - 2 : : PU O 11 punct - 3 7月 7月 NT DATE 4 compound:nn - 4 12日 12日 NT DATE 11 nmod:tmod - 5 , , PU O 11 punct - - 1 这 这 DT O 3 det - 2 款 款 M O 1 mark:clf - 3 飞行 飞行 NN O 8 nsubj - 4 从 从 P O 5 case - 5 外型 外型 NN O 8 nmod:prep + 分词文件应该为:: + + 1 编者按 编者按 NN O 11 nmod:topic + 2 : : PU O 11 punct + 3 7月 7月 NT DATE 4 compound:nn + 4 12日 12日 NT DATE 11 nmod:tmod + 5 , , PU O 11 punct + + 1 这 这 DT O 3 det + 2 款 款 M O 1 mark:clf + 3 飞行 飞行 NN O 8 nsubj + 4 从 从 P O 5 case + 5 外型 外型 NN O 8 nmod:prep + 以空行分割两个句子,有内容的每行有7列。 :param filepath: str, 文件路径路径。 diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index 33d73a07..4155fdd6 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -112,12 +112,15 @@ class MultiHeadAttention(nn.Module): class BiAttention(nn.Module): """Bi Attention module Calculate Bi Attention matrix `e` + .. math:: + \begin{array}{ll} \\ e_ij = {a}^{\mathbf{T}}_{i}{b}_{j} \\ a_i = b_j = \end{array} + """ def __init__(self): @@ -131,7 +134,8 @@ class BiAttention(nn.Module): :param torch.Tensor x1_len: [batch_size, x1_seq_len] 第一句的0/1mask矩阵 :param torch.Tensor x2_len: [batch_size, x2_seq_len] 第二句的0/1mask矩阵 :return: torch.Tensor out_x1: [batch_size, x1_seq_len, hidden_size] 第一句attend到的特征表示 - torch.Tensor out_x2: [batch_size, x2_seq_len, hidden_size] 第一句attend到的特征表示 + torch.Tensor out_x2: [batch_size, x2_seq_len, hidden_size] 第一句attend到的特征表示 + """ assert in_x1.size()[0] == in_x2.size()[0] From c344f7a2f9f637d0c5d6b2b059d59a69d7fb885f Mon Sep 17 00:00:00 2001 From: yunfan Date: Mon, 22 Apr 2019 01:04:10 +0800 Subject: [PATCH 037/173] - add pad sequence for lstm - add csv, conll, json filereader - update dataloader - remove useless dataloader - fix trainer loss print - fix tests --- fastNLP/api/api.py | 81 ++- fastNLP/core/dataset.py | 6 +- fastNLP/core/trainer.py | 3 +- fastNLP/io/dataset_loader.py | 700 +++----------------------- fastNLP/io/file_reader.py | 112 +++++ fastNLP/modules/encoder/lstm.py | 39 +- test/core/test_dataset.py | 24 +- test/data_for_tests/sample_snli.jsonl | 3 + test/io/test_dataset_loader.py | 19 +- 9 files changed, 316 insertions(+), 671 deletions(-) create mode 100644 fastNLP/io/file_reader.py create mode 100644 test/data_for_tests/sample_snli.jsonl diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index 53a80131..512f485b 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -9,7 +9,7 @@ from fastNLP.core.dataset import DataSet from fastNLP.api.utils import load_url from fastNLP.api.processor import ModelProcessor -from fastNLP.io.dataset_loader import ConllCWSReader, ConllxDataLoader +from fastNLP.io.dataset_loader import cut_long_sentence, ConllLoader from fastNLP.core.instance import Instance from fastNLP.api.pipeline import Pipeline from fastNLP.core.metrics import SpanFPreRecMetric @@ -23,6 +23,85 @@ model_urls = { } +class ConllCWSReader(object): + """Deprecated. Use ConllLoader for all types of conll-format files.""" + def __init__(self): + pass + + def load(self, path, cut_long_sent=False): + """ + 返回的DataSet只包含raw_sentence这个field,内容为str。 + 假定了输入为conll的格式,以空行隔开两个句子,每行共7列,即 + :: + + 1 编者按 编者按 NN O 11 nmod:topic + 2 : : PU O 11 punct + 3 7月 7月 NT DATE 4 compound:nn + 4 12日 12日 NT DATE 11 nmod:tmod + 5 , , PU O 11 punct + + 1 这 这 DT O 3 det + 2 款 款 M O 1 mark:clf + 3 飞行 飞行 NN O 8 nsubj + 4 从 从 P O 5 case + 5 外型 外型 NN O 8 nmod:prep + + """ + datalist = [] + with open(path, 'r', encoding='utf-8') as f: + sample = [] + for line in f: + if line.startswith('\n'): + datalist.append(sample) + sample = [] + elif line.startswith('#'): + continue + else: + sample.append(line.strip().split()) + if len(sample) > 0: + datalist.append(sample) + + ds = DataSet() + for sample in datalist: + # print(sample) + res = self.get_char_lst(sample) + if res is None: + continue + line = ' '.join(res) + if cut_long_sent: + sents = cut_long_sentence(line) + else: + sents = [line] + for raw_sentence in sents: + ds.append(Instance(raw_sentence=raw_sentence)) + return ds + + def get_char_lst(self, sample): + if len(sample) == 0: + return None + text = [] + for w in sample: + t1, t2, t3, t4 = w[1], w[3], w[6], w[7] + if t3 == '_': + return None + text.append(t1) + return text + +class ConllxDataLoader(ConllLoader): + """返回“词级别”的标签信息,包括词、词性、(句法)头依赖、(句法)边标签。跟``ZhConllPOSReader``完全不同。 + + Deprecated. Use ConllLoader for all types of conll-format files. + """ + def __init__(self): + headers = [ + 'words', 'pos_tags', 'heads', 'labels', + ] + indexs = [ + 1, 3, 6, 7, + ] + super(ConllxDataLoader, self).__init__(headers=headers, indexs=indexs) + + class API: def __init__(self): self.pipeline = None diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 24376a72..3ef61177 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -373,6 +373,9 @@ class DataSet(object): :return dataset: the read data set """ + import warnings + warnings.warn('read_csv is deprecated, use CSVLoader instead', + category=DeprecationWarning) with open(csv_path, "r") as f: start_idx = 0 if headers is None: @@ -398,9 +401,6 @@ class DataSet(object): _dict[header].append(content) return cls(_dict) - # def read_pos(self): - # return DataLoaderRegister.get_reader('read_pos') - def save(self, path): """Save the DataSet object as pickle. diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index d9aa520f..1b5c1edf 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -268,8 +268,9 @@ class Trainer(object): self.callback_manager.on_step_end() if self.step % self.print_every == 0: + avg_loss = float(avg_loss) / self.print_every if self.use_tqdm: - print_output = "loss:{0:<6.5f}".format(avg_loss / self.print_every) + print_output = "loss:{0:<6.5f}".format(avg_loss) pbar.update(self.print_every) else: end = time.time() diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index e33384a8..5657e194 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,71 +1,13 @@ import os import json +from nltk.tree import Tree from fastNLP.core.dataset import DataSet from fastNLP.core.instance import Instance -from fastNLP.io.base_loader import DataLoaderRegister +from fastNLP.io.file_reader import read_csv, read_json, read_conll -def convert_seq_dataset(data): - """Create an DataSet instance that contains no labels. - - :param data: list of list of strings, [num_examples, *]. - Example:: - - [ - [word_11, word_12, ...], - ... - ] - - :return: a DataSet. - """ - dataset = DataSet() - for word_seq in data: - dataset.append(Instance(word_seq=word_seq)) - return dataset - - -def convert_seq2tag_dataset(data): - """Convert list of data into DataSet. - - :param data: list of list of strings, [num_examples, *]. - Example:: - - [ - [ [word_11, word_12, ...], label_1 ], - [ [word_21, word_22, ...], label_2 ], - ... - ] - - :return: a DataSet. - """ - dataset = DataSet() - for sample in data: - dataset.append(Instance(word_seq=sample[0], label=sample[1])) - return dataset - - -def convert_seq2seq_dataset(data): - """Convert list of data into DataSet. - - :param data: list of list of strings, [num_examples, *]. - Example:: - - [ - [ [word_11, word_12, ...], [label_1, label_1, ...] ], - [ [word_21, word_22, ...], [label_2, label_1, ...] ], - ... - ] - - :return: a DataSet. - """ - dataset = DataSet() - for sample in data: - dataset.append(Instance(word_seq=sample[0], label_seq=sample[1])) - return dataset - - -def download_from_url(url, path): +def _download_from_url(url, path): from tqdm import tqdm import requests @@ -81,7 +23,7 @@ def download_from_url(url, path): t.update(len(chunk)) return -def uncompress(src, dst): +def _uncompress(src, dst): import zipfile, gzip, tarfile, os def unzip(src, dst): @@ -134,241 +76,6 @@ class DataSetLoader: raise NotImplementedError -class NativeDataSetLoader(DataSetLoader): - """A simple example of DataSetLoader - - """ - - def __init__(self): - super(NativeDataSetLoader, self).__init__() - - def load(self, path): - ds = DataSet.read_csv(path, headers=("raw_sentence", "label"), sep="\t") - ds.set_input("raw_sentence") - ds.set_target("label") - return ds - - -DataLoaderRegister.set_reader(NativeDataSetLoader, 'read_naive') - - -class RawDataSetLoader(DataSetLoader): - """A simple example of raw data reader - - """ - - def __init__(self): - super(RawDataSetLoader, self).__init__() - - def load(self, data_path, split=None): - with open(data_path, "r", encoding="utf-8") as f: - lines = f.readlines() - lines = lines if split is None else [l.split(split) for l in lines] - lines = list(filter(lambda x: len(x) > 0, lines)) - return self.convert(lines) - - def convert(self, data): - return convert_seq_dataset(data) - - -DataLoaderRegister.set_reader(RawDataSetLoader, 'read_rawdata') - - -class DummyPOSReader(DataSetLoader): - """A simple reader for a dummy POS tagging dataset. - - In these datasets, each line are divided by "\t". The first Col is the vocabulary and the second - Col is the label. Different sentence are divided by an empty line. - E.g:: - - Tom label1 - and label2 - Jerry label1 - . label3 - (separated by an empty line) - Hello label4 - world label5 - ! label3 - - In this example, there are two sentences "Tom and Jerry ." and "Hello world !". Each word has its own label. - """ - - def __init__(self): - super(DummyPOSReader, self).__init__() - - def load(self, data_path): - """ - :return data: three-level list - Example:: - [ - [ [word_11, word_12, ...], [label_1, label_1, ...] ], - [ [word_21, word_22, ...], [label_2, label_1, ...] ], - ... - ] - """ - with open(data_path, "r", encoding="utf-8") as f: - lines = f.readlines() - data = self.parse(lines) - return self.convert(data) - - @staticmethod - def parse(lines): - data = [] - sentence = [] - for line in lines: - line = line.strip() - if len(line) > 1: - sentence.append(line.split('\t')) - else: - words = [] - labels = [] - for tokens in sentence: - words.append(tokens[0]) - labels.append(tokens[1]) - data.append([words, labels]) - sentence = [] - if len(sentence) != 0: - words = [] - labels = [] - for tokens in sentence: - words.append(tokens[0]) - labels.append(tokens[1]) - data.append([words, labels]) - return data - - def convert(self, data): - """Convert lists of strings into Instances with Fields. - """ - return convert_seq2seq_dataset(data) - - -DataLoaderRegister.set_reader(DummyPOSReader, 'read_pos') - - -class DummyCWSReader(DataSetLoader): - """Load pku dataset for Chinese word segmentation. - """ - def __init__(self): - super(DummyCWSReader, self).__init__() - - def load(self, data_path, max_seq_len=32): - """Load pku dataset for Chinese word segmentation. - CWS (Chinese Word Segmentation) pku training dataset format: - 1. Each line is a sentence. - 2. Each word in a sentence is separated by space. - This function convert the pku dataset into three-level lists with labels . - B: beginning of a word - M: middle of a word - E: ending of a word - S: single character - - :param str data_path: path to the data set. - :param max_seq_len: int, the maximum length of a sequence. If a sequence is longer than it, split it into - several sequences. - :return: three-level lists - """ - assert isinstance(max_seq_len, int) and max_seq_len > 0 - with open(data_path, "r", encoding="utf-8") as f: - sentences = f.readlines() - data = [] - for sent in sentences: - tokens = sent.strip().split() - words = [] - labels = [] - for token in tokens: - if len(token) == 1: - words.append(token) - labels.append("S") - else: - words.append(token[0]) - labels.append("B") - for idx in range(1, len(token) - 1): - words.append(token[idx]) - labels.append("M") - words.append(token[-1]) - labels.append("E") - num_samples = len(words) // max_seq_len - if len(words) % max_seq_len != 0: - num_samples += 1 - for sample_idx in range(num_samples): - start = sample_idx * max_seq_len - end = (sample_idx + 1) * max_seq_len - seq_words = words[start:end] - seq_labels = labels[start:end] - data.append([seq_words, seq_labels]) - return self.convert(data) - - def convert(self, data): - return convert_seq2seq_dataset(data) - - -class DummyClassificationReader(DataSetLoader): - """Loader for a dummy classification data set""" - - def __init__(self): - super(DummyClassificationReader, self).__init__() - - def load(self, data_path): - assert os.path.exists(data_path) - with open(data_path, "r", encoding="utf-8") as f: - lines = f.readlines() - data = self.parse(lines) - return self.convert(data) - - @staticmethod - def parse(lines): - """每行第一个token是标签,其余是字/词;由空格分隔。 - - :param lines: lines from dataset - :return: list(list(list())): the three level of lists are words, sentence, and dataset - """ - dataset = list() - for line in lines: - line = line.strip().split() - label = line[0] - words = line[1:] - if len(words) <= 1: - continue - - sentence = [words, label] - dataset.append(sentence) - return dataset - - def convert(self, data): - return convert_seq2tag_dataset(data) - - -class DummyLMReader(DataSetLoader): - """A Dummy Language Model Dataset Reader - """ - def __init__(self): - super(DummyLMReader, self).__init__() - - def load(self, data_path): - if not os.path.exists(data_path): - raise FileNotFoundError("file {} not found.".format(data_path)) - with open(data_path, "r", encoding="utf=8") as f: - text = " ".join(f.readlines()) - tokens = text.strip().split() - data = self.sentence_cut(tokens) - return self.convert(data) - - def sentence_cut(self, tokens, sentence_length=15): - start_idx = 0 - data_set = [] - for idx in range(len(tokens) // sentence_length): - x = tokens[start_idx * idx: start_idx * idx + sentence_length] - y = tokens[start_idx * idx + 1: start_idx * idx + sentence_length + 1] - if start_idx * idx + sentence_length + 1 >= len(tokens): - # ad hoc - y.extend([""]) - data_set.append([x, y]) - return data_set - - def convert(self, data): - pass - - class PeopleDailyCorpusLoader(DataSetLoader): """人民日报数据集 """ @@ -448,8 +155,9 @@ class PeopleDailyCorpusLoader(DataSetLoader): class ConllLoader: - def __init__(self, headers, indexs=None): + def __init__(self, headers, indexs=None, dropna=True): self.headers = headers + self.dropna = dropna if indexs is None: self.indexs = list(range(len(self.headers))) else: @@ -458,33 +166,10 @@ class ConllLoader: self.indexs = indexs def load(self, path): - datalist = [] - with open(path, 'r', encoding='utf-8') as f: - sample = [] - start = next(f) - if '-DOCSTART-' not in start: - sample.append(start.split()) - for line in f: - if line.startswith('\n'): - if len(sample): - datalist.append(sample) - sample = [] - elif line.startswith('#'): - continue - else: - sample.append(line.split()) - if len(sample) > 0: - datalist.append(sample) - - data = [self.get_one(sample) for sample in datalist] - data = filter(lambda x: x is not None, data) - ds = DataSet() - for sample in data: - ins = Instance() - for name, idx in zip(self.headers, self.indexs): - ins.add_field(field_name=name, field=sample[idx]) - ds.append(ins) + for idx, data in read_conll(path, indexes=self.indexs, dropna=self.dropna): + ins = {h:data[idx] for h, idx in zip(self.headers, self.indexs)} + ds.append(Instance(**ins)) return ds def get_one(self, sample): @@ -499,9 +184,7 @@ class Conll2003Loader(ConllLoader): """Loader for conll2003 dataset More information about the given dataset cound be found on - https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data - - Deprecated. Use ConllLoader for all types of conll-format files. + https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ def __init__(self): headers = [ @@ -510,194 +193,6 @@ class Conll2003Loader(ConllLoader): super(Conll2003Loader, self).__init__(headers=headers) -class SNLIDataSetReader(DataSetLoader): - """A data set loader for SNLI data set. - - """ - def __init__(self): - super(SNLIDataSetReader, self).__init__() - - def load(self, path_list): - """ - - :param list path_list: A list of file name, in the order of premise file, hypothesis file, and label file. - :return: A DataSet object. - """ - assert len(path_list) == 3 - line_set = [] - for file in path_list: - if not os.path.exists(file): - raise FileNotFoundError("file {} NOT found".format(file)) - - with open(file, 'r', encoding='utf-8') as f: - lines = f.readlines() - line_set.append(lines) - - premise_lines, hypothesis_lines, label_lines = line_set - assert len(premise_lines) == len(hypothesis_lines) and len(premise_lines) == len(label_lines) - - data_set = [] - for premise, hypothesis, label in zip(premise_lines, hypothesis_lines, label_lines): - p = premise.strip().split() - h = hypothesis.strip().split() - l = label.strip() - data_set.append([p, h, l]) - - return self.convert(data_set) - - def convert(self, data): - """Convert a 3D list to a DataSet object. - - :param data: A 3D tensor. - Example:: - [ - [ [premise_word_11, premise_word_12, ...], [hypothesis_word_11, hypothesis_word_12, ...], [label_1] ], - [ [premise_word_21, premise_word_22, ...], [hypothesis_word_21, hypothesis_word_22, ...], [label_2] ], - ... - ] - - :return: A DataSet object. - """ - - data_set = DataSet() - - for example in data: - p, h, l = example - # list, list, str - instance = Instance() - instance.add_field("premise", p) - instance.add_field("hypothesis", h) - instance.add_field("truth", l) - data_set.append(instance) - data_set.apply(lambda ins: len(ins["premise"]), new_field_name="premise_len") - data_set.apply(lambda ins: len(ins["hypothesis"]), new_field_name="hypothesis_len") - data_set.set_input("premise", "hypothesis", "premise_len", "hypothesis_len") - data_set.set_target("truth") - return data_set - - -class ConllCWSReader(object): - """Deprecated. Use ConllLoader for all types of conll-format files.""" - def __init__(self): - pass - - def load(self, path, cut_long_sent=False): - """ - 返回的DataSet只包含raw_sentence这个field,内容为str。 - 假定了输入为conll的格式,以空行隔开两个句子,每行共7列,即 - :: - - 1 编者按 编者按 NN O 11 nmod:topic - 2 : : PU O 11 punct - 3 7月 7月 NT DATE 4 compound:nn - 4 12日 12日 NT DATE 11 nmod:tmod - 5 , , PU O 11 punct - - 1 这 这 DT O 3 det - 2 款 款 M O 1 mark:clf - 3 飞行 飞行 NN O 8 nsubj - 4 从 从 P O 5 case - 5 外型 外型 NN O 8 nmod:prep - - """ - datalist = [] - with open(path, 'r', encoding='utf-8') as f: - sample = [] - for line in f: - if line.startswith('\n'): - datalist.append(sample) - sample = [] - elif line.startswith('#'): - continue - else: - sample.append(line.strip().split()) - if len(sample) > 0: - datalist.append(sample) - - ds = DataSet() - for sample in datalist: - # print(sample) - res = self.get_char_lst(sample) - if res is None: - continue - line = ' '.join(res) - if cut_long_sent: - sents = cut_long_sentence(line) - else: - sents = [line] - for raw_sentence in sents: - ds.append(Instance(raw_sentence=raw_sentence)) - return ds - - def get_char_lst(self, sample): - if len(sample) == 0: - return None - text = [] - for w in sample: - t1, t2, t3, t4 = w[1], w[3], w[6], w[7] - if t3 == '_': - return None - text.append(t1) - return text - - -class NaiveCWSReader(DataSetLoader): - """ - 这个reader假设了分词数据集为以下形式, 即已经用空格分割好内容了 - 例如:: - - 这是 fastNLP , 一个 非常 good 的 包 . - - 或者,即每个part后面还有一个pos tag - 例如:: - - 也/D 在/P 團員/Na 之中/Ng ,/COMMACATEGORY - - """ - - def __init__(self, in_word_splitter=None): - super(NaiveCWSReader, self).__init__() - self.in_word_splitter = in_word_splitter - - def load(self, filepath, in_word_splitter=None, cut_long_sent=False): - """ - 允许使用的情况有(默认以\t或空格作为seg) - 这是 fastNLP , 一个 非常 good 的 包 . - 和 - 也/D 在/P 團員/Na 之中/Ng ,/COMMACATEGORY - 如果splitter不为None则认为是第二种情况, 且我们会按splitter分割"也/D", 然后取第一部分. 例如"也/D".split('/')[0] - - :param filepath: - :param in_word_splitter: - :param cut_long_sent: - :return: - """ - if in_word_splitter == None: - in_word_splitter = self.in_word_splitter - dataset = DataSet() - with open(filepath, 'r') as f: - for line in f: - line = line.strip() - if len(line.replace(' ', '')) == 0: # 不能接受空行 - continue - - if not in_word_splitter is None: - words = [] - for part in line.split(): - word = part.split(in_word_splitter)[0] - words.append(word) - line = ' '.join(words) - if cut_long_sent: - sents = cut_long_sentence(line) - else: - sents = [line] - for sent in sents: - instance = Instance(raw_sentence=sent) - dataset.append(instance) - - return dataset - - def cut_long_sentence(sent, max_sample_length=200): """ 将长于max_sample_length的sentence截成多段,只会在有空格的地方发生截断。所以截取的句子可能长于或者短于max_sample_length @@ -727,103 +222,6 @@ def cut_long_sentence(sent, max_sample_length=200): return cutted_sentence -class ZhConllPOSReader(object): - """读取中文Conll格式。返回“字级别”的标签,使用BMES记号扩展原来的词级别标签。 - - Deprecated. Use ConllLoader for all types of conll-format files. - """ - def __init__(self): - pass - - def load(self, path): - """ - 返回的DataSet, 包含以下的field - words:list of str, - tag: list of str, 被加入了BMES tag, 比如原来的序列为['VP', 'NN', 'NN', ..],会被认为是["S-VP", "B-NN", "M-NN",..] - 假定了输入为conll的格式,以空行隔开两个句子,每行共7列,即 - :: - - 1 编者按 编者按 NN O 11 nmod:topic - 2 : : PU O 11 punct - 3 7月 7月 NT DATE 4 compound:nn - 4 12日 12日 NT DATE 11 nmod:tmod - 5 , , PU O 11 punct - - 1 这 这 DT O 3 det - 2 款 款 M O 1 mark:clf - 3 飞行 飞行 NN O 8 nsubj - 4 从 从 P O 5 case - 5 外型 外型 NN O 8 nmod:prep - - """ - datalist = [] - with open(path, 'r', encoding='utf-8') as f: - sample = [] - for line in f: - if line.startswith('\n'): - datalist.append(sample) - sample = [] - elif line.startswith('#'): - continue - else: - sample.append(line.split('\t')) - if len(sample) > 0: - datalist.append(sample) - - ds = DataSet() - for sample in datalist: - # print(sample) - res = self.get_one(sample) - if res is None: - continue - char_seq = [] - pos_seq = [] - for word, tag in zip(res[0], res[1]): - char_seq.extend(list(word)) - if len(word) == 1: - pos_seq.append('S-{}'.format(tag)) - elif len(word) > 1: - pos_seq.append('B-{}'.format(tag)) - for _ in range(len(word) - 2): - pos_seq.append('M-{}'.format(tag)) - pos_seq.append('E-{}'.format(tag)) - else: - raise ValueError("Zero length of word detected.") - - ds.append(Instance(words=char_seq, - tag=pos_seq)) - - return ds - - def get_one(self, sample): - if len(sample) == 0: - return None - text = [] - pos_tags = [] - for w in sample: - t1, t2, t3, t4 = w[1], w[3], w[6], w[7] - if t3 == '_': - return None - text.append(t1) - pos_tags.append(t2) - return text, pos_tags - - -class ConllxDataLoader(ConllLoader): - """返回“词级别”的标签信息,包括词、词性、(句法)头依赖、(句法)边标签。跟``ZhConllPOSReader``完全不同。 - - Deprecated. Use ConllLoader for all types of conll-format files. - """ - def __init__(self): - headers = [ - 'words', 'pos_tags', 'heads', 'labels', - ] - indexs = [ - 1, 3, 6, 7, - ] - super(ConllxDataLoader, self).__init__(headers=headers, indexs=indexs) - - class SSTLoader(DataSetLoader): """load SST data in PTB tree format data source: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip @@ -842,10 +240,7 @@ class SSTLoader(DataSetLoader): """ :param path: str,存储数据的路径 - :return: DataSet。内含field有'words', 'pos_tags', 'heads', 'labels'(parser的label) - 类似于拥有以下结构, 一行为一个instance(sample) - words pos_tags heads labels - ['some', ..] ['NN', ...] [2, 3...] ['nn', 'nn'...] + :return: DataSet。 """ datalist = [] with open(path, 'r', encoding='utf-8') as f: @@ -860,7 +255,6 @@ class SSTLoader(DataSetLoader): @staticmethod def get_one(data, subtree): - from nltk.tree import Tree tree = Tree.fromstring(data) if subtree: return [(t.leaves(), t.label()) for t in tree.subtrees()] @@ -872,26 +266,72 @@ class JsonLoader(DataSetLoader): every line contains a json obj, like a dict fields is the dict key that need to be load """ - def __init__(self, **fields): + def __init__(self, dropna=False, fields=None): super(JsonLoader, self).__init__() - self.fields = {} - for k, v in fields.items(): - self.fields[k] = k if v is None else v + self.dropna = dropna + self.fields = None + self.fields_list = None + if fields: + self.fields = {} + for k, v in fields.items(): + self.fields[k] = k if v is None else v + self.fields_list = list(self.fields.keys()) + + def load(self, path): + ds = DataSet() + for idx, d in read_json(path, fields=self.fields_list, dropna=self.dropna): + ins = {self.fields[k]:v for k,v in d.items()} + ds.append(Instance(**ins)) + return ds + + +class SNLILoader(JsonLoader): + """ + data source: https://nlp.stanford.edu/projects/snli/snli_1.0.zip + """ + def __init__(self): + fields = { + 'sentence1_parse': 'words1', + 'sentence2_parse': 'words2', + 'gold_label': 'target', + } + super(SNLILoader, self).__init__(fields=fields) + + def load(self, path): + ds = super(SNLILoader, self).load(path) + def parse_tree(x): + t = Tree.fromstring(x) + return t.leaves() + ds.apply(lambda ins: parse_tree(ins['words1']), new_field_name='words1') + ds.apply(lambda ins: parse_tree(ins['words2']), new_field_name='words2') + ds.drop(lambda x: x['target'] == '-') + return ds + + +class CSVLoader(DataSetLoader): + """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 + + """ + def __init__(self, headers=None, sep=",", dropna=True): + self.headers = headers + self.sep = sep + self.dropna = dropna def load(self, path): - with open(path, 'r', encoding='utf-8') as f: - datas = [json.loads(l) for l in f] ds = DataSet() - for d in datas: - ins = Instance() - for k, v in d.items(): - if k in self.fields: - ins.add_field(self.fields[k], v) - ds.append(ins) + for idx, data in read_csv(path, headers=self.headers, + sep=self.sep, dropna=self.dropna): + ds.append(Instance(**data)) return ds -def add_seg_tag(data): +def _add_seg_tag(data): """ :param data: list of ([word], [pos], [heads], [head_tags]) diff --git a/fastNLP/io/file_reader.py b/fastNLP/io/file_reader.py new file mode 100644 index 00000000..22766ebb --- /dev/null +++ b/fastNLP/io/file_reader.py @@ -0,0 +1,112 @@ +import json + + +def read_csv(path, encoding='utf-8', headers=None, sep=',', dropna=True): + """ + Construct a generator to read csv items + :param path: file path + :param encoding: file's encoding, default: utf-8 + :param headers: file's headers, if None, make file's first line as headers. default: None + :param sep: separator for each column. default: ',' + :param dropna: weather to ignore and drop invalid data, + if False, raise ValueError when reading invalid data. default: True + :return: generator, every time yield (line number, csv item) + """ + with open(path, 'r', encoding=encoding) as f: + start_idx = 0 + if headers is None: + headers = f.readline().rstrip('\r\n') + headers = headers.split(sep) + start_idx += 1 + elif not isinstance(headers, (list, tuple)): + raise TypeError("headers should be list or tuple, not {}." \ + .format(type(headers))) + for line_idx, line in enumerate(f, start_idx): + contents = line.rstrip('\r\n').split(sep) + if len(contents) != len(headers): + if dropna: + continue + else: + raise ValueError("Line {} has {} parts, while header has {} parts." \ + .format(line_idx, len(contents), len(headers))) + _dict = {} + for header, content in zip(headers, contents): + _dict[header] = content + yield line_idx, _dict + + +def read_json(path, encoding='utf-8', fields=None, dropna=True): + """ + Construct a generator to read json items + :param path: file path + :param encoding: file's encoding, default: utf-8 + :param fields: json object's fields that needed, if None, all fields are needed. default: None + :param dropna: weather to ignore and drop invalid data, + if False, raise ValueError when reading invalid data. default: True + :return: generator, every time yield (line number, json item) + """ + if fields: + fields = set(fields) + with open(path, 'r', encoding=encoding) as f: + for line_idx, line in enumerate(f): + data = json.loads(line) + if fields is None: + yield line_idx, data + continue + _res = {} + for k, v in data.items(): + if k in fields: + _res[k] = v + if len(_res) < len(fields): + if dropna: + continue + else: + raise ValueError('invalid instance at line: {}'.format(line_idx)) + yield line_idx, _res + + +def read_conll(path, encoding='utf-8', indexes=None, dropna=True): + """ + Construct a generator to read conll items + :param path: file path + :param encoding: file's encoding, default: utf-8 + :param indexes: conll object's column indexes that needed, if None, all columns are needed. default: None + :param dropna: weather to ignore and drop invalid data, + if False, raise ValueError when reading invalid data. default: True + :return: generator, every time yield (line number, conll item) + """ + def parse_conll(sample): + sample = list(map(list, zip(*sample))) + sample = [sample[i] for i in indexes] + for f in sample: + if len(f) <= 0: + raise ValueError('empty field') + return sample + with open(path, 'r', encoding=encoding) as f: + sample = [] + start = next(f) + if '-DOCSTART-' not in start: + sample.append(start.split()) + for line_idx, line in enumerate(f, 1): + if line.startswith('\n'): + if len(sample): + try: + res = parse_conll(sample) + sample = [] + yield line_idx, res + except Exception as e: + if dropna: + continue + raise ValueError('invalid instance at line: {}'.format(line_idx)) + elif line.startswith('#'): + continue + else: + sample.append(line.split()) + if len(sample) > 0: + try: + res = parse_conll(sample) + yield line_idx, res + except Exception as e: + if dropna: + return + raise ValueError('invalid instance at line: {}'.format(line_idx)) diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index 48c67a64..04f331f7 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -1,4 +1,6 @@ +import torch import torch.nn as nn +import torch.nn.utils.rnn as rnn from fastNLP.modules.utils import initial_parameter @@ -19,21 +21,44 @@ class LSTM(nn.Module): def __init__(self, input_size, hidden_size=100, num_layers=1, dropout=0.0, batch_first=True, bidirectional=False, bias=True, initial_method=None, get_hidden=False): super(LSTM, self).__init__() + self.batch_first = batch_first self.lstm = nn.LSTM(input_size, hidden_size, num_layers, bias=bias, batch_first=batch_first, dropout=dropout, bidirectional=bidirectional) self.get_hidden = get_hidden initial_parameter(self, initial_method) - def forward(self, x, h0=None, c0=None): + def forward(self, x, seq_lens=None, h0=None, c0=None): if h0 is not None and c0 is not None: - x, (ht, ct) = self.lstm(x, (h0, c0)) + hx = (h0, c0) else: - x, (ht, ct) = self.lstm(x) - if self.get_hidden: - return x, (ht, ct) + hx = None + if seq_lens is not None and not isinstance(x, rnn.PackedSequence): + print('padding') + sort_lens, sort_idx = torch.sort(seq_lens, dim=0, descending=True) + if self.batch_first: + x = x[sort_idx] + else: + x = x[:, sort_idx] + x = rnn.pack_padded_sequence(x, sort_lens, batch_first=self.batch_first) + output, hx = self.lstm(x, hx) # -> [N,L,C] + output, _ = rnn.pad_packed_sequence(output, batch_first=self.batch_first) + _, unsort_idx = torch.sort(sort_idx, dim=0, descending=False) + if self.batch_first: + output = output[unsort_idx] + else: + output = output[:, unsort_idx] else: - return x + output, hx = self.lstm(x, hx) + if self.get_hidden: + return output, hx + return output if __name__ == "__main__": - lstm = LSTM(10) + lstm = LSTM(input_size=2, hidden_size=2, get_hidden=False) + x = torch.randn((3, 5, 2)) + seq_lens = torch.tensor([5,1,2]) + y = lstm(x, seq_lens) + print(x) + print(y) + print(x.size(), y.size(), ) diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 356b157a..4384a680 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -202,25 +202,11 @@ class TestDataSetMethods(unittest.TestCase): self.assertTrue(isinstance(ans, FieldArray)) self.assertEqual(ans.content, [[5, 6]] * 10) - def test_reader(self): - # 跑通即可 - ds = DataSet().read_naive("test/data_for_tests/tutorial_sample_dataset.csv") - self.assertTrue(isinstance(ds, DataSet)) - self.assertTrue(len(ds) > 0) - - ds = DataSet().read_rawdata("test/data_for_tests/people_daily_raw.txt") - self.assertTrue(isinstance(ds, DataSet)) - self.assertTrue(len(ds) > 0) - - ds = DataSet().read_pos("test/data_for_tests/people.txt") - self.assertTrue(isinstance(ds, DataSet)) - self.assertTrue(len(ds) > 0) - - def test_add_null(self): - # TODO test failed because 'fastNLP\core\fieldarray.py:143: RuntimeError' - ds = DataSet() - ds.add_field('test', []) - ds.set_target('test') + # def test_add_null(self): + # # TODO test failed because 'fastNLP\core\fieldarray.py:143: RuntimeError' + # ds = DataSet() + # ds.add_field('test', []) + # ds.set_target('test') class TestDataSetIter(unittest.TestCase): diff --git a/test/data_for_tests/sample_snli.jsonl b/test/data_for_tests/sample_snli.jsonl new file mode 100644 index 00000000..e62856ac --- /dev/null +++ b/test/data_for_tests/sample_snli.jsonl @@ -0,0 +1,3 @@ +{"annotator_labels": ["neutral"], "captionID": "3416050480.jpg#4", "gold_label": "neutral", "pairID": "3416050480.jpg#4r1n", "sentence1": "A person on a horse jumps over a broken down airplane.", "sentence1_binary_parse": "( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) )", "sentence1_parse": "(ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))", "sentence2": "A person is training his horse for a competition.", "sentence2_binary_parse": "( ( A person ) ( ( is ( ( training ( his horse ) ) ( for ( a competition ) ) ) ) . ) )", "sentence2_parse": "(ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (VP (VBG training) (NP (PRP$ his) (NN horse)) (PP (IN for) (NP (DT a) (NN competition))))) (. .)))"} +{"annotator_labels": ["contradiction"], "captionID": "3416050480.jpg#4", "gold_label": "contradiction", "pairID": "3416050480.jpg#4r1c", "sentence1": "A person on a horse jumps over a broken down airplane.", "sentence1_binary_parse": "( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) )", "sentence1_parse": "(ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))", "sentence2": "A person is at a diner, ordering an omelette.", "sentence2_binary_parse": "( ( A person ) ( ( ( ( is ( at ( a diner ) ) ) , ) ( ordering ( an omelette ) ) ) . ) )", "sentence2_parse": "(ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (PP (IN at) (NP (DT a) (NN diner))) (, ,) (S (VP (VBG ordering) (NP (DT an) (NN omelette))))) (. .)))"} +{"annotator_labels": ["entailment"], "captionID": "3416050480.jpg#4", "gold_label": "entailment", "pairID": "3416050480.jpg#4r1e", "sentence1": "A person on a horse jumps over a broken down airplane.", "sentence1_binary_parse": "( ( ( A person ) ( on ( a horse ) ) ) ( ( jumps ( over ( a ( broken ( down airplane ) ) ) ) ) . ) )", "sentence1_parse": "(ROOT (S (NP (NP (DT A) (NN person)) (PP (IN on) (NP (DT a) (NN horse)))) (VP (VBZ jumps) (PP (IN over) (NP (DT a) (JJ broken) (JJ down) (NN airplane)))) (. .)))", "sentence2": "A person is outdoors, on a horse.", "sentence2_binary_parse": "( ( A person ) ( ( ( ( is outdoors ) , ) ( on ( a horse ) ) ) . ) )", "sentence2_parse": "(ROOT (S (NP (DT A) (NN person)) (VP (VBZ is) (ADVP (RB outdoors)) (, ,) (PP (IN on) (NP (DT a) (NN horse)))) (. .)))"} \ No newline at end of file diff --git a/test/io/test_dataset_loader.py b/test/io/test_dataset_loader.py index 16e7d7ea..97379a7d 100644 --- a/test/io/test_dataset_loader.py +++ b/test/io/test_dataset_loader.py @@ -1,8 +1,7 @@ import unittest -from fastNLP.io.dataset_loader import Conll2003Loader, PeopleDailyCorpusLoader, ConllCWSReader, \ - ZhConllPOSReader, ConllxDataLoader - +from fastNLP.io.dataset_loader import Conll2003Loader, PeopleDailyCorpusLoader, \ + CSVLoader, SNLILoader class TestDatasetLoader(unittest.TestCase): @@ -17,11 +16,11 @@ class TestDatasetLoader(unittest.TestCase): def test_PeopleDailyCorpusLoader(self): data_set = PeopleDailyCorpusLoader().load("test/data_for_tests/people_daily_raw.txt") - def test_ConllCWSReader(self): - dataset = ConllCWSReader().load("test/data_for_tests/conll_example.txt") - - def test_ZhConllPOSReader(self): - dataset = ZhConllPOSReader().load("test/data_for_tests/zh_sample.conllx") + def test_CSVLoader(self): + ds = CSVLoader(sep='\t', headers=['words', 'label'])\ + .load('test/data_for_tests/tutorial_sample_dataset.csv') + assert len(ds) > 0 - def test_ConllxDataLoader(self): - dataset = ConllxDataLoader().load("test/data_for_tests/zh_sample.conllx") + def test_SNLILoader(self): + ds = SNLILoader().load('test/data_for_tests/sample_snli.jsonl') + assert len(ds) == 3 From abff8d9daadc160bcc5abe36d6f79b0cef0a7479 Mon Sep 17 00:00:00 2001 From: yunfan Date: Mon, 22 Apr 2019 13:57:28 +0800 Subject: [PATCH 038/173] - fix test_tutorial --- fastNLP/models/snli.py | 2 +- test/test_tutorials.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 5816d2af..901f2dd4 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -110,5 +110,5 @@ class ESIM(BaseModel): def predict(self, words1, words2, seq_len1, seq_len2): prediction = self.forward(words1, words2, seq_len1, seq_len2)['pred'] - return torch.argmax(prediction, dim=-1) + return {'pred': torch.argmax(prediction, dim=-1)} diff --git a/test/test_tutorials.py b/test/test_tutorials.py index bc0b5d2b..600699a3 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -379,6 +379,14 @@ class TestTutorial(unittest.TestCase): dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis') train_data_2[-1], dev_data_2[-1] + for data in [train_data, dev_data, test_data]: + data.rename_field('premise', 'words1') + data.rename_field('hypothesis', 'words2') + data.rename_field('premise_len', 'seq_len1') + data.rename_field('hypothesis_len', 'seq_len2') + data.set_input('words1', 'words2', 'seq_len1', 'seq_len2') + + # step 1:加载模型参数(非必选) from fastNLP.io.config_io import ConfigSection, ConfigLoader args = ConfigSection() From 361a090c26509a21616c06ace42aee5aed423632 Mon Sep 17 00:00:00 2001 From: yh Date: Mon, 22 Apr 2019 14:07:21 +0800 Subject: [PATCH 039/173] =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=A2=9E=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 46 +++++-- fastNLP/core/fieldarray.py | 241 +++++++++++++++++++++---------------- 2 files changed, 172 insertions(+), 115 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 013816a6..41858bdc 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -210,34 +210,62 @@ class DataSet(object): raise KeyError("DataSet has no field named {}.".format(old_name)) def set_target(self, *field_names, flag=True): - """Change the target flag of these fields. + """将field_names的target设置为flag状态 + Example:: - :param field_names: a sequence of str, indicating field names - :param bool flag: Set these fields as target if True. Unset them if False. + dataset.set_target('labels', 'seq_len') # 将labels和seq_len这两个field的target属性设置为True + dataset.set_target('labels', 'seq_lens', flag=False) # 将labels和seq_len的target属性设置为False + + :param field_names: str, field的名称 + :param flag: bool, 将field_name的target状态设置为flag """ + assert isinstance(flag, bool), "Only bool type supported." for name in field_names: if name in self.field_arrays: self.field_arrays[name].is_target = flag else: raise KeyError("{} is not a valid field name.".format(name)) - def set_input(self, *field_name, flag=True): - """Set the input flag of these fields. + def set_input(self, *field_names, flag=True): + """将field_name的input设置为flag状态 + Example:: + + dataset.set_input('words', 'seq_len') # 将words和seq_len这两个field的input属性设置为True + dataset.set_input('words', flag=False) # 将words这个field的input属性设置为False - :param field_name: a sequence of str, indicating field names. - :param bool flag: Set these fields as input if True. Unset them if False. + :param field_names: str, field的名称 + :param flag: bool, 将field_name的input状态设置为flag """ - for name in field_name: + for name in field_names: if name in self.field_arrays: 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_names的ignore_type设置为flag状态 + + :param field_names: str, field的名称 + :param flag: bool, + :return: + """ + assert isinstance(flag, bool), "Only bool type supported." + for name in field_names: + if name in self.field_arrays: + 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 + Example:: + + from fastNLP import EngChar2DPadder + padder = EngChar2DPadder() + dataset.set_padder('chars', padder) # 则chars这个field会使用EngChar2DPadder进行pad操作 :param field_name: str, 设置field的padding方式为padder - :param padder: PadderBase类型或None. 设置为None即删除padder。即对该field不进行padding操作. + :param padder: (None, PadderBase). 设置为None即删除padder, 即对该field不进行padding操作. :return: """ if field_name not in self.field_arrays: diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index c9caea56..3d0fb582 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -1,98 +1,6 @@ import numpy as np from copy import deepcopy -class PadderBase: - """ - 所有padder都需要继承这个类,并覆盖__call__()方法。 - 用于对batch进行padding操作。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前deepcopy一份。 - """ - def __init__(self, pad_val=0, **kwargs): - self.pad_val = pad_val - - def set_pad_val(self, pad_val): - self.pad_val = pad_val - - def __call__(self, contents, field_name, field_ele_dtype): - """ - 传入的是List内容。假设有以下的DataSet。 - from fastNLP import DataSet - from fastNLP import Instance - dataset = DataSet() - dataset.append(Instance(word='this is a demo', length=4, - chars=[['t', 'h', 'i', 's'], ['i', 's'], ['a'], ['d', 'e', 'm', 'o']])) - dataset.append(Instance(word='another one', length=2, - chars=[['a', 'n', 'o', 't', 'h', 'e', 'r'], ['o', 'n', 'e']])) - # 如果batch_size=2, 下面只是用str的方式看起来更直观一点,但实际上可能word和chars在pad时都已经为index了。 - word这个field的pad_func会接收到的内容会是 - [ - 'this is a demo', - 'another one' - ] - length这个field的pad_func会接收到的内容会是 - [4, 2] - chars这个field的pad_func会接收到的内容会是 - [ - [['t', 'h', 'i', 's'], ['i', 's'], ['a'], ['d', 'e', 'm', 'o']], - [['a', 'n', 'o', 't', 'h', 'e', 'r'], ['o', 'n', 'e']] - ] - 即把每个instance中某个field的内容合成一个List传入 - :param contents: List[element]。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前 - deepcopy一份。 - :param field_name: str, field的名称,帮助定位错误 - :param field_ele_dtype: np.int64, np.float64, np.str. 该field的内层list元素的类型。辅助判断是否pad,大多数情况用不上 - :return: List[padded_element]或np.array([padded_element]) - """ - raise NotImplementedError - - -class AutoPadder(PadderBase): - """ - 根据contents的数据自动判定是否需要做padding。 - - 1 如果元素类型(元素类型是指field中最里层List的元素的数据类型, 可以通过FieldArray.dtype查看,比如['This', 'is', ...]的元素类 - 型为np.str, [[1,2], ...]的元素类型为np.int64)的数据不为(np.int64, np.float64)则不会进行padding - - 2 如果元素类型为(np.int64, np.float64), - - 2.1 如果该field的内容只有一个,比如为sequence_length, 则不进行padding - - 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 - 如果某个instance中field为[1, 2, 3],则可以pad; 若为[[1,2], [3,4, ...]]则不能进行pad - """ - def __init__(self, pad_val=0): - """ - :param pad_val: int, padding的位置使用该index - """ - super().__init__(pad_val=pad_val) - - def _is_two_dimension(self, contents): - """ - 判断contents是不是只有两个维度。[[1,2], [3]]是两个维度. [[[1,2], [3, 4, 5]], [[4,5]]]有三个维度 - :param contents: - :return: - """ - value = contents[0] - if isinstance(value , (np.ndarray, list)): - value = value[0] - if isinstance(value, (np.ndarray, list)): - return False - return True - return False - - def __call__(self, contents, field_name, field_ele_dtype): - if not is_iterable(contents[0]): - array = np.array([content for content in contents], dtype=field_ele_dtype) - elif field_ele_dtype in (np.int64, np.float64) and self._is_two_dimension(contents): - max_len = max([len(content) for content in contents]) - array = np.full((len(contents), max_len), self.pad_val, dtype=field_ele_dtype) - for i, content in enumerate(contents): - array[i][:len(content)] = content - elif field_ele_dtype is None: - array = np.array(contents) # 当ignore_type=True时,直接返回contents - else: # should only be str - array = np.array([content for content in contents]) - return array - class FieldArray(object): """``FieldArray`` is the collection of ``Instance``s of the same field. @@ -336,18 +244,18 @@ class FieldArray(object): self.content.append(val) def __getitem__(self, indices): - return self.get(indices) + return self.get(indices, pad=False) def __setitem__(self, idx, val): assert isinstance(idx, int) self.content[idx] = val def get(self, indices, pad=True): - """Fetch instances based on indices. + """根据给定的indices返回内容 - :param indices: an int, or a list of int. - :param pad: bool, 是否对返回的结果进行padding。 - :return: + :param indices: (int, List[int]), 获取indices对应的内容。 + :param pad: bool, 是否对返回的结果进行padding。仅对indices为List[int]时有效 + :return: (single, List) """ if isinstance(indices, int): return self.content[indices] @@ -362,14 +270,16 @@ class FieldArray(object): def set_padder(self, padder): """ - 设置padding方式 + 设置padder,在这个field进行pad的时候用这个padder进行pad,如果为None则不进行pad。 - :param padder: PadderBase类型或None. 设置为None即删除padder. + :param padder: (None, PadderBase). 设置为None即删除padder. :return: """ if padder is not None: assert isinstance(padder, PadderBase), "padder must be of type PadderBase." - self.padder = deepcopy(padder) + self.padder = deepcopy(padder) + else: + self.padder = None def set_pad_val(self, pad_val): """修改padder的pad_val. @@ -391,7 +301,7 @@ class FieldArray(object): def to(self, other): """ - 将other的属性复制给本FieldArray(other必须为FieldArray类型). 包含 is_input, is_target, padder, ignore_type + 将other的属性复制给本FieldArray(other必须为FieldArray类型).属性包括 is_input, is_target, padder, ignore_type :param other: FieldArray :return: @@ -413,17 +323,136 @@ def is_iterable(content): return True +class PadderBase: + """ + 所有padder都需要继承这个类,并覆盖__call__()方法。 + 用于对batch进行padding操作。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前deepcopy一份。 + """ + + def __init__(self, pad_val=0, **kwargs): + self.pad_val = pad_val + + def set_pad_val(self, pad_val): + self.pad_val = pad_val + + def __call__(self, contents, field_name, field_ele_dtype): + """ + 传入的是List内容。假设有以下的DataSet。 + + :param contents: List[element]。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前 + deepcopy一份。 + :param field_name: str, field的名称。 + :param field_ele_dtype: (np.int64, np.float64, np.str, None), 该field的内层元素的类型。如果该field的ignore_type + 为True,该这个值为None。 + :return: np.array([padded_element]) + + Example:: + + from fastNLP import DataSet + from fastNLP import Instance + dataset = DataSet() + dataset.append(Instance(sent='this is a demo', length=4, + chars=[['t', 'h', 'i', 's'], ['i', 's'], ['a'], ['d', 'e', 'm', 'o']])) + dataset.append(Instance(sent='another one', length=2, + chars=[['a', 'n', 'o', 't', 'h', 'e', 'r'], ['o', 'n', 'e']])) + 如果调用 + batch = dataset.get([0,1], pad=True) + sent这个field的padder的__call__会接收到的内容会是 + [ + 'this is a demo', + 'another one' + ] + + length这个field的padder的__call__会接收到的内容会是 + [4, 2] + + chars这个field的padder的__call__会接收到的内容会是 + [ + [['t', 'h', 'i', 's'], ['i', 's'], ['a'], ['d', 'e', 'm', 'o']], + [['a', 'n', 'o', 't', 'h', 'e', 'r'], ['o', 'n', 'e']] + ] + + 即把每个instance中某个field的内容合成一个List传入 + + """ + raise NotImplementedError + + +class AutoPadder(PadderBase): + """ + 根据contents的数据自动判定是否需要做padding。 + + 1 如果元素类型(元素类型是指field中最里层元素的数据类型, 可以通过FieldArray.dtype查看,比如['This', 'is', ...]的元素类 + 型为np.str, [[1,2], ...]的元素类型为np.int64)的数据不为(np.int64, np.float64)则不会进行pad + + 2 如果元素类型为(np.int64, np.float64), + + 2.1 如果该field的内容为(np.int64, np.float64),比如为seq_len, 则不进行padding + + 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 + 如果某个instance中field为[1, 2, 3],则可以pad;若为[[1,2], [3,4, ...]]则不能进行pad + """ + + def __init__(self, pad_val=0): + """ + :param pad_val: int, padding的位置使用该index + """ + super().__init__(pad_val=pad_val) + + def _is_two_dimension(self, contents): + """ + 判断contents是不是只有两个维度。[[1,2], [3]]是两个维度. [[[1,2], [3, 4, 5]], [[4,5]]]有三个维度 + :param contents: + :return: + """ + value = contents[0] + if isinstance(value, (np.ndarray, list)): + value = value[0] + if isinstance(value, (np.ndarray, list)): + return False + return True + return False + + def __call__(self, contents, field_name, field_ele_dtype): + if not is_iterable(contents[0]): + array = np.array([content for content in contents], dtype=field_ele_dtype) + elif field_ele_dtype in (np.int64, np.float64) and self._is_two_dimension(contents): + max_len = max([len(content) for content in contents]) + array = np.full((len(contents), max_len), self.pad_val, dtype=field_ele_dtype) + for i, content in enumerate(contents): + array[i][:len(content)] = content + elif field_ele_dtype is None: + array = np.array(contents) # 当ignore_type=True时,直接返回contents + else: # should only be str + array = np.array([content for content in contents]) + return array + + class EngChar2DPadder(PadderBase): """ - 用于为英语执行character级别的2D padding操作。对应的field内容应该为[['T', 'h', 'i', 's'], ['a'], ['d', 'e', 'm', 'o']](这里为 - 了更直观,把它们写为str,但实际使用时它们应该是character的index)。 - padded过后的batch内容,形状为(batch_size, max_sentence_length, max_word_length). max_sentence_length最大句子长度。 - max_word_length最长的word的长度 + 用于为英语执行character级别的2D padding操作。对应的field内容应该类似[['T', 'h', 'i', 's'], ['a'], ['d', 'e', 'm', 'o']], + 但这个Padder只能处理index为int的情况。 + padded过后的batch内容,形状为(batch_size, max_sentence_length, max_word_length). max_sentence_length为这个batch中最大句 + 子长度;max_word_length为这个batch中最长的word的长度 + + Example:: + + from fastNLP import DataSet + from fastNLP import EnChar2DPadder + from fastNLP import Vocabulary + dataset = DataSet({'sent': ['This is the first demo', 'This is the second demo']}) + dataset.apply(lambda ins:[list(word) for word in ins['sent'].split()], new_field_name='chars') + vocab = Vocabulary() + vocab.from_dataset(dataset, field_name='chars') + vocab.index_dataset(dataset, field_name='chars') + dataset.set_input('chars') + padder = EnChar2DPadder() + dataset.set_padder('chars', padder) # chars这个field的设置为了EnChar2DPadder """ def __init__(self, pad_val=0, pad_length=0): """ - :param pad_val: int, padding的位置使用该index + :param pad_val: int, pad的位置使用该index :param pad_length: int, 如果为0则取一个batch中最大的单词长度作为padding长度。如果为大于0的数,则将所有单词的长度都pad或截 取到该长度. """ From 2c202bb1516d7d49c24cacca98c38c6a35c46583 Mon Sep 17 00:00:00 2001 From: yh Date: Mon, 22 Apr 2019 17:18:28 +0800 Subject: [PATCH 040/173] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.io.rst | 8 ++ docs/source/fastNLP.modules.decoder.rst | 8 ++ fastNLP/core/dataset.py | 168 ++++++++++++++---------- fastNLP/core/fieldarray.py | 20 +-- test/core/test_dataset.py | 5 + 5 files changed, 133 insertions(+), 76 deletions(-) diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index bb30c5e7..e73f27d3 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -36,6 +36,14 @@ fastNLP.io.embed\_loader module :undoc-members: :show-inheritance: +fastNLP.io.file\_reader module +------------------------------ + +.. automodule:: fastNLP.io.file_reader + :members: + :undoc-members: + :show-inheritance: + fastNLP.io.model\_io module --------------------------- diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index 25602b2c..60706b06 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -20,6 +20,14 @@ fastNLP.modules.decoder.MLP module :undoc-members: :show-inheritance: +fastNLP.modules.decoder.utils module +------------------------------------ + +.. automodule:: fastNLP.modules.decoder.utils + :members: + :undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 5bf32d02..3a4dfa55 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -1,3 +1,18 @@ +""" +fastNLP.core.DataSet的介绍文档 + +DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格,每一行是一个instance(或sample),每一列是一个feature。 + +csv-table:: +:header: "Field1", "Field2", "Field3" +:widths:20, 10, 10 + +"This is the first instance", ['This', 'is', 'the', 'first', 'instance'], 5 +"Second instance", ['Second', 'instance'], 2 + +""" + + import _pickle as pickle import numpy as np @@ -31,7 +46,7 @@ class DataSet(object): length_set.add(len(value)) assert len(length_set) == 1, "Arrays must all be same length." for key, value in data.items(): - self.add_field(name=key, fields=value) + self.add_field(field_name=key, fields=value) elif isinstance(data, list): for ins in data: assert isinstance(ins, Instance), "Must be Instance type, not {}.".format(type(ins)) @@ -88,7 +103,7 @@ class DataSet(object): 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(name=field.name, fields=field.content[idx], padder=field.padder, + data_set.add_field(field_name=field.name, fields=field.content[idx], padder=field.padder, is_input=field.is_input, is_target=field.is_target, ignore_type=field.ignore_type) return data_set elif isinstance(idx, str): @@ -131,7 +146,7 @@ class DataSet(object): return "DataSet(" + self.__inner_repr__() + ")" def append(self, ins): - """Add an instance to the DataSet. + """将一个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 ins: an Instance object @@ -151,57 +166,60 @@ class DataSet(object): assert name in self.field_arrays self.field_arrays[name].append(field) - def add_field(self, name, fields, padder=None, is_input=False, is_target=False, ignore_type=False): - """Add a new field to the DataSet. + def add_field(self, field_name, fields, padder=AutoPadder(), is_input=False, is_target=False, ignore_type=False): + """新增一个field - :param str name: the name of the field. - :param fields: a list of int, float, or other objects. - :param padder: PadBase对象,如何对该Field进行padding。如果为None则使用 - :param bool is_input: whether this field is model input. - :param bool is_target: whether this field is label or target. - :param bool ignore_type: If True, do not perform type check. (Default: False) + :param str field_name: 新增的field的名称 + :param list fields: 需要新增的field的内容 + :param None, Padder padder: 如果为None,则不进行pad。 + :param bool is_input: 新加入的field是否是input + :param bool is_target: 新加入的field是否是target + :param bool ignore_type: 是否忽略对新加入的field的类型检查 """ - if padder is None: - padder = AutoPadder(pad_val=0) if len(self.field_arrays) != 0: if len(self) != len(fields): raise RuntimeError(f"The field to append must have the same size as dataset. " f"Dataset size {len(self)} != field size {len(fields)}") - self.field_arrays[name] = FieldArray(name, fields, is_target=is_target, is_input=is_input, - padder=padder, ignore_type=ignore_type) + self.field_arrays[field_name] = FieldArray(field_name, fields, is_target=is_target, is_input=is_input, + padder=padder, ignore_type=ignore_type) - def delete_field(self, name): - """Delete a field based on the field name. + def delete_field(self, field_name): + """删除field - :param name: the name of the field to be deleted. + :param str field_name: 需要删除的field的名称. """ - self.field_arrays.pop(name) + self.field_arrays.pop(field_name) def get_field(self, field_name): + """获取field_name这个field + + :param str field_name: field的名称 + :return: 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): - """Return all the fields with their names. + """返回一个dict,key为field_name, value为对应的FieldArray - :return field_arrays: the internal data structure of DataSet. + :return: dict: """ return self.field_arrays def get_length(self): - """Fetch the length of the dataset. + """获取DataSet的元素数量 - :return length: + :return: int length: """ return len(self) def rename_field(self, old_name, new_name): - """Rename a field. + """将某个field重新命名. - :param str old_name: - :param str 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) @@ -216,8 +234,8 @@ class DataSet(object): dataset.set_target('labels', 'seq_len') # 将labels和seq_len这两个field的target属性设置为True dataset.set_target('labels', 'seq_lens', flag=False) # 将labels和seq_len的target属性设置为False - :param field_names: str, field的名称 - :param flag: bool, 将field_name的target状态设置为flag + :param str field_names: field的名称 + :param bool flag: 将field_name的target状态设置为flag """ assert isinstance(flag, bool), "Only bool type supported." for name in field_names: @@ -233,8 +251,8 @@ class DataSet(object): dataset.set_input('words', 'seq_len') # 将words和seq_len这两个field的input属性设置为True dataset.set_input('words', flag=False) # 将words这个field的input属性设置为False - :param field_names: str, field的名称 - :param flag: bool, 将field_name的input状态设置为flag + :param str field_names: field的名称 + :param bool flag: 将field_name的input状态设置为flag """ for name in field_names: if name in self.field_arrays: @@ -245,8 +263,8 @@ class DataSet(object): def set_ignore_type(self, *field_names, flag=True): """将field_names的ignore_type设置为flag状态 - :param field_names: str, field的名称 - :param flag: bool, + :param str field_names: field的名称 + :param bool flag: 将field_name的ignore_type状态设置为flag :return: """ assert isinstance(flag, bool), "Only bool type supported." @@ -264,8 +282,8 @@ class DataSet(object): padder = EngChar2DPadder() dataset.set_padder('chars', padder) # 则chars这个field会使用EngChar2DPadder进行pad操作 - :param field_name: str, 设置field的padding方式为padder - :param padder: (None, PadderBase). 设置为None即删除padder, 即对该field不进行padding操作. + :param str field_name: 设置field的padding方式为padder + :param None, Padder padder: 设置为None即删除padder, 即对该field不进行pad操作. :return: """ if field_name not in self.field_arrays: @@ -275,8 +293,8 @@ class DataSet(object): def set_pad_val(self, field_name, pad_val): """为某个field设置对应的pad_val. - :param field_name: str,修改该field的pad_val - :param pad_val: int,该field的padder会以pad_val作为padding index + :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: @@ -286,7 +304,7 @@ class DataSet(object): def get_input_name(self): """返回所有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] @@ -300,15 +318,22 @@ class DataSet(object): def apply_field(self, func, field_name, new_field_name=None, **kwargs): """将DataSet中的每个instance中的`field_name`这个field传给func,并获取它的返回值. - :param func: Callable, input是instance的`field_name`这个field. - :param field_name: str, 传入func的是哪个field. - :param new_field_name: (str, None). 如果不是None,将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有 - 的field相同,则覆盖之前的field. - :param **kwargs: 合法的参数有以下三个 - (1) is_input: bool, 如果为True则将`new_field_name`这个field设置为input - (2) is_target: bool, 如果为True则将`new_field_name`这个field设置为target - (3) ignore_type: bool, 如果为True则将`new_field_name`这个field的ignore_type设置为true, 忽略其类型 - :return: List[], 里面的元素为func的返回值,所以list长度为DataSet的长度 + :param callable func: input是instance的`field_name`这个field. + :param str field_name: 传入func的是哪个field. + :param str, None new_field_name: 将func返回的内容放入到什么field中 + + 1. str, 将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有的field相 + 同,则覆盖之前的field + + 2. None, 不创建新的field + :param kwargs: 合法的参数有以下三个 + + 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input + + 2. is_target: bool, 如果为True则将`new_field_name`的field设置为target + + 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()." @@ -334,9 +359,9 @@ class DataSet(object): def _add_apply_field(self, results, new_field_name, kwargs): """将results作为加入到新的field中,field名称为new_field_name - :param results: List[], 一般是apply*()之后的结果 - :param new_field_name: str, 新加入的field的名称 - :param kwargs: dict, 用户apply*()时传入的自定义参数 + :param list(str) results: 一般是apply*()之后的结果 + :param str new_field_name: 新加入的field的名称 + :param dict kwargs: 用户apply*()时传入的自定义参数 :return: """ extra_param = {} @@ -355,23 +380,30 @@ class DataSet(object): extra_param['is_target'] = old_field.is_target if 'ignore_type' not in extra_param: extra_param['ignore_type'] = old_field.ignore_type - self.add_field(name=new_field_name, fields=results, is_input=extra_param["is_input"], + self.add_field(field_name=new_field_name, fields=results, is_input=extra_param["is_input"], is_target=extra_param["is_target"], ignore_type=extra_param['ignore_type']) else: - self.add_field(name=new_field_name, fields=results, is_input=extra_param.get("is_input", None), + 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中,并获取它的返回值. - :param func: Callable, 参数是DataSet中的instance - :param new_field_name: (None, str). (1) None, 不创建新的field; (2) str,将func的返回值放入这个名为 - `new_field_name`的新field中,如果名称与已有的field相同,则覆盖之前的field; + :param callable func: 参数是DataSet中的instance + :param str, None new_field_name: 将func返回的内容放入到什么field中 + + 1. str, 将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有的field相 + 同,则覆盖之前的field + + 2. None, 不创建新的field :param kwargs: 合法的参数有以下三个 - (1) is_input: bool, 如果为True则将`new_field_name`的field设置为input - (2) is_target: bool, 如果为True则将`new_field_name`的field设置为target - (3) ignore_type: bool, 如果为True则将`new_field_name`的field的ignore_type设置为true, 忽略其类型 + + 1. is_input: bool, 如果为True则将`new_field_name`的field设置为input + + 2. is_target: bool, 如果为True则将`new_field_name`的field设置为target + + 3. ignore_type: bool, 如果为True则将`new_field_name`的field的ignore_type设置为true, 忽略其类型 :return: List[], 里面的元素为func的返回值,所以list长度为DataSet的长度 """ assert len(self)!=0, "Null DataSet cannot use apply()." @@ -396,10 +428,10 @@ class DataSet(object): def drop(self, func, inplace=True): """func接受一个instance,返回bool值,返回值为True时,该instance会被删除。 - :param func: Callable, 接受一个instance作为参数,返回bool值。为True时删除该instance - :param inplace: bool, 是否在当前DataSet中直接删除instance。如果为False,返回值为一个删除了相应instance的新的DataSet + :param callable func: 接受一个instance作为参数,返回bool值。为True时删除该instance + :param bool inplace: 是否在当前DataSet中直接删除instance。如果为False,返回值为一个删除了相应instance的新的DataSet - :return: DataSet. + :return: DataSet """ if inplace: results = [ins for ins in self._inner_iter() if not func(ins)] @@ -408,16 +440,16 @@ class DataSet(object): return self else: results = [ins for ins in self if not func(ins)] - data = DataSet(results) + dataset = DataSet(results) for field_name, field in self.field_arrays.items(): - data.field_arrays[field_name].to(field) - return data + dataset.field_arrays[field_name].to(field) + return dataset def split(self, ratio): """将DataSet按照ratio的比例拆分,返回两个DataSet - :param ratio: float, 0 Date: Mon, 22 Apr 2019 17:22:23 +0800 Subject: [PATCH 041/173] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/embed_loader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 5ad27c53..c80e8b5f 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -170,6 +170,7 @@ class EmbedLoader(BaseLoader): if error == 'ignore': warnings.warn("Error occurred at the {} line.".format(idx)) else: + print("Error occurred at the {} line.".format(idx)) raise e total_hits = sum(hit_flags) print("Found {} out of {} words in the pre-training embedding.".format(total_hits, len(vocab))) @@ -234,11 +235,11 @@ class EmbedLoader(BaseLoader): warnings.warn("Error occurred at the {} line.".format(idx)) pass else: + print("Error occurred at the {} line.".format(idx)) raise e if dim==-1: raise RuntimeError("{} is an empty file.".format(embed_filepath)) matrix = np.random.randn(len(vocab), dim).astype(dtype) - # TODO 需要保证unk其它数据同分布的吗? if (unknown is not None and not found_unknown) or (padding is not None and not found_pad): start_idx = 0 if padding is not None: From 95aabc941d9283638ce739926f436a3b79c7615b Mon Sep 17 00:00:00 2001 From: yh_cc Date: Mon, 22 Apr 2019 17:26:55 +0800 Subject: [PATCH 042/173] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../models/cws_transformer.py | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/reproduction/Chinese_word_segmentation/models/cws_transformer.py b/reproduction/Chinese_word_segmentation/models/cws_transformer.py index 736edade..375eaa14 100644 --- a/reproduction/Chinese_word_segmentation/models/cws_transformer.py +++ b/reproduction/Chinese_word_segmentation/models/cws_transformer.py @@ -8,7 +8,8 @@ from torch import nn import torch -from fastNLP.modules.encoder.transformer import TransformerEncoder +# from fastNLP.modules.encoder.transformer import TransformerEncoder +from reproduction.Chinese_word_segmentation.models.transformer import TransformerEncoder from fastNLP.modules.decoder.CRF import ConditionalRandomField,seq_len_to_byte_mask from fastNLP.modules.decoder.CRF import allowed_transitions @@ -27,11 +28,83 @@ class TransformerCWS(nn.Module): self.fc1 = nn.Linear(input_size, hidden_size) - value_size = hidden_size//num_heads - self.transformer = TransformerEncoder(num_layers, model_size=hidden_size, inner_size=hidden_size, - key_size=value_size, - value_size=value_size, num_head=num_heads) + # value_size = hidden_size//num_heads + # self.transformer = TransformerEncoder(num_layers, model_size=hidden_size, inner_size=hidden_size, + # key_size=value_size, + # value_size=value_size, num_head=num_heads) + self.transformer = TransformerEncoder(num_layers=num_layers, model_size=hidden_size, num_heads=num_heads, + hidden_size=hidden_size) + self.fc2 = nn.Linear(hidden_size, tag_size) + + allowed_trans = allowed_transitions({0:'b', 1:'m', 2:'e', 3:'s'}, encoding_type='bmes') + self.crf = ConditionalRandomField(num_tags=tag_size, include_start_end_trans=False, + allowed_transitions=allowed_trans) + + def forward(self, chars, target, seq_lens, bigrams=None): + masks = seq_len_to_byte_mask(seq_lens) + x = self.embedding(chars) + batch_size = x.size(0) + length = x.size(1) + if hasattr(self, 'bigram_embedding'): + bigrams = self.bigram_embedding(bigrams) # batch_size x seq_lens x per_char x embed_size + x = torch.cat([x, bigrams.view(batch_size, length, -1)], dim=-1) + self.drop(x) + x = self.fc1(x) + feats = self.transformer(x, masks) + feats = self.fc2(feats) + losses = self.crf(feats, target, masks.float()) + + pred_dict = {} + pred_dict['seq_lens'] = seq_lens + pred_dict['loss'] = torch.mean(losses) + + return pred_dict + def predict(self, chars, seq_lens, bigrams=None): + masks = seq_len_to_byte_mask(seq_lens) + + x = self.embedding(chars) + batch_size = x.size(0) + length = x.size(1) + if hasattr(self, 'bigram_embedding'): + bigrams = self.bigram_embedding(bigrams) # batch_size x seq_lens x per_char x embed_size + x = torch.cat([x, bigrams.view(batch_size, length, -1)], dim=-1) + self.drop(x) + x = self.fc1(x) + feats = self.transformer(x, masks) + feats = self.fc2(feats) + + probs = self.crf.viterbi_decode(feats, masks, get_score=False) + + return {'pred': probs, 'seq_lens':seq_lens} + + +from reproduction.Chinese_word_segmentation.models.dilated_transformer import TransformerDilateEncoder + +class TransformerDilatedCWS(nn.Module): + def __init__(self, vocab_num, embed_dim=100, bigram_vocab_num=None, bigram_embed_dim=100, num_bigram_per_char=None, + embed_drop_p=0.3, hidden_size=200, kernel_size=3, dilate='none', + num_layers=1, num_heads=8, tag_size=4, + relative_pos_embed_dim=0): + super().__init__() + + self.embedding = nn.Embedding(vocab_num, embed_dim) + input_size = embed_dim + if bigram_vocab_num: + self.bigram_embedding = nn.Embedding(bigram_vocab_num, bigram_embed_dim) + input_size += num_bigram_per_char*bigram_embed_dim + + self.drop = nn.Dropout(embed_drop_p, inplace=True) + + self.fc1 = nn.Linear(input_size, hidden_size) + + # value_size = hidden_size//num_heads + # self.transformer = TransformerEncoder(num_layers, model_size=hidden_size, inner_size=hidden_size, + # key_size=value_size, + # value_size=value_size, num_head=num_heads) + self.transformer = TransformerDilateEncoder(num_layers=num_layers, model_size=hidden_size, num_heads=num_heads, + hidden_size=hidden_size, kernel_size=kernel_size, dilate=dilate, + relative_pos_embed_dim=relative_pos_embed_dim) self.fc2 = nn.Linear(hidden_size, tag_size) allowed_trans = allowed_transitions({0:'b', 1:'m', 2:'e', 3:'s'}, encoding_type='bmes') @@ -39,7 +112,7 @@ class TransformerCWS(nn.Module): allowed_transitions=allowed_trans) def forward(self, chars, target, seq_lens, bigrams=None): - masks = seq_len_to_byte_mask(seq_lens).float() + masks = seq_len_to_byte_mask(seq_lens) x = self.embedding(chars) batch_size = x.size(0) length = x.size(1) @@ -59,7 +132,7 @@ class TransformerCWS(nn.Module): return pred_dict def predict(self, chars, seq_lens, bigrams=None): - masks = seq_len_to_byte_mask(seq_lens).float() + masks = seq_len_to_byte_mask(seq_lens) x = self.embedding(chars) batch_size = x.size(0) @@ -77,6 +150,7 @@ class TransformerCWS(nn.Module): return {'pred': probs, 'seq_lens':seq_lens} + class NoamOpt(torch.optim.Optimizer): "Optim wrapper that implements rate." @@ -107,10 +181,7 @@ class NoamOpt(torch.optim.Optimizer): (self.model_size ** (-0.5) * min(step ** (-0.5), step * self.warmup ** (-1.5))) - -if __name__ == '__main__': - - +def TransformerCWS_test(): transformer = TransformerCWS(10, embed_dim=100, bigram_vocab_num=10, bigram_embed_dim=100, num_bigram_per_char=8, hidden_size=200, embed_drop_p=0.3, num_layers=1, num_heads=8, tag_size=4) chars = torch.randint(10, size=(4, 7)).long() @@ -122,4 +193,7 @@ if __name__ == '__main__': optimizer = torch.optim.Adam(transformer.parameters()) - opt = NoamOpt(10 ,1, 400, optimizer) \ No newline at end of file + opt = NoamOpt(10 ,1, 400, optimizer) + +if __name__ == '__main__': + TransformerCWS_test() From f3e4abfee42c5afad4d37845c7bf64816c1fb41f Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 23 Apr 2019 12:58:43 +0800 Subject: [PATCH 043/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E7=9A=84=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Makefile b/docs/Makefile index 6f2f2821..6a2ed12a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,7 +14,7 @@ help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) apidoc: - @$(SPHINXAPIDOC) -f -o source ../fastNLP + @$(SPHINXAPIDOC) -fM -o source ../fastNLP server: cd build/html && python -m http.server From 001586fa3ec28a565026dee6532062eba6e18b26 Mon Sep 17 00:00:00 2001 From: yunfan Date: Tue, 23 Apr 2019 14:20:17 +0800 Subject: [PATCH 044/173] - add document --- fastNLP/api/api.py | 4 +- fastNLP/core/__init__.py | 2 +- fastNLP/core/batch.py | 62 ++++--- fastNLP/core/dataset.py | 2 +- fastNLP/core/sampler.py | 59 ++---- fastNLP/core/trainer.py | 6 +- fastNLP/core/vocabulary.py | 132 +++++++++----- fastNLP/io/dataset_loader.py | 120 ++++++++----- fastNLP/io/file_reader.py | 21 ++- fastNLP/models/biaffine_parser.py | 188 ++++++++++++++------ fastNLP/models/star_transformer.py | 155 +++++++++++++--- fastNLP/modules/encoder/lstm.py | 36 ++-- fastNLP/modules/encoder/star_transformer.py | 47 ++--- fastNLP/modules/encoder/transformer.py | 17 +- fastNLP/modules/encoder/variational_rnn.py | 95 +++++++--- test/core/test_sampler.py | 8 +- test/models/test_biaffine_parser.py | 20 +-- test/test_tutorials.py | 8 +- 18 files changed, 655 insertions(+), 327 deletions(-) diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index 24a1ab1d..c4c21832 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -47,7 +47,7 @@ from fastNLP.core.dataset import DataSet from fastNLP.api.utils import load_url from fastNLP.api.processor import ModelProcessor -from fastNLP.io.dataset_loader import cut_long_sentence, ConllLoader +from fastNLP.io.dataset_loader import _cut_long_sentence, ConllLoader from fastNLP.core.instance import Instance from fastNLP.api.pipeline import Pipeline from fastNLP.core.metrics import SpanFPreRecMetric @@ -107,7 +107,7 @@ class ConllCWSReader(object): continue line = ' '.join(res) if cut_long_sent: - sents = cut_long_sentence(line) + sents = _cut_long_sentence(line) else: sents = [line] for raw_sentence in sents: diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index dbe86953..087882aa 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -5,7 +5,7 @@ from .instance import Instance from .losses import LossFunc, CrossEntropyLoss, L1Loss, BCELoss, NLLLoss, LossInForward from .metrics import AccuracyMetric from .optimizer import Optimizer, SGD, Adam -from .sampler import SequentialSampler, BucketSampler, RandomSampler, BaseSampler +from .sampler import SequentialSampler, BucketSampler, RandomSampler, Sampler from .tester import Tester from .trainer import Trainer from .vocabulary import Vocabulary diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 9d65ada8..3a62cefe 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -2,7 +2,7 @@ import numpy as np import torch import atexit -from fastNLP.core.sampler import RandomSampler +from fastNLP.core.sampler import RandomSampler, Sampler import torch.multiprocessing as mp _python_is_exit = False @@ -12,19 +12,25 @@ def _set_python_is_exit(): atexit.register(_set_python_is_exit) class Batch(object): - """Batch is an iterable object which iterates over mini-batches. - - Example:: - - for batch_x, batch_y in Batch(data_set, batch_size=16, sampler=SequentialSampler()): - # ... - - :param DataSet dataset: a DataSet object - :param int batch_size: the size of the batch - :param Sampler sampler: a Sampler object. If None, use fastNLP.sampler.RandomSampler - :param bool as_numpy: If True, return Numpy array. Otherwise, return torch tensors. - :param bool prefetch: If True, use multiprocessing to fetch next batch when training. - :param str or torch.device device: the batch's device, if as_numpy is True, device is ignored. + """ + Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出. + 组成 `x` 和 `y` + + Example:: + + batch = Batch(data_set, batch_size=16, sampler=SequentialSampler()) + num_batch = len(batch) + for batch_x, batch_y in batch: + # do stuff ... + + :param DataSet dataset: `DataSet` 对象, 数据集 + :param int batch_size: 取出的batch大小 + :param Sampler sampler: 规定使用的 Sample 方式. 若为 ``None`` , 使用 RandomSampler. + Default: ``None`` + :param bool as_numpy: 若为 ``True`` , 输出batch为 numpy.array. 否则为 torch.Tensor. + Default: ``False`` + :param bool prefetch: 若为 ``True`` 使用多进程预先取出下一batch. + Default: ``False`` """ def __init__(self, dataset, batch_size, sampler=None, as_numpy=False, prefetch=False): @@ -41,7 +47,7 @@ class Batch(object): self.prefetch = prefetch self.lengths = 0 - def fetch_one(self): + def _fetch_one(self): if self.curidx >= len(self.idx_list): return None else: @@ -55,7 +61,7 @@ class Batch(object): if field.is_target or field.is_input: batch = field.get(indices) if not self.as_numpy and field.padder is not None: - batch = to_tensor(batch, field.dtype) + batch = _to_tensor(batch, field.dtype) if field.is_target: batch_y[field_name] = batch if field.is_input: @@ -70,17 +76,17 @@ class Batch(object): :return: """ if self.prefetch: - return run_batch_iter(self) + 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() @@ -89,10 +95,14 @@ class Batch(object): return self.num_batches def get_batch_indices(self): + """取得当前batch在DataSet中所在的index下标序列 + + :return list(int) indexes: 下标序列 + """ return self.cur_batch_indices -def to_tensor(batch, dtype): +def _to_tensor(batch, dtype): try: if dtype in (int, np.int8, np.int16, np.int32, np.int64): batch = torch.LongTensor(batch) @@ -103,12 +113,12 @@ def to_tensor(batch, dtype): return batch -def run_fetch(batch, q): +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: @@ -124,9 +134,9 @@ def run_fetch(batch, q): # print('fetch exit') -def run_batch_iter(batch): +def _run_batch_iter(batch): q = mp.JoinableQueue(maxsize=10) - fetch_p = mp.Process(target=run_fetch, args=(batch, q)) + fetch_p = mp.Process(target=_run_fetch, args=(batch, q)) fetch_p.daemon = True fetch_p.start() # print('fork fetch process') diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 3a4dfa55..68dfcc51 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -482,7 +482,7 @@ class DataSet(object): """ import warnings - warnings.warn('read_csv is deprecated, use CSVLoader instead', + warnings.warn('DataSet.read_csv is deprecated, use CSVLoader instead', category=DeprecationWarning) with open(csv_path, "r", encoding='utf-8') as f: start_idx = 0 diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 4a523f10..080825df 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -3,72 +3,49 @@ from itertools import chain import numpy as np import torch +class Sampler(object): + """ `Sampler` 类的基类. 规定以何种顺序取出data中的元素 -def convert_to_torch_tensor(data_list, use_cuda): - """Convert lists into (cuda) Tensors. - - :param data_list: 2-level lists - :param use_cuda: bool, whether to use GPU or not - :return data_list: PyTorch Tensor of shape [batch_size, max_seq_len] - """ - data_list = torch.Tensor(data_list).long() - if torch.cuda.is_available() and use_cuda: - data_list = data_list.cuda() - return data_list - - -class BaseSampler(object): - """The base class of all samplers. - - Sub-classes must implement the ``__call__`` method. - ``__call__`` takes a DataSet object and returns a list of int - the sampling indices. + 子类必须实现 ``__call__`` 方法. 输入 `DataSet` 对象, 返回其中元素的下标序列 """ - def __call__(self, *args, **kwargs): + def __call__(self, data_set): + """ + :param DataSet data_set: `DataSet` 对象, 需要Sample的数据 + :return result: list(int) 其中元素的下标序列, ``data_set`` 中元素会按 ``result`` 中顺序取出 + """ raise NotImplementedError -class SequentialSampler(BaseSampler): - """Sample data in the original order. +class SequentialSampler(Sampler): + """顺序取出元素的 `Sampler` """ def __call__(self, data_set): - """ - - :param DataSet data_set: - :return result: a list of integers. - """ return list(range(len(data_set))) -class RandomSampler(BaseSampler): - """Sample data in random permutation order. +class RandomSampler(Sampler): + """随机化取元素的 `Sampler` """ def __call__(self, data_set): - """ - - :param DataSet data_set: - :return result: a list of integers. - """ return list(np.random.permutation(len(data_set))) -class BucketSampler(BaseSampler): - """ - - :param int num_buckets: the number of buckets to use. - :param int batch_size: batch size per epoch. - :param str seq_lens_field_name: the field name indicating the field about sequence length. +class BucketSampler(Sampler): + """带Bucket的 `Random Sampler`. 可以随机地取出长度相似的元素 + :param int num_buckets: bucket的数量 + :param int batch_size: batch的大小 + :param str seq_lens_field_name: 对应序列长度的 `field` 的名字 """ - def __init__(self, num_buckets=10, batch_size=32, seq_lens_field_name='seq_lens'): + def __init__(self, num_buckets=10, batch_size=32, seq_lens_field_name='seq_len'): self.num_buckets = num_buckets self.batch_size = batch_size self.seq_lens_field_name = seq_lens_field_name def __call__(self, data_set): - seq_lens = data_set.get_all_fields()[self.seq_lens_field_name].content total_sample_num = len(seq_lens) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 67e7d2c0..867989bf 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -18,7 +18,7 @@ from fastNLP.core.dataset import DataSet from fastNLP.core.losses import _prepare_losser from fastNLP.core.metrics import _prepare_metrics from fastNLP.core.optimizer import Adam -from fastNLP.core.sampler import BaseSampler +from fastNLP.core.sampler import Sampler from fastNLP.core.sampler import RandomSampler from fastNLP.core.sampler import SequentialSampler from fastNLP.core.tester import Tester @@ -57,7 +57,7 @@ class Trainer(object): smaller, add "-" in front of the string. For example:: metric_key="-PPL" # language model gets better as perplexity gets smaller - :param BaseSampler sampler: method used to generate batch data. + :param Sampler sampler: method used to generate batch data. :param prefetch: bool, 是否使用额外的进程对产生batch数据。 :param bool use_tqdm: whether to use tqdm to show train progress. :param callbacks: List[Callback]. 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 @@ -102,7 +102,7 @@ class Trainer(object): losser = _prepare_losser(loss) # sampler check - if sampler is not None and not isinstance(sampler, BaseSampler): + if sampler is not None and not isinstance(sampler, Sampler): raise ValueError("The type of sampler should be fastNLP.BaseSampler, got {}.".format(type(sampler))) if check_code_level > -1: diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index c580dbec..6a1830ad 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -1,3 +1,4 @@ +from functools import wraps from collections import Counter from fastNLP.core.dataset import DataSet @@ -5,7 +6,7 @@ def check_build_vocab(func): """A decorator to make sure the indexing is built before used. """ - + @wraps(func) # to solve missing docstring def _wrapper(self, *args, **kwargs): if self.word2idx is None or self.rebuild is True: self.build_vocab() @@ -18,7 +19,7 @@ def check_build_status(func): """A decorator to check whether the vocabulary updates after the last build. """ - + @wraps(func) # to solve missing docstring def _wrapper(self, *args, **kwargs): if self.rebuild is False: self.rebuild = True @@ -32,23 +33,28 @@ def check_build_status(func): class Vocabulary(object): - """Use for word and index one to one mapping + """ + 用于构建, 存储和使用 `str` 到 `int` 的一一映射 Example:: vocab = Vocabulary() word_list = "this is a word list".split() vocab.update(word_list) - vocab["word"] - vocab.to_word(5) - - :param int max_size: set the max number of words in Vocabulary. Default: None - :param int min_freq: set the min occur frequency of words in Vocabulary. Default: None - :param padding: str, padding的字符,默认为。如果设置为None,则vocabulary中不考虑padding,为None的情况多在为label建立 - Vocabulary的情况。 - :param unknown: str, unknown的字符,默认为。如果设置为None,则vocabulary中不考虑unknown,为None的情况多在为label建立 - Vocabulary的情况。 - + vocab["word"] # str to int + vocab.to_word(5) # int to str + + :param int max_size: `Vocabulary` 的最大大小, 即能存储词的最大数量 + 若为 ``None`` , 则不限制大小. Default: ``None`` + :param int min_freq: 能被记录下的词在文本中的最小出现频率, 应大于或等于 1. + 若小于该频率, 词语将被视为 `unknown`. 若为 ``None`` , 所有文本中的词都被记录. Default: ``None`` + :param str padding: padding的字符. 如果设置为 ``None`` , + 则vocabulary中不考虑padding, 也不计入词表大小,为 ``None`` 的情况多在为label建立Vocabulary的情况. + Default: '' + :param str unknow: unknow的字符,所有未被记录的词在转为 `int` 时将被视为unknown. + 如果设置为 ``None`` ,则vocabulary中不考虑unknow, 也不计入词表大小. + 为 ``None`` 的情况多在为label建立Vocabulary的情况. + Default: '' """ def __init__(self, max_size=None, min_freq=None, padding='', unknown=''): @@ -63,7 +69,7 @@ class Vocabulary(object): @check_build_status def update(self, word_lst): - """Add a list of words into the vocabulary. + """依次增加序列中词在词典中的出现频率 :param list word_lst: a list of strings """ @@ -71,32 +77,35 @@ class Vocabulary(object): @check_build_status def add(self, word): - """Add a single word into the vocabulary. + """ + 增加一个新词在词典中的出现频率 - :param str word: a word or token. + :param str word: 新词 """ self.word_count[word] += 1 @check_build_status def add_word(self, word): - """Add a single word into the vocabulary. - - :param str word: a word or token. + """ + 增加一个新词在词典中的出现频率 + :param str word: 新词 """ self.add(word) @check_build_status def add_word_lst(self, word_lst): - """Add a list of words into the vocabulary. - - :param list word_lst: a list of strings + """ + 依次增加序列中词在词典中的出现频率 + :param list(str) word_lst: 词的序列 """ self.update(word_lst) def build_vocab(self): - """Build a mapping from word to index, and filter the word using ``max_size`` and ``min_freq``. + """ + 根据已经出现的词和出现频率构建词典. 注意: 重复构建可能会改变词典的大小, + 但已经记录在词典中的词, 不会改变对应的 `int` """ self.word2idx = {} @@ -117,7 +126,8 @@ class Vocabulary(object): self.rebuild = False def build_reverse_vocab(self): - """Build "index to word" dict based on "word to index" dict. + """ + 基于 "word to index" dict, 构建 "index to word" dict. """ self.idx2word = {i: w for w, i in self.word2idx.items()} @@ -128,7 +138,8 @@ class Vocabulary(object): @check_build_vocab def __contains__(self, item): - """Check if a word in vocabulary. + """ + 检查词是否被记录 :param item: the word :return: True or False @@ -136,11 +147,24 @@ class Vocabulary(object): return item in self.word2idx def has_word(self, w): + """ + 检查词是否被记录 + + Example:: + + has_abc = vocab.has_word('abc') + # equals to + has_abc = 'abc' in vocab + + :param item: the word + :return: ``True`` or ``False`` + """ return self.__contains__(w) @check_build_vocab def __getitem__(self, w): - """To support usage like:: + """ + To support usage like:: vocab[w] """ @@ -154,14 +178,19 @@ class Vocabulary(object): @check_build_vocab def index_dataset(self, *datasets, field_name, new_field_name=None): """ - example: - # remember to use `field_name` - vocab.index_dataset(tr_data, dev_data, te_data, field_name='words') + 将DataSet中对应field的词转为数字. + + Example:: - :param datasets: fastNLP Dataset type. you can pass multiple datasets - :param field_name: str, what field to index. Only support 0,1,2 dimension. - :param new_field_name: str. What the indexed field should be named, default is to overwrite field_name - :return: + # remember to use `field_name` + vocab.index_dataset(train_data, dev_data, test_data, field_name='words') + + :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个 + :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): """ @@ -194,11 +223,18 @@ class Vocabulary(object): def from_dataset(self, *datasets, field_name): """ - Construct vocab from dataset. + 使用dataset的对应field中词构建词典 + + Example:: + + # remember to use `field_name` + vocab.from_dataset(train_data1, train_data2, field_name='words') - :param datasets: DataSet. - :param field_name: str, what field is used to construct dataset. - :return: + :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个. + :param str field_name: 构建词典所使用的 field. + 若有多个 DataSet, 每个DataSet都必须有此 field. + 目前仅支持 ``str`` , ``list(str)`` , ``list(list(str))`` + :return self: """ def construct_vocab(ins): field = ins[field_name] @@ -223,15 +259,27 @@ class Vocabulary(object): return self def to_index(self, w): - """ Turn a word to an index. If w is not in Vocabulary, return the unknown label. + """ + 将词转为数字. 若词不再词典中被记录, 将视为 unknown, 若 ``unknown=None`` , 将抛出 + ``ValueError`` + + Example:: + + index = vocab.to_index('abc') + # equals to + index = vocab['abc'] :param str w: a word + :return int index: the number """ return self.__getitem__(w) @property @check_build_vocab def unknown_idx(self): + """ + unknown 对应的数字. + """ if self.unknown is None: return None return self.word2idx[self.unknown] @@ -239,16 +287,20 @@ class Vocabulary(object): @property @check_build_vocab def padding_idx(self): + """ + padding 对应的数字 + """ if self.padding is None: return None return self.word2idx[self.padding] @check_build_vocab def to_word(self, idx): - """given a word's index, return the word itself + """ + 给定一个数字, 将其转为对应的词. :param int idx: the index - :return str word: the indexed word + :return str word: the word """ return self.idx2word[idx] diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 5657e194..039c4242 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -4,7 +4,7 @@ from nltk.tree import Tree from fastNLP.core.dataset import DataSet from fastNLP.core.instance import Instance -from fastNLP.io.file_reader import read_csv, read_json, read_conll +from fastNLP.io.file_reader import _read_csv, _read_json, _read_conll def _download_from_url(url, path): @@ -55,12 +55,12 @@ def _uncompress(src, dst): class DataSetLoader: - """Interface for all DataSetLoaders. + """所有`DataSetLoader`的接口 """ def load(self, path): - """Load data from a given file. + """从指定 ``path`` 的文件中读取数据,返回DataSet :param str path: file path :return: a DataSet object @@ -68,7 +68,7 @@ class DataSetLoader: raise NotImplementedError def convert(self, data): - """Optional operation to build a DataSet. + """用Python数据对象创建DataSet :param data: inner data structure (user-defined) to represent the data. :return: a DataSet object @@ -77,7 +77,7 @@ class DataSetLoader: class PeopleDailyCorpusLoader(DataSetLoader): - """人民日报数据集 + """读取人民日报数据集 """ def __init__(self): super(PeopleDailyCorpusLoader, self).__init__() @@ -154,8 +154,35 @@ class PeopleDailyCorpusLoader(DataSetLoader): return data_set -class ConllLoader: +class ConllLoader(DataSetLoader): + """ + 读取Conll格式的数据. 数据格式详见 http://conll.cemantix.org/2012/data.html + + 列号从0开始, 每列对应内容为:: + + Column Type + 0 Document ID + 1 Part number + 2 Word number + 3 Word itself + 4 Part-of-Speech + 5 Parse bit + 6 Predicate lemma + 7 Predicate Frameset ID + 8 Word sense + 9 Speaker/Author + 10 Named Entities + 11:N Predicate Arguments + N Coreference + + :param headers: 每一列数据的名称,需为List or Tuple of str。``header`` 与 ``indexs`` 一一对应 + :param indexs: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` + :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``True`` + """ def __init__(self, headers, indexs=None, dropna=True): + super(ConllLoader, self).__init__() + if not isinstance(headers, (list, tuple)): + raise TypeError('invalid headers: {}, should be list of strings'.format(headers)) self.headers = headers self.dropna = dropna if indexs is None: @@ -167,24 +194,17 @@ class ConllLoader: def load(self, path): ds = DataSet() - for idx, data in read_conll(path, indexes=self.indexs, dropna=self.dropna): - ins = {h:data[idx] for h, idx in zip(self.headers, self.indexs)} + for idx, data in _read_conll(path, indexes=self.indexs, dropna=self.dropna): + ins = {h:data[i] for i, h in enumerate(self.headers)} ds.append(Instance(**ins)) return ds - def get_one(self, sample): - sample = list(map(list, zip(*sample))) - for field in sample: - if len(field) <= 0: - return None - return sample - class Conll2003Loader(ConllLoader): - """Loader for conll2003 dataset + """读取Conll2003数据 - More information about the given dataset cound be found on - https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data + 关于数据集的更多信息,参考: + https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ def __init__(self): headers = [ @@ -193,9 +213,10 @@ class Conll2003Loader(ConllLoader): super(Conll2003Loader, self).__init__(headers=headers) -def cut_long_sentence(sent, max_sample_length=200): +def _cut_long_sentence(sent, max_sample_length=200): """ - 将长于max_sample_length的sentence截成多段,只会在有空格的地方发生截断。所以截取的句子可能长于或者短于max_sample_length + 将长于max_sample_length的sentence截成多段,只会在有空格的地方发生截断。 + 所以截取的句子可能长于或者短于max_sample_length :param sent: str. :param max_sample_length: int. @@ -223,8 +244,15 @@ def cut_long_sentence(sent, max_sample_length=200): class SSTLoader(DataSetLoader): - """load SST data in PTB tree format - data source: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip + """读取SST数据集, DataSet包含fields:: + + words: list(str) 需要分类的文本 + target: str 文本的标签 + + 数据来源: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip + + :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` + :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` """ def __init__(self, subtree=False, fine_grained=False): self.subtree = subtree @@ -247,14 +275,14 @@ class SSTLoader(DataSetLoader): datas = [] for l in f: datas.extend([(s, self.tag_v[t]) - for s, t in self.get_one(l, self.subtree)]) + for s, t in self._get_one(l, self.subtree)]) ds = DataSet() for words, tag in datas: - ds.append(Instance(words=words, raw_tag=tag)) + ds.append(Instance(words=words, target=tag)) return ds @staticmethod - def get_one(data, subtree): + def _get_one(data, subtree): tree = Tree.fromstring(data) if subtree: return [(t.leaves(), t.label()) for t in tree.subtrees()] @@ -262,11 +290,17 @@ class SSTLoader(DataSetLoader): class JsonLoader(DataSetLoader): - """Load json-format data, - every line contains a json obj, like a dict - fields is the dict key that need to be load """ - def __init__(self, dropna=False, fields=None): + 读取json格式数据.数据必须按行存储,每行是一个包含各类属性的json对象 + + :param dict fields: 需要读入的json属性名称, 和读入后在DataSet中存储的field_name + ``fields`` 的`key`必须是json对象的属性名. ``fields`` 的`value`为读入后在DataSet存储的`field_name`, + `value`也可为 ``None`` , 这时读入后的`field_name`与json对象对应属性同名 + ``fields`` 可为 ``None`` , 这时,json对象所有属性都保存在DataSet中. Default: ``None`` + :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . + Default: ``True`` + """ + def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() self.dropna = dropna self.fields = None @@ -279,7 +313,7 @@ class JsonLoader(DataSetLoader): def load(self, path): ds = DataSet() - for idx, d in read_json(path, fields=self.fields_list, dropna=self.dropna): + for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): ins = {self.fields[k]:v for k,v in d.items()} ds.append(Instance(**ins)) return ds @@ -287,7 +321,13 @@ class JsonLoader(DataSetLoader): class SNLILoader(JsonLoader): """ - data source: https://nlp.stanford.edu/projects/snli/snli_1.0.zip + 读取SNLI数据集,读取的DataSet包含fields:: + + words1: list(str),第一句文本, premise + words2: list(str), 第二句文本, hypothesis + target: str, 真实标签 + + 数据来源: https://nlp.stanford.edu/projects/snli/snli_1.0.zip """ def __init__(self): fields = { @@ -309,14 +349,14 @@ class SNLILoader(JsonLoader): class CSVLoader(DataSetLoader): - """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格式的数据集。返回 ``DataSet`` + :param List[str] headers: CSV文件的文件头.定义每一列的属性名称,即返回的DataSet中`field`的名称 + 若为 ``None`` ,则将读入文件的第一行视作 ``headers`` . Default: ``None`` + :param str sep: CSV文件中列与列之间的分隔符. Default: "," + :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . + Default: ``True`` """ def __init__(self, headers=None, sep=",", dropna=True): self.headers = headers @@ -325,8 +365,8 @@ class CSVLoader(DataSetLoader): def load(self, path): ds = DataSet() - for idx, data in read_csv(path, headers=self.headers, - sep=self.sep, dropna=self.dropna): + for idx, data in _read_csv(path, headers=self.headers, + sep=self.sep, dropna=self.dropna): ds.append(Instance(**data)) return ds diff --git a/fastNLP/io/file_reader.py b/fastNLP/io/file_reader.py index 22766ebb..ffbab510 100644 --- a/fastNLP/io/file_reader.py +++ b/fastNLP/io/file_reader.py @@ -1,15 +1,16 @@ import json -def read_csv(path, encoding='utf-8', headers=None, sep=',', dropna=True): +def _read_csv(path, encoding='utf-8', headers=None, sep=',', dropna=True): """ - Construct a generator to read csv items + Construct a generator to read csv items. + :param path: file path :param encoding: file's encoding, default: utf-8 :param headers: file's headers, if None, make file's first line as headers. default: None :param sep: separator for each column. default: ',' :param dropna: weather to ignore and drop invalid data, - if False, raise ValueError when reading invalid data. default: True + :if False, raise ValueError when reading invalid data. default: True :return: generator, every time yield (line number, csv item) """ with open(path, 'r', encoding=encoding) as f: @@ -35,14 +36,15 @@ def read_csv(path, encoding='utf-8', headers=None, sep=',', dropna=True): yield line_idx, _dict -def read_json(path, encoding='utf-8', fields=None, dropna=True): +def _read_json(path, encoding='utf-8', fields=None, dropna=True): """ - Construct a generator to read json items + Construct a generator to read json items. + :param path: file path :param encoding: file's encoding, default: utf-8 :param fields: json object's fields that needed, if None, all fields are needed. default: None :param dropna: weather to ignore and drop invalid data, - if False, raise ValueError when reading invalid data. default: True + :if False, raise ValueError when reading invalid data. default: True :return: generator, every time yield (line number, json item) """ if fields: @@ -65,14 +67,15 @@ def read_json(path, encoding='utf-8', fields=None, dropna=True): yield line_idx, _res -def read_conll(path, encoding='utf-8', indexes=None, dropna=True): +def _read_conll(path, encoding='utf-8', indexes=None, dropna=True): """ - Construct a generator to read conll items + Construct a generator to read conll items. + :param path: file path :param encoding: file's encoding, default: utf-8 :param indexes: conll object's column indexes that needed, if None, all columns are needed. default: None :param dropna: weather to ignore and drop invalid data, - if False, raise ValueError when reading invalid data. default: True + :if False, raise ValueError when reading invalid data. default: True :return: generator, every time yield (line number, conll item) """ def parse_conll(sample): diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index dc294eb3..9a070c92 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -16,7 +16,7 @@ from fastNLP.modules.utils import initial_parameter from fastNLP.modules.utils import seq_mask -def mst(scores): +def _mst(scores): """ with some modification to support parser output for MST decoding https://github.com/tdozat/Parser/blob/0739216129cd39d69997d28cbc4133b360ea3934/lib/models/nn.py#L692 @@ -120,12 +120,22 @@ def _find_cycle(vertices, edges): class GraphParser(BaseModel): - """Graph based Parser helper class, support greedy decoding and MST(Maximum Spanning Tree) decoding + """ + 基于图的parser base class, 支持贪婪解码和最大生成树解码 """ def __init__(self): super(GraphParser, self).__init__() - def _greedy_decoder(self, arc_matrix, mask=None): + @staticmethod + def greedy_decoder(arc_matrix, mask=None): + """ + 贪心解码方式, 输入图, 输出贪心解码的parsing结果, 不保证合法的构成树 + + :param arc_matrix: [batch, seq_len, seq_len] 输入图矩阵 + :param mask: [batch, seq_len] 输入图的padding mask, 有内容的部分为 1, 否则为 0. + 若为 ``None`` 时, 默认为全1向量. Default: ``None`` + :return heads: [batch, seq_len] 每个元素在树中对应的head(parent)预测结果 + """ _, seq_len, _ = arc_matrix.shape matrix = arc_matrix + torch.diag(arc_matrix.new(seq_len).fill_(-np.inf)) flip_mask = (mask == 0).byte() @@ -135,22 +145,34 @@ class GraphParser(BaseModel): heads *= mask.long() return heads - def _mst_decoder(self, arc_matrix, mask=None): + @staticmethod + def mst_decoder(arc_matrix, mask=None): + """ + 用最大生成树算法, 计算parsing结果, 保证输出合法的树结构 + + :param arc_matrix: [batch, seq_len, seq_len] 输入图矩阵 + :param mask: [batch, seq_len] 输入图的padding mask, 有内容的部分为 1, 否则为 0. + 若为 ``None`` 时, 默认为全1向量. Default: ``None`` + :return heads: [batch, seq_len] 每个元素在树中对应的head(parent)预测结果 + """ batch_size, seq_len, _ = arc_matrix.shape matrix = arc_matrix.clone() ans = matrix.new_zeros(batch_size, seq_len).long() lens = (mask.long()).sum(1) if mask is not None else torch.zeros(batch_size) + seq_len - batch_idx = torch.arange(batch_size, dtype=torch.long, device=lens.device) for i, graph in enumerate(matrix): len_i = lens[i] - ans[i, :len_i] = torch.as_tensor(mst(graph.detach()[:len_i, :len_i].cpu().numpy()), device=ans.device) + ans[i, :len_i] = torch.as_tensor(_mst(graph.detach()[:len_i, :len_i].cpu().numpy()), device=ans.device) if mask is not None: ans *= mask.long() return ans class ArcBiaffine(nn.Module): - """helper module for Biaffine Dependency Parser predicting arc + """ + Biaffine Dependency Parser 的子模块, 用于构建预测边的图 + + :param hidden_size: 输入的特征维度 + :param bias: 是否使用bias. Default: ``True`` """ def __init__(self, hidden_size, bias=True): super(ArcBiaffine, self).__init__() @@ -164,10 +186,10 @@ class ArcBiaffine(nn.Module): def forward(self, head, dep): """ - :param head arc-head tensor = [batch, length, emb_dim] - :param dep arc-dependent tensor = [batch, length, emb_dim] - :return output tensor = [bacth, length, length] + :param head: arc-head tensor [batch, length, hidden] + :param dep: arc-dependent tensor [batch, length, hidden] + :return output: tensor [bacth, length, length] """ output = dep.matmul(self.U) output = output.bmm(head.transpose(-1, -2)) @@ -177,7 +199,13 @@ class ArcBiaffine(nn.Module): class LabelBilinear(nn.Module): - """helper module for Biaffine Dependency Parser predicting label + """ + Biaffine Dependency Parser 的子模块, 用于构建预测边类别的图 + + :param in1_features: 输入的特征1维度 + :param in2_features: 输入的特征2维度 + :param num_label: 边类别的个数 + :param bias: 是否使用bias. Default: ``True`` """ def __init__(self, in1_features, in2_features, num_label, bias=True): super(LabelBilinear, self).__init__() @@ -185,14 +213,34 @@ class LabelBilinear(nn.Module): self.lin = nn.Linear(in1_features + in2_features, num_label, bias=False) def forward(self, x1, x2): + """ + + :param x1: [batch, seq_len, hidden] 输入特征1, 即label-head + :param x2: [batch, seq_len, hidden] 输入特征2, 即label-dep + :return output: [batch, seq_len, num_cls] 每个元素对应类别的概率图 + """ output = self.bilinear(x1, x2) output += self.lin(torch.cat([x1, x2], dim=2)) return output class BiaffineParser(GraphParser): - """Biaffine Dependency Parser implemantation. - refer to ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) + """Biaffine Dependency Parser 实现. + 论文参考 ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) `_ . + + :param word_vocab_size: 单词词典大小 + :param word_emb_dim: 单词词嵌入向量的维度 + :param pos_vocab_size: part-of-speech 词典大小 + :param pos_emb_dim: part-of-speech 向量维度 + :param num_label: 边的类别个数 + :param rnn_layers: rnn encoder的层数 + :param rnn_hidden_size: rnn encoder 的隐状态维度 + :param arc_mlp_size: 边预测的MLP维度 + :param label_mlp_size: 类别预测的MLP维度 + :param dropout: dropout概率. + :param encoder: encoder类别, 可选 ('lstm', 'var-lstm', 'transformer'). Default: lstm + :param use_greedy_infer: 是否在inference时使用贪心算法. + 若 ``False`` , 使用更加精确但相对缓慢的MST算法. Default: ``False`` """ def __init__(self, word_vocab_size, @@ -207,7 +255,6 @@ class BiaffineParser(GraphParser): dropout=0.3, encoder='lstm', use_greedy_infer=False): - super(BiaffineParser, self).__init__() rnn_out_size = 2 * rnn_hidden_size word_hid_dim = pos_hid_dim = rnn_hidden_size @@ -275,27 +322,31 @@ class BiaffineParser(GraphParser): for p in m.parameters(): nn.init.normal_(p, 0, 0.1) - def forward(self, word_seq, pos_seq, seq_lens, gold_heads=None): - """ - :param word_seq: [batch_size, seq_len] sequence of word's indices - :param pos_seq: [batch_size, seq_len] sequence of word's indices - :param seq_lens: [batch_size, seq_len] sequence of length masks - :param gold_heads: [batch_size, seq_len] sequence of golden heads - :return dict: parsing results - arc_pred: [batch_size, seq_len, seq_len] - label_pred: [batch_size, seq_len, seq_len] - mask: [batch_size, seq_len] - head_pred: [batch_size, seq_len] if gold_heads is not provided, predicting the heads + def forward(self, words1, words2, seq_len, gold_heads=None): + """模型forward阶段 + + :param words1: [batch_size, seq_len] 输入word序列 + :param words2: [batch_size, seq_len] 输入pos序列 + :param seq_len: [batch_size, seq_len] 输入序列长度 + :param gold_heads: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, + 用于训练label分类器. 若为 ``None`` , 使用预测的heads输入到label分类器 + Default: ``None`` + :return dict: parsing结果:: + + arc_pred: [batch_size, seq_len, seq_len] 边预测logits + label_pred: [batch_size, seq_len, num_label] label预测logits + mask: [batch_size, seq_len] 预测结果的mask + head_pred: [batch_size, seq_len] heads的预测结果, 在 ``gold_heads=None`` 时预测 """ # prepare embeddings - batch_size, seq_len = word_seq.shape + batch_size, length = words1.shape # print('forward {} {}'.format(batch_size, seq_len)) # get sequence mask - mask = seq_mask(seq_lens, seq_len).long() + mask = seq_mask(seq_len, length).long() - word = self.word_embedding(word_seq) # [N,L] -> [N,L,C_0] - pos = self.pos_embedding(pos_seq) # [N,L] -> [N,L,C_1] + word = self.word_embedding(words1) # [N,L] -> [N,L,C_0] + pos = self.pos_embedding(words2) # [N,L] -> [N,L,C_1] word, pos = self.word_fc(word), self.pos_fc(pos) word, pos = self.word_norm(word), self.pos_norm(pos) @@ -303,7 +354,7 @@ class BiaffineParser(GraphParser): # encoder, extract features if self.encoder_name.endswith('lstm'): - sort_lens, sort_idx = torch.sort(seq_lens, dim=0, descending=True) + sort_lens, sort_idx = torch.sort(seq_len, dim=0, descending=True) x = x[sort_idx] x = nn.utils.rnn.pack_padded_sequence(x, sort_lens, batch_first=True) feat, _ = self.encoder(x) # -> [N,L,C] @@ -329,20 +380,20 @@ class BiaffineParser(GraphParser): if gold_heads is None or not self.training: # use greedy decoding in training if self.training or self.use_greedy_infer: - heads = self._greedy_decoder(arc_pred, mask) + heads = self.greedy_decoder(arc_pred, mask) else: - heads = self._mst_decoder(arc_pred, mask) + heads = self.mst_decoder(arc_pred, mask) head_pred = heads else: assert self.training # must be training mode if gold_heads is None: - heads = self._greedy_decoder(arc_pred, mask) + heads = self.greedy_decoder(arc_pred, mask) head_pred = heads else: head_pred = None heads = gold_heads - batch_range = torch.arange(start=0, end=batch_size, dtype=torch.long, device=word_seq.device).unsqueeze(1) + batch_range = torch.arange(start=0, end=batch_size, dtype=torch.long, device=words1.device).unsqueeze(1) label_head = label_head[batch_range, heads].contiguous() label_pred = self.label_predictor(label_head, label_dep) # [N, L, num_label] res_dict = {'arc_pred': arc_pred, 'label_pred': label_pred, 'mask': mask} @@ -355,11 +406,11 @@ class BiaffineParser(GraphParser): """ Compute loss. - :param arc_pred: [batch_size, seq_len, seq_len] - :param label_pred: [batch_size, seq_len, n_tags] - :param arc_true: [batch_size, seq_len] - :param label_true: [batch_size, seq_len] - :param mask: [batch_size, seq_len] + :param arc_pred: [batch_size, seq_len, seq_len] 边预测logits + :param label_pred: [batch_size, seq_len, num_label] label预测logits + :param arc_true: [batch_size, seq_len] 真实边的标注 + :param label_true: [batch_size, seq_len] 真实类别的标注 + :param mask: [batch_size, seq_len] 预测结果的mask :return: loss value """ @@ -381,16 +432,23 @@ class BiaffineParser(GraphParser): label_nll = -label_loss.mean() return arc_nll + label_nll - def predict(self, word_seq, pos_seq, seq_lens): - """ - - :param word_seq: - :param pos_seq: - :param seq_lens: - :return: arc_pred: [B, L] - label_pred: [B, L] + def predict(self, words1, words2, seq_len): + """模型预测API + + :param words1: [batch_size, seq_len] 输入word序列 + :param words2: [batch_size, seq_len] 输入pos序列 + :param seq_len: [batch_size, seq_len] 输入序列长度 + :param gold_heads: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, + 用于训练label分类器. 若为 ``None`` , 使用预测的heads输入到label分类器 + Default: ``None`` + :return dict: parsing结果:: + + arc_pred: [batch_size, seq_len, seq_len] 边预测logits + label_pred: [batch_size, seq_len, num_label] label预测logits + mask: [batch_size, seq_len] 预测结果的mask + head_pred: [batch_size, seq_len] heads的预测结果, 在 ``gold_heads=None`` 时预测 """ - res = self(word_seq, pos_seq, seq_lens) + res = self(words1, words2, seq_len) output = {} output['arc_pred'] = res.pop('head_pred') _, label_pred = res.pop('label_pred').max(2) @@ -399,6 +457,16 @@ class BiaffineParser(GraphParser): class ParserLoss(LossFunc): + """ + 计算parser的loss + + :param arc_pred: [batch_size, seq_len, seq_len] 边预测logits + :param label_pred: [batch_size, seq_len, num_label] label预测logits + :param arc_true: [batch_size, seq_len] 真实边的标注 + :param label_true: [batch_size, seq_len] 真实类别的标注 + :param mask: [batch_size, seq_len] 预测结果的mask + :return loss: scalar + """ def __init__(self, arc_pred=None, label_pred=None, arc_true=None, label_true=None): super(ParserLoss, self).__init__(BiaffineParser.loss, arc_pred=arc_pred, @@ -408,12 +476,26 @@ class ParserLoss(LossFunc): class ParserMetric(MetricBase): + """ + 评估parser的性能 + + :param arc_pred: 边预测logits + :param label_pred: label预测logits + :param arc_true: 真实边的标注 + :param label_true: 真实类别的标注 + :param seq_len: 序列长度 + :return dict: 评估结果:: + + UAS: 不带label时, 边预测的准确率 + LAS: 同时预测边和label的准确率 + """ def __init__(self, arc_pred=None, label_pred=None, - arc_true=None, label_true=None, seq_lens=None): + arc_true=None, label_true=None, seq_len=None): + super().__init__() self._init_param_map(arc_pred=arc_pred, label_pred=label_pred, arc_true=arc_true, label_true=label_true, - seq_lens=seq_lens) + seq_len=seq_len) self.num_arc = 0 self.num_label = 0 self.num_sample = 0 @@ -424,13 +506,13 @@ class ParserMetric(MetricBase): self.num_sample = self.num_label = self.num_arc = 0 return res - def evaluate(self, arc_pred, label_pred, arc_true, label_true, seq_lens=None): + def evaluate(self, arc_pred, label_pred, arc_true, label_true, seq_len=None): """Evaluate the performance of prediction. """ - if seq_lens is None: + if seq_len is None: seq_mask = arc_pred.new_ones(arc_pred.size(), dtype=torch.long) else: - seq_mask = seq_lens_to_masks(seq_lens.long(), float=False).long() + seq_mask = seq_lens_to_masks(seq_len.long(), float=False).long() # mask out tag seq_mask[:,0] = 0 head_pred_correct = (arc_pred == arc_true).long() * seq_mask diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index 3af3fe19..4f4ed551 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -7,6 +7,21 @@ import torch.nn.functional as F class StarTransEnc(nn.Module): + """ + 带word embedding的Star-Transformer Encoder + + :param vocab_size: 词嵌入的词典大小 + :param emb_dim: 每个词嵌入的特征维度 + :param num_cls: 输出类别个数 + :param hidden_size: 模型中特征维度. + :param num_layers: 模型层数. + :param num_head: 模型中multi-head的head个数. + :param head_dim: 模型中multi-head中每个head特征维度. + :param max_len: 模型能接受的最大输入长度. + :param cls_hidden_size: 分类器隐层维度. + :param emb_dropout: 词嵌入的dropout概率. + :param dropout: 模型除词嵌入外的dropout概率. + """ def __init__(self, vocab_size, emb_dim, hidden_size, num_layers, @@ -27,15 +42,23 @@ class StarTransEnc(nn.Module): max_len=max_len) def forward(self, x, mask): + """ + :param FloatTensor data: [batch, length, hidden] 输入的序列 + :param ByteTensor mask: [batch, length] 输入序列的padding mask, 在没有内容(padding 部分) 为 0, + 否则为 1 + :return: [batch, length, hidden] 编码后的输出序列 + + [batch, hidden] 全局 relay 节点, 详见论文 + """ x = self.embedding(x) x = self.emb_fc(self.emb_drop(x)) nodes, relay = self.encoder(x, mask) return nodes, relay -class Cls(nn.Module): +class _Cls(nn.Module): def __init__(self, in_dim, num_cls, hid_dim, dropout=0.1): - super(Cls, self).__init__() + super(_Cls, self).__init__() self.fc = nn.Sequential( nn.Linear(in_dim, hid_dim), nn.LeakyReLU(), @@ -48,9 +71,9 @@ class Cls(nn.Module): return h -class NLICls(nn.Module): +class _NLICls(nn.Module): def __init__(self, in_dim, num_cls, hid_dim, dropout=0.1): - super(NLICls, self).__init__() + super(_NLICls, self).__init__() self.fc = nn.Sequential( nn.Dropout(dropout), nn.Linear(in_dim*4, hid_dim), #4 @@ -65,7 +88,19 @@ class NLICls(nn.Module): return h class STSeqLabel(nn.Module): - """star-transformer model for sequence labeling + """用于序列标注的Star-Transformer模型 + + :param vocab_size: 词嵌入的词典大小 + :param emb_dim: 每个词嵌入的特征维度 + :param num_cls: 输出类别个数 + :param hidden_size: 模型中特征维度. Default: 300 + :param num_layers: 模型层数. Default: 4 + :param num_head: 模型中multi-head的head个数. Default: 8 + :param head_dim: 模型中multi-head中每个head特征维度. Default: 32 + :param max_len: 模型能接受的最大输入长度. Default: 512 + :param cls_hidden_size: 分类器隐层维度. Default: 600 + :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 + :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ def __init__(self, vocab_size, emb_dim, num_cls, hidden_size=300, @@ -86,23 +121,47 @@ class STSeqLabel(nn.Module): max_len=max_len, emb_dropout=emb_dropout, dropout=dropout) - self.cls = Cls(hidden_size, num_cls, cls_hidden_size) + self.cls = _Cls(hidden_size, num_cls, cls_hidden_size) + + def forward(self, words, seq_len): + """ - def forward(self, word_seq, seq_lens): - mask = seq_lens_to_masks(seq_lens) - nodes, _ = self.enc(word_seq, mask) + :param words: [batch, seq_len] 输入序列 + :param seq_len: [batch,] 输入序列的长度 + :return output: [batch, num_cls, seq_len] 输出序列中每个元素的分类的概率 + """ + mask = seq_lens_to_masks(seq_len) + nodes, _ = self.enc(words, mask) output = self.cls(nodes) output = output.transpose(1,2) # make hidden to be dim 1 return {'output': output} # [bsz, n_cls, seq_len] - def predict(self, word_seq, seq_lens): - y = self.forward(word_seq, seq_lens) + def predict(self, words, seq_len): + """ + + :param words: [batch, seq_len] 输入序列 + :param seq_len: [batch,] 输入序列的长度 + :return output: [batch, seq_len] 输出序列中每个元素的分类 + """ + y = self.forward(words, seq_len) _, pred = y['output'].max(1) - return {'output': pred, 'seq_lens': seq_lens} + return {'output': pred} class STSeqCls(nn.Module): - """star-transformer model for sequence classification + """用于分类任务的Star-Transformer + + :param vocab_size: 词嵌入的词典大小 + :param emb_dim: 每个词嵌入的特征维度 + :param num_cls: 输出类别个数 + :param hidden_size: 模型中特征维度. Default: 300 + :param num_layers: 模型层数. Default: 4 + :param num_head: 模型中multi-head的head个数. Default: 8 + :param head_dim: 模型中multi-head中每个head特征维度. Default: 32 + :param max_len: 模型能接受的最大输入长度. Default: 512 + :param cls_hidden_size: 分类器隐层维度. Default: 600 + :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 + :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ def __init__(self, vocab_size, emb_dim, num_cls, @@ -124,23 +183,47 @@ class STSeqCls(nn.Module): max_len=max_len, emb_dropout=emb_dropout, dropout=dropout) - self.cls = Cls(hidden_size, num_cls, cls_hidden_size) + self.cls = _Cls(hidden_size, num_cls, cls_hidden_size) - def forward(self, word_seq, seq_lens): - mask = seq_lens_to_masks(seq_lens) - nodes, relay = self.enc(word_seq, mask) + def forward(self, words, seq_len): + """ + + :param words: [batch, seq_len] 输入序列 + :param seq_len: [batch,] 输入序列的长度 + :return output: [batch, num_cls] 输出序列的分类的概率 + """ + mask = seq_lens_to_masks(seq_len) + nodes, relay = self.enc(words, mask) y = 0.5 * (relay + nodes.max(1)[0]) output = self.cls(y) # [bsz, n_cls] return {'output': output} - def predict(self, word_seq, seq_lens): - y = self.forward(word_seq, seq_lens) + def predict(self, words, seq_len): + """ + + :param words: [batch, seq_len] 输入序列 + :param seq_len: [batch,] 输入序列的长度 + :return output: [batch, num_cls] 输出序列的分类 + """ + y = self.forward(words, seq_len) _, pred = y['output'].max(1) return {'output': pred} class STNLICls(nn.Module): - """star-transformer model for NLI + """用于自然语言推断(NLI)的Star-Transformer + + :param vocab_size: 词嵌入的词典大小 + :param emb_dim: 每个词嵌入的特征维度 + :param num_cls: 输出类别个数 + :param hidden_size: 模型中特征维度. Default: 300 + :param num_layers: 模型层数. Default: 4 + :param num_head: 模型中multi-head的head个数. Default: 8 + :param head_dim: 模型中multi-head中每个head特征维度. Default: 32 + :param max_len: 模型能接受的最大输入长度. Default: 512 + :param cls_hidden_size: 分类器隐层维度. Default: 600 + :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 + :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ def __init__(self, vocab_size, emb_dim, num_cls, @@ -162,20 +245,36 @@ class STNLICls(nn.Module): max_len=max_len, emb_dropout=emb_dropout, dropout=dropout) - self.cls = NLICls(hidden_size, num_cls, cls_hidden_size) + self.cls = _NLICls(hidden_size, num_cls, cls_hidden_size) + + def forward(self, words1, words2, seq_len1, seq_len2): + """ - def forward(self, word_seq1, word_seq2, seq_lens1, seq_lens2): - mask1 = seq_lens_to_masks(seq_lens1) - mask2 = seq_lens_to_masks(seq_lens2) + :param words1: [batch, seq_len] 输入序列1 + :param words2: [batch, seq_len] 输入序列2 + :param seq_len1: [batch,] 输入序列1的长度 + :param seq_len2: [batch,] 输入序列2的长度 + :return output: [batch, num_cls] 输出分类的概率 + """ + mask1 = seq_lens_to_masks(seq_len1) + mask2 = seq_lens_to_masks(seq_len2) def enc(seq, mask): nodes, relay = self.enc(seq, mask) return 0.5 * (relay + nodes.max(1)[0]) - y1 = enc(word_seq1, mask1) - y2 = enc(word_seq2, mask2) + y1 = enc(words1, mask1) + y2 = enc(words2, mask2) output = self.cls(y1, y2) # [bsz, n_cls] return {'output': output} - def predict(self, word_seq1, word_seq2, seq_lens1, seq_lens2): - y = self.forward(word_seq1, word_seq2, seq_lens1, seq_lens2) + def predict(self, words1, words2, seq_len1, seq_len2): + """ + + :param words1: [batch, seq_len] 输入序列1 + :param words2: [batch, seq_len] 输入序列2 + :param seq_len1: [batch,] 输入序列1的长度 + :param seq_len2: [batch,] 输入序列2的长度 + :return output: [batch, num_cls] 输出分类的概率 + """ + y = self.forward(words1, words2, seq_len1, seq_len2) _, pred = y['output'].max(1) return {'output': pred} diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index 04f331f7..fe740c70 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -6,17 +6,17 @@ from fastNLP.modules.utils import initial_parameter class LSTM(nn.Module): - """Long Short Term Memory + """LSTM 模块, 轻量封装的Pytorch LSTM - :param int input_size: - :param int hidden_size: - :param int num_layers: - :param float dropout: - :param bool batch_first: - :param bool bidirectional: - :param bool bias: - :param str initial_method: - :param bool get_hidden: + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度 + :param num_layers: rnn的层数. Default: 1 + :param dropout: 层间dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + :param get_hidden: 是否返回隐状态 `h` . Default: ``False`` """ def __init__(self, input_size, hidden_size=100, num_layers=1, dropout=0.0, batch_first=True, bidirectional=False, bias=True, initial_method=None, get_hidden=False): @@ -27,14 +27,24 @@ class LSTM(nn.Module): self.get_hidden = get_hidden initial_parameter(self, initial_method) - def forward(self, x, seq_lens=None, h0=None, c0=None): + def forward(self, x, seq_len=None, h0=None, c0=None): + """ + + :param x: [batch, seq_len, input_size] 输入序列 + :param seq_len: [batch, ] 序列长度, 若为 ``None``, 所有输入看做一样长. Default: ``None`` + :param h0: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` + :param c0: [batch, hidden_size] 初始Cell状态, 若为 ``None`` , 设为全1向量. Default: ``None`` + :return (output, ht) 或 output: 若 ``get_hidden=True`` [batch, seq_len, hidden_size*num_direction] 输出序列 + :和 [batch, hidden_size*num_direction] 最后时刻隐状态. + :若 ``get_hidden=False`` 仅返回输出序列. + """ if h0 is not None and c0 is not None: hx = (h0, c0) else: hx = None - if seq_lens is not None and not isinstance(x, rnn.PackedSequence): + if seq_len is not None and not isinstance(x, rnn.PackedSequence): print('padding') - sort_lens, sort_idx = torch.sort(seq_lens, dim=0, descending=True) + sort_lens, sort_idx = torch.sort(seq_len, dim=0, descending=True) if self.batch_first: x = x[sort_idx] else: diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 1618c8ee..034cfa96 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -5,16 +5,19 @@ import numpy as NP class StarTransformer(nn.Module): - """Star-Transformer Encoder part。 + """ + Star-Transformer 的encoder部分。 输入3d的文本输入, 返回相同长度的文本编码 + paper: https://arxiv.org/abs/1902.09113 - :param hidden_size: int, 输入维度的大小。同时也是输出维度的大小。 - :param num_layers: int, star-transformer的层数 - :param num_head: int,head的数量。 - :param head_dim: int, 每个head的维度大小。 - :param dropout: float dropout 概率 - :param max_len: int or None, 如果为int,输入序列的最大长度, - 模型会为属于序列加上position embedding。 - 若为None,忽略加上position embedding的步骤 + + :param int hidden_size: 输入维度的大小。同时也是输出维度的大小。 + :param int num_layers: star-transformer的层数 + :param int num_head: head的数量。 + :param int head_dim: 每个head的维度大小。 + :param float dropout: dropout 概率. Default: 0.1 + :param int max_len: int or None, 如果为int,输入序列的最大长度, + 模型会为输入序列加上position embedding。 + 若为`None`,忽略加上position embedding的步骤. Default: `None` """ def __init__(self, hidden_size, num_layers, num_head, head_dim, dropout=0.1, max_len=None): super(StarTransformer, self).__init__() @@ -22,11 +25,11 @@ class StarTransformer(nn.Module): self.norm = nn.ModuleList([nn.LayerNorm(hidden_size) for _ in range(self.iters)]) self.ring_att = nn.ModuleList( - [MSA1(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) - for _ in range(self.iters)]) + [_MSA1(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) + for _ in range(self.iters)]) self.star_att = nn.ModuleList( - [MSA2(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) - for _ in range(self.iters)]) + [_MSA2(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) + for _ in range(self.iters)]) if max_len is not None: self.pos_emb = self.pos_emb = nn.Embedding(max_len, hidden_size) @@ -35,10 +38,12 @@ class StarTransformer(nn.Module): def forward(self, data, mask): """ - :param FloatTensor data: [batch, length, hidden] the input sequence - :param ByteTensor mask: [batch, length] the padding mask for input, in which padding pos is 0 - :return: [batch, length, hidden] the output sequence - [batch, hidden] the global relay node + :param FloatTensor data: [batch, length, hidden] 输入的序列 + :param ByteTensor mask: [batch, length] 输入序列的padding mask, 在没有内容(padding 部分) 为 0, + 否则为 1 + :return: [batch, length, hidden] 编码后的输出序列 + + [batch, hidden] 全局 relay 节点, 详见论文 """ def norm_func(f, x): # B, H, L, 1 @@ -70,9 +75,9 @@ class StarTransformer(nn.Module): return nodes, relay.view(B, H) -class MSA1(nn.Module): +class _MSA1(nn.Module): def __init__(self, nhid, nhead=10, head_dim=10, dropout=0.1): - super(MSA1, self).__init__() + super(_MSA1, self).__init__() # Multi-head Self Attention Case 1, doing self-attention for small regions # Due to the architecture of GPU, using hadamard production and summation are faster than dot production when unfold_size is very small self.WQ = nn.Conv2d(nhid, nhead * head_dim, 1) @@ -113,10 +118,10 @@ class MSA1(nn.Module): return ret -class MSA2(nn.Module): +class _MSA2(nn.Module): def __init__(self, nhid, nhead=10, head_dim=10, dropout=0.1): # Multi-head Self Attention Case 2, a broadcastable query for a sequence key and value - super(MSA2, self).__init__() + super(_MSA2, self).__init__() self.WQ = nn.Conv2d(nhid, nhead * head_dim, 1) self.WK = nn.Conv2d(nhid, nhead * head_dim, 1) self.WV = nn.Conv2d(nhid, nhead * head_dim, 1) diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index d1262141..60216c2b 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -7,13 +7,13 @@ from ..dropout import TimestepDropout class TransformerEncoder(nn.Module): """transformer的encoder模块,不包含embedding层 - :param num_layers: int, transformer的层数 - :param model_size: int, 输入维度的大小。同时也是输出维度的大小。 - :param inner_size: int, FFN层的hidden大小 - :param key_size: int, 每个head的维度大小。 - :param value_size: int,每个head中value的维度。 - :param num_head: int,head的数量。 - :param dropout: float。 + :param int num_layers: transformer的层数 + :param int model_size: 输入维度的大小。同时也是输出维度的大小。 + :param int inner_size: FFN层的hidden大小 + :param int key_size: 每个head的维度大小。 + :param int value_size: 每个head中value的维度。 + :param int num_head: head的数量。 + :param float dropout: dropout概率. Default: 0.1 """ class SubLayer(nn.Module): def __init__(self, model_size, inner_size, key_size, value_size, num_head, dropout=0.1): @@ -48,7 +48,8 @@ class TransformerEncoder(nn.Module): def forward(self, x, seq_mask=None): """ :param x: [batch, seq_len, model_size] 输入序列 - :param seq_mask: [batch, seq_len] 输入序列的padding mask + :param seq_mask: [batch, seq_len] 输入序列的padding mask, 若为 ``None`` , 生成全1向量. + Default: ``None`` :return: [batch, seq_len, model_size] 输出序列 """ output = x diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index a7902813..d63aa6e7 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -28,11 +28,11 @@ class VarRnnCellWrapper(nn.Module): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] :param hidden: for LSTM, tuple of (h_0, c_0), [batch_size, hidden_size] - for other RNN, h_0, [batch_size, hidden_size] + :for other RNN, h_0, [batch_size, hidden_size] :param mask_x: [batch_size, input_size] dropout mask for input :param mask_h: [batch_size, hidden_size] dropout mask for hidden :return PackedSequence output: [seq_len, bacth_size, hidden_size] - hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] + :hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ def get_hi(hi, h0, size): @@ -84,9 +84,21 @@ class VarRnnCellWrapper(nn.Module): class VarRNNBase(nn.Module): - """Implementation of Variational Dropout RNN network. - refer to `A Theoretically Grounded Application of Dropout in Recurrent Neural Networks (Yarin Gal and Zoubin Ghahramani, 2016) + """Variational Dropout RNN 实现. + 论文参考: `A Theoretically Grounded Application of Dropout in Recurrent Neural Networks (Yarin Gal and Zoubin Ghahramani, 2016) https://arxiv.org/abs/1512.05287`. + + :param mode: rnn 模式, (lstm or not) + :param Cell: rnn cell 类型, (lstm, gru, etc) + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度 + :param num_layers: rnn的层数. Default: 1 + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param input_dropout: 对输入的dropout概率. Default: 0 + :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ def __init__(self, mode, Cell, input_size, hidden_size, num_layers=1, @@ -120,36 +132,43 @@ class VarRNNBase(nn.Module): output_x, hidden_x = cell(input, hi, mask_x, mask_h, is_reversed=(n_direction == 1)) return output_x, hidden_x - def forward(self, input, hx=None): + def forward(self, x, hx=None): + """ + + :param x: [batch, seq_len, input_size] 输入序列 + :param hx: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` + :return (output, ht): [batch, seq_len, hidden_size*num_direction] 输出序列 + :和 [batch, hidden_size*num_direction] 最后时刻隐状态 + """ is_lstm = self.is_lstm - is_packed = isinstance(input, PackedSequence) + is_packed = isinstance(x, PackedSequence) if not is_packed: - seq_len = input.size(1) if self.batch_first else input.size(0) - max_batch_size = input.size(0) if self.batch_first else input.size(1) + seq_len = x.size(1) if self.batch_first else x.size(0) + max_batch_size = x.size(0) if self.batch_first else x.size(1) seq_lens = torch.LongTensor([seq_len for _ in range(max_batch_size)]) - input, batch_sizes = pack_padded_sequence(input, seq_lens, batch_first=self.batch_first) + x, batch_sizes = pack_padded_sequence(x, seq_lens, batch_first=self.batch_first) else: - max_batch_size = int(input.batch_sizes[0]) - input, batch_sizes = input + max_batch_size = int(x.batch_sizes[0]) + x, batch_sizes = x if hx is None: - hx = input.new_zeros(self.num_layers * self.num_directions, - max_batch_size, self.hidden_size, requires_grad=True) + hx = x.new_zeros(self.num_layers * self.num_directions, + max_batch_size, self.hidden_size, requires_grad=True) if is_lstm: hx = (hx, hx.new_zeros(hx.size(), requires_grad=True)) - mask_x = input.new_ones((max_batch_size, self.input_size)) - mask_out = input.new_ones((max_batch_size, self.hidden_size * self.num_directions)) - mask_h_ones = input.new_ones((max_batch_size, self.hidden_size)) + mask_x = x.new_ones((max_batch_size, self.input_size)) + mask_out = x.new_ones((max_batch_size, self.hidden_size * self.num_directions)) + mask_h_ones = x.new_ones((max_batch_size, self.hidden_size)) nn.functional.dropout(mask_x, p=self.input_dropout, training=self.training, inplace=True) nn.functional.dropout(mask_out, p=self.hidden_dropout, training=self.training, inplace=True) - hidden = input.new_zeros((self.num_layers*self.num_directions, max_batch_size, self.hidden_size)) + hidden = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) if is_lstm: - cellstate = input.new_zeros((self.num_layers*self.num_directions, max_batch_size, self.hidden_size)) + cellstate = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) for layer in range(self.num_layers): output_list = [] - input_seq = PackedSequence(input, batch_sizes) + input_seq = PackedSequence(x, batch_sizes) mask_h = nn.functional.dropout(mask_h_ones, p=self.hidden_dropout, training=self.training, inplace=False) for direction in range(self.num_directions): output_x, hidden_x = self._forward_one(layer, direction, input_seq, hx, @@ -161,22 +180,32 @@ class VarRNNBase(nn.Module): cellstate[idx] = hidden_x[1] else: hidden[idx] = hidden_x - input = torch.cat(output_list, dim=-1) + x = torch.cat(output_list, dim=-1) if is_lstm: hidden = (hidden, cellstate) if is_packed: - output = PackedSequence(input, batch_sizes) + output = PackedSequence(x, batch_sizes) else: - input = PackedSequence(input, batch_sizes) - output, _ = pad_packed_sequence(input, batch_first=self.batch_first) + x = PackedSequence(x, batch_sizes) + output, _ = pad_packed_sequence(x, batch_first=self.batch_first) return output, hidden class VarLSTM(VarRNNBase): """Variational Dropout LSTM. + + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度 + :param num_layers: rnn的层数. Default: 1 + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param input_dropout: 对输入的dropout概率. Default: 0 + :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` """ def __init__(self, *args, **kwargs): @@ -185,6 +214,16 @@ class VarLSTM(VarRNNBase): class VarRNN(VarRNNBase): """Variational Dropout RNN. + + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度 + :param num_layers: rnn的层数. Default: 1 + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param input_dropout: 对输入的dropout概率. Default: 0 + :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ def __init__(self, *args, **kwargs): @@ -193,6 +232,16 @@ class VarRNN(VarRNNBase): class VarGRU(VarRNNBase): """Variational Dropout GRU. + + :param input_size: 输入 `x` 的特征维度 + :param hidden_size: 隐状态 `h` 的特征维度 + :param num_layers: rnn的层数. Default: 1 + :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` + :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 + :(batch, seq, feature). Default: ``False`` + :param input_dropout: 对输入的dropout概率. Default: 0 + :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 + :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` """ def __init__(self, *args, **kwargs): diff --git a/test/core/test_sampler.py b/test/core/test_sampler.py index b23af470..f3cbb77f 100644 --- a/test/core/test_sampler.py +++ b/test/core/test_sampler.py @@ -4,17 +4,11 @@ import unittest import torch from fastNLP.core.dataset import DataSet -from fastNLP.core.sampler import convert_to_torch_tensor, SequentialSampler, RandomSampler, \ +from fastNLP.core.sampler import SequentialSampler, RandomSampler, \ k_means_1d, k_means_bucketing, simple_sort_bucketing, BucketSampler class TestSampler(unittest.TestCase): - def test_convert_to_torch_tensor(self): - data = [[1, 2, 3, 4, 5], [5, 4, 3, 2, 1], [1, 3, 4, 5, 2]] - ans = convert_to_torch_tensor(data, False) - assert isinstance(ans, torch.Tensor) - assert tuple(ans.shape) == (3, 5) - def test_sequential_sampler(self): sampler = SequentialSampler() data = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10] diff --git a/test/models/test_biaffine_parser.py b/test/models/test_biaffine_parser.py index 88ba09b8..5d6c2102 100644 --- a/test/models/test_biaffine_parser.py +++ b/test/models/test_biaffine_parser.py @@ -44,34 +44,34 @@ data_file = """ def init_data(): ds = fastNLP.DataSet() - v = {'word_seq': fastNLP.Vocabulary(), - 'pos_seq': fastNLP.Vocabulary(), + v = {'words1': fastNLP.Vocabulary(), + 'words2': fastNLP.Vocabulary(), 'label_true': fastNLP.Vocabulary()} data = [] for line in data_file.split('\n'): line = line.split() if len(line) == 0 and len(data) > 0: data = list(zip(*data)) - ds.append(fastNLP.Instance(word_seq=data[1], - pos_seq=data[4], + ds.append(fastNLP.Instance(words1=data[1], + words2=data[4], arc_true=data[6], label_true=data[7])) data = [] elif len(line) > 0: data.append(line) - for name in ['word_seq', 'pos_seq', 'label_true']: + for name in ['words1', 'words2', 'label_true']: ds.apply(lambda x: [''] + list(x[name]), new_field_name=name) ds.apply(lambda x: v[name].add_word_lst(x[name])) - for name in ['word_seq', 'pos_seq', 'label_true']: + for name in ['words1', 'words2', 'label_true']: ds.apply(lambda x: [v[name].to_index(w) for w in x[name]], new_field_name=name) ds.apply(lambda x: [0] + list(map(int, x['arc_true'])), new_field_name='arc_true') - ds.apply(lambda x: len(x['word_seq']), new_field_name='seq_lens') - ds.set_input('word_seq', 'pos_seq', 'seq_lens', flag=True) - ds.set_target('arc_true', 'label_true', 'seq_lens', flag=True) - return ds, v['word_seq'], v['pos_seq'], v['label_true'] + ds.apply(lambda x: len(x['words1']), new_field_name='seq_len') + ds.set_input('words1', 'words2', 'seq_len', flag=True) + ds.set_target('arc_true', 'label_true', 'seq_len', flag=True) + return ds, v['words1'], v['words2'], v['label_true'] class TestBiaffineParser(unittest.TestCase): diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 600699a3..eb77321c 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -437,4 +437,10 @@ class TestTutorial(unittest.TestCase): ) tester.test() - os.chdir("../..") + def setUp(self): + import os + self._init_wd = os.path.abspath(os.curdir) + + def tearDown(self): + import os + os.chdir(self._init_wd) From 28ece53df07a4601ce75e560de0ebeb7f2272ecb Mon Sep 17 00:00:00 2001 From: yh_cc Date: Tue, 23 Apr 2019 14:52:56 +0800 Subject: [PATCH 045/173] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.io.rst | 24 +-- docs/source/fastNLP.models.rst | 40 ++-- docs/source/fastNLP.modules.aggregator.rst | 16 +- docs/source/fastNLP.modules.encoder.rst | 20 +- docs/source/fastNLP.modules.rst | 4 +- fastNLP/core/dataset.py | 222 ++++++++++++++++----- fastNLP/core/fieldarray.py | 5 + fastNLP/core/instance.py | 11 + fastNLP/core/utils.py | 43 ++-- fastNLP/io/embed_loader.py | 115 ----------- fastNLP/modules/decoder/utils.py | 3 +- reproduction/Biaffine_parser/run.py | 2 +- requirements.txt | 3 +- test/core/test_utils.py | 6 +- test/io/test_embed_loader.py | 6 - 15 files changed, 267 insertions(+), 253 deletions(-) diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index e73f27d3..e677b50a 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -4,48 +4,48 @@ fastNLP.io package Submodules ---------- -fastNLP.io.base\_loader module ------------------------------- +fastNLP.io.base_loader module +----------------------------- .. automodule:: fastNLP.io.base_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.config\_io module ----------------------------- +fastNLP.io.config_io module +--------------------------- .. automodule:: fastNLP.io.config_io :members: :undoc-members: :show-inheritance: -fastNLP.io.dataset\_loader module ---------------------------------- +fastNLP.io.dataset_loader module +-------------------------------- .. automodule:: fastNLP.io.dataset_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.embed\_loader module -------------------------------- +fastNLP.io.embed_loader module +------------------------------ .. automodule:: fastNLP.io.embed_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.file\_reader module ------------------------------- +fastNLP.io.file_reader module +----------------------------- .. automodule:: fastNLP.io.file_reader :members: :undoc-members: :show-inheritance: -fastNLP.io.model\_io module ---------------------------- +fastNLP.io.model_io module +-------------------------- .. automodule:: fastNLP.io.model_io :members: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index 3ebf9608..ccf6abb2 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -4,8 +4,8 @@ fastNLP.models package Submodules ---------- -fastNLP.models.base\_model module ---------------------------------- +fastNLP.models.base_model module +-------------------------------- .. automodule:: fastNLP.models.base_model :members: @@ -20,64 +20,64 @@ fastNLP.models.bert module :undoc-members: :show-inheritance: -fastNLP.models.biaffine\_parser module --------------------------------------- +fastNLP.models.biaffine_parser module +------------------------------------- .. automodule:: fastNLP.models.biaffine_parser :members: :undoc-members: :show-inheritance: -fastNLP.models.char\_language\_model module -------------------------------------------- +fastNLP.models.char_language_model module +----------------------------------------- .. automodule:: fastNLP.models.char_language_model :members: :undoc-members: :show-inheritance: -fastNLP.models.cnn\_text\_classification module ------------------------------------------------ +fastNLP.models.cnn_text_classification module +--------------------------------------------- .. automodule:: fastNLP.models.cnn_text_classification :members: :undoc-members: :show-inheritance: -fastNLP.models.enas\_controller module --------------------------------------- +fastNLP.models.enas_controller module +------------------------------------- .. automodule:: fastNLP.models.enas_controller :members: :undoc-members: :show-inheritance: -fastNLP.models.enas\_model module ---------------------------------- +fastNLP.models.enas_model module +-------------------------------- .. automodule:: fastNLP.models.enas_model :members: :undoc-members: :show-inheritance: -fastNLP.models.enas\_trainer module ------------------------------------ +fastNLP.models.enas_trainer module +---------------------------------- .. automodule:: fastNLP.models.enas_trainer :members: :undoc-members: :show-inheritance: -fastNLP.models.enas\_utils module ---------------------------------- +fastNLP.models.enas_utils module +-------------------------------- .. automodule:: fastNLP.models.enas_utils :members: :undoc-members: :show-inheritance: -fastNLP.models.sequence\_modeling module ----------------------------------------- +fastNLP.models.sequence_modeling module +--------------------------------------- .. automodule:: fastNLP.models.sequence_modeling :members: @@ -92,8 +92,8 @@ fastNLP.models.snli module :undoc-members: :show-inheritance: -fastNLP.models.star\_transformer module ---------------------------------------- +fastNLP.models.star_transformer module +-------------------------------------- .. automodule:: fastNLP.models.star_transformer :members: diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst index 63d351e4..478ac218 100644 --- a/docs/source/fastNLP.modules.aggregator.rst +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -12,32 +12,32 @@ fastNLP.modules.aggregator.attention module :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.avg\_pool module -------------------------------------------- +fastNLP.modules.aggregator.avg_pool module +------------------------------------------ .. automodule:: fastNLP.modules.aggregator.avg_pool :members: :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.kmax\_pool module --------------------------------------------- +fastNLP.modules.aggregator.kmax_pool module +------------------------------------------- .. automodule:: fastNLP.modules.aggregator.kmax_pool :members: :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.max\_pool module -------------------------------------------- +fastNLP.modules.aggregator.max_pool module +------------------------------------------ .. automodule:: fastNLP.modules.aggregator.max_pool :members: :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.self\_attention module -------------------------------------------------- +fastNLP.modules.aggregator.self_attention module +------------------------------------------------ .. automodule:: fastNLP.modules.aggregator.self_attention :members: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index ab93a169..4cc0b8ab 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -4,8 +4,8 @@ fastNLP.modules.encoder package Submodules ---------- -fastNLP.modules.encoder.char\_embedding module ----------------------------------------------- +fastNLP.modules.encoder.char_embedding module +--------------------------------------------- .. automodule:: fastNLP.modules.encoder.char_embedding :members: @@ -20,8 +20,8 @@ fastNLP.modules.encoder.conv module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.conv\_maxpool module --------------------------------------------- +fastNLP.modules.encoder.conv_maxpool module +------------------------------------------- .. automodule:: fastNLP.modules.encoder.conv_maxpool :members: @@ -52,16 +52,16 @@ fastNLP.modules.encoder.lstm module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.masked\_rnn module ------------------------------------------- +fastNLP.modules.encoder.masked_rnn module +----------------------------------------- .. automodule:: fastNLP.modules.encoder.masked_rnn :members: :undoc-members: :show-inheritance: -fastNLP.modules.encoder.star\_transformer module ------------------------------------------------- +fastNLP.modules.encoder.star_transformer module +----------------------------------------------- .. automodule:: fastNLP.modules.encoder.star_transformer :members: @@ -76,8 +76,8 @@ fastNLP.modules.encoder.transformer module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.variational\_rnn module ------------------------------------------------ +fastNLP.modules.encoder.variational_rnn module +---------------------------------------------- .. automodule:: fastNLP.modules.encoder.variational_rnn :members: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 57858176..53cc19a0 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -21,8 +21,8 @@ fastNLP.modules.dropout module :undoc-members: :show-inheritance: -fastNLP.modules.other\_modules module -------------------------------------- +fastNLP.modules.other_modules module +------------------------------------ .. automodule:: fastNLP.modules.other_modules :members: diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 3a4dfa55..030b9c3b 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -1,14 +1,114 @@ """ -fastNLP.core.DataSet的介绍文档 +DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格,每一行是一个sample(在fastNLP中被称为Instance),每一列是一个feature(在fastNLP中称为field)。 -DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格,每一行是一个instance(或sample),每一列是一个feature。 + .. _DataSet: -csv-table:: -:header: "Field1", "Field2", "Field3" -:widths:20, 10, 10 +.. csv-table:: Following is a demo layout of DataSet + :header: "sentence", "words", "seq_len" + + "This is the first instance .", "[This, is, the, first, instance, .]", 6 + "Second instance .", "[Second, instance, .]", 3 + "Third instance .", "[Third, instance, .]", 3 + "...", "[...]", "..." + +在fastNLP内部每一行是一个 Instance_ 对象; 每一列是一个 FieldArray_ 对象。 + +1. DataSet的创建 + + 创建DataSet主要有以下的3种方式 + + 1. 传入dict + + 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 + + 2. 通过构建Instance + + 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 + + 3. 通过list(Instance) + + 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 + + Example:: + + 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:: + + 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}) + + 3. 对DataSet中的内容处理 + + 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') + + 4. 删除DataSet的内容 + + 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数量减少 -"This is the first instance", ['This', 'is', 'the', 'first', 'instance'], 5 -"Second instance", ['Second', 'instance'], 2 """ @@ -22,7 +122,6 @@ from fastNLP.core.fieldarray import FieldArray 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. @@ -87,10 +186,7 @@ class DataSet(object): return inner_iter_func() def __getitem__(self, idx): - """Fetch Instance(s) at the `idx` position(s) in the dataset. - Notice: This method returns a copy of the actual instance(s). Any change to the returned value would not modify - the origin instance(s) of the DataSet. - If you want to make in-place changes to all Instances, use `apply` method. + """给定int的index,返回一个Instance; 给定slice,返回包含这个slice内容的新的DataSet。 :param idx: can be int or slice. :return: If `idx` is int, return an Instance object. @@ -145,33 +241,48 @@ class DataSet(object): def __repr__(self): return "DataSet(" + self.__inner_repr__() + ")" - def append(self, ins): + 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 ins: an Instance object + :param instance: an Instance object """ if len(self.field_arrays) == 0: # DataSet has no field yet - for name, field in ins.fields.items(): + for name, field in instance.fields.items(): field = field.tolist() if isinstance(field, np.ndarray) else field self.field_arrays[name] = FieldArray(name, [field]) # 第一个样本,必须用list包装起来 else: - if len(self.field_arrays) != len(ins.fields): + if len(self.field_arrays) != len(instance.fields): raise ValueError( "DataSet object has {} fields, but attempt to append an Instance object with {} fields." - .format(len(self.field_arrays), len(ins.fields))) - for name, field in ins.fields.items(): + .format(len(self.field_arrays), len(instance.fields))) + 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中. + + :param str field_name: 新加入的field的名称 + :param FieldArray fieldarray: 需要加入DataSet的field的内容 + :return: + """ + if not isinstance(fieldarray, FieldArray): + raise TypeError("Only fastNLP.FieldArray supported.") + if len(self) != len(fieldarray): + 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 :param str field_name: 新增的field的名称 :param list fields: 需要新增的field的内容 - :param None, Padder padder: 如果为None,则不进行pad。 + :param None,Padder padder: 如果为None,则不进行pad。 :param bool is_input: 新加入的field是否是input :param bool is_target: 新加入的field是否是target :param bool ignore_type: 是否忽略对新加入的field的类型检查 @@ -179,18 +290,28 @@ class DataSet(object): if len(self.field_arrays) != 0: if len(self) != len(fields): - raise RuntimeError(f"The field to append must have the same size as dataset. " + 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) def delete_field(self, field_name): - """删除field + """删除名为field_name的field :param str field_name: 需要删除的field的名称. """ self.field_arrays.pop(field_name) + def has_field(self, field_name): + """判断DataSet中是否有field_name这个field + + :param str field_name: field的名称 + :return: bool + """ + if isinstance(field_name, str): + return field_name in self.field_arrays + return False + def get_field(self, field_name): """获取field_name这个field @@ -318,25 +439,21 @@ class DataSet(object): def apply_field(self, func, field_name, new_field_name=None, **kwargs): """将DataSet中的每个instance中的`field_name`这个field传给func,并获取它的返回值. - :param callable func: input是instance的`field_name`这个field. - :param str field_name: 传入func的是哪个field. - :param str, None new_field_name: 将func返回的内容放入到什么field中 - - 1. str, 将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有的field相 - 同,则覆盖之前的field - - 2. None, 不创建新的field - :param kwargs: 合法的参数有以下三个 + :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。 + :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_field()." if field_name not in self: raise KeyError("DataSet has no field named `{}`.".format(field_name)) results = [] @@ -388,23 +505,19 @@ class DataSet(object): ignore_type=extra_param.get("ignore_type", False)) def apply(self, func, new_field_name=None, **kwargs): - """将DataSet中每个instance传入到func中,并获取它的返回值. - - :param callable func: 参数是DataSet中的instance - :param str, None new_field_name: 将func返回的内容放入到什么field中 - - 1. str, 将func的返回值放入这个名为`new_field_name`的新field中,如果名称与已有的field相 - 同,则覆盖之前的field + """ 将DataSet中每个instance传入到func中,并获取它的返回值. - 2. None, 不创建新的field - :param kwargs: 合法的参数有以下三个 + :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, 忽略其类型 - :return: List[], 里面的元素为func的返回值,所以list长度为DataSet的长度 + 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()." idx = -1 @@ -426,10 +539,10 @@ class DataSet(object): return results def drop(self, func, inplace=True): - """func接受一个instance,返回bool值,返回值为True时,该instance会被删除。 + """func接受一个instance,返回bool值,返回值为True时,该instance会被移除或者加入到返回的DataSet中。 :param callable func: 接受一个instance作为参数,返回bool值。为True时删除该instance - :param bool inplace: 是否在当前DataSet中直接删除instance。如果为False,返回值为一个删除了相应instance的新的DataSet + :param bool inplace: 是否在当前DataSet中直接删除instance。如果为False,返回值被删除的instance的组成的新DataSet :return: DataSet """ @@ -440,10 +553,13 @@ class DataSet(object): return self else: results = [ins for ins in self if not func(ins)] - dataset = DataSet(results) - for field_name, field in self.field_arrays.items(): - dataset.field_arrays[field_name].to(field) - return dataset + 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 diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index cc970b78..29b20a72 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -1,4 +1,9 @@ +""" +FieldArray是 DataSet_ 中一列的存储方式 + .. _FieldArray: + +""" import numpy as np diff --git a/fastNLP/core/instance.py b/fastNLP/core/instance.py index fff992cc..4271901f 100644 --- a/fastNLP/core/instance.py +++ b/fastNLP/core/instance.py @@ -1,3 +1,14 @@ +""" +Instance文档 + + .. _Instance: + +测试 + +""" + + + class Instance(object): """An Instance is an example of data. Example:: diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index b2c10fb4..4aad0101 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -24,47 +24,50 @@ def _prepare_cache_filepath(filepath): if not os.path.exists(cache_dir): os.makedirs(cache_dir) + # TODO 可以保存下缓存时的参数,如果load的时候发现参数不一致,发出警告。 -def cache_results(cache_filepath, refresh=False, verbose=1): +def cache_results(_cache_fp, _refresh=False, _verbose=1): def wrapper_(func): signature = inspect.signature(func) for key, _ in signature.parameters.items(): if key in ('cache_filepath', 'refresh', 'verbose'): raise RuntimeError("The function decorated by cache_results cannot have keyword `{}`.".format(key)) def wrapper(*args, **kwargs): - if 'cache_filepath' in kwargs: - _cache_filepath = kwargs.pop('cache_filepath') - assert isinstance(_cache_filepath, str), "cache_filepath can only be str." + if '_cache_fp' in kwargs: + cache_filepath = kwargs.pop('_cache_fp') + assert isinstance(cache_filepath, str), "_cache_fp can only be str." + else: + cache_filepath = _cache_fp + if '_refresh' in kwargs: + refresh = kwargs.pop('_refresh') + assert isinstance(refresh, bool), "_refresh can only be bool." else: - _cache_filepath = cache_filepath - if 'refresh' in kwargs: - _refresh = kwargs.pop('refresh') - assert isinstance(_refresh, bool), "refresh can only be bool." + refresh = _refresh + if '_verbose' in kwargs: + verbose = kwargs.pop('_verbose') + assert isinstance(verbose, int), "_verbose can only be integer." else: - _refresh = refresh - if 'verbose' in kwargs: - _verbose = kwargs.pop('verbose') - assert isinstance(_verbose, int), "verbose can only be integer." + verbose = _verbose refresh_flag = True - if _cache_filepath is not None and _refresh is False: + if cache_filepath is not None and refresh is False: # load data - if os.path.exists(_cache_filepath): - with open(_cache_filepath, 'rb') as f: + if os.path.exists(cache_filepath): + with open(cache_filepath, 'rb') as f: results = _pickle.load(f) if verbose==1: - print("Read cache from {}.".format(_cache_filepath)) + print("Read cache from {}.".format(cache_filepath)) refresh_flag = False if refresh_flag: results = func(*args, **kwargs) - if _cache_filepath is not None: + if cache_filepath is not None: if results is None: raise RuntimeError("The return value is None. Delete the decorator.") - _prepare_cache_filepath(_cache_filepath) - with open(_cache_filepath, 'wb') as f: + _prepare_cache_filepath(cache_filepath) + with open(cache_filepath, 'wb') as f: _pickle.dump(results, f) - print("Save cache to {}.".format(_cache_filepath)) + print("Save cache to {}.".format(cache_filepath)) return results return wrapper diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 258e8595..e1f20b94 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,7 +1,6 @@ import os import numpy as np -import torch from fastNLP.core.vocabulary import Vocabulary from fastNLP.io.base_loader import BaseLoader @@ -14,120 +13,6 @@ class EmbedLoader(BaseLoader): def __init__(self): super(EmbedLoader, self).__init__() - @staticmethod - def _load_glove(emb_file): - """Read file as a glove embedding - - file format: - embeddings are split by line, - for one embedding, word and numbers split by space - Example:: - - word_1 float_1 float_2 ... float_emb_dim - word_2 float_1 float_2 ... float_emb_dim - ... - """ - emb = {} - with open(emb_file, 'r', encoding='utf-8') as f: - for line in f: - line = list(filter(lambda w: len(w) > 0, line.strip().split(' '))) - if len(line) > 2: - emb[line[0]] = torch.Tensor(list(map(float, line[1:]))) - return emb - - @staticmethod - def _load_pretrain(emb_file, emb_type): - """Read txt data from embedding file and convert to np.array as pre-trained embedding - - :param str emb_file: the pre-trained embedding file path - :param str emb_type: the pre-trained embedding data format - :return: a dict of ``{str: np.array}`` - """ - if emb_type == 'glove': - return EmbedLoader._load_glove(emb_file) - else: - raise Exception("embedding type {} not support yet".format(emb_type)) - - @staticmethod - def load_embedding(emb_dim, emb_file, emb_type, vocab): - """Load the pre-trained embedding and combine with the given dictionary. - - :param int emb_dim: the dimension of the embedding. Should be the same as pre-trained embedding. - :param str emb_file: the pre-trained embedding file path. - :param str emb_type: the pre-trained embedding format, support glove now - :param Vocabulary vocab: a mapping from word to index, can be provided by user or built from pre-trained embedding - :return (embedding_tensor, vocab): - embedding_tensor - Tensor of shape (len(word_dict), emb_dim); - vocab - input vocab or vocab built by pre-train - - """ - pretrain = EmbedLoader._load_pretrain(emb_file, emb_type) - if vocab is None: - # build vocabulary from pre-trained embedding - vocab = Vocabulary() - for w in pretrain.keys(): - vocab.add(w) - embedding_tensor = torch.randn(len(vocab), emb_dim) - for w, v in pretrain.items(): - if len(v.shape) > 1 or emb_dim != v.shape[0]: - raise ValueError( - "Pretrained embedding dim is {}. Dimension dismatched. Required {}".format(v.shape, (emb_dim,))) - if vocab.has_word(w): - embedding_tensor[vocab[w]] = v - return embedding_tensor, vocab - - @staticmethod - def parse_glove_line(line): - line = line.split() - if len(line) <= 2: - raise RuntimeError("something goes wrong in parsing glove embedding") - return line[0], line[1:] - - @staticmethod - def str_list_2_vec(line): - try: - return torch.Tensor(list(map(float, line))) - except Exception: - raise RuntimeError("something goes wrong in parsing glove embedding") - - - @staticmethod - def fast_load_embedding(emb_dim, emb_file, vocab): - """Fast load the pre-trained embedding and combine with the given dictionary. - This loading method uses line-by-line operation. - - :param int emb_dim: the dimension of the embedding. Should be the same as pre-trained embedding. - :param str emb_file: the pre-trained embedding file path. - :param Vocabulary vocab: a mapping from word to index, can be provided by user or built from pre-trained embedding - :return embedding_matrix: numpy.ndarray - - """ - if vocab is None: - raise RuntimeError("You must provide a vocabulary.") - embedding_matrix = np.zeros(shape=(len(vocab), emb_dim), dtype=np.float32) - hit_flags = np.zeros(shape=(len(vocab),), dtype=int) - with open(emb_file, "r", encoding="utf-8") as f: - startline = f.readline() - if len(startline.split()) > 2: - f.seek(0) - for line in f: - word, vector = EmbedLoader.parse_glove_line(line) - if word in vocab: - vector = EmbedLoader.str_list_2_vec(vector) - if len(vector.shape) > 1 or emb_dim != vector.shape[0]: - raise ValueError("Pre-trained embedding dim is {}. Expect {}.".format(vector.shape, (emb_dim,))) - embedding_matrix[vocab[word]] = vector - hit_flags[vocab[word]] = 1 - - if np.sum(hit_flags) < len(vocab): - # some words from vocab are missing in pre-trained embedding - # we normally sample each dimension - vocab_embed = embedding_matrix[np.where(hit_flags)] - sampled_vectors = np.random.normal(vocab_embed.mean(axis=0), vocab_embed.std(axis=0), - size=(len(vocab) - np.sum(hit_flags), emb_dim)) - embedding_matrix[np.where(1 - hit_flags)] = sampled_vectors - return embedding_matrix - @staticmethod def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True, error='ignore'): """ diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 6e35af9a..120345b8 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -36,8 +36,7 @@ def viterbi_decode(feats, transitions, mask=None, unpad=False): vpath = feats.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) vscore = feats[0] - vscore += transitions[n_tags, :n_tags] - trans_score = transitions[:n_tags, :n_tags].view(1, n_tags, n_tags).data + 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) diff --git a/reproduction/Biaffine_parser/run.py b/reproduction/Biaffine_parser/run.py index c226ce69..a69d3d58 100644 --- a/reproduction/Biaffine_parser/run.py +++ b/reproduction/Biaffine_parser/run.py @@ -155,7 +155,7 @@ print('test len {}'.format(len(test_data))) def train(path): # test saving pipeline save_pipe(path) - embed = EmbedLoader.fast_load_embedding(model_args['word_emb_dim'], emb_file_name, word_v) + embed = EmbedLoader.load_with_vocab(emb_file_name, word_v) embed = torch.tensor(embed, dtype=torch.float32) # embed = EmbedLoader.fast_load_embedding(emb_dim=model_args['word_emb_dim'], emb_file=emb_file_name, vocab=word_v) diff --git a/requirements.txt b/requirements.txt index 45c84bc2..931ca285 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ numpy>=1.14.2 torch>=0.4.0 tensorboardX -tqdm>=4.28.1 \ No newline at end of file +tqdm>=4.28.1 +nltk>=3.4.1 \ No newline at end of file diff --git a/test/core/test_utils.py b/test/core/test_utils.py index 5c325127..11bb0f22 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -89,17 +89,17 @@ class TestCache(unittest.TestCase): def test_duplicate_keyword(self): with self.assertRaises(RuntimeError): @cache_results(None) - def func_verbose(a, verbose): + def func_verbose(a, _verbose): pass func_verbose(0, 1) with self.assertRaises(RuntimeError): @cache_results(None) - def func_cache(a, cache_filepath): + def func_cache(a, _cache_fp): pass func_cache(1, 2) with self.assertRaises(RuntimeError): @cache_results(None) - def func_refresh(a, refresh): + def func_refresh(a, _refresh): pass func_refresh(1, 2) diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index 9e325334..c41aec96 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -6,12 +6,6 @@ from fastNLP.io.embed_loader import EmbedLoader class TestEmbedLoader(unittest.TestCase): - def test_case(self): - vocab = Vocabulary() - vocab.update(["the", "in", "I", "to", "of", "hahaha"]) - embedding = EmbedLoader().fast_load_embedding(50, "test/data_for_tests/glove.6B.50d_test.txt", vocab) - self.assertEqual(tuple(embedding.shape), (len(vocab), 50)) - def test_load_with_vocab(self): vocab = Vocabulary() glove = "test/data_for_tests/glove.6B.50d_test.txt" From 1e9e42e839ac4f9f7cdab7aba21a53f6493efbea Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 16:43:22 +0800 Subject: [PATCH 046/173] update pooling --- fastNLP/modules/aggregator/__init__.py | 10 +- fastNLP/modules/aggregator/avg_pool.py | 36 ------- fastNLP/modules/aggregator/kmax_pool.py | 20 ---- fastNLP/modules/aggregator/max_pool.py | 38 ------- fastNLP/modules/aggregator/pooling.py | 133 ++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 99 deletions(-) delete mode 100644 fastNLP/modules/aggregator/avg_pool.py delete mode 100644 fastNLP/modules/aggregator/kmax_pool.py delete mode 100644 fastNLP/modules/aggregator/max_pool.py create mode 100644 fastNLP/modules/aggregator/pooling.py diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 43d60cac..c0a63fd3 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,8 +1,8 @@ -from .max_pool import MaxPool -from .max_pool import MaxPoolWithMask -from .avg_pool import AvgPool -from .avg_pool import MeanPoolWithMask -from .kmax_pool import KMaxPool +from .pooling import MaxPool +from .pooling import MaxPoolWithMask +from .pooling import AvgPool +from .pooling import MeanPoolWithMask +from .pooling import KMaxPool from .attention import Attention from .attention import BiAttention diff --git a/fastNLP/modules/aggregator/avg_pool.py b/fastNLP/modules/aggregator/avg_pool.py deleted file mode 100644 index e6f3fd4b..00000000 --- a/fastNLP/modules/aggregator/avg_pool.py +++ /dev/null @@ -1,36 +0,0 @@ -# python: 3.6 -# encoding: utf-8 - -import torch -import torch.nn as nn -import torch.nn.functional as F - - -class AvgPool(nn.Module): - """1-d average pooling module.""" - - def __init__(self, stride=None, padding=0): - super(AvgPool, self).__init__() - self.stride = stride - self.padding = padding - - def forward(self, x): - # [N,C,L] -> [N,C] - kernel_size = x.size(2) - x = F.max_pool1d( - input=x, - kernel_size=kernel_size, - stride=self.stride, - padding=self.padding) - return x.squeeze(dim=-1) - - -class MeanPoolWithMask(nn.Module): - def __init__(self): - super(MeanPoolWithMask, self).__init__() - self.inf = 10e12 - - def forward(self, tensor, mask, dim=0): - masks = mask.view(mask.size(0), mask.size(1), -1).float() - return torch.sum(tensor * masks, dim=dim) / torch.sum(masks, dim=1) - diff --git a/fastNLP/modules/aggregator/kmax_pool.py b/fastNLP/modules/aggregator/kmax_pool.py deleted file mode 100644 index 4d71130e..00000000 --- a/fastNLP/modules/aggregator/kmax_pool.py +++ /dev/null @@ -1,20 +0,0 @@ -# python: 3.6 -# encoding: utf-8 - -import torch -import torch.nn as nn -# import torch.nn.functional as F - - -class KMaxPool(nn.Module): - """K max-pooling module.""" - - def __init__(self, k=1): - super(KMaxPool, self).__init__() - self.k = k - - def forward(self, x): - # [N,C,L] -> [N,C*k] - x, index = torch.topk(x, self.k, dim=-1, sorted=False) - x = torch.reshape(x, (x.size(0), -1)) - return x diff --git a/fastNLP/modules/aggregator/max_pool.py b/fastNLP/modules/aggregator/max_pool.py deleted file mode 100644 index 60d68497..00000000 --- a/fastNLP/modules/aggregator/max_pool.py +++ /dev/null @@ -1,38 +0,0 @@ -# python: 3.6 -# encoding: utf-8 - -import torch -import torch.nn as nn -import torch.nn.functional as F - - -class MaxPool(nn.Module): - """1-d max-pooling module.""" - - def __init__(self, stride=None, padding=0, dilation=1): - super(MaxPool, self).__init__() - self.stride = stride - self.padding = padding - self.dilation = dilation - - def forward(self, x): - x = torch.transpose(x, 1, 2) # [N,L,C] -> [N,C,L] - kernel_size = x.size(2) - x = F.max_pool1d( # [N,L,C] -> [N,C,1] - input=x, - kernel_size=kernel_size, - stride=self.stride, - padding=self.padding, - dilation=self.dilation) - return x.squeeze(dim=-1) # [N,C,1] -> [N,C] - - -class MaxPoolWithMask(nn.Module): - def __init__(self): - super(MaxPoolWithMask, self).__init__() - self.inf = 10e12 - - def forward(self, tensor, mask, dim=0): - masks = mask.view(mask.size(0), mask.size(1), -1) - masks = masks.expand(-1, -1, tensor.size(2)).float() - return torch.max(tensor + masks.le(0.5).float() * -self.inf, dim=dim) diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py new file mode 100644 index 00000000..876f5fb1 --- /dev/null +++ b/fastNLP/modules/aggregator/pooling.py @@ -0,0 +1,133 @@ +# python: 3.6 +# encoding: utf-8 + +import torch +import torch.nn as nn + + +class MaxPool(nn.Module): + """Max-pooling模块。""" + + def __init__( + self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, + return_indices=False, ceil_mode=False + ): + """ + :param stride: 窗口移动大小,默认为kernel_size + :param padding: padding的内容,默认为0 + :param dilation: 控制窗口内元素移动距离的大小 + :param dimension: MaxPool的维度,支持1,2,3维。 + :param kernel_size: max pooling的窗口大小,默认为tensor最后k维,其中k为dimension + :param return_indices: + :param ceil_mode: + """ + super(MaxPool, self).__init__() + assert (1 <= dimension) and (dimension <= 3) + self.dimension = dimension + self.stride = stride + self.padding = padding + self.dilation = dilation + self.kernel_size = kernel_size + self.return_indices = return_indices + self.ceil_mode = ceil_mode + + def forward(self, x): + if self.dimension == 1: + pooling = nn.MaxPool1d( + stride=self.stride, padding=self.padding, dilation=self.dilation, + kernel_size=self.kernel_size if self.kernel_size is not None else x.size(-1), + return_indices=self.return_indices, ceil_mode=self.ceil_mode + ) + x = torch.transpose(x, 1, 2) # [N,L,C] -> [N,C,L] + elif self.dimension == 2: + pooling = nn.MaxPool2d( + stride=self.stride, padding=self.padding, dilation=self.dilation, + kernel_size=self.kernel_size if self.kernel_size is not None else (x.size(-2), x.size(-1)), + return_indices=self.return_indices, ceil_mode=self.ceil_mode + ) + else: + pooling = nn.MaxPool2d( + stride=self.stride, padding=self.padding, dilation=self.dilation, + kernel_size=self.kernel_size if self.kernel_size is not None else (x.size(-3), x.size(-2), x.size(-1)), + return_indices=self.return_indices, ceil_mode=self.ceil_mode + ) + x = pooling(x) + return x.squeeze(dim=-1) # [N,C,1] -> [N,C] + + +class MaxPoolWithMask(nn.Module): + """带mask矩阵的1维max pooling""" + def __init__(self): + super(MaxPoolWithMask, self).__init__() + self.inf = 10e12 + + def forward(self, tensor, mask, dim=1): + """ + :param torch.Tensor tensor: [batch_size, seq_len, channels] 初始tensor + :param torch.Tensor mask: [batch_size, seq_len] 0/1的mask矩阵 + :param int dim: 需要进行max pooling的维度 + :return: + """ + masks = mask.view(mask.size(0), mask.size(1), -1) + masks = masks.expand(-1, -1, tensor.size(2)).float() + return torch.max(tensor + masks.le(0.5).float() * -self.inf, dim=dim)[0] + + +class KMaxPool(nn.Module): + """K max-pooling module.""" + + def __init__(self, k=1): + super(KMaxPool, self).__init__() + self.k = k + + def forward(self, x): + """ + :param torch.Tensor x: [N, C, L] 初始tensor + :return: torch.Tensor x: [N, C*k] k-max pool后的结果 + """ + x, index = torch.topk(x, self.k, dim=-1, sorted=False) + x = torch.reshape(x, (x.size(0), -1)) + return x + + +class AvgPool(nn.Module): + """1-d average pooling module.""" + + def __init__(self, stride=None, padding=0): + super(AvgPool, self).__init__() + self.stride = stride + self.padding = padding + + def forward(self, x): + """ + :param torch.Tensor x: [N, C, L] 初始tensor + :return: torch.Tensor x: [N, C] avg pool后的结果 + """ + # [N,C,L] -> [N,C] + kernel_size = x.size(2) + pooling = nn.AvgPool1d( + kernel_size=kernel_size, + stride=self.stride, + padding=self.padding) + x = pooling(x) + return x.squeeze(dim=-1) + + +class MeanPoolWithMask(nn.Module): + def __init__(self): + super(MeanPoolWithMask, self).__init__() + self.inf = 10e12 + + def forward(self, tensor, mask, dim=1): + """ + :param torch.Tensor tensor: [batch_size, seq_len, channels] 初始tensor + :param torch.Tensor mask: [batch_size, seq_len] 0/1的mask矩阵 + :param int dim: 需要进行max pooling的维度 + :return: + """ + masks = mask.view(mask.size(0), mask.size(1), -1).float() + return torch.sum(tensor * masks, dim=dim) / torch.sum(masks, dim=1) + + + + From 7de69b60b896cee013564b254f21b245395da9d4 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 17:56:16 +0800 Subject: [PATCH 047/173] update bert --- fastNLP/component/__init__.py | 1 + fastNLP/component/bert_tokenizer.py | 378 +++++++++++++++++ fastNLP/models/bert.py | 637 ++++++++++++---------------- fastNLP/modules/encoder/__init__.py | 4 +- fastNLP/modules/encoder/bert.py | 362 ++++++++++++++++ 5 files changed, 1027 insertions(+), 355 deletions(-) create mode 100644 fastNLP/component/__init__.py create mode 100644 fastNLP/component/bert_tokenizer.py create mode 100644 fastNLP/modules/encoder/bert.py diff --git a/fastNLP/component/__init__.py b/fastNLP/component/__init__.py new file mode 100644 index 00000000..c6784aef --- /dev/null +++ b/fastNLP/component/__init__.py @@ -0,0 +1 @@ +from .bert_tokenizer import BertTokenizer diff --git a/fastNLP/component/bert_tokenizer.py b/fastNLP/component/bert_tokenizer.py new file mode 100644 index 00000000..6354076d --- /dev/null +++ b/fastNLP/component/bert_tokenizer.py @@ -0,0 +1,378 @@ +""" +bert_tokenizer.py is modified from huggingface/pytorch-pretrained-BERT, which is licensed under the Apache License 2.0. +""" +import collections +import os +import unicodedata +from io import open + + +PRETRAINED_VOCAB_ARCHIVE_MAP = { + 'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased-vocab.txt", + 'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased-vocab.txt", + 'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased-vocab.txt", + 'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased-vocab.txt", + 'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased-vocab.txt", + 'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased-vocab.txt", + 'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-vocab.txt", +} +PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP = { + 'bert-base-uncased': 512, + 'bert-large-uncased': 512, + 'bert-base-cased': 512, + 'bert-large-cased': 512, + 'bert-base-multilingual-uncased': 512, + 'bert-base-multilingual-cased': 512, + 'bert-base-chinese': 512, +} +VOCAB_NAME = 'vocab.txt' + + +def load_vocab(vocab_file): + """Loads a vocabulary file into a dictionary.""" + vocab = collections.OrderedDict() + index = 0 + with open(vocab_file, "r", encoding="utf-8") as reader: + while True: + token = reader.readline() + if not token: + break + token = token.strip() + vocab[token] = index + index += 1 + return vocab + + +def whitespace_tokenize(text): + """Runs basic whitespace cleaning and splitting on a piece of text.""" + text = text.strip() + if not text: + return [] + tokens = text.split() + return tokens + + +class BertTokenizer(object): + """Runs end-to-end tokenization: punctuation splitting + wordpiece""" + + def __init__(self, vocab_file, do_lower_case=True, max_len=None, do_basic_tokenize=True, + never_split=("[UNK]", "[SEP]", "[PAD]", "[CLS]", "[MASK]")): + """Constructs a BertTokenizer. + Args: + vocab_file: Path to a one-wordpiece-per-line vocabulary file + do_lower_case: Whether to lower case the input + Only has an effect when do_wordpiece_only=False + do_basic_tokenize: Whether to do basic tokenization before wordpiece. + max_len: An artificial maximum length to truncate tokenized sequences to; + Effective maximum length is always the minimum of this + value (if specified) and the underlying BERT model's + sequence length. + never_split: List of tokens which will never be split during tokenization. + Only has an effect when do_wordpiece_only=False + """ + if not os.path.isfile(vocab_file): + raise ValueError( + "Can't find a vocabulary file at path '{}'. To load the vocabulary from a Google pretrained " + "model use `tokenizer = BertTokenizer.from_pretrained(PRETRAINED_MODEL_NAME)`".format(vocab_file)) + self.vocab = load_vocab(vocab_file) + self.ids_to_tokens = collections.OrderedDict( + [(ids, tok) for tok, ids in self.vocab.items()]) + self.do_basic_tokenize = do_basic_tokenize + if do_basic_tokenize: + self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case, + never_split=never_split) + self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab) + self.max_len = max_len if max_len is not None else int(1e12) + + def tokenize(self, text): + split_tokens = [] + if self.do_basic_tokenize: + for token in self.basic_tokenizer.tokenize(text): + for sub_token in self.wordpiece_tokenizer.tokenize(token): + split_tokens.append(sub_token) + else: + split_tokens = self.wordpiece_tokenizer.tokenize(text) + return split_tokens + + def convert_tokens_to_ids(self, tokens): + """Converts a sequence of tokens into ids using the vocab.""" + ids = [] + for token in tokens: + ids.append(self.vocab[token]) + if len(ids) > self.max_len: + print( + "WARNING!\n\"" + "Token indices sequence length is longer than the specified maximum " + "sequence length for this BERT model ({} > {}). Running this" + " sequence through BERT will result in indexing errors".format(len(ids), self.max_len) + ) + return ids + + def convert_ids_to_tokens(self, ids): + """Converts a sequence of ids in wordpiece tokens using the vocab.""" + tokens = [] + for i in ids: + tokens.append(self.ids_to_tokens[i]) + return tokens + + def save_vocabulary(self, vocab_path): + """Save the tokenizer vocabulary to a directory or file.""" + index = 0 + if os.path.isdir(vocab_path): + vocab_file = os.path.join(vocab_path, VOCAB_NAME) + with open(vocab_file, "w", encoding="utf-8") as writer: + for token, token_index in sorted(self.vocab.items(), key=lambda kv: kv[1]): + if index != token_index: + print("Saving vocabulary to {}: vocabulary indices are not consecutive." + " Please check that the vocabulary is not corrupted!".format(vocab_file)) + index = token_index + writer.write(token + u'\n') + index += 1 + return vocab_file + + @classmethod + def from_pretrained(cls, pretrained_model_name_or_path, cache_dir=None, *inputs, **kwargs): + """ + Instantiate a PreTrainedBertModel from a pre-trained model file. + Download and cache the pre-trained model file if needed. + """ + if pretrained_model_name_or_path in PRETRAINED_VOCAB_ARCHIVE_MAP: + vocab_file = PRETRAINED_VOCAB_ARCHIVE_MAP[pretrained_model_name_or_path] + if '-cased' in pretrained_model_name_or_path and kwargs.get('do_lower_case', True): + print("The pre-trained model you are loading is a cased model but you have not set " + "`do_lower_case` to False. We are setting `do_lower_case=False` for you but " + "you may want to check this behavior.") + kwargs['do_lower_case'] = False + elif '-cased' not in pretrained_model_name_or_path and not kwargs.get('do_lower_case', True): + print("The pre-trained model you are loading is an uncased model but you have set " + "`do_lower_case` to False. We are setting `do_lower_case=True` for you " + "but you may want to check this behavior.") + kwargs['do_lower_case'] = True + else: + vocab_file = pretrained_model_name_or_path + if os.path.isdir(vocab_file): + vocab_file = os.path.join(vocab_file, VOCAB_NAME) + # redirect to the cache, if necessary + resolved_vocab_file = vocab_file + print("loading vocabulary file {}".format(vocab_file)) + if pretrained_model_name_or_path in PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP: + # if we're using a pretrained model, ensure the tokenizer wont index sequences longer + # than the number of positional embeddings + max_len = PRETRAINED_VOCAB_POSITIONAL_EMBEDDINGS_SIZE_MAP[pretrained_model_name_or_path] + kwargs['max_len'] = min(kwargs.get('max_len', int(1e12)), max_len) + # Instantiate tokenizer. + tokenizer = cls(resolved_vocab_file, *inputs, **kwargs) + return tokenizer + + +class BasicTokenizer(object): + """Runs basic tokenization (punctuation splitting, lower casing, etc.).""" + + def __init__(self, + do_lower_case=True, + never_split=("[UNK]", "[SEP]", "[PAD]", "[CLS]", "[MASK]")): + """Constructs a BasicTokenizer. + Args: + do_lower_case: Whether to lower case the input. + """ + self.do_lower_case = do_lower_case + self.never_split = never_split + + def tokenize(self, text): + """Tokenizes a piece of text.""" + text = self._clean_text(text) + # This was added on November 1st, 2018 for the multilingual and Chinese + # models. This is also applied to the English models now, but it doesn't + # matter since the English models were not trained on any Chinese data + # and generally don't have any Chinese data in them (there are Chinese + # characters in the vocabulary because Wikipedia does have some Chinese + # words in the English Wikipedia.). + text = self._tokenize_chinese_chars(text) + orig_tokens = whitespace_tokenize(text) + split_tokens = [] + for token in orig_tokens: + if self.do_lower_case and token not in self.never_split: + token = token.lower() + token = self._run_strip_accents(token) + split_tokens.extend(self._run_split_on_punc(token)) + + output_tokens = whitespace_tokenize(" ".join(split_tokens)) + return output_tokens + + def _run_strip_accents(self, text): + """Strips accents from a piece of text.""" + text = unicodedata.normalize("NFD", text) + output = [] + for char in text: + cat = unicodedata.category(char) + if cat == "Mn": + continue + output.append(char) + return "".join(output) + + def _run_split_on_punc(self, text): + """Splits punctuation on a piece of text.""" + if text in self.never_split: + return [text] + chars = list(text) + i = 0 + start_new_word = True + output = [] + while i < len(chars): + char = chars[i] + if _is_punctuation(char): + output.append([char]) + start_new_word = True + else: + if start_new_word: + output.append([]) + start_new_word = False + output[-1].append(char) + i += 1 + + return ["".join(x) for x in output] + + def _tokenize_chinese_chars(self, text): + """Adds whitespace around any CJK character.""" + output = [] + for char in text: + cp = ord(char) + if self._is_chinese_char(cp): + output.append(" ") + output.append(char) + output.append(" ") + else: + output.append(char) + return "".join(output) + + def _is_chinese_char(self, cp): + """Checks whether CP is the codepoint of a CJK character.""" + # This defines a "chinese character" as anything in the CJK Unicode block: + # https://en.wikipedia.org/wiki/CJK_Unified_Ideographs_(Unicode_block) + # + # Note that the CJK Unicode block is NOT all Japanese and Korean characters, + # despite its name. The modern Korean Hangul alphabet is a different block, + # as is Japanese Hiragana and Katakana. Those alphabets are used to write + # space-separated words, so they are not treated specially and handled + # like the all of the other languages. + if ((cp >= 0x4E00 and cp <= 0x9FFF) or # + (cp >= 0x3400 and cp <= 0x4DBF) or # + (cp >= 0x20000 and cp <= 0x2A6DF) or # + (cp >= 0x2A700 and cp <= 0x2B73F) or # + (cp >= 0x2B740 and cp <= 0x2B81F) or # + (cp >= 0x2B820 and cp <= 0x2CEAF) or + (cp >= 0xF900 and cp <= 0xFAFF) or # + (cp >= 0x2F800 and cp <= 0x2FA1F)): # + return True + + return False + + def _clean_text(self, text): + """Performs invalid character removal and whitespace cleanup on text.""" + output = [] + for char in text: + cp = ord(char) + if cp == 0 or cp == 0xfffd or _is_control(char): + continue + if _is_whitespace(char): + output.append(" ") + else: + output.append(char) + return "".join(output) + + +class WordpieceTokenizer(object): + """Runs WordPiece tokenization.""" + + def __init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=100): + self.vocab = vocab + self.unk_token = unk_token + self.max_input_chars_per_word = max_input_chars_per_word + + def tokenize(self, text): + """Tokenizes a piece of text into its word pieces. + This uses a greedy longest-match-first algorithm to perform tokenization + using the given vocabulary. + For example: + input = "unaffable" + output = ["un", "##aff", "##able"] + Args: + text: A single token or whitespace separated tokens. This should have + already been passed through `BasicTokenizer`. + Returns: + A list of wordpiece tokens. + """ + + output_tokens = [] + for token in whitespace_tokenize(text): + chars = list(token) + if len(chars) > self.max_input_chars_per_word: + output_tokens.append(self.unk_token) + continue + + is_bad = False + start = 0 + sub_tokens = [] + while start < len(chars): + end = len(chars) + cur_substr = None + while start < end: + substr = "".join(chars[start:end]) + if start > 0: + substr = "##" + substr + if substr in self.vocab: + cur_substr = substr + break + end -= 1 + if cur_substr is None: + is_bad = True + break + sub_tokens.append(cur_substr) + start = end + + if is_bad: + output_tokens.append(self.unk_token) + else: + output_tokens.extend(sub_tokens) + return output_tokens + + +def _is_whitespace(char): + """Checks whether `chars` is a whitespace character.""" + # \t, \n, and \r are technically contorl characters but we treat them + # as whitespace since they are generally considered as such. + if char == " " or char == "\t" or char == "\n" or char == "\r": + return True + cat = unicodedata.category(char) + if cat == "Zs": + return True + return False + + +def _is_control(char): + """Checks whether `chars` is a control character.""" + # These are technically control characters but we count them as whitespace + # characters. + if char == "\t" or char == "\n" or char == "\r": + return False + cat = unicodedata.category(char) + if cat.startswith("C"): + return True + return False + + +def _is_punctuation(char): + """Checks whether `chars` is a punctuation character.""" + cp = ord(char) + # We treat all non-letter/number ASCII as punctuation. + # Characters such as "^", "$", and "`" are not in the Unicode + # Punctuation class but we treat them as punctuation anyways, for + # consistency. + if ((cp >= 33 and cp <= 47) or (cp >= 58 and cp <= 64) or + (cp >= 91 and cp <= 96) or (cp >= 123 and cp <= 126)): + return True + cat = unicodedata.category(char) + if cat.startswith("P"): + return True + return False + diff --git a/fastNLP/models/bert.py b/fastNLP/models/bert.py index e87f6f5d..98d81025 100644 --- a/fastNLP/models/bert.py +++ b/fastNLP/models/bert.py @@ -2,361 +2,290 @@ bert.py is modified from huggingface/pytorch-pretrained-BERT, which is licensed under the Apache License 2.0. """ -import copy -import json -import math -import os - import torch from torch import nn -CONFIG_FILE = 'bert_config.json' -MODEL_WEIGHTS = 'pytorch_model.bin' - - -def gelu(x): - return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0))) - - -def swish(x): - return x * torch.sigmoid(x) - - -ACT2FN = {"gelu": gelu, "relu": torch.nn.functional.relu, "swish": swish} - - -class BertLayerNorm(nn.Module): - def __init__(self, hidden_size, eps=1e-12): - super(BertLayerNorm, self).__init__() - self.weight = nn.Parameter(torch.ones(hidden_size)) - self.bias = nn.Parameter(torch.zeros(hidden_size)) - self.variance_epsilon = eps - - def forward(self, x): - u = x.mean(-1, keepdim=True) - s = (x - u).pow(2).mean(-1, keepdim=True) - x = (x - u) / torch.sqrt(s + self.variance_epsilon) - return self.weight * x + self.bias - - -class BertEmbeddings(nn.Module): - def __init__(self, vocab_size, hidden_size, max_position_embeddings, type_vocab_size, hidden_dropout_prob): - super(BertEmbeddings, self).__init__() - self.word_embeddings = nn.Embedding(vocab_size, hidden_size) - self.position_embeddings = nn.Embedding(max_position_embeddings, hidden_size) - self.token_type_embeddings = nn.Embedding(type_vocab_size, hidden_size) - - # self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load - # any TensorFlow checkpoint file - self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) - self.dropout = nn.Dropout(hidden_dropout_prob) - - def forward(self, input_ids, token_type_ids=None): - seq_length = input_ids.size(1) - position_ids = torch.arange(seq_length, dtype=torch.long, device=input_ids.device) - position_ids = position_ids.unsqueeze(0).expand_as(input_ids) - if token_type_ids is None: - token_type_ids = torch.zeros_like(input_ids) - - words_embeddings = self.word_embeddings(input_ids) - position_embeddings = self.position_embeddings(position_ids) - token_type_embeddings = self.token_type_embeddings(token_type_ids) - - embeddings = words_embeddings + position_embeddings + token_type_embeddings - embeddings = self.LayerNorm(embeddings) - embeddings = self.dropout(embeddings) - return embeddings - - -class BertSelfAttention(nn.Module): - def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob): - super(BertSelfAttention, self).__init__() - if hidden_size % num_attention_heads != 0: - raise ValueError( - "The hidden size (%d) is not a multiple of the number of attention " - "heads (%d)" % (hidden_size, num_attention_heads)) - self.num_attention_heads = num_attention_heads - self.attention_head_size = int(hidden_size / num_attention_heads) - self.all_head_size = self.num_attention_heads * self.attention_head_size - - self.query = nn.Linear(hidden_size, self.all_head_size) - self.key = nn.Linear(hidden_size, self.all_head_size) - self.value = nn.Linear(hidden_size, self.all_head_size) - - self.dropout = nn.Dropout(attention_probs_dropout_prob) - - def transpose_for_scores(self, x): - new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) - x = x.view(*new_x_shape) - return x.permute(0, 2, 1, 3) - - def forward(self, hidden_states, attention_mask): - mixed_query_layer = self.query(hidden_states) - mixed_key_layer = self.key(hidden_states) - mixed_value_layer = self.value(hidden_states) - - query_layer = self.transpose_for_scores(mixed_query_layer) - key_layer = self.transpose_for_scores(mixed_key_layer) - value_layer = self.transpose_for_scores(mixed_value_layer) - - # Take the dot product between "query" and "key" to get the raw attention scores. - attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) - attention_scores = attention_scores / math.sqrt(self.attention_head_size) - # Apply the attention mask is (precomputed for all layers in BertModel forward() function) - attention_scores = attention_scores + attention_mask - - # Normalize the attention scores to probabilities. - attention_probs = nn.Softmax(dim=-1)(attention_scores) - - # This is actually dropping out entire tokens to attend to, which might - # seem a bit unusual, but is taken from the original Transformer paper. - attention_probs = self.dropout(attention_probs) - - context_layer = torch.matmul(attention_probs, value_layer) - context_layer = context_layer.permute(0, 2, 1, 3).contiguous() - new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,) - context_layer = context_layer.view(*new_context_layer_shape) - return context_layer - - -class BertSelfOutput(nn.Module): - def __init__(self, hidden_size, hidden_dropout_prob): - super(BertSelfOutput, self).__init__() - self.dense = nn.Linear(hidden_size, hidden_size) - self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) - self.dropout = nn.Dropout(hidden_dropout_prob) - - def forward(self, hidden_states, input_tensor): - hidden_states = self.dense(hidden_states) - hidden_states = self.dropout(hidden_states) - hidden_states = self.LayerNorm(hidden_states + input_tensor) - return hidden_states - - -class BertAttention(nn.Module): - def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob): - super(BertAttention, self).__init__() - self.self = BertSelfAttention(hidden_size, num_attention_heads, attention_probs_dropout_prob) - self.output = BertSelfOutput(hidden_size, hidden_dropout_prob) - - def forward(self, input_tensor, attention_mask): - self_output = self.self(input_tensor, attention_mask) - attention_output = self.output(self_output, input_tensor) - return attention_output - - -class BertIntermediate(nn.Module): - def __init__(self, hidden_size, intermediate_size, hidden_act): - super(BertIntermediate, self).__init__() - self.dense = nn.Linear(hidden_size, intermediate_size) - self.intermediate_act_fn = ACT2FN[hidden_act] \ - if isinstance(hidden_act, str) else hidden_act - - def forward(self, hidden_states): - hidden_states = self.dense(hidden_states) - hidden_states = self.intermediate_act_fn(hidden_states) - return hidden_states - - -class BertOutput(nn.Module): - def __init__(self, hidden_size, intermediate_size, hidden_dropout_prob): - super(BertOutput, self).__init__() - self.dense = nn.Linear(intermediate_size, hidden_size) - self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) - self.dropout = nn.Dropout(hidden_dropout_prob) - - def forward(self, hidden_states, input_tensor): - hidden_states = self.dense(hidden_states) - hidden_states = self.dropout(hidden_states) - hidden_states = self.LayerNorm(hidden_states + input_tensor) - return hidden_states - - -class BertLayer(nn.Module): - def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob, - intermediate_size, hidden_act): - super(BertLayer, self).__init__() - self.attention = BertAttention(hidden_size, num_attention_heads, attention_probs_dropout_prob, - hidden_dropout_prob) - self.intermediate = BertIntermediate(hidden_size, intermediate_size, hidden_act) - self.output = BertOutput(hidden_size, intermediate_size, hidden_dropout_prob) - - def forward(self, hidden_states, attention_mask): - attention_output = self.attention(hidden_states, attention_mask) - intermediate_output = self.intermediate(attention_output) - layer_output = self.output(intermediate_output, attention_output) - return layer_output - - -class BertEncoder(nn.Module): - def __init__(self, num_hidden_layers, hidden_size, num_attention_heads, attention_probs_dropout_prob, - hidden_dropout_prob, - intermediate_size, hidden_act): - super(BertEncoder, self).__init__() - layer = BertLayer(hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob, - intermediate_size, hidden_act) - self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(num_hidden_layers)]) - - def forward(self, hidden_states, attention_mask, output_all_encoded_layers=True): - all_encoder_layers = [] - for layer_module in self.layer: - hidden_states = layer_module(hidden_states, attention_mask) - if output_all_encoded_layers: - all_encoder_layers.append(hidden_states) - if not output_all_encoded_layers: - all_encoder_layers.append(hidden_states) - return all_encoder_layers - - -class BertPooler(nn.Module): - def __init__(self, hidden_size): - super(BertPooler, self).__init__() - self.dense = nn.Linear(hidden_size, hidden_size) - self.activation = nn.Tanh() - - def forward(self, hidden_states): - # We "pool" the model by simply taking the hidden state corresponding - # to the first token. - first_token_tensor = hidden_states[:, 0] - pooled_output = self.dense(first_token_tensor) - pooled_output = self.activation(pooled_output) - return pooled_output - - -class BertModel(nn.Module): - """Bidirectional Embedding Representations from Transformers. - - If you want to use pre-trained weights, please download from the following sources provided by pytorch-pretrained-BERT. - sources:: - - 'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz", - 'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased.tar.gz", - 'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased.tar.gz", - 'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased.tar.gz", - 'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased.tar.gz", - 'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased.tar.gz", - 'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese.tar.gz", - - - Construct a BERT model with pre-trained weights:: - - model = BertModel.from_pretrained("path/to/weights/directory") - +from .base_model import BaseModel +from fastNLP.modules.encoder import BertModel + + +class BertForSequenceClassification(BaseModel): + """BERT model for classification. + This module is composed of the BERT model with a linear layer on top of + the pooled output. + Params: + `config`: a BertConfig class instance with the configuration to build a new model. + `num_labels`: the number of classes for the classifier. Default = 2. + Inputs: + `input_ids`: a torch.LongTensor of shape [batch_size, sequence_length] + with the word token indices in the vocabulary. Items in the batch should begin with the special "CLS" token. (see the tokens preprocessing logic in the scripts + `extract_features.py`, `run_classifier.py` and `run_squad.py`) + `token_type_ids`: an optional torch.LongTensor of shape [batch_size, sequence_length] with the token + types indices selected in [0, 1]. Type 0 corresponds to a `sentence A` and type 1 corresponds to + a `sentence B` token (see BERT paper for more details). + `attention_mask`: an optional torch.LongTensor of shape [batch_size, sequence_length] with indices + selected in [0, 1]. It's a mask to be used if the input sequence length is smaller than the max + input sequence length in the current batch. It's the mask that we typically use for attention when + a batch has varying length sentences. + `labels`: labels for the classification output: torch.LongTensor of shape [batch_size] + with indices selected in [0, ..., num_labels]. + Outputs: + if `labels` is not `None`: + Outputs the CrossEntropy classification loss of the output with the labels. + if `labels` is `None`: + Outputs the classification logits of shape [batch_size, num_labels]. + Example usage: + ```python + # Already been converted into WordPiece token ids + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, + num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + num_labels = 2 + model = BertForSequenceClassification(config, num_labels) + logits = model(input_ids, token_type_ids, input_mask) + ``` """ - - def __init__(self, vocab_size, - hidden_size=768, - num_hidden_layers=12, - num_attention_heads=12, - intermediate_size=3072, - hidden_act="gelu", - hidden_dropout_prob=0.1, - attention_probs_dropout_prob=0.1, - max_position_embeddings=512, - type_vocab_size=2, - initializer_range=0.02, **kwargs): - super(BertModel, self).__init__() - self.embeddings = BertEmbeddings(vocab_size, hidden_size, max_position_embeddings, - type_vocab_size, hidden_dropout_prob) - self.encoder = BertEncoder(num_hidden_layers, hidden_size, num_attention_heads, - attention_probs_dropout_prob, hidden_dropout_prob, intermediate_size, - hidden_act) - self.pooler = BertPooler(hidden_size) - self.initializer_range = initializer_range - - self.apply(self.init_bert_weights) - - def init_bert_weights(self, module): - if isinstance(module, (nn.Linear, nn.Embedding)): - # Slightly different from the TF version which uses truncated_normal for initialization - # cf https://github.com/pytorch/pytorch/pull/5617 - module.weight.data.normal_(mean=0.0, std=self.initializer_range) - elif isinstance(module, BertLayerNorm): - module.bias.data.zero_() - module.weight.data.fill_(1.0) - if isinstance(module, nn.Linear) and module.bias is not None: - module.bias.data.zero_() - - def forward(self, input_ids, token_type_ids=None, attention_mask=None, output_all_encoded_layers=True): - if attention_mask is None: - attention_mask = torch.ones_like(input_ids) - if token_type_ids is None: - token_type_ids = torch.zeros_like(input_ids) - - # We create a 3D attention mask from a 2D tensor mask. - # Sizes are [batch_size, 1, 1, to_seq_length] - # So we can broadcast to [batch_size, num_heads, from_seq_length, to_seq_length] - # this attention mask is more simple than the triangular masking of causal attention - # used in OpenAI GPT, we just need to prepare the broadcast dimension here. - extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) - - # Since attention_mask is 1.0 for positions we want to attend and 0.0 for - # masked positions, this operation will create a tensor which is 0.0 for - # positions we want to attend and -10000.0 for masked positions. - # Since we are adding it to the raw scores before the softmax, this is - # effectively the same as removing these entirely. - extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype) # fp16 compatibility - extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0 - - embedding_output = self.embeddings(input_ids, token_type_ids) - encoded_layers = self.encoder(embedding_output, - extended_attention_mask, - output_all_encoded_layers=output_all_encoded_layers) - sequence_output = encoded_layers[-1] - pooled_output = self.pooler(sequence_output) - if not output_all_encoded_layers: - encoded_layers = encoded_layers[-1] - return encoded_layers, pooled_output - - @classmethod - def from_pretrained(cls, pretrained_model_dir, state_dict=None, *inputs, **kwargs): - # Load config - config_file = os.path.join(pretrained_model_dir, CONFIG_FILE) - config = json.load(open(config_file, "r")) - # config = BertConfig.from_json_file(config_file) - # logger.info("Model config {}".format(config)) - # Instantiate model. - model = cls(*inputs, **config, **kwargs) - if state_dict is None: - weights_path = os.path.join(pretrained_model_dir, MODEL_WEIGHTS) - state_dict = torch.load(weights_path) - - old_keys = [] - new_keys = [] - for key in state_dict.keys(): - new_key = None - if 'gamma' in key: - new_key = key.replace('gamma', 'weight') - if 'beta' in key: - new_key = key.replace('beta', 'bias') - if new_key: - old_keys.append(key) - new_keys.append(new_key) - for old_key, new_key in zip(old_keys, new_keys): - state_dict[new_key] = state_dict.pop(old_key) - - missing_keys = [] - unexpected_keys = [] - error_msgs = [] - # copy state_dict so _load_from_state_dict can modify it - metadata = getattr(state_dict, '_metadata', None) - state_dict = state_dict.copy() - if metadata is not None: - state_dict._metadata = metadata - - def load(module, prefix=''): - local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {}) - module._load_from_state_dict( - state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs) - for name, child in module._modules.items(): - if child is not None: - load(child, prefix + name + '.') - - load(model, prefix='' if hasattr(model, 'bert') else 'bert.') - if len(missing_keys) > 0: - print("Weights of {} not initialized from pretrained model: {}".format( - model.__class__.__name__, missing_keys)) - if len(unexpected_keys) > 0: - print("Weights from pretrained model not used in {}: {}".format( - model.__class__.__name__, unexpected_keys)) - return model + def __init__(self, config, num_labels, bert_dir): + super(BertForSequenceClassification, self).__init__() + self.num_labels = num_labels + self.bert = BertModel.from_pretrained(bert_dir) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.classifier = nn.Linear(config.hidden_size, num_labels) + + def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None): + _, pooled_output = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False) + pooled_output = self.dropout(pooled_output) + logits = self.classifier(pooled_output) + + if labels is not None: + loss_fct = nn.CrossEntropyLoss() + loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) + return {"pred": logits, "loss": loss} + else: + return {"pred": logits} + + def predict(self, input_ids, token_type_ids=None, attention_mask=None): + logits = self.forward(input_ids, token_type_ids, attention_mask) + return {"pred": torch.argmax(logits, dim=-1)} + + +class BertForMultipleChoice(BaseModel): + """BERT model for multiple choice tasks. + This module is composed of the BERT model with a linear layer on top of + the pooled output. + Params: + `config`: a BertConfig class instance with the configuration to build a new model. + `num_choices`: the number of classes for the classifier. Default = 2. + Inputs: + `input_ids`: a torch.LongTensor of shape [batch_size, num_choices, sequence_length] + with the word token indices in the vocabulary(see the tokens preprocessing logic in the scripts + `extract_features.py`, `run_classifier.py` and `run_squad.py`) + `token_type_ids`: an optional torch.LongTensor of shape [batch_size, num_choices, sequence_length] + with the token types indices selected in [0, 1]. Type 0 corresponds to a `sentence A` + and type 1 corresponds to a `sentence B` token (see BERT paper for more details). + `attention_mask`: an optional torch.LongTensor of shape [batch_size, num_choices, sequence_length] with indices + selected in [0, 1]. It's a mask to be used if the input sequence length is smaller than the max + input sequence length in the current batch. It's the mask that we typically use for attention when + a batch has varying length sentences. + `labels`: labels for the classification output: torch.LongTensor of shape [batch_size] + with indices selected in [0, ..., num_choices]. + Outputs: + if `labels` is not `None`: + Outputs the CrossEntropy classification loss of the output with the labels. + if `labels` is `None`: + Outputs the classification logits of shape [batch_size, num_labels]. + Example usage: + ```python + # Already been converted into WordPiece token ids + input_ids = torch.LongTensor([[[31, 51, 99], [15, 5, 0]], [[12, 16, 42], [14, 28, 57]]]) + input_mask = torch.LongTensor([[[1, 1, 1], [1, 1, 0]],[[1,1,0], [1, 0, 0]]]) + token_type_ids = torch.LongTensor([[[0, 0, 1], [0, 1, 0]],[[0, 1, 1], [0, 0, 1]]]) + config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, + num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + num_choices = 2 + model = BertForMultipleChoice(config, num_choices, bert_dir) + logits = model(input_ids, token_type_ids, input_mask) + ``` + """ + def __init__(self, config, num_choices, bert_dir): + super(BertForMultipleChoice, self).__init__() + self.num_choices = num_choices + self.bert = BertModel.from_pretrained(bert_dir) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.classifier = nn.Linear(config.hidden_size, 1) + + def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None): + flat_input_ids = input_ids.view(-1, input_ids.size(-1)) + flat_token_type_ids = token_type_ids.view(-1, token_type_ids.size(-1)) + flat_attention_mask = attention_mask.view(-1, attention_mask.size(-1)) + _, pooled_output = self.bert(flat_input_ids, flat_token_type_ids, flat_attention_mask, output_all_encoded_layers=False) + pooled_output = self.dropout(pooled_output) + logits = self.classifier(pooled_output) + reshaped_logits = logits.view(-1, self.num_choices) + + if labels is not None: + loss_fct = nn.CrossEntropyLoss() + loss = loss_fct(reshaped_logits, labels) + return {"pred": reshaped_logits, "loss": loss} + else: + return {"pred": reshaped_logits} + + def predict(self, input_ids, token_type_ids=None, attention_mask=None): + logits = self.forward(input_ids, token_type_ids, attention_mask)["pred"] + return {"pred": torch.argmax(logits, dim=-1)} + + +class BertForTokenClassification(BaseModel): + """BERT model for token-level classification. + This module is composed of the BERT model with a linear layer on top of + the full hidden state of the last layer. + Params: + `config`: a BertConfig class instance with the configuration to build a new model. + `num_labels`: the number of classes for the classifier. Default = 2. + `bert_dir`: a dir which contains the bert parameters within file `pytorch_model.bin` + Inputs: + `input_ids`: a torch.LongTensor of shape [batch_size, sequence_length] + with the word token indices in the vocabulary(see the tokens preprocessing logic in the scripts + `extract_features.py`, `run_classifier.py` and `run_squad.py`) + `token_type_ids`: an optional torch.LongTensor of shape [batch_size, sequence_length] with the token + types indices selected in [0, 1]. Type 0 corresponds to a `sentence A` and type 1 corresponds to + a `sentence B` token (see BERT paper for more details). + `attention_mask`: an optional torch.LongTensor of shape [batch_size, sequence_length] with indices + selected in [0, 1]. It's a mask to be used if the input sequence length is smaller than the max + input sequence length in the current batch. It's the mask that we typically use for attention when + a batch has varying length sentences. + `labels`: labels for the classification output: torch.LongTensor of shape [batch_size, sequence_length] + with indices selected in [0, ..., num_labels]. + Outputs: + if `labels` is not `None`: + Outputs the CrossEntropy classification loss of the output with the labels. + if `labels` is `None`: + Outputs the classification logits of shape [batch_size, sequence_length, num_labels]. + Example usage: + ```python + # Already been converted into WordPiece token ids + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, + num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + num_labels = 2 + bert_dir = 'your-bert-file-dir' + model = BertForTokenClassification(config, num_labels, bert_dir) + logits = model(input_ids, token_type_ids, input_mask) + ``` + """ + def __init__(self, config, num_labels, bert_dir): + super(BertForTokenClassification, self).__init__() + self.num_labels = num_labels + self.bert = BertModel.from_pretrained(bert_dir) + self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.classifier = nn.Linear(config.hidden_size, num_labels) + + def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None): + sequence_output, _ = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False) + sequence_output = self.dropout(sequence_output) + logits = self.classifier(sequence_output) + + if labels is not None: + loss_fct = nn.CrossEntropyLoss() + # Only keep active parts of the loss + if attention_mask is not None: + active_loss = attention_mask.view(-1) == 1 + active_logits = logits.view(-1, self.num_labels)[active_loss] + active_labels = labels.view(-1)[active_loss] + loss = loss_fct(active_logits, active_labels) + else: + loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) + return {"pred": logits, "loss": loss} + else: + return {"pred": logits} + + def predict(self, input_ids, token_type_ids=None, attention_mask=None): + logits = self.forward(input_ids, token_type_ids, attention_mask)["pred"] + return {"pred": torch.argmax(logits, dim=-1)} + + +class BertForQuestionAnswering(BaseModel): + """BERT model for Question Answering (span extraction). + This module is composed of the BERT model with a linear layer on top of + the sequence output that computes start_logits and end_logits + Params: + `config`: a BertConfig class instance with the configuration to build a new model. + `bert_dir`: a dir which contains the bert parameters within file `pytorch_model.bin` + Inputs: + `input_ids`: a torch.LongTensor of shape [batch_size, sequence_length] + with the word token indices in the vocabulary(see the tokens preprocessing logic in the scripts + `extract_features.py`, `run_classifier.py` and `run_squad.py`) + `token_type_ids`: an optional torch.LongTensor of shape [batch_size, sequence_length] with the token + types indices selected in [0, 1]. Type 0 corresponds to a `sentence A` and type 1 corresponds to + a `sentence B` token (see BERT paper for more details). + `attention_mask`: an optional torch.LongTensor of shape [batch_size, sequence_length] with indices + selected in [0, 1]. It's a mask to be used if the input sequence length is smaller than the max + input sequence length in the current batch. It's the mask that we typically use for attention when + a batch has varying length sentences. + `start_positions`: position of the first token for the labeled span: torch.LongTensor of shape [batch_size]. + Positions are clamped to the length of the sequence and position outside of the sequence are not taken + into account for computing the loss. + `end_positions`: position of the last token for the labeled span: torch.LongTensor of shape [batch_size]. + Positions are clamped to the length of the sequence and position outside of the sequence are not taken + into account for computing the loss. + Outputs: + if `start_positions` and `end_positions` are not `None`: + Outputs the total_loss which is the sum of the CrossEntropy loss for the start and end token positions. + if `start_positions` or `end_positions` is `None`: + Outputs a tuple of start_logits, end_logits which are the logits respectively for the start and end + position tokens of shape [batch_size, sequence_length]. + Example usage: + ```python + # Already been converted into WordPiece token ids + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, + num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + bert_dir = 'your-bert-file-dir' + model = BertForQuestionAnswering(config, bert_dir) + start_logits, end_logits = model(input_ids, token_type_ids, input_mask) + ``` + """ + def __init__(self, config, bert_dir): + super(BertForQuestionAnswering, self).__init__() + self.bert = BertModel.from_pretrained(bert_dir) + # TODO check with Google if it's normal there is no dropout on the token classifier of SQuAD in the TF version + # self.dropout = nn.Dropout(config.hidden_dropout_prob) + self.qa_outputs = nn.Linear(config.hidden_size, 2) + + def forward(self, input_ids, token_type_ids=None, attention_mask=None, start_positions=None, end_positions=None): + sequence_output, _ = self.bert(input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False) + logits = self.qa_outputs(sequence_output) + start_logits, end_logits = logits.split(1, dim=-1) + start_logits = start_logits.squeeze(-1) + end_logits = end_logits.squeeze(-1) + + if start_positions is not None and end_positions is not None: + # If we are on multi-GPU, split add a dimension + if len(start_positions.size()) > 1: + start_positions = start_positions.squeeze(-1) + if len(end_positions.size()) > 1: + end_positions = end_positions.squeeze(-1) + # sometimes the start/end positions are outside our model inputs, we ignore these terms + ignored_index = start_logits.size(1) + start_positions.clamp_(0, ignored_index) + end_positions.clamp_(0, ignored_index) + + loss_fct = nn.CrossEntropyLoss(ignore_index=ignored_index) + start_loss = loss_fct(start_logits, start_positions) + end_loss = loss_fct(end_logits, end_positions) + total_loss = (start_loss + end_loss) / 2 + return {"loss": total_loss} + else: + return {"pred1": start_logits, "pred2": end_logits} + + def predict(self, input_ids, token_type_ids=None, attention_mask=None, **kwargs): + logits = self.forward(input_ids, token_type_ids, attention_mask) + start_logits = logits["pred1"] + end_logits = logits["pred2"] + return {"pred1": torch.argmax(start_logits, dim=-1), "pred2": torch.argmax(end_logits, dim=-1)} diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index b00a0ae9..56b9ca59 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -3,9 +3,11 @@ from .conv_maxpool import ConvMaxpool from .embedding import Embedding from .linear import Linear from .lstm import LSTM +from .bert import BertModel __all__ = ["LSTM", "Embedding", "Linear", "Conv", - "ConvMaxpool"] + "ConvMaxpool", + "BertModel"] diff --git a/fastNLP/modules/encoder/bert.py b/fastNLP/modules/encoder/bert.py new file mode 100644 index 00000000..e87f6f5d --- /dev/null +++ b/fastNLP/modules/encoder/bert.py @@ -0,0 +1,362 @@ +""" +bert.py is modified from huggingface/pytorch-pretrained-BERT, which is licensed under the Apache License 2.0. + +""" +import copy +import json +import math +import os + +import torch +from torch import nn + +CONFIG_FILE = 'bert_config.json' +MODEL_WEIGHTS = 'pytorch_model.bin' + + +def gelu(x): + return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0))) + + +def swish(x): + return x * torch.sigmoid(x) + + +ACT2FN = {"gelu": gelu, "relu": torch.nn.functional.relu, "swish": swish} + + +class BertLayerNorm(nn.Module): + def __init__(self, hidden_size, eps=1e-12): + super(BertLayerNorm, self).__init__() + self.weight = nn.Parameter(torch.ones(hidden_size)) + self.bias = nn.Parameter(torch.zeros(hidden_size)) + self.variance_epsilon = eps + + def forward(self, x): + u = x.mean(-1, keepdim=True) + s = (x - u).pow(2).mean(-1, keepdim=True) + x = (x - u) / torch.sqrt(s + self.variance_epsilon) + return self.weight * x + self.bias + + +class BertEmbeddings(nn.Module): + def __init__(self, vocab_size, hidden_size, max_position_embeddings, type_vocab_size, hidden_dropout_prob): + super(BertEmbeddings, self).__init__() + self.word_embeddings = nn.Embedding(vocab_size, hidden_size) + self.position_embeddings = nn.Embedding(max_position_embeddings, hidden_size) + self.token_type_embeddings = nn.Embedding(type_vocab_size, hidden_size) + + # self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load + # any TensorFlow checkpoint file + self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, input_ids, token_type_ids=None): + seq_length = input_ids.size(1) + position_ids = torch.arange(seq_length, dtype=torch.long, device=input_ids.device) + position_ids = position_ids.unsqueeze(0).expand_as(input_ids) + if token_type_ids is None: + token_type_ids = torch.zeros_like(input_ids) + + words_embeddings = self.word_embeddings(input_ids) + position_embeddings = self.position_embeddings(position_ids) + token_type_embeddings = self.token_type_embeddings(token_type_ids) + + embeddings = words_embeddings + position_embeddings + token_type_embeddings + embeddings = self.LayerNorm(embeddings) + embeddings = self.dropout(embeddings) + return embeddings + + +class BertSelfAttention(nn.Module): + def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob): + super(BertSelfAttention, self).__init__() + if hidden_size % num_attention_heads != 0: + raise ValueError( + "The hidden size (%d) is not a multiple of the number of attention " + "heads (%d)" % (hidden_size, num_attention_heads)) + self.num_attention_heads = num_attention_heads + self.attention_head_size = int(hidden_size / num_attention_heads) + self.all_head_size = self.num_attention_heads * self.attention_head_size + + self.query = nn.Linear(hidden_size, self.all_head_size) + self.key = nn.Linear(hidden_size, self.all_head_size) + self.value = nn.Linear(hidden_size, self.all_head_size) + + self.dropout = nn.Dropout(attention_probs_dropout_prob) + + def transpose_for_scores(self, x): + new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) + x = x.view(*new_x_shape) + return x.permute(0, 2, 1, 3) + + def forward(self, hidden_states, attention_mask): + mixed_query_layer = self.query(hidden_states) + mixed_key_layer = self.key(hidden_states) + mixed_value_layer = self.value(hidden_states) + + query_layer = self.transpose_for_scores(mixed_query_layer) + key_layer = self.transpose_for_scores(mixed_key_layer) + value_layer = self.transpose_for_scores(mixed_value_layer) + + # Take the dot product between "query" and "key" to get the raw attention scores. + attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) + attention_scores = attention_scores / math.sqrt(self.attention_head_size) + # Apply the attention mask is (precomputed for all layers in BertModel forward() function) + attention_scores = attention_scores + attention_mask + + # Normalize the attention scores to probabilities. + attention_probs = nn.Softmax(dim=-1)(attention_scores) + + # This is actually dropping out entire tokens to attend to, which might + # seem a bit unusual, but is taken from the original Transformer paper. + attention_probs = self.dropout(attention_probs) + + context_layer = torch.matmul(attention_probs, value_layer) + context_layer = context_layer.permute(0, 2, 1, 3).contiguous() + new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,) + context_layer = context_layer.view(*new_context_layer_shape) + return context_layer + + +class BertSelfOutput(nn.Module): + def __init__(self, hidden_size, hidden_dropout_prob): + super(BertSelfOutput, self).__init__() + self.dense = nn.Linear(hidden_size, hidden_size) + self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, hidden_states, input_tensor): + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + hidden_states = self.LayerNorm(hidden_states + input_tensor) + return hidden_states + + +class BertAttention(nn.Module): + def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob): + super(BertAttention, self).__init__() + self.self = BertSelfAttention(hidden_size, num_attention_heads, attention_probs_dropout_prob) + self.output = BertSelfOutput(hidden_size, hidden_dropout_prob) + + def forward(self, input_tensor, attention_mask): + self_output = self.self(input_tensor, attention_mask) + attention_output = self.output(self_output, input_tensor) + return attention_output + + +class BertIntermediate(nn.Module): + def __init__(self, hidden_size, intermediate_size, hidden_act): + super(BertIntermediate, self).__init__() + self.dense = nn.Linear(hidden_size, intermediate_size) + self.intermediate_act_fn = ACT2FN[hidden_act] \ + if isinstance(hidden_act, str) else hidden_act + + def forward(self, hidden_states): + hidden_states = self.dense(hidden_states) + hidden_states = self.intermediate_act_fn(hidden_states) + return hidden_states + + +class BertOutput(nn.Module): + def __init__(self, hidden_size, intermediate_size, hidden_dropout_prob): + super(BertOutput, self).__init__() + self.dense = nn.Linear(intermediate_size, hidden_size) + self.LayerNorm = BertLayerNorm(hidden_size, eps=1e-12) + self.dropout = nn.Dropout(hidden_dropout_prob) + + def forward(self, hidden_states, input_tensor): + hidden_states = self.dense(hidden_states) + hidden_states = self.dropout(hidden_states) + hidden_states = self.LayerNorm(hidden_states + input_tensor) + return hidden_states + + +class BertLayer(nn.Module): + def __init__(self, hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob, + intermediate_size, hidden_act): + super(BertLayer, self).__init__() + self.attention = BertAttention(hidden_size, num_attention_heads, attention_probs_dropout_prob, + hidden_dropout_prob) + self.intermediate = BertIntermediate(hidden_size, intermediate_size, hidden_act) + self.output = BertOutput(hidden_size, intermediate_size, hidden_dropout_prob) + + def forward(self, hidden_states, attention_mask): + attention_output = self.attention(hidden_states, attention_mask) + intermediate_output = self.intermediate(attention_output) + layer_output = self.output(intermediate_output, attention_output) + return layer_output + + +class BertEncoder(nn.Module): + def __init__(self, num_hidden_layers, hidden_size, num_attention_heads, attention_probs_dropout_prob, + hidden_dropout_prob, + intermediate_size, hidden_act): + super(BertEncoder, self).__init__() + layer = BertLayer(hidden_size, num_attention_heads, attention_probs_dropout_prob, hidden_dropout_prob, + intermediate_size, hidden_act) + self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(num_hidden_layers)]) + + def forward(self, hidden_states, attention_mask, output_all_encoded_layers=True): + all_encoder_layers = [] + for layer_module in self.layer: + hidden_states = layer_module(hidden_states, attention_mask) + if output_all_encoded_layers: + all_encoder_layers.append(hidden_states) + if not output_all_encoded_layers: + all_encoder_layers.append(hidden_states) + return all_encoder_layers + + +class BertPooler(nn.Module): + def __init__(self, hidden_size): + super(BertPooler, self).__init__() + self.dense = nn.Linear(hidden_size, hidden_size) + self.activation = nn.Tanh() + + def forward(self, hidden_states): + # We "pool" the model by simply taking the hidden state corresponding + # to the first token. + first_token_tensor = hidden_states[:, 0] + pooled_output = self.dense(first_token_tensor) + pooled_output = self.activation(pooled_output) + return pooled_output + + +class BertModel(nn.Module): + """Bidirectional Embedding Representations from Transformers. + + If you want to use pre-trained weights, please download from the following sources provided by pytorch-pretrained-BERT. + sources:: + + 'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz", + 'bert-large-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-uncased.tar.gz", + 'bert-base-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-cased.tar.gz", + 'bert-large-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-large-cased.tar.gz", + 'bert-base-multilingual-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-uncased.tar.gz", + 'bert-base-multilingual-cased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-multilingual-cased.tar.gz", + 'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese.tar.gz", + + + Construct a BERT model with pre-trained weights:: + + model = BertModel.from_pretrained("path/to/weights/directory") + + """ + + def __init__(self, vocab_size, + hidden_size=768, + num_hidden_layers=12, + num_attention_heads=12, + intermediate_size=3072, + hidden_act="gelu", + hidden_dropout_prob=0.1, + attention_probs_dropout_prob=0.1, + max_position_embeddings=512, + type_vocab_size=2, + initializer_range=0.02, **kwargs): + super(BertModel, self).__init__() + self.embeddings = BertEmbeddings(vocab_size, hidden_size, max_position_embeddings, + type_vocab_size, hidden_dropout_prob) + self.encoder = BertEncoder(num_hidden_layers, hidden_size, num_attention_heads, + attention_probs_dropout_prob, hidden_dropout_prob, intermediate_size, + hidden_act) + self.pooler = BertPooler(hidden_size) + self.initializer_range = initializer_range + + self.apply(self.init_bert_weights) + + def init_bert_weights(self, module): + if isinstance(module, (nn.Linear, nn.Embedding)): + # Slightly different from the TF version which uses truncated_normal for initialization + # cf https://github.com/pytorch/pytorch/pull/5617 + module.weight.data.normal_(mean=0.0, std=self.initializer_range) + elif isinstance(module, BertLayerNorm): + module.bias.data.zero_() + module.weight.data.fill_(1.0) + if isinstance(module, nn.Linear) and module.bias is not None: + module.bias.data.zero_() + + def forward(self, input_ids, token_type_ids=None, attention_mask=None, output_all_encoded_layers=True): + if attention_mask is None: + attention_mask = torch.ones_like(input_ids) + if token_type_ids is None: + token_type_ids = torch.zeros_like(input_ids) + + # We create a 3D attention mask from a 2D tensor mask. + # Sizes are [batch_size, 1, 1, to_seq_length] + # So we can broadcast to [batch_size, num_heads, from_seq_length, to_seq_length] + # this attention mask is more simple than the triangular masking of causal attention + # used in OpenAI GPT, we just need to prepare the broadcast dimension here. + extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) + + # Since attention_mask is 1.0 for positions we want to attend and 0.0 for + # masked positions, this operation will create a tensor which is 0.0 for + # positions we want to attend and -10000.0 for masked positions. + # Since we are adding it to the raw scores before the softmax, this is + # effectively the same as removing these entirely. + extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype) # fp16 compatibility + extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0 + + embedding_output = self.embeddings(input_ids, token_type_ids) + encoded_layers = self.encoder(embedding_output, + extended_attention_mask, + output_all_encoded_layers=output_all_encoded_layers) + sequence_output = encoded_layers[-1] + pooled_output = self.pooler(sequence_output) + if not output_all_encoded_layers: + encoded_layers = encoded_layers[-1] + return encoded_layers, pooled_output + + @classmethod + def from_pretrained(cls, pretrained_model_dir, state_dict=None, *inputs, **kwargs): + # Load config + config_file = os.path.join(pretrained_model_dir, CONFIG_FILE) + config = json.load(open(config_file, "r")) + # config = BertConfig.from_json_file(config_file) + # logger.info("Model config {}".format(config)) + # Instantiate model. + model = cls(*inputs, **config, **kwargs) + if state_dict is None: + weights_path = os.path.join(pretrained_model_dir, MODEL_WEIGHTS) + state_dict = torch.load(weights_path) + + old_keys = [] + new_keys = [] + for key in state_dict.keys(): + new_key = None + if 'gamma' in key: + new_key = key.replace('gamma', 'weight') + if 'beta' in key: + new_key = key.replace('beta', 'bias') + if new_key: + old_keys.append(key) + new_keys.append(new_key) + for old_key, new_key in zip(old_keys, new_keys): + state_dict[new_key] = state_dict.pop(old_key) + + missing_keys = [] + unexpected_keys = [] + error_msgs = [] + # copy state_dict so _load_from_state_dict can modify it + metadata = getattr(state_dict, '_metadata', None) + state_dict = state_dict.copy() + if metadata is not None: + state_dict._metadata = metadata + + def load(module, prefix=''): + local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {}) + module._load_from_state_dict( + state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs) + for name, child in module._modules.items(): + if child is not None: + load(child, prefix + name + '.') + + load(model, prefix='' if hasattr(model, 'bert') else 'bert.') + if len(missing_keys) > 0: + print("Weights of {} not initialized from pretrained model: {}".format( + model.__class__.__name__, missing_keys)) + if len(unexpected_keys) > 0: + print("Weights from pretrained model not used in {}: {}".format( + model.__class__.__name__, unexpected_keys)) + return model From 96bfbf19fbbcd1318a306da6ceca0227296f2547 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 20:42:41 +0800 Subject: [PATCH 048/173] update documents in MLP --- fastNLP/modules/decoder/MLP.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index 3a793f24..e7fafd68 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -7,17 +7,33 @@ from fastNLP.modules.utils import initial_parameter class MLP(nn.Module): """Multilayer Perceptrons as a decoder - :param list size_layer: list of int, define the size of MLP layers. layer的层数为 len(size_layer) - 1 - :param str or list activation: str or function or a list, the activation function for hidden layers. - :param str or function output_activation : str or function, the activation function for output layer - :param str initial_method: the name of initialization method. - :param float dropout: the probability of dropout. + :param list size_layer: 一个int的列表,用来定义MLP的层数,列表中的数字为每一层是hidden数目。MLP的层数为 len(size_layer) - 1 + :param str or list activation: + 一个字符串或者函数或者字符串跟函数的列表,用来定义每一个隐层的激活函数,字符串包括relu,tanh和sigmoid,默认值为relu + :param str or function output_activation : 字符串或者函数,用来定义输出层的激活函数,默认值为None,表示输出层没有激活函数 + :param str initial_method: 参数初始化方式 + :param float dropout: dropout概率,默认值为0 .. note:: 隐藏层的激活函数通过activation定义。一个str/function或者一个str/function的list可以被传入activation。 如果只传入了一个str/function,那么所有隐藏层的激活函数都由这个str/function定义; 如果传入了一个str/function的list,那么每一个隐藏层的激活函数由这个list中对应的元素定义,其中list的长度为隐藏层数。 输出层的激活函数由output_activation定义,默认值为None,此时输出层没有激活函数。 + + Examples:: + + >>> net1 = MLP([5, 10, 5]) + >>> net2 = MLP([5, 10, 5], 'tanh') + >>> net3 = MLP([5, 6, 7, 8, 5], 'tanh') + >>> net4 = MLP([5, 6, 7, 8, 5], 'relu', output_activation='tanh') + >>> net5 = MLP([5, 6, 7, 8, 5], ['tanh', 'relu', 'tanh'], 'tanh') + >>> for net in [net1, net2, net3, net4, net5]: + >>> x = torch.randn(5, 5) + >>> y = net(x) + >>> print(x) + >>> print(y) + >>> + """ def __init__(self, size_layer, activation='relu', output_activation=None, initial_method=None, dropout=0.0): @@ -63,6 +79,10 @@ class MLP(nn.Module): initial_parameter(self, initial_method) def forward(self, x): + """ + :param torch.Tensor x: MLP接受的输入 + :return: torch.Tensor : MLP的输出结果 + """ for layer, func in zip(self.hiddens, self.hidden_active): x = self.dropout(func(layer(x))) x = self.output(x) From 23625fe954c996b91a5c6a7e856a33ba21ca103d Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 21:05:24 +0800 Subject: [PATCH 049/173] update documents in snli --- fastNLP/models/snli.py | 65 ++++++++++++++++++--------- fastNLP/modules/aggregator/pooling.py | 10 ++--- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 901f2dd4..7ead5c18 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -5,38 +5,45 @@ from fastNLP.models.base_model import BaseModel from fastNLP.modules import decoder as Decoder from fastNLP.modules import encoder as Encoder from fastNLP.modules import aggregator as Aggregator +from fastNLP.modules.utils import seq_mask my_inf = 10e12 class ESIM(BaseModel): - """ - PyTorch Network for SNLI task using ESIM model. + """ESIM模型的一个PyTorch实现。 + ESIM模型的论文: Enhanced LSTM for Natural Language Inference (arXiv: 1609.06038) """ - def __init__(self, **kwargs): + def __init__(self, vocab_size, embed_dim, hidden_size, dropout=0.0, num_classes=3, init_embedding=None): + """ + :param int vocab_size: 词表大小 + :param int embed_dim: 词嵌入维度 + :param int hidden_size: LSTM隐层大小 + :param float dropout: dropout大小,默认为0 + :param int num_classes: 标签数目,默认为3 + :param numpy.array init_embedding: 初始词嵌入矩阵,形状为(vocab_size, embed_dim),默认为None,即随机初始化词嵌入矩阵 + """ super(ESIM, self).__init__() - self.vocab_size = kwargs["vocab_size"] - self.embed_dim = kwargs["embed_dim"] - self.hidden_size = kwargs["hidden_size"] - self.batch_first = kwargs["batch_first"] - self.dropout = kwargs["dropout"] - self.n_labels = kwargs["num_classes"] - self.gpu = kwargs["gpu"] and torch.cuda.is_available() + self.vocab_size = vocab_size + self.embed_dim = embed_dim + self.hidden_size = hidden_size + self.dropout = dropout + self.n_labels = num_classes self.drop = nn.Dropout(self.dropout) self.embedding = Encoder.Embedding( self.vocab_size, self.embed_dim, dropout=self.dropout, - init_emb=kwargs["init_embedding"] if "inin_embedding" in kwargs.keys() else None, + init_emb=init_embedding, ) self.embedding_layer = Encoder.Linear(self.embed_dim, self.hidden_size) self.encoder = Encoder.LSTM( input_size=self.embed_dim, hidden_size=self.hidden_size, num_layers=1, bias=True, - batch_first=self.batch_first, bidirectional=True + batch_first=True, bidirectional=True ) self.bi_attention = Aggregator.BiAttention() @@ -47,24 +54,34 @@ class ESIM(BaseModel): self.decoder = Encoder.LSTM( input_size=self.hidden_size, hidden_size=self.hidden_size, num_layers=1, bias=True, - batch_first=self.batch_first, bidirectional=True + batch_first=True, bidirectional=True ) self.output = Decoder.MLP([4 * self.hidden_size, self.hidden_size, self.n_labels], 'tanh', dropout=self.dropout) - def forward(self, words1, words2, seq_len1, seq_len2): + def forward(self, words1, words2, seq_len1=None, seq_len2=None): """ Forward function - - :param words1: A Tensor represents premise: [batch size(B), premise seq len(PL)]. - :param words2: A Tensor represents hypothesis: [B, hypothesis seq len(HL)]. - :param seq_len1: A Tensor record which is a real word and which is a padding word in premise: [B]. - :param seq_len2: A Tensor record which is a real word and which is a padding word in hypothesis: [B]. - :return: prediction: A Dict with Tensor of classification result: [B, n_labels(N)]. + :param torch.Tensor words1: [batch size(B), premise seq len(PL)] premise的token表示 + :param torch.Tensor words2: [B, hypothesis seq len(HL)] hypothesis的token表示 + :param torch.LongTensor seq_len1: [B] premise的长度 + :param torch.LongTensor seq_len2: [B] hypothesis的长度 + :return: dict prediction: [B, n_labels(N)] 预测结果 """ premise0 = self.embedding_layer(self.embedding(words1)) hypothesis0 = self.embedding_layer(self.embedding(words2)) + if seq_len1 is not None: + seq_len1 = seq_mask(seq_len1, premise0.size(1)) + else: + seq_len1 = torch.ones(premise0.size(0), premise0.size(1)) + seq_len1 = (seq_len1.long()).to(device=premise0.device) + if seq_len2 is not None: + seq_len2 = seq_mask(seq_len2, hypothesis0.size(1)) + else: + seq_len2 = torch.ones(hypothesis0.size(0), hypothesis0.size(1)) + seq_len2 = (seq_len2.long()).to(device=hypothesis0.device) + _BP, _PSL, _HP = premise0.size() _BH, _HSL, _HH = hypothesis0.size() _BPL, _PLL = seq_len1.size() @@ -109,6 +126,14 @@ class ESIM(BaseModel): return {'pred': prediction} def predict(self, words1, words2, seq_len1, seq_len2): + """ Predict function + + :param torch.Tensor words1: [batch size(B), premise seq len(PL)] premise的token表示 + :param torch.Tensor words2: [B, hypothesis seq len(HL)] hypothesis的token表示 + :param torch.LongTensor seq_len1: [B] premise的长度 + :param torch.LongTensor seq_len2: [B] hypothesis的长度 + :return: dict prediction: [B, n_labels(N)] 预测结果 + """ prediction = self.forward(words1, words2, seq_len1, seq_len2)['pred'] return {'pred': torch.argmax(prediction, dim=-1)} diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index 876f5fb1..9961b87f 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -63,8 +63,8 @@ class MaxPoolWithMask(nn.Module): def forward(self, tensor, mask, dim=1): """ - :param torch.Tensor tensor: [batch_size, seq_len, channels] 初始tensor - :param torch.Tensor mask: [batch_size, seq_len] 0/1的mask矩阵 + :param torch.FloatTensor tensor: [batch_size, seq_len, channels] 初始tensor + :param torch.LongTensor mask: [batch_size, seq_len] 0/1的mask矩阵 :param int dim: 需要进行max pooling的维度 :return: """ @@ -120,13 +120,13 @@ class MeanPoolWithMask(nn.Module): def forward(self, tensor, mask, dim=1): """ - :param torch.Tensor tensor: [batch_size, seq_len, channels] 初始tensor - :param torch.Tensor mask: [batch_size, seq_len] 0/1的mask矩阵 + :param torch.FloatTensor tensor: [batch_size, seq_len, channels] 初始tensor + :param torch.LongTensor mask: [batch_size, seq_len] 0/1的mask矩阵 :param int dim: 需要进行max pooling的维度 :return: """ masks = mask.view(mask.size(0), mask.size(1), -1).float() - return torch.sum(tensor * masks, dim=dim) / torch.sum(masks, dim=1) + return torch.sum(tensor * masks.float(), dim=dim) / torch.sum(masks.float(), dim=1) From f22cb585593dfdef01e547381904a7ca2a4e4de0 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 21:24:19 +0800 Subject: [PATCH 050/173] combine self attention module to attention.py --- fastNLP/modules/aggregator/__init__.py | 2 +- fastNLP/modules/aggregator/attention.py | 59 +++++++++++++++++ fastNLP/modules/aggregator/self_attention.py | 68 -------------------- 3 files changed, 60 insertions(+), 69 deletions(-) delete mode 100644 fastNLP/modules/aggregator/self_attention.py diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index c0a63fd3..51106a76 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -6,5 +6,5 @@ from .pooling import KMaxPool from .attention import Attention from .attention import BiAttention -from .self_attention import SelfAttention +from .attention import SelfAttention diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index 4155fdd6..f2f2ac68 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -7,6 +7,8 @@ from torch import nn from fastNLP.modules.dropout import TimestepDropout from fastNLP.modules.utils import mask_softmax +from fastNLP.modules.utils import initial_parameter + class Attention(torch.nn.Module): def __init__(self, normalize=False): @@ -168,3 +170,60 @@ class BiAttention(nn.Module): out_x2 = torch.bmm(attention_b_t, in_x1) # [batch_size, x2_seq_len, hidden_size] return out_x1, out_x2 + +class SelfAttention(nn.Module): + """Self Attention Module. + :param int input_size: 输入tensor的hidden维度 + :param int attention_unit: 输出tensor的hidden维度 + :param int attention_hops: + :param float drop: dropout概率,默认值为0.5 + :param str initial_method: 初始化参数方法 + """ + + def __init__(self, input_size, attention_unit=300, attention_hops=10, drop=0.5, initial_method=None,): + super(SelfAttention, self).__init__() + + self.attention_hops = attention_hops + self.ws1 = nn.Linear(input_size, attention_unit, bias=False) + self.ws2 = nn.Linear(attention_unit, attention_hops, bias=False) + self.I = torch.eye(attention_hops, requires_grad=False) + self.I_origin = self.I + self.drop = nn.Dropout(drop) + self.tanh = nn.Tanh() + initial_parameter(self, initial_method) + + def _penalization(self, attention): + """ + compute the penalization term for attention module + """ + baz = attention.size(0) + size = self.I.size() + if len(size) != 3 or size[0] != baz: + self.I = self.I_origin.expand(baz, -1, -1) + self.I = self.I.to(device=attention.device) + attention_t = torch.transpose(attention, 1, 2).contiguous() + mat = torch.bmm(attention, attention_t) - self.I[:attention.size(0)] + ret = (torch.sum(torch.sum((mat ** 2), 2), 1).squeeze() + 1e-10) ** 0.5 + return torch.sum(ret) / size[0] + + def forward(self, input, input_origin): + """ + :param torch.Tensor input: [baz, senLen, h_dim] 要做attention的矩阵 + :param torch.Tensor input_origin: [baz , senLen] 原始token的index组成的矩阵,含有pad部分内容 + :return torch.Tensor output1: [baz, multi-head , h_dim] 经过attention操作后输入矩阵的结果 + :return torch.Tensor output2: [1] attention惩罚项,是一个标量 + """ + input = input.contiguous() + size = input.size() # [bsz, len, nhid] + + input_origin = input_origin.expand(self.attention_hops, -1, -1) # [hops,baz, len] + input_origin = input_origin.transpose(0, 1).contiguous() # [baz, hops,len] + + y1 = self.tanh(self.ws1(self.drop(input))) # [baz,len,dim] -->[bsz,len, attention-unit] + attention = self.ws2(y1).transpose(1, 2).contiguous() + # [bsz,len, attention-unit]--> [bsz, len, hop]--> [baz,hop,len] + + attention = attention + (-999999 * (input_origin == 0).float()) # remove the weight on padding token. + attention = F.softmax(attention, 2) # [baz ,hop, len] + return torch.bmm(attention, input), self._penalization(attention) # output1 --> [baz ,hop ,nhid] + diff --git a/fastNLP/modules/aggregator/self_attention.py b/fastNLP/modules/aggregator/self_attention.py deleted file mode 100644 index b0f03791..00000000 --- a/fastNLP/modules/aggregator/self_attention.py +++ /dev/null @@ -1,68 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F -from torch.autograd import Variable - -from fastNLP.modules.utils import initial_parameter - - -class SelfAttention(nn.Module): - """Self Attention Module. - - :param int input_size: - :param int attention_unit: - :param int attention_hops: - :param float drop: - :param str initial_method: - :param bool use_cuda: - """ - - def __init__(self, input_size, attention_unit=350, attention_hops=10, drop=0.5, initial_method=None, - use_cuda=False): - super(SelfAttention, self).__init__() - - self.attention_hops = attention_hops - self.ws1 = nn.Linear(input_size, attention_unit, bias=False) - self.ws2 = nn.Linear(attention_unit, attention_hops, bias=False) - if use_cuda: - self.I = Variable(torch.eye(attention_hops).cuda(), requires_grad=False) - else: - self.I = Variable(torch.eye(attention_hops), requires_grad=False) - self.I_origin = self.I - self.drop = nn.Dropout(drop) - self.tanh = nn.Tanh() - initial_parameter(self, initial_method) - - def penalization(self, attention): - """ - compute the penalization term for attention module - """ - baz = attention.size(0) - size = self.I.size() - if len(size) != 3 or size[0] != baz: - self.I = self.I_origin.expand(baz, -1, -1) - attentionT = torch.transpose(attention, 1, 2).contiguous() - mat = torch.bmm(attention, attentionT) - self.I[:attention.size(0)] - ret = (torch.sum(torch.sum((mat ** 2), 2), 1).squeeze() + 1e-10) ** 0.5 - return torch.sum(ret) / size[0] - - def forward(self, input, input_origin): - """ - :param input: the matrix to do attention. [baz, senLen, h_dim] - :param inp: then token index include pad token( 0 ) [baz , senLen] - :return output1: the input matrix after attention operation [baz, multi-head , h_dim] - :return output2: the attention penalty term, a scalar [1] - """ - input = input.contiguous() - size = input.size() # [bsz, len, nhid] - - input_origin = input_origin.expand(self.attention_hops, -1, -1) # [hops,baz, len] - input_origin = input_origin.transpose(0, 1).contiguous() # [baz, hops,len] - - y1 = self.tanh(self.ws1(self.drop(input))) # [baz,len,dim] -->[bsz,len, attention-unit] - attention = self.ws2(y1).transpose(1, 2).contiguous() - # [bsz,len, attention-unit]--> [bsz, len, hop]--> [baz,hop,len] - - attention = attention + (-999999 * (input_origin == 0).float()) # remove the weight on padding token. - attention = F.softmax(attention, 2) # [baz ,hop, len] - return torch.bmm(attention, input), self.penalization(attention) # output1 --> [baz ,hop ,nhid] From 5b8a62783c55c60093b5fbc13f25c12e34944e79 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 23 Apr 2019 22:03:50 +0800 Subject: [PATCH 051/173] update documents on losses.py --- fastNLP/core/losses.py | 78 ++++++++++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 19 deletions(-) diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 6b0b4460..08702034 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -13,7 +13,7 @@ from fastNLP.core.utils import get_func_signature class LossBase(object): - """Base class for all losses. + """所有loss的基类. """ def __init__(self): @@ -24,10 +24,10 @@ class LossBase(object): raise NotImplementedError def _init_param_map(self, key_map=None, **kwargs): - """Check the validity of key_map and other param map. Add these into self.param_map + """检查key_map和其他参数map,并将这些映射关系添加到self.param_map - :param key_map: dict - :param kwargs: + :param dict key_map: 表示key的映射关系 + :param kwargs: key word args里面的每一个的键-值对都会被构造成映射关系 :return: None """ value_counter = defaultdict(set) @@ -87,9 +87,9 @@ class LossBase(object): def __call__(self, pred_dict, target_dict, check=False): """ - :param pred_dict: A dict from forward function of the network. - :param target_dict: A dict from DataSet.batch_y. - :param check: Boolean. Force to check the mapping functions when it is running. + :param dict pred_dict: 模型的forward函数返回的dict + :param dict target_dict: DataSet.batch_y里的键-值对所组成的dict + :param Boolean check: 每一次执行映射函数的时候是否检查映射表,默认为不检查 :return: """ fast_param = self._fast_param_map(pred_dict, target_dict) @@ -162,15 +162,25 @@ class LossBase(object): class LossFunc(LossBase): - """A wrapper of user-provided loss function. - + """提供给用户使用自定义损失函数的类 """ def __init__(self, func, key_map=None, **kwargs): """ - :param func: a callable object, such as a function. - :param dict key_map: - :param kwargs: + :param func: 用户自行定义的损失函数,应当为一个函数或者callable(func)为True的ojbect + :param dict key_map: 参数映射表。键为Model/DataSet参数名,值为损失函数参数名。 + fastNLP的trainer将在训练时从模型返回值或者训练数据DataSet的target=True的field中 + 找到相对应的参数名为value的参数,并传入func中作为参数名为key的参数 + :param kwargs: 除了参数映射表以外可以用key word args的方式设置参数映射关系 + + Example:: + + >>> func = torch.nn.CrossEntropyLoss() + >>> loss_func = LossFunc(func, input="pred", target="label") + >>> # 这表示构建了一个损失函数类,由func计算损失函数,其中将从模型返回值或者DataSet的target=True的field + >>> # 当中找到一个参数名为`pred`的参数传入func一个参数名为`input`的参数;找到一个参数名为`label`的参数 + >>> # 传入func作为一个名为`target`的参数 + """ super(LossFunc, self).__init__() _check_function_or_method(func) @@ -186,7 +196,17 @@ class LossFunc(LossBase): class CrossEntropyLoss(LossBase): + """交叉熵损失函数""" def __init__(self, pred=None, target=None, padding_idx=-100): + """ + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + :param padding_idx: padding的index,在计算loss时将忽略target中标号为padding_idx的内容 + + Example:: + + >>> loss = CrossEntropyLoss(pred='pred', target='label', padding_idx=0) + """ # TODO 需要做一些检查,F.cross_entropy在计算时,如果pred是(16, 10 ,4), target的形状按道理应该是(16, 10), 但实际却需要 # TODO (16, 4) super(CrossEntropyLoss, self).__init__() @@ -199,7 +219,12 @@ class CrossEntropyLoss(LossBase): class L1Loss(LossBase): + """L1损失函数""" def __init__(self, pred=None, target=None): + """ + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + """ super(L1Loss, self).__init__() self._init_param_map(pred=pred, target=target) @@ -208,7 +233,12 @@ class L1Loss(LossBase): class BCELoss(LossBase): + """二分类交叉熵损失函数""" def __init__(self, pred=None, target=None): + """ + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + """ super(BCELoss, self).__init__() self._init_param_map(pred=pred, target=target) @@ -217,7 +247,12 @@ class BCELoss(LossBase): class NLLLoss(LossBase): + """负对数似然损失函数""" def __init__(self, pred=None, target=None): + """ + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + """ super(NLLLoss, self).__init__() self._init_param_map(pred=pred, target=target) @@ -226,7 +261,11 @@ class NLLLoss(LossBase): class LossInForward(LossBase): + """Forward函数中计算得到的损失函数结果""" def __init__(self, loss_key='loss'): + """ + :param str loss_key: 在forward函数中取得loss的键名,默认为loss + """ super().__init__() if not isinstance(loss_key, str): raise TypeError(f"Only str allowed for loss_key, got {type(loss_key)}.") @@ -234,13 +273,14 @@ class LossInForward(LossBase): def get_loss(self, **kwargs): if self.loss_key not in kwargs: - check_res = CheckRes(missing=[self.loss_key + f"(assign to `{self.loss_key}` " \ - f"in `{self.__class__.__name__}`"], - unused=[], - duplicated=[], - required=[], - all_needed=[], - varargs=[]) + check_res = CheckRes( + missing=[self.loss_key + f"(assign to `{self.loss_key}` in `{self.__class__.__name__}`"], + unused=[], + duplicated=[], + required=[], + all_needed=[], + varargs=[] + ) raise CheckError(check_res=check_res, func_signature=get_func_signature(self.get_loss)) return kwargs[self.loss_key] From 7997dce8a7cc0030bff1c7678276973e00483fb1 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Tue, 23 Apr 2019 22:09:17 +0800 Subject: [PATCH 052/173] =?UTF-8?q?=E5=AF=B9DataSet=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=BF=9B=E8=A1=8C=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/batch.py | 3 + fastNLP/core/dataset.py | 246 +++++++++++++++++++++++++------ fastNLP/core/fieldarray.py | 6 + fastNLP/core/sampler.py | 2 + fastNLP/core/trainer.py | 10 ++ fastNLP/modules/decoder/utils.py | 22 +-- test/core/test_batch.py | 14 +- 7 files changed, 242 insertions(+), 61 deletions(-) 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 Date: Tue, 23 Apr 2019 23:07:59 +0800 Subject: [PATCH 053/173] update documents on metrics --- fastNLP/core/metrics.py | 240 +++++++++++++++++++++----------------- test/core/test_metrics.py | 22 ++-- 2 files changed, 147 insertions(+), 115 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 314be0d9..71ca2926 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -14,7 +14,7 @@ from fastNLP.core.vocabulary import Vocabulary class MetricBase(object): - """Base class for all metrics. + """所有metrics的基类 所有的传入到Trainer, Tester的Metric需要继承自该对象。需要覆盖写入evaluate(), get_metric()方法。 @@ -85,18 +85,22 @@ class MetricBase(object): return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 - ``MetricBase`` handles validity check of its input dictionaries - ``pred_dict`` and ``target_dict``. - ``pred_dict`` is the output of ``forward()`` or prediction function of a model. - ``target_dict`` is the ground truth from DataSet where ``is_target`` is set ``True``. - ``MetricBase`` will do the following type checks: + ``MetricBase`` 将会在输入的字典``pred_dict``和``target_dict``中进行检查. + ``pred_dict`` 是模型当中``forward()``函数或者``predict()``函数的返回值. + ``target_dict`` 是DataSet当中的ground truth, 判定ground truth的条件是field的``is_target``被设置为True. - 1. whether self.evaluate has varargs, which is not supported. - 2. whether params needed by self.evaluate is not included in ``pred_dict``, ``target_dict``. - 3. whether params needed by self.evaluate duplicate in ``pred_dict``, ``target_dict``. + ``MetricBase`` 会进行以下的类型检测: - Besides, before passing params into self.evaluate, this function will filter out params from output_dict and - target_dict which are not used in self.evaluate. (but if kwargs presented in self.evaluate, no filtering - will be conducted.) + 1. self.evaluate当中是否有varargs, 这是不支持的. + 2. self.evaluate当中所需要的参数是否既不在``pred_dict``也不在``target_dict``. + 3. self.evaluate当中所需要的参数是否既在``pred_dict``也在``target_dict``. + + 除此以外,在参数被传入self.evaluate以前,这个函数会检测``pred_dict``和``target_dict``当中没有被用到的参数 + 如果kwargs是self.evaluate的参数,则不会检测 + + + self.evaluate将计算一个批次(batch)的评价指标,并累计 + self.get_metric将统计当前的评价指标并返回评价结果 """ def __init__(self): @@ -107,10 +111,10 @@ class MetricBase(object): raise NotImplementedError def _init_param_map(self, key_map=None, **kwargs): - """Check the validity of key_map and other param map. Add these into self.param_map + """检查key_map和其他参数map,并将这些映射关系添加到self.param_map - :param key_map: dict - :param kwargs: + :param dict key_map: 表示key的映射关系 + :param kwargs: key word args里面的每一个的键-值对都会被构造成映射关系 :return: None """ value_counter = defaultdict(set) @@ -153,17 +157,16 @@ class MetricBase(object): def __call__(self, pred_dict, target_dict): """ - - This method will call self.evaluate method. - Before calling self.evaluate, it will first check the validity of output_dict, target_dict - (1) whether params needed by self.evaluate is not included in output_dict,target_dict. - (2) whether params needed by self.evaluate duplicate in pred_dict, target_dict - (3) whether params in output_dict, target_dict are not used by evaluate.(Might cause warning) - Besides, before passing params into self.evaluate, this function will filter out params from output_dict and - target_dict which are not used in self.evaluate. (but if **kwargs presented in self.evaluate, no filtering - will be conducted.) - :param pred_dict: usually the output of forward or prediction function - :param target_dict: usually features set as target.. + 这个方法会调用self.evaluate 方法. + 在调用之前,会进行以下检测: + 1. self.evaluate当中是否有varargs, 这是不支持的. + 2. self.evaluate当中所需要的参数是否既不在``pred_dict``也不在``target_dict``. + 3. self.evaluate当中所需要的参数是否既在``pred_dict``也在``target_dict``. + + 除此以外,在参数被传入self.evaluate以前,这个函数会检测``pred_dict``和``target_dict``当中没有被用到的参数 + 如果kwargs是self.evaluate的参数,则不会检测 + :param pred_dict: 模型的forward函数或者predict函数返回的dict + :param target_dict: DataSet.batch_y里的键-值对所组成的dict(即is_target=True的fields的内容) :return: """ if not callable(self.evaluate): @@ -235,25 +238,29 @@ class MetricBase(object): class AccuracyMetric(MetricBase): - """Accuracy Metric - - """ - def __init__(self, pred=None, target=None, seq_lens=None): + """准确率Metric""" + def __init__(self, pred=None, target=None, seq_len=None): + """ + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + :param seq_len: 参数映射表中`seq_lens`的映射关系,None表示映射关系为`seq_len`->`seq_len` + """ super().__init__() - self._init_param_map(pred=pred, target=target, seq_lens=seq_lens) + self._init_param_map(pred=pred, target=target, seq_len=seq_len) self.total = 0 self.acc_count = 0 - def evaluate(self, pred, target, seq_lens=None): - """ + def evaluate(self, pred, target, seq_len=None): + """evaluate函数将针对一个批次的预测结果做评价指标的累计 - :param pred: . Element's shape can be: torch.Size([B,]), torch.Size([B, n_classes]), torch.Size([B, max_len]), - torch.Size([B, max_len, n_classes]) - :param target: Element's can be: torch.Size([B,]), torch.Size([B,]), torch.Size([B, max_len]), - torch.Size([B, max_len]) - :param seq_lens: Element's can be: None, None, torch.Size([B], torch.Size([B]). ignored if masks are provided. + :param torch.Tensor pred: 预测的tensor, tensor的形状可以是torch.Size([B,]), torch.Size([B, n_classes]), + torch.Size([B, max_len]), 或者torch.Size([B, max_len, n_classes]) + :param torch.Tensor target: 真实值的tensor, tensor的形状可以是Element's can be: torch.Size([B,]), + torch.Size([B,]), torch.Size([B, max_len]), 或者torch.Size([B, max_len]) + :param torch.Tensor seq_len: 序列长度标记, 标记的形状可以是None, None, torch.Size([B]), 或者torch.Size([B]). + 如果mask也被传进来的话seq_len会被忽略. """ # TODO 这里报错需要更改,因为pred是啥用户并不知道。需要告知用户真实的value @@ -264,12 +271,12 @@ class AccuracyMetric(MetricBase): raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") - if seq_lens is not None and not isinstance(seq_lens, torch.Tensor): + if seq_len is not None and not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_lens)}.") - if seq_lens is not None: - masks = seq_lens_to_masks(seq_lens=seq_lens) + if seq_len is not None: + masks = seq_lens_to_masks(seq_lens=seq_len) else: masks = None @@ -291,10 +298,10 @@ class AccuracyMetric(MetricBase): self.total += np.prod(list(pred.size())) def get_metric(self, reset=True): - """Returns computed metric. + """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果. - :param bool reset: whether to recount next time. - :return evaluate_result: {"acc": float} + :param bool reset: 在调用完get_metric后是否清空评价指标统计量. + :return dict evaluate_result: {"acc": float} """ evaluate_result = {'acc': round(float(self.acc_count) / (self.total + 1e-12), 6)} if reset: @@ -302,7 +309,8 @@ class AccuracyMetric(MetricBase): self.total = 0 return evaluate_result -def bmes_tag_to_spans(tags, ignore_labels=None): + +def _bmes_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['S', 'B-singer', 'M-singer', 'E-singer', 'S', 'S']。 返回[('', (0, 1)), ('singer', (1, 4)), ('', (4, 5)), ('', (5, 6))] (左闭右开区间) @@ -330,7 +338,8 @@ def bmes_tag_to_spans(tags, ignore_labels=None): if span[0] not in ignore_labels ] -def bmeso_tag_to_spans(tags, ignore_labels=None): + +def _bmeso_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['O', 'B-singer', 'M-singer', 'E-singer', 'O', 'O']。 返回[('singer', (1, 4))] (左闭右开区间) @@ -360,7 +369,8 @@ def bmeso_tag_to_spans(tags, ignore_labels=None): if span[0] not in ignore_labels ] -def bio_tag_to_spans(tags, ignore_labels=None): + +def _bio_tag_to_spans(tags, ignore_labels=None): """ 给定一个tags的lis,比如['O', 'B-singer', 'I-singer', 'I-singer', 'O', 'O']。 返回[('singer', (1, 4))] (左闭右开区间) @@ -385,9 +395,7 @@ def bio_tag_to_spans(tags, ignore_labels=None): else: spans.append((label, [idx, idx])) prev_bio_tag = bio_tag - return [(span[0], (span[1][0], span[1][1]+1)) - for span in spans - if span[0] not in ignore_labels] + return [(span[0], (span[1][0], span[1][1]+1)) for span in spans if span[0] not in ignore_labels] class SpanFPreRecMetric(MetricBase): @@ -416,23 +424,23 @@ class SpanFPreRecMetric(MetricBase): } """ - def __init__(self, tag_vocab, pred=None, target=None, seq_lens=None, encoding_type='bio', ignore_labels=None, + def __init__(self, tag_vocab, pred=None, target=None, seq_len=None, encoding_type='bio', ignore_labels=None, only_gross=True, f_type='micro', beta=1): """ - :param tag_vocab: Vocabulary, 标签的vocabulary。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), + :param Vocabulary tag_vocab: 标签的vocabulary。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. - :param pred: str, 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 - :param target: str, 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param seq_lens: str, 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_lens'取数据。 - :param encoding_type: str, 目前支持bio, bmes - :param ignore_labels, List[str]. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'这 + :param str pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 + :param str target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 + :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_lens'取数据。 + :param str encoding_type: 目前支持bio, bmes + :param list ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'这 个label - :param only_gross, bool. 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个 + :param bool only_gross: 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个 label的f1, pre, rec - :param f_type, str. 'micro'或'macro'. 'micro':通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; 'macro': + :param str f_type: 'micro'或'macro'. 'micro':通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; 'macro': 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) - :param beta, float. f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 + :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 """ encoding_type = encoding_type.lower() @@ -444,11 +452,11 @@ class SpanFPreRecMetric(MetricBase): self.encoding_type = encoding_type if self.encoding_type == 'bmes': - self.tag_to_span_func = bmes_tag_to_spans + self.tag_to_span_func = _bmes_tag_to_spans elif self.encoding_type == 'bio': - self.tag_to_span_func = bio_tag_to_spans + self.tag_to_span_func = _bio_tag_to_spans elif self.encoding_type == 'bmeso': - self.tag_to_span_func = bmeso_tag_to_spans + self.tag_to_span_func = _bmeso_tag_to_spans else: raise ValueError("Only support 'bio', 'bmes', 'bmeso' type.") @@ -459,7 +467,7 @@ class SpanFPreRecMetric(MetricBase): self.only_gross = only_gross super().__init__() - self._init_param_map(pred=pred, target=target, seq_lens=seq_lens) + self._init_param_map(pred=pred, target=target, seq_len=seq_len) self.tag_vocab = tag_vocab @@ -467,12 +475,12 @@ class SpanFPreRecMetric(MetricBase): self._false_positives = defaultdict(int) self._false_negatives = defaultdict(int) - def evaluate(self, pred, target, seq_lens): - """ - A lot of design idea comes from allennlp's measure - :param pred: - :param target: - :param seq_lens: + def evaluate(self, pred, target, seq_len): + """evaluate函数将针对一个批次的预测结果做评价指标的累计 + + :param pred: [batch, seq_len] 或者 [batch, seq_len, len(tag_vocab)], 预测的结果 + :param target: [batch, seq_len], 真实值 + :param seq_len: [batch] 文本长度标记 :return: """ if not isinstance(pred, torch.Tensor): @@ -482,9 +490,9 @@ class SpanFPreRecMetric(MetricBase): raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") - if not isinstance(seq_lens, torch.Tensor): + if not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(seq_lens)}.") + f"got {type(seq_len)}.") if pred.size() == target.size() and len(target.size()) == 2: pass @@ -501,8 +509,8 @@ class SpanFPreRecMetric(MetricBase): batch_size = pred.size(0) for i in range(batch_size): - pred_tags = pred[i, :int(seq_lens[i])].tolist() - gold_tags = target[i, :int(seq_lens[i])].tolist() + pred_tags = pred[i, :int(seq_len[i])].tolist() + gold_tags = target[i, :int(seq_len[i])].tolist() pred_str_tags = [self.tag_vocab.to_word(tag) for tag in pred_tags] gold_str_tags = [self.tag_vocab.to_word(tag) for tag in gold_tags] @@ -520,8 +528,9 @@ class SpanFPreRecMetric(MetricBase): self._false_negatives[span[0]] += 1 def get_metric(self, reset=True): + """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" evaluate_result = {} - if not self.only_gross or self.f_type=='macro': + if not self.only_gross or self.f_type == 'macro': tags = set(self._false_negatives.keys()) tags.update(set(self._false_positives.keys())) tags.update(set(self._true_positives.keys())) @@ -578,6 +587,7 @@ class SpanFPreRecMetric(MetricBase): return f, pre, rec + class BMESF1PreRecMetric(MetricBase): """ 按照BMES标注方式计算f1, precision, recall。由于可能存在非法tag,比如"BS",所以需要用以下的表格做转换,cur_B意思是当前tag是B, @@ -607,7 +617,7 @@ class BMESF1PreRecMetric(MetricBase): """ - def __init__(self, b_idx=0, m_idx=1, e_idx=2, s_idx=3, pred=None, target=None, seq_lens=None): + def __init__(self, b_idx=0, m_idx=1, e_idx=2, s_idx=3, pred=None, target=None, seq_len=None): """ 需要申明BMES这四种tag中,各种tag对应的idx。所有不为b_idx, m_idx, e_idx, s_idx的数字都认为是s_idx。 @@ -617,11 +627,11 @@ class BMESF1PreRecMetric(MetricBase): :param s_idx: int, Single标签所对应的tag idx :param pred: str, 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 :param target: str, 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param seq_lens: str, 用该key在evaluate()时从传入dict中取出seqence length数据。为None,则使用'seq_lens'取数据。 + :param seq_len: str, 用该key在evaluate()时从传入dict中取出seqence length数据。为None,则使用'seq_len'取数据。 """ super().__init__() - self._init_param_map(pred=pred, target=target, seq_lens=seq_lens) + self._init_param_map(pred=pred, target=target, seq_len=seq_len) self.yt_wordnum = 0 self.yp_wordnum = 0 @@ -644,7 +654,7 @@ class BMESF1PreRecMetric(MetricBase): """ 给定一个tag的Tensor,返回合法tag - :param tags: Tensor, shape: (seq_len, ) + :param torch.Tensor tags: [seq_len] :return: 返回修改为合法tag的list """ assert len(tags)!=0 @@ -663,7 +673,14 @@ class BMESF1PreRecMetric(MetricBase): return padded_tags[1:-1] - def evaluate(self, pred, target, seq_lens): + def evaluate(self, pred, target, seq_len): + """evaluate函数将针对一个批次的预测结果做评价指标的累计 + + :param pred: [batch, seq_len] 或者 [batch, seq_len, 4] + :param target: [batch, seq_len] + :param seq_len: [batch] + :return: + """ if not isinstance(pred, torch.Tensor): raise TypeError(f"`pred` in {get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(pred)}.") @@ -671,9 +688,9 @@ class BMESF1PreRecMetric(MetricBase): raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") - if not isinstance(seq_lens, torch.Tensor): + if not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(seq_lens)}.") + f"got {type(seq_len)}.") if pred.size() == target.size() and len(target.size()) == 2: pass @@ -685,7 +702,7 @@ class BMESF1PreRecMetric(MetricBase): f"{pred.size()[:-1]}, got {target.size()}.") for idx in range(len(pred)): - seq_len = seq_lens[idx] + seq_len = seq_len[idx] target_tags = target[idx][:seq_len].tolist() pred_tags = pred[idx][:seq_len] pred_tags = self._validate_tags(pred_tags) @@ -704,6 +721,7 @@ class BMESF1PreRecMetric(MetricBase): self.yp_wordnum += 1 def get_metric(self, reset=True): + """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" P = self.corr_num / (self.yp_wordnum + 1e-12) R = self.corr_num / (self.yt_wordnum + 1e-12) F = 2 * P * R / (P + R + 1e-12) @@ -746,7 +764,7 @@ def _prepare_metrics(metrics): return _metrics -def accuracy_topk(y_true, y_prob, k=1): +def _accuracy_topk(y_true, y_prob, k=1): """Compute accuracy of y_true matching top-k probable labels in y_prob. :param y_true: ndarray, true label, [n_samples] @@ -762,7 +780,7 @@ def accuracy_topk(y_true, y_prob, k=1): return acc -def pred_topk(y_prob, k=1): +def _pred_topk(y_prob, k=1): """Return top-k predicted labels and corresponding probabilities. :param y_prob: ndarray, size [n_samples, n_classes], probabilities on labels @@ -781,22 +799,24 @@ def pred_topk(y_prob, k=1): class SQuADMetric(MetricBase): + """SQuAD数据集metric + """ - def __init__(self, pred_start=None, pred_end=None, target_start=None, target_end=None, - beta=1, right_open=False, print_predict_stat=False): + def __init__(self, pred1=None, pred2=None, target1=None, target2=None, + beta=1, right_open=True, print_predict_stat=False): """ - :param pred_start: [batch], 预测答案开始的index, 如果SQuAD2.0中答案为空则为0 - :param pred_end: [batch], 预测答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) - :param target_start: [batch], 正确答案开始的index, 如果SQuAD2.0中答案为空则为0 - :param target_end: [batch], 正确答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) - :param beta: float. f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 + :param pred1: 参数映射表中`pred1`的映射关系,None表示映射关系为`pred1`->`pred1` + :param pred2: 参数映射表中`pred2`的映射关系,None表示映射关系为`pred2`->`pred2` + :param target1: 参数映射表中`target1`的映射关系,None表示映射关系为`target1`->`target1` + :param target2: 参数映射表中`target2`的映射关系,None表示映射关系为`target2`->`target2` + :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 - :param right_open: boolean. right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 - :param print_predict_stat: boolean. True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 + :param bool right_open: right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 + :param bool print_predict_stat: True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 """ super(SQuADMetric, self).__init__() - self._init_param_map(pred_start=pred_start, pred_end=pred_end, target_start=target_start, target_end=target_end) + self._init_param_map(pred1=pred1, pred2=pred2, target1=target1, target2=target2) self.print_predict_stat = print_predict_stat @@ -817,18 +837,29 @@ class SQuADMetric(MetricBase): self.right_open = right_open - def evaluate(self, pred_start, pred_end, target_start, target_end): - """ + def evaluate(self, pred1, pred2, target1, target2): + """evaluate函数将针对一个批次的预测结果做评价指标的累计 - :param pred_start: [batch, seq_len] - :param pred_end: [batch, seq_len] - :param target_start: [batch] - :param target_end: [batch] - :param labels: [batch] - :return: + :param pred1: [batch]或者[batch, seq_len], 预测答案开始的index, 如果SQuAD2.0中答案为空则为0 + :param pred2: [batch]或者[batch, seq_len] 预测答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) + :param target1: [batch], 正确答案开始的index, 如果SQuAD2.0中答案为空则为0 + :param target2: [batch], 正确答案结束的index, 如果SQuAD2.0中答案为空则为0(左闭右闭区间)或者1(左闭右开区间) + :return: None """ - start_inference = pred_start.max(dim=-1)[1].cpu().tolist() - end_inference = pred_end.max(dim=-1)[1].cpu().tolist() + pred_start = pred1 + pred_end = pred2 + target_start = target1 + target_end = target2 + + if len(pred_start.size()) == 2: + start_inference = pred_start.max(dim=-1)[1].cpu().tolist() + else: + start_inference = pred_start.cpu().tolist() + if len(pred_end.size()) == 2: + end_inference = pred_end.max(dim=-1)[1].cpu().tolist() + else: + end_inference = pred_end.cpu().tolist() + start, end = [], [] max_len = pred_start.size(1) t_start = target_start.cpu().tolist() @@ -873,6 +904,7 @@ class SQuADMetric(MetricBase): self.has_ans_f += f def get_metric(self, reset=True): + """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" evaluate_result = {} if self.no_ans_correct + self.no_ans_wrong + self.has_ans_correct + self.no_ans_wrong <= 0: diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index 4fb2a04e..a0e8f1a5 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -5,7 +5,7 @@ import torch from fastNLP.core.metrics import AccuracyMetric from fastNLP.core.metrics import BMESF1PreRecMetric -from fastNLP.core.metrics import pred_topk, accuracy_topk +from fastNLP.core.metrics import _pred_topk, _accuracy_topk class TestAccuracyMetric(unittest.TestCase): @@ -134,8 +134,8 @@ class TestAccuracyMetric(unittest.TestCase): class SpanF1PreRecMetric(unittest.TestCase): def test_case1(self): - from fastNLP.core.metrics import bmes_tag_to_spans - from fastNLP.core.metrics import bio_tag_to_spans + from fastNLP.core.metrics import _bmes_tag_to_spans + from fastNLP.core.metrics import _bio_tag_to_spans bmes_lst = ['M-8', 'S-2', 'S-0', 'B-9', 'B-6', 'E-5', 'B-7', 'S-2', 'E-7', 'S-8'] bio_lst = ['O-8', 'O-2', 'B-0', 'O-9', 'I-6', 'I-5', 'I-7', 'I-2', 'I-7', 'O-8'] @@ -145,8 +145,8 @@ class SpanF1PreRecMetric(unittest.TestCase): expect_bio_res = set() expect_bio_res.update([('7', (8, 9)), ('0', (2, 3)), ('2', (7, 8)), ('5', (5, 6)), ('6', (4, 5)), ('7', (6, 7))]) - self.assertSetEqual(expect_bmes_res,set(bmes_tag_to_spans(bmes_lst))) - self.assertSetEqual(expect_bio_res, set(bio_tag_to_spans(bio_lst))) + self.assertSetEqual(expect_bmes_res,set(_bmes_tag_to_spans(bmes_lst))) + self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans # from allennlp.data.dataset_readers.dataset_utils import bmes_tags_to_spans as allen_bmes_tags_to_spans @@ -161,8 +161,8 @@ class SpanF1PreRecMetric(unittest.TestCase): def test_case2(self): # 测试不带label的 - from fastNLP.core.metrics import bmes_tag_to_spans - from fastNLP.core.metrics import bio_tag_to_spans + from fastNLP.core.metrics import _bmes_tag_to_spans + from fastNLP.core.metrics import _bio_tag_to_spans bmes_lst = ['B', 'E', 'B', 'S', 'B', 'M', 'E', 'M', 'B', 'E'] bio_lst = ['I', 'B', 'O', 'O', 'I', 'O', 'I', 'B', 'O', 'O'] @@ -170,8 +170,8 @@ class SpanF1PreRecMetric(unittest.TestCase): expect_bmes_res.update([('', (0, 2)), ('', (2, 3)), ('', (3, 4)), ('', (4, 7)), ('', (7, 8)), ('', (8, 10))]) expect_bio_res = set() expect_bio_res.update([('', (7, 8)), ('', (6, 7)), ('', (4, 5)), ('', (0, 1)), ('', (1, 2))]) - self.assertSetEqual(expect_bmes_res,set(bmes_tag_to_spans(bmes_lst))) - self.assertSetEqual(expect_bio_res, set(bio_tag_to_spans(bio_lst))) + self.assertSetEqual(expect_bmes_res,set(_bmes_tag_to_spans(bmes_lst))) + self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans # from allennlp.data.dataset_readers.dataset_utils import bmes_tags_to_spans as allen_bmes_tags_to_spans @@ -366,7 +366,7 @@ class TestUsefulFunctions(unittest.TestCase): # 测试metrics.py中一些看上去挺有用的函数 def test_case_1(self): # multi-class - _ = accuracy_topk(np.random.randint(0, 3, size=(10, 1)), np.random.randint(0, 3, size=(10, 1)), k=3) - _ = pred_topk(np.random.randint(0, 3, size=(10, 1))) + _ = _accuracy_topk(np.random.randint(0, 3, size=(10, 1)), np.random.randint(0, 3, size=(10, 1)), k=3) + _ = _pred_topk(np.random.randint(0, 3, size=(10, 1))) # 跑通即可 From 47f812647de0ef082eb3c0d913c1d644a2703355 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 24 Apr 2019 11:12:45 +0800 Subject: [PATCH 054/173] =?UTF-8?q?1.=20=E5=88=A0=E9=99=A4=E4=BA=86LSTM?= =?UTF-8?q?=E4=B8=ADprint=E7=9A=84=E5=86=85=E5=AE=B9;=202.=20=E5=B0=86Trai?= =?UTF-8?q?ner=E5=92=8CTester=E7=9A=84use=5Fcuda=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=BA=86device;=203.=E8=A1=A5=E5=85=85Trainer?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 7 ++ fastNLP/core/dataset.py | 16 ++- fastNLP/core/losses.py | 25 ++++- fastNLP/core/metrics.py | 12 +++ fastNLP/core/optimizer.py | 15 ++- fastNLP/core/sampler.py | 14 ++- fastNLP/core/tester.py | 13 ++- fastNLP/core/trainer.py | 181 ++++++++++++++++++++++++-------- fastNLP/core/utils.py | 45 ++++++++ fastNLP/modules/encoder/lstm.py | 1 - 10 files changed, 269 insertions(+), 60 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 2ee5b3a6..4ce7d2e7 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -1,3 +1,10 @@ +""" + + .. _Callback: + +""" + + import os import torch diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index f329623e..e04978cb 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -206,6 +206,20 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 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的类型,会报如下的 错误: @@ -214,7 +228,7 @@ DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一 from fastNLP import DataSet d = DataSet({'data': [1, 'a']}) d.set_input('data') - >> RuntimeError: Mixed data types in Field data: [, ] + >>> RuntimeError: Mixed data types in Field data: [, ] 可以通过设置以忽略对该field进行类型检查 diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 08702034..4b27d402 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -1,3 +1,11 @@ +""" + + .. _LossBase: + + .. _Loss: + +""" + import inspect from collections import defaultdict @@ -196,7 +204,10 @@ class LossFunc(LossBase): class CrossEntropyLoss(LossBase): - """交叉熵损失函数""" + """ + .. _CrossEntropyLoss: + + 交叉熵损失函数""" def __init__(self, pred=None, target=None, padding_idx=-100): """ :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` @@ -261,10 +272,15 @@ class NLLLoss(LossBase): class LossInForward(LossBase): - """Forward函数中计算得到的损失函数结果""" + """ + + .. _LossInForward: + + 从forward()函数返回结果中获取loss + """ def __init__(self, loss_key='loss'): """ - :param str loss_key: 在forward函数中取得loss的键名,默认为loss + :param str loss_key: 在forward函数中loss的键名,默认为loss """ super().__init__() if not isinstance(loss_key, str): @@ -279,8 +295,7 @@ class LossInForward(LossBase): duplicated=[], required=[], all_needed=[], - varargs=[] - ) + varargs=[]) raise CheckError(check_res=check_res, func_signature=get_func_signature(self.get_loss)) return kwargs[self.loss_key] diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 314be0d9..9415926d 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -1,3 +1,12 @@ +""" + + .. _Metric: + +""" + + + + import inspect from collections import defaultdict @@ -392,6 +401,9 @@ def bio_tag_to_spans(tags, ignore_labels=None): class SpanFPreRecMetric(MetricBase): """ + + .. _SpanFPreRecMetric: + 在序列标注问题中,以span的方式计算F, pre, rec. 比如中文Part of speech中,会以character的方式进行标注,句子'中国在亚洲'对应的POS可能为(以BMES为例) ['B-NN', 'E-NN', 'S-DET', 'B-NN', 'E-NN']。该metric就是为类似情况下的F1计算。 diff --git a/fastNLP/core/optimizer.py b/fastNLP/core/optimizer.py index 145f117c..da2c45fe 100644 --- a/fastNLP/core/optimizer.py +++ b/fastNLP/core/optimizer.py @@ -13,6 +13,13 @@ class Optimizer(object): self.model_params = model_params self.settings = kwargs + def _get_require_grads_param(self, params): + """ + 将params中不需要gradient的删除 + :param iterable params: parameters + :return: list(nn.Parameters) + """ + return [param for param in params if param.requires_grad] class SGD(Optimizer): """ @@ -30,9 +37,9 @@ class SGD(Optimizer): def construct_from_pytorch(self, model_params): if self.model_params is None: # careful! generator cannot be assigned. - return torch.optim.SGD(model_params, **self.settings) + return torch.optim.SGD(self._get_require_grads_param(model_params), **self.settings) else: - return torch.optim.SGD(self.model_params, **self.settings) + return torch.optim.SGD(self._get_require_grads_param(self.model_params), **self.settings) class Adam(Optimizer): @@ -52,6 +59,6 @@ class Adam(Optimizer): def construct_from_pytorch(self, model_params): if self.model_params is None: # careful! generator cannot be assigned. - return torch.optim.Adam(model_params, **self.settings) + return torch.optim.Adam(self._get_require_grads_param(model_params), **self.settings) else: - return torch.optim.Adam(self.model_params, **self.settings) + return torch.optim.Adam(self._get_require_grads_param(self.model_params), **self.settings) diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 730e8efb..1f9b92fb 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -1,3 +1,11 @@ +""" + + .. _Sampler: + +""" + + + from itertools import chain import numpy as np @@ -28,7 +36,11 @@ class SequentialSampler(Sampler): class RandomSampler(Sampler): - """随机化取元素的 `Sampler` + """ + + .. _RandomSampler: + + 随机化取元素的 `Sampler` """ def __call__(self, data_set): diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 48e1f090..0c18ed34 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -10,6 +10,7 @@ from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_device class Tester(object): @@ -19,12 +20,14 @@ class Tester(object): :param torch.nn.modules.module model: a PyTorch model :param MetricBase metrics: a metric object or a list of metrics (List[MetricBase]) :param int batch_size: batch size for validation - :param bool use_cuda: whether to use CUDA in validation. + :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 + 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 :param int verbose: the number of steps after which an information is printed. """ - def __init__(self, data, model, metrics, batch_size=16, use_cuda=False, verbose=1): + def __init__(self, data, model, metrics, batch_size=16, device=None, verbose=1): super(Tester, self).__init__() if not isinstance(data, DataSet): @@ -35,12 +38,12 @@ class Tester(object): self.metrics = _prepare_metrics(metrics) self.data = data - self.use_cuda = use_cuda + self.device = _get_device(device, check_exist=False) self.batch_size = batch_size self.verbose = verbose - if torch.cuda.is_available() and self.use_cuda: - self._model = model.cuda() + if self.device is not None: + self._model = model.to(self.device) else: self._model = model self._model_device = model.parameters().__next__().device diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index a1190c60..7e07dd18 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -3,6 +3,88 @@ Trainer的说明文档 .. _Trainer: +Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰写 (1) epoch循环; (2) 将数据分成不同的Batch; (3) +对Batch进行pad; (4) 每个epoch结束或一定step后进行验证集验证; (5) 保存获得更好验证性能的模型等。 + +1. Trainer的基本使用 + 下面的例子是使用神经网络来进行预测一个序列中是否有偶数个1。 + + Example:: + + import numpy as np + from torch import nn + import torch + import torch.nn.functional as F + from torch.optim import SGD + + from fastNLP import DataSet + from fastNLP import Trainer + from fastNLP.core.losses import CrossEntropyLoss + from fastNLP.core.metrics import AccuracyMetric + from fastNLP.modules.decoder import MLP + + # 模型 + class Model(nn.Module): + def __init__(self, input_num): + super().__init__() + self.fcs = MLP([input_num, 40, 40, 2], 'relu') + + def forward(self, x): + x = self.fcs(x) + return {'pred': x} + model = Model(10) + + # 生成数据 + def generate_psedo_dataset(num_samples): + dataset = DataSet() + data = np.random.randint(2, size=(num_samples, 10)) + label = np.sum(data, axis=1)%2 + dataset = DataSet({'x':data.astype(float), 'label': label}) + dataset.set_input('x') + dataset.set_target('label') + return dataset + tr_dataset = generate_psedo_dataset(1000) + dev_data = generate_psedo_dataset(100) + + # 训练 + trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'), + optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000, + dev_data = dev_data, metrics=AccuracyMetric(target='label')) + trainer.train() + + 由上面的例子可以看出通过使用Trainer,可以使得训练部分的代码大幅减少。 + 使用Trainer需要满足以下几个条件 + + 1. 模型 + + 1. 模型的forward()的参数名需要与DataSet中的名字对应。实际上fastNLP在将DataSet中的数据传递给模型forward()时,是 + 通过匹配名称实现的。所以上例中,如果Model的forward函数修改为forward(self, data), 则DataSet中的'x'这个field就应该 + 改名为'data'。 + 2. 传递给forward()的参数是DataSet中被设置为input的那些field。但如果forward()中没有对应的参数,则不会将数据传递 + 给forward()。例如,DataSet中'x1', 'x2'都是input,但是模型的函数为forward(self, x1), 那么'x2'不会传递给forward()。 + 3. 模型的forward()返回值需要为一个dict。 + + 2. Loss与Metric + fastNLP中的为了不限制forward函数的返回内容数量,以及对多Metric等的支持等, Loss_ 与 Metric_ 都使用了通过名称来匹配相 + 应内容。如上面的例子中 + + Example:: + + trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'), + optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000, + dev_data = dev_data, metrics=AccuracyMetric(target='label')) + + loss被设置为了 CrossEntropyLoss_ , 但在初始化的时候传入了一个target='label'这个参数, CrossEntropyLoss_ 的初始化 + 参数为(pred=None, target=None, padding_idx=-100)。这里的两个参数分别为计算CrossEntropy时需要使用到的模型的预测值 + 与ground truth的label。其中'pred'一般是模型forward()返回结果的内容,'target'一般是来自于DataSet中被设置为target的 + field。 + +2. Trainer与callback + + +3. Trainer的代码检查 + + """ @@ -16,7 +98,7 @@ from datetime import timedelta import numpy as np import torch from torch import nn - +import warnings try: from tqdm.autonotebook import tqdm except: @@ -27,7 +109,6 @@ from fastNLP.core.callback import CallbackManager, CallbackException from fastNLP.core.dataset import DataSet from fastNLP.core.losses import _prepare_losser from fastNLP.core.metrics import _prepare_metrics -from fastNLP.core.optimizer import Adam from fastNLP.core.sampler import Sampler from fastNLP.core.sampler import RandomSampler from fastNLP.core.sampler import SequentialSampler @@ -38,42 +119,52 @@ from fastNLP.core.utils import _check_forward_error from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_device class Trainer(object): - def __init__(self, train_data, model, loss=None, metrics=None, n_epochs=3, batch_size=32, print_every=50, - validate_every=-1, dev_data=None, save_path=None, optimizer=None, - check_code_level=0, metric_key=None, sampler=None, prefetch=False, use_tqdm=True, - use_cuda=False, callbacks=None, update_every=1): + def __init__(self, train_data, model, loss, optimizer, + batch_size=32, sampler=None, update_every=1, + n_epochs=10, print_every=5, + dev_data=None, metrics=None, metric_key=None, + validate_every=-1, save_path=None, + prefetch=False, use_tqdm=True, device=None, + callbacks=None, + check_code_level=0): """ - :param DataSet train_data: the training data - :param torch.nn.modules.module model: a PyTorch model - :param LossBase loss: a loss object - :param MetricBase metrics: a metric object or a list of metrics (List[MetricBase]) - :param int n_epochs: the number of training epochs - :param int batch_size: batch size for training and validation - :param int print_every: step interval to print next training information. Default: -1(no print). - :param int validate_every: step interval to do next validation. Default: -1(validate every epoch). - :param DataSet dev_data: the validation data - :param str save_path: file path to save models - :param Optimizer optimizer: an optimizer object - :param int check_code_level: level of FastNLP code checker. -1: don't check, 0: ignore. 1: warning. 2: strict.\\ - `ignore` will not check unused field; `warning` when warn if some field are not used; `strict` means - it will raise error if some field are not used. 检查的原理是通过使用很小的batch(默认两个sample)来检查代码是 - 否能够运行,但是这个过程理论上不会修改任何参数,只是会检查能否运行。但如果(1)模型中存在将batch_size写为某个 - 固定值的情况;(2)模型中存在累加前向计算次数的,可能会多计算几次。以上情况建议将check_code_level设置为-1 - :param str metric_key: a single indicator used to decide the best model based on metric results. It must be one - of the keys returned by the FIRST metric in `metrics`. If the overall result gets better if the indicator gets - smaller, add "-" in front of the string. For example:: - - metric_key="-PPL" # language model gets better as perplexity gets smaller - :param Sampler sampler: method used to generate batch data. - :param prefetch: bool, 是否使用额外的进程对产生batch数据。 - :param bool use_tqdm: whether to use tqdm to show train progress. - :param callbacks: List[Callback]. 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 - 通过callback机制实现。 - :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128会导致内存 - 不足,通过设置batch_size=32, update_every=4达到目的 + :param DataSet train_data: 训练集 + :param nn.modules model: 待训练的模型 + :param Optimizer,None optimizer: 优化器,pytorch的torch.optim.Optimizer类型。如果为None,则Trainer不会更新模型, + 请确保已在callback中进行了更新 + :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。 + :param int batch_size: 训练和验证的时候的batch大小。 + :param Sampler sampler: Batch数据生成的顺序。详见 Sampler_ 。如果为None,默认使用 RandomSampler_ 。 + :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128 + 会导致内存不足,通过设置batch_size=32, update_every=4达到目的。当optimizer为None时,该参数无效。 + :param int n_epochs: 需要优化迭代多少次。 + :param int print_every: 多少次反向传播更新tqdm显示的loss; 如果use_tqdm=False, 则多少次反向传播打印loss。 + :param DataSet dev_data: 用于做验证的DataSet。 + :param MetricBase,list(MetricBase) metrics: 验证的评估函数。可以只使用一个Metric,也可以使用多个Metric,通过 + 列表传入。如验证时取得了更好的验证结果(如果有多个Metric,以列表中第一个Metric为准),且save_path不为None, + 则保存当前模型。Metric种类详见 Metric_ 。仅在传入dev_data时有效。 + :param str,None metric_key: Metric_ 有时会有多个指标,比如 SpanFPreRecMetric_ 中包含了'f', 'pre', 'rec'。此时需 + 要指定以哪个指标为准。另外有些指标是越小效果越好,比如语言模型的困惑度,这种情况下,在key前面增加一个'-'来表 + 明验证时,值越小越好(比如: "-ppl")。仅在传入dev_data时有效。 + :param int validate_every: 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有 + 效。 + :param str,None save_path: 将模型保存路径。如果为None,则不保存模型。如果dev_data为None,则保存最后一次迭代的模 + 型。保存的时候不仅保存了参数,还保存了模型结构。 + :param prefetch: bool, 是否使用额外的进程对产生batch数据。理论上会使得Batch迭代更快。 + :param bool use_tqdm: 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。 + :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 + 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 + :param list(callbacks) callbacks: 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 + 通过callback机制实现。 可使用的callback参见 Callback_ 。 + :param int check_code_level: 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, + 报告警告信息; 2: 有任何field没有被使用都报错. 检查的原理是通过使用很小的batch(默认2个sample)来运行代码,但是 + 这个过程理论上不会修改任何参数,只是会检查能否运行。但如果(1)模型中存在将batch_size写为某个固定值的情况; + (2)模型中存在累加前向计算次数的,可能会多计算1次。以上情况建议将check_code_level设置为-1。 """ super(Trainer, self).__init__() @@ -127,7 +218,6 @@ class Trainer(object): self.metrics = metrics self.n_epochs = int(n_epochs) self.batch_size = int(batch_size) - self.use_cuda = bool(use_cuda) self.save_path = save_path self.print_every = int(print_every) self.validate_every = int(validate_every) if validate_every != 0 else -1 @@ -141,12 +231,17 @@ class Trainer(object): self.n_steps = (len(self.train_data) // self.batch_size + int( len(self.train_data) % self.batch_size != 0)) * self.n_epochs + check_exist = check_code_level>-1 + self.device = _get_device(device, check_exist=check_exist) + if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer + elif optimizer is None: + warnings.warn("The optimizer is set to None, Trainer will update your model. Make sure you update the model" + " in the callback.") + self.optimizer = None else: - if optimizer is None: - optimizer = Adam(lr=0.01, weight_decay=0) - self.optimizer = optimizer.construct_from_pytorch(self.model.parameters()) + raise TypeError("optimizer can only be torch.optim.Optimizer type, not {}.".format(type(optimizer))) self.use_tqdm = use_tqdm self.pbar = None @@ -157,7 +252,7 @@ class Trainer(object): data=self.dev_data, metrics=self.metrics, batch_size=self.batch_size, - use_cuda=self.use_cuda, + device=self.device, verbose=0) self.step = 0 @@ -200,7 +295,7 @@ class Trainer(object): seconds: float, 表示训练时长 以下三个内容只有在提供了dev_data的情况下会有。 - best_eval: Dict of Dict, 表示evaluation的结果 + best_eval: Dict of Dict, 表示evaluation的结果。第一层的key为Metric的名称,第二层的key为具体的Metric best_epoch: int,在第几个epoch取得的最佳值 best_step: int, 在第几个step(batch)更新取得的最佳值 @@ -211,8 +306,8 @@ class Trainer(object): results['seconds'] = 0. return results try: - if torch.cuda.is_available() and self.use_cuda: - self.model = self.model.cuda() + if self.device is not None: + self.model = self.model.to(self.device) self._model_device = self.model.parameters().__next__().device self._mode(self.model, is_test=False) @@ -355,7 +450,7 @@ class Trainer(object): """Perform weight update on a model. """ - if (self.step + 1) % self.update_every == 0: + if self.optimizer is not None and (self.step + 1) % self.update_every == 0: self.optimizer.step() def _data_forward(self, network, x): diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 4aad0101..c7a6fdd8 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -7,6 +7,7 @@ from collections import namedtuple import numpy as np import torch +from torch import nn CheckRes = namedtuple('CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', 'varargs']) @@ -116,6 +117,50 @@ def pickle_exist(pickle_path, pickle_name): else: return False +def _get_device(device, check_exist=False): + """ + 传入一个device,返回None或者torch.device。当不为None时,且被设置为使用gpu, 但机器没有gpu时,会返回torch.device('cpu') + + :param str,None,torch.device device: str, None或者torch.device。 + :param bool check_exist: 检查该device是否存在,不存在的话报错 + :return: None,torch.device + """ + if device is not None: + if isinstance(device, str): + device = torch.device(device) + elif isinstance(device, torch.device): + device = device + else: + raise ValueError("device does not support {} type.".format(type(device))) + + if device.type=='cuda' and not torch.cuda.is_available(): + device = torch.device('cpu') + + if check_exist: + tensor = torch.zeros(0).to(device) + tensor = tensor.to('cpu') + del tensor + else: + device = None + + return device + + +def _get_model_device(model): + """ + 传入一个nn.Module的模型,获取它所在的device + + :param model: nn.Module + :return: torch.device,None 如果返回值为None,说明这个模型没有任何参数。 + """ + assert isinstance(model, nn.Module) + + parameters = list(model.parameters()) + if len(parameters)==0: + return None + else: + return parameters[0].device + def _build_args(func, **kwargs): spect = inspect.getfullargspec(func) diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index fe740c70..9ab8e273 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -43,7 +43,6 @@ class LSTM(nn.Module): else: hx = None if seq_len is not None and not isinstance(x, rnn.PackedSequence): - print('padding') sort_lens, sort_idx = torch.sort(seq_len, dim=0, descending=True) if self.batch_first: x = x[sort_idx] From 4a57011315283562a256b8c84583d6924d2b3a03 Mon Sep 17 00:00:00 2001 From: yh Date: Wed, 24 Apr 2019 22:28:47 +0800 Subject: [PATCH 055/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=B9Trainer?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 28 ++++- fastNLP/core/trainer.py | 227 +++++++++++++++++++++++++++++++++++++--- fastNLP/core/utils.py | 45 ++++---- 3 files changed, 261 insertions(+), 39 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 4dab772c..64f9dfbe 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -119,6 +119,9 @@ class MetricBase(object): def evaluate(self, *args, **kwargs): raise NotImplementedError + def get_metric(self, reset=True): + raise NotImplemented + def _init_param_map(self, key_map=None, **kwargs): """检查key_map和其他参数map,并将这些映射关系添加到self.param_map @@ -161,8 +164,20 @@ class MetricBase(object): f"Parameter `{func_param}` is not in {get_func_signature(self.evaluate)}. Please check the " f"initialization parameters, or change its signature.") - def get_metric(self, reset=True): - raise NotImplemented + def _fast_param_map(self, pred_dict, target_dict): + """Only used as inner function. When the pred_dict, target is unequivocal. Don't need users to pass key_map. + such as pred_dict has one element, target_dict has one element + + :param pred_dict: + :param target_dict: + :return: dict, if dict is not {}, pass it to self.evaluate. Otherwise do mapping. + """ + fast_param = {} + if len(self.param_map) == 2 and len(pred_dict) == 1 and len(target_dict) == 1: + fast_param['pred'] = list(pred_dict.values())[0] + fast_param['target'] = list(target_dict.values())[0] + return fast_param + return fast_param def __call__(self, pred_dict, target_dict): """ @@ -178,10 +193,15 @@ class MetricBase(object): :param target_dict: DataSet.batch_y里的键-值对所组成的dict(即is_target=True的fields的内容) :return: """ - if not callable(self.evaluate): - raise TypeError(f"{self.__class__.__name__}.evaluate has to be callable, not {type(self.evaluate)}.") + + fast_param = self._fast_param_map(pred_dict, target_dict) + if fast_param: + self.evaluate(**fast_param) + return if not self._checked: + if not callable(self.evaluate): + raise TypeError(f"{self.__class__.__name__}.evaluate has to be callable, not {type(self.evaluate)}.") # 1. check consistence between signature and param_map func_spect = inspect.getfullargspec(self.evaluate) func_args = set([arg for arg in func_spect.args if arg != 'self']) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 7e07dd18..09075940 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -7,6 +7,7 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 对Batch进行pad; (4) 每个epoch结束或一定step后进行验证集验证; (5) 保存获得更好验证性能的模型等。 1. Trainer的基本使用 + 下面的例子是使用神经网络来进行预测一个序列中是否有偶数个1。 Example:: @@ -53,20 +54,23 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 trainer.train() 由上面的例子可以看出通过使用Trainer,可以使得训练部分的代码大幅减少。 - 使用Trainer需要满足以下几个条件 + 使用Trainer需要满足以下几个条件: 1. 模型 1. 模型的forward()的参数名需要与DataSet中的名字对应。实际上fastNLP在将DataSet中的数据传递给模型forward()时,是 通过匹配名称实现的。所以上例中,如果Model的forward函数修改为forward(self, data), 则DataSet中的'x'这个field就应该 改名为'data'。 + 2. 传递给forward()的参数是DataSet中被设置为input的那些field。但如果forward()中没有对应的参数,则不会将数据传递 给forward()。例如,DataSet中'x1', 'x2'都是input,但是模型的函数为forward(self, x1), 那么'x2'不会传递给forward()。 + 3. 模型的forward()返回值需要为一个dict。 - 2. Loss与Metric - fastNLP中的为了不限制forward函数的返回内容数量,以及对多Metric等的支持等, Loss_ 与 Metric_ 都使用了通过名称来匹配相 - 应内容。如上面的例子中 + 2. Loss + + fastNLP中的为了不限制forward函数的返回内容数量(比如一些复杂任务需要返回多个内容,如Dependency Parsing, Loss_ 与 Metric_ 都使 + 用了通过名称来匹配相应内容的策略。如上面的例子中 Example:: @@ -74,17 +78,216 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000, dev_data = dev_data, metrics=AccuracyMetric(target='label')) - loss被设置为了 CrossEntropyLoss_ , 但在初始化的时候传入了一个target='label'这个参数, CrossEntropyLoss_ 的初始化 + loss被设置为了 CrossEntropyLoss_ , 但在初始化的时候传入了target='label'这个参数, CrossEntropyLoss_ 的初始化 参数为(pred=None, target=None, padding_idx=-100)。这里的两个参数分别为计算CrossEntropy时需要使用到的模型的预测值 - 与ground truth的label。其中'pred'一般是模型forward()返回结果的内容,'target'一般是来自于DataSet中被设置为target的 - field。 + 与真实值。其中'pred'一般来自于模型forward()的返回结果,'target'一般是来自于DataSet中被设置为target的 + field。由于每个人对真实值或者model的返回值取名并不一样,所以fastNLP的 Loss_ 提供一种类似于映射的机制来匹配 + 对应的值,比如这里 CrossEntropyLoss_ 将尝试找到名为'label'的内容来作为真实值得到loss;而pred=None, 则 CrossEntropyLoss_ + 使用'pred'作为名称匹配预测值,正好forward的返回值也叫pred,所以这里不需要申明pred。 + + 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。fastNLP中提供了 + LossInForward_ 这个loss。这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor, + 并使用它作为loss。 如果Trainer初始化没有提供loss则使用这个loss TODO 补充一个例子 + + 3. Metric -2. Trainer与callback + Metric_ 使用了与上述Loss一样的策略,即使用名称进行匹配。AccuracyMetric(target='label')的情况与CrossEntropyLoss 是同理的。 + 在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, + 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果,传入到predict()的参数也是从DataSet中的input的选择 + 出来的; 与forward()一样,返回值需要为一个dict。具体例子可以参考 TODO 补充一个例子 -3. Trainer的代码检查 +2. Trainer的代码检查 + 由于在fastNLP中采取了映射的机制,所以难免可能存在对应出错的情况。Trainer提供一种映射检查机制,可以通过check_code_level来进行控制 + 比如下面的例子中,由于各种原因产生的报错 + + Example1:: + + import numpy as np + from torch import nn + import torch + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, x, b): + loss = torch.mean((self.fc(x)-b)**2) + return {'loss': loss} + model = Model() + + dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) + dataset.set_input('a', 'b') + + trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001)) + + trainer = Trainer(dataset, model, SGD(model.parameters())) + # 会报以下的错误 + # input fields after batch(if batch size is 2): + # a: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + # b: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + # There is no target field. + # .... + # NameError: + # Problems occurred when calling Model.forward(self, x, b) + # missing param: ['x'] + # unused field: ['a'] + # Suggestion: You need to provide ['x'] in DataSet and set it as input. + + 这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里由两类 + 信息可以为你提供参考 + + 1. 'input fields after batch...'这部分显示的是train dataset经过Batch操作后,每个field对应的类型以及进行shape。这里 + 因为train dataset没有target所以没有显示。根据这里你可以看出是否正确将需要的内容设置为了input或target。 + + 2. 如果出现了映射错误,出现NameError。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 + 出当前是在调取forward出错),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 + 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x' + ,或者model的参数从'x'修改为'a'都可以解决问题。 + + 下面的例子是由于loss计算的时候找不到需要的值 + + Example2:: + + import numpy as np + from torch import nn + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet + from fastNLP.core.losses import L1Loss + import torch + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a): + return {'pred_b': self.fc(a.unsqueeze(1)).squeeze(1), 'No use':1} + + model = Model() + + dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) + + dataset.set_input('a') + dataset.set_target('b') + + trainer = Trainer(dataset, model, loss=L1Loss(target='label'), optimizer=SGD(model.parameters(), lr=0.001)) + # 报错信息如下 + # input fields after batch(if batch size is 2): + # a: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) + # target fields after batch(if batch size is 2): + # b: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) + # .... + # NameError: + # Problems occurred when calling L1Loss.get_loss(self, pred, target) + # missing param: ['pred(assign to `pred` in `L1Loss`)', 'label(assign to `target` in `L1Loss`)'] + # unused field: ['b'] + # unused param: ['pred_b', 'No use'] + # target field: ['b'] + # param from Model.forward(self, a): ['pred_b', 'No use'] + # Suggestion: (1). Check key assignment for `target` when initialize L1Loss. Or provide `label` in DataSet or output of Model.forward(self, a). + # (2). Check key assignment for `pred` when initialize L1Loss. Or provide `pred` in DataSet or output of Model.forward(self, a). + + 报错信息也包含两部分: + + 1. 第一部分与上面是一样的 + + 2. 这里报错的原因是由于计算loss的时候找不到相应的值(通过L1Loss.get_loss(self, pred, target)判断出来的);报错的原因是因为 + `pred`和`label`(我们在初始化L1Loss时将target指定为了label)都没有找到。这里'unused field'是DataSet中出现了,但却没有 + 被设置为input或者target的field;'unused param'是forward()中返回且没有被使用到的内容;'target field'是被设置为了 + target的field; 'param from Model.forward(self, a)'是forward()返回的所有key。"Suggestion"是关于当前错误处理的建议。 + + 但是在一些情况下,比如forward()返回值只有一个,target也只有一个,fastNLP不会进行匹配,而直接将forward()的结果作为pred, 将 + DataSet中的target设置为target。上面的例子在返回值中加入了一个'No use'则只是为了使得Loss去匹配结果。 + + + 下面是带有dev dataset时如果出现错误会发生的报错, + + Example3:: + + import numpy as np + from torch import nn + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet + from fastNLP.core.metrics import AccuracyMetric + import torch + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a, b): + loss = torch.mean((self.fc(a.float().unsqueeze(1))-b.float())**2) + return {'loss': loss} + def predict(self, a): # 使用predict()进行验证 + return {'output':self.fc(a.float().unsqueeze(1))} #这里return的值不包含'pred'这个key + model = Model() + + dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) + dev_data = DataSet({'a': np.arange(10, 20), 'b':np.arange(10, 20)*2}) + + dataset.set_input('a', 'b') + dev_data.set_input('a') # 这里没有设置target + + trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001), + dev_data=dev_data, metrics=AccuracyMetric()) + + # 报错信息 + # ... + # NameError: + # Problems occurred when calling AccuracyMetric.evaluate(self, pred, target, seq_len=None) + # missing param: ['pred(assign to `pred` in `AccuracyMetric`)', 'target(assign to `target` in `AccuracyMetric`)'] + # unused param: ['output'] + # target field: [] + # param from Model.predict(self, a): ['output'] + # Suggestion: (1). Check key assignment for `pred` when initialize AccuracyMetric. Or provide `pred` in DataSet or output of Model.predict(self, a). + # (2). Check key assignment for `target` when initialize AccuracyMetric. Or provide `target` in DataSet or output of Model.predict(self, a). + + 报错信息和前面都是类似的,但是可以通过'AccuracyMetric.evaluate(self, pred, target, seq_len=None)'看出这里是evaluation + 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation的弄错的情况。这里的修改是通过在初始化metric的时候 + 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output'). + + 可以通过check_code_level调节检查的强度。默认为0,即进行检查。 + +3. Trainer与callback + + 虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能,比如负采样,learning rate decay, Early Stop等。 + 为了解决这个问题fastNLP引入了callback的机制,Callback_ 是一种在Trainer训练过程中特定阶段会运行的类,所有的 Callback_ 都具有 + on_*(比如on_train_start, on_backward_begin)等函数。如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用。 + + 我们将Train.train()这个函数内部分为以下的阶段 + + Example:: + callback.on_train_begin() # 开始进行训练 + for i in range(1, n_epochs+1): + callback.on_epoch_begin() # 开始新的epoch + for batch_x, batch_y in Batch: + callback.on_batch_begin(batch_x, batch_y, indices) # batch_x是设置为input的field,batch_y是设置为target的field + 获取模型输出 + callback.on_loss_begin() + 计算loss + callback.on_backward_begin() # 可以进行一些检查,比如loss是否为None + 反向梯度回传 + callback.on_backward_end() # 进行梯度截断等 + 进行参数更新 + callback.on_step_end() + callback.on_batch_end() + # 根据设置进行evaluation,比如这是本epoch最后一个batch或者达到一定step + if do evaluation: + callback.on_valid_begin() + 进行dev data上的验证 + callback.on_valid_end() # 可以进行在其它数据集上进行验证 + callback.on_epoch_end() # epoch结束调用 + callback.on_train_end() # 训练结束 + callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 + + fastNLP已经自带了很多callback函数供使用,可以参考 Callback_ 。一些关于callback的例子,请参考 #TODO callback的例子 """ @@ -123,7 +326,7 @@ from fastNLP.core.utils import _get_device class Trainer(object): - def __init__(self, train_data, model, loss, optimizer, + def __init__(self, train_data, model, optimizer, loss=None, batch_size=32, sampler=None, update_every=1, n_epochs=10, print_every=5, dev_data=None, metrics=None, metric_key=None, @@ -135,9 +338,9 @@ class Trainer(object): :param DataSet train_data: 训练集 :param nn.modules model: 待训练的模型 :param Optimizer,None optimizer: 优化器,pytorch的torch.optim.Optimizer类型。如果为None,则Trainer不会更新模型, - 请确保已在callback中进行了更新 - :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。 + 请确保已在callback中进行了更新。 :param int batch_size: 训练和验证的时候的batch大小。 + :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。当loss为None时,默认使用 LossInForward_ 。 :param Sampler sampler: Batch数据生成的顺序。详见 Sampler_ 。如果为None,默认使用 RandomSampler_ 。 :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128 会导致内存不足,通过设置batch_size=32, update_every=4达到目的。当optimizer为None时,该参数无效。 diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index c7a6fdd8..af1a7db6 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -373,14 +373,13 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re if check_res.missing: errs.append(f"\tmissing param: {check_res.missing}") import re - mapped_missing = [] - unmapped_missing = [] + mapped_missing = [] # 提供了映射的参数 + unmapped_missing = [] # 没有指定映射的参数 input_func_map = {} - for _miss in check_res.missing: - if '(' in _miss: - # if they are like 'SomeParam(assign to xxx)' - _miss = _miss.split('(')[0] - matches = re.findall("(?<=`)[a-zA-Z0-9]*?(?=`)", _miss) + for _miss_ in check_res.missing: + # they shoudl like 'SomeParam(assign to xxx)' + _miss = _miss_.split('(')[0] + matches = re.findall("(?<=`)[a-zA-Z0-9]*?(?=`)", _miss_) if len(matches) == 2: fun_arg, module_name = matches input_func_map[_miss] = fun_arg @@ -391,30 +390,30 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re else: unmapped_missing.append(_miss) - for _miss in mapped_missing: + for _miss in mapped_missing + unmapped_missing: if _miss in dataset: - suggestions.append(f"Set {_miss} as target.") + suggestions.append(f"Set `{_miss}` as target.") else: _tmp = '' if check_res.unused: _tmp = f"Check key assignment for `{input_func_map.get(_miss, _miss)}` when initialize {module_name}." if _tmp: - _tmp += f' Or provide {_miss} in DataSet or output of {prev_func_signature}.' - else: - _tmp = f'Provide {_miss} in DataSet or output of {prev_func_signature}.' - suggestions.append(_tmp) - for _miss in unmapped_missing: - if _miss in dataset: - suggestions.append(f"Set {_miss} as target.") - else: - _tmp = '' - if check_res.unused: - _tmp = f"Specify your assignment for `{input_func_map.get(_miss, _miss)}` when initialize {module_name}." - if _tmp: - _tmp += f' Or provide {_miss} in DataSet or output of {prev_func_signature}.' + _tmp += f' Or provide `{_miss}` in DataSet or output of {prev_func_signature}.' else: - _tmp = f'Provide {_miss} in output of {prev_func_signature} or DataSet.' + _tmp = f'Provide `{_miss}` in DataSet or output of {prev_func_signature}.' suggestions.append(_tmp) + # for _miss in unmapped_missing: + # if _miss in dataset: + # suggestions.append(f"Set `{_miss}` as target.") + # else: + # _tmp = '' + # if check_res.unused: + # _tmp = f"Specify your assignment for `{input_func_map.get(_miss, _miss)}` when initialize {module_name}." + # if _tmp: + # _tmp += f' Or provide `{_miss}` in DataSet or output of {prev_func_signature}.' + # else: + # _tmp = f'Provide `{_miss}` in output of {prev_func_signature} or DataSet.' + # suggestions.append(_tmp) if check_res.duplicated: errs.append(f"\tduplicated param: {check_res.duplicated}.") From 6e265e5ae908a0b6ba6479c64ebf232418800e82 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 25 Apr 2019 00:26:15 +0800 Subject: [PATCH 056/173] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86trainer?= =?UTF-8?q?=EF=BC=8Ccallback=E7=AD=89=E7=9A=84=E6=96=87=E6=A1=A3;=20?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E7=9A=84=E5=91=BD=E5=90=8D=E4=BB=A5=E4=BD=BF=E5=BE=97=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BB=8E=E6=96=87=E6=A1=A3=E4=B8=AD=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/automl/enas_trainer.py | 4 +- fastNLP/core/callback.py | 213 ++++++++++++++++++--------------- fastNLP/core/dataset.py | 6 +- fastNLP/core/fieldarray.py | 92 ++++++-------- fastNLP/core/instance.py | 29 +++-- fastNLP/core/losses.py | 30 ++--- fastNLP/core/metrics.py | 50 ++++---- fastNLP/core/tester.py | 12 +- fastNLP/core/trainer.py | 91 ++++++-------- fastNLP/core/utils.py | 163 ++++++++++++++++--------- fastNLP/core/vocabulary.py | 26 ++-- fastNLP/models/enas_trainer.py | 6 +- test/core/test_tester.py | 2 +- 13 files changed, 373 insertions(+), 351 deletions(-) diff --git a/fastNLP/automl/enas_trainer.py b/fastNLP/automl/enas_trainer.py index 061d604c..a6316341 100644 --- a/fastNLP/automl/enas_trainer.py +++ b/fastNLP/automl/enas_trainer.py @@ -11,7 +11,7 @@ import torch try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import pseudo_tqdm as tqdm + from fastNLP.core.utils import _pseudo_tqdm as tqdm from fastNLP.core.batch import Batch from fastNLP.core.callback import CallbackException @@ -115,7 +115,7 @@ class ENASTrainer(fastNLP.Trainer): def _train(self): if not self.use_tqdm: - from fastNLP.core.utils import pseudo_tqdm as inner_tqdm + from fastNLP.core.utils import _pseudo_tqdm as inner_tqdm else: inner_tqdm = tqdm self.step = 0 diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 4ce7d2e7..a416f655 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -2,21 +2,21 @@ .. _Callback: -""" +Callback是fastNLP中被设计用于增强 Trainer_ 的类。如果Callback被传递给了 Trainer_ , 则 Trainer_ 会在对应的阶段调用Callback +的函数,具体调用时机可以通过 Trainer_ 查看。 +""" import os - import torch -from tensorboardX import SummaryWriter - from fastNLP.io.model_io import ModelSaver, ModelLoader - +try: + from tensorboardX import SummaryWriter +except: + pass class Callback(object): - """An Interface for all callbacks. - - Any customized callback should implement at least one of the following methods. + """这是Callback的基类,所有的callback必须继承自这个类。 """ @@ -26,93 +26,150 @@ class Callback(object): @property def trainer(self): + """ + 该属性可以通过self.trainer获取到,一般情况下不需要使用这个属性。 + :return: + """ return self._trainer @property def step(self): - """current step number, in range(1, self.n_steps+1)""" + """当前运行到的step, 范围为[1, self.n_steps+1)""" return self._trainer.step @property def n_steps(self): - """total number of steps for training""" + """Trainer一共会运行多少步""" return self._trainer.n_steps @property def batch_size(self): - """batch size for training""" + """train和evaluate时的batch_size为多大""" return self._trainer.batch_size @property def epoch(self): - """current epoch number, in range(1, self.n_epochs+1)""" + """当前运行的epoch数,范围是[1, self.n_epochs+1)""" return self._trainer.epoch @property def n_epochs(self): - """total number of epochs""" + """一共会运行多少个epoch""" return self._trainer.n_epochs @property def optimizer(self): - """torch.optim.Optimizer for current model""" + """初始化Trainer时传递的Optimizer""" return self._trainer.optimizer @property def model(self): - """training model""" + """正在被Trainer训练的模型""" return self._trainer.model @property def pbar(self): - """If use_tqdm, return trainer's tqdm print bar, else return None.""" + """如果在Callback中需要打印内容,请使用self.pbar.write(str)。否则可能出现命令行显示效果不太好的问题。""" return self._trainer.pbar @property def update_every(self): - """The model in trainer will update parameters every `update_every` batches.""" + """Trainer中的模型多少次反向传播才进行一次梯度更新,在Trainer初始化时传入的。""" return self._trainer.update_every + + @property + def batch_per_epoch(self): + """每个epoch一共有多少个batch,只有在on_epoch_begin之后才能调用该属性。""" + return self._trainer.batch_per_epoch + def on_train_begin(self): - # before the main training loop + """ + 在Train过程开始之前调用。 + + :return: + """ pass def on_epoch_begin(self): - # at the beginning of each epoch + """ + 在每个epoch开始之前调用一次 + + :return: + """ pass def on_batch_begin(self, batch_x, batch_y, indices): - # at the beginning of each step/mini-batch + """ + 每次采集到一个batch的数据则调用一次。这里对batch_x或batch_y删除添加内容是可以影响到Trainer中内容的。所以在这一步 + 可以进行一些负采样之类的操作 + + :param dict batch_x: DataSet中被设置为input的field的batch。 + :param dict batch_y: DataSet中被设置为target的field的batch。 + :param list(int) indices: 这次采样使用到的indices,可以通过DataSet[indices]获取出这个batch采出的Instance,在一些 + 情况下可以帮助定位是哪个Sample导致了错误。 + :return: + """ pass def on_loss_begin(self, batch_y, predict_y): - # after data_forward, and before loss computation + """ + 在计算loss前调用,即这里修改batch_y或predict_y的值是可以影响到loss计算的。 + + :param dict batch_y: 在DataSet中被设置为target的field的batch集合。 + :param dict predict_y: 模型的forward()返回的结果。 + :return: + """ pass def on_backward_begin(self, loss): - # after loss computation, and before gradient backward + """ + 在loss得到之后,但在反向传播之前。可能可以进行loss是否为NaN的检查。 + + :param torch.Tensor loss: 计算得到的loss值 + :return: + """ pass def on_backward_end(self): + """ + 反向梯度传播已完成,但由于update_every的设置,可能并不是每一次调用都有梯度。到这一步,还没有更新参数。 + + :return: + """ pass def on_step_end(self): + """ + 到这里模型的参数已经按照梯度更新。但可能受update_every影响,并不是每次都更新了。 + + :return: + """ pass - def on_batch_end(self, *args): - # at the end of each step/mini-batch + def on_batch_end(self): + """ + 这一步与on_step_end是紧接着的。只是为了对称性加上了这一步。 + + """ pass def on_valid_begin(self): + """ + 如果Trainer中设置了验证,则发生验证前会调用该函数 + + :return: + """ pass def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): """ - 每次执行验证机的evaluation后会调用。传入eval_result + 每次执行验证集的evaluation后会调用。 - :param eval_result: Dict[str: Dict[str: float]], evaluation的结果 - :param metric_key: str - :param optimizer: optimizer passed to trainer - :param is_better_eval: bool, 当前dev结果是否比之前的好 + :param Dict[str: Dict[str: float]] eval_result: , evaluation的结果。一个例子为{'AccuracyMetric':{'acc':1.0}},即 + 传入的dict是有两层,第一层是metric的名称,第二层是metric的具体指标。 + :param str metric_key: 初始化Trainer时传入的metric_key。 + :param torch.Optimizer optimizer: Trainer中使用的优化器。 + :param bool is_better_eval: 当前dev结果是否比之前的好。 :return: """ pass @@ -137,7 +194,7 @@ class Callback(object): pass -def transfer(func): +def _transfer(func): """装饰器,将对CallbackManager的调用转发到各个Callback子类. :param func: :return: @@ -153,9 +210,7 @@ def transfer(func): class CallbackManager(Callback): - """A manager for all callbacks passed into Trainer. - It collects resources inside Trainer and raise callbacks. - + """内部使用的Callback管理类 """ def __init__(self, env, callbacks=None): @@ -182,104 +237,70 @@ class CallbackManager(Callback): for callback in self.callbacks: setattr(callback, '_'+env_name, env_val) # Callback.trainer - @transfer + @_transfer def on_train_begin(self): pass - @transfer + @_transfer def on_epoch_begin(self): pass - @transfer + @_transfer def on_batch_begin(self, batch_x, batch_y, indices): pass - @transfer + @_transfer def on_loss_begin(self, batch_y, predict_y): pass - @transfer + @_transfer def on_backward_begin(self, loss): pass - @transfer + @_transfer def on_backward_end(self): pass - @transfer + @_transfer def on_step_end(self): pass - @transfer + @_transfer def on_batch_end(self): pass - @transfer + @_transfer def on_valid_begin(self): pass - @transfer + @_transfer def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): pass - @transfer + @_transfer def on_epoch_end(self): pass - @transfer + @_transfer def on_train_end(self): pass - @transfer + @_transfer def on_exception(self, exception): pass -class DummyCallback(Callback): - def on_train_begin(self, *arg): - print(arg) - - def on_epoch_end(self): - print(self.epoch, self.n_epochs) - - -class EchoCallback(Callback): - def on_train_begin(self): - print("before_train") - - def on_epoch_begin(self): - print("before_epoch") - - def on_batch_begin(self, batch_x, batch_y, indices): - print("before_batch") - - def on_loss_begin(self, batch_y, predict_y): - print("before_loss") - - def on_backward_begin(self, loss): - print("before_backward") - - def on_batch_end(self): - print("after_batch") - - def on_epoch_end(self): - print("after_epoch") - - def on_train_end(self): - print("after_train") - - class GradientClipCallback(Callback): def __init__(self, parameters=None, clip_value=1, clip_type='norm'): """每次backward前,将parameter的gradient clip到某个范围。 - :param parameters: None, torch.Tensor或List[torch.Tensor], 一般通过model.parameters()获得。如果为None则默认对Trainer + :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer 的model中所有参数进行clip - :param clip_value: float, 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 - :param clip_type: str, 支持'norm', 'value'两种。 - (1) 'norm', 将gradient的norm rescale到[-clip_value, clip_value] - (2) 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; 大于 - clip_value的gradient被赋值为clip_value. + :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 + :param str clip_type: 支持'norm', 'value'两种。 + 1. 'norm', 将gradient的norm rescale到[-clip_value, clip_value] + 2. 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; 大于 + clip_value的gradient被赋值为clip_value. """ super().__init__() @@ -314,7 +335,7 @@ class EarlyStopCallback(Callback): def __init__(self, patience): """ - :param int patience: 停止之前等待的epoch数 + :param int patience: 多少个epoch没有变好就停止训练 """ super(EarlyStopCallback, self).__init__() self.patience = patience @@ -341,7 +362,7 @@ class LRScheduler(Callback): def __init__(self, lr_scheduler): """对PyTorch LR Scheduler的包装 - :param lr_scheduler: PyTorch的lr_scheduler + :param torch.optim.lr_scheduler._LRScheduler lr_scheduler: PyTorch的lr_scheduler """ super(LRScheduler, self).__init__() import torch.optim @@ -358,7 +379,7 @@ class ControlC(Callback): def __init__(self, quit_all): """ - :param quit_all: 若为True,则检测到control+C 直接退出程序;否则只退出Trainer + :param bool quit_all: 若为True,则检测到control+C 直接退出程序;否则只退出Trainer """ super(ControlC, self).__init__() if type(quit_all) != bool: @@ -389,16 +410,16 @@ class SmoothValue(object): class LRFinder(Callback): - def __init__(self, n_batch, start_lr=1e-6, end_lr=10): + def __init__(self, start_lr=1e-6, end_lr=10): """用第一个 epoch 找最佳的学习率,从第二个epoch开始应用它 - :param n_batch: 一个epoch内的iteration数 - :param start_lr: 学习率下界 - :param end_lr: 学习率上界 + :param int n_batch: 一个epoch内的iteration数 + :param float start_lr: 学习率下界 + :param float end_lr: 学习率上界 """ super(LRFinder, self).__init__() self.start_lr, self.end_lr = start_lr, end_lr - self.num_it = n_batch + self.num_it = self.batch_per_epoch self.stop = False self.best_loss = 0. self.best_lr = None @@ -514,7 +535,3 @@ class TensorboardCallback(Callback): del self._summary_writer -if __name__ == "__main__": - manager = CallbackManager(env={"n_epoch": 3}, callbacks=[DummyCallback(), DummyCallback()]) - manager.on_train_begin() - # print(manager.after_epoch()) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index e04978cb..00f8ce04 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -277,7 +277,7 @@ 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 fastNLP.core.utils import _get_func_signature class DataSet(object): """fastNLP的数据容器 @@ -642,7 +642,7 @@ class DataSet(object): 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))) + 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) @@ -707,7 +707,7 @@ class DataSet(object): 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))) + 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) diff --git a/fastNLP/core/fieldarray.py b/fastNLP/core/fieldarray.py index f3f20935..127410d1 100644 --- a/fastNLP/core/fieldarray.py +++ b/fastNLP/core/fieldarray.py @@ -1,5 +1,5 @@ """ -FieldArray是 DataSet_ 中一列的存储方式 +FieldArray是 DataSet_ 中一列的存储方式,原理部分请参考 DataSet_ 处 .. _FieldArray: @@ -11,41 +11,19 @@ from copy import deepcopy class FieldArray(object): - """``FieldArray`` is the collection of ``Instance``s of the same field. - It is the basic element of ``DataSet`` class. - - :param str name: the name of the FieldArray - :param list content: a list of int, float, str or np.ndarray, or a list of list of one, or a np.ndarray. - :param bool is_target: If True, this FieldArray is used to compute loss. - :param bool is_input: If True, this FieldArray is used to the model input. - :param Padder padder: PadderBase类型。赋值给fieldarray的padder的对象会被deepcopy一份,需要修改padder参数必须通过 - fieldarray.set_pad_val()。 - 默认为None,(1)如果某个field是scalar,则不进行任何padding;(2)如果为一维list, 且fieldarray的dtype为float或int类型 - 则会进行padding;(3)其它情况不进行padder。 - 假设需要对English word中character进行padding,则需要使用其他的padder。 - 或ignore_type为True但是需要进行padding。 - :param bool ignore_type: whether to ignore type. If True, no type detection will rise for this FieldArray. - (default: False) - """ - def __init__(self, name, content, is_target=None, is_input=None, padder=None, ignore_type=False): - """DataSet在初始化时会有两类方法对FieldArray操作: - 1) 如果DataSet使用dict初始化,那么在add_field中会构造FieldArray: - 1.1) 二维list DataSet({"x": [[1, 2], [3, 4]]}) - 1.2) 二维array DataSet({"x": np.array([[1, 2], [3, 4]])}) - 1.3) 三维list DataSet({"x": [[[1, 2], [3, 4]], [[1, 2], [3, 4]]]}) - 1.4) list of array: DataSet({"x": [np.array([1,2,3]), np.array([1,2,3])]}) - 2) 如果DataSet使用list of Instance 初始化,那么在append中会先对第一个样本初始化FieldArray; - 然后后面的样本使用FieldArray.append进行添加。 - 2.1) 一维list DataSet([Instance(x=[1, 2, 3, 4])]) - 2.2) 一维array DataSet([Instance(x=np.array([1, 2, 3, 4]))]) - 2.3) 二维list DataSet([Instance(x=[[1, 2], [3, 4]])]) - 2.4) 二维array DataSet([Instance(x=np.array([[1, 2], [3, 4]]))]) - - 类型检查(dtype check)发生在当该field被设置为is_input或者is_target时。 - ignore_type用来控制是否进行类型检查,如果为True,则不检查。 - + """FieldArray是用于保存 DataSet_ 中一个field的实体。 + + :param str name: FieldArray的名称 + :param list,numpy.ndarray content: 列表的元素可以为list,int,float, + :param bool is_target: 这个field是否是一个target field。 + :param bool is_input: 这个field是否是一个input field。 + :param Padder padder: PadderBase类型。赋值给fieldarray的padder的对象会被deepcopy一份,需要修改padder参数必须通过 + fieldarray.set_pad_val()。默认为None,即使用 AutoPadder_ 。 + :param bool ignore_type: 是否忽略该field的type,一般如果这个field不需要转为torch.FloatTensor或torch.LongTensor, 就 + 可以设置为True。具体意义请参考 DataSet_ 。 """ + self.name = name if isinstance(content, list): # 如果DataSet使用dict初始化, content 可能是二维list/二维array/三维list @@ -211,10 +189,10 @@ class FieldArray(object): return "FieldArray {}: {}".format(self.name, self.content.__repr__()) def append(self, val): - """将val增加到FieldArray中,若该field的ignore_type为True则直接append到这个field中;若ignore_type为False,且当前field为 - input或者target,则会检查传入的content是否与之前的内容在dimension, 元素的类型上是匹配的。 + """将val append到这个field的尾部。如果这个field已经被设置为input或者target,则在append之前会检查该类型是否与已有 + 的内容是匹配的。 - :param val: Any. + :param Any val: 需要append的值。 """ if self.ignore_type is False: if isinstance(val, list): @@ -262,8 +240,8 @@ class FieldArray(object): def get(self, indices, pad=True): """根据给定的indices返回内容 - :param indices: (int, List[int]), 获取indices对应的内容。 - :param pad: bool, 是否对返回的结果进行padding。仅对indices为List[int]时有效 + :param int,list(int) indices:, 获取indices对应的内容。 + :param bool pad: , 是否对返回的结果进行padding。仅对indices为List[int]时有效 :return: (single, List) """ if isinstance(indices, int): @@ -281,7 +259,7 @@ class FieldArray(object): """ 设置padder,在这个field进行pad的时候用这个padder进行pad,如果为None则不进行pad。 - :param padder: (None, Padder). 设置为None即删除padder. + :param None,Padder padder:. 设置为None即删除padder。 :return: """ if padder is not None: @@ -293,7 +271,7 @@ class FieldArray(object): def set_pad_val(self, pad_val): """修改padder的pad_val. - :param pad_val: int。将该field的pad值设置为该值 + :param int pad_val: 该field的pad值设置为该值。 :return: """ if self.padder is not None: @@ -312,8 +290,8 @@ class FieldArray(object): """ 将other的属性复制给本FieldArray(other必须为FieldArray类型).属性包括 is_input, is_target, padder, ignore_type - :param other: FieldArray - :return: + :param FieldArray other: 从哪个field拷贝属性 + :return: FieldArray """ assert isinstance(other, FieldArray), "Only support FieldArray type, not {}.".format(type(other)) @@ -324,7 +302,7 @@ class FieldArray(object): return self -def is_iterable(content): +def _is_iterable(content): try: _ = (e for e in content) except TypeError: @@ -350,11 +328,10 @@ class Padder: """ 传入的是List内容。假设有以下的DataSet。 - :param contents: List[element]。传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前 + :param list(Any) contents: 传入的element是inplace的,即直接修改element可能导致数据变化,建议inplace修改之前 deepcopy一份。 - :param field_name: str, field的名称。 - :param field_ele_dtype: (np.int64, np.float64, np.str, None), 该field的内层元素的类型。如果该field的ignore_type - 为True,该这个值为None。 + :param str, field_name: field的名称。 + :param np.int64,np.float64,np.str,None, field_ele_dtype: 该field的内层元素的类型。如果该field的ignore_type为True,该这个值为None。 :return: np.array([padded_element]) Example:: @@ -400,10 +377,10 @@ class AutoPadder(Padder): 2 如果元素类型为(np.int64, np.float64), - 2.1 如果该field的内容为(np.int64, np.float64),比如为seq_len, 则不进行padding + 2.1 如果该field的内容为(np.int64, np.float64),比如为seq_len, 则不进行padding - 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 - 如果某个instance中field为[1, 2, 3],则可以pad;若为[[1,2], [3,4, ...]]则不能进行pad + 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 + 即如果Instance中field形如[1, 2, 3, ...],则可以pad;若为[[1,2], [3,4, ...]]则不能进行pad """ def __init__(self, pad_val=0): @@ -427,7 +404,7 @@ class AutoPadder(Padder): return False def __call__(self, contents, field_name, field_ele_dtype): - if not is_iterable(contents[0]): + if not _is_iterable(contents[0]): array = np.array([content for content in contents], dtype=field_ele_dtype) elif field_ele_dtype in (np.int64, np.float64) and self._is_two_dimension(contents): max_len = max([len(content) for content in contents]) @@ -454,7 +431,7 @@ class EngChar2DPadder(Padder): Example:: from fastNLP import DataSet - from fastNLP import EnChar2DPadder + from fastNLP import EngChar2DPadder from fastNLP import Vocabulary dataset = DataSet({'sent': ['This is the first demo', 'This is the second demo']}) dataset.apply(lambda ins:[list(word) for word in ins['sent'].split()], new_field_name='chars') @@ -462,14 +439,15 @@ class EngChar2DPadder(Padder): vocab.from_dataset(dataset, field_name='chars') vocab.index_dataset(dataset, field_name='chars') dataset.set_input('chars') - padder = EnChar2DPadder() + padder = EngChar2DPadder() dataset.set_padder('chars', padder) # chars这个field的设置为了EnChar2DPadder + """ def __init__(self, pad_val=0, pad_length=0): """ :param pad_val: int, pad的位置使用该index - :param pad_length: int, 如果为0则取一个batch中最大的单词长度作为padding长度。如果为大于0的数,则将所有单词的长度都pad或截 - 取到该长度. + :param pad_length: int, 如果为0则取一个batch中最大的单词长度作为padding长度。如果为大于0的数,则将所有单词的长度 + 都pad或截取到该长度. """ super().__init__(pad_val=pad_val) @@ -494,7 +472,7 @@ class EngChar2DPadder(Padder): except: raise ValueError("Field:{} only has two dimensions.".format(field_name)) - if is_iterable(value): + if _is_iterable(value): raise ValueError("Field:{} has more than 3 dimension.".format(field_name)) def __call__(self, contents, field_name, field_ele_dtype): diff --git a/fastNLP/core/instance.py b/fastNLP/core/instance.py index 4271901f..51441906 100644 --- a/fastNLP/core/instance.py +++ b/fastNLP/core/instance.py @@ -3,34 +3,33 @@ Instance文档 .. _Instance: -测试 +Instance是fastNLP中对应于一个sample的类。一个sample可以认为是fastNLP中的一个Instance对象。一个具像化的表示类似与 DataSet_ +出那个表中所展示的一行。 """ class Instance(object): - """An Instance is an example of data. - 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]) - - """ - 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]) - :param fields: 可能是一维或者二维的 list or np.array + ins = Instance(**{'x1': 1, 'x2':np.zeros((3, 4))}) """ self.fields = fields def add_field(self, field_name, field): - """Add a new field to the instance. + """向Instance中增加一个field - :param field_name: str, the name of the field. + :param str field_name: 新增field的名称 + :param Any field: 新增field的内容 """ self.fields[field_name] = field diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 4b27d402..abb1cd29 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -12,12 +12,12 @@ from collections import defaultdict import torch import torch.nn.functional as F -from fastNLP.core.utils import CheckError -from fastNLP.core.utils import CheckRes +from fastNLP.core.utils import _CheckError +from fastNLP.core.utils import _CheckRes from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_arg_dict_list from fastNLP.core.utils import _check_function_or_method -from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_func_signature class LossBase(object): @@ -70,7 +70,7 @@ class LossBase(object): for func_param, input_param in self.param_map.items(): if func_param not in func_args: raise NameError( - f"Parameter `{func_param}` is not in {get_func_signature(self.get_loss)}. Please check the " + f"Parameter `{func_param}` is not in {_get_func_signature(self.get_loss)}. Please check the " f"initialization parameters, or change its signature.") # evaluate should not have varargs. @@ -111,7 +111,7 @@ class LossBase(object): func_args = set([arg for arg in func_spect.args if arg != 'self']) for func_arg, input_arg in self.param_map.items(): if func_arg not in func_args: - raise NameError(f"`{func_arg}` not in {get_func_signature(self.get_loss)}.") + raise NameError(f"`{func_arg}` not in {_get_func_signature(self.get_loss)}.") # 2. only part of the param_map are passed, left are not for arg in func_args: @@ -151,16 +151,16 @@ class LossBase(object): replaced_missing[idx] = f"{self.param_map[func_arg]}" + f"(assign to `{func_arg}` " \ f"in `{self.__class__.__name__}`)" - check_res = CheckRes(missing=replaced_missing, - unused=check_res.unused, - duplicated=duplicated, - required=check_res.required, - all_needed=check_res.all_needed, - varargs=check_res.varargs) + check_res = _CheckRes(missing=replaced_missing, + unused=check_res.unused, + duplicated=duplicated, + required=check_res.required, + all_needed=check_res.all_needed, + varargs=check_res.varargs) if check_res.missing or check_res.duplicated: - raise CheckError(check_res=check_res, - func_signature=get_func_signature(self.get_loss)) + raise _CheckError(check_res=check_res, + func_signature=_get_func_signature(self.get_loss)) refined_args = _build_args(self.get_loss, **mapped_pred_dict, **mapped_target_dict) loss = self.get_loss(**refined_args) @@ -289,14 +289,14 @@ class LossInForward(LossBase): def get_loss(self, **kwargs): if self.loss_key not in kwargs: - check_res = CheckRes( + check_res = _CheckRes( missing=[self.loss_key + f"(assign to `{self.loss_key}` in `{self.__class__.__name__}`"], unused=[], duplicated=[], required=[], all_needed=[], varargs=[]) - raise CheckError(check_res=check_res, func_signature=get_func_signature(self.get_loss)) + raise _CheckError(check_res=check_res, func_signature=_get_func_signature(self.get_loss)) return kwargs[self.loss_key] def __call__(self, pred_dict, target_dict, check=False): diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 64f9dfbe..206904ca 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -13,11 +13,11 @@ from collections import defaultdict import numpy as np import torch -from fastNLP.core.utils import CheckError -from fastNLP.core.utils import CheckRes +from fastNLP.core.utils import _CheckError +from fastNLP.core.utils import _CheckRes from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_arg_dict_list -from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_func_signature from fastNLP.core.utils import seq_lens_to_masks from fastNLP.core.vocabulary import Vocabulary @@ -161,7 +161,7 @@ class MetricBase(object): for func_param, input_param in self.param_map.items(): if func_param not in func_args: raise NameError( - f"Parameter `{func_param}` is not in {get_func_signature(self.evaluate)}. Please check the " + f"Parameter `{func_param}` is not in {_get_func_signature(self.evaluate)}. Please check the " f"initialization parameters, or change its signature.") def _fast_param_map(self, pred_dict, target_dict): @@ -207,7 +207,7 @@ class MetricBase(object): func_args = set([arg for arg in func_spect.args if arg != 'self']) for func_arg, input_arg in self.param_map.items(): if func_arg not in func_args: - raise NameError(f"`{func_arg}` not in {get_func_signature(self.evaluate)}.") + raise NameError(f"`{func_arg}` not in {_get_func_signature(self.evaluate)}.") # 2. only part of the param_map are passed, left are not for arg in func_args: @@ -248,16 +248,16 @@ class MetricBase(object): replaced_missing[idx] = f"{self.param_map[func_arg]}" + f"(assign to `{func_arg}` " \ f"in `{self.__class__.__name__}`)" - check_res = CheckRes(missing=replaced_missing, - unused=check_res.unused, - duplicated=duplicated, - required=check_res.required, - all_needed=check_res.all_needed, - varargs=check_res.varargs) + check_res = _CheckRes(missing=replaced_missing, + unused=check_res.unused, + duplicated=duplicated, + required=check_res.required, + all_needed=check_res.all_needed, + varargs=check_res.varargs) if check_res.missing or check_res.duplicated: - raise CheckError(check_res=check_res, - func_signature=get_func_signature(self.evaluate)) + raise _CheckError(check_res=check_res, + func_signature=_get_func_signature(self.evaluate)) refined_args = _build_args(self.evaluate, **mapped_pred_dict, **mapped_target_dict) self.evaluate(**refined_args) @@ -294,14 +294,14 @@ class AccuracyMetric(MetricBase): """ # TODO 这里报错需要更改,因为pred是啥用户并不知道。需要告知用户真实的value if not isinstance(pred, torch.Tensor): - raise TypeError(f"`pred` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`pred` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(pred)}.") if not isinstance(target, torch.Tensor): - raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") if seq_len is not None and not isinstance(seq_len, torch.Tensor): - raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_lens)}.") if seq_len is not None: @@ -314,7 +314,7 @@ class AccuracyMetric(MetricBase): elif len(pred.size()) == len(target.size()) + 1: pred = pred.argmax(dim=-1) else: - raise RuntimeError(f"In {get_func_signature(self.evaluate)}, when pred have " + raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") @@ -516,14 +516,14 @@ class SpanFPreRecMetric(MetricBase): :return: """ if not isinstance(pred, torch.Tensor): - raise TypeError(f"`pred` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`pred` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(pred)}.") if not isinstance(target, torch.Tensor): - raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") if not isinstance(seq_len, torch.Tensor): - raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_len)}.") if pred.size() == target.size() and len(target.size()) == 2: @@ -535,7 +535,7 @@ class SpanFPreRecMetric(MetricBase): raise ValueError("A gold label passed to SpanBasedF1Metric contains an " "id >= {}, the number of classes.".format(num_classes)) else: - raise RuntimeError(f"In {get_func_signature(self.evaluate)}, when pred have " + raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") @@ -714,14 +714,14 @@ class BMESF1PreRecMetric(MetricBase): :return: """ if not isinstance(pred, torch.Tensor): - raise TypeError(f"`pred` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`pred` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(pred)}.") if not isinstance(target, torch.Tensor): - raise TypeError(f"`target` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") if not isinstance(seq_len, torch.Tensor): - raise TypeError(f"`seq_lens` in {get_func_signature(self.evaluate)} must be torch.Tensor," + raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_len)}.") if pred.size() == target.size() and len(target.size()) == 2: @@ -729,7 +729,7 @@ class BMESF1PreRecMetric(MetricBase): elif len(pred.size()) == len(target.size()) + 1 and len(target.size()) == 2: pred = pred.argmax(dim=-1) else: - raise RuntimeError(f"In {get_func_signature(self.evaluate)}, when pred have " + raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 0c18ed34..9737f53a 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -5,11 +5,11 @@ from fastNLP.core.batch import Batch from fastNLP.core.dataset import DataSet from fastNLP.core.metrics import _prepare_metrics from fastNLP.core.sampler import SequentialSampler -from fastNLP.core.utils import CheckError +from fastNLP.core.utils import _CheckError from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device -from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_func_signature from fastNLP.core.utils import _get_device @@ -75,19 +75,19 @@ class Tester(object): _move_dict_value_to_device(batch_x, batch_y, device=self._model_device) pred_dict = self._data_forward(self._predict_func, batch_x) if not isinstance(pred_dict, dict): - raise TypeError(f"The return value of {get_func_signature(self._predict_func)} " + raise TypeError(f"The return value of {_get_func_signature(self._predict_func)} " f"must be `dict`, got {type(pred_dict)}.") for metric in self.metrics: metric(pred_dict, batch_y) for metric in self.metrics: eval_result = metric.get_metric() if not isinstance(eval_result, dict): - raise TypeError(f"The return value of {get_func_signature(metric.get_metric)} must be " + raise TypeError(f"The return value of {_get_func_signature(metric.get_metric)} must be " f"`dict`, got {type(eval_result)}") metric_name = metric.__class__.__name__ eval_results[metric_name] = eval_result - except CheckError as e: - prev_func_signature = get_func_signature(self._predict_func) + except _CheckError as e: + prev_func_signature = _get_func_signature(self._predict_func) _check_loss_evaluate(prev_func_signature=prev_func_signature, func_signature=e.func_signature, check_res=e.check_res, pred_dict=pred_dict, target_dict=batch_y, dataset=self.data, check_level=0) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 09075940..44d88d3c 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -85,17 +85,17 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 对应的值,比如这里 CrossEntropyLoss_ 将尝试找到名为'label'的内容来作为真实值得到loss;而pred=None, 则 CrossEntropyLoss_ 使用'pred'作为名称匹配预测值,正好forward的返回值也叫pred,所以这里不需要申明pred。 - 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。fastNLP中提供了 - LossInForward_ 这个loss。这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor, - 并使用它作为loss。 如果Trainer初始化没有提供loss则使用这个loss TODO 补充一个例子 + 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。fastNLP中提供了 LossInForward_ 这 + 个loss。这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor, + 并使用它作为loss。 如果Trainer初始化没有提供loss则默认使用 LossInForward_ 。详细例子可以参照 TODO 补充一个例子 3. Metric Metric_ 使用了与上述Loss一样的策略,即使用名称进行匹配。AccuracyMetric(target='label')的情况与CrossEntropyLoss 是同理的。 在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, - 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果,传入到predict()的参数也是从DataSet中的input的选择 - 出来的; 与forward()一样,返回值需要为一个dict。具体例子可以参考 TODO 补充一个例子 + 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果,传入到predict()的参数也是从DataSet中被设置为input + 的field中选择出来的; 与forward()一样,返回值需要为一个dict。具体例子可以参考 TODO 补充一个例子 2. Trainer的代码检查 @@ -112,12 +112,12 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 from fastNLP import DataSet class Model(nn.Module): - def __init__(self): - super().__init__() - self.fc = nn.Linear(1, 1) - def forward(self, x, b): - loss = torch.mean((self.fc(x)-b)**2) - return {'loss': loss} + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, x, b): + loss = torch.mean((self.fc(x)-b)**2) + return {'loss': loss} model = Model() dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) @@ -138,16 +138,15 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 # unused field: ['a'] # Suggestion: You need to provide ['x'] in DataSet and set it as input. - 这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里由两类 + 这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里有两类 信息可以为你提供参考 1. 'input fields after batch...'这部分显示的是train dataset经过Batch操作后,每个field对应的类型以及进行shape。这里 - 因为train dataset没有target所以没有显示。根据这里你可以看出是否正确将需要的内容设置为了input或target。 + 因为train dataset没有target所以没有显示。根据这里可以看出是否正确将需要的内容设置为了input或target。 - 2. 如果出现了映射错误,出现NameError。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 - 出当前是在调取forward出错),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 - 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x' - ,或者model的参数从'x'修改为'a'都可以解决问题。 + 2. NameError,NameError发生在映射出错的情况。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 + 出当前是在调取forward),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 + 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x',或者model的参数从'x'修改为'a'都可以解决问题。 下面的例子是由于loss计算的时候找不到需要的值 @@ -249,18 +248,18 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 # (2). Check key assignment for `target` when initialize AccuracyMetric. Or provide `target` in DataSet or output of Model.predict(self, a). 报错信息和前面都是类似的,但是可以通过'AccuracyMetric.evaluate(self, pred, target, seq_len=None)'看出这里是evaluation - 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation的弄错的情况。这里的修改是通过在初始化metric的时候 - 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output'). + 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation弄错的情况。这里的修改是通过在初始化metric的时候 + 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output')。 可以通过check_code_level调节检查的强度。默认为0,即进行检查。 3. Trainer与callback 虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能,比如负采样,learning rate decay, Early Stop等。 - 为了解决这个问题fastNLP引入了callback的机制,Callback_ 是一种在Trainer训练过程中特定阶段会运行的类,所有的 Callback_ 都具有 + 为了解决这个问题fastNLP引入了callback的机制,Callback_ 是一种在Trainer训练过程中特定阶段会运行的函数集合,所有的 Callback_ 都具有 on_*(比如on_train_start, on_backward_begin)等函数。如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用。 - 我们将Train.train()这个函数内部分为以下的阶段 + 我们将Train.train()这个函数内部分为以下的阶段,在对应阶段会触发相应的调用。 Example:: @@ -305,7 +304,7 @@ import warnings try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import pseudo_tqdm as tqdm + from fastNLP.core.utils import _pseudo_tqdm as tqdm from fastNLP.core.batch import Batch from fastNLP.core.callback import CallbackManager, CallbackException @@ -316,12 +315,12 @@ from fastNLP.core.sampler import Sampler from fastNLP.core.sampler import RandomSampler from fastNLP.core.sampler import SequentialSampler from fastNLP.core.tester import Tester -from fastNLP.core.utils import CheckError +from fastNLP.core.utils import _CheckError from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_forward_error from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device -from fastNLP.core.utils import get_func_signature +from fastNLP.core.utils import _get_func_signature from fastNLP.core.utils import _get_device @@ -466,34 +465,11 @@ class Trainer(object): def train(self, load_best_model=True): """ - - 开始训练过程。主要有以下几个步骤:: - - for epoch in range(num_epochs): - # 使用Batch从DataSet中按批取出数据,并自动对DataSet中dtype为(float, int)的fields进行padding。并转换为Tensor。 - 非float,int类型的参数将不会被转换为Tensor,且不进行padding。 - for batch_x, batch_y in Batch(DataSet) - # batch_x是一个dict, 被设为input的field会出现在这个dict中, - key为DataSet中的field_name, value为该field的value - # batch_y也是一个dict,被设为target的field会出现在这个dict中, - key为DataSet中的field_name, value为该field的value - 2. 将batch_x的数据送入到model.forward函数中,并获取结果。这里我们就是通过匹配batch_x中的key与forward函数的形 - 参完成参数传递。例如, - forward(self, x, seq_lens) # fastNLP会在batch_x中找到key为"x"的value传递给x,key为"seq_lens"的 - value传递给seq_lens。若在batch_x中没有找到所有必须要传递的参数,就会报错。如果forward存在默认参数 - 而且默认参数这个key没有在batch_x中,则使用默认参数。 - 3. 将batch_y与model.forward的结果一并送入loss中计算loss。loss计算时一般都涉及到pred与target。但是在不同情况 - 中,可能pred称为output或prediction, target称为y或label。fastNLP通过初始化loss时传入的映射找到pred或 - target。比如在初始化Trainer时初始化loss为CrossEntropyLoss(pred='output', target='y'), 那么fastNLP计 - 算loss时,就会使用"output"在batch_y与forward的结果中找到pred;使用"y"在batch_y与forward的结果中找target - , 并完成loss的计算。 - 4. 获取到loss之后,进行反向求导并更新梯度 - 根据需要适时进行验证机测试 - 根据metrics进行evaluation,并根据是否提供了save_path判断是否存储模型 + 使用该函数使Trainer开始训练。 :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 最好的模型参数。 - :return results: 返回一个字典类型的数据, + :return dict: 返回一个字典类型的数据, 内含以下内容:: seconds: float, 表示训练时长 @@ -547,7 +523,7 @@ class Trainer(object): def _train(self): if not self.use_tqdm: - from fastNLP.core.utils import pseudo_tqdm as inner_tqdm + from fastNLP.core.utils import _pseudo_tqdm as inner_tqdm else: inner_tqdm = tqdm self.step = 0 @@ -559,6 +535,7 @@ class Trainer(object): avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) + self.batch_per_epoch = data_iterator.num_batches for epoch in range(1, self.n_epochs + 1): self.epoch = epoch pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) @@ -660,7 +637,7 @@ class Trainer(object): x = _build_args(network.forward, **x) y = network(**x) if not isinstance(y, dict): - raise TypeError(f"The return value of {get_func_signature(network.forward)} should be dict, got {type(y)}.") + raise TypeError(f"The return value of {_get_func_signature(network.forward)} should be dict, got {type(y)}.") return y def _grad_backward(self, loss): @@ -796,7 +773,7 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ refined_batch_x = _build_args(model.forward, **batch_x) pred_dict = model(**refined_batch_x) - func_signature = get_func_signature(model.forward) + func_signature = _get_func_signature(model.forward) if not isinstance(pred_dict, dict): raise TypeError(f"The return value of {func_signature} should be `dict`, not `{type(pred_dict)}`.") @@ -807,16 +784,16 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ if batch_count == 0: if not isinstance(loss, torch.Tensor): raise TypeError( - f"The return value of {get_func_signature(losser.get_loss)} should be `torch.Tensor`, " + f"The return value of {_get_func_signature(losser.get_loss)} should be `torch.Tensor`, " f"but got `{type(loss)}`.") if len(loss.size()) != 0: raise ValueError( - f"The size of return value of {get_func_signature(losser.get_loss)} is {loss.size()}, " + f"The size of return value of {_get_func_signature(losser.get_loss)} is {loss.size()}, " f"should be torch.size([])") loss.backward() - except CheckError as e: - # TODO: another error raised if CheckError caught - pre_func_signature = get_func_signature(model.forward) + except _CheckError as e: + # TODO: another error raised if _CheckError caught + pre_func_signature = _get_func_signature(model.forward) _check_loss_evaluate(prev_func_signature=pre_func_signature, func_signature=e.func_signature, check_res=e.check_res, pred_dict=pred_dict, target_dict=batch_y, dataset=dataset, check_level=check_level) diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index af1a7db6..f34092df 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -9,7 +9,7 @@ import numpy as np import torch from torch import nn -CheckRes = namedtuple('CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', +_CheckRes = namedtuple('_CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', 'varargs']) def _prepare_cache_filepath(filepath): @@ -28,6 +28,57 @@ def _prepare_cache_filepath(filepath): # TODO 可以保存下缓存时的参数,如果load的时候发现参数不一致,发出警告。 def cache_results(_cache_fp, _refresh=False, _verbose=1): + """ + cache_results是fastNLP中用于cache数据的装饰器。通过下面的例子看一下如何使用 + + Example:: + + import time + import numpy as np + from fastNLP import cache_results + + @cache_results('cache.pkl') + def process_data(): + # 一些比较耗时的工作,比如读取数据,预处理数据等,这里用time.sleep()代替耗时 + time.sleep(1) + return np.random.randint(5, size=(10, 20)) + + start_time = time.time() + process_data() + print(time.time() - start_time) + + start_time = time.time() + process_data() + print(time.time() - start_time) + + # 输出内容如下 + # Save cache to cache.pkl. + # 1.0015439987182617 + # Read cache from cache.pkl. + # 0.00013065338134765625 + + 可以看到第二次运行的时候,只用了0.0001s左右,是由于第二次运行将直接从cache.pkl这个文件读取数据,而不会经过再次预处理 + + Example:: + + # 还是以上面的例子为例,如果需要重新生成另一个cache,比如另一个数据集的内容,通过如下的方式调用即可 + process_data(_cache_fp='cache2.pkl') # 完全不影响之前的‘cache.pkl' + + 上面的_cache_fp是cache_results会识别的参数,它将从'cache2.pkl'这里缓存/读取数据,即这里的'cache2.pkl'覆盖默认的 + 'cache.pkl'。如果在你的函数前面加上了@cache_results()则你的函数会增加三个参数[_cache_fp, _refresh, _verbose]。 + 上面的例子即为使用_cache_fp的情况,这三个参数不会传入到你的函数中,当然你写的函数参数名也不可能包含这三个名称。 + + Example:: + + process_data(_cache_fp='cache2.pkl', _refresh=True) # 这里强制重新生成一份对预处理的cache。 + # _verbose是用于控制输出信息的,如果为0,则不输出任何内容;如果为1,则会提醒当前步骤是读取的cache还是生成了新的cache + + :param str _cache_fp: 将返回结果缓存到什么位置;或从什么位置读取缓存。如果为None,cache_results没有任何效用,除非在 + 函数调用的时候传入_cache_fp这个参数。 + :param bool _refresh: 是否重新生成cache。 + :param int _verbose: 是否打印cache的信息。 + :return: + """ def wrapper_(func): signature = inspect.signature(func) for key, _ in signature.parameters.items(): @@ -74,48 +125,48 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): return wrapper return wrapper_ -def save_pickle(obj, pickle_path, file_name): - """Save an object into a pickle file. - - :param obj: an object - :param pickle_path: str, the directory where the pickle file is to be saved - :param file_name: str, the name of the pickle file. In general, it should be ended by "pkl". - """ - if not os.path.exists(pickle_path): - os.mkdir(pickle_path) - print("make dir {} before saving pickle file".format(pickle_path)) - with open(os.path.join(pickle_path, file_name), "wb") as f: - _pickle.dump(obj, f) - print("{} saved in {}".format(file_name, pickle_path)) - - -def load_pickle(pickle_path, file_name): - """Load an object from a given pickle file. - - :param pickle_path: str, the directory where the pickle file is. - :param file_name: str, the name of the pickle file. - :return obj: an object stored in the pickle - """ - with open(os.path.join(pickle_path, file_name), "rb") as f: - obj = _pickle.load(f) - print("{} loaded from {}".format(file_name, pickle_path)) - return obj - - -def pickle_exist(pickle_path, pickle_name): - """Check if a given pickle file exists in the directory. - - :param pickle_path: the directory of target pickle file - :param pickle_name: the filename of target pickle file - :return: True if file exists else False - """ - if not os.path.exists(pickle_path): - os.makedirs(pickle_path) - file_name = os.path.join(pickle_path, pickle_name) - if os.path.exists(file_name): - return True - else: - return False +# def save_pickle(obj, pickle_path, file_name): +# """Save an object into a pickle file. +# +# :param obj: an object +# :param pickle_path: str, the directory where the pickle file is to be saved +# :param file_name: str, the name of the pickle file. In general, it should be ended by "pkl". +# """ +# if not os.path.exists(pickle_path): +# os.mkdir(pickle_path) +# print("make dir {} before saving pickle file".format(pickle_path)) +# with open(os.path.join(pickle_path, file_name), "wb") as f: +# _pickle.dump(obj, f) +# print("{} saved in {}".format(file_name, pickle_path)) +# +# +# def load_pickle(pickle_path, file_name): +# """Load an object from a given pickle file. +# +# :param pickle_path: str, the directory where the pickle file is. +# :param file_name: str, the name of the pickle file. +# :return obj: an object stored in the pickle +# """ +# with open(os.path.join(pickle_path, file_name), "rb") as f: +# obj = _pickle.load(f) +# print("{} loaded from {}".format(file_name, pickle_path)) +# return obj +# +# +# def pickle_exist(pickle_path, pickle_name): +# """Check if a given pickle file exists in the directory. +# +# :param pickle_path: the directory of target pickle file +# :param pickle_name: the filename of target pickle file +# :return: True if file exists else False +# """ +# if not os.path.exists(pickle_path): +# os.makedirs(pickle_path) +# file_name = os.path.join(pickle_path, pickle_name) +# if os.path.exists(file_name): +# return True +# else: +# return False def _get_device(device, check_exist=False): """ @@ -232,15 +283,15 @@ def _check_arg_dict_list(func, args): missing = list(require_args - input_args) unused = list(input_args - all_args) varargs = [] if not spect.varargs else [spect.varargs] - return CheckRes(missing=missing, - unused=unused, - duplicated=duplicated, - required=list(require_args), - all_needed=list(all_args), - varargs=varargs) + return _CheckRes(missing=missing, + unused=unused, + duplicated=duplicated, + required=list(require_args), + all_needed=list(all_args), + varargs=varargs) -def get_func_signature(func): +def _get_func_signature(func): """ Given a function or method, return its signature. @@ -318,13 +369,13 @@ def _move_dict_value_to_device(*args, device: torch.device, non_blocking=False): raise TypeError("Only support `dict` type right now.") -class CheckError(Exception): +class _CheckError(Exception): """ - CheckError. Used in losses.LossBase, metrics.MetricBase. + _CheckError. Used in losses.LossBase, metrics.MetricBase. """ - def __init__(self, check_res: CheckRes, func_signature: str): + def __init__(self, check_res: _CheckRes, func_signature: str): errs = [f'Problems occurred when calling `{func_signature}`'] if check_res.varargs: @@ -347,7 +398,7 @@ WARNING_CHECK_LEVEL = 1 STRICT_CHECK_LEVEL = 2 -def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_res: CheckRes, +def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_res: _CheckRes, pred_dict: dict, target_dict: dict, dataset, check_level=0): errs = [] unuseds = [] @@ -449,7 +500,7 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re def _check_forward_error(forward_func, batch_x, dataset, check_level): check_res = _check_arg_dict_list(forward_func, batch_x) - func_signature = get_func_signature(forward_func) + func_signature = _get_func_signature(forward_func) errs = [] suggestions = [] @@ -543,7 +594,7 @@ def seq_mask(seq_len, max_len): return torch.gt(seq_len, seq_range) # [batch_size, max_len] -class pseudo_tqdm: +class _pseudo_tqdm: """ 当无法引入tqdm,或者Trainer中设置use_tqdm为false的时候,用该方法打印数据 """ diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 6a1830ad..7ac6ed65 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -2,7 +2,7 @@ from functools import wraps from collections import Counter from fastNLP.core.dataset import DataSet -def check_build_vocab(func): +def _check_build_vocab(func): """A decorator to make sure the indexing is built before used. """ @@ -15,7 +15,7 @@ def check_build_vocab(func): return _wrapper -def check_build_status(func): +def _check_build_status(func): """A decorator to check whether the vocabulary updates after the last build. """ @@ -67,7 +67,7 @@ class Vocabulary(object): self.idx2word = None self.rebuild = True - @check_build_status + @_check_build_status def update(self, word_lst): """依次增加序列中词在词典中的出现频率 @@ -75,7 +75,7 @@ class Vocabulary(object): """ self.word_count.update(word_lst) - @check_build_status + @_check_build_status def add(self, word): """ 增加一个新词在词典中的出现频率 @@ -84,7 +84,7 @@ class Vocabulary(object): """ self.word_count[word] += 1 - @check_build_status + @_check_build_status def add_word(self, word): """ 增加一个新词在词典中的出现频率 @@ -93,7 +93,7 @@ class Vocabulary(object): """ self.add(word) - @check_build_status + @_check_build_status def add_word_lst(self, word_lst): """ 依次增加序列中词在词典中的出现频率 @@ -132,11 +132,11 @@ class Vocabulary(object): """ self.idx2word = {i: w for w, i in self.word2idx.items()} - @check_build_vocab + @_check_build_vocab def __len__(self): return len(self.word2idx) - @check_build_vocab + @_check_build_vocab def __contains__(self, item): """ 检查词是否被记录 @@ -161,7 +161,7 @@ class Vocabulary(object): """ return self.__contains__(w) - @check_build_vocab + @_check_build_vocab def __getitem__(self, w): """ To support usage like:: @@ -175,7 +175,7 @@ class Vocabulary(object): else: raise ValueError("word {} not in vocabulary".format(w)) - @check_build_vocab + @_check_build_vocab def index_dataset(self, *datasets, field_name, new_field_name=None): """ 将DataSet中对应field的词转为数字. @@ -275,7 +275,7 @@ class Vocabulary(object): return self.__getitem__(w) @property - @check_build_vocab + @_check_build_vocab def unknown_idx(self): """ unknown 对应的数字. @@ -285,7 +285,7 @@ class Vocabulary(object): return self.word2idx[self.unknown] @property - @check_build_vocab + @_check_build_vocab def padding_idx(self): """ padding 对应的数字 @@ -294,7 +294,7 @@ class Vocabulary(object): return None return self.word2idx[self.padding] - @check_build_vocab + @_check_build_vocab def to_word(self, idx): """ 给定一个数字, 将其转为对应的词. diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index 26b7cd49..d8110db0 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -13,12 +13,12 @@ from torch import nn try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import pseudo_tqdm as tqdm + from fastNLP.core.utils import _pseudo_tqdm as tqdm from fastNLP.core.batch import Batch from fastNLP.core.callback import CallbackManager, CallbackException from fastNLP.core.dataset import DataSet -from fastNLP.core.utils import CheckError +from fastNLP.core.utils import _CheckError from fastNLP.core.utils import _move_dict_value_to_device import fastNLP import fastNLP.models.enas_utils as utils @@ -118,7 +118,7 @@ class ENASTrainer(fastNLP.Trainer): def _train(self): if not self.use_tqdm: - from fastNLP.core.utils import pseudo_tqdm as inner_tqdm + from fastNLP.core.utils import _pseudo_tqdm as inner_tqdm else: inner_tqdm = tqdm self.step = 0 diff --git a/test/core/test_tester.py b/test/core/test_tester.py index d606c0b8..d78377aa 100644 --- a/test/core/test_tester.py +++ b/test/core/test_tester.py @@ -8,7 +8,7 @@ import numpy as np import torch.nn.functional as F from torch import nn import time -from fastNLP.core.utils import CheckError +from fastNLP.core.utils import _CheckError from fastNLP.core.dataset import DataSet from fastNLP.core.instance import Instance from fastNLP.core.losses import BCELoss From ee49f4177ee1efb96c05f0c70030ac3aefaef32c Mon Sep 17 00:00:00 2001 From: xuyige Date: Thu, 25 Apr 2019 17:11:39 +0800 Subject: [PATCH 057/173] update char level encoder --- fastNLP/models/bert.py | 2 +- .../{char_embedding.py => char_encoder.py} | 50 +++++++++---------- ...char_embedding.py => test_char_encoder.py} | 6 +-- 3 files changed, 29 insertions(+), 29 deletions(-) rename fastNLP/modules/encoder/{char_embedding.py => char_encoder.py} (53%) rename test/modules/{test_char_embedding.py => test_char_encoder.py} (81%) diff --git a/fastNLP/models/bert.py b/fastNLP/models/bert.py index 98d81025..42626934 100644 --- a/fastNLP/models/bert.py +++ b/fastNLP/models/bert.py @@ -280,7 +280,7 @@ class BertForQuestionAnswering(BaseModel): start_loss = loss_fct(start_logits, start_positions) end_loss = loss_fct(end_logits, end_positions) total_loss = (start_loss + end_loss) / 2 - return {"loss": total_loss} + return {"pred1": start_logits, "pred2": end_logits, "loss": total_loss} else: return {"pred1": start_logits, "pred2": end_logits} diff --git a/fastNLP/modules/encoder/char_embedding.py b/fastNLP/modules/encoder/char_encoder.py similarity index 53% rename from fastNLP/modules/encoder/char_embedding.py rename to fastNLP/modules/encoder/char_encoder.py index 057d080c..39e4b43e 100644 --- a/fastNLP/modules/encoder/char_embedding.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -5,19 +5,18 @@ from fastNLP.modules.utils import initial_parameter # from torch.nn.init import xavier_uniform -class ConvCharEmbedding(nn.Module): - """Character-level Embedding with CNN. - - :param int char_emb_size: the size of character level embedding. Default: 50 - say 26 characters, each embedded to 50 dim vector, then the input_size is 50. - :param tuple feature_maps: tuple of int. The length of the tuple is the number of convolution operations - over characters. The i-th integer is the number of filters (dim of out channels) for the i-th - convolution. - :param tuple kernels: tuple of int. The width of each kernel. - """ +class ConvolutionCharEncoder(nn.Module): + """char级别的卷积编码器.""" def __init__(self, char_emb_size=50, feature_maps=(40, 30, 30), kernels=(3, 4, 5), initial_method=None): - super(ConvCharEmbedding, self).__init__() + """ + :param int char_emb_size: char级别embedding的维度. Default: 50 + 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. + :param tuple feature_maps: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的filter. + :param tuple kernels: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的卷积核. + :param initial_method: 初始化参数的方式, 默认为`xavier normal` + """ + super(ConvolutionCharEncoder, self).__init__() self.convs = nn.ModuleList([ nn.Conv2d(1, feature_maps[i], kernel_size=(char_emb_size, kernels[i]), bias=True, padding=(0, 4)) for i in range(len(kernels))]) @@ -26,16 +25,16 @@ class ConvCharEmbedding(nn.Module): def forward(self, x): """ - :param x: ``[batch_size * sent_length, word_length, char_emb_size]`` - :return: feature map of shape [batch_size * sent_length, sum(feature_maps), 1] + :param torch.Tensor x: ``[batch_size * sent_length, word_length, char_emb_size]`` 输入字符的embedding + :return: torch.Tensor : 卷积计算的结果, 维度为[batch_size * sent_length, sum(feature_maps), 1] """ x = x.contiguous().view(x.size(0), 1, x.size(1), x.size(2)) # [batch_size*sent_length, channel, width, height] x = x.transpose(2, 3) # [batch_size*sent_length, channel, height, width] - return self.convolute(x).unsqueeze(2) + return self._convolute(x).unsqueeze(2) - def convolute(self, x): + def _convolute(self, x): feats = [] for conv in self.convs: y = conv(x) @@ -49,15 +48,16 @@ class ConvCharEmbedding(nn.Module): return torch.cat(feats, 1) # [batch_size*sent_length, sum(feature_maps)] -class LSTMCharEmbedding(nn.Module): - """Character-level Embedding with LSTM. - - :param int char_emb_size: the size of character level embedding. Default: 50 - say 26 characters, each embedded to 50 dim vector, then the input_size is 50. - :param int hidden_size: the number of hidden units. Default: equal to char_emb_size. - """ +class LSTMCharEncoder(nn.Module): + """char级别基于LSTM的encoder.""" def __init__(self, char_emb_size=50, hidden_size=None, initial_method=None): - super(LSTMCharEmbedding, self).__init__() + """ + :param int char_emb_size: char级别embedding的维度. Default: 50 + 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. + :param int hidden_size: LSTM隐层的大小, 默认为char的embedding维度 + :param initial_method: 初始化参数的方式, 默认为`xavier normal` + """ + super(LSTMCharEncoder, self).__init__() self.hidden_size = char_emb_size if hidden_size is None else hidden_size self.lstm = nn.LSTM(input_size=char_emb_size, @@ -69,8 +69,8 @@ class LSTMCharEmbedding(nn.Module): def forward(self, x): """ - :param x: ``[ n_batch*n_word, word_length, char_emb_size]`` - :return: [ n_batch*n_word, char_emb_size] + :param torch.Tensor x: ``[ n_batch*n_word, word_length, char_emb_size]`` 输入字符的embedding + :return: torch.Tensor : [ n_batch*n_word, char_emb_size]经过LSTM编码的结果 """ batch_size = x.shape[0] h0 = torch.empty(1, batch_size, self.hidden_size) diff --git a/test/modules/test_char_embedding.py b/test/modules/test_char_encoder.py similarity index 81% rename from test/modules/test_char_embedding.py rename to test/modules/test_char_encoder.py index 07def64a..cf3ec15e 100644 --- a/test/modules/test_char_embedding.py +++ b/test/modules/test_char_encoder.py @@ -2,7 +2,7 @@ import unittest import torch -from fastNLP.modules.encoder.char_embedding import ConvCharEmbedding, LSTMCharEmbedding +from fastNLP.modules.encoder.char_encoder import ConvolutionCharEncoder, LSTMCharEncoder class TestCharEmbed(unittest.TestCase): @@ -13,14 +13,14 @@ class TestCharEmbed(unittest.TestCase): x = torch.Tensor(batch_size, char_emb, word_length) x = x.transpose(1, 2) - cce = ConvCharEmbedding(char_emb) + cce = ConvolutionCharEncoder(char_emb) y = cce(x) self.assertEqual(tuple(x.shape), (batch_size, word_length, char_emb)) print("CNN Char Emb input: ", x.shape) self.assertEqual(tuple(y.shape), (batch_size, char_emb, 1)) print("CNN Char Emb output: ", y.shape) # [128, 100] - lce = LSTMCharEmbedding(char_emb) + lce = LSTMCharEncoder(char_emb) o = lce(x) self.assertEqual(tuple(x.shape), (batch_size, word_length, char_emb)) print("LSTM Char Emb input: ", x.shape) From e76dca9ad7cb5ab4730ae1633997470a3ab87183 Mon Sep 17 00:00:00 2001 From: xuyige Date: Thu, 25 Apr 2019 20:07:52 +0800 Subject: [PATCH 058/173] update documents on embedding.py --- fastNLP/modules/encoder/embedding.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index 7bcffb8e..c93fa1a3 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -2,20 +2,25 @@ import torch.nn as nn class Embedding(nn.Module): - """A simple lookup table. + """Embedding组件.""" - :param int nums: the size of the lookup table - :param int dims: the size of each vector - :param int padding_idx: pads the tensor with zeros whenever it encounters this index - :param bool sparse: If True, gradient matrix will be a sparse tensor. In this case, only optim.SGD(cuda and cpu) and optim.Adagrad(cpu) can be used - """ - def __init__(self, nums, dims, padding_idx=0, sparse=False, init_emb=None, dropout=0.0): + def __init__(self, vocab_size, embed_dim, padding_idx=0, sparse=False, init_emb=None, dropout=0.0): + """ + :param int vocab_size: 词表大小. + :param int embed_dim: embedding维度. + :param int padding_idx: 如果碰到padding_idx则自动补0. + :param bool sparse: 如果为`True`则权重矩阵是一个sparse的矩阵. + :param torch.Tensor init_emb: 初始的embedding矩阵. + :param float dropout: dropout概率. + """ super(Embedding, self).__init__() - self.embed = nn.Embedding(nums, dims, padding_idx, sparse=sparse) - if init_emb is not None: - self.embed.weight = nn.Parameter(init_emb) + self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx, sparse=sparse, _weight=init_emb) self.dropout = nn.Dropout(dropout) def forward(self, x): + """ + :param torch.LongTensor x: [batch, seq_len] + :return: torch.Tensor : [batch, seq_len, embed_dim] + """ x = self.embed(x) return self.dropout(x) From 2c9484c274c73bcfb6f0324e18957f54750b7294 Mon Sep 17 00:00:00 2001 From: yunfan Date: Wed, 24 Apr 2019 11:04:32 +0800 Subject: [PATCH 059/173] - update doc --- fastNLP/io/__init__.py | 36 +++++++++++++++++++++ fastNLP/io/config_io.py | 5 +++ fastNLP/io/dataset_loader.py | 18 ++++++++++- fastNLP/io/embed_loader.py | 5 +++ fastNLP/io/model_io.py | 5 +++ fastNLP/models/biaffine_parser.py | 2 ++ fastNLP/models/star_transformer.py | 2 ++ fastNLP/modules/encoder/lstm.py | 7 ++-- fastNLP/modules/encoder/star_transformer.py | 2 ++ fastNLP/modules/encoder/variational_rnn.py | 25 +++++++++----- requirements.txt | 6 ++-- 11 files changed, 99 insertions(+), 14 deletions(-) diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index a3b18aa5..e8ccca30 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -1 +1,37 @@ +""" +用于IO的模块, 具体包括: + +1. 用于读入 embedding 的 :ref:`EmbedLoader ` 类, + +2. 用于读入数据的 :ref:`DataSetLoader ` 类 + +3. 用于读写config文件的类, 参考 :ref:`Config-io ` + +4. 用于保存和载入模型的类, 参考 :ref:`Model-io ` + +这些类的使用方法可以在对应module的文档下查看. +""" from .embed_loader import EmbedLoader +from .dataset_loader import * +from .config_io import * +from .model_io import * + +__all__ = [ + 'EmbedLoader', + + 'DataSetLoader', + 'CSVLoader', + 'JsonLoader', + 'ConllLoader', + 'SNLILoader', + 'SSTLoader', + 'PeopleDailyCorpusLoader', + 'Conll2003Loader', + + 'ConfigLoader', + 'ConfigSection', + 'ConfigSaver', + + 'ModelLoader', + 'ModelSaver', +] \ No newline at end of file diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index c0ffe53e..f303f0e9 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -1,3 +1,8 @@ +""" +.. _config-io: + +用于读入和处理和保存 config 文件 +""" import configparser import json import os diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 039c4242..bb5e2f64 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,3 +1,18 @@ +""" +.. _dataset-loader: + +DataSetLoader 的 API, 用于读取不同格式的数据, 并返回 `DataSet` , +得到的 `DataSet` 对象可以直接传入 `Trainer`, `Tester`, 用于模型的训练和测试 + +Example:: + + loader = SNLILoader() + train_ds = loader.load('path/to/train') + dev_ds = loader.load('path/to/dev') + test_ds = loader.load('path/to/test') + + # ... do stuff +""" import os import json from nltk.tree import Tree @@ -55,8 +70,9 @@ def _uncompress(src, dst): class DataSetLoader: - """所有`DataSetLoader`的接口 + """ + 所有`DataSetLoader`的接口 """ def load(self, path): diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index e1f20b94..7f055b3f 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,3 +1,8 @@ +""" +.. _embed-loader: + +用于读取预训练的embedding, 读取结果可直接载入为模型参数 +""" import os import numpy as np diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index 53bdc7ce..d28034c8 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -1,3 +1,8 @@ +""" +.. _model-io: + +用于载入和保存模型 +""" import torch from fastNLP.io.base_loader import BaseLoader diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 9a070c92..59d95558 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -1,3 +1,5 @@ +"""Biaffine Dependency Parser 的 Pytorch 实现. +""" from collections import defaultdict import numpy as np diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index 4f4ed551..f68aca42 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,3 +1,5 @@ +"""Star-Transformer 的 一个 Pytorch 实现. +""" from fastNLP.modules.encoder.star_transformer import StarTransformer from fastNLP.core.utils import seq_lens_to_masks diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index 9ab8e273..cff39c84 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -1,3 +1,6 @@ +"""轻量封装的 Pytorch LSTM 模块. +可在 forward 时传入序列的长度, 自动对padding做合适的处理. +""" import torch import torch.nn as nn import torch.nn.utils.rnn as rnn @@ -35,8 +38,8 @@ class LSTM(nn.Module): :param h0: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :param c0: [batch, hidden_size] 初始Cell状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :return (output, ht) 或 output: 若 ``get_hidden=True`` [batch, seq_len, hidden_size*num_direction] 输出序列 - :和 [batch, hidden_size*num_direction] 最后时刻隐状态. - :若 ``get_hidden=False`` 仅返回输出序列. + 和 [batch, hidden_size*num_direction] 最后时刻隐状态. + 若 ``get_hidden=False`` 仅返回输出序列. """ if h0 is not None and c0 is not None: hx = (h0, c0) diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 034cfa96..42662804 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -1,3 +1,5 @@ +"""Star-Transformer 的encoder部分的 Pytorch 实现 +""" import torch from torch import nn from torch.nn import functional as F diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index d63aa6e7..89ab44d9 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -1,3 +1,5 @@ +"""Variational RNN 的 Pytorch 实现 +""" import torch import torch.nn as nn from torch.nn.utils.rnn import PackedSequence, pack_padded_sequence, pad_packed_sequence @@ -28,11 +30,11 @@ class VarRnnCellWrapper(nn.Module): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] :param hidden: for LSTM, tuple of (h_0, c_0), [batch_size, hidden_size] - :for other RNN, h_0, [batch_size, hidden_size] + for other RNN, h_0, [batch_size, hidden_size] :param mask_x: [batch_size, input_size] dropout mask for input :param mask_h: [batch_size, hidden_size] dropout mask for hidden :return PackedSequence output: [seq_len, bacth_size, hidden_size] - :hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] + hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ def get_hi(hi, h0, size): @@ -95,7 +97,7 @@ class VarRNNBase(nn.Module): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` @@ -138,7 +140,7 @@ class VarRNNBase(nn.Module): :param x: [batch, seq_len, input_size] 输入序列 :param hx: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :return (output, ht): [batch, seq_len, hidden_size*num_direction] 输出序列 - :和 [batch, hidden_size*num_direction] 最后时刻隐状态 + 和 [batch, hidden_size*num_direction] 最后时刻隐状态 """ is_lstm = self.is_lstm is_packed = isinstance(x, PackedSequence) @@ -193,7 +195,6 @@ class VarRNNBase(nn.Module): return output, hidden - class VarLSTM(VarRNNBase): """Variational Dropout LSTM. @@ -202,7 +203,7 @@ class VarLSTM(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` @@ -211,6 +212,9 @@ class VarLSTM(VarRNNBase): def __init__(self, *args, **kwargs): super(VarLSTM, self).__init__(mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarLSTM, self).forward(x, hx) + class VarRNN(VarRNNBase): """Variational Dropout RNN. @@ -220,7 +224,7 @@ class VarRNN(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` @@ -229,6 +233,8 @@ class VarRNN(VarRNNBase): def __init__(self, *args, **kwargs): super(VarRNN, self).__init__(mode="RNN", Cell=nn.RNNCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarRNN, self).forward(x, hx) class VarGRU(VarRNNBase): """Variational Dropout GRU. @@ -238,7 +244,7 @@ class VarGRU(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` @@ -247,6 +253,9 @@ class VarGRU(VarRNNBase): def __init__(self, *args, **kwargs): super(VarGRU, self).__init__(mode="GRU", Cell=nn.GRUCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarGRU, self).forward(x, hx) + # if __name__ == '__main__': # x = torch.Tensor([[1,2,3], [4,5,0], [6,0,0]])[:,:,None] * 0.1 # mask = (x != 0).float().view(3, -1) diff --git a/requirements.txt b/requirements.txt index 931ca285..d763ea1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -numpy>=1.14.2 +numpy torch>=0.4.0 tensorboardX -tqdm>=4.28.1 -nltk>=3.4.1 \ No newline at end of file +tqdm +nltk \ No newline at end of file From 06891cf90af7ca9f16261163f70a2a1b80cb7e9d Mon Sep 17 00:00:00 2001 From: yh_cc Date: Fri, 26 Apr 2019 11:53:38 +0800 Subject: [PATCH 060/173] =?UTF-8?q?=E8=A1=A5=E5=85=85=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 2 + fastNLP/core/callback.py | 15 +- fastNLP/core/optimizer.py | 3 + fastNLP/core/tester.py | 60 ++- fastNLP/core/trainer.py | 9 +- fastNLP/io/embed_loader.py | 50 ++- fastNLP/models/cnn_text_classification.py | 47 ++- fastNLP/models/sequence_modeling.py | 258 ++++++------- fastNLP/modules/decoder/CRF.py | 66 ++-- fastNLP/modules/decoder/utils.py | 13 +- fastNLP/modules/encoder/__init__.py | 2 - fastNLP/modules/encoder/conv.py | 58 --- fastNLP/modules/encoder/conv_maxpool.py | 55 ++- fastNLP/modules/encoder/masked_rnn.py | 424 ---------------------- test/modules/test_masked_rnn.py | 27 -- test/test_tutorials.py | 6 +- 16 files changed, 340 insertions(+), 755 deletions(-) delete mode 100644 fastNLP/modules/encoder/conv.py delete mode 100644 fastNLP/modules/encoder/masked_rnn.py delete mode 100644 test/modules/test_masked_rnn.py diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 0f6da45f..35309bd3 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -1,3 +1,5 @@ from .core import * from . import models from . import modules + +__version__ = '0.4.0' \ No newline at end of file diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index a416f655..914e4d28 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -1,4 +1,5 @@ """ +Callback的说明文档 .. _Callback: @@ -28,7 +29,6 @@ class Callback(object): def trainer(self): """ 该属性可以通过self.trainer获取到,一般情况下不需要使用这个属性。 - :return: """ return self._trainer @@ -323,11 +323,16 @@ class GradientClipCallback(Callback): class CallbackException(BaseException): def __init__(self, msg): + """ + 当需要通过callback跳出训练的时候可以通过抛出CallbackException并在on_exception中捕获这个值。 + :param str msg: Exception的信息。 + """ super(CallbackException, self).__init__(msg) class EarlyStopError(CallbackException): def __init__(self, msg): + """用于EarlyStop时从Trainer训练循环中跳出。""" super(EarlyStopError, self).__init__(msg) @@ -360,7 +365,13 @@ class EarlyStopCallback(Callback): class LRScheduler(Callback): def __init__(self, lr_scheduler): - """对PyTorch LR Scheduler的包装 + """对PyTorch LR Scheduler的包装以使得其可以被Trainer所使用 + + Example:: + + from fastNLP import LRScheduler + + :param torch.optim.lr_scheduler._LRScheduler lr_scheduler: PyTorch的lr_scheduler """ diff --git a/fastNLP/core/optimizer.py b/fastNLP/core/optimizer.py index da2c45fe..584aa5ff 100644 --- a/fastNLP/core/optimizer.py +++ b/fastNLP/core/optimizer.py @@ -13,6 +13,9 @@ class Optimizer(object): self.model_params = model_params self.settings = kwargs + def construct_from_pytorch(self, model_params): + raise NotImplementedError + def _get_require_grads_param(self, params): """ 将params中不需要gradient的删除 diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 9737f53a..6e3f98b5 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -14,20 +14,56 @@ from fastNLP.core.utils import _get_device class Tester(object): - """An collection of model inference and evaluation of performance, used over validation/dev set and test set. + """ + Tester是在提供数据,模型以及metric的情况下进行性能测试的类 + + Example:: + + import numpy as np + import torch + from torch import nn + from fastNLP import Tester + from fastNLP import DataSet + from fastNLP import AccuracyMetric + + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a): + return {'pred': self.fc(a.unsqueeze(1)).squeeze(1)} + + model = Model() + + dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) + + dataset.set_input('a') + dataset.set_target('b') + + tester = Tester(dataset, model, metrics=AccuracyMetric()) + eval_results = tester.test() + + 这里Metric的映射规律是和 Trainer_ 中一致的,请参考 Trainer_ 使用metrics。 + - :param DataSet data: a validation/development set - :param torch.nn.modules.module model: a PyTorch model - :param MetricBase metrics: a metric object or a list of metrics (List[MetricBase]) - :param int batch_size: batch size for validation - :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 - 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, - 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 - :param int verbose: the number of steps after which an information is printed. """ def __init__(self, data, model, metrics, batch_size=16, device=None, verbose=1): + """传入模型,数据以及metric进行验证。 + + :param DataSet data: 需要测试的数据集 + :param torch.nn.module model: 使用的模型 + :param MetricBase metrics: 一个Metric或者一个列表的metric对象 + :param int batch_size: evaluation时使用的batch_size有多大。 + :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 + 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 + :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 + + """ + super(Tester, self).__init__() if not isinstance(data, DataSet): @@ -59,10 +95,10 @@ class Tester(object): self._predict_func = self._model.forward def test(self): - """Start test or validation. - - :return eval_results: a dictionary whose keys are the class name of metrics to use, values are the evaluation results of these metrics. + """开始进行验证,并返回验证结果。 + :return dict(dict) eval_results: dict为二层嵌套结构,dict的第一层是metric的名称; 第二层是这个metric的指标。 + 一个AccuracyMetric的例子为{'AccuracyMetric': {'acc': 1.0}}。 """ # turn on the testing mode; clean up the history network = self._model diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 44d88d3c..48733652 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -213,7 +213,7 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 from torch.optim import SGD from fastNLP import Trainer from fastNLP import DataSet - from fastNLP.core.metrics import AccuracyMetric + from fastNLP import AccuracyMetric import torch class Model(nn.Module): @@ -322,7 +322,7 @@ from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device from fastNLP.core.utils import _get_func_signature from fastNLP.core.utils import _get_device - +from fastNLP.core.optimizer import Optimizer class Trainer(object): def __init__(self, train_data, model, optimizer, loss=None, @@ -336,8 +336,7 @@ class Trainer(object): """ :param DataSet train_data: 训练集 :param nn.modules model: 待训练的模型 - :param Optimizer,None optimizer: 优化器,pytorch的torch.optim.Optimizer类型。如果为None,则Trainer不会更新模型, - 请确保已在callback中进行了更新。 + :param torch.optim.Optimizer,None optimizer: 优化器。如果为None,则Trainer不会更新模型,请确保已在callback中进行了更新。 :param int batch_size: 训练和验证的时候的batch大小。 :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。当loss为None时,默认使用 LossInForward_ 。 :param Sampler sampler: Batch数据生成的顺序。详见 Sampler_ 。如果为None,默认使用 RandomSampler_ 。 @@ -438,6 +437,8 @@ class Trainer(object): if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer + elif isinstance(optimizer, Optimizer): + self.optimizer = optimizer.construct_from_pytorch(model.parameters()) elif optimizer is None: warnings.warn("The optimizer is set to None, Trainer will update your model. Make sure you update the model" " in the callback.") diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index e1f20b94..31e590da 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -8,7 +8,7 @@ from fastNLP.io.base_loader import BaseLoader import warnings class EmbedLoader(BaseLoader): - """docstring for EmbedLoader""" + """这个类用于从预训练的Embedding中load数据。""" def __init__(self): super(EmbedLoader, self).__init__() @@ -16,18 +16,17 @@ class EmbedLoader(BaseLoader): @staticmethod def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True, error='ignore'): """ - load pretraining embedding in {embed_file} based on words in vocab. Words in vocab but not in the pretraining - embedding are initialized from a normal distribution which has the mean and std of the found words vectors. - The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). - - :param embed_filepath: str, where to read pretrain embedding - :param vocab: Vocabulary. - :param dtype: the dtype of the embedding matrix - :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. - :param error: str, 'ignore', 'strict'; if 'ignore' errors will not raise. if strict, any bad format error will - raise - :return: np.ndarray() will have the same [len(vocab), dimension], dimension is determined by the pretrain - embedding + 从embed_filepath这个预训练的词向量中抽取出vocab这个词表的词的embedding。EmbedLoader将自动判断embed_filepath是 + word2vec(第一行只有两个元素)还是glove格式的数据。 + + :param str embed_filepath: 预训练的embedding的路径。 + :param Vocabulary vocab: 词表,读取出现在vocab中的词的embedding。没有出现在vocab中的词的embedding将通过找到的词的 + embedding的正态分布采样出来,以使得整个Embedding是同分布的。 + :param dtype: 读出的embedding的类型 + :param bool normalize: 是否将每个vector归一化到norm为1 + :param str error: 'ignore', 'strict'; 如果'ignore',错误将自动跳过; 如果strict, 错误将抛出。这里主要可能出错的地 + 方在于词表有空行或者词表出现了维度不一致。 + :return: numpy.ndarray, shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 """ assert isinstance(vocab, Vocabulary), "Only fastNLP.Vocabulary is supported." if not os.path.exists(embed_filepath): @@ -76,19 +75,18 @@ class EmbedLoader(BaseLoader): def load_without_vocab(embed_filepath, dtype=np.float32, padding='', unknown='', normalize=True, error='ignore'): """ - load pretraining embedding in {embed_file}. And construct a Vocabulary based on the pretraining embedding. - The embedding type is determined automatically, support glove and word2vec(the first line only has two elements). - - :param embed_filepath: str, where to read pretrain embedding - :param dtype: the dtype of the embedding matrix - :param padding: the padding tag for vocabulary. - :param unknown: the unknown tag for vocabulary. - :param normalize: bool, whether to normalize each word vector so that every vector has norm 1. - :param error: str, 'ignore', 'strict'; if 'ignore' errors will not raise. if strict, any bad format error will - :raise - :return: np.ndarray() is determined by the pretraining embeddings - Vocabulary: contain all pretraining words and two special tag[, ] - + 从embed_filepath中读取预训练的word vector。根据预训练的词表读取embedding并生成一个对应的Vocabulary。 + + :param str embed_filepath: 预训练的embedding的路径。 + :param dtype: 读出的embedding的类型 + :param str padding: the padding tag for vocabulary. + :param str unknown: the unknown tag for vocabulary. + :param bool normalize: 是否将每个vector归一化到norm为1 + :param str error: 'ignore', 'strict'; 如果'ignore',错误将自动跳过; 如果strict, 错误将抛出。这里主要可能出错的地 + 方在于词表有空行或者词表出现了维度不一致。 + :return: numpy.ndarray, shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 + :return: numpy.ndarray,Vocabulary embedding的shape是[词表大小+x, 词表维度], "词表大小+x"是由于最终的大小还取决与 + 是否使用padding, 以及unknown有没有在词表中找到对应的词。Vocabulary中的词的顺序与Embedding的顺序是一一对应的。 """ vocab = Vocabulary(padding=padding, unknown=unknown) vec_dict = {} diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index f3898c00..37551e14 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -3,29 +3,38 @@ import torch import torch.nn as nn +import numpy as np -# import torch.nn.functional as F import fastNLP.modules.encoder as encoder class CNNText(torch.nn.Module): """ - Text classification model by character CNN, the implementation of paper - 'Yoon Kim. 2014. Convolution Neural Networks for Sentence - Classification.' + 使用CNN进行文本分类的模型 + 'Yoon Kim. 2014. Convolution Neural Networks for Sentence Classification.' """ - def __init__(self, embed_num, + def __init__(self, vocab_size, embed_dim, num_classes, kernel_nums=(3, 4, 5), kernel_sizes=(3, 4, 5), padding=0, dropout=0.5): + """ + + :param int vocab_size: 词表的大小 + :param int embed_dim: 词embedding的维度大小 + :param int num_classes: 一共有多少类 + :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 + :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 + :param int padding: + :param float dropout: Dropout的大小 + """ super(CNNText, self).__init__() # no support for pre-trained embedding currently - self.embed = encoder.Embedding(embed_num, embed_dim) + self.embed = encoder.Embedding(vocab_size, embed_dim) self.conv_pool = encoder.ConvMaxpool( in_channels=embed_dim, out_channels=kernel_nums, @@ -34,24 +43,36 @@ class CNNText(torch.nn.Module): self.dropout = nn.Dropout(dropout) self.fc = encoder.Linear(sum(kernel_nums), num_classes) - def forward(self, word_seq): + def init_embed(self, embed): + """ + 加载预训练的模型 + :param numpy.ndarray embed: vocab_size x embed_dim的embedding + :return: + """ + assert isinstance(embed, np.ndarray) + assert embed.shape == self.embed.embed.weight.shape + self.embed.embed.weight.data = torch.from_numpy(embed) + + def forward(self, words, seq_len=None): """ - :param word_seq: torch.LongTensor, [batch_size, seq_len] + :param torch.LongTensor words: [batch_size, seq_len],句子中word的index + :param torch.LongTensor seq_len: [batch,] 每个句子的长度 :return output: dict of torch.LongTensor, [batch_size, num_classes] """ - x = self.embed(word_seq) # [N,L] -> [N,L,C] + x = self.embed(words) # [N,L] -> [N,L,C] x = self.conv_pool(x) # [N,L,C] -> [N,C] x = self.dropout(x) x = self.fc(x) # [N,C] -> [N, N_class] return {'pred': x} - def predict(self, word_seq): + def predict(self, words, seq_len=None): """ + :param torch.LongTensor words: [batch_size, seq_len],句子中word的index + :param torch.LongTensor seq_len: [batch,] 每个句子的长度 - :param word_seq: torch.LongTensor, [batch_size, seq_len] - :return predict: dict of torch.LongTensor, [batch_size, seq_len] + :return predict: dict of torch.LongTensor, [batch_size, ] """ - output = self(word_seq) + output = self(words, seq_len) _, predict = output['pred'].max(dim=1) return {'pred': predict} diff --git a/fastNLP/models/sequence_modeling.py b/fastNLP/models/sequence_modeling.py index cb615daf..bd04a803 100644 --- a/fastNLP/models/sequence_modeling.py +++ b/fastNLP/models/sequence_modeling.py @@ -8,47 +8,64 @@ from fastNLP.modules.utils import seq_mask class SeqLabeling(BaseModel): """ - PyTorch Network for sequence labeling + 一个基础的Sequence labeling的模型 """ - def __init__(self, args): + def __init__(self, vocab_size, embed_dim, hidden_size, num_classes): + """ + 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 + + :param int vocab_size: 词表大小。 + :param int embed_dim: embedding的维度 + :param int hidden_size: LSTM隐藏层的大小 + :param int num_classes: 一共有多少类 + """ super(SeqLabeling, self).__init__() - vocab_size = args["vocab_size"] - word_emb_dim = args["word_emb_dim"] - hidden_dim = args["rnn_hidden_units"] - num_classes = args["num_classes"] - - self.Embedding = encoder.embedding.Embedding(vocab_size, word_emb_dim) - self.Rnn = encoder.lstm.LSTM(word_emb_dim, hidden_dim) - self.Linear = encoder.linear.Linear(hidden_dim, num_classes) + + self.Embedding = encoder.embedding.Embedding(vocab_size, embed_dim) + self.Rnn = encoder.lstm.LSTM(embed_dim, hidden_size) + self.Linear = encoder.linear.Linear(hidden_size, num_classes) self.Crf = decoder.CRF.ConditionalRandomField(num_classes) self.mask = None - def forward(self, word_seq, word_seq_origin_len, truth=None): + def forward(self, words, seq_len, target): """ - :param word_seq: LongTensor, [batch_size, mex_len] - :param word_seq_origin_len: LongTensor, [batch_size,], the origin lengths of the sequences. - :param truth: LongTensor, [batch_size, max_len] + :param torch.LongTensor words: [batch_size, max_len],序列的index + :param torch.LongTensor seq_len: [batch_size,], 这个序列的长度 + :param torch.LongTensor target: [batch_size, max_len], 序列的目标值 :return y: If truth is None, return list of [decode path(list)]. Used in testing and predicting. If truth is not None, return loss, a scalar. Used in training. """ - assert word_seq.shape[0] == word_seq_origin_len.shape[0] - if truth is not None: - assert truth.shape == word_seq.shape - self.mask = self.make_mask(word_seq, word_seq_origin_len) + assert words.shape[0] == seq_len.shape[0] + assert target.shape == words.shape + self.mask = self._make_mask(words, seq_len) - x = self.Embedding(word_seq) + x = self.Embedding(words) # [batch_size, max_len, word_emb_dim] x = self.Rnn(x) # [batch_size, max_len, hidden_size * direction] x = self.Linear(x) # [batch_size, max_len, num_classes] - return {"loss": self._internal_loss(x, truth) if truth is not None else None, - "predict": self.decode(x)} + return {"loss": self._internal_loss(x, target)} - def loss(self, x, y): - """ Since the loss has been computed in forward(), this function simply returns x.""" - return x + def predict(self, words, seq_len): + """ + 用于在预测时使用 + + :param torch.LongTensor words: [batch_size, max_len] + :param torch.LongTensor seq_len: [batch_size,] + :return: + """ + self.mask = self._make_mask(words, seq_len) + + x = self.Embedding(words) + # [batch_size, max_len, word_emb_dim] + x = self.Rnn(x) + # [batch_size, max_len, hidden_size * direction] + x = self.Linear(x) + # [batch_size, max_len, num_classes] + pred = self._decode(x) + return {'pred': pred} def _internal_loss(self, x, y): """ @@ -65,89 +82,114 @@ class SeqLabeling(BaseModel): total_loss = self.Crf(x, y, self.mask) return torch.mean(total_loss) - def make_mask(self, x, seq_len): + def _make_mask(self, x, seq_len): batch_size, max_len = x.size(0), x.size(1) mask = seq_mask(seq_len, max_len) mask = mask.view(batch_size, max_len) mask = mask.to(x).float() return mask - def decode(self, x, pad=True): + def _decode(self, x): """ - :param x: FloatTensor, [batch_size, max_len, tag_size] - :param pad: pad the output sequence to equal lengths + :param torch.FloatTensor x: [batch_size, max_len, tag_size] :return prediction: list of [decode path(list)] """ - max_len = x.shape[1] - tag_seq, _ = self.Crf.viterbi_decode(x, self.mask) - # pad prediction to equal length - if pad is True: - for pred in tag_seq: - if len(pred) < max_len: - pred += [0] * (max_len - len(pred)) + tag_seq, _ = self.Crf.viterbi_decode(x, self.mask, unpad=True) return tag_seq -class AdvSeqLabel(SeqLabeling): +class AdvSeqLabel: """ - Advanced Sequence Labeling Model + 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 """ - def __init__(self, args, emb=None, id2words=None): - super(AdvSeqLabel, self).__init__(args) - - vocab_size = args["vocab_size"] - word_emb_dim = args["word_emb_dim"] - hidden_dim = args["rnn_hidden_units"] - num_classes = args["num_classes"] - dropout = args['dropout'] + def __init__(self, vocab_size, embed_dim, hidden_size, num_classes, dropout=0.3, embedding=None, + id2words=None, encoding_type='bmes'): + """ - self.Embedding = encoder.embedding.Embedding(vocab_size, word_emb_dim, init_emb=emb) - self.norm1 = torch.nn.LayerNorm(word_emb_dim) - # self.Rnn = encoder.lstm.LSTM(word_emb_dim, hidden_dim, num_layers=2, dropout=dropout, bidirectional=True) - self.Rnn = torch.nn.LSTM(input_size=word_emb_dim, hidden_size=hidden_dim, num_layers=2, dropout=dropout, + :param int vocab_size: 词表的大小 + :param int embed_dim: embedding的维度 + :param int hidden_size: LSTM的隐层大小 + :param int num_classes: 有多少个类 + :param float dropout: LSTM中以及DropOut层的drop概率 + :param numpy.ndarray embedding: 预训练的embedding,需要与指定的词表大小等一致 + :param dict id2words: tag id转为其tag word的表。用于在CRF解码时防止解出非法的顺序,比如'BMES'这个标签规范中,'S' + 不能出现在'B'之后。这里也支持类似与'B-NN',即'-'前为标签类型的指示,后面为具体的tag的情况。这里不但会保证 + 'B-NN'后面不为'S-NN'还会保证'B-NN'后面不会出现'M-xx'(任何非'M-NN'和'E-NN'的情况。) + :param str encoding_type: 支持"BIO", "BMES", "BEMSO"。 + """ + self.Embedding = encoder.embedding.Embedding(vocab_size, embed_dim, init_emb=embedding) + self.norm1 = torch.nn.LayerNorm(embed_dim) + self.Rnn = torch.nn.LSTM(input_size=embed_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, bidirectional=True, batch_first=True) - self.Linear1 = encoder.Linear(hidden_dim * 2, hidden_dim * 2 // 3) - self.norm2 = torch.nn.LayerNorm(hidden_dim * 2 // 3) - # self.batch_norm = torch.nn.BatchNorm1d(hidden_dim * 2 // 3) + self.Linear1 = encoder.Linear(hidden_size * 2, hidden_size * 2 // 3) + self.norm2 = torch.nn.LayerNorm(hidden_size * 2 // 3) self.relu = torch.nn.LeakyReLU() self.drop = torch.nn.Dropout(dropout) - self.Linear2 = encoder.Linear(hidden_dim * 2 // 3, num_classes) + self.Linear2 = encoder.Linear(hidden_size * 2 // 3, num_classes) if id2words is None: self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False) else: self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False, allowed_transitions=allowed_transitions(id2words, - encoding_type="bmes")) + encoding_type=encoding_type)) + + def _decode(self, x): + """ + :param torch.FloatTensor x: [batch_size, max_len, tag_size] + :return prediction: list of [decode path(list)] + """ + tag_seq, _ = self.Crf.viterbi_decode(x, self.mask, unpad=True) + return tag_seq + + def _internal_loss(self, x, y): + """ + Negative log likelihood loss. + :param x: Tensor, [batch_size, max_len, tag_size] + :param y: Tensor, [batch_size, max_len] + :return loss: a scalar Tensor + + """ + x = x.float() + y = y.long() + assert x.shape[:2] == y.shape + assert y.shape == self.mask.shape + total_loss = self.Crf(x, y, self.mask) + return torch.mean(total_loss) + + def _make_mask(self, x, seq_len): + batch_size, max_len = x.size(0), x.size(1) + mask = seq_mask(seq_len, max_len) + mask = mask.view(batch_size, max_len) + mask = mask.to(x).float() + return mask - def forward(self, word_seq, word_seq_origin_len, truth=None): + def _forward(self, words, seq_len, target=None): """ - :param word_seq: LongTensor, [batch_size, mex_len] - :param word_seq_origin_len: LongTensor, [batch_size, ] - :param truth: LongTensor, [batch_size, max_len] + :param torch.LongTensor words: [batch_size, mex_len] + :param torch.LongTensor seq_len:[batch_size, ] + :param torch.LongTensor target: [batch_size, max_len] :return y: If truth is None, return list of [decode path(list)]. Used in testing and predicting. If truth is not None, return loss, a scalar. Used in training. """ - word_seq = word_seq.long() - word_seq_origin_len = word_seq_origin_len.long() - self.mask = self.make_mask(word_seq, word_seq_origin_len) - sent_len, idx_sort = torch.sort(word_seq_origin_len, descending=True) + words = words.long() + seq_len = seq_len.long() + self.mask = self._make_mask(words, seq_len) + sent_len, idx_sort = torch.sort(seq_len, descending=True) _, idx_unsort = torch.sort(idx_sort, descending=False) - # word_seq_origin_len = word_seq_origin_len.long() - truth = truth.long() if truth is not None else None + # seq_len = seq_len.long() + target = target.long() if target is not None else None - batch_size = word_seq.size(0) - max_len = word_seq.size(1) if next(self.parameters()).is_cuda: - word_seq = word_seq.cuda() + words = words.cuda() idx_sort = idx_sort.cuda() idx_unsort = idx_unsort.cuda() self.mask = self.mask.cuda() - x = self.Embedding(word_seq) + x = self.Embedding(words) x = self.norm1(x) # [batch_size, max_len, word_emb_dim] @@ -155,71 +197,35 @@ class AdvSeqLabel(SeqLabeling): sent_packed = torch.nn.utils.rnn.pack_padded_sequence(sent_variable, sent_len, batch_first=True) x, _ = self.Rnn(sent_packed) - # print(x) - # [batch_size, max_len, hidden_size * direction] sent_output = torch.nn.utils.rnn.pad_packed_sequence(x, batch_first=True)[0] x = sent_output[idx_unsort] x = x.contiguous() - # x = x.view(batch_size * max_len, -1) x = self.Linear1(x) - # x = self.batch_norm(x) x = self.norm2(x) x = self.relu(x) x = self.drop(x) x = self.Linear2(x) - # x = x.view(batch_size, max_len, -1) - # [batch_size, max_len, num_classes] - # TODO seq_lens的key这样做不合理 - return {"loss": self._internal_loss(x, truth) if truth is not None else None, - "predict": self.decode(x), - 'word_seq_origin_len': word_seq_origin_len} - - def predict(self, **x): - out = self.forward(**x) - return {"predict": out["predict"]} - - def loss(self, **kwargs): - assert 'loss' in kwargs - return kwargs['loss'] - - -if __name__ == '__main__': - args = { - 'vocab_size': 20, - 'word_emb_dim': 100, - 'rnn_hidden_units': 100, - 'num_classes': 10, - } - model = AdvSeqLabel(args) - data = [] - for i in range(20): - word_seq = torch.randint(20, (15,)).long() - word_seq_len = torch.LongTensor([15]) - truth = torch.randint(10, (15,)).long() - data.append((word_seq, word_seq_len, truth)) - optimizer = torch.optim.Adam(model.parameters(), lr=0.01) - print(model) - curidx = 0 - for i in range(1000): - endidx = min(len(data), curidx + 5) - b_word, b_len, b_truth = [], [], [] - for word_seq, word_seq_len, truth in data[curidx: endidx]: - b_word.append(word_seq) - b_len.append(word_seq_len) - b_truth.append(truth) - word_seq = torch.stack(b_word, dim=0) - word_seq_len = torch.cat(b_len, dim=0) - truth = torch.stack(b_truth, dim=0) - res = model(word_seq, word_seq_len, truth) - loss = res['loss'] - pred = res['predict'] - print('loss: {} acc {}'.format(loss.item(), - ((pred.data == truth).long().sum().float() / word_seq_len.sum().float()))) - optimizer.zero_grad() - loss.backward() - optimizer.step() - curidx = endidx - if curidx == len(data): - curidx = 0 + if target is not None: + return {"loss": self._internal_loss(x, target)} + else: + return {"pred": self._decode(x)} + + def forward(self, words, seq_len, target): + """ + :param torch.LongTensor words: [batch_size, mex_len] + :param torch.LongTensor seq_len:[batch_size, ] + :param torch.LongTensor target: [batch_size, max_len], 目标 + :return torch.Tensor, a scalar loss + """ + return self._forward(words, seq_len, target) + + def predict(self, words, seq_len): + """ + + :param torch.LongTensor words: [batch_size, mex_len] + :param torch.LongTensor seq_len:[batch_size, ] + :return: [list1, list2, ...], 内部每个list为一个路径,已经unpad了。 + """ + return self._forward(words, seq_len, ) diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 99e7a9c2..cc713bc6 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -19,10 +19,10 @@ def allowed_transitions(id2label, encoding_type='bio', include_start_end=True): """ 给定一个id到label的映射表,返回所有可以跳转的(from_tag_id, to_tag_id)列表。 - :param id2label: Dict, key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 + :param dict id2label: key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 "B-NN", "M-NN", tag和label之间一定要用"-"隔开。一般可以通过Vocabulary.get_id2word()得到id2label。 - :param encoding_type: str, 支持"bio", "bmes", "bmeso"。 - :param include_start_end: bool, 是否包含开始与结尾的转换。比如在bio中,b/o可以在开头,但是i不能在开头; + :param str encoding_type: 支持"bio", "bmes", "bmeso"。 + :param bool include_start_end: 是否包含开始与结尾的转换。比如在bio中,b/o可以在开头,但是i不能在开头; 为True,返回的结果中会包含(start_idx, b_idx), (start_idx, o_idx), 但是不包含(start_idx, i_idx); start_idx=len(id2label), end_idx=len(id2label)+1。 为False, 返回的结果中不含与开始结尾相关的内容 @@ -62,11 +62,11 @@ def allowed_transitions(id2label, encoding_type='bio', include_start_end=True): def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label): """ - :param encoding_type: str, 支持"BIO", "BMES", "BEMSO"。 - :param from_tag: str, 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag - :param from_label: str, 比如"PER", "LOC"等label - :param to_tag: str, 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag - :param to_label: str, 比如"PER", "LOC"等label + :param str encoding_type: 支持"BIO", "BMES", "BEMSO"。 + :param str from_tag: 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag + :param str from_label: 比如"PER", "LOC"等label + :param str to_tag: 比如"B", "M"之类的标注tag. 还包括start, end等两种特殊tag + :param str to_label: 比如"PER", "LOC"等label :return: bool,能否跃迁 """ if to_tag=='start' or from_tag=='end': @@ -149,12 +149,12 @@ class ConditionalRandomField(nn.Module): """条件随机场。 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 - :param num_tags: int, 标签的数量 - :param include_start_end_trans: bool, 是否考虑各个tag作为开始以及结尾的分数。 - :param allowed_transitions: List[Tuple[from_tag_id(int), to_tag_id(int)]], 内部的Tuple[from_tag_id(int), + :param int num_tags: 标签的数量 + :param bool include_start_end_trans: 是否考虑各个tag作为开始以及结尾的分数。 + :param List[Tuple[from_tag_id(int), to_tag_id(int)]] allowed_transitions: 内部的Tuple[from_tag_id(int), to_tag_id(int)]视为允许发生的跃迁,其他没有包含的跃迁认为是禁止跃迁,可以通过 allowed_transitions()函数得到;如果为None,则所有跃迁均为合法 - :param initial_method: str, 初始化方法。见initial_parameter + :param str initial_method: 初始化方法。见initial_parameter """ super(ConditionalRandomField, self).__init__() @@ -237,10 +237,10 @@ class ConditionalRandomField(nn.Module): """ 用于计算CRF的前向loss,返回值为一个batch_size的FloatTensor,可能需要mean()求得loss。 - :param feats:FloatTensor, batch_size x max_len x num_tags,特征矩阵。 - :param tags:LongTensor, batch_size x max_len,标签矩阵。 - :param mask:ByteTensor batch_size x max_len,为0的位置认为是padding。 - :return:FloatTensor, batch_size + :param torch.FloatTensor feats:batch_size x max_len x num_tags,特征矩阵。 + :param torch.LongTensor tags: batch_size x max_len,标签矩阵。 + :param torch.ByteTensor mask: batch_size x max_len,为0的位置认为是padding。 + :return:torch.FloatTensor, (batch_size,) """ feats = feats.transpose(0, 1) tags = tags.transpose(0, 1).long() @@ -250,27 +250,26 @@ class ConditionalRandomField(nn.Module): return all_path_score - gold_path_score - def viterbi_decode(self, feats, mask, unpad=False): + def viterbi_decode(self, logits, mask, unpad=False): """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 - :param feats: FloatTensor, batch_size x max_len x num_tags,特征矩阵。 - :param mask: ByteTensor, batch_size x max_len, 为0的位置认为是pad;如果为None,则认为没有padding。 - :param unpad: bool, 是否将结果删去padding, - False, 返回的是batch_size x max_len的tensor, - True,返回的是List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int] - 的长度是这个sample的有效长度。 + :param torch.FloatTensor logits: batch_size x max_len x num_tags,特征矩阵。 + :param torch.ByteTensor mask: batch_size x max_len, 为0的位置认为是pad;如果为None,则认为没有padding。 + :param bool unpad: 是否将结果删去padding。False, 返回的是batch_size x max_len的tensor; True,返回的是 + List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int]的长度是这 + 个sample的有效长度。 :return: 返回 (paths, scores)。 paths: 是解码后的路径, 其值参照unpad参数. scores: torch.FloatTensor, size为(batch_size,), 对应每个最优路径的分数。 """ - batch_size, seq_len, n_tags = feats.size() - feats = feats.transpose(0, 1).data # L, B, H + batch_size, seq_len, n_tags = logits.size() + logits = logits.transpose(0, 1).data # L, B, H mask = mask.transpose(0, 1).data.byte() # L, B # 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] transitions = self._constrain.data.clone() transitions[:n_tags, :n_tags] += self.trans_m.data if self.include_start_end_trans: @@ -281,7 +280,7 @@ class ConditionalRandomField(nn.Module): trans_score = transitions[:n_tags, :n_tags].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 @@ -292,13 +291,13 @@ class ConditionalRandomField(nn.Module): vscore += transitions[:n_tags, n_tags+1].view(1, -1) # 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): @@ -311,6 +310,5 @@ class ConditionalRandomField(nn.Module): paths.append(ans[idx, :seq_len+1].tolist()) else: paths = ans - if get_score: - return paths, ans_score.tolist() - return paths + return paths, ans_score + diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index dfaac622..67db08f7 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -11,13 +11,12 @@ def log_sum_exp(x, dim=-1): def viterbi_decode(logits, transitions, mask=None, unpad=False): """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 - :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, - False, 返回的是batch_size x max_len的tensor, - True,返回的是List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int]的长度是 - 这个sample的有效长度。 + :param torch.FloatTensor logits: batch_size x max_len x num_tags,特征矩阵。 + :param torch.FloatTensor transitions: n_tags x n_tags。[i, j]位置的值认为是从tag i到tag j的转换。 + :param torch.ByteTensor mask: batch_size x max_len, 为0的位置认为是pad;如果为None,则认为没有padding。 + :param bool unpad: 是否将结果删去padding。False, 返回的是batch_size x max_len的tensor; True,返回的是 + List[List[int]], 内部的List[int]为每个sequence的label,已经除去pad部分,即每个List[int]的长度是这 + 个sample的有效长度。 :return: 返回 (paths, scores)。 paths: 是解码后的路径, 其值参照unpad参数. scores: torch.FloatTensor, size为(batch_size,), 对应每个最优路径的分数。 diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index 56b9ca59..06d8b86a 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -1,4 +1,3 @@ -from .conv import Conv from .conv_maxpool import ConvMaxpool from .embedding import Embedding from .linear import Linear @@ -8,6 +7,5 @@ from .bert import BertModel __all__ = ["LSTM", "Embedding", "Linear", - "Conv", "ConvMaxpool", "BertModel"] diff --git a/fastNLP/modules/encoder/conv.py b/fastNLP/modules/encoder/conv.py deleted file mode 100644 index 42254a8b..00000000 --- a/fastNLP/modules/encoder/conv.py +++ /dev/null @@ -1,58 +0,0 @@ -# python: 3.6 -# encoding: utf-8 - -import torch -import torch.nn as nn - -from fastNLP.modules.utils import initial_parameter - - -# import torch.nn.functional as F - - -class Conv(nn.Module): - """Basic 1-d convolution module, initialized with xavier_uniform. - - :param int in_channels: - :param int out_channels: - :param tuple kernel_size: - :param int stride: - :param int padding: - :param int dilation: - :param int groups: - :param bool bias: - :param str activation: - :param str initial_method: - """ - def __init__(self, in_channels, out_channels, kernel_size, - stride=1, padding=0, dilation=1, - groups=1, bias=True, activation='relu', initial_method=None): - super(Conv, self).__init__() - self.conv = nn.Conv1d( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=kernel_size, - stride=stride, - padding=padding, - dilation=dilation, - groups=groups, - bias=bias) - # xavier_uniform_(self.conv.weight) - - activations = { - 'relu': nn.ReLU(), - 'tanh': nn.Tanh()} - if activation in activations: - self.activation = activations[activation] - else: - raise Exception( - 'Should choose activation function from: ' + - ', '.join([x for x in activations])) - initial_parameter(self, initial_method) - - def forward(self, x): - x = torch.transpose(x, 1, 2) # [N,L,C] -> [N,C,L] - x = self.conv(x) # [N,C_in,L] -> [N,C_out,L] - x = self.activation(x) - x = torch.transpose(x, 1, 2) # [N,C,L] -> [N,L,C] - return x diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index 8b035871..d7a8b286 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -9,18 +9,21 @@ from fastNLP.modules.utils import initial_parameter class ConvMaxpool(nn.Module): - """Convolution and max-pooling module with multiple kernel sizes. + """集合了Convolution和Max-Pooling于一体的层。 + 给定一个batch_size x max_len x input_size的输入,返回batch_size x sum(output_channels) 大小的matrix。在内部,是先使用 + CNN给输入做卷积,然后经过activation激活层,在通过在长度(max_len)这一维进行max_pooling。最后得到每个sample的一个vector + 表示。 - :param int in_channels: - :param int out_channels: - :param tuple kernel_sizes: - :param int stride: - :param int padding: - :param int dilation: - :param int groups: - :param bool bias: - :param str activation: - :param str initial_method: + :param int in_channels: 输入channel的大小,一般是embedding的维度; 或encoder的output维度 + :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 + :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 + :param int stride: 见pytorch Conv1D文档。所有kernel共享一个stride。 + :param int padding: 见pytorch Conv1D文档。所有kernel共享一个padding。 + :param int dilation: 见pytorch Conv1D文档。所有kernel共享一个dilation。 + :param int groups: 见pytorch Conv1D文档。所有kernel共享一个groups。 + :param bool bias: 见pytorch Conv1D文档。所有kernel共享一个bias。 + :param str activation: Convolution后的结果将通过该activation后再经过max-pooling。支持relu, sigmoid, tanh + :param str initial_method: str。 """ def __init__(self, in_channels, out_channels, kernel_sizes, stride=1, padding=0, dilation=1, @@ -29,9 +32,14 @@ class ConvMaxpool(nn.Module): # convolution if isinstance(kernel_sizes, (list, tuple, int)): - if isinstance(kernel_sizes, int): + if isinstance(kernel_sizes, int) and isinstance(out_channels, int): out_channels = [out_channels] kernel_sizes = [kernel_sizes] + elif isinstance(kernel_sizes, (tuple, list)) and isinstance(out_channels, (tuple, list)): + assert len(out_channels)==len(kernel_sizes), "The number of out_channels should be equal to the number" \ + " of kernel_sizes." + else: + raise ValueError("The type of out_channels and kernel_sizes should be the same.") self.convs = nn.ModuleList([nn.Conv1d( in_channels=in_channels, @@ -51,18 +59,31 @@ class ConvMaxpool(nn.Module): # activation function if activation == 'relu': self.activation = F.relu + elif activation == 'sigmoid': + self.activation = F.sigmoid + elif activation == 'tanh': + self.activation = F.tanh else: raise Exception( - "Undefined activation function: choose from: relu") + "Undefined activation function: choose from: relu, tanh, sigmoid") initial_parameter(self, initial_method) - def forward(self, x): + def forward(self, x, mask=None): + """ + + :param torch.FloatTensor x: batch_size x max_len x input_size, 一般是经过embedding后的值 + :param mask: batch_size x max_len, pad的地方为0。不影响卷积运算,max-pool一定不会pool到pad为0的位置 + :return: + """ # [N,L,C] -> [N,C,L] x = torch.transpose(x, 1, 2) # convolution - xs = [self.activation(conv(x)) for conv in self.convs] # [[N,C,L]] + xs = [self.activation(conv(x)) for conv in self.convs] # [[N,C,L], ...] + if mask is not None: + mask = mask.unsqueeze(1) # B x 1 x L + xs = [x.masked_fill_(mask, float('-inf')) for x in xs] # max-pooling xs = [F.max_pool1d(input=i, kernel_size=i.size(2)).squeeze(2) - for i in xs] # [[N, C]] - return torch.cat(xs, dim=-1) # [N,C] + for i in xs] # [[N, C], ...] + return torch.cat(xs, dim=-1) # [N, C] \ No newline at end of file diff --git a/fastNLP/modules/encoder/masked_rnn.py b/fastNLP/modules/encoder/masked_rnn.py deleted file mode 100644 index 321546c4..00000000 --- a/fastNLP/modules/encoder/masked_rnn.py +++ /dev/null @@ -1,424 +0,0 @@ -__author__ = 'max' - -import torch -import torch.nn as nn -import torch.nn.functional as F - -from fastNLP.modules.utils import initial_parameter - - -def MaskedRecurrent(reverse=False): - def forward(input, hidden, cell, mask, train=True, dropout=0): - """ - :param input: - :param hidden: - :param cell: - :param mask: - :param dropout: step之间的dropout,对mask了的也会drop,应该是没问题的,反正没有gradient - :param train: 控制dropout的行为,在StackedRNN的forward中调用 - :return: - """ - output = [] - steps = range(input.size(0) - 1, -1, -1) if reverse else range(input.size(0)) - for i in steps: - if mask is None or mask[i].data.min() > 0.5: # 没有mask,都是1 - hidden = cell(input[i], hidden) - elif mask[i].data.max() > 0.5: # 有mask,但不全为0 - hidden_next = cell(input[i], hidden) # 一次喂入一个batch! - # hack to handle LSTM - if isinstance(hidden, tuple): # LSTM outputs a tuple of (hidden, cell), this is a common hack 😁 - mask = mask.float() - hx, cx = hidden - hp1, cp1 = hidden_next - hidden = ( - hx + (hp1 - hx) * mask[i].squeeze(), - cx + (cp1 - cx) * mask[i].squeeze()) # Why? 我知道了!!如果是mask就不用改变 - else: - hidden = hidden + (hidden_next - hidden) * mask[i] - - # if dropout != 0 and train: # warning, should i treat masked tensor differently? - # if isinstance(hidden, tuple): - # hidden = (F.dropout(hidden[0], p=dropout, training=train), - # F.dropout(hidden[1], p=dropout, training=train)) - # else: - # hidden = F.dropout(hidden, p=dropout, training=train) - - # hack to handle LSTM - output.append(hidden[0] if isinstance(hidden, tuple) else hidden) - - if reverse: - output.reverse() - output = torch.cat(output, 0).view(input.size(0), *output[0].size()) - - return hidden, output - - return forward - - -def StackedRNN(inners, num_layers, lstm=False, train=True, step_dropout=0, layer_dropout=0): - num_directions = len(inners) # rec_factory! - total_layers = num_layers * num_directions - - def forward(input, hidden, cells, mask): - assert (len(cells) == total_layers) - next_hidden = [] - - if lstm: - hidden = list(zip(*hidden)) - - for i in range(num_layers): - all_output = [] - for j, inner in enumerate(inners): - l = i * num_directions + j - hy, output = inner(input, hidden[l], cells[l], mask, step_dropout, train) - next_hidden.append(hy) - all_output.append(output) - - input = torch.cat(all_output, input.dim() - 1) # 下一层的输入 - - if layer_dropout != 0 and i < num_layers - 1: - input = F.dropout(input, p=layer_dropout, training=train, inplace=False) - - if lstm: - next_h, next_c = zip(*next_hidden) - next_hidden = ( - torch.cat(next_h, 0).view(total_layers, *next_h[0].size()), - torch.cat(next_c, 0).view(total_layers, *next_c[0].size()) - ) - else: - next_hidden = torch.cat(next_hidden, 0).view(total_layers, *next_hidden[0].size()) - - return next_hidden, input - - return forward - - -def AutogradMaskedRNN(num_layers=1, batch_first=False, train=True, layer_dropout=0, step_dropout=0, - bidirectional=False, lstm=False): - rec_factory = MaskedRecurrent - - if bidirectional: - layer = (rec_factory(), rec_factory(reverse=True)) - else: - layer = (rec_factory(),) # rec_factory 就是每层的结构啦!!在MaskedRecurrent中进行每层的计算!然后用StackedRNN接起来 - - func = StackedRNN(layer, - num_layers, - lstm=lstm, - layer_dropout=layer_dropout, step_dropout=step_dropout, - train=train) - - def forward(input, cells, hidden, mask): - if batch_first: - input = input.transpose(0, 1) - if mask is not None: - mask = mask.transpose(0, 1) - - nexth, output = func(input, hidden, cells, mask) - - if batch_first: - output = output.transpose(0, 1) - - return output, nexth - - return forward - - -def MaskedStep(): - def forward(input, hidden, cell, mask): - if mask is None or mask.data.min() > 0.5: - hidden = cell(input, hidden) - elif mask.data.max() > 0.5: - hidden_next = cell(input, hidden) - # hack to handle LSTM - if isinstance(hidden, tuple): - hx, cx = hidden - hp1, cp1 = hidden_next - hidden = (hx + (hp1 - hx) * mask, cx + (cp1 - cx) * mask) - else: - hidden = hidden + (hidden_next - hidden) * mask - # hack to handle LSTM - output = hidden[0] if isinstance(hidden, tuple) else hidden - - return hidden, output - - return forward - - -def StackedStep(layer, num_layers, lstm=False, dropout=0, train=True): - def forward(input, hidden, cells, mask): - assert (len(cells) == num_layers) - next_hidden = [] - - if lstm: - hidden = list(zip(*hidden)) - - for l in range(num_layers): - hy, output = layer(input, hidden[l], cells[l], mask) - next_hidden.append(hy) - input = output - - if dropout != 0 and l < num_layers - 1: - input = F.dropout(input, p=dropout, training=train, inplace=False) - - if lstm: - next_h, next_c = zip(*next_hidden) - next_hidden = ( - torch.cat(next_h, 0).view(num_layers, *next_h[0].size()), - torch.cat(next_c, 0).view(num_layers, *next_c[0].size()) - ) - else: - next_hidden = torch.cat(next_hidden, 0).view(num_layers, *next_hidden[0].size()) - - return next_hidden, input - - return forward - - -def AutogradMaskedStep(num_layers=1, dropout=0, train=True, lstm=False): - layer = MaskedStep() - - func = StackedStep(layer, - num_layers, - lstm=lstm, - dropout=dropout, - train=train) - - def forward(input, cells, hidden, mask): - nexth, output = func(input, hidden, cells, mask) - return output, nexth - - return forward - - -class MaskedRNNBase(nn.Module): - def __init__(self, Cell, input_size, hidden_size, - num_layers=1, bias=True, batch_first=False, - layer_dropout=0, step_dropout=0, bidirectional=False, initial_method = None , **kwargs): - """ - :param Cell: - :param input_size: - :param hidden_size: - :param num_layers: - :param bias: - :param batch_first: - :param layer_dropout: - :param step_dropout: - :param bidirectional: - :param kwargs: - """ - - super(MaskedRNNBase, self).__init__() - self.Cell = Cell - self.input_size = input_size - self.hidden_size = hidden_size - self.num_layers = num_layers - self.bias = bias - self.batch_first = batch_first - self.layer_dropout = layer_dropout - self.step_dropout = step_dropout - self.bidirectional = bidirectional - num_directions = 2 if bidirectional else 1 - - self.all_cells = [] - for layer in range(num_layers): # 初始化所有cell - for direction in range(num_directions): - layer_input_size = input_size if layer == 0 else hidden_size * num_directions - - cell = self.Cell(layer_input_size, hidden_size, self.bias, **kwargs) - self.all_cells.append(cell) - self.add_module('cell%d' % (layer * num_directions + direction), cell) # Max的代码写得真好看 - initial_parameter(self, initial_method) - def reset_parameters(self): - for cell in self.all_cells: - cell.reset_parameters() - - def forward(self, input, mask=None, hx=None): - batch_size = input.size(0) if self.batch_first else input.size(1) - lstm = self.Cell is nn.LSTMCell - if hx is None: - num_directions = 2 if self.bidirectional else 1 - hx = torch.autograd.Variable( - input.data.new(self.num_layers * num_directions, batch_size, self.hidden_size).zero_()) - if lstm: - hx = (hx, hx) - - func = AutogradMaskedRNN(num_layers=self.num_layers, - batch_first=self.batch_first, - step_dropout=self.step_dropout, - layer_dropout=self.layer_dropout, - train=self.training, - bidirectional=self.bidirectional, - lstm=lstm) # 传入all_cells,继续往底层封装走 - - output, hidden = func(input, self.all_cells, hx, - None if mask is None else mask.view(mask.size() + (1,))) # 这个+ (1, )是个什么操作? - return output, hidden - - def step(self, input, hx=None, mask=None): - """Execute one step forward (only for one-directional RNN). - - :param Tensor input: input tensor of this step. (batch, input_size) - :param Tensor hx: the hidden state of last step. (num_layers, batch, hidden_size) - :param Tensor mask: the mask tensor of this step. (batch, ) - :returns: - **output** (batch, hidden_size), tensor containing the output of this step from the last layer of RNN. - **hn** (num_layers, batch, hidden_size), tensor containing the hidden state of this step - - """ - assert not self.bidirectional, "step only cannot be applied to bidirectional RNN." # aha, typo! - batch_size = input.size(0) - lstm = self.Cell is nn.LSTMCell - if hx is None: - hx = torch.autograd.Variable(input.data.new(self.num_layers, batch_size, self.hidden_size).zero_()) - if lstm: - hx = (hx, hx) - - func = AutogradMaskedStep(num_layers=self.num_layers, - dropout=self.step_dropout, - train=self.training, - lstm=lstm) - - output, hidden = func(input, self.all_cells, hx, mask) - return output, hidden - - -class MaskedRNN(MaskedRNNBase): - r"""Applies a multi-layer Elman RNN with costomized non-linearity to an - input sequence. - For each element in the input sequence, each layer computes the following - function. :math:`h_t = \tanh(w_{ih} * x_t + b_{ih} + w_{hh} * h_{(t-1)} + b_{hh})` - - where :math:`h_t` is the hidden state at time `t`, and :math:`x_t` is - the hidden state of the previous layer at time `t` or :math:`input_t` - for the first layer. If nonlinearity='relu', then `ReLU` is used instead - of `tanh`. - - - :param int input_size: The number of expected features in the input x - :param int hidden_size: The number of features in the hidden state h - :param int num_layers: Number of recurrent layers. - :param str nonlinearity: The non-linearity to use ['tanh'|'relu']. Default: 'tanh' - :param bool bias: If False, then the layer does not use bias weights b_ih and b_hh. Default: True - :param bool batch_first: If True, then the input and output tensors are provided as (batch, seq, feature) - :param float dropout: If non-zero, introduces a dropout layer on the outputs of each RNN layer except the last layer - :param bool bidirectional: If True, becomes a bidirectional RNN. Default: False - - Inputs: input, mask, h_0 - - **input** (seq_len, batch, input_size): tensor containing the features - of the input sequence. - **mask** (seq_len, batch): 0-1 tensor containing the mask of the input sequence. - - **h_0** (num_layers * num_directions, batch, hidden_size): tensor - containing the initial hidden state for each element in the batch. - Outputs: output, h_n - - **output** (seq_len, batch, hidden_size * num_directions): tensor - containing the output features (h_k) from the last layer of the RNN, - for each k. If a :class:`torch.nn.utils.rnn.PackedSequence` has - been given as the input, the output will also be a packed sequence. - - **h_n** (num_layers * num_directions, batch, hidden_size): tensor - containing the hidden state for k=seq_len. - """ - - def __init__(self, *args, **kwargs): - super(MaskedRNN, self).__init__(nn.RNNCell, *args, **kwargs) - - -class MaskedLSTM(MaskedRNNBase): - r"""Applies a multi-layer long short-term memory (LSTM) RNN to an input - sequence. - For each element in the input sequence, each layer computes the following - function. - - .. math:: - - \begin{array}{ll} - i_t = \mathrm{sigmoid}(W_{ii} x_t + b_{ii} + W_{hi} h_{(t-1)} + b_{hi}) \\ - f_t = \mathrm{sigmoid}(W_{if} x_t + b_{if} + W_{hf} h_{(t-1)} + b_{hf}) \\ - g_t = \tanh(W_{ig} x_t + b_{ig} + W_{hc} h_{(t-1)} + b_{hg}) \\ - o_t = \mathrm{sigmoid}(W_{io} x_t + b_{io} + W_{ho} h_{(t-1)} + b_{ho}) \\ - c_t = f_t * c_{(t-1)} + i_t * g_t \\ - h_t = o_t * \tanh(c_t) - \end{array} - - where :math:`h_t` is the hidden state at time `t`, :math:`c_t` is the cell - state at time `t`, :math:`x_t` is the hidden state of the previous layer at - time `t` or :math:`input_t` for the first layer, and :math:`i_t`, - :math:`f_t`, :math:`g_t`, :math:`o_t` are the input, forget, cell, - and out gates, respectively. - - :param int input_size: The number of expected features in the input x - :param int hidden_size: The number of features in the hidden state h - :param int num_layers: Number of recurrent layers. - :param bool bias: If False, then the layer does not use bias weights b_ih and b_hh. Default: True - :param bool batch_first: If True, then the input and output tensors are provided as (batch, seq, feature) - :param bool dropout: If non-zero, introduces a dropout layer on the outputs of each RNN layer except the last layer - :param bool bidirectional: If True, becomes a bidirectional RNN. Default: False - - Inputs: input, mask, (h_0, c_0) - - **input** (seq_len, batch, input_size): tensor containing the features - of the input sequence. - **mask** (seq_len, batch): 0-1 tensor containing the mask of the input sequence. - - **h_0** (num_layers \* num_directions, batch, hidden_size): tensor - containing the initial hidden state for each element in the batch. - - **c_0** (num_layers \* num_directions, batch, hidden_size): tensor - containing the initial cell state for each element in the batch. - Outputs: output, (h_n, c_n) - - **output** (seq_len, batch, hidden_size * num_directions): tensor - containing the output features `(h_t)` from the last layer of the RNN, - for each t. If a :class:`torch.nn.utils.rnn.PackedSequence` has been - given as the input, the output will also be a packed sequence. - - **h_n** (num_layers * num_directions, batch, hidden_size): tensor - containing the hidden state for t=seq_len - - **c_n** (num_layers * num_directions, batch, hidden_size): tensor - containing the cell state for t=seq_len - """ - - def __init__(self, *args, **kwargs): - super(MaskedLSTM, self).__init__(nn.LSTMCell, *args, **kwargs) - - -class MaskedGRU(MaskedRNNBase): - r"""Applies a multi-layer gated recurrent unit (GRU) RNN to an input sequence. - For each element in the input sequence, each layer computes the following - function: - - .. math:: - - \begin{array}{ll} - r_t = \mathrm{sigmoid}(W_{ir} x_t + b_{ir} + W_{hr} h_{(t-1)} + b_{hr}) \\ - z_t = \mathrm{sigmoid}(W_{iz} x_t + b_{iz} + W_{hz} h_{(t-1)} + b_{hz}) \\ - n_t = \tanh(W_{in} x_t + b_{in} + r_t * (W_{hn} h_{(t-1)}+ b_{hn})) \\ - h_t = (1 - z_t) * n_t + z_t * h_{(t-1)} \\ - \end{array} - - where :math:`h_t` is the hidden state at time `t`, :math:`x_t` is the hidden - state of the previous layer at time `t` or :math:`input_t` for the first - layer, and :math:`r_t`, :math:`z_t`, :math:`n_t` are the reset, input, - and new gates, respectively. - - :param int input_size: The number of expected features in the input x - :param int hidden_size: The number of features in the hidden state h - :param int num_layers: Number of recurrent layers. - :param str nonlinearity: The non-linearity to use ['tanh'|'relu']. Default: 'tanh' - :param bool bias: If False, then the layer does not use bias weights b_ih and b_hh. Default: True - :param bool batch_first: If True, then the input and output tensors are provided as (batch, seq, feature) - :param bool dropout: If non-zero, introduces a dropout layer on the outputs of each RNN layer except the last layer - :param bool bidirectional: If True, becomes a bidirectional RNN. Default: False - - Inputs: input, mask, h_0 - - **input** (seq_len, batch, input_size): tensor containing the features - of the input sequence. - **mask** (seq_len, batch): 0-1 tensor containing the mask of the input sequence. - - **h_0** (num_layers * num_directions, batch, hidden_size): tensor - containing the initial hidden state for each element in the batch. - Outputs: output, h_n - - **output** (seq_len, batch, hidden_size * num_directions): tensor - containing the output features (h_k) from the last layer of the RNN, - for each k. If a :class:`torch.nn.utils.rnn.PackedSequence` has - been given as the input, the output will also be a packed sequence. - - **h_n** (num_layers * num_directions, batch, hidden_size): tensor - containing the hidden state for k=seq_len. - """ - - def __init__(self, *args, **kwargs): - super(MaskedGRU, self).__init__(nn.GRUCell, *args, **kwargs) diff --git a/test/modules/test_masked_rnn.py b/test/modules/test_masked_rnn.py deleted file mode 100644 index 80f49f33..00000000 --- a/test/modules/test_masked_rnn.py +++ /dev/null @@ -1,27 +0,0 @@ - -import torch -import unittest - -from fastNLP.modules.encoder.masked_rnn import MaskedRNN - -class TestMaskedRnn(unittest.TestCase): - def test_case_1(self): - masked_rnn = MaskedRNN(input_size=1, hidden_size=1, bidirectional=True, batch_first=True) - x = torch.tensor([[[1.0], [2.0]]]) - print(x.size()) - y = masked_rnn(x) - mask = torch.tensor([[[1], [1]]]) - y = masked_rnn(x, mask=mask) - mask = torch.tensor([[[1], [0]]]) - y = masked_rnn(x, mask=mask) - - def test_case_2(self): - masked_rnn = MaskedRNN(input_size=1, hidden_size=1, bidirectional=False, batch_first=True) - x = torch.tensor([[[1.0], [2.0]]]) - print(x.size()) - y = masked_rnn(x) - mask = torch.tensor([[[1], [1]]]) - y = masked_rnn(x, mask=mask) - xx = torch.tensor([[[1.0]]]) - y = masked_rnn.step(xx) - y = masked_rnn.step(xx, mask=mask) \ No newline at end of file diff --git a/test/test_tutorials.py b/test/test_tutorials.py index eb77321c..a1d47dde 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -70,7 +70,7 @@ class TestTutorial(unittest.TestCase): break from fastNLP.models import CNNText - model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) + model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) from fastNLP import Trainer from copy import deepcopy @@ -145,7 +145,7 @@ class TestTutorial(unittest.TestCase): is_input=True) from fastNLP.models import CNNText - model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) + model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric trainer = Trainer(model=model, @@ -405,7 +405,7 @@ class TestTutorial(unittest.TestCase): # 另一个例子:加载CNN文本分类模型 from fastNLP.models import CNNText - cnn_text_model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) + cnn_text_model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) cnn_text_model from fastNLP import CrossEntropyLoss From 46d72c7439524a07672168dce735407501651811 Mon Sep 17 00:00:00 2001 From: yunfan Date: Thu, 25 Apr 2019 22:14:26 +0800 Subject: [PATCH 061/173] - update doc - add get_embeddings --- fastNLP/core/vocabulary.py | 33 ++++++++++++++++++------------ fastNLP/models/biaffine_parser.py | 13 ++++++------ fastNLP/models/star_transformer.py | 11 ++++++---- fastNLP/modules/utils.py | 23 +++++++++++++++++++++ 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 6a1830ad..6779a282 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -231,22 +231,29 @@ class Vocabulary(object): vocab.from_dataset(train_data1, train_data2, field_name='words') :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个. - :param str field_name: 构建词典所使用的 field. - 若有多个 DataSet, 每个DataSet都必须有此 field. - 目前仅支持 ``str`` , ``list(str)`` , ``list(list(str))`` + :param field_name: 可为 ``str`` 或 ``list(str)`` . + 构建词典所使用的 field(s), 支持一个或多个field + 若有多个 DataSet, 每个DataSet都必须有这些field. + 目前仅支持的field结构: ``str`` , ``list(str)`` , ``list(list(str))`` :return self: """ + if isinstance(field_name, str): + field_name = [field_name] + elif not isinstance(field_name, list): + raise TypeError('invalid argument field_name: {}'.format(field_name)) + def construct_vocab(ins): - field = ins[field_name] - if isinstance(field, str): - self.add_word(field) - elif isinstance(field, list): - if not isinstance(field[0], list): - self.add_word_lst(field) - else: - if isinstance(field[0][0], list): - raise RuntimeError("Only support field with 2 dimensions.") - [self.add_word_lst(w) for w in field] + for fn in field_name: + field = ins[fn] + if isinstance(field, str): + self.add_word(field) + elif isinstance(field, list): + if not isinstance(field[0], list): + self.add_word_lst(field) + else: + if isinstance(field[0][0], list): + raise RuntimeError("Only support field with 2 dimensions.") + [self.add_word_lst(w) for w in field] for idx, dataset in enumerate(datasets): if isinstance(dataset, DataSet): try: diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 59d95558..f2329dca 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -16,7 +16,7 @@ from fastNLP.modules.encoder.transformer import TransformerEncoder from fastNLP.modules.encoder.variational_rnn import VarLSTM from fastNLP.modules.utils import initial_parameter from fastNLP.modules.utils import seq_mask - +from fastNLP.modules.utils import get_embeddings def _mst(scores): """ @@ -230,8 +230,9 @@ class BiaffineParser(GraphParser): 论文参考 ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) `_ . - :param word_vocab_size: 单词词典大小 - :param word_emb_dim: 单词词嵌入向量的维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param pos_vocab_size: part-of-speech 词典大小 :param pos_emb_dim: part-of-speech 向量维度 :param num_label: 边的类别个数 @@ -245,8 +246,7 @@ class BiaffineParser(GraphParser): 若 ``False`` , 使用更加精确但相对缓慢的MST算法. Default: ``False`` """ def __init__(self, - word_vocab_size, - word_emb_dim, + init_embed, pos_vocab_size, pos_emb_dim, num_label, @@ -260,7 +260,8 @@ class BiaffineParser(GraphParser): super(BiaffineParser, self).__init__() rnn_out_size = 2 * rnn_hidden_size word_hid_dim = pos_hid_dim = rnn_hidden_size - self.word_embedding = nn.Embedding(num_embeddings=word_vocab_size, embedding_dim=word_emb_dim) + self.word_embedding = get_embeddings(init_embed) + word_emb_dim = self.word_embedding.embedding_dim self.pos_embedding = nn.Embedding(num_embeddings=pos_vocab_size, embedding_dim=pos_emb_dim) self.word_fc = nn.Linear(word_emb_dim, word_hid_dim) self.pos_fc = nn.Linear(pos_emb_dim, pos_hid_dim) diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index f68aca42..e4fbeb28 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -2,6 +2,7 @@ """ from fastNLP.modules.encoder.star_transformer import StarTransformer from fastNLP.core.utils import seq_lens_to_masks +from ..modules.utils import get_embeddings import torch from torch import nn @@ -12,8 +13,9 @@ class StarTransEnc(nn.Module): """ 带word embedding的Star-Transformer Encoder - :param vocab_size: 词嵌入的词典大小 - :param emb_dim: 每个词嵌入的特征维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. :param num_layers: 模型层数. @@ -24,7 +26,7 @@ class StarTransEnc(nn.Module): :param emb_dropout: 词嵌入的dropout概率. :param dropout: 模型除词嵌入外的dropout概率. """ - def __init__(self, vocab_size, emb_dim, + def __init__(self, init_embed, hidden_size, num_layers, num_head, @@ -33,9 +35,10 @@ class StarTransEnc(nn.Module): emb_dropout, dropout): super(StarTransEnc, self).__init__() + self.embedding = get_embeddings(init_embed) + emb_dim = self.embedding.embedding_dim self.emb_fc = nn.Linear(emb_dim, hidden_size) self.emb_drop = nn.Dropout(emb_dropout) - self.embedding = nn.Embedding(vocab_size, emb_dim) self.encoder = StarTransformer(hidden_size=hidden_size, num_layers=num_layers, num_head=num_head, diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 4ae15b18..56dbb894 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -1,3 +1,4 @@ +import numpy as np import torch import torch.nn as nn import torch.nn.init as init @@ -88,3 +89,25 @@ def seq_mask(seq_len, max_len): seq_len = seq_len.view(-1, 1).long() # [batch_size, 1] seq_range = torch.arange(start=0, end=max_len, dtype=torch.long, device=seq_len.device).view(1, -1) # [1, max_len] return torch.gt(seq_len, seq_range) # [batch_size, max_len] + + +def get_embeddings(init_embed): + """得到词嵌入 + + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding + :return embeddings: + """ + if isinstance(init_embed, tuple): + res = nn.Embedding(num_embeddings=init_embed[0], embedding_dim=init_embed[1]) + elif isinstance(init_embed, nn.Embedding): + res = init_embed + elif isinstance(init_embed, torch.Tensor): + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + elif isinstance(init_embed, np.ndarray): + init_embed = torch.tensor(init_embed, dtype=torch.float32) + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + else: + raise TypeError('invalid init_embed type: {}'.format((type(init_embed)))) + return res From a7a9fc34b4b9c47c4869a738ad8404766be7499f Mon Sep 17 00:00:00 2001 From: ChenXin Date: Fri, 26 Apr 2019 13:11:17 +0800 Subject: [PATCH 062/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Makefile | 2 +- docs/source/conf.py | 32 ++++++++----- docs/source/fastNLP.api.rst | 12 +++-- docs/source/fastNLP.automl.rst | 44 ++++++++++++++++++ docs/source/fastNLP.component.rst | 20 +++++++++ docs/source/fastNLP.core.rst | 12 +++-- docs/source/fastNLP.io.rst | 36 +++++++-------- docs/source/fastNLP.models.rst | 52 +++++++++++----------- docs/source/fastNLP.modules.aggregator.rst | 42 ++++------------- docs/source/fastNLP.modules.decoder.rst | 12 +++-- docs/source/fastNLP.modules.encoder.rst | 42 +++++++++-------- docs/source/fastNLP.modules.rst | 16 +++---- docs/source/fastNLP.rst | 13 +++--- 13 files changed, 189 insertions(+), 146 deletions(-) create mode 100644 docs/source/fastNLP.automl.rst create mode 100644 docs/source/fastNLP.component.rst diff --git a/docs/Makefile b/docs/Makefile index 6a2ed12a..3050e655 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,7 +14,7 @@ help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) apidoc: - @$(SPHINXAPIDOC) -fM -o source ../fastNLP + $(SPHINXAPIDOC) -fM -o source ../$(SPHINXPROJ) server: cd build/html && python -m http.server diff --git a/docs/source/conf.py b/docs/source/conf.py index 96f7f437..5fd9e56d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,6 +14,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('../../')) # -- Project information ----------------------------------------------------- @@ -27,7 +28,6 @@ version = '0.4' # The full version, including alpha/beta/rc tags release = '0.4' - # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. @@ -42,9 +42,15 @@ extensions = [ 'sphinx.ext.viewcode', 'sphinx.ext.autosummary', 'sphinx.ext.mathjax', - + 'sphinx.ext.todo' ] +autodoc_default_options = { + 'member-order': 'bysource', + 'special-members': '__init__', + 'undoc-members': True, +} + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -72,7 +78,6 @@ exclude_patterns = ['modules.rst'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' - # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for @@ -107,22 +112,21 @@ html_static_path = ['_static'] # Output file base name for HTML help builder. htmlhelp_basename = 'fastNLPdoc' - # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - + # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - + # Additional stuff for the LaTeX preamble. # # 'preamble': '', - + # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -136,7 +140,6 @@ latex_documents = [ 'xpqiu', 'manual'), ] - # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples @@ -146,7 +149,6 @@ man_pages = [ [author], 1) ] - # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples @@ -159,4 +161,14 @@ texinfo_documents = [ ] -# -- Extension configuration ------------------------------------------------- \ No newline at end of file +# -- Extension configuration ------------------------------------------------- +def maybe_skip_member(app, what, name, obj, skip, options): + if name.startswith("_"): + return True + if obj.__doc__ is None: + return True + return False + + +def setup(app): + app.connect('autodoc-skip-member', maybe_skip_member) diff --git a/docs/source/fastNLP.api.rst b/docs/source/fastNLP.api.rst index ee2413fb..955eb8c5 100644 --- a/docs/source/fastNLP.api.rst +++ b/docs/source/fastNLP.api.rst @@ -1,6 +1,11 @@ fastNLP.api package =================== +.. automodule:: fastNLP.api + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- @@ -53,10 +58,3 @@ fastNLP.api.utils module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.automl.rst b/docs/source/fastNLP.automl.rst new file mode 100644 index 00000000..3c12e271 --- /dev/null +++ b/docs/source/fastNLP.automl.rst @@ -0,0 +1,44 @@ +fastNLP.automl package +====================== + +.. automodule:: fastNLP.automl + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +fastNLP.automl.enas\_controller module +-------------------------------------- + +.. automodule:: fastNLP.automl.enas_controller + :members: + :undoc-members: + :show-inheritance: + +fastNLP.automl.enas\_model module +--------------------------------- + +.. automodule:: fastNLP.automl.enas_model + :members: + :undoc-members: + :show-inheritance: + +fastNLP.automl.enas\_trainer module +----------------------------------- + +.. automodule:: fastNLP.automl.enas_trainer + :members: + :undoc-members: + :show-inheritance: + +fastNLP.automl.enas\_utils module +--------------------------------- + +.. automodule:: fastNLP.automl.enas_utils + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/source/fastNLP.component.rst b/docs/source/fastNLP.component.rst new file mode 100644 index 00000000..81fcf561 --- /dev/null +++ b/docs/source/fastNLP.component.rst @@ -0,0 +1,20 @@ +fastNLP.component package +========================= + +.. automodule:: fastNLP.component + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +fastNLP.component.bert\_tokenizer module +---------------------------------------- + +.. automodule:: fastNLP.component.bert_tokenizer + :members: + :undoc-members: + :show-inheritance: + + diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst index 79d26c76..540bd03c 100644 --- a/docs/source/fastNLP.core.rst +++ b/docs/source/fastNLP.core.rst @@ -1,6 +1,11 @@ fastNLP.core package ==================== +.. automodule:: fastNLP.core + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- @@ -117,10 +122,3 @@ fastNLP.core.vocabulary module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.core - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index e677b50a..1eb95c6a 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -1,51 +1,56 @@ fastNLP.io package ================== +.. automodule:: fastNLP.io + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -fastNLP.io.base_loader module ------------------------------ +fastNLP.io.base\_loader module +------------------------------ .. automodule:: fastNLP.io.base_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.config_io module ---------------------------- +fastNLP.io.config\_io module +---------------------------- .. automodule:: fastNLP.io.config_io :members: :undoc-members: :show-inheritance: -fastNLP.io.dataset_loader module --------------------------------- +fastNLP.io.dataset\_loader module +--------------------------------- .. automodule:: fastNLP.io.dataset_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.embed_loader module ------------------------------- +fastNLP.io.embed\_loader module +------------------------------- .. automodule:: fastNLP.io.embed_loader :members: :undoc-members: :show-inheritance: -fastNLP.io.file_reader module ------------------------------ +fastNLP.io.file\_reader module +------------------------------ .. automodule:: fastNLP.io.file_reader :members: :undoc-members: :show-inheritance: -fastNLP.io.model_io module --------------------------- +fastNLP.io.model\_io module +--------------------------- .. automodule:: fastNLP.io.model_io :members: @@ -53,10 +58,3 @@ fastNLP.io.model_io module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.io - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index ccf6abb2..18b8186f 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -1,11 +1,16 @@ fastNLP.models package ====================== +.. automodule:: fastNLP.models + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -fastNLP.models.base_model module --------------------------------- +fastNLP.models.base\_model module +--------------------------------- .. automodule:: fastNLP.models.base_model :members: @@ -20,64 +25,64 @@ fastNLP.models.bert module :undoc-members: :show-inheritance: -fastNLP.models.biaffine_parser module -------------------------------------- +fastNLP.models.biaffine\_parser module +-------------------------------------- .. automodule:: fastNLP.models.biaffine_parser :members: :undoc-members: :show-inheritance: -fastNLP.models.char_language_model module ------------------------------------------ +fastNLP.models.char\_language\_model module +------------------------------------------- .. automodule:: fastNLP.models.char_language_model :members: :undoc-members: :show-inheritance: -fastNLP.models.cnn_text_classification module ---------------------------------------------- +fastNLP.models.cnn\_text\_classification module +----------------------------------------------- .. automodule:: fastNLP.models.cnn_text_classification :members: :undoc-members: :show-inheritance: -fastNLP.models.enas_controller module -------------------------------------- +fastNLP.models.enas\_controller module +-------------------------------------- .. automodule:: fastNLP.models.enas_controller :members: :undoc-members: :show-inheritance: -fastNLP.models.enas_model module --------------------------------- +fastNLP.models.enas\_model module +--------------------------------- .. automodule:: fastNLP.models.enas_model :members: :undoc-members: :show-inheritance: -fastNLP.models.enas_trainer module ----------------------------------- +fastNLP.models.enas\_trainer module +----------------------------------- .. automodule:: fastNLP.models.enas_trainer :members: :undoc-members: :show-inheritance: -fastNLP.models.enas_utils module --------------------------------- +fastNLP.models.enas\_utils module +--------------------------------- .. automodule:: fastNLP.models.enas_utils :members: :undoc-members: :show-inheritance: -fastNLP.models.sequence_modeling module ---------------------------------------- +fastNLP.models.sequence\_modeling module +---------------------------------------- .. automodule:: fastNLP.models.sequence_modeling :members: @@ -92,8 +97,8 @@ fastNLP.models.snli module :undoc-members: :show-inheritance: -fastNLP.models.star_transformer module --------------------------------------- +fastNLP.models.star\_transformer module +--------------------------------------- .. automodule:: fastNLP.models.star_transformer :members: @@ -101,10 +106,3 @@ fastNLP.models.star_transformer module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.models - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst index 478ac218..74ff5aed 100644 --- a/docs/source/fastNLP.modules.aggregator.rst +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -1,54 +1,28 @@ fastNLP.modules.aggregator package ================================== -Submodules ----------- - -fastNLP.modules.aggregator.attention module -------------------------------------------- - -.. automodule:: fastNLP.modules.aggregator.attention +.. automodule:: fastNLP.modules.aggregator :members: :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.avg_pool module ------------------------------------------- - -.. automodule:: fastNLP.modules.aggregator.avg_pool - :members: - :undoc-members: - :show-inheritance: +Submodules +---------- -fastNLP.modules.aggregator.kmax_pool module +fastNLP.modules.aggregator.attention module ------------------------------------------- -.. automodule:: fastNLP.modules.aggregator.kmax_pool - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.aggregator.max_pool module ------------------------------------------- - -.. automodule:: fastNLP.modules.aggregator.max_pool +.. automodule:: fastNLP.modules.aggregator.attention :members: :undoc-members: :show-inheritance: -fastNLP.modules.aggregator.self_attention module ------------------------------------------------- +fastNLP.modules.aggregator.pooling module +----------------------------------------- -.. automodule:: fastNLP.modules.aggregator.self_attention +.. automodule:: fastNLP.modules.aggregator.pooling :members: :undoc-members: :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.modules.aggregator - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index 60706b06..5e467b98 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -1,6 +1,11 @@ fastNLP.modules.decoder package =============================== +.. automodule:: fastNLP.modules.decoder + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- @@ -29,10 +34,3 @@ fastNLP.modules.decoder.utils module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.modules.decoder - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index 4cc0b8ab..ff048be7 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -1,13 +1,26 @@ fastNLP.modules.encoder package =============================== +.. automodule:: fastNLP.modules.encoder + :members: + :undoc-members: + :show-inheritance: + Submodules ---------- -fastNLP.modules.encoder.char_embedding module ---------------------------------------------- +fastNLP.modules.encoder.bert module +----------------------------------- -.. automodule:: fastNLP.modules.encoder.char_embedding +.. automodule:: fastNLP.modules.encoder.bert + :members: + :undoc-members: + :show-inheritance: + +fastNLP.modules.encoder.char\_encoder module +-------------------------------------------- + +.. automodule:: fastNLP.modules.encoder.char_encoder :members: :undoc-members: :show-inheritance: @@ -20,8 +33,8 @@ fastNLP.modules.encoder.conv module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.conv_maxpool module -------------------------------------------- +fastNLP.modules.encoder.conv\_maxpool module +-------------------------------------------- .. automodule:: fastNLP.modules.encoder.conv_maxpool :members: @@ -52,16 +65,16 @@ fastNLP.modules.encoder.lstm module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.masked_rnn module ------------------------------------------ +fastNLP.modules.encoder.masked\_rnn module +------------------------------------------ .. automodule:: fastNLP.modules.encoder.masked_rnn :members: :undoc-members: :show-inheritance: -fastNLP.modules.encoder.star_transformer module ------------------------------------------------ +fastNLP.modules.encoder.star\_transformer module +------------------------------------------------ .. automodule:: fastNLP.modules.encoder.star_transformer :members: @@ -76,8 +89,8 @@ fastNLP.modules.encoder.transformer module :undoc-members: :show-inheritance: -fastNLP.modules.encoder.variational_rnn module ----------------------------------------------- +fastNLP.modules.encoder.variational\_rnn module +----------------------------------------------- .. automodule:: fastNLP.modules.encoder.variational_rnn :members: @@ -85,10 +98,3 @@ fastNLP.modules.encoder.variational_rnn module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.modules.encoder - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 53cc19a0..5884e655 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -1,6 +1,11 @@ fastNLP.modules package ======================= +.. automodule:: fastNLP.modules + :members: + :undoc-members: + :show-inheritance: + Subpackages ----------- @@ -21,8 +26,8 @@ fastNLP.modules.dropout module :undoc-members: :show-inheritance: -fastNLP.modules.other_modules module ------------------------------------- +fastNLP.modules.other\_modules module +------------------------------------- .. automodule:: fastNLP.modules.other_modules :members: @@ -38,10 +43,3 @@ fastNLP.modules.utils module :show-inheritance: -Module contents ---------------- - -.. automodule:: fastNLP.modules - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst index 6348c9a6..f5247748 100644 --- a/docs/source/fastNLP.rst +++ b/docs/source/fastNLP.rst @@ -1,6 +1,11 @@ fastNLP package =============== +.. automodule:: fastNLP + :members: + :undoc-members: + :show-inheritance: + Subpackages ----------- @@ -8,15 +13,9 @@ Subpackages fastNLP.api fastNLP.automl + fastNLP.component fastNLP.core fastNLP.io fastNLP.models fastNLP.modules -Module contents ---------------- - -.. automodule:: fastNLP - :members: - :undoc-members: - :show-inheritance: From 5e9c406761e8eb510a4604e603fba9d6befcb4a9 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Fri, 26 Apr 2019 13:27:55 +0800 Subject: [PATCH 063/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9embedding=E4=B8=BAini?= =?UTF-8?q?t=5Fembed=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/vocabulary.py | 33 ++++++++++------- fastNLP/io/__init__.py | 36 +++++++++++++++++++ fastNLP/io/config_io.py | 5 +++ fastNLP/io/dataset_loader.py | 18 +++++++++- fastNLP/io/embed_loader.py | 5 +++ fastNLP/io/model_io.py | 5 +++ fastNLP/models/biaffine_parser.py | 15 ++++---- fastNLP/models/cnn_text_classification.py | 11 +++--- fastNLP/models/sequence_modeling.py | 24 ++++++------- fastNLP/models/snli.py | 3 +- fastNLP/models/star_transformer.py | 13 ++++--- fastNLP/modules/encoder/embedding.py | 36 ++++++++++++------- fastNLP/modules/encoder/lstm.py | 7 ++-- fastNLP/modules/encoder/star_transformer.py | 2 ++ fastNLP/modules/encoder/variational_rnn.py | 25 ++++++++----- fastNLP/modules/utils.py | 23 ++++++++++++ .../main.py | 2 +- requirements.txt | 6 ++-- test/api/test_processor.py | 2 +- test/test_tutorials.py | 12 +++---- 20 files changed, 204 insertions(+), 79 deletions(-) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 7ac6ed65..633a748f 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -231,22 +231,29 @@ class Vocabulary(object): vocab.from_dataset(train_data1, train_data2, field_name='words') :param DataSet datasets: 需要转index的 DataSet, 支持一个或多个. - :param str field_name: 构建词典所使用的 field. - 若有多个 DataSet, 每个DataSet都必须有此 field. - 目前仅支持 ``str`` , ``list(str)`` , ``list(list(str))`` + :param field_name: 可为 ``str`` 或 ``list(str)`` . + 构建词典所使用的 field(s), 支持一个或多个field + 若有多个 DataSet, 每个DataSet都必须有这些field. + 目前仅支持的field结构: ``str`` , ``list(str)`` , ``list(list(str))`` :return self: """ + if isinstance(field_name, str): + field_name = [field_name] + elif not isinstance(field_name, list): + raise TypeError('invalid argument field_name: {}'.format(field_name)) + def construct_vocab(ins): - field = ins[field_name] - if isinstance(field, str): - self.add_word(field) - elif isinstance(field, list): - if not isinstance(field[0], list): - self.add_word_lst(field) - else: - if isinstance(field[0][0], list): - raise RuntimeError("Only support field with 2 dimensions.") - [self.add_word_lst(w) for w in field] + for fn in field_name: + field = ins[fn] + if isinstance(field, str): + self.add_word(field) + elif isinstance(field, list): + if not isinstance(field[0], list): + self.add_word_lst(field) + else: + if isinstance(field[0][0], list): + raise RuntimeError("Only support field with 2 dimensions.") + [self.add_word_lst(w) for w in field] for idx, dataset in enumerate(datasets): if isinstance(dataset, DataSet): try: diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index a3b18aa5..e8ccca30 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -1 +1,37 @@ +""" +用于IO的模块, 具体包括: + +1. 用于读入 embedding 的 :ref:`EmbedLoader ` 类, + +2. 用于读入数据的 :ref:`DataSetLoader ` 类 + +3. 用于读写config文件的类, 参考 :ref:`Config-io ` + +4. 用于保存和载入模型的类, 参考 :ref:`Model-io ` + +这些类的使用方法可以在对应module的文档下查看. +""" from .embed_loader import EmbedLoader +from .dataset_loader import * +from .config_io import * +from .model_io import * + +__all__ = [ + 'EmbedLoader', + + 'DataSetLoader', + 'CSVLoader', + 'JsonLoader', + 'ConllLoader', + 'SNLILoader', + 'SSTLoader', + 'PeopleDailyCorpusLoader', + 'Conll2003Loader', + + 'ConfigLoader', + 'ConfigSection', + 'ConfigSaver', + + 'ModelLoader', + 'ModelSaver', +] \ No newline at end of file diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index c0ffe53e..f303f0e9 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -1,3 +1,8 @@ +""" +.. _config-io: + +用于读入和处理和保存 config 文件 +""" import configparser import json import os diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 039c4242..bb5e2f64 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,3 +1,18 @@ +""" +.. _dataset-loader: + +DataSetLoader 的 API, 用于读取不同格式的数据, 并返回 `DataSet` , +得到的 `DataSet` 对象可以直接传入 `Trainer`, `Tester`, 用于模型的训练和测试 + +Example:: + + loader = SNLILoader() + train_ds = loader.load('path/to/train') + dev_ds = loader.load('path/to/dev') + test_ds = loader.load('path/to/test') + + # ... do stuff +""" import os import json from nltk.tree import Tree @@ -55,8 +70,9 @@ def _uncompress(src, dst): class DataSetLoader: - """所有`DataSetLoader`的接口 + """ + 所有`DataSetLoader`的接口 """ def load(self, path): diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 31e590da..39d93fab 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,3 +1,8 @@ +""" +.. _embed-loader: + +用于读取预训练的embedding, 读取结果可直接载入为模型参数 +""" import os import numpy as np diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index 53bdc7ce..d28034c8 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -1,3 +1,8 @@ +""" +.. _model-io: + +用于载入和保存模型 +""" import torch from fastNLP.io.base_loader import BaseLoader diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 9a070c92..f2329dca 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -1,3 +1,5 @@ +"""Biaffine Dependency Parser 的 Pytorch 实现. +""" from collections import defaultdict import numpy as np @@ -14,7 +16,7 @@ from fastNLP.modules.encoder.transformer import TransformerEncoder from fastNLP.modules.encoder.variational_rnn import VarLSTM from fastNLP.modules.utils import initial_parameter from fastNLP.modules.utils import seq_mask - +from fastNLP.modules.utils import get_embeddings def _mst(scores): """ @@ -228,8 +230,9 @@ class BiaffineParser(GraphParser): 论文参考 ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) `_ . - :param word_vocab_size: 单词词典大小 - :param word_emb_dim: 单词词嵌入向量的维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param pos_vocab_size: part-of-speech 词典大小 :param pos_emb_dim: part-of-speech 向量维度 :param num_label: 边的类别个数 @@ -243,8 +246,7 @@ class BiaffineParser(GraphParser): 若 ``False`` , 使用更加精确但相对缓慢的MST算法. Default: ``False`` """ def __init__(self, - word_vocab_size, - word_emb_dim, + init_embed, pos_vocab_size, pos_emb_dim, num_label, @@ -258,7 +260,8 @@ class BiaffineParser(GraphParser): super(BiaffineParser, self).__init__() rnn_out_size = 2 * rnn_hidden_size word_hid_dim = pos_hid_dim = rnn_hidden_size - self.word_embedding = nn.Embedding(num_embeddings=word_vocab_size, embedding_dim=word_emb_dim) + self.word_embedding = get_embeddings(init_embed) + word_emb_dim = self.word_embedding.embedding_dim self.pos_embedding = nn.Embedding(num_embeddings=pos_vocab_size, embedding_dim=pos_emb_dim) self.word_fc = nn.Linear(word_emb_dim, word_hid_dim) self.pos_fc = nn.Linear(pos_emb_dim, pos_hid_dim) diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index 37551e14..86848d0c 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -14,8 +14,7 @@ class CNNText(torch.nn.Module): 'Yoon Kim. 2014. Convolution Neural Networks for Sentence Classification.' """ - def __init__(self, vocab_size, - embed_dim, + def __init__(self, init_embed, num_classes, kernel_nums=(3, 4, 5), kernel_sizes=(3, 4, 5), @@ -23,8 +22,8 @@ class CNNText(torch.nn.Module): dropout=0.5): """ - :param int vocab_size: 词表的大小 - :param int embed_dim: 词embedding的维度大小 + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding :param int num_classes: 一共有多少类 :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 @@ -34,9 +33,9 @@ class CNNText(torch.nn.Module): super(CNNText, self).__init__() # no support for pre-trained embedding currently - self.embed = encoder.Embedding(vocab_size, embed_dim) + self.embed = encoder.Embedding(init_embed) self.conv_pool = encoder.ConvMaxpool( - in_channels=embed_dim, + in_channels=self.embed.embedding_dim, out_channels=kernel_nums, kernel_sizes=kernel_sizes, padding=padding) diff --git a/fastNLP/models/sequence_modeling.py b/fastNLP/models/sequence_modeling.py index bd04a803..b9b0677d 100644 --- a/fastNLP/models/sequence_modeling.py +++ b/fastNLP/models/sequence_modeling.py @@ -11,19 +11,19 @@ class SeqLabeling(BaseModel): 一个基础的Sequence labeling的模型 """ - def __init__(self, vocab_size, embed_dim, hidden_size, num_classes): + def __init__(self, init_embed, hidden_size, num_classes): """ 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 - :param int vocab_size: 词表大小。 - :param int embed_dim: embedding的维度 + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding :param int hidden_size: LSTM隐藏层的大小 :param int num_classes: 一共有多少类 """ super(SeqLabeling, self).__init__() - self.Embedding = encoder.embedding.Embedding(vocab_size, embed_dim) - self.Rnn = encoder.lstm.LSTM(embed_dim, hidden_size) + self.Embedding = encoder.embedding.Embedding(init_embed) + self.Rnn = encoder.lstm.LSTM(self.Embedding.embedding_dim, hidden_size) self.Linear = encoder.linear.Linear(hidden_size, num_classes) self.Crf = decoder.CRF.ConditionalRandomField(num_classes) self.mask = None @@ -103,24 +103,22 @@ class AdvSeqLabel: 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 """ - def __init__(self, vocab_size, embed_dim, hidden_size, num_classes, dropout=0.3, embedding=None, - id2words=None, encoding_type='bmes'): + def __init__(self, init_embed, hidden_size, num_classes, dropout=0.3, id2words=None, encoding_type='bmes'): """ - :param int vocab_size: 词表的大小 - :param int embed_dim: embedding的维度 + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding :param int hidden_size: LSTM的隐层大小 :param int num_classes: 有多少个类 :param float dropout: LSTM中以及DropOut层的drop概率 - :param numpy.ndarray embedding: 预训练的embedding,需要与指定的词表大小等一致 :param dict id2words: tag id转为其tag word的表。用于在CRF解码时防止解出非法的顺序,比如'BMES'这个标签规范中,'S' 不能出现在'B'之后。这里也支持类似与'B-NN',即'-'前为标签类型的指示,后面为具体的tag的情况。这里不但会保证 'B-NN'后面不为'S-NN'还会保证'B-NN'后面不会出现'M-xx'(任何非'M-NN'和'E-NN'的情况。) :param str encoding_type: 支持"BIO", "BMES", "BEMSO"。 """ - self.Embedding = encoder.embedding.Embedding(vocab_size, embed_dim, init_emb=embedding) - self.norm1 = torch.nn.LayerNorm(embed_dim) - self.Rnn = torch.nn.LSTM(input_size=embed_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, + self.Embedding = encoder.embedding.Embedding(init_embed) + self.norm1 = torch.nn.LayerNorm(self.Embedding.embedding_dim) + self.Rnn = torch.nn.LSTM(input_size=self.Embedding.embedding_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, bidirectional=True, batch_first=True) self.Linear1 = encoder.Linear(hidden_size * 2, hidden_size * 2 // 3) self.norm2 = torch.nn.LayerNorm(hidden_size * 2 // 3) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 7ead5c18..d4bf3d59 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -35,8 +35,7 @@ class ESIM(BaseModel): self.drop = nn.Dropout(self.dropout) self.embedding = Encoder.Embedding( - self.vocab_size, self.embed_dim, dropout=self.dropout, - init_emb=init_embedding, + (self.vocab_size, self.embed_dim), dropout=self.dropout, ) self.embedding_layer = Encoder.Linear(self.embed_dim, self.hidden_size) diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index 4f4ed551..e4fbeb28 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,5 +1,8 @@ +"""Star-Transformer 的 一个 Pytorch 实现. +""" from fastNLP.modules.encoder.star_transformer import StarTransformer from fastNLP.core.utils import seq_lens_to_masks +from ..modules.utils import get_embeddings import torch from torch import nn @@ -10,8 +13,9 @@ class StarTransEnc(nn.Module): """ 带word embedding的Star-Transformer Encoder - :param vocab_size: 词嵌入的词典大小 - :param emb_dim: 每个词嵌入的特征维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. :param num_layers: 模型层数. @@ -22,7 +26,7 @@ class StarTransEnc(nn.Module): :param emb_dropout: 词嵌入的dropout概率. :param dropout: 模型除词嵌入外的dropout概率. """ - def __init__(self, vocab_size, emb_dim, + def __init__(self, init_embed, hidden_size, num_layers, num_head, @@ -31,9 +35,10 @@ class StarTransEnc(nn.Module): emb_dropout, dropout): super(StarTransEnc, self).__init__() + self.embedding = get_embeddings(init_embed) + emb_dim = self.embedding.embedding_dim self.emb_fc = nn.Linear(emb_dim, hidden_size) self.emb_drop = nn.Dropout(emb_dropout) - self.embedding = nn.Embedding(vocab_size, emb_dim) self.encoder = StarTransformer(hidden_size=hidden_size, num_layers=num_layers, num_head=num_head, diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index c93fa1a3..098788a8 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -1,20 +1,30 @@ import torch.nn as nn +from fastNLP.modules.utils import get_embeddings +class Embedding(nn.Embedding): + """Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" -class Embedding(nn.Module): - """Embedding组件.""" - - def __init__(self, vocab_size, embed_dim, padding_idx=0, sparse=False, init_emb=None, dropout=0.0): + def __init__(self, init_embed, padding_idx=None, dropout=0.0, sparse=False, max_norm=None, norm_type=2, + scale_grad_by_freq=False): """ - :param int vocab_size: 词表大小. - :param int embed_dim: embedding维度. - :param int padding_idx: 如果碰到padding_idx则自动补0. - :param bool sparse: 如果为`True`则权重矩阵是一个sparse的矩阵. - :param torch.Tensor init_emb: 初始的embedding矩阵. - :param float dropout: dropout概率. + + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding + :param None,int padding_idx: 该index的Embedding将一直为0. + :param float dropout: 对Embedding的输出的dropout。 + :param bool sparse: 如果为True,则对Embedding的梯度将是sparse的,参考Pytorch Embedding获取更多信息。 + :param None,float max_norm: 每个vector最大的norm能为多大 + :param int norm_type: norm的类型 + :param bool scale_grad_by_freq: 如果为True,将会把梯度除以这个词出现的次数. """ - super(Embedding, self).__init__() - self.embed = nn.Embedding(vocab_size, embed_dim, padding_idx, sparse=sparse, _weight=init_emb) + embed = get_embeddings(init_embed) + num_embeddings, embedding_dim = embed.weight.size() + + super().__init__(num_embeddings, embedding_dim, padding_idx=padding_idx, + max_norm=max_norm, norm_type=norm_type, scale_grad_by_freq=scale_grad_by_freq, + sparse=sparse, _weight=embed.weight.data) + del embed + self.dropout = nn.Dropout(dropout) def forward(self, x): @@ -22,5 +32,5 @@ class Embedding(nn.Module): :param torch.LongTensor x: [batch, seq_len] :return: torch.Tensor : [batch, seq_len, embed_dim] """ - x = self.embed(x) + x = super().forward(x) return self.dropout(x) diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index 9ab8e273..cff39c84 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -1,3 +1,6 @@ +"""轻量封装的 Pytorch LSTM 模块. +可在 forward 时传入序列的长度, 自动对padding做合适的处理. +""" import torch import torch.nn as nn import torch.nn.utils.rnn as rnn @@ -35,8 +38,8 @@ class LSTM(nn.Module): :param h0: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :param c0: [batch, hidden_size] 初始Cell状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :return (output, ht) 或 output: 若 ``get_hidden=True`` [batch, seq_len, hidden_size*num_direction] 输出序列 - :和 [batch, hidden_size*num_direction] 最后时刻隐状态. - :若 ``get_hidden=False`` 仅返回输出序列. + 和 [batch, hidden_size*num_direction] 最后时刻隐状态. + 若 ``get_hidden=False`` 仅返回输出序列. """ if h0 is not None and c0 is not None: hx = (h0, c0) diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 034cfa96..42662804 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -1,3 +1,5 @@ +"""Star-Transformer 的encoder部分的 Pytorch 实现 +""" import torch from torch import nn from torch.nn import functional as F diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index d63aa6e7..89ab44d9 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -1,3 +1,5 @@ +"""Variational RNN 的 Pytorch 实现 +""" import torch import torch.nn as nn from torch.nn.utils.rnn import PackedSequence, pack_padded_sequence, pad_packed_sequence @@ -28,11 +30,11 @@ class VarRnnCellWrapper(nn.Module): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] :param hidden: for LSTM, tuple of (h_0, c_0), [batch_size, hidden_size] - :for other RNN, h_0, [batch_size, hidden_size] + for other RNN, h_0, [batch_size, hidden_size] :param mask_x: [batch_size, input_size] dropout mask for input :param mask_h: [batch_size, hidden_size] dropout mask for hidden :return PackedSequence output: [seq_len, bacth_size, hidden_size] - :hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] + hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ def get_hi(hi, h0, size): @@ -95,7 +97,7 @@ class VarRNNBase(nn.Module): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` @@ -138,7 +140,7 @@ class VarRNNBase(nn.Module): :param x: [batch, seq_len, input_size] 输入序列 :param hx: [batch, hidden_size] 初始隐状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :return (output, ht): [batch, seq_len, hidden_size*num_direction] 输出序列 - :和 [batch, hidden_size*num_direction] 最后时刻隐状态 + 和 [batch, hidden_size*num_direction] 最后时刻隐状态 """ is_lstm = self.is_lstm is_packed = isinstance(x, PackedSequence) @@ -193,7 +195,6 @@ class VarRNNBase(nn.Module): return output, hidden - class VarLSTM(VarRNNBase): """Variational Dropout LSTM. @@ -202,7 +203,7 @@ class VarLSTM(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` @@ -211,6 +212,9 @@ class VarLSTM(VarRNNBase): def __init__(self, *args, **kwargs): super(VarLSTM, self).__init__(mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarLSTM, self).forward(x, hx) + class VarRNN(VarRNNBase): """Variational Dropout RNN. @@ -220,7 +224,7 @@ class VarRNN(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` @@ -229,6 +233,8 @@ class VarRNN(VarRNNBase): def __init__(self, *args, **kwargs): super(VarRNN, self).__init__(mode="RNN", Cell=nn.RNNCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarRNN, self).forward(x, hx) class VarGRU(VarRNNBase): """Variational Dropout GRU. @@ -238,7 +244,7 @@ class VarGRU(VarRNNBase): :param num_layers: rnn的层数. Default: 1 :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 - :(batch, seq, feature). Default: ``False`` + (batch, seq, feature). Default: ``False`` :param input_dropout: 对输入的dropout概率. Default: 0 :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` @@ -247,6 +253,9 @@ class VarGRU(VarRNNBase): def __init__(self, *args, **kwargs): super(VarGRU, self).__init__(mode="GRU", Cell=nn.GRUCell, *args, **kwargs) + def forward(self, x, hx=None): + return super(VarGRU, self).forward(x, hx) + # if __name__ == '__main__': # x = torch.Tensor([[1,2,3], [4,5,0], [6,0,0]])[:,:,None] * 0.1 # mask = (x != 0).float().view(3, -1) diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 4ae15b18..c6d8be9d 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -1,3 +1,4 @@ +import numpy as np import torch import torch.nn as nn import torch.nn.init as init @@ -88,3 +89,25 @@ def seq_mask(seq_len, max_len): seq_len = seq_len.view(-1, 1).long() # [batch_size, 1] seq_range = torch.arange(start=0, end=max_len, dtype=torch.long, device=seq_len.device).view(1, -1) # [1, max_len] return torch.gt(seq_len, seq_range) # [batch_size, max_len] + + +def get_embeddings(init_embed): + """得到词嵌入 + + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding + :return nn.Embedding embeddings: + """ + if isinstance(init_embed, tuple): + res = nn.Embedding(num_embeddings=init_embed[0], embedding_dim=init_embed[1]) + elif isinstance(init_embed, nn.Embedding): + res = init_embed + elif isinstance(init_embed, torch.Tensor): + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + elif isinstance(init_embed, np.ndarray): + init_embed = torch.tensor(init_embed, dtype=torch.float32) + res = nn.Embedding.from_pretrained(init_embed, freeze=False) + else: + raise TypeError('invalid init_embed type: {}'.format((type(init_embed)))) + return res diff --git a/reproduction/LSTM+self_attention_sentiment_analysis/main.py b/reproduction/LSTM+self_attention_sentiment_analysis/main.py index ff2d7a67..4ca5388f 100644 --- a/reproduction/LSTM+self_attention_sentiment_analysis/main.py +++ b/reproduction/LSTM+self_attention_sentiment_analysis/main.py @@ -42,7 +42,7 @@ train_data, dev_data = preprocess.run(train_data, dev_data) class SELF_ATTENTION_YELP_CLASSIFICATION(BaseModel): def __init__(self, args=None): super(SELF_ATTENTION_YELP_CLASSIFICATION,self).__init__() - self.embedding = Embedding(len(word2index) ,embeding_size , init_emb= None ) + self.embedding = Embedding((len(word2index) ,embeding_size)) self.lstm = LSTM(input_size=embeding_size, hidden_size=lstm_hidden_size, bidirectional=True) self.attention = SelfAttention(lstm_hidden_size * 2 ,dim =attention_unit ,num_vec=attention_hops) self.mlp = MLP(size_layer=[lstm_hidden_size * 2*attention_hops ,nfc ,class_num ]) diff --git a/requirements.txt b/requirements.txt index 931ca285..d763ea1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -numpy>=1.14.2 +numpy torch>=0.4.0 tensorboardX -tqdm>=4.28.1 -nltk>=3.4.1 \ No newline at end of file +tqdm +nltk \ No newline at end of file diff --git a/test/api/test_processor.py b/test/api/test_processor.py index d0c27c40..9611e458 100644 --- a/test/api/test_processor.py +++ b/test/api/test_processor.py @@ -59,7 +59,7 @@ class TestProcessor(unittest.TestCase): def test_ModelProcessor(self): from fastNLP.models.cnn_text_classification import CNNText - model = CNNText(100, 100, 5) + model = CNNText((100, 100), 5) ins_list = [] for _ in range(64): seq_len = np.random.randint(5, 30) diff --git a/test/test_tutorials.py b/test/test_tutorials.py index a1d47dde..8c0e37bf 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -70,7 +70,7 @@ class TestTutorial(unittest.TestCase): break from fastNLP.models import CNNText - model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) + model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1) from fastNLP import Trainer from copy import deepcopy @@ -145,13 +145,15 @@ class TestTutorial(unittest.TestCase): is_input=True) from fastNLP.models import CNNText - model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) + model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1) + + from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric, Adam - from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, loss=CrossEntropyLoss(), + optimizer= Adam(), metrics=AccuracyMetric(target='label_seq') ) trainer.train() @@ -405,8 +407,7 @@ class TestTutorial(unittest.TestCase): # 另一个例子:加载CNN文本分类模型 from fastNLP.models import CNNText - cnn_text_model = CNNText(vocab_size=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) - cnn_text_model + cnn_text_model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1) from fastNLP import CrossEntropyLoss from fastNLP import Adam @@ -421,7 +422,6 @@ class TestTutorial(unittest.TestCase): print_every=-1, validate_every=-1, dev_data=dev_data, - use_cuda=False, optimizer=Adam(lr=1e-3, weight_decay=0), check_code_level=-1, metric_key='acc', From ded4228f93d1c90ac62b77ee74a05b657b37ec1b Mon Sep 17 00:00:00 2001 From: yh_cc Date: Fri, 26 Apr 2019 22:34:05 +0800 Subject: [PATCH 064/173] =?UTF-8?q?1.=E5=A2=9E=E5=8A=A0=E5=AF=B9Trainer?= =?UTF-8?q?=E5=92=8CTester=E7=9A=84=E5=A4=9A=E5=8D=A1=E6=94=AF=E6=8C=81;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 8 ++-- fastNLP/core/tester.py | 29 ++++++++----- fastNLP/core/trainer.py | 51 +++++++++++++--------- fastNLP/core/utils.py | 84 ++++++++++++++++++++++++++---------- test/core/test_metrics.py | 2 +- test/core/test_utils.py | 89 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 205 insertions(+), 58 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 206904ca..938a67be 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -108,8 +108,8 @@ class MetricBase(object): 如果kwargs是self.evaluate的参数,则不会检测 - self.evaluate将计算一个批次(batch)的评价指标,并累计 - self.get_metric将统计当前的评价指标并返回评价结果 + self.evaluate将计算一个批次(batch)的评价指标,并累计。 没有返回值 + self.get_metric将统计当前的评价指标并返回评价结果, 返回值需要是一个dict, key是指标名称,value是指标的值 """ def __init__(self): @@ -302,7 +302,7 @@ class AccuracyMetric(MetricBase): if seq_len is not None and not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(seq_lens)}.") + f"got {type(seq_len)}.") if seq_len is not None: masks = seq_lens_to_masks(seq_lens=seq_len) @@ -320,7 +320,7 @@ class AccuracyMetric(MetricBase): target = target.to(pred) if masks is not None: - self.acc_count += torch.sum(torch.eq(pred, target).masked_fill(masks, 0)).item() + self.acc_count += torch.sum(torch.eq(pred, target).masked_fill(masks.eq(0), 0)).item() self.total += torch.sum(masks).item() else: self.acc_count += torch.sum(torch.eq(pred, target)).item() diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 6e3f98b5..c2aae37b 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -10,7 +10,8 @@ from fastNLP.core.utils import _build_args from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device from fastNLP.core.utils import _get_func_signature -from fastNLP.core.utils import _get_device +from fastNLP.core.utils import _get_model_device +from fastNLP.core.utils import _move_model_to_device class Tester(object): @@ -57,9 +58,20 @@ class Tester(object): :param torch.nn.module model: 使用的模型 :param MetricBase metrics: 一个Metric或者一个列表的metric对象 :param int batch_size: evaluation时使用的batch_size有多大。 - :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 - 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, - 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 + :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 + 的计算位置进行管理。支持以下的输入: + + 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; + + 2. torch.device:将模型装载到torch.device上。 + + 3. int: 将使用device_id为该值的gpu进行训练 + + 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 + + 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 """ @@ -74,16 +86,10 @@ class Tester(object): self.metrics = _prepare_metrics(metrics) self.data = data - self.device = _get_device(device, check_exist=False) + self._model = _move_model_to_device(model, device=device) self.batch_size = batch_size self.verbose = verbose - if self.device is not None: - self._model = model.to(self.device) - else: - self._model = model - self._model_device = model.parameters().__next__().device - # check predict if hasattr(self._model, 'predict'): self._predict_func = self._model.predict @@ -101,6 +107,7 @@ class Tester(object): 一个AccuracyMetric的例子为{'AccuracyMetric': {'acc': 1.0}}。 """ # turn on the testing mode; clean up the history + self._model_device = _get_model_device(self._model) network = self._model self._mode(network, is_test=True) data_iterator = Batch(self.data, self.batch_size, sampler=SequentialSampler(), as_numpy=False) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 48733652..b6c282b4 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -321,11 +321,12 @@ from fastNLP.core.utils import _check_forward_error from fastNLP.core.utils import _check_loss_evaluate from fastNLP.core.utils import _move_dict_value_to_device from fastNLP.core.utils import _get_func_signature -from fastNLP.core.utils import _get_device +from fastNLP.core.utils import _get_model_device from fastNLP.core.optimizer import Optimizer +from fastNLP.core.utils import _move_model_to_device class Trainer(object): - def __init__(self, train_data, model, optimizer, loss=None, + def __init__(self, train_data, model, optimizer=None, loss=None, batch_size=32, sampler=None, update_every=1, n_epochs=10, print_every=5, dev_data=None, metrics=None, metric_key=None, @@ -336,7 +337,7 @@ class Trainer(object): """ :param DataSet train_data: 训练集 :param nn.modules model: 待训练的模型 - :param torch.optim.Optimizer,None optimizer: 优化器。如果为None,则Trainer不会更新模型,请确保已在callback中进行了更新。 + :param torch.optim.Optimizer,None optimizer: 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器 :param int batch_size: 训练和验证的时候的batch大小。 :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。当loss为None时,默认使用 LossInForward_ 。 :param Sampler sampler: Batch数据生成的顺序。详见 Sampler_ 。如果为None,默认使用 RandomSampler_ 。 @@ -354,12 +355,23 @@ class Trainer(object): :param int validate_every: 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有 效。 :param str,None save_path: 将模型保存路径。如果为None,则不保存模型。如果dev_data为None,则保存最后一次迭代的模 - 型。保存的时候不仅保存了参数,还保存了模型结构。 + 型。保存的时候不仅保存了参数,还保存了模型结构。即便使用DataParallel,这里也只保存模型。 :param prefetch: bool, 是否使用额外的进程对产生batch数据。理论上会使得Batch迭代更快。 :param bool use_tqdm: 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。 - :param str,torch.device,None device: 将模型load到哪个设备。默认为None,即Trainer不对模型的计算位置进行管理。支持 - 以下的输入str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, - 可见的第二个GPU中; torch.device,将模型装载到torch.device上。 + :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 + 的计算位置进行管理。支持以下的输入: + + 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; + + 2. torch.device:将模型装载到torch.device上。 + + 3. int: 将使用device_id为该值的gpu进行训练 + + 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 + + 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + :param list(callbacks) callbacks: 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 通过callback机制实现。 可使用的callback参见 Callback_ 。 :param int check_code_level: 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, @@ -432,17 +444,15 @@ class Trainer(object): self.n_steps = (len(self.train_data) // self.batch_size + int( len(self.train_data) % self.batch_size != 0)) * self.n_epochs - check_exist = check_code_level>-1 - self.device = _get_device(device, check_exist=check_exist) + # 是否一开始就是DataParallel的。 + self.model = _move_model_to_device(self.model, device=device) if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer elif isinstance(optimizer, Optimizer): self.optimizer = optimizer.construct_from_pytorch(model.parameters()) elif optimizer is None: - warnings.warn("The optimizer is set to None, Trainer will update your model. Make sure you update the model" - " in the callback.") - self.optimizer = None + self.optimizer = torch.optim.Adam(model.parameters(), lr=4e-3) else: raise TypeError("optimizer can only be torch.optim.Optimizer type, not {}.".format(type(optimizer))) @@ -455,7 +465,7 @@ class Trainer(object): data=self.dev_data, metrics=self.metrics, batch_size=self.batch_size, - device=self.device, + device=None, # 由上面的部分处理device verbose=0) self.step = 0 @@ -486,11 +496,9 @@ class Trainer(object): results['seconds'] = 0. return results try: - if self.device is not None: - self.model = self.model.to(self.device) - self._model_device = self.model.parameters().__next__().device + self._model_device = _get_model_device(self.model) self._mode(self.model, is_test=False) - + self._load_best_model = load_best_model self.start_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) start_time = time.time() print("training epochs started " + self.start_time, flush=True) @@ -605,7 +613,7 @@ class Trainer(object): if self.save_path is not None: self._save_model(self.model, "best_" + "_".join([self.model.__class__.__name__, self.metric_key, self.start_time])) - else: + elif self._load_best_model: self._best_model_states = {name: param.cpu().clone() for name, param in self.model.named_parameters()} self.best_dev_perf = res self.best_dev_epoch = epoch @@ -672,6 +680,8 @@ class Trainer(object): model_path = os.path.join(self.save_path, model_name) if not os.path.exists(self.save_path): os.makedirs(self.save_path, exist_ok=True) + if isinstance(model, nn.DataParallel): + model = model.module if only_param: state_dict = model.state_dict() for key in state_dict: @@ -690,7 +700,10 @@ class Trainer(object): states = torch.load(model_path) else: states = torch.load(model_path).state_dict() - model.load_state_dict(states) + if isinstance(model, nn.DataParallel): + model.module.load_state_dict(states) + else: + model.load_state_dict(states) elif hasattr(self, "_best_model_states"): model.load_state_dict(self._best_model_states) else: diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index f34092df..efb4faa7 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -168,33 +168,73 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): # else: # return False -def _get_device(device, check_exist=False): +def _move_model_to_device(model, device): """ - 传入一个device,返回None或者torch.device。当不为None时,且被设置为使用gpu, 但机器没有gpu时,会返回torch.device('cpu') + 将model移动到device - :param str,None,torch.device device: str, None或者torch.device。 - :param bool check_exist: 检查该device是否存在,不存在的话报错 - :return: None,torch.device - """ - if device is not None: - if isinstance(device, str): - device = torch.device(device) - elif isinstance(device, torch.device): - device = device - else: - raise ValueError("device does not support {} type.".format(type(device))) + :param model: torch.nn.DataParallel or torch.nn.Module. 当为torch.nn.DataParallel, 则只是调用一次cuda。device必须为 + None。 + :param str,int,torch.device,list(int),list(torch.device) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 + 的计算位置进行管理。支持以下的输入: - if device.type=='cuda' and not torch.cuda.is_available(): - device = torch.device('cpu') + 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; - if check_exist: - tensor = torch.zeros(0).to(device) - tensor = tensor.to('cpu') - del tensor - else: - device = None + 2. torch.device:将模型装载到torch.device上。 + + 3. int: 将使用device_id为该值的gpu进行训练 - return device + 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 + + 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + + :return: torch.nn.DataParallel or torch.nn.Module + """ + if isinstance(model, torch.nn.parallel.DistributedDataParallel): + raise RuntimeError("model of `torch.nn.parallel.DistributedDataParallel` is not supported right now.") + + if not torch.cuda.is_available() and (device!='cpu' or (isinstance(device, torch.device) and device.type!='cpu')): + raise ValueError("There is no usable gpu. set `device` as `cpu`.") + + if device is None: + if isinstance(model, torch.nn.DataParallel): + model.cuda() + return model + + if isinstance(model, torch.nn.DataParallel): + raise RuntimeError("When model is `torch.nn.DataParallel`, the device has to be `None`.") + + if isinstance(device, int): + assert device>-1, "device can only be positive integer" + assert torch.cuda.device_count()>device, "Only has {} gpus, cannot use device {}.".format(torch.cuda.device_count(), + device) + device = torch.device('cuda:{}'.format(device)) + elif isinstance(device, str): + device = torch.device(device) + if device.type == 'cuda' and device.index is not None: + assert device.index-1, "Only positive device id allowed." + if len(device)>1: + output_device = device[0] + model = nn.DataParallel(model, device_ids=device, output_device=output_device) + device = torch.device(device[0]) + else: + raise TypeError("Unsupported device type.") + model = model.to(device) + return model def _get_model_device(model): diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index a0e8f1a5..3f37c495 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -123,7 +123,7 @@ class TestAccuracyMetric(unittest.TestCase): # (10) check _fast_metric try: metric = AccuracyMetric() - pred_dict = {"predictions": torch.zeros(4, 3, 2), "masks": torch.zeros(4, 3)} + pred_dict = {"predictions": torch.zeros(4, 3, 2), "seq_len": torch.ones(3)*3} target_dict = {'targets': torch.zeros(4, 3)} metric(pred_dict=pred_dict, target_dict=target_dict) self.assertDictEqual(metric.get_metric(), {'acc': 1}) diff --git a/test/core/test_utils.py b/test/core/test_utils.py index 11bb0f22..33202364 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -7,6 +7,94 @@ from fastNLP import DataSet from fastNLP import Instance import time import os +import torch +from torch import nn +from fastNLP.core.utils import _move_model_to_device, _get_model_device + +class Model(nn.Module): + def __init__(self): + super().__init__() + self.param = nn.Parameter(torch.zeros(0)) + +class TestMoveModelDeivce(unittest.TestCase): + def test_case1(self): + # 测试str + model = Model() + model = _move_model_to_device(model, 'cpu') + assert model.param.device == torch.device('cpu') + # 测试不存在的device报错 + with self.assertRaises(Exception): + _move_model_to_device(model, 'cpuu') + # 测试gpu + if torch.cuda.is_available(): + model = _move_model_to_device(model, 'cuda') + assert model.param.is_cuda + model = _move_model_to_device(model, 'cuda:0') + assert model.param.device == torch.device('cuda:0') + with self.assertRaises(Exception): + _move_model_to_device(model, 'cuda:1000') + + def test_case2(self): + # 测试使用int初始化 + model = Model() + if torch.cuda.is_available(): + model = _move_model_to_device(model, 0) + assert model.param.device == torch.device('cuda:0') + assert model.param.device==torch.device('cuda:0'), "The model should be in " + with self.assertRaises(Exception): + _move_model_to_device(model, 100) + with self.assertRaises(Exception): + _move_model_to_device(model, -1) + + def test_case3(self): + # 测试None + model = Model() + device = _get_model_device(model) + model = _move_model_to_device(model, None) + assert device==_get_model_device(model), "The device should not change." + if torch.cuda.is_available(): + model.cuda() + device = _get_model_device(model) + model = _move_model_to_device(model, None) + assert device==_get_model_device(model), "The device should not change." + + model = nn.DataParallel(model, device_ids=[0]) + _move_model_to_device(model, None) + with self.assertRaises(Exception): + _move_model_to_device(model, 'cpu') + + def test_case4(self): + # 测试传入list的内容 + model = Model() + device = ['cpu'] + with self.assertRaises(Exception): + _move_model_to_device(model, device) + if torch.cuda.is_available(): + device = [0] + _model = _move_model_to_device(model, device) + assert isinstance(_model, nn.DataParallel) + device = [torch.device('cuda:0'), torch.device('cuda:0')] + with self.assertRaises(Exception): + _model = _move_model_to_device(model, device) + if torch.cuda.device_count()>1: + device = [0, 1] + _model = _move_model_to_device(model, device) + assert isinstance(_model, nn.DataParallel) + device = ['cuda', 'cuda:1'] + with self.assertRaises(Exception): + _move_model_to_device(model, device) + + def test_case5(self): + # torch.device() + device = torch.device('cpu') + model = Model() + _move_model_to_device(model, device) + device = torch.device('cuda') + model = _move_model_to_device(model, device) + assert model.param.device == torch.device('cuda:0') + with self.assertRaises(Exception): + _move_model_to_device(model, torch.device('cuda:100')) + @cache_results('test/demo1.pkl') def process_data_1(embed_file, cws_train): @@ -20,7 +108,6 @@ def process_data_1(embed_file, cws_train): d.append(Instance(raw=line)) return embed, vocab, d - class TestCache(unittest.TestCase): def test_cache_save(self): try: From 799d4dbc68bd72ea4651de4a98a888aa883ef227 Mon Sep 17 00:00:00 2001 From: yunfan Date: Sat, 27 Apr 2019 23:19:13 +0800 Subject: [PATCH 065/173] - add test - fix jsonloader --- fastNLP/io/dataset_loader.py | 5 ++++- test/core/test_batch.py | 3 +-- test/core/test_metrics.py | 13 +++++++++++++ test/models/test_biaffine_parser.py | 4 ++-- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index bb5e2f64..a9babce5 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -330,7 +330,10 @@ class JsonLoader(DataSetLoader): def load(self, path): ds = DataSet() for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): - ins = {self.fields[k]:v for k,v in d.items()} + if self.fields: + ins = {self.fields[k]:v for k,v in d.items()} + else: + ins = d ds.append(Instance(**ins)) return ds diff --git a/test/core/test_batch.py b/test/core/test_batch.py index 2bf2dcea..072f7c83 100644 --- a/test/core/test_batch.py +++ b/test/core/test_batch.py @@ -142,13 +142,12 @@ class TestCase1(unittest.TestCase): def test_sequential_batch(self): batch_size = 32 - pause_seconds = 0.01 num_samples = 1000 dataset = generate_fake_dataset(num_samples) batch = Batch(dataset, batch_size=batch_size, sampler=SequentialSampler()) for batch_x, batch_y in batch: - time.sleep(pause_seconds) + pass """ def test_multi_workers_batch(self): diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index 3f37c495..d4422ec4 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -132,6 +132,19 @@ class TestAccuracyMetric(unittest.TestCase): return self.assertTrue(True, False), "No exception catches." + def test_seq_len(self): + N = 256 + seq_len = torch.zeros(N).long() + seq_len[0] = 2 + pred = {'pred': torch.ones(N, 2)} + target = {'target': torch.ones(N, 2), 'seq_len': seq_len} + metric = AccuracyMetric() + metric(pred_dict=pred, target_dict=target) + self.assertDictEqual(metric.get_metric(), {'acc': 1.}) + seq_len[1:] = 1 + metric(pred_dict=pred, target_dict=target) + self.assertDictEqual(metric.get_metric(), {'acc': 1.}) + class SpanF1PreRecMetric(unittest.TestCase): def test_case1(self): from fastNLP.core.metrics import _bmes_tag_to_spans diff --git a/test/models/test_biaffine_parser.py b/test/models/test_biaffine_parser.py index 5d6c2102..918f0fd9 100644 --- a/test/models/test_biaffine_parser.py +++ b/test/models/test_biaffine_parser.py @@ -77,13 +77,13 @@ def init_data(): class TestBiaffineParser(unittest.TestCase): def test_train(self): ds, v1, v2, v3 = init_data() - model = BiaffineParser(word_vocab_size=len(v1), word_emb_dim=30, + model = BiaffineParser(init_embed=(len(v1), 30), pos_vocab_size=len(v2), pos_emb_dim=30, num_label=len(v3), encoder='var-lstm') trainer = fastNLP.Trainer(model=model, train_data=ds, dev_data=ds, loss=ParserLoss(), metrics=ParserMetric(), metric_key='UAS', batch_size=1, validate_every=10, - n_epochs=10, use_cuda=False, use_tqdm=False) + n_epochs=10, use_tqdm=False) trainer.train(load_best_model=False) From bb01904fc8de29047abc8eff1b1fbc67029df0c8 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 28 Apr 2019 18:01:35 +0800 Subject: [PATCH 066/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/api/api.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index c4c21832..88f1755a 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -1,40 +1,5 @@ """ api.api的介绍文档 - 直接缩进会把上面的文字变成标题 - -空行缩进的写法比较合理 - - 比较合理 - -*这里是斜体内容* - -**这里是粗体内容** - -数学公式块 - -.. math:: - E = mc^2 - -.. note:: - 注解型提示。 - -.. warning:: - 警告型提示。 - -.. seealso:: - `参考与超链接 `_ - -普通代码块需要空一行, Example:: - - from fitlog import fitlog - fitlog.commit() - -普通下标和上标: - -H\ :sub:`2`\ O - -E = mc\ :sup:`2` - """ import warnings From 3465d2a08f3cc8c3ebf27797d314f2255f0b32f3 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 28 Apr 2019 20:13:51 +0800 Subject: [PATCH 067/173] =?UTF-8?q?=E7=BB=99=20dataset=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E4=BA=86get=5Ffield=5Fnames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 00f8ce04..82288221 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -501,6 +501,14 @@ class DataSet(object): """ return self.field_arrays + + def get_field_names(self)->list: + """返回一个list,包含所有 field 的名字 + + :return: list: + """ + return sorted(self.field_arrays.keys()) + def get_length(self): """获取DataSet的元素数量 From c077107555ffa9dee9be26138f47f223e99f2b76 Mon Sep 17 00:00:00 2001 From: yh Date: Mon, 29 Apr 2019 14:43:44 +0800 Subject: [PATCH 068/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/trainer.py | 1 - fastNLP/core/utils.py | 7 ++++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index b6c282b4..253ae46d 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -444,7 +444,6 @@ class Trainer(object): self.n_steps = (len(self.train_data) // self.batch_size + int( len(self.train_data) % self.batch_size != 0)) * self.n_epochs - # 是否一开始就是DataParallel的。 self.model = _move_model_to_device(self.model, device=device) if isinstance(optimizer, torch.optim.Optimizer): diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index efb4faa7..cc9e8164 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -193,13 +193,14 @@ def _move_model_to_device(model, device): if isinstance(model, torch.nn.parallel.DistributedDataParallel): raise RuntimeError("model of `torch.nn.parallel.DistributedDataParallel` is not supported right now.") - if not torch.cuda.is_available() and (device!='cpu' or (isinstance(device, torch.device) and device.type!='cpu')): - raise ValueError("There is no usable gpu. set `device` as `cpu`.") - if device is None: if isinstance(model, torch.nn.DataParallel): model.cuda() return model + else: + if not torch.cuda.is_available() and ( + device != 'cpu' or (isinstance(device, torch.device) and device.type != 'cpu')): + raise ValueError("There is no usable gpu. set `device` as `cpu`.") if isinstance(model, torch.nn.DataParallel): raise RuntimeError("When model is `torch.nn.DataParallel`, the device has to be `None`.") From e864aecb03b7d0a2fca0b97da23d06eb907066d1 Mon Sep 17 00:00:00 2001 From: yunfan Date: Mon, 29 Apr 2019 12:31:14 +0800 Subject: [PATCH 069/173] - add Const - fix bugs --- fastNLP/core/const.py | 46 ++++++++++++++++++++++++++++++ fastNLP/io/dataset_loader.py | 10 +++---- fastNLP/models/star_transformer.py | 23 ++++++++------- test/core/test_callbacks.py | 2 +- test/io/test_dataset_loader.py | 7 ++++- 5 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 fastNLP/core/const.py diff --git a/fastNLP/core/const.py b/fastNLP/core/const.py new file mode 100644 index 00000000..56447395 --- /dev/null +++ b/fastNLP/core/const.py @@ -0,0 +1,46 @@ +class Const(): + """fastNLP中field命名常量。 + 具体列表:: + + INPUT 模型的序列输入 words(复数words1, words2) + CHAR_INPUT 模型character输入 chars(复数chars1, chars2) + INPUT_LEN 序列长度 seq_len(复数seq_len1,seq_len2) + OUTPUT 模型输出 pred(复数pred1, pred2) + TARGET 真实目标 target(复数target1,target2) + + """ + INPUT = 'words' + CHAR_INPUT = 'chars' + INPUT_LEN = 'seq_len' + OUTPUT = 'pred' + TARGET = 'target' + + @staticmethod + def INPUTS(i): + """得到第 i 个 ``INPUT`` 的命名""" + i = int(i) + 1 + return Const.INPUT + str(i) + + @staticmethod + def CHAR_INPUTS(i): + """得到第 i 个 ``CHAR_INPUT`` 的命名""" + i = int(i) + 1 + return Const.CHAR_INPUT + str(i) + + @staticmethod + def INPUT_LENS(i): + """得到第 i 个 ``INPUT_LEN`` 的命名""" + i = int(i) + 1 + return Const.INPUT_LEN + str(i) + + @staticmethod + def OUTPUTS(i): + """得到第 i 个 ``OUTPUT`` 的命名""" + i = int(i) + 1 + return Const.OUTPUT + str(i) + + @staticmethod + def TARGETS(i): + """得到第 i 个 ``TARGET`` 的命名""" + i = int(i) + 1 + return Const.TARGET + str(i) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index a9babce5..0fcdcf29 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -193,9 +193,9 @@ class ConllLoader(DataSetLoader): :param headers: 每一列数据的名称,需为List or Tuple of str。``header`` 与 ``indexs`` 一一对应 :param indexs: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` - :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``True`` + :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ - def __init__(self, headers, indexs=None, dropna=True): + def __init__(self, headers, indexs=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): raise TypeError('invalid headers: {}, should be list of strings'.format(headers)) @@ -314,7 +314,7 @@ class JsonLoader(DataSetLoader): `value`也可为 ``None`` , 这时读入后的`field_name`与json对象对应属性同名 ``fields`` 可为 ``None`` , 这时,json对象所有属性都保存在DataSet中. Default: ``None`` :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . - Default: ``True`` + Default: ``False`` """ def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() @@ -375,9 +375,9 @@ class CSVLoader(DataSetLoader): 若为 ``None`` ,则将读入文件的第一行视作 ``headers`` . Default: ``None`` :param str sep: CSV文件中列与列之间的分隔符. Default: "," :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . - Default: ``True`` + Default: ``False`` """ - def __init__(self, headers=None, sep=",", dropna=True): + def __init__(self, headers=None, sep=",", dropna=False): self.headers = headers self.sep = sep self.dropna = dropna diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index e4fbeb28..f0c6f33f 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,8 +1,9 @@ """Star-Transformer 的 一个 Pytorch 实现. """ -from fastNLP.modules.encoder.star_transformer import StarTransformer -from fastNLP.core.utils import seq_lens_to_masks +from ..modules.encoder.star_transformer import StarTransformer +from ..core.utils import seq_lens_to_masks from ..modules.utils import get_embeddings +from ..core.const import Const import torch from torch import nn @@ -139,7 +140,7 @@ class STSeqLabel(nn.Module): nodes, _ = self.enc(words, mask) output = self.cls(nodes) output = output.transpose(1,2) # make hidden to be dim 1 - return {'output': output} # [bsz, n_cls, seq_len] + return {Const.OUTPUT: output} # [bsz, n_cls, seq_len] def predict(self, words, seq_len): """ @@ -149,8 +150,8 @@ class STSeqLabel(nn.Module): :return output: [batch, seq_len] 输出序列中每个元素的分类 """ y = self.forward(words, seq_len) - _, pred = y['output'].max(1) - return {'output': pred} + _, pred = y[Const.OUTPUT].max(1) + return {Const.OUTPUT: pred} class STSeqCls(nn.Module): @@ -201,7 +202,7 @@ class STSeqCls(nn.Module): nodes, relay = self.enc(words, mask) y = 0.5 * (relay + nodes.max(1)[0]) output = self.cls(y) # [bsz, n_cls] - return {'output': output} + return {Const.OUTPUT: output} def predict(self, words, seq_len): """ @@ -211,8 +212,8 @@ class STSeqCls(nn.Module): :return output: [batch, num_cls] 输出序列的分类 """ y = self.forward(words, seq_len) - _, pred = y['output'].max(1) - return {'output': pred} + _, pred = y[Const.OUTPUT].max(1) + return {Const.OUTPUT: pred} class STNLICls(nn.Module): @@ -269,7 +270,7 @@ class STNLICls(nn.Module): y1 = enc(words1, mask1) y2 = enc(words2, mask2) output = self.cls(y1, y2) # [bsz, n_cls] - return {'output': output} + return {Const.OUTPUT: output} def predict(self, words1, words2, seq_len1, seq_len2): """ @@ -281,5 +282,5 @@ class STNLICls(nn.Module): :return output: [batch, num_cls] 输出分类的概率 """ y = self.forward(words1, words2, seq_len1, seq_len2) - _, pred = y['output'].max(1) - return {'output': pred} + _, pred = y[Const.OUTPUT].max(1) + return {Const.OUTPUT: pred} diff --git a/test/core/test_callbacks.py b/test/core/test_callbacks.py index 3329e7a1..cf3e2fff 100644 --- a/test/core/test_callbacks.py +++ b/test/core/test_callbacks.py @@ -3,7 +3,7 @@ import unittest import numpy as np import torch -from fastNLP.core.callback import EchoCallback, EarlyStopCallback, GradientClipCallback, LRScheduler, ControlC, \ +from fastNLP.core.callback import EarlyStopCallback, GradientClipCallback, LRScheduler, ControlC, \ LRFinder, \ TensorboardCallback from fastNLP.core.dataset import DataSet diff --git a/test/io/test_dataset_loader.py b/test/io/test_dataset_loader.py index 97379a7d..2e367567 100644 --- a/test/io/test_dataset_loader.py +++ b/test/io/test_dataset_loader.py @@ -1,7 +1,7 @@ import unittest from fastNLP.io.dataset_loader import Conll2003Loader, PeopleDailyCorpusLoader, \ - CSVLoader, SNLILoader + CSVLoader, SNLILoader, JsonLoader class TestDatasetLoader(unittest.TestCase): @@ -24,3 +24,8 @@ class TestDatasetLoader(unittest.TestCase): def test_SNLILoader(self): ds = SNLILoader().load('test/data_for_tests/sample_snli.jsonl') assert len(ds) == 3 + + def test_JsonLoader(self): + ds = JsonLoader().load('test/data_for_tests/sample_snli.jsonl') + assert len(ds) == 3 + From 16388d56984adc09ea7a0a51233c5ac36e915206 Mon Sep 17 00:00:00 2001 From: yh Date: Mon, 29 Apr 2019 15:12:50 +0800 Subject: [PATCH 070/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E5=88=86?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/trainer.py | 4 ++-- fastNLP/core/utils.py | 6 +++--- test/core/test_utils.py | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 253ae46d..e953a58f 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -355,7 +355,7 @@ class Trainer(object): :param int validate_every: 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有 效。 :param str,None save_path: 将模型保存路径。如果为None,则不保存模型。如果dev_data为None,则保存最后一次迭代的模 - 型。保存的时候不仅保存了参数,还保存了模型结构。即便使用DataParallel,这里也只保存模型。 + 型。保存的时候不仅保存了参数,还保存了模型结构。即便使用了nn.DataParallel,这里也只保存模型。 :param prefetch: bool, 是否使用额外的进程对产生batch数据。理论上会使得Batch迭代更快。 :param bool use_tqdm: 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。 :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 @@ -366,7 +366,7 @@ class Trainer(object): 2. torch.device:将模型装载到torch.device上。 - 3. int: 将使用device_id为该值的gpu进行训练 + 3. int: 将使用该gpu进行训练 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index cc9e8164..76db790e 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -200,13 +200,13 @@ def _move_model_to_device(model, device): else: if not torch.cuda.is_available() and ( device != 'cpu' or (isinstance(device, torch.device) and device.type != 'cpu')): - raise ValueError("There is no usable gpu. set `device` as `cpu`.") + raise ValueError("There is no usable gpu. set `device` as `cpu` or `None`.") if isinstance(model, torch.nn.DataParallel): raise RuntimeError("When model is `torch.nn.DataParallel`, the device has to be `None`.") if isinstance(device, int): - assert device>-1, "device can only be positive integer" + assert device>-1, "device can only be non-negative integer" assert torch.cuda.device_count()>device, "Only has {} gpus, cannot use device {}.".format(torch.cuda.device_count(), device) device = torch.device('cuda:{}'.format(device)) @@ -227,7 +227,7 @@ def _move_model_to_device(model, device): assert list(types)[0] == int, "Only int supported for multiple devices." assert len(set(device))==len(device), "Duplicated device id found in device." for d in device: - assert d>-1, "Only positive device id allowed." + assert d>-1, "Only non-negative device id allowed." if len(device)>1: output_device = device[0] model = nn.DataParallel(model, device_ids=device, output_device=output_device) diff --git a/test/core/test_utils.py b/test/core/test_utils.py index 33202364..d676ceed 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -33,6 +33,8 @@ class TestMoveModelDeivce(unittest.TestCase): assert model.param.device == torch.device('cuda:0') with self.assertRaises(Exception): _move_model_to_device(model, 'cuda:1000') + # 测试None + model = _move_model_to_device(model, None) def test_case2(self): # 测试使用int初始化 From 4f65e17d1a9f2d4fda0af836f3d4d36811b54599 Mon Sep 17 00:00:00 2001 From: yunfan Date: Mon, 29 Apr 2019 18:53:05 +0800 Subject: [PATCH 071/173] - add model runner for easier test models - add model tests --- fastNLP/models/biaffine_parser.py | 137 +++++++++++++------------ fastNLP/models/star_transformer.py | 15 ++- test/models/__init__.py | 0 test/models/model_runner.py | 151 ++++++++++++++++++++++++++++ test/models/test_biaffine_parser.py | 111 +++++--------------- test/models/test_star_trans.py | 16 +++ 6 files changed, 268 insertions(+), 162 deletions(-) create mode 100644 test/models/__init__.py create mode 100644 test/models/model_runner.py create mode 100644 test/models/test_star_trans.py diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index f2329dca..b8d1e7a9 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -7,16 +7,17 @@ import torch from torch import nn from torch.nn import functional as F -from fastNLP.core.losses import LossFunc -from fastNLP.core.metrics import MetricBase -from fastNLP.core.utils import seq_lens_to_masks -from fastNLP.models.base_model import BaseModel -from fastNLP.modules.dropout import TimestepDropout -from fastNLP.modules.encoder.transformer import TransformerEncoder -from fastNLP.modules.encoder.variational_rnn import VarLSTM -from fastNLP.modules.utils import initial_parameter -from fastNLP.modules.utils import seq_mask -from fastNLP.modules.utils import get_embeddings +from ..core.const import Const as C +from ..core.losses import LossFunc +from ..core.metrics import MetricBase +from ..core.utils import seq_lens_to_masks +from ..modules.dropout import TimestepDropout +from ..modules.encoder.transformer import TransformerEncoder +from ..modules.encoder.variational_rnn import VarLSTM +from ..modules.utils import initial_parameter +from ..modules.utils import seq_mask +from ..modules.utils import get_embeddings +from .base_model import BaseModel def _mst(scores): """ @@ -325,21 +326,20 @@ class BiaffineParser(GraphParser): for p in m.parameters(): nn.init.normal_(p, 0, 0.1) - def forward(self, words1, words2, seq_len, gold_heads=None): + def forward(self, words1, words2, seq_len, target1=None): """模型forward阶段 :param words1: [batch_size, seq_len] 输入word序列 :param words2: [batch_size, seq_len] 输入pos序列 :param seq_len: [batch_size, seq_len] 输入序列长度 - :param gold_heads: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, + :param target1: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, 用于训练label分类器. 若为 ``None`` , 使用预测的heads输入到label分类器 Default: ``None`` :return dict: parsing结果:: - arc_pred: [batch_size, seq_len, seq_len] 边预测logits - label_pred: [batch_size, seq_len, num_label] label预测logits - mask: [batch_size, seq_len] 预测结果的mask - head_pred: [batch_size, seq_len] heads的预测结果, 在 ``gold_heads=None`` 时预测 + pred1: [batch_size, seq_len, seq_len] 边预测logits + pred2: [batch_size, seq_len, num_label] label预测logits + pred3: [batch_size, seq_len] heads的预测结果, 在 ``target1=None`` 时预测 """ # prepare embeddings batch_size, length = words1.shape @@ -365,7 +365,7 @@ class BiaffineParser(GraphParser): _, unsort_idx = torch.sort(sort_idx, dim=0, descending=False) feat = feat[unsort_idx] else: - seq_range = torch.arange(seq_len, dtype=torch.long, device=x.device)[None,:] + seq_range = torch.arange(length, dtype=torch.long, device=x.device)[None,:] x = x + self.position_emb(seq_range) feat = self.encoder(x, mask.float()) @@ -380,7 +380,7 @@ class BiaffineParser(GraphParser): arc_pred = self.arc_predictor(arc_head, arc_dep) # [N, L, L] # use gold or predicted arc to predict label - if gold_heads is None or not self.training: + if target1 is None or not self.training: # use greedy decoding in training if self.training or self.use_greedy_infer: heads = self.greedy_decoder(arc_pred, mask) @@ -389,44 +389,45 @@ class BiaffineParser(GraphParser): head_pred = heads else: assert self.training # must be training mode - if gold_heads is None: + if target1 is None: heads = self.greedy_decoder(arc_pred, mask) head_pred = heads else: head_pred = None - heads = gold_heads + heads = target1 batch_range = torch.arange(start=0, end=batch_size, dtype=torch.long, device=words1.device).unsqueeze(1) label_head = label_head[batch_range, heads].contiguous() label_pred = self.label_predictor(label_head, label_dep) # [N, L, num_label] - res_dict = {'arc_pred': arc_pred, 'label_pred': label_pred, 'mask': mask} + res_dict = {C.OUTPUTS(0): arc_pred, C.OUTPUTS(1): label_pred} if head_pred is not None: - res_dict['head_pred'] = head_pred + res_dict[C.OUTPUTS(2)] = head_pred return res_dict @staticmethod - def loss(arc_pred, label_pred, arc_true, label_true, mask): + def loss(pred1, pred2, target1, target2, seq_len): """ - Compute loss. - - :param arc_pred: [batch_size, seq_len, seq_len] 边预测logits - :param label_pred: [batch_size, seq_len, num_label] label预测logits - :param arc_true: [batch_size, seq_len] 真实边的标注 - :param label_true: [batch_size, seq_len] 真实类别的标注 - :param mask: [batch_size, seq_len] 预测结果的mask - :return: loss value + 计算parser的loss + + :param pred1: [batch_size, seq_len, seq_len] 边预测logits + :param pred2: [batch_size, seq_len, num_label] label预测logits + :param target1: [batch_size, seq_len] 真实边的标注 + :param target2: [batch_size, seq_len] 真实类别的标注 + :param seq_len: [batch_size, seq_len] 真实目标的长度 + :return loss: scalar """ - batch_size, seq_len, _ = arc_pred.shape + batch_size, length, _ = pred1.shape + mask = seq_mask(seq_len, length) flip_mask = (mask == 0) - _arc_pred = arc_pred.clone() + _arc_pred = pred1.clone() _arc_pred.masked_fill_(flip_mask.unsqueeze(1), -float('inf')) arc_logits = F.log_softmax(_arc_pred, dim=2) - label_logits = F.log_softmax(label_pred, dim=2) + label_logits = F.log_softmax(pred2, dim=2) batch_index = torch.arange(batch_size, device=arc_logits.device, dtype=torch.long).unsqueeze(1) - child_index = torch.arange(seq_len, device=arc_logits.device, dtype=torch.long).unsqueeze(0) - arc_loss = arc_logits[batch_index, child_index, arc_true] - label_loss = label_logits[batch_index, child_index, label_true] + child_index = torch.arange(length, device=arc_logits.device, dtype=torch.long).unsqueeze(0) + arc_loss = arc_logits[batch_index, child_index, target1] + label_loss = label_logits[batch_index, child_index, target2] byte_mask = flip_mask.byte() arc_loss.masked_fill_(byte_mask, 0) @@ -441,21 +442,16 @@ class BiaffineParser(GraphParser): :param words1: [batch_size, seq_len] 输入word序列 :param words2: [batch_size, seq_len] 输入pos序列 :param seq_len: [batch_size, seq_len] 输入序列长度 - :param gold_heads: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, - 用于训练label分类器. 若为 ``None`` , 使用预测的heads输入到label分类器 - Default: ``None`` :return dict: parsing结果:: - arc_pred: [batch_size, seq_len, seq_len] 边预测logits - label_pred: [batch_size, seq_len, num_label] label预测logits - mask: [batch_size, seq_len] 预测结果的mask - head_pred: [batch_size, seq_len] heads的预测结果, 在 ``gold_heads=None`` 时预测 + pred1: [batch_size, seq_len] heads的预测结果 + pred2: [batch_size, seq_len, num_label] label预测logits """ res = self(words1, words2, seq_len) output = {} - output['arc_pred'] = res.pop('head_pred') - _, label_pred = res.pop('label_pred').max(2) - output['label_pred'] = label_pred + output[C.OUTPUTS(0)] = res.pop(C.OUTPUTS(2)) + _, label_pred = res.pop(C.OUTPUTS(1)).max(2) + output[C.OUTPUTS(1)] = label_pred return output @@ -463,41 +459,44 @@ class ParserLoss(LossFunc): """ 计算parser的loss - :param arc_pred: [batch_size, seq_len, seq_len] 边预测logits - :param label_pred: [batch_size, seq_len, num_label] label预测logits - :param arc_true: [batch_size, seq_len] 真实边的标注 - :param label_true: [batch_size, seq_len] 真实类别的标注 - :param mask: [batch_size, seq_len] 预测结果的mask + :param pred1: [batch_size, seq_len, seq_len] 边预测logits + :param pred2: [batch_size, seq_len, num_label] label预测logits + :param target1: [batch_size, seq_len] 真实边的标注 + :param target2: [batch_size, seq_len] 真实类别的标注 + :param seq_len: [batch_size, seq_len] 真实目标的长度 :return loss: scalar """ - def __init__(self, arc_pred=None, label_pred=None, arc_true=None, label_true=None): + def __init__(self, pred1=None, pred2=None, + target1=None, target2=None, + seq_len=None): super(ParserLoss, self).__init__(BiaffineParser.loss, - arc_pred=arc_pred, - label_pred=label_pred, - arc_true=arc_true, - label_true=label_true) + pred1=pred1, + pred2=pred2, + target1=target1, + target2=target2, + seq_len=seq_len) class ParserMetric(MetricBase): """ 评估parser的性能 - :param arc_pred: 边预测logits - :param label_pred: label预测logits - :param arc_true: 真实边的标注 - :param label_true: 真实类别的标注 + :param pred1: 边预测logits + :param pred2: label预测logits + :param target1: 真实边的标注 + :param target2: 真实类别的标注 :param seq_len: 序列长度 :return dict: 评估结果:: UAS: 不带label时, 边预测的准确率 LAS: 同时预测边和label的准确率 """ - def __init__(self, arc_pred=None, label_pred=None, - arc_true=None, label_true=None, seq_len=None): + def __init__(self, pred1=None, pred2=None, + target1=None, target2=None, seq_len=None): super().__init__() - self._init_param_map(arc_pred=arc_pred, label_pred=label_pred, - arc_true=arc_true, label_true=label_true, + self._init_param_map(pred1=pred1, pred2=pred2, + target1=target1, target2=target2, seq_len=seq_len) self.num_arc = 0 self.num_label = 0 @@ -509,17 +508,17 @@ class ParserMetric(MetricBase): self.num_sample = self.num_label = self.num_arc = 0 return res - def evaluate(self, arc_pred, label_pred, arc_true, label_true, seq_len=None): + def evaluate(self, pred1, pred2, target1, target2, seq_len=None): """Evaluate the performance of prediction. """ if seq_len is None: - seq_mask = arc_pred.new_ones(arc_pred.size(), dtype=torch.long) + seq_mask = pred1.new_ones(pred1.size(), dtype=torch.long) else: seq_mask = seq_lens_to_masks(seq_len.long(), float=False).long() # mask out tag seq_mask[:,0] = 0 - head_pred_correct = (arc_pred == arc_true).long() * seq_mask - label_pred_correct = (label_pred == label_true).long() * head_pred_correct + head_pred_correct = (pred1 == target1).long() * seq_mask + label_pred_correct = (pred2 == target2).long() * head_pred_correct self.num_arc += head_pred_correct.sum().item() self.num_label += label_pred_correct.sum().item() self.num_sample += seq_mask.sum().item() diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index f0c6f33f..c3247333 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -108,7 +108,7 @@ class STSeqLabel(nn.Module): :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ - def __init__(self, vocab_size, emb_dim, num_cls, + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, num_head=8, @@ -118,8 +118,7 @@ class STSeqLabel(nn.Module): emb_dropout=0.1, dropout=0.1,): super(STSeqLabel, self).__init__() - self.enc = StarTransEnc(vocab_size=vocab_size, - emb_dim=emb_dim, + self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, num_layers=num_layers, num_head=num_head, @@ -170,7 +169,7 @@ class STSeqCls(nn.Module): :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ - def __init__(self, vocab_size, emb_dim, num_cls, + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, num_head=8, @@ -180,8 +179,7 @@ class STSeqCls(nn.Module): emb_dropout=0.1, dropout=0.1,): super(STSeqCls, self).__init__() - self.enc = StarTransEnc(vocab_size=vocab_size, - emb_dim=emb_dim, + self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, num_layers=num_layers, num_head=num_head, @@ -232,7 +230,7 @@ class STNLICls(nn.Module): :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ - def __init__(self, vocab_size, emb_dim, num_cls, + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, num_head=8, @@ -242,8 +240,7 @@ class STNLICls(nn.Module): emb_dropout=0.1, dropout=0.1,): super(STNLICls, self).__init__() - self.enc = StarTransEnc(vocab_size=vocab_size, - emb_dim=emb_dim, + self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, num_layers=num_layers, num_head=num_head, diff --git a/test/models/__init__.py b/test/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/models/model_runner.py b/test/models/model_runner.py new file mode 100644 index 00000000..7a8d0593 --- /dev/null +++ b/test/models/model_runner.py @@ -0,0 +1,151 @@ +""" +此模块可以非常方便的测试模型。 +若你的模型属于:文本分类,序列标注,自然语言推理(NLI),可以直接使用此模块测试 +若模型不属于上述类别,也可以自己准备假数据,设定loss和metric进行测试 + +此模块的测试仅保证模型能使用fastNLP进行训练和测试,不测试模型实际性能 + +Example:: + # import 全大写变量... + from model_runner import * + + # 测试一个文本分类模型 + init_emb = (VOCAB_SIZE, 50) + model = SomeModel(init_emb, num_cls=NUM_CLS) + RUNNER.run_model_with_task(TEXT_CLS, model) + + # 序列标注模型 + RUNNER.run_model_with_task(POS_TAGGING, model) + + # NLI模型 + RUNNER.run_model_with_task(NLI, model) + + # 自定义模型 + RUNNER.run_model(model, data=get_mydata(), + loss=Myloss(), metrics=Mymetric()) +""" +from fastNLP import Trainer, Tester, DataSet +from fastNLP import AccuracyMetric +from fastNLP import CrossEntropyLoss +from fastNLP.core.const import Const as C +from random import randrange + +VOCAB_SIZE = 100 +NUM_CLS = 100 +MAX_LEN = 10 +N_SAMPLES = 100 +N_EPOCHS = 1 +BATCH_SIZE = 5 + +TEXT_CLS = 'text_cls' +POS_TAGGING = 'pos_tagging' +NLI = 'nli' + +class ModelRunner(): + def gen_seq(self, length, vocab_size): + """generate fake sequence indexes with given length""" + # reserve 0 for padding + return [randrange(1, vocab_size) for _ in range(length)] + + def gen_var_seq(self, max_len, vocab_size): + """generate fake sequence indexes in variant length""" + length = randrange(3, max_len) # at least 3 words in a seq + return self.gen_seq(length, vocab_size) + + def prepare_text_classification_data(self): + index = 'index' + ds = DataSet({index: list(range(N_SAMPLES))}) + ds.apply_field(lambda x: self.gen_var_seq(MAX_LEN, VOCAB_SIZE), + field_name=index, new_field_name=C.INPUT, + is_input=True) + ds.apply_field(lambda x: randrange(NUM_CLS), + field_name=index, new_field_name=C.TARGET, + is_target=True) + ds.apply_field(len, C.INPUT, C.INPUT_LEN, + is_input=True) + return ds + + def prepare_pos_tagging_data(self): + index = 'index' + ds = DataSet({index: list(range(N_SAMPLES))}) + ds.apply_field(lambda x: self.gen_var_seq(MAX_LEN, VOCAB_SIZE), + field_name=index, new_field_name=C.INPUT, + is_input=True) + ds.apply_field(lambda x: self.gen_seq(len(x), NUM_CLS), + field_name=C.INPUT, new_field_name=C.TARGET, + is_target=True) + ds.apply_field(len, C.INPUT, C.INPUT_LEN, + is_input=True, is_target=True) + return ds + + def prepare_nli_data(self): + index = 'index' + ds = DataSet({index: list(range(N_SAMPLES))}) + ds.apply_field(lambda x: self.gen_var_seq(MAX_LEN, VOCAB_SIZE), + field_name=index, new_field_name=C.INPUTS(0), + is_input=True) + ds.apply_field(lambda x: self.gen_var_seq(MAX_LEN, VOCAB_SIZE), + field_name=index, new_field_name=C.INPUTS(1), + is_input=True) + ds.apply_field(lambda x: randrange(NUM_CLS), + field_name=index, new_field_name=C.TARGET, + is_target=True) + ds.apply_field(len, C.INPUTS(0), C.INPUT_LENS(0), + is_input=True, is_target=True) + ds.apply_field(len, C.INPUTS(1), C.INPUT_LENS(1), + is_input = True, is_target = True) + ds.set_input(C.INPUTS(0), C.INPUTS(1)) + ds.set_target(C.TARGET) + return ds + + def run_text_classification(self, model, data=None): + if data is None: + data = self.prepare_text_classification_data() + loss = CrossEntropyLoss(pred=C.OUTPUT, target=C.TARGET) + metric = AccuracyMetric(pred=C.OUTPUT, target=C.TARGET) + self.run_model(model, data, loss, metric) + + def run_pos_tagging(self, model, data=None): + if data is None: + data = self.prepare_pos_tagging_data() + loss = CrossEntropyLoss(pred=C.OUTPUT, target=C.TARGET, padding_idx=0) + metric = AccuracyMetric(pred=C.OUTPUT, target=C.TARGET, seq_len=C.INPUT_LEN) + self.run_model(model, data, loss, metric) + + def run_nli(self, model, data=None): + if data is None: + data = self.prepare_nli_data() + loss = CrossEntropyLoss(pred=C.OUTPUT, target=C.TARGET) + metric = AccuracyMetric(pred=C.OUTPUT, target=C.TARGET) + self.run_model(model, data, loss, metric) + + def run_model(self, model, data, loss, metrics): + """run a model, test if it can run with fastNLP""" + print('testing model:', model.__class__.__name__) + tester = Tester(data=data, model=model, metrics=metrics, + batch_size=BATCH_SIZE, verbose=0) + before_train = tester.test() + trainer = Trainer(model=model, train_data=data, dev_data=None, + n_epochs=N_EPOCHS, batch_size=BATCH_SIZE, + loss=loss, + save_path=None, + use_tqdm=False) + trainer.train(load_best_model=False) + after_train = tester.test() + for metric_name, v1 in before_train.items(): + assert metric_name in after_train + # # at least we can sure model params changed, even if we don't know performance + # v2 = after_train[metric_name] + # assert v1 != v2 + + def run_model_with_task(self, task, model): + """run a model with certain task""" + TASKS = { + TEXT_CLS: self.run_text_classification, + POS_TAGGING: self.run_pos_tagging, + NLI: self.run_nli, + } + assert task in TASKS + TASKS[task](model) + +RUNNER = ModelRunner() diff --git a/test/models/test_biaffine_parser.py b/test/models/test_biaffine_parser.py index 918f0fd9..e4746391 100644 --- a/test/models/test_biaffine_parser.py +++ b/test/models/test_biaffine_parser.py @@ -2,90 +2,33 @@ import unittest import fastNLP from fastNLP.models.biaffine_parser import BiaffineParser, ParserLoss, ParserMetric - -data_file = """ -1 The _ DET DT _ 3 det _ _ -2 new _ ADJ JJ _ 3 amod _ _ -3 rate _ NOUN NN _ 6 nsubj _ _ -4 will _ AUX MD _ 6 aux _ _ -5 be _ VERB VB _ 6 cop _ _ -6 payable _ ADJ JJ _ 0 root _ _ -7 mask _ ADJ JJ _ 6 punct _ _ -8 mask _ ADJ JJ _ 6 punct _ _ -9 cents _ NOUN NNS _ 4 nmod _ _ -10 from _ ADP IN _ 12 case _ _ -11 seven _ NUM CD _ 12 nummod _ _ -12 cents _ NOUN NNS _ 4 nmod _ _ -13 a _ DET DT _ 14 det _ _ -14 share _ NOUN NN _ 12 nmod:npmod _ _ -15 . _ PUNCT . _ 4 punct _ _ - -1 The _ DET DT _ 3 det _ _ -2 new _ ADJ JJ _ 3 amod _ _ -3 rate _ NOUN NN _ 6 nsubj _ _ -4 will _ AUX MD _ 6 aux _ _ -5 be _ VERB VB _ 6 cop _ _ -6 payable _ ADJ JJ _ 0 root _ _ -7 Feb. _ PROPN NNP _ 6 nmod:tmod _ _ -8 15 _ NUM CD _ 7 nummod _ _ -9 . _ PUNCT . _ 6 punct _ _ - -1 A _ DET DT _ 3 det _ _ -2 record _ NOUN NN _ 3 compound _ _ -3 date _ NOUN NN _ 7 nsubjpass _ _ -4 has _ AUX VBZ _ 7 aux _ _ -5 n't _ PART RB _ 7 neg _ _ -6 been _ AUX VBN _ 7 auxpass _ _ -7 set _ VERB VBN _ 0 root _ _ -8 . _ PUNCT . _ 7 punct _ _ - -""" - - -def init_data(): - ds = fastNLP.DataSet() - v = {'words1': fastNLP.Vocabulary(), - 'words2': fastNLP.Vocabulary(), - 'label_true': fastNLP.Vocabulary()} - data = [] - for line in data_file.split('\n'): - line = line.split() - if len(line) == 0 and len(data) > 0: - data = list(zip(*data)) - ds.append(fastNLP.Instance(words1=data[1], - words2=data[4], - arc_true=data[6], - label_true=data[7])) - data = [] - elif len(line) > 0: - data.append(line) - - for name in ['words1', 'words2', 'label_true']: - ds.apply(lambda x: [''] + list(x[name]), new_field_name=name) - ds.apply(lambda x: v[name].add_word_lst(x[name])) - - for name in ['words1', 'words2', 'label_true']: - ds.apply(lambda x: [v[name].to_index(w) for w in x[name]], new_field_name=name) - - ds.apply(lambda x: [0] + list(map(int, x['arc_true'])), new_field_name='arc_true') - ds.apply(lambda x: len(x['words1']), new_field_name='seq_len') - ds.set_input('words1', 'words2', 'seq_len', flag=True) - ds.set_target('arc_true', 'label_true', 'seq_len', flag=True) - return ds, v['words1'], v['words2'], v['label_true'] - +from .model_runner import * + + +def prepare_parser_data(): + index = 'index' + ds = DataSet({index: list(range(N_SAMPLES))}) + ds.apply_field(lambda x: RUNNER.gen_var_seq(MAX_LEN, VOCAB_SIZE), + field_name=index, new_field_name=C.INPUTS(0), + is_input=True) + ds.apply_field(lambda x: RUNNER.gen_seq(len(x), NUM_CLS), + field_name=C.INPUTS(0), new_field_name=C.INPUTS(1), + is_input=True) + # target1 is heads, should in range(0, len(words)) + ds.apply_field(lambda x: RUNNER.gen_seq(len(x), len(x)), + field_name=C.INPUTS(0), new_field_name=C.TARGETS(0), + is_target=True) + ds.apply_field(lambda x: RUNNER.gen_seq(len(x), NUM_CLS), + field_name=C.INPUTS(0), new_field_name=C.TARGETS(1), + is_target=True) + ds.apply_field(len, field_name=C.INPUTS(0), new_field_name=C.INPUT_LEN, + is_input=True, is_target=True) + return ds class TestBiaffineParser(unittest.TestCase): def test_train(self): - ds, v1, v2, v3 = init_data() - model = BiaffineParser(init_embed=(len(v1), 30), - pos_vocab_size=len(v2), pos_emb_dim=30, - num_label=len(v3), encoder='var-lstm') - trainer = fastNLP.Trainer(model=model, train_data=ds, dev_data=ds, - loss=ParserLoss(), metrics=ParserMetric(), metric_key='UAS', - batch_size=1, validate_every=10, - n_epochs=10, use_tqdm=False) - trainer.train(load_best_model=False) - - -if __name__ == '__main__': - unittest.main() + model = BiaffineParser(init_embed=(VOCAB_SIZE, 30), + pos_vocab_size=VOCAB_SIZE, pos_emb_dim=30, + num_label=NUM_CLS, encoder='var-lstm') + ds = prepare_parser_data() + RUNNER.run_model(model, ds, loss=ParserLoss(), metrics=ParserMetric()) diff --git a/test/models/test_star_trans.py b/test/models/test_star_trans.py new file mode 100644 index 00000000..b08e2efe --- /dev/null +++ b/test/models/test_star_trans.py @@ -0,0 +1,16 @@ +from .model_runner import * +from fastNLP.models.star_transformer import STNLICls, STSeqCls, STSeqLabel + + +# add star-transformer tests, for 3 kinds of tasks. +def test_cls(): + model = STSeqCls((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + RUNNER.run_model_with_task(TEXT_CLS, model) + +def test_nli(): + model = STNLICls((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + RUNNER.run_model_with_task(NLI, model) + +def test_seq_label(): + model = STSeqLabel((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + RUNNER.run_model_with_task(POS_TAGGING, model) From dfcc2f70c2d4be8a19565c315f9d9a4e9bc2b079 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sat, 4 May 2019 08:53:18 +0800 Subject: [PATCH 072/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20docs=20?= =?UTF-8?q?=E7=9A=84=E9=85=8D=E7=BD=AE=E5=92=8C=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Makefile | 2 +- docs/source/conf.py | 7 +- docs/source/fastNLP.api.rst | 60 ---------- docs/source/fastNLP.automl.rst | 44 -------- docs/source/fastNLP.component.rst | 20 ---- docs/source/fastNLP.core.rst | 124 --------------------- docs/source/fastNLP.io.rst | 60 ---------- docs/source/fastNLP.models.rst | 108 ------------------ docs/source/fastNLP.modules.aggregator.rst | 28 ----- docs/source/fastNLP.modules.decoder.rst | 36 ------ docs/source/fastNLP.modules.encoder.rst | 100 ----------------- docs/source/fastNLP.modules.rst | 45 -------- docs/source/fastNLP.rst | 21 ---- 13 files changed, 6 insertions(+), 649 deletions(-) delete mode 100644 docs/source/fastNLP.api.rst delete mode 100644 docs/source/fastNLP.automl.rst delete mode 100644 docs/source/fastNLP.component.rst delete mode 100644 docs/source/fastNLP.core.rst delete mode 100644 docs/source/fastNLP.io.rst delete mode 100644 docs/source/fastNLP.models.rst delete mode 100644 docs/source/fastNLP.modules.aggregator.rst delete mode 100644 docs/source/fastNLP.modules.decoder.rst delete mode 100644 docs/source/fastNLP.modules.encoder.rst delete mode 100644 docs/source/fastNLP.modules.rst delete mode 100644 docs/source/fastNLP.rst diff --git a/docs/Makefile b/docs/Makefile index 3050e655..6ba2fa54 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,7 +14,7 @@ help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) apidoc: - $(SPHINXAPIDOC) -fM -o source ../$(SPHINXPROJ) + $(SPHINXAPIDOC) -efM -o source ../$(SPHINXPROJ) server: cd build/html && python -m http.server diff --git a/docs/source/conf.py b/docs/source/conf.py index 5fd9e56d..3e9753af 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,7 +68,7 @@ master_doc = 'index' # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "zh_CN" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -89,7 +89,10 @@ html_theme = 'sphinx_rtd_theme' # further. For a list of options available for each theme, see the # documentation. # -# html_theme_options = {} +html_theme_options = { + 'collapse_navigation': False, + 'titles_only': True +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/source/fastNLP.api.rst b/docs/source/fastNLP.api.rst deleted file mode 100644 index 955eb8c5..00000000 --- a/docs/source/fastNLP.api.rst +++ /dev/null @@ -1,60 +0,0 @@ -fastNLP.api package -=================== - -.. automodule:: fastNLP.api - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.api.api module ----------------------- - -.. automodule:: fastNLP.api.api - :members: - :undoc-members: - :show-inheritance: - -fastNLP.api.converter module ----------------------------- - -.. automodule:: fastNLP.api.converter - :members: - :undoc-members: - :show-inheritance: - -fastNLP.api.examples module ---------------------------- - -.. automodule:: fastNLP.api.examples - :members: - :undoc-members: - :show-inheritance: - -fastNLP.api.pipeline module ---------------------------- - -.. automodule:: fastNLP.api.pipeline - :members: - :undoc-members: - :show-inheritance: - -fastNLP.api.processor module ----------------------------- - -.. automodule:: fastNLP.api.processor - :members: - :undoc-members: - :show-inheritance: - -fastNLP.api.utils module ------------------------- - -.. automodule:: fastNLP.api.utils - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.automl.rst b/docs/source/fastNLP.automl.rst deleted file mode 100644 index 3c12e271..00000000 --- a/docs/source/fastNLP.automl.rst +++ /dev/null @@ -1,44 +0,0 @@ -fastNLP.automl package -====================== - -.. automodule:: fastNLP.automl - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.automl.enas\_controller module --------------------------------------- - -.. automodule:: fastNLP.automl.enas_controller - :members: - :undoc-members: - :show-inheritance: - -fastNLP.automl.enas\_model module ---------------------------------- - -.. automodule:: fastNLP.automl.enas_model - :members: - :undoc-members: - :show-inheritance: - -fastNLP.automl.enas\_trainer module ------------------------------------ - -.. automodule:: fastNLP.automl.enas_trainer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.automl.enas\_utils module ---------------------------------- - -.. automodule:: fastNLP.automl.enas_utils - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.component.rst b/docs/source/fastNLP.component.rst deleted file mode 100644 index 81fcf561..00000000 --- a/docs/source/fastNLP.component.rst +++ /dev/null @@ -1,20 +0,0 @@ -fastNLP.component package -========================= - -.. automodule:: fastNLP.component - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.component.bert\_tokenizer module ----------------------------------------- - -.. automodule:: fastNLP.component.bert_tokenizer - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst deleted file mode 100644 index 540bd03c..00000000 --- a/docs/source/fastNLP.core.rst +++ /dev/null @@ -1,124 +0,0 @@ -fastNLP.core package -==================== - -.. automodule:: fastNLP.core - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.core.batch module -------------------------- - -.. automodule:: fastNLP.core.batch - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.callback module ----------------------------- - -.. automodule:: fastNLP.core.callback - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.dataset module ---------------------------- - -.. automodule:: fastNLP.core.dataset - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.fieldarray module ------------------------------- - -.. automodule:: fastNLP.core.fieldarray - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.instance module ----------------------------- - -.. automodule:: fastNLP.core.instance - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.losses module --------------------------- - -.. automodule:: fastNLP.core.losses - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.metrics module ---------------------------- - -.. automodule:: fastNLP.core.metrics - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.optimizer module ------------------------------ - -.. automodule:: fastNLP.core.optimizer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.predictor module ------------------------------ - -.. automodule:: fastNLP.core.predictor - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.sampler module ---------------------------- - -.. automodule:: fastNLP.core.sampler - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.tester module --------------------------- - -.. automodule:: fastNLP.core.tester - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.trainer module ---------------------------- - -.. automodule:: fastNLP.core.trainer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.utils module -------------------------- - -.. automodule:: fastNLP.core.utils - :members: - :undoc-members: - :show-inheritance: - -fastNLP.core.vocabulary module ------------------------------- - -.. automodule:: fastNLP.core.vocabulary - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst deleted file mode 100644 index 1eb95c6a..00000000 --- a/docs/source/fastNLP.io.rst +++ /dev/null @@ -1,60 +0,0 @@ -fastNLP.io package -================== - -.. automodule:: fastNLP.io - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.io.base\_loader module ------------------------------- - -.. automodule:: fastNLP.io.base_loader - :members: - :undoc-members: - :show-inheritance: - -fastNLP.io.config\_io module ----------------------------- - -.. automodule:: fastNLP.io.config_io - :members: - :undoc-members: - :show-inheritance: - -fastNLP.io.dataset\_loader module ---------------------------------- - -.. automodule:: fastNLP.io.dataset_loader - :members: - :undoc-members: - :show-inheritance: - -fastNLP.io.embed\_loader module -------------------------------- - -.. automodule:: fastNLP.io.embed_loader - :members: - :undoc-members: - :show-inheritance: - -fastNLP.io.file\_reader module ------------------------------- - -.. automodule:: fastNLP.io.file_reader - :members: - :undoc-members: - :show-inheritance: - -fastNLP.io.model\_io module ---------------------------- - -.. automodule:: fastNLP.io.model_io - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst deleted file mode 100644 index 18b8186f..00000000 --- a/docs/source/fastNLP.models.rst +++ /dev/null @@ -1,108 +0,0 @@ -fastNLP.models package -====================== - -.. automodule:: fastNLP.models - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.models.base\_model module ---------------------------------- - -.. automodule:: fastNLP.models.base_model - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.bert module --------------------------- - -.. automodule:: fastNLP.models.bert - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.biaffine\_parser module --------------------------------------- - -.. automodule:: fastNLP.models.biaffine_parser - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.char\_language\_model module -------------------------------------------- - -.. automodule:: fastNLP.models.char_language_model - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.cnn\_text\_classification module ------------------------------------------------ - -.. automodule:: fastNLP.models.cnn_text_classification - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.enas\_controller module --------------------------------------- - -.. automodule:: fastNLP.models.enas_controller - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.enas\_model module ---------------------------------- - -.. automodule:: fastNLP.models.enas_model - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.enas\_trainer module ------------------------------------ - -.. automodule:: fastNLP.models.enas_trainer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.enas\_utils module ---------------------------------- - -.. automodule:: fastNLP.models.enas_utils - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.sequence\_modeling module ----------------------------------------- - -.. automodule:: fastNLP.models.sequence_modeling - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.snli module --------------------------- - -.. automodule:: fastNLP.models.snli - :members: - :undoc-members: - :show-inheritance: - -fastNLP.models.star\_transformer module ---------------------------------------- - -.. automodule:: fastNLP.models.star_transformer - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst deleted file mode 100644 index 74ff5aed..00000000 --- a/docs/source/fastNLP.modules.aggregator.rst +++ /dev/null @@ -1,28 +0,0 @@ -fastNLP.modules.aggregator package -================================== - -.. automodule:: fastNLP.modules.aggregator - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.modules.aggregator.attention module -------------------------------------------- - -.. automodule:: fastNLP.modules.aggregator.attention - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.aggregator.pooling module ------------------------------------------ - -.. automodule:: fastNLP.modules.aggregator.pooling - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst deleted file mode 100644 index 5e467b98..00000000 --- a/docs/source/fastNLP.modules.decoder.rst +++ /dev/null @@ -1,36 +0,0 @@ -fastNLP.modules.decoder package -=============================== - -.. automodule:: fastNLP.modules.decoder - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.modules.decoder.CRF module ----------------------------------- - -.. automodule:: fastNLP.modules.decoder.CRF - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.decoder.MLP module ----------------------------------- - -.. automodule:: fastNLP.modules.decoder.MLP - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.decoder.utils module ------------------------------------- - -.. automodule:: fastNLP.modules.decoder.utils - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst deleted file mode 100644 index ff048be7..00000000 --- a/docs/source/fastNLP.modules.encoder.rst +++ /dev/null @@ -1,100 +0,0 @@ -fastNLP.modules.encoder package -=============================== - -.. automodule:: fastNLP.modules.encoder - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -fastNLP.modules.encoder.bert module ------------------------------------ - -.. automodule:: fastNLP.modules.encoder.bert - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.char\_encoder module --------------------------------------------- - -.. automodule:: fastNLP.modules.encoder.char_encoder - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.conv module ------------------------------------ - -.. automodule:: fastNLP.modules.encoder.conv - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.conv\_maxpool module --------------------------------------------- - -.. automodule:: fastNLP.modules.encoder.conv_maxpool - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.embedding module ----------------------------------------- - -.. automodule:: fastNLP.modules.encoder.embedding - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.linear module -------------------------------------- - -.. automodule:: fastNLP.modules.encoder.linear - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.lstm module ------------------------------------ - -.. automodule:: fastNLP.modules.encoder.lstm - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.masked\_rnn module ------------------------------------------- - -.. automodule:: fastNLP.modules.encoder.masked_rnn - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.star\_transformer module ------------------------------------------------- - -.. automodule:: fastNLP.modules.encoder.star_transformer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.transformer module ------------------------------------------- - -.. automodule:: fastNLP.modules.encoder.transformer - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.encoder.variational\_rnn module ------------------------------------------------ - -.. automodule:: fastNLP.modules.encoder.variational_rnn - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst deleted file mode 100644 index 5884e655..00000000 --- a/docs/source/fastNLP.modules.rst +++ /dev/null @@ -1,45 +0,0 @@ -fastNLP.modules package -======================= - -.. automodule:: fastNLP.modules - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - - fastNLP.modules.aggregator - fastNLP.modules.decoder - fastNLP.modules.encoder - -Submodules ----------- - -fastNLP.modules.dropout module ------------------------------- - -.. automodule:: fastNLP.modules.dropout - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.other\_modules module -------------------------------------- - -.. automodule:: fastNLP.modules.other_modules - :members: - :undoc-members: - :show-inheritance: - -fastNLP.modules.utils module ----------------------------- - -.. automodule:: fastNLP.modules.utils - :members: - :undoc-members: - :show-inheritance: - - diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst deleted file mode 100644 index f5247748..00000000 --- a/docs/source/fastNLP.rst +++ /dev/null @@ -1,21 +0,0 @@ -fastNLP package -=============== - -.. automodule:: fastNLP - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - - fastNLP.api - fastNLP.automl - fastNLP.component - fastNLP.core - fastNLP.io - fastNLP.models - fastNLP.modules - From 257eb2b9eb0d878e4578f2385b193caf971e6438 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sat, 4 May 2019 17:05:17 +0800 Subject: [PATCH 073/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E9=83=A8=E5=88=86=E7=9A=84=E4=B8=80=E5=A4=A7=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=96=87=E6=A1=A3=EF=BC=8CTODO=EF=BC=9A=201.=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=20trainer=20=E5=92=8C=20tester=20=E9=83=A8?= =?UTF-8?q?=E5=88=86=E7=9A=84=E6=96=87=E6=A1=A3=202.=20=E7=A0=94=E7=A9=B6?= =?UTF-8?q?=E6=B3=A8=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. From ef525bf74c15ced22376a8e0fefcab833262288e Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 01:05:52 +0800 Subject: [PATCH 074/173] =?UTF-8?q?core=E9=83=A8=E5=88=86=E7=9A=84?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E5=9F=BA=E6=9C=AC=E6=A3=80=E6=9F=A5=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 12 +- fastNLP/core/callback.py | 272 ++++++++-------- fastNLP/core/const.py | 2 +- fastNLP/core/dataset.py | 5 +- fastNLP/core/losses.py | 188 +++++------ fastNLP/core/metrics.py | 141 ++++----- fastNLP/core/optimizer.py | 23 +- fastNLP/core/predictor.py | 8 +- fastNLP/core/sampler.py | 54 ++-- fastNLP/core/tester.py | 110 +++---- fastNLP/core/trainer.py | 641 ++++++++++++++++++++------------------ fastNLP/core/utils.py | 6 + 12 files changed, 772 insertions(+), 690 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 35309bd3..e7975c9b 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -1,5 +1,15 @@ +""" +fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.modules` 等子模块组成,但常用的组件都可以直接 import ,常用组件如下: +""" +__all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", + "Trainer", "Tester", "Callback", + "Padder", "AutoPadder", "EngChar2DPadder", + "AccuracyMetric", "Optimizer", "SGD", "Adam", + "Sampler", "SequentialSampler", "BucketSampler", "RandomSampler", + "LossFunc", "CrossEntropyLoss", "L1Loss", "BCELoss", "NLLLoss", "LossInForward", + "cache_results"] from .core import * from . import models from . import modules -__version__ = '0.4.0' \ No newline at end of file +__version__ = '0.4.0' diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 914e4d28..b3aaffaa 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -1,87 +1,89 @@ """ -Callback的说明文档 - - .. _Callback: - -Callback是fastNLP中被设计用于增强 Trainer_ 的类。如果Callback被传递给了 Trainer_ , 则 Trainer_ 会在对应的阶段调用Callback -的函数,具体调用时机可以通过 Trainer_ 查看。 - +callback模块实现了 fastNLP 中的Callback类,用于增强 :class:`~fastNLP.Trainer` 类, +关于Trainer的详细文档,请参见 :doc:`trainer 模块` """ - import os import torch -from fastNLP.io.model_io import ModelSaver, ModelLoader +from ..io.model_io import ModelSaver, ModelLoader + try: from tensorboardX import SummaryWriter except: pass -class Callback(object): - """这是Callback的基类,所有的callback必须继承自这个类。 +class Callback(object): """ + 别名::class:`fastNLP.Callback` :class:`fastNLP.core.callback.Callback` + + Callback是fastNLP中被设计用于增强 :class:`~fastNLP.Trainer` 的类。 + 如果Callback被传递给了 Trainer , 则 Trainer 会在对应的阶段调用Callback的函数, + 具体调用时机可以通过 :doc:`trainer 模块` 查看。 + 这是Callback的基类,所有的callback必须继承自这个类(参见 :doc:`callback 模块 ` ) + """ + def __init__(self): super(Callback, self).__init__() self._trainer = None # 在Trainer内部被重新赋值 - + @property def trainer(self): """ 该属性可以通过self.trainer获取到,一般情况下不需要使用这个属性。 """ return self._trainer - + @property def step(self): """当前运行到的step, 范围为[1, self.n_steps+1)""" return self._trainer.step - + @property def n_steps(self): """Trainer一共会运行多少步""" return self._trainer.n_steps - + @property def batch_size(self): """train和evaluate时的batch_size为多大""" return self._trainer.batch_size - + @property def epoch(self): """当前运行的epoch数,范围是[1, self.n_epochs+1)""" return self._trainer.epoch - + @property def n_epochs(self): """一共会运行多少个epoch""" return self._trainer.n_epochs - + @property def optimizer(self): """初始化Trainer时传递的Optimizer""" return self._trainer.optimizer - + @property def model(self): """正在被Trainer训练的模型""" return self._trainer.model - + @property def pbar(self): """如果在Callback中需要打印内容,请使用self.pbar.write(str)。否则可能出现命令行显示效果不太好的问题。""" return self._trainer.pbar - + @property def update_every(self): """Trainer中的模型多少次反向传播才进行一次梯度更新,在Trainer初始化时传入的。""" return self._trainer.update_every - + @property def batch_per_epoch(self): """每个epoch一共有多少个batch,只有在on_epoch_begin之后才能调用该属性。""" return self._trainer.batch_per_epoch - + def on_train_begin(self): """ 在Train过程开始之前调用。 @@ -89,7 +91,7 @@ class Callback(object): :return: """ pass - + def on_epoch_begin(self): """ 在每个epoch开始之前调用一次 @@ -97,7 +99,7 @@ class Callback(object): :return: """ pass - + def on_batch_begin(self, batch_x, batch_y, indices): """ 每次采集到一个batch的数据则调用一次。这里对batch_x或batch_y删除添加内容是可以影响到Trainer中内容的。所以在这一步 @@ -110,7 +112,7 @@ class Callback(object): :return: """ pass - + def on_loss_begin(self, batch_y, predict_y): """ 在计算loss前调用,即这里修改batch_y或predict_y的值是可以影响到loss计算的。 @@ -120,7 +122,7 @@ class Callback(object): :return: """ pass - + def on_backward_begin(self, loss): """ 在loss得到之后,但在反向传播之前。可能可以进行loss是否为NaN的检查。 @@ -129,7 +131,7 @@ class Callback(object): :return: """ pass - + def on_backward_end(self): """ 反向梯度传播已完成,但由于update_every的设置,可能并不是每一次调用都有梯度。到这一步,还没有更新参数。 @@ -137,7 +139,7 @@ class Callback(object): :return: """ pass - + def on_step_end(self): """ 到这里模型的参数已经按照梯度更新。但可能受update_every影响,并不是每次都更新了。 @@ -145,14 +147,14 @@ class Callback(object): :return: """ pass - + def on_batch_end(self): """ 这一步与on_step_end是紧接着的。只是为了对称性加上了这一步。 """ pass - + def on_valid_begin(self): """ 如果Trainer中设置了验证,则发生验证前会调用该函数 @@ -160,7 +162,7 @@ class Callback(object): :return: """ pass - + def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): """ 每次执行验证集的evaluation后会调用。 @@ -173,19 +175,19 @@ class Callback(object): :return: """ pass - + def on_epoch_end(self): """ 每个epoch结束将会调用该方法 """ pass - + def on_train_end(self): """ 训练结束,调用该方法 """ pass - + def on_exception(self, exception): """ 当训练过程出现异常,会触发该方法 @@ -196,32 +198,31 @@ class Callback(object): def _transfer(func): """装饰器,将对CallbackManager的调用转发到各个Callback子类. + :param func: :return: """ - + def wrapper(manager, *arg): returns = [] for callback in manager.callbacks: returns.append(getattr(callback, func.__name__)(*arg)) return returns - + return wrapper class CallbackManager(Callback): - """内部使用的Callback管理类 - """ - def __init__(self, env, callbacks=None): """ + 内部使用的Callback管理类 :param dict env: The key is the name of the Trainer attribute(str). The value is the attribute itself. :param List[Callback] callbacks: """ super(CallbackManager, self).__init__() # set attribute of trainer environment - + self.callbacks = [] if callbacks is not None: if isinstance(callbacks, list): @@ -232,78 +233,82 @@ class CallbackManager(Callback): raise TypeError(f"Expect sub-classes of Callback. Got {type(obj)}") else: raise TypeError(f"Expect callbacks in CallbackManager(callbacks) to be list. Got {type(callbacks)}.") - + for env_name, env_val in env.items(): for callback in self.callbacks: - setattr(callback, '_'+env_name, env_val) # Callback.trainer - + setattr(callback, '_' + env_name, env_val) # Callback.trainer + @_transfer def on_train_begin(self): pass - + @_transfer def on_epoch_begin(self): pass - + @_transfer def on_batch_begin(self, batch_x, batch_y, indices): pass - + @_transfer def on_loss_begin(self, batch_y, predict_y): pass - + @_transfer def on_backward_begin(self, loss): pass - + @_transfer def on_backward_end(self): pass - + @_transfer def on_step_end(self): pass - + @_transfer def on_batch_end(self): pass - + @_transfer def on_valid_begin(self): pass - + @_transfer def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): pass - + @_transfer def on_epoch_end(self): pass - + @_transfer def on_train_end(self): pass - + @_transfer def on_exception(self, exception): pass class GradientClipCallback(Callback): + """每次backward前,将parameter的gradient clip到某个范围。 + + :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer + 的model中所有参数进行clip + :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 + :param str clip_type: 支持'norm', 'value'两种:: + + 1 'norm', 将gradient的norm rescale到[-clip_value, clip_value] + + 2 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; + 大于clip_value的gradient被赋值为clip_value. + """ + def __init__(self, parameters=None, clip_value=1, clip_type='norm'): - """每次backward前,将parameter的gradient clip到某个范围。 - - :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer - 的model中所有参数进行clip - :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 - :param str clip_type: 支持'norm', 'value'两种。 - 1. 'norm', 将gradient的norm rescale到[-clip_value, clip_value] - 2. 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; 大于 - clip_value的gradient被赋值为clip_value. - """ + super().__init__() - + from torch import nn if clip_type == 'norm': self.clip_fun = nn.utils.clip_grad_norm_ @@ -313,7 +318,7 @@ class GradientClipCallback(Callback): raise ValueError("Only supports `norm` or `value` right now.") self.parameters = parameters self.clip_value = clip_value - + def on_backward_end(self): if self.parameters is None: self.clip_fun(self.model.parameters(), self.clip_value) @@ -321,31 +326,17 @@ class GradientClipCallback(Callback): self.clip_fun(self.parameters, self.clip_value) -class CallbackException(BaseException): - def __init__(self, msg): - """ - 当需要通过callback跳出训练的时候可以通过抛出CallbackException并在on_exception中捕获这个值。 - :param str msg: Exception的信息。 - """ - super(CallbackException, self).__init__(msg) - - -class EarlyStopError(CallbackException): - def __init__(self, msg): - """用于EarlyStop时从Trainer训练循环中跳出。""" - super(EarlyStopError, self).__init__(msg) - - class EarlyStopCallback(Callback): - def __init__(self, patience): - """ + """ - :param int patience: 多少个epoch没有变好就停止训练 - """ + :param int patience: 多少个epoch没有变好就停止训练 + """ + + def __init__(self, patience): super(EarlyStopCallback, self).__init__() self.patience = patience self.wait = 0 - + def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): if not is_better_eval: # current result is getting worse @@ -355,7 +346,7 @@ class EarlyStopCallback(Callback): self.wait += 1 else: self.wait = 0 - + def on_exception(self, exception): if isinstance(exception, EarlyStopError): print("Early Stopping triggered in epoch {}!".format(self.epoch)) @@ -364,39 +355,41 @@ class EarlyStopCallback(Callback): class LRScheduler(Callback): - def __init__(self, lr_scheduler): - """对PyTorch LR Scheduler的包装以使得其可以被Trainer所使用 - - Example:: + """对PyTorch LR Scheduler的包装以使得其可以被Trainer所使用 - from fastNLP import LRScheduler + Example:: + from fastNLP import LRScheduler - - :param torch.optim.lr_scheduler._LRScheduler lr_scheduler: PyTorch的lr_scheduler - """ + :param torch.optim.lr_scheduler._LRScheduler lr_scheduler: PyTorch的lr_scheduler + """ + + def __init__(self, lr_scheduler): + super(LRScheduler, self).__init__() import torch.optim if isinstance(lr_scheduler, torch.optim.lr_scheduler._LRScheduler): self.scheduler = lr_scheduler else: raise ValueError(f"Expect torch.optim.lr_scheduler for LRScheduler. Got {type(lr_scheduler)}.") - + def on_epoch_begin(self): self.scheduler.step() class ControlC(Callback): - def __init__(self, quit_all): - """ + """ - :param bool quit_all: 若为True,则检测到control+C 直接退出程序;否则只退出Trainer - """ + :param bool quit_all: 若为True,则检测到control+C 直接退出程序;否则只退出Trainer + """ + + def __init__(self, quit_all): + super(ControlC, self).__init__() if type(quit_all) != bool: raise ValueError("In KeyBoardInterrupt, quit_all arguemnt must be a bool.") self.quit_all = quit_all - + def on_exception(self, exception): if isinstance(exception, KeyboardInterrupt): if self.quit_all is True: @@ -412,7 +405,7 @@ class SmoothValue(object): def __init__(self, beta: float): self.beta, self.n, self.mov_avg = beta, 0, 0 self.smooth = None - + def add_value(self, val: float) -> None: "Add `val` to calculate updated smoothed value." self.n += 1 @@ -421,13 +414,15 @@ class SmoothValue(object): class LRFinder(Callback): - def __init__(self, start_lr=1e-6, end_lr=10): - """用第一个 epoch 找最佳的学习率,从第二个epoch开始应用它 + """ + 用第一个 epoch 找最佳的学习率,从第二个epoch开始应用它 - :param int n_batch: 一个epoch内的iteration数 - :param float start_lr: 学习率下界 - :param float end_lr: 学习率上界 - """ + :param float start_lr: 学习率下界 + :param float end_lr: 学习率上界 + """ + + def __init__(self, start_lr=1e-6, end_lr=10): + super(LRFinder, self).__init__() self.start_lr, self.end_lr = start_lr, end_lr self.num_it = self.batch_per_epoch @@ -438,19 +433,19 @@ class LRFinder(Callback): self.smooth_value = SmoothValue(0.8) self.opt = None scale = (self.end_lr - self.start_lr) / self.num_it - + self.lr_gen = (self.start_lr + scale * (step + 1) for step in range(self.num_it)) self.find = None self.loader = ModelLoader() - + def on_epoch_begin(self): - if self.epoch == 1: # first epoch + if self.epoch == 1: # first epoch self.opt = self.trainer.optimizer # pytorch optimizer self.opt.param_groups[0]["lr"] = self.start_lr # save model ModelSaver("tmp").save_pytorch(self.trainer.model, param_only=True) self.find = True - + def on_backward_begin(self, loss): if self.find: if torch.isnan(loss) or self.stop is True: @@ -462,7 +457,7 @@ class LRFinder(Callback): if self.best_loss == 0. or self.smooth_value.smooth < self.best_loss: self.best_loss = self.smooth_value.smooth self.best_lr = self.opt.param_groups[0]["lr"] - + def on_batch_end(self, *args): if self.find: lr = next(self.lr_gen, None) @@ -471,9 +466,9 @@ class LRFinder(Callback): return self.opt.param_groups[0]["lr"] = lr # self.loader.load_pytorch(self.trainer.model, "tmp") - + def on_epoch_end(self): - if self.epoch == 1: # first epoch + if self.epoch == 1: # first epoch self.opt.param_groups[0]["lr"] = self.best_lr self.find = False # reset model @@ -483,12 +478,12 @@ class LRFinder(Callback): class TensorboardCallback(Callback): """ - 接受以下一个或多个字符串作为参数: - - "model" - - "loss" - - "metric" + 接受以下一个或多个字符串作为参数: + - "model" + - "loss" + - "metric" """ - + def __init__(self, *options): super(TensorboardCallback, self).__init__() args = {"model", "loss", "metric"} @@ -498,7 +493,7 @@ class TensorboardCallback(Callback): self.options = options self._summary_writer = None self.graph_added = False - + def on_train_begin(self): save_dir = self.trainer.save_path if save_dir is None: @@ -506,7 +501,7 @@ class TensorboardCallback(Callback): else: path = os.path.join(save_dir, 'tensorboard_logs_{}'.format(self.trainer.start_time)) self._summary_writer = SummaryWriter(path) - + def on_batch_begin(self, batch_x, batch_y, indices): if "model" in self.options and self.graph_added is False: # tesorboardX 这里有大bug,暂时没法画模型图 @@ -516,11 +511,11 @@ class TensorboardCallback(Callback): # args = args[0] if len(args) == 1 else args # self._summary_writer.add_graph(self.trainer.model, torch.zeros(32, 2)) self.graph_added = True - + def on_backward_begin(self, loss): if "loss" in self.options: self._summary_writer.add_scalar("loss", loss.item(), global_step=self.trainer.step) - + if "model" in self.options: for name, param in self.trainer.model.named_parameters(): if param.requires_grad: @@ -528,21 +523,40 @@ class TensorboardCallback(Callback): # self._summary_writer.add_scalar(name + "_std", param.std(), global_step=self.trainer.step) self._summary_writer.add_scalar(name + "_grad_mean", param.grad.mean(), global_step=self.trainer.step) - + def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): if "metric" in self.options: for name, metric in eval_result.items(): for metric_key, metric_val in metric.items(): self._summary_writer.add_scalar("valid_{}_{}".format(name, metric_key), metric_val, global_step=self.trainer.step) - + def on_train_end(self): self._summary_writer.close() del self._summary_writer - + def on_exception(self, exception): if hasattr(self, "_summary_writer"): self._summary_writer.close() del self._summary_writer +class CallbackException(BaseException): + """ + 当需要通过callback跳出训练的时候可以通过抛出CallbackException并在on_exception中捕获这个值。 + + :param str msg: Exception的信息。 + """ + + def __init__(self, msg): + super(CallbackException, self).__init__(msg) + + +class EarlyStopError(CallbackException): + """ + 用于EarlyStop时从Trainer训练循环中跳出。 + + """ + + def __init__(self, msg): + super(EarlyStopError, self).__init__(msg) diff --git a/fastNLP/core/const.py b/fastNLP/core/const.py index 56447395..dcb0a786 100644 --- a/fastNLP/core/const.py +++ b/fastNLP/core/const.py @@ -1,4 +1,4 @@ -class Const(): +class Const: """fastNLP中field命名常量。 具体列表:: diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 28bb2010..013f7602 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -1,5 +1,5 @@ """ -DataSet是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格, +:class:`~fastNLP.core.dataset.DataSet` 是fastNLP中用于承载数据的容器。可以将DataSet看做是一个表格, 每一行是一个sample (在fastNLP中被称为 :mod:`~.instance` ), 每一列是一个feature (在fastNLP中称为 :mod:`.field` )。 @@ -294,7 +294,8 @@ class DataSet(object): fastNLP的数据容器,详细的使用方法见文档 :doc:`fastNLP.core.dataset` :param data: 如果为dict类型,则每个key的value应该为等长的list; 如果为list, - 每个元素应该为具有相同field的 :class:`~fastNLP.Instance` 。 + 每个元素应该为具有相同field的 :class:`~fastNLP.Instance` 。 + """ def __init__(self, data=None): diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index abb1cd29..ac08b46f 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -1,36 +1,34 @@ """ - - .. _LossBase: - - .. _Loss: +losses 模块定义了 fastNLP 中所需的各种损失函数,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ - +__all__ = ["LossBase", "L1Loss", "LossFunc", "LossInForward", "BCELoss", "CrossEntropyLoss", "NLLLoss"] import inspect from collections import defaultdict import torch import torch.nn.functional as F -from fastNLP.core.utils import _CheckError -from fastNLP.core.utils import _CheckRes -from fastNLP.core.utils import _build_args -from fastNLP.core.utils import _check_arg_dict_list -from fastNLP.core.utils import _check_function_or_method -from fastNLP.core.utils import _get_func_signature +from .utils import _CheckError +from .utils import _CheckRes +from .utils import _build_args +from .utils import _check_arg_dict_list +from .utils import _check_function_or_method +from .utils import _get_func_signature class LossBase(object): - """所有loss的基类. - """ + 所有loss的基类。如果想了解其中的原理,请查看源码。 + """ + def __init__(self): self.param_map = {} self._checked = False - + def get_loss(self, *args, **kwargs): raise NotImplementedError - + def _init_param_map(self, key_map=None, **kwargs): """检查key_map和其他参数map,并将这些映射关系添加到self.param_map @@ -63,7 +61,7 @@ class LossBase(object): for value, key_set in value_counter.items(): if len(key_set) > 1: raise ValueError(f"Several parameters:{key_set} are provided with one output {value}.") - + # check consistence between signature and param_map func_spect = inspect.getfullargspec(self.get_loss) func_args = [arg for arg in func_spect.args if arg != 'self'] @@ -72,12 +70,12 @@ class LossBase(object): raise NameError( f"Parameter `{func_param}` is not in {_get_func_signature(self.get_loss)}. Please check the " f"initialization parameters, or change its signature.") - + # evaluate should not have varargs. # if func_spect.varargs: # raise NameError(f"Delete `*{func_spect.varargs}` in {get_func_signature(self.get_loss)}(Do not use " # f"positional argument.).") - + def _fast_param_map(self, pred_dict, target_dict): """Only used as inner function. When the pred_dict, target is unequivocal. Don't need users to pass key_map. such as pred_dict has one element, target_dict has one element @@ -92,7 +90,7 @@ class LossBase(object): fast_param['target'] = list(target_dict.values())[0] return fast_param return fast_param - + def __call__(self, pred_dict, target_dict, check=False): """ :param dict pred_dict: 模型的forward函数返回的dict @@ -104,7 +102,7 @@ class LossBase(object): if fast_param: loss = self.get_loss(**fast_param) return loss - + if not self._checked: # 1. check consistence between signature and param_map func_spect = inspect.getfullargspec(self.get_loss) @@ -112,14 +110,14 @@ class LossBase(object): for func_arg, input_arg in self.param_map.items(): if func_arg not in func_args: raise NameError(f"`{func_arg}` not in {_get_func_signature(self.get_loss)}.") - + # 2. only part of the param_map are passed, left are not for arg in func_args: if arg not in self.param_map: self.param_map[arg] = arg # This param does not need mapping. self._evaluate_args = func_args self._reverse_param_map = {input_arg: func_arg for func_arg, input_arg in self.param_map.items()} - + # need to wrap inputs in dict. mapped_pred_dict = {} mapped_target_dict = {} @@ -139,7 +137,7 @@ class LossBase(object): not_duplicate_flag += 1 if not_duplicate_flag == 3: duplicated.append(input_arg) - + # missing if not self._checked: check_res = _check_arg_dict_list(self.get_loss, [mapped_pred_dict, mapped_target_dict]) @@ -149,47 +147,50 @@ class LossBase(object): for idx, func_arg in enumerate(missing): # Don't delete `` in this information, nor add `` replaced_missing[idx] = f"{self.param_map[func_arg]}" + f"(assign to `{func_arg}` " \ - f"in `{self.__class__.__name__}`)" - + f"in `{self.__class__.__name__}`)" + check_res = _CheckRes(missing=replaced_missing, unused=check_res.unused, duplicated=duplicated, required=check_res.required, all_needed=check_res.all_needed, varargs=check_res.varargs) - + if check_res.missing or check_res.duplicated: raise _CheckError(check_res=check_res, func_signature=_get_func_signature(self.get_loss)) refined_args = _build_args(self.get_loss, **mapped_pred_dict, **mapped_target_dict) - + loss = self.get_loss(**refined_args) self._checked = True - + return loss class LossFunc(LossBase): - """提供给用户使用自定义损失函数的类 """ - def __init__(self, func, key_map=None, **kwargs): - """ + 别名::class:`fastNLP.LossFunc` :class:`fastNLP.core.losses.LossFunc` - :param func: 用户自行定义的损失函数,应当为一个函数或者callable(func)为True的ojbect - :param dict key_map: 参数映射表。键为Model/DataSet参数名,值为损失函数参数名。 - fastNLP的trainer将在训练时从模型返回值或者训练数据DataSet的target=True的field中 - 找到相对应的参数名为value的参数,并传入func中作为参数名为key的参数 - :param kwargs: 除了参数映射表以外可以用key word args的方式设置参数映射关系 + 提供给用户使用自定义损失函数的类 - Example:: + :param func: 用户自行定义的损失函数,应当为一个函数或者callable(func)为True的ojbect + :param dict key_map: 参数映射表。键为Model/DataSet参数名,值为损失函数参数名。 + fastNLP的trainer将在训练时从模型返回值或者训练数据DataSet的target=True的field中 + 找到相对应的参数名为value的参数,并传入func中作为参数名为key的参数 + :param kwargs: 除了参数映射表以外可以用key word args的方式设置参数映射关系 - >>> func = torch.nn.CrossEntropyLoss() - >>> loss_func = LossFunc(func, input="pred", target="label") - >>> # 这表示构建了一个损失函数类,由func计算损失函数,其中将从模型返回值或者DataSet的target=True的field - >>> # 当中找到一个参数名为`pred`的参数传入func一个参数名为`input`的参数;找到一个参数名为`label`的参数 - >>> # 传入func作为一个名为`target`的参数 + Example:: - """ + >>> func = torch.nn.CrossEntropyLoss() + >>> loss_func = LossFunc(func, input="pred", target="label") + # 这表示构建了一个损失函数类,由func计算损失函数,其中将从模型返回值或者DataSet的target=True的field + # 当中找到一个参数名为`pred`的参数传入func一个参数名为`input`的参数;找到一个参数名为`label`的参数 + # 传入func作为一个名为`target`的参数 + + """ + + def __init__(self, func, key_map=None, **kwargs): + super(LossFunc, self).__init__() _check_function_or_method(func) if key_map is not None: @@ -199,94 +200,108 @@ class LossFunc(LossBase): if len(kwargs) > 0: for key, val in kwargs.items(): self.param_map.update({key: val}) - + self.get_loss = func class CrossEntropyLoss(LossBase): """ - .. _CrossEntropyLoss: + 别名::class:`fastNLP.CrossEntropyLoss` :class:`fastNLP.core.losses.CrossEntropyLoss` - 交叉熵损失函数""" - def __init__(self, pred=None, target=None, padding_idx=-100): - """ - :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` - :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` - :param padding_idx: padding的index,在计算loss时将忽略target中标号为padding_idx的内容 + 交叉熵损失函数 + + :param pred: 参数映射表中 `pred` 的映射关系,None表示映射关系为 `pred` -> `pred` + :param target: 参数映射表中 `target` 的映射关系,None表示映射关系为 `target` -> `target` + :param padding_idx: padding的index,在计算loss时将忽略target中标号为padding_idx的内容 - Example:: + Example:: - >>> loss = CrossEntropyLoss(pred='pred', target='label', padding_idx=0) - """ + >>> loss = CrossEntropyLoss(pred='pred', target='label', padding_idx=0) + + """ + + def __init__(self, pred=None, target=None, padding_idx=-100): # TODO 需要做一些检查,F.cross_entropy在计算时,如果pred是(16, 10 ,4), target的形状按道理应该是(16, 10), 但实际却需要 # TODO (16, 4) super(CrossEntropyLoss, self).__init__() self._init_param_map(pred=pred, target=target) self.padding_idx = padding_idx - + def get_loss(self, pred, target): return F.cross_entropy(input=pred, target=target, ignore_index=self.padding_idx) class L1Loss(LossBase): - """L1损失函数""" + """ + 别名::class:`fastNLP.L1Loss` :class:`fastNLP.core.losses.L1Loss` + + L1损失函数 + + :param pred: 参数映射表中 `pred` 的映射关系,None表示映射关系为 `pred` -> `pred` + :param target: 参数映射表中 `target` 的映射关系,None表示映射关系为 `target` >`target` + + """ + def __init__(self, pred=None, target=None): - """ - :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` - :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` - """ super(L1Loss, self).__init__() self._init_param_map(pred=pred, target=target) - + def get_loss(self, pred, target): return F.l1_loss(input=pred, target=target) class BCELoss(LossBase): - """二分类交叉熵损失函数""" + """ + 别名::class:`fastNLP.BCELoss` :class:`fastNLP.core.losses.BCELoss` + + 二分类交叉熵损失函数 + + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + """ + def __init__(self, pred=None, target=None): - """ - :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` - :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` - """ super(BCELoss, self).__init__() self._init_param_map(pred=pred, target=target) - + def get_loss(self, pred, target): return F.binary_cross_entropy(input=pred, target=target) class NLLLoss(LossBase): - """负对数似然损失函数""" + """ + 别名::class:`fastNLP.NLLLoss` :class:`fastNLP.core.losses.NLLLoss` + + 负对数似然损失函数 + + :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` + :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` + """ + def __init__(self, pred=None, target=None): - """ - :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` - :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` - """ super(NLLLoss, self).__init__() self._init_param_map(pred=pred, target=target) - + def get_loss(self, pred, target): return F.nll_loss(input=pred, target=target) class LossInForward(LossBase): """ - - .. _LossInForward: + 别名::class:`fastNLP.LossInForward` :class:`fastNLP.core.losses.LossInForward` 从forward()函数返回结果中获取loss + + :param str loss_key: 在forward函数中loss的键名,默认为loss """ + def __init__(self, loss_key='loss'): - """ - :param str loss_key: 在forward函数中loss的键名,默认为loss - """ super().__init__() if not isinstance(loss_key, str): raise TypeError(f"Only str allowed for loss_key, got {type(loss_key)}.") self.loss_key = loss_key - + def get_loss(self, **kwargs): if self.loss_key not in kwargs: check_res = _CheckRes( @@ -298,17 +313,17 @@ class LossInForward(LossBase): varargs=[]) raise _CheckError(check_res=check_res, func_signature=_get_func_signature(self.get_loss)) return kwargs[self.loss_key] - + def __call__(self, pred_dict, target_dict, check=False): - + loss = self.get_loss(**pred_dict) - + if not (isinstance(loss, torch.Tensor) and len(loss.size()) == 0): if not isinstance(loss, torch.Tensor): raise TypeError(f"Loss excepted to be a torch.Tensor, got {type(loss)}") loss = torch.sum(loss) / (loss.view(-1)).size(0) # raise RuntimeError(f"The size of loss excepts to be torch.Size([]), got {loss.size()}") - + return loss @@ -378,13 +393,13 @@ def mask(predict, truth, **kwargs): if kwargs.get("mask") is None: return predict, truth mask = kwargs["mask"] - + predict, truth = squash(predict, truth) mask = mask.view(-1, ) - + predict = torch.masked_select(predict.permute(1, 0), mask).view(predict.size()[-1], -1).permute(1, 0) truth = torch.masked_select(truth, mask) - + return predict, truth @@ -399,4 +414,3 @@ def make_mask(lens, tar_len): mask = [torch.ge(lens, i + 1) for i in range(tar_len)] mask = torch.stack(mask, 1) return mask - diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 938a67be..829684d3 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -1,31 +1,25 @@ """ - - .. _Metric: +metrics 模块实现了 fastNLP 所需的各种常用衡量指标,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ - - - - import inspect from collections import defaultdict import numpy as np import torch -from fastNLP.core.utils import _CheckError -from fastNLP.core.utils import _CheckRes -from fastNLP.core.utils import _build_args -from fastNLP.core.utils import _check_arg_dict_list -from fastNLP.core.utils import _get_func_signature -from fastNLP.core.utils import seq_lens_to_masks -from fastNLP.core.vocabulary import Vocabulary +from .utils import _CheckError +from .utils import _CheckRes +from .utils import _build_args +from .utils import _check_arg_dict_list +from .utils import _get_func_signature +from .utils import seq_lens_to_masks +from .vocabulary import Vocabulary class MetricBase(object): - """所有metrics的基类 - - 所有的传入到Trainer, Tester的Metric需要继承自该对象。需要覆盖写入evaluate(), get_metric()方法。 + """ + 所有metrics的基类,,所有的传入到Trainer, Tester的Metric需要继承自该对象,需要覆盖写入evaluate(), get_metric()方法。 evaluate(xxx)中传入的是一个batch的数据。 @@ -94,17 +88,17 @@ class MetricBase(object): return {'acc': acc} # 需要返回一个dict,key为该metric的名称,该名称会显示到Trainer的progress bar中 - ``MetricBase`` 将会在输入的字典``pred_dict``和``target_dict``中进行检查. - ``pred_dict`` 是模型当中``forward()``函数或者``predict()``函数的返回值. - ``target_dict`` 是DataSet当中的ground truth, 判定ground truth的条件是field的``is_target``被设置为True. + ``MetricBase`` 将会在输入的字典 ``pred_dict`` 和 ``target_dict`` 中进行检查. + ``pred_dict`` 是模型当中 ``forward()`` 函数或者 ``predict()`` 函数的返回值. + ``target_dict`` 是DataSet当中的ground truth, 判定ground truth的条件是field的 ``is_target`` 被设置为True. ``MetricBase`` 会进行以下的类型检测: 1. self.evaluate当中是否有varargs, 这是不支持的. - 2. self.evaluate当中所需要的参数是否既不在``pred_dict``也不在``target_dict``. - 3. self.evaluate当中所需要的参数是否既在``pred_dict``也在``target_dict``. + 2. self.evaluate当中所需要的参数是否既不在 ``pred_dict`` 也不在 ``target_dict`` . + 3. self.evaluate当中所需要的参数是否既在 ``pred_dict`` 也在 ``target_dict`` . - 除此以外,在参数被传入self.evaluate以前,这个函数会检测``pred_dict``和``target_dict``当中没有被用到的参数 + 除此以外,在参数被传入self.evaluate以前,这个函数会检测 ``pred_dict`` 和 ``target_dict`` 当中没有被用到的参数 如果kwargs是self.evaluate的参数,则不会检测 @@ -267,13 +261,18 @@ class MetricBase(object): class AccuracyMetric(MetricBase): - """准确率Metric""" + """ + + 别名::class:`fastNLP.AccuracyMetric` :class:`fastNLP.core.metrics.AccuracyMetric` + + 准确率Metric(其它的Metric参见 :doc:`fastNLP.core.metrics` ) + + :param pred: 参数映射表中 `pred` 的映射关系,None表示映射关系为 `pred` -> `pred` + :param target: 参数映射表中 `target` 的映射关系,None表示映射关系为 `target` -> `target` + :param seq_len: 参数映射表中 `seq_lens` 的映射关系,None表示映射关系为 `seq_len` -> `seq_len` + """ def __init__(self, pred=None, target=None, seq_len=None): - """ - :param pred: 参数映射表中`pred`的映射关系,None表示映射关系为`pred`->`pred` - :param target: 参数映射表中`target`的映射关系,None表示映射关系为`target`->`target` - :param seq_len: 参数映射表中`seq_lens`的映射关系,None表示映射关系为`seq_len`->`seq_len` - """ + super().__init__() self._init_param_map(pred=pred, target=target, seq_len=seq_len) @@ -282,7 +281,8 @@ class AccuracyMetric(MetricBase): self.acc_count = 0 def evaluate(self, pred, target, seq_len=None): - """evaluate函数将针对一个批次的预测结果做评价指标的累计 + """ + evaluate函数将针对一个批次的预测结果做评价指标的累计 :param torch.Tensor pred: 预测的tensor, tensor的形状可以是torch.Size([B,]), torch.Size([B, n_classes]), torch.Size([B, max_len]), 或者torch.Size([B, max_len, n_classes]) @@ -327,7 +327,8 @@ class AccuracyMetric(MetricBase): self.total += np.prod(list(pred.size())) def get_metric(self, reset=True): - """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果. + """ + get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果. :param bool reset: 在调用完get_metric后是否清空评价指标统计量. :return dict evaluate_result: {"acc": float} @@ -430,8 +431,6 @@ def _bio_tag_to_spans(tags, ignore_labels=None): class SpanFPreRecMetric(MetricBase): """ - .. _SpanFPreRecMetric: - 在序列标注问题中,以span的方式计算F, pre, rec. 比如中文Part of speech中,会以character的方式进行标注,句子'中国在亚洲'对应的POS可能为(以BMES为例) ['B-NN', 'E-NN', 'S-DET', 'B-NN', 'E-NN']。该metric就是为类似情况下的F1计算。 @@ -455,26 +454,24 @@ class SpanFPreRecMetric(MetricBase): ... } + :param tag_vocab: 标签的 :class:`~fastNLP.Vocabulary` 。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), + 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. + :param str pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 + :param str target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 + :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_lens'取数据。 + :param str encoding_type: 目前支持bio, bmes + :param list ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'这 + 个label + :param bool only_gross: 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个 + label的f1, pre, rec + :param str f_type: 'micro'或'macro'. 'micro':通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; 'macro': + 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) + :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 + 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 """ def __init__(self, tag_vocab, pred=None, target=None, seq_len=None, encoding_type='bio', ignore_labels=None, only_gross=True, f_type='micro', beta=1): - """ - - :param Vocabulary tag_vocab: 标签的vocabulary。支持的标签为"B"(没有label);或"B-xxx"(xxx为某种label,比如POS中的NN), - 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. - :param str pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 - :param str target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_lens'取数据。 - :param str encoding_type: 目前支持bio, bmes - :param list ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'这 - 个label - :param bool only_gross: 是否只计算总的f1, precision, recall的值;如果为False,不仅返回总的f1, pre, rec, 还会返回每个 - label的f1, pre, rec - :param str f_type: 'micro'或'macro'. 'micro':通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; 'macro': - 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) - :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 - 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 - """ + encoding_type = encoding_type.lower() if not isinstance(tag_vocab, Vocabulary): @@ -647,20 +644,18 @@ class BMESF1PreRecMetric(MetricBase): target形状为 (batch_size, max_len) seq_lens形状为 (batch_size, ) - """ + 需要申明BMES这四种tag中,各种tag对应的idx。所有不为b_idx, m_idx, e_idx, s_idx的数字都认为是s_idx。 + :param b_idx: int, Begin标签所对应的tag idx. + :param m_idx: int, Middle标签所对应的tag idx. + :param e_idx: int, End标签所对应的tag idx. + :param s_idx: int, Single标签所对应的tag idx + :param pred: str, 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 + :param target: str, 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 + :param seq_len: str, 用该key在evaluate()时从传入dict中取出seqence length数据。为None,则使用'seq_len'取数据。 + """ + def __init__(self, b_idx=0, m_idx=1, e_idx=2, s_idx=3, pred=None, target=None, seq_len=None): - """ - 需要申明BMES这四种tag中,各种tag对应的idx。所有不为b_idx, m_idx, e_idx, s_idx的数字都认为是s_idx。 - - :param b_idx: int, Begin标签所对应的tag idx. - :param m_idx: int, Middle标签所对应的tag idx. - :param e_idx: int, End标签所对应的tag idx. - :param s_idx: int, Single标签所对应的tag idx - :param pred: str, 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 - :param target: str, 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param seq_len: str, 用该key在evaluate()时从传入dict中取出seqence length数据。为None,则使用'seq_len'取数据。 - """ super().__init__() self._init_param_map(pred=pred, target=target, seq_len=seq_len) @@ -831,21 +826,23 @@ def _pred_topk(y_prob, k=1): class SQuADMetric(MetricBase): - """SQuAD数据集metric + """ + SQuAD数据集metric + + :param pred1: 参数映射表中`pred1`的映射关系,None表示映射关系为`pred1`->`pred1` + :param pred2: 参数映射表中`pred2`的映射关系,None表示映射关系为`pred2`->`pred2` + :param target1: 参数映射表中`target1`的映射关系,None表示映射关系为`target1`->`target1` + :param target2: 参数映射表中`target2`的映射关系,None表示映射关系为`target2`->`target2` + :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 + 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 + :param bool right_open: right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 + :param bool print_predict_stat: True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 + """ def __init__(self, pred1=None, pred2=None, target1=None, target2=None, beta=1, right_open=True, print_predict_stat=False): - """ - :param pred1: 参数映射表中`pred1`的映射关系,None表示映射关系为`pred1`->`pred1` - :param pred2: 参数映射表中`pred2`的映射关系,None表示映射关系为`pred2`->`pred2` - :param target1: 参数映射表中`target1`的映射关系,None表示映射关系为`target1`->`target1` - :param target2: 参数映射表中`target2`的映射关系,None表示映射关系为`target2`->`target2` - :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 - 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 - :param bool right_open: right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 - :param bool print_predict_stat: True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 - """ + super(SQuADMetric, self).__init__() self._init_param_map(pred1=pred1, pred2=pred2, target1=target1, target2=target2) diff --git a/fastNLP/core/optimizer.py b/fastNLP/core/optimizer.py index 584aa5ff..ea4905eb 100644 --- a/fastNLP/core/optimizer.py +++ b/fastNLP/core/optimizer.py @@ -1,11 +1,16 @@ +""" +optimizer 模块定义了 fastNLP 中所需的各种优化器,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 + +""" import torch class Optimizer(object): """ + 别名::class:`fastNLP.Optimizer` :class:`fastNLP.core.optimizer.Optimizer` - :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. - :param kwargs: additional parameters. + :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. + :param kwargs: additional parameters. """ def __init__(self, model_params, **kwargs): if model_params is not None and not hasattr(model_params, "__next__"): @@ -26,10 +31,11 @@ class Optimizer(object): class SGD(Optimizer): """ + 别名::class:`fastNLP.SGD` :class:`fastNLP.core.optimizer.SGD` - :param float lr: learning rate. Default: 0.01 - :param float momentum: momentum. Default: 0 - :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. + :param float lr: learning rate. Default: 0.01 + :param float momentum: momentum. Default: 0 + :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. """ def __init__(self, lr=0.001, momentum=0, model_params=None): @@ -47,10 +53,11 @@ class SGD(Optimizer): class Adam(Optimizer): """ + 别名::class:`fastNLP.Adam` :class:`fastNLP.core.optimizer.Adam` - :param float lr: learning rate - :param float weight_decay: - :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. + :param float lr: learning rate + :param float weight_decay: + :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. """ def __init__(self, lr=0.001, weight_decay=0, betas=(0.9, 0.999), eps=1e-8, amsgrad=False, model_params=None): diff --git a/fastNLP/core/predictor.py b/fastNLP/core/predictor.py index ae648e47..34784b7c 100644 --- a/fastNLP/core/predictor.py +++ b/fastNLP/core/predictor.py @@ -2,10 +2,10 @@ from collections import defaultdict import torch -from fastNLP.core import Batch -from fastNLP.core import DataSet -from fastNLP.core import SequentialSampler -from fastNLP.core.utils import _build_args +from . import Batch +from . import DataSet +from . import SequentialSampler +from .utils import _build_args class Predictor(object): diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 1f9b92fb..2182ae1c 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -1,22 +1,24 @@ """ +sampler 子类实现了 fastNLP 所需的各种采样器。 - .. _Sampler: """ - - - +__all__ = ["Sampler", "BucketSampler", "SequentialSampler", "RandomSampler"] from itertools import chain import numpy as np -import torch + class Sampler(object): - """ `Sampler` 类的基类. 规定以何种顺序取出data中的元素 + """ + 别名::class:`fastNLP.Sampler` :class:`fastNLP.core.sampler.Sampler` + + + `Sampler` 类的基类. 规定以何种顺序取出data中的元素 子类必须实现 ``__call__`` 方法. 输入 `DataSet` 对象, 返回其中元素的下标序列 """ - + def __call__(self, data_set): """ :param DataSet data_set: `DataSet` 对象, 需要Sample的数据 @@ -26,56 +28,62 @@ class Sampler(object): class SequentialSampler(Sampler): - """顺序取出元素的 `Sampler` - - .. _SequentialSampler: + """ + 别名::class:`fastNLP.SequentialSampler` :class:`fastNLP.core.sampler.SequentialSampler` + + 顺序取出元素的 `Sampler` """ + def __call__(self, data_set): return list(range(len(data_set))) class RandomSampler(Sampler): """ - - .. _RandomSampler: + 别名::class:`fastNLP.RandomSampler` :class:`fastNLP.core.sampler.RandomSampler` 随机化取元素的 `Sampler` """ + def __call__(self, data_set): return list(np.random.permutation(len(data_set))) class BucketSampler(Sampler): - """带Bucket的 `Random Sampler`. 可以随机地取出长度相似的元素 + """ + 别名::class:`fastNLP.BucketSampler` :class:`fastNLP.core.sampler.BucketSampler` + + 带Bucket的 `Random Sampler`. 可以随机地取出长度相似的元素 :param int num_buckets: bucket的数量 :param int batch_size: batch的大小 :param str seq_lens_field_name: 对应序列长度的 `field` 的名字 """ + def __init__(self, num_buckets=10, batch_size=32, seq_lens_field_name='seq_len'): self.num_buckets = num_buckets self.batch_size = batch_size self.seq_lens_field_name = seq_lens_field_name - + def __call__(self, data_set): seq_lens = data_set.get_all_fields()[self.seq_lens_field_name].content total_sample_num = len(seq_lens) - + bucket_indexes = [] - assert total_sample_num>=self.num_buckets, "The number of samples is smaller than the number of buckets." + assert total_sample_num >= self.num_buckets, "The number of samples is smaller than the number of buckets." num_sample_per_bucket = total_sample_num // self.num_buckets for i in range(self.num_buckets): bucket_indexes.append([num_sample_per_bucket * i, num_sample_per_bucket * (i + 1)]) bucket_indexes[-1][1] = total_sample_num - + sorted_seq_lens = list(sorted([(idx, seq_len) for idx, seq_len in zip(range(total_sample_num), seq_lens)], key=lambda x: x[1])) - + batchs = [] - + left_init_indexes = [] for b_idx in range(self.num_buckets): start_idx = bucket_indexes[b_idx][0] @@ -90,7 +98,7 @@ class BucketSampler(Sampler): if (left_init_indexes) != 0: batchs.append(left_init_indexes) np.random.shuffle(batchs) - + return list(chain(*batchs)) @@ -128,10 +136,10 @@ def k_means_1d(x, k, max_iter=100): if len(sorted_x) < k: raise ValueError("too few buckets") gap = len(sorted_x) / k - + centroids = np.array([sorted_x[int(x * gap)] for x in range(k)]) assign = None - + for i in range(max_iter): # Cluster Assignment step assign = np.array([np.argmin([np.absolute(x_i - x) for x in centroids]) for x_i in x]) @@ -163,7 +171,7 @@ def k_means_bucketing(lengths, buckets): bucket_data = [[] for _ in buckets] num_buckets = len(buckets) _, assignments = k_means_1d(lengths, num_buckets) - + for idx, bucket_id in enumerate(assignments): if buckets[bucket_id] is None or lengths[idx] <= buckets[bucket_id]: bucket_data[bucket_id].append(idx) diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index c2aae37b..6eaa5add 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -1,81 +1,81 @@ -import torch -from torch import nn +""" +tester模块实现了 fastNLP 所需的Tester类,能在提供数据、模型以及metric的情况下进行性能测试。 -from fastNLP.core.batch import Batch -from fastNLP.core.dataset import DataSet -from fastNLP.core.metrics import _prepare_metrics -from fastNLP.core.sampler import SequentialSampler -from fastNLP.core.utils import _CheckError -from fastNLP.core.utils import _build_args -from fastNLP.core.utils import _check_loss_evaluate -from fastNLP.core.utils import _move_dict_value_to_device -from fastNLP.core.utils import _get_func_signature -from fastNLP.core.utils import _get_model_device -from fastNLP.core.utils import _move_model_to_device +Example:: + import numpy as np + import torch + from torch import nn + from fastNLP import Tester + from fastNLP import DataSet + from fastNLP import AccuracyMetric -class Tester(object): - """ - Tester是在提供数据,模型以及metric的情况下进行性能测试的类 + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a): + return {'pred': self.fc(a.unsqueeze(1)).squeeze(1)} - Example:: + model = Model() - import numpy as np - import torch - from torch import nn - from fastNLP import Tester - from fastNLP import DataSet - from fastNLP import AccuracyMetric + dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) + dataset.set_input('a') + dataset.set_target('b') - class Model(nn.Module): - def __init__(self): - super().__init__() - self.fc = nn.Linear(1, 1) - def forward(self, a): - return {'pred': self.fc(a.unsqueeze(1)).squeeze(1)} + tester = Tester(dataset, model, metrics=AccuracyMetric()) + eval_results = tester.test() - model = Model() +这里Metric的映射规律是和 :class:`fastNLP.Trainer` 中一致的,具体使用请参考 :doc:`trainer 模块` 的1.3部分 - dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) - dataset.set_input('a') - dataset.set_target('b') - tester = Tester(dataset, model, metrics=AccuracyMetric()) - eval_results = tester.test() - - 这里Metric的映射规律是和 Trainer_ 中一致的,请参考 Trainer_ 使用metrics。 +""" +import torch +from torch import nn +from .batch import Batch +from .dataset import DataSet +from .metrics import _prepare_metrics +from .sampler import SequentialSampler +from .utils import _CheckError +from .utils import _build_args +from .utils import _check_loss_evaluate +from .utils import _move_dict_value_to_device +from .utils import _get_func_signature +from .utils import _get_model_device +from .utils import _move_model_to_device +class Tester(object): """ + 别名::class:`fastNLP.Tester` :class:`fastNLP.core.tester.Tester` - def __init__(self, data, model, metrics, batch_size=16, device=None, verbose=1): - """传入模型,数据以及metric进行验证。 - - :param DataSet data: 需要测试的数据集 - :param torch.nn.module model: 使用的模型 - :param MetricBase metrics: 一个Metric或者一个列表的metric对象 - :param int batch_size: evaluation时使用的batch_size有多大。 - :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 - 的计算位置进行管理。支持以下的输入: + Tester是在提供数据,模型以及metric的情况下进行性能测试的类。需要传入模型,数据以及metric进行验证。 - 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, - 可见的第二个GPU中; + :param data: 需要测试的数据集, :class:`~fastNLP.DataSet` 类型 + :param torch.nn.module model: 使用的模型 + :param metrics: :class:`~fastNLP.core.metrics.MetricBase` 或者一个列表的 :class:`~fastNLP.core.metrics.MetricBase` + :param int batch_size: evaluation时使用的batch_size有多大。 + :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 + 的计算位置进行管理。支持以下的输入: - 2. torch.device:将模型装载到torch.device上。 + 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; - 3. int: 将使用device_id为该值的gpu进行训练 + 2. torch.device:将模型装载到torch.device上。 - 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 + 3. int: 将使用device_id为该值的gpu进行训练 - 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 - :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 + 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 - """ + :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 + """ + def __init__(self, data, model, metrics, batch_size=16, device=None, verbose=1): super(Tester, self).__init__() if not isinstance(data, DataSet): @@ -103,7 +103,7 @@ class Tester(object): def test(self): """开始进行验证,并返回验证结果。 - :return dict(dict) eval_results: dict为二层嵌套结构,dict的第一层是metric的名称; 第二层是这个metric的指标。 + :return Dict[Dict] : dict的二层嵌套结构,dict的第一层是metric的名称; 第二层是这个metric的指标。 一个AccuracyMetric的例子为{'AccuracyMetric': {'acc': 1.0}}。 """ # turn on the testing mode; clean up the history diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 253ae46d..9cd3d91e 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -1,13 +1,17 @@ """ -Trainer的说明文档 - - .. _Trainer: - -Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰写 (1) epoch循环; (2) 将数据分成不同的Batch; (3) -对Batch进行pad; (4) 每个epoch结束或一定step后进行验证集验证; (5) 保存获得更好验证性能的模型等。 - -1. Trainer的基本使用 - +Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰以下步骤的代码 + + (1) epoch循环; + + (2) 将数据分成不同的Batch; + + (3) 对Batch进行pad; + + (4) 每个epoch结束或一定step后进行验证集验证; + + (5) 保存获得更好验证性能的模型。 + +1 Trainer的基本使用 下面的例子是使用神经网络来进行预测一个序列中是否有偶数个1。 Example:: @@ -20,8 +24,8 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 from fastNLP import DataSet from fastNLP import Trainer - from fastNLP.core.losses import CrossEntropyLoss - from fastNLP.core.metrics import AccuracyMetric + from fastNLP import CrossEntropyLoss + from fastNLP import AccuracyMetric from fastNLP.modules.decoder import MLP # 模型 @@ -56,208 +60,214 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 由上面的例子可以看出通过使用Trainer,可以使得训练部分的代码大幅减少。 使用Trainer需要满足以下几个条件: - 1. 模型 +1.1 模型 + 1 模型的forward()的参数名需要与DataSet中的名字对应。实际上fastNLP在将DataSet中的数据传递给模型forward()时,是 + 通过匹配名称实现的。所以上例中,如果Model的forward函数修改为forward(self, data), 则DataSet中的'x'这个field就应该 + 改名为'data'。 - 1. 模型的forward()的参数名需要与DataSet中的名字对应。实际上fastNLP在将DataSet中的数据传递给模型forward()时,是 - 通过匹配名称实现的。所以上例中,如果Model的forward函数修改为forward(self, data), 则DataSet中的'x'这个field就应该 - 改名为'data'。 + 2 传递给forward()的参数是DataSet中被设置为input的那些field。但如果forward()中没有对应的参数,则不会将数据传递 + 给forward()。例如,DataSet中'x1', 'x2'都是input,但是模型的函数为forward(self, x1), 那么'x2'不会传递给forward()。 - 2. 传递给forward()的参数是DataSet中被设置为input的那些field。但如果forward()中没有对应的参数,则不会将数据传递 - 给forward()。例如,DataSet中'x1', 'x2'都是input,但是模型的函数为forward(self, x1), 那么'x2'不会传递给forward()。 + 3 模型的forward()返回值需要为一个dict。 - 3. 模型的forward()返回值需要为一个dict。 +1.2 Loss + fastNLP中的为了不限制forward函数的返回内容数量(比如一些复杂任务需要返回多个内容,如Dependency Parsing, + :mod:`Loss` 与 :mod:`Metric` 都使用了通过名称来匹配相应内容的策略。如上面的例子中 - 2. Loss + Example:: - fastNLP中的为了不限制forward函数的返回内容数量(比如一些复杂任务需要返回多个内容,如Dependency Parsing, Loss_ 与 Metric_ 都使 - 用了通过名称来匹配相应内容的策略。如上面的例子中 + trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'), + optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000, + dev_data = dev_data, metrics=AccuracyMetric(target='label')) + + loss被设置为了 :class:`~fastNLP.CrossEntropyLoss` , 但在初始化的时候传入了target='label'这个参数, + :class:`~fastNLP.CrossEntropyLoss` 的初始化参数为(pred=None, target=None, padding_idx=-100)。 + + 这里的两个参数分别为计算CrossEntropy时需要使用到的模型的预测值与真实值。 + 其中 `pred` 一般来自于模型forward()的返回结果,`target` 一般是来自于DataSet中被设置为target的field。 + 由于每个人对真实值或者model的返回值取名并不一样,所以fastNLP的 :mod:`Loss` 提供一种类似于映射的机制来匹配对应的值, + 比如这里 :class:`~fastNLP.CrossEntropyLoss` 将尝试找到名为'label'的内容来作为真实值得到loss; + 而pred=None, 则 :class:`~fastNLP.CrossEntropyLoss` 使用'pred'作为名称匹配预测值, + 正好forward的返回值也叫pred,所以这里不需要申明pred。 + + 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。 + fastNLP中提供了 :class:`~fastNLP.LossInForward` 这个loss。 + 这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor,并使用它作为loss。 + 如果Trainer初始化没有提供loss则默认使用 :class:`~fastNLP.LossInForward` 。TODO 补充一个例子 详细例子可以参照 + +1.3 Metric + :mod:`Metric` 使用了与上述Loss一样的策略,即使用名称进行匹配。 + AccuracyMetric(target='label')的情况与CrossEntropyLoss 是同理的。 + + 在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, + 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果, + 传入到predict()的参数也是从DataSet中被设置为input的field中选择出来的; + 与forward()一样,返回值需要为一个dict。 TODO 补充一个例子 具体例子可以参考 - Example:: +2 Trainer的代码检查 + 由于在fastNLP中采取了映射的机制,所以难免可能存在对应出错的情况。Trainer提供一种映射检查机制,可以通过check_code_level来进行控制 + 比如下面的例子中,由于各种原因产生的报错 + +Example2.1 + :: + + import numpy as np + from torch import nn + import torch + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet - trainer = Trainer(tr_dataset, model, loss=CrossEntropyLoss(target='label'), - optimizer=SGD(model.parameters(), lr=0.1),n_epochs=1000, - dev_data = dev_data, metrics=AccuracyMetric(target='label')) + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, x, b): + loss = torch.mean((self.fc(x)-b)**2) + return {'loss': loss} + model = Model() - loss被设置为了 CrossEntropyLoss_ , 但在初始化的时候传入了target='label'这个参数, CrossEntropyLoss_ 的初始化 - 参数为(pred=None, target=None, padding_idx=-100)。这里的两个参数分别为计算CrossEntropy时需要使用到的模型的预测值 - 与真实值。其中'pred'一般来自于模型forward()的返回结果,'target'一般是来自于DataSet中被设置为target的 - field。由于每个人对真实值或者model的返回值取名并不一样,所以fastNLP的 Loss_ 提供一种类似于映射的机制来匹配 - 对应的值,比如这里 CrossEntropyLoss_ 将尝试找到名为'label'的内容来作为真实值得到loss;而pred=None, 则 CrossEntropyLoss_ - 使用'pred'作为名称匹配预测值,正好forward的返回值也叫pred,所以这里不需要申明pred。 + dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) + dataset.set_input('a', 'b') - 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。fastNLP中提供了 LossInForward_ 这 - 个loss。这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor, - 并使用它作为loss。 如果Trainer初始化没有提供loss则默认使用 LossInForward_ 。详细例子可以参照 TODO 补充一个例子 + trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001)) - 3. Metric + trainer = Trainer(dataset, model, SGD(model.parameters())) + # 会报以下的错误 + # input fields after batch(if batch size is 2): + # a: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + # b: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + # There is no target field. + # .... + # NameError: + # Problems occurred when calling Model.forward(self, x, b) + # missing param: ['x'] + # unused field: ['a'] + # Suggestion: You need to provide ['x'] in DataSet and set it as input. - Metric_ 使用了与上述Loss一样的策略,即使用名称进行匹配。AccuracyMetric(target='label')的情况与CrossEntropyLoss 是同理的。 + 这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里有两类 + 信息可以为你提供参考 - 在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, - 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果,传入到predict()的参数也是从DataSet中被设置为input - 的field中选择出来的; 与forward()一样,返回值需要为一个dict。具体例子可以参考 TODO 补充一个例子 + 1 'input fields after batch...'这部分显示的是train dataset经过Batch操作后,每个field对应的类型以及进行shape。这里 + 因为train dataset没有target所以没有显示。根据这里可以看出是否正确将需要的内容设置为了input或target。 -2. Trainer的代码检查 + 2 NameError,NameError发生在映射出错的情况。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 + 出当前是在调取forward),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 + 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x',或者model的参数从'x'修改为'a'都可以解决问题。 - 由于在fastNLP中采取了映射的机制,所以难免可能存在对应出错的情况。Trainer提供一种映射检查机制,可以通过check_code_level来进行控制 - 比如下面的例子中,由于各种原因产生的报错 + 下面的例子是由于loss计算的时候找不到需要的值 - Example1:: - - import numpy as np - from torch import nn - import torch - from torch.optim import SGD - from fastNLP import Trainer - from fastNLP import DataSet - - class Model(nn.Module): - def __init__(self): - super().__init__() - self.fc = nn.Linear(1, 1) - def forward(self, x, b): - loss = torch.mean((self.fc(x)-b)**2) - return {'loss': loss} - model = Model() - - dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) - dataset.set_input('a', 'b') - - trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001)) - - trainer = Trainer(dataset, model, SGD(model.parameters())) - # 会报以下的错误 - # input fields after batch(if batch size is 2): - # a: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) - # b: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) - # There is no target field. - # .... - # NameError: - # Problems occurred when calling Model.forward(self, x, b) - # missing param: ['x'] - # unused field: ['a'] - # Suggestion: You need to provide ['x'] in DataSet and set it as input. - - 这里就是由于在Trainer初始化的时候,fastNLP会尝试使用一个batch_size=2的batch去运行一遍forward()以及backward()。这里有两类 - 信息可以为你提供参考 - - 1. 'input fields after batch...'这部分显示的是train dataset经过Batch操作后,每个field对应的类型以及进行shape。这里 - 因为train dataset没有target所以没有显示。根据这里可以看出是否正确将需要的内容设置为了input或target。 - - 2. NameError,NameError发生在映射出错的情况。这里报错的原因是由于尝试进行forward计算时(可以通过Model.forward(self, x, b)判断 - 出当前是在调取forward),却没有获取到forward()函数中需要的'x';在报错信息中同时指出了缺'x',而'a'没有被使用,那么可能 - 就是由于field的名称不对。这里将dataset中'a'这个field的名称改为'x',或者model的参数从'x'修改为'a'都可以解决问题。 - - 下面的例子是由于loss计算的时候找不到需要的值 - - Example2:: - - import numpy as np - from torch import nn - from torch.optim import SGD - from fastNLP import Trainer - from fastNLP import DataSet - from fastNLP.core.losses import L1Loss - import torch - - class Model(nn.Module): - def __init__(self): - super().__init__() - self.fc = nn.Linear(1, 1) - def forward(self, a): - return {'pred_b': self.fc(a.unsqueeze(1)).squeeze(1), 'No use':1} - - model = Model() - - dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) - - dataset.set_input('a') - dataset.set_target('b') - - trainer = Trainer(dataset, model, loss=L1Loss(target='label'), optimizer=SGD(model.parameters(), lr=0.001)) - # 报错信息如下 - # input fields after batch(if batch size is 2): - # a: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) - # target fields after batch(if batch size is 2): - # b: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) - # .... - # NameError: - # Problems occurred when calling L1Loss.get_loss(self, pred, target) - # missing param: ['pred(assign to `pred` in `L1Loss`)', 'label(assign to `target` in `L1Loss`)'] - # unused field: ['b'] - # unused param: ['pred_b', 'No use'] - # target field: ['b'] - # param from Model.forward(self, a): ['pred_b', 'No use'] - # Suggestion: (1). Check key assignment for `target` when initialize L1Loss. Or provide `label` in DataSet or output of Model.forward(self, a). - # (2). Check key assignment for `pred` when initialize L1Loss. Or provide `pred` in DataSet or output of Model.forward(self, a). - - 报错信息也包含两部分: - - 1. 第一部分与上面是一样的 - - 2. 这里报错的原因是由于计算loss的时候找不到相应的值(通过L1Loss.get_loss(self, pred, target)判断出来的);报错的原因是因为 - `pred`和`label`(我们在初始化L1Loss时将target指定为了label)都没有找到。这里'unused field'是DataSet中出现了,但却没有 - 被设置为input或者target的field;'unused param'是forward()中返回且没有被使用到的内容;'target field'是被设置为了 - target的field; 'param from Model.forward(self, a)'是forward()返回的所有key。"Suggestion"是关于当前错误处理的建议。 - - 但是在一些情况下,比如forward()返回值只有一个,target也只有一个,fastNLP不会进行匹配,而直接将forward()的结果作为pred, 将 - DataSet中的target设置为target。上面的例子在返回值中加入了一个'No use'则只是为了使得Loss去匹配结果。 - - - 下面是带有dev dataset时如果出现错误会发生的报错, - - Example3:: - - import numpy as np - from torch import nn - from torch.optim import SGD - from fastNLP import Trainer - from fastNLP import DataSet - from fastNLP import AccuracyMetric - import torch - - class Model(nn.Module): - def __init__(self): - super().__init__() - self.fc = nn.Linear(1, 1) - def forward(self, a, b): - loss = torch.mean((self.fc(a.float().unsqueeze(1))-b.float())**2) - return {'loss': loss} - def predict(self, a): # 使用predict()进行验证 - return {'output':self.fc(a.float().unsqueeze(1))} #这里return的值不包含'pred'这个key - model = Model() - - dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) - dev_data = DataSet({'a': np.arange(10, 20), 'b':np.arange(10, 20)*2}) - - dataset.set_input('a', 'b') - dev_data.set_input('a') # 这里没有设置target - - trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001), - dev_data=dev_data, metrics=AccuracyMetric()) - - # 报错信息 - # ... - # NameError: - # Problems occurred when calling AccuracyMetric.evaluate(self, pred, target, seq_len=None) - # missing param: ['pred(assign to `pred` in `AccuracyMetric`)', 'target(assign to `target` in `AccuracyMetric`)'] - # unused param: ['output'] - # target field: [] - # param from Model.predict(self, a): ['output'] - # Suggestion: (1). Check key assignment for `pred` when initialize AccuracyMetric. Or provide `pred` in DataSet or output of Model.predict(self, a). - # (2). Check key assignment for `target` when initialize AccuracyMetric. Or provide `target` in DataSet or output of Model.predict(self, a). - - 报错信息和前面都是类似的,但是可以通过'AccuracyMetric.evaluate(self, pred, target, seq_len=None)'看出这里是evaluation - 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation弄错的情况。这里的修改是通过在初始化metric的时候 - 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output')。 +Example2.2 + :: - 可以通过check_code_level调节检查的强度。默认为0,即进行检查。 + import numpy as np + from torch import nn + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet + from fastNLP import L1Loss + import torch + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a): + return {'pred_b': self.fc(a.unsqueeze(1)).squeeze(1), 'No use':1} + + model = Model() + + dataset = DataSet({'a': np.arange(10, dtype=float), 'b':np.arange(10, dtype=float)*2}) + + dataset.set_input('a') + dataset.set_target('b') + + trainer = Trainer(dataset, model, loss=L1Loss(target='label'), optimizer=SGD(model.parameters(), lr=0.001)) + # 报错信息如下 + # input fields after batch(if batch size is 2): + # a: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) + # target fields after batch(if batch size is 2): + # b: (1)type:torch.Tensor (2)dtype:torch.float32, (3)shape:torch.Size([2]) + # .... + # NameError: + # Problems occurred when calling L1Loss.get_loss(self, pred, target) + # missing param: ['pred(assign to `pred` in `L1Loss`)', 'label(assign to `target` in `L1Loss`)'] + # unused field: ['b'] + # unused param: ['pred_b', 'No use'] + # target field: ['b'] + # param from Model.forward(self, a): ['pred_b', 'No use'] + # Suggestion: (1). Check key assignment for `target` when initialize L1Loss. Or provide `label` in DataSet or output of Model.forward(self, a). + # (2). Check key assignment for `pred` when initialize L1Loss. Or provide `pred` in DataSet or output of Model.forward(self, a). + + 报错信息也包含两部分: + + 1 第一部分与上面是一样的 + + 2 这里报错的原因是由于计算loss的时候找不到相应的值(通过L1Loss.get_loss(self, pred, target)判断出来的); + 报错的原因是因为 `pred` 和 `label` (我们在初始化L1Loss时将target指定为了label)都没有找到。 + 这里'unused field'是DataSet中出现了,但却没有被设置为input或者target的field; + 'unused param'是forward()中返回且没有被使用到的内容;'target field'是被设置为了target的field; + 'param from Model.forward(self, a)'是forward()返回的所有key。"Suggestion"是关于当前错误处理的建议。 + + 但是在一些情况下,比如forward()返回值只有一个,target也只有一个,fastNLP不会进行匹配,而直接将forward()的结果作为pred, + 将DataSet中的target设置为target。上面的例子在返回值中加入了一个'No use'则只是为了使得Loss去匹配结果。 + + + 下面是带有dev dataset时如果出现错误会发生的报错, + +Example2.3 + :: + + import numpy as np + from torch import nn + from torch.optim import SGD + from fastNLP import Trainer + from fastNLP import DataSet + from fastNLP import AccuracyMetric + import torch + + class Model(nn.Module): + def __init__(self): + super().__init__() + self.fc = nn.Linear(1, 1) + def forward(self, a, b): + loss = torch.mean((self.fc(a.float().unsqueeze(1))-b.float())**2) + return {'loss': loss} + def predict(self, a): # 使用predict()进行验证 + return {'output':self.fc(a.float().unsqueeze(1))} #这里return的值不包含'pred'这个key + model = Model() + + dataset = DataSet({'a': np.arange(10), 'b':np.arange(10)*2}) + dev_data = DataSet({'a': np.arange(10, 20), 'b':np.arange(10, 20)*2}) + + dataset.set_input('a', 'b') + dev_data.set_input('a') # 这里没有设置target + + trainer = Trainer(dataset, model, loss=None, optimizer=SGD(model.parameters(), lr=0.001), + dev_data=dev_data, metrics=AccuracyMetric()) + + # 报错信息 + # ... + # NameError: + # Problems occurred when calling AccuracyMetric.evaluate(self, pred, target, seq_len=None) + # missing param: ['pred(assign to `pred` in `AccuracyMetric`)', 'target(assign to `target` in `AccuracyMetric`)'] + # unused param: ['output'] + # target field: [] + # param from Model.predict(self, a): ['output'] + # Suggestion: (1). Check key assignment for `pred` when initialize AccuracyMetric. Or provide `pred` in DataSet or output of Model.predict(self, a). + # (2). Check key assignment for `target` when initialize AccuracyMetric. Or provide `target` in DataSet or output of Model.predict(self, a). + + 报错信息和前面都是类似的,但是可以通过'AccuracyMetric.evaluate(self, pred, target, seq_len=None)'看出这里是evaluation + 的时候发生了错误。这样避免了需要在完成一整个epoch的训练才能发现evaluation弄错的情况。这里的修改是通过在初始化metric的时候 + 指明通过'output'获取`pred`, 即AccuracyMetric(pred='output')。 -3. Trainer与callback + 可以通过check_code_level调节检查的强度。默认为0,即进行检查。 +3 Trainer与callback 虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能,比如负采样,learning rate decay, Early Stop等。 - 为了解决这个问题fastNLP引入了callback的机制,Callback_ 是一种在Trainer训练过程中特定阶段会运行的函数集合,所有的 Callback_ 都具有 - on_*(比如on_train_start, on_backward_begin)等函数。如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用。 + 为了解决这个问题fastNLP引入了callback的机制,:class:`~fastNLP.Callback` 是一种在Trainer训练过程中特定阶段会运行的函数集合, + 所有的 :class:`~fastNLP.Callback` 都具有on_*(比如on_train_start, on_backward_begin)等函数。 + 如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用。 我们将Train.train()这个函数内部分为以下的阶段,在对应阶段会触发相应的调用。 @@ -286,12 +296,11 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 callback.on_train_end() # 训练结束 callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 - fastNLP已经自带了很多callback函数供使用,可以参考 Callback_ 。一些关于callback的例子,请参考 #TODO callback的例子 + fastNLP已经自带了很多callback函数供使用,可以参考 :class:`~fastNLP.Callback` 。 + TODO callback的例子 一些关于callback的例子,请参考 """ - - import os import time from datetime import datetime @@ -300,32 +309,91 @@ from datetime import timedelta import numpy as np import torch from torch import nn -import warnings + try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import _pseudo_tqdm as tqdm - -from fastNLP.core.batch import Batch -from fastNLP.core.callback import CallbackManager, CallbackException -from fastNLP.core.dataset import DataSet -from fastNLP.core.losses import _prepare_losser -from fastNLP.core.metrics import _prepare_metrics -from fastNLP.core.sampler import Sampler -from fastNLP.core.sampler import RandomSampler -from fastNLP.core.sampler import SequentialSampler -from fastNLP.core.tester import Tester -from fastNLP.core.utils import _CheckError -from fastNLP.core.utils import _build_args -from fastNLP.core.utils import _check_forward_error -from fastNLP.core.utils import _check_loss_evaluate -from fastNLP.core.utils import _move_dict_value_to_device -from fastNLP.core.utils import _get_func_signature -from fastNLP.core.utils import _get_model_device -from fastNLP.core.optimizer import Optimizer -from fastNLP.core.utils import _move_model_to_device + from .utils import _pseudo_tqdm as tqdm + +from .batch import Batch +from .callback import CallbackManager, CallbackException +from .dataset import DataSet +from .losses import _prepare_losser +from .metrics import _prepare_metrics +from .sampler import Sampler +from .sampler import RandomSampler +from .sampler import SequentialSampler +from .tester import Tester +from .utils import _CheckError +from .utils import _build_args +from .utils import _check_forward_error +from .utils import _check_loss_evaluate +from .utils import _move_dict_value_to_device +from .utils import _get_func_signature +from .utils import _get_model_device +from .optimizer import Optimizer +from .utils import _move_model_to_device + class Trainer(object): + """ + 别名::class:`fastNLP.Trainer` :class:`fastNLP.core.trainer.Trainer` + + Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰写 + (1) epoch循环; + (2) 将数据分成不同的Batch; + (3) 对Batch进行pad; + (4) 每个epoch结束或一定step后进行验证集验证; + (5) 保存获得更好验证性能的模型等。 + + 详细的介绍参见 :doc:`fastNLP.core.trainer` + + :param train_data: 训练集, :class:`~fastNLP.DataSet` 类型。 + :param nn.modules model: 待训练的模型 + :param torch.optim.Optimizer optimizer: 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器 + :param int batch_size: 训练和验证的时候的batch大小。 + :param loss: 使用的 :class:`~fastNLP.core.losses.LossBase` 对象。当为None时,默认使用 :class:`~fastNLP.LossInForward` + :param sampler: Batch数据生成的顺序, :class:`~fastNLP.Sampler` 类型。如果为None,默认使用 :class:`~fastNLP.RandomSampler` + :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128 + 会导致内存不足,通过设置batch_size=32, update_every=4达到目的。当optimizer为None时,该参数无效。 + :param int n_epochs: 需要优化迭代多少次。 + :param int print_every: 多少次反向传播更新tqdm显示的loss; 如果use_tqdm=False, 则多少次反向传播打印loss。 + :param dev_data: 用于做验证的DataSet, :class:`~fastNLP.DataSet` 类型。 + :param metrics: 验证的评估函数。可以只使用一个 :class:`Metric` , + 也可以使用多个 :class:`Metric` ,通过列表传入。 + 如验证时取得了更好的验证结果(如果有多个Metric,以列表中第一个Metric为准),且save_path不为None, + 则保存当前模型。Metric种类详见 :doc:`metrics模块 ` 。仅在传入dev_data时有效。 + :param str,None metric_key: :class:`Metric` 有时会有多个指标, + 比如 :class:`~fastNLP.core.metrics.SpanFPreRecMetric` 中包含了'f', 'pre', 'rec'。此时需 + 要指定以哪个指标为准。另外有些指标是越小效果越好,比如语言模型的困惑度,这种情况下,在key前面增加一个'-'来表 + 明验证时,值越小越好(比如: "-ppl")。仅在传入dev_data时有效。 + :param int validate_every: 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有效。 + :param str,None save_path: 将模型保存路径。如果为None,则不保存模型。如果dev_data为None,则保存最后一次迭代的模型。 + 保存的时候不仅保存了参数,还保存了模型结构。即便使用DataParallel,这里也只保存模型。 + :param prefetch: bool, 是否使用额外的进程对产生batch数据。理论上会使得Batch迭代更快。 + :param bool use_tqdm: 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。 + :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 + 的计算位置进行管理。支持以下的输入: + + 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, + 可见的第二个GPU中; + + 2. torch.device:将模型装载到torch.device上。 + + 3. int: 将使用device_id为该值的gpu进行训练 + + 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 + + 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + + :param list(callbacks) callbacks: 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 + 通过callback机制实现。 可使用的callback参见 :doc:`callback模块 ` + :param int check_code_level: 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, + 报告警告信息; 2: 有任何field没有被使用都报错. 检查的原理是通过使用很小的batch(默认2个sample)来运行代码,但是 + 这个过程理论上不会修改任何参数,只是会检查能否运行。但如果(1)模型中存在将batch_size写为某个固定值的情况; + (2)模型中存在累加前向计算次数的,可能会多计算1次。以上情况建议将check_code_level设置为-1。 + """ + def __init__(self, train_data, model, optimizer=None, loss=None, batch_size=32, sampler=None, update_every=1, n_epochs=10, print_every=5, @@ -334,74 +402,30 @@ class Trainer(object): prefetch=False, use_tqdm=True, device=None, callbacks=None, check_code_level=0): - """ - :param DataSet train_data: 训练集 - :param nn.modules model: 待训练的模型 - :param torch.optim.Optimizer,None optimizer: 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器 - :param int batch_size: 训练和验证的时候的batch大小。 - :param LossBase loss: 使用的Loss对象。 详见 LossBase_ 。当loss为None时,默认使用 LossInForward_ 。 - :param Sampler sampler: Batch数据生成的顺序。详见 Sampler_ 。如果为None,默认使用 RandomSampler_ 。 - :param update_every: int, 多少步更新一次梯度。用于希望累计梯度的场景,比如需要128的batch_size, 但是直接设为128 - 会导致内存不足,通过设置batch_size=32, update_every=4达到目的。当optimizer为None时,该参数无效。 - :param int n_epochs: 需要优化迭代多少次。 - :param int print_every: 多少次反向传播更新tqdm显示的loss; 如果use_tqdm=False, 则多少次反向传播打印loss。 - :param DataSet dev_data: 用于做验证的DataSet。 - :param MetricBase,list(MetricBase) metrics: 验证的评估函数。可以只使用一个Metric,也可以使用多个Metric,通过 - 列表传入。如验证时取得了更好的验证结果(如果有多个Metric,以列表中第一个Metric为准),且save_path不为None, - 则保存当前模型。Metric种类详见 Metric_ 。仅在传入dev_data时有效。 - :param str,None metric_key: Metric_ 有时会有多个指标,比如 SpanFPreRecMetric_ 中包含了'f', 'pre', 'rec'。此时需 - 要指定以哪个指标为准。另外有些指标是越小效果越好,比如语言模型的困惑度,这种情况下,在key前面增加一个'-'来表 - 明验证时,值越小越好(比如: "-ppl")。仅在传入dev_data时有效。 - :param int validate_every: 多少个step在验证集上验证一次; 如果为-1,则每个epoch结束验证一次。仅在传入dev_data时有 - 效。 - :param str,None save_path: 将模型保存路径。如果为None,则不保存模型。如果dev_data为None,则保存最后一次迭代的模 - 型。保存的时候不仅保存了参数,还保存了模型结构。即便使用DataParallel,这里也只保存模型。 - :param prefetch: bool, 是否使用额外的进程对产生batch数据。理论上会使得Batch迭代更快。 - :param bool use_tqdm: 是否使用tqdm来显示训练进度; 如果为False,则将loss打印在终端中。 - :param str,int,torch.device,list(int) device: 将模型load到哪个设备。默认为None,即Trainer不对模型 - 的计算位置进行管理。支持以下的输入: - - 1. str: ['cpu', 'cuda', 'cuda:0', 'cuda:1', ...] 依次为'cpu'中, 可见的第一个GPU中, 可见的第一个GPU中, - 可见的第二个GPU中; - - 2. torch.device:将模型装载到torch.device上。 - - 3. int: 将使用device_id为该值的gpu进行训练 - - 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 - - 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 - - :param list(callbacks) callbacks: 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 - 通过callback机制实现。 可使用的callback参见 Callback_ 。 - :param int check_code_level: 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, - 报告警告信息; 2: 有任何field没有被使用都报错. 检查的原理是通过使用很小的batch(默认2个sample)来运行代码,但是 - 这个过程理论上不会修改任何参数,只是会检查能否运行。但如果(1)模型中存在将batch_size写为某个固定值的情况; - (2)模型中存在累加前向计算次数的,可能会多计算1次。以上情况建议将check_code_level设置为-1。 - """ + super(Trainer, self).__init__() - + if not isinstance(train_data, DataSet): raise TypeError(f"The type of train_data must be fastNLP.DataSet, got {type(train_data)}.") if not isinstance(model, nn.Module): raise TypeError(f"The type of model must be torch.nn.Module, got {type(model)}.") - + # check metrics and dev_data if (not metrics) and dev_data is not None: raise ValueError("No metric for dev_data evaluation.") if metrics and (dev_data is None): raise ValueError("No dev_data for evaluations, pass dev_data or set metrics to None. ") - + # check update every assert update_every >= 1, "update_every must be no less than 1." self.update_every = int(update_every) - + # check save_path if not (save_path is None or isinstance(save_path, str)): raise ValueError("save_path can only be None or `str`.") # prepare evaluate metrics = _prepare_metrics(metrics) - + # parse metric_key # increase_better is True. It means the exp result gets better if the indicator increases. # It is true by default. @@ -411,19 +435,19 @@ class Trainer(object): self.metric_key = metric_key[1:] if metric_key[0] == "+" or metric_key[0] == "-" else metric_key elif len(metrics) > 0: self.metric_key = metrics[0].__class__.__name__.lower().strip('metric') - + # prepare loss losser = _prepare_losser(loss) - + # sampler check if sampler is not None and not isinstance(sampler, Sampler): raise ValueError("The type of sampler should be fastNLP.BaseSampler, got {}.".format(type(sampler))) - + if check_code_level > -1: _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data, metric_key=metric_key, check_level=check_code_level, batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE)) - + self.train_data = train_data self.dev_data = dev_data # If None, No validation. self.model = model @@ -443,9 +467,9 @@ class Trainer(object): self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) self.n_steps = (len(self.train_data) // self.batch_size + int( len(self.train_data) % self.batch_size != 0)) * self.n_epochs - + self.model = _move_model_to_device(self.model, device=device) - + if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer elif isinstance(optimizer, Optimizer): @@ -454,11 +478,11 @@ class Trainer(object): self.optimizer = torch.optim.Adam(model.parameters(), lr=4e-3) else: raise TypeError("optimizer can only be torch.optim.Optimizer type, not {}.".format(type(optimizer))) - + self.use_tqdm = use_tqdm self.pbar = None self.print_every = abs(self.print_every) - + if self.dev_data is not None: self.tester = Tester(model=self.model, data=self.dev_data, @@ -466,13 +490,13 @@ class Trainer(object): batch_size=self.batch_size, device=None, # 由上面的部分处理device verbose=0) - + self.step = 0 self.start_time = None # start timestamp - + self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) - + def train(self, load_best_model=True): """ 使用该函数使Trainer开始训练。 @@ -501,14 +525,14 @@ class Trainer(object): self.start_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) start_time = time.time() print("training epochs started " + self.start_time, flush=True) - + try: self.callback_manager.on_train_begin() self._train() self.callback_manager.on_train_end() except (CallbackException, KeyboardInterrupt) as e: self.callback_manager.on_exception(e) - + if self.dev_data is not None and hasattr(self, 'best_dev_perf'): print( "\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + @@ -526,9 +550,9 @@ class Trainer(object): finally: pass results['seconds'] = round(time.time() - start_time, 2) - + return results - + def _train(self): if not self.use_tqdm: from fastNLP.core.utils import _pseudo_tqdm as inner_tqdm @@ -537,7 +561,7 @@ class Trainer(object): self.step = 0 self.epoch = 0 start = time.time() - + with inner_tqdm(total=self.n_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: self.pbar = pbar if isinstance(pbar, tqdm) else None avg_loss = 0 @@ -556,21 +580,21 @@ class Trainer(object): # negative sampling; replace unknown; re-weight batch_y self.callback_manager.on_batch_begin(batch_x, batch_y, indices) prediction = self._data_forward(self.model, batch_x) - + # edit prediction self.callback_manager.on_loss_begin(batch_y, prediction) loss = self._compute_loss(prediction, batch_y).mean() avg_loss += loss.item() loss = loss / self.update_every - + # Is loss NaN or inf? requires_grad = False self.callback_manager.on_backward_begin(loss) self._grad_backward(loss) self.callback_manager.on_backward_end() - + self._update() self.callback_manager.on_step_end() - + if self.step % self.print_every == 0: avg_loss = float(avg_loss) / self.print_every if self.use_tqdm: @@ -584,7 +608,7 @@ class Trainer(object): pbar.set_postfix_str(print_output) avg_loss = 0 self.callback_manager.on_batch_end() - + if ((self.validate_every > 0 and self.step % self.validate_every == 0) or (self.validate_every < 0 and self.step % len(data_iterator) == 0)) \ and self.dev_data is not None: @@ -593,20 +617,20 @@ class Trainer(object): self.n_steps) + \ self.tester._format_eval_results(eval_res) pbar.write(eval_str + '\n') - + # ================= mini-batch end ==================== # - + # lr decay; early stopping self.callback_manager.on_epoch_end() # =============== epochs end =================== # pbar.close() self.pbar = None # ============ tqdm end ============== # - + def _do_validation(self, epoch, step): self.callback_manager.on_valid_begin() res = self.tester.test() - + is_better_eval = False if self._better_eval_result(res): if self.save_path is not None: @@ -621,7 +645,7 @@ class Trainer(object): # get validation results; adjust optimizer self.callback_manager.on_valid_end(res, self.metric_key, self.optimizer, is_better_eval) return res - + def _mode(self, model, is_test=False): """Train mode or Test mode. This is for PyTorch currently. @@ -633,21 +657,22 @@ class Trainer(object): model.eval() else: model.train() - + def _update(self): """Perform weight update on a model. """ if self.optimizer is not None and (self.step + 1) % self.update_every == 0: self.optimizer.step() - + def _data_forward(self, network, x): x = _build_args(network.forward, **x) y = network(**x) if not isinstance(y, dict): - raise TypeError(f"The return value of {_get_func_signature(network.forward)} should be dict, got {type(y)}.") + raise TypeError( + f"The return value of {_get_func_signature(network.forward)} should be dict, got {type(y)}.") return y - + def _grad_backward(self, loss): """Compute gradient with link rules. @@ -658,7 +683,7 @@ class Trainer(object): if self.step % self.update_every == 0: self.model.zero_grad() loss.backward() - + def _compute_loss(self, predict, truth): """Compute loss given prediction and ground truth. @@ -667,7 +692,7 @@ class Trainer(object): :return: a scalar """ return self.losser(predict, truth) - + def _save_model(self, model, model_name, only_param=False): """ 存储不含有显卡信息的state_dict或model :param model: @@ -690,7 +715,7 @@ class Trainer(object): model.cpu() torch.save(model, model_path) model.to(self._model_device) - + def _load_model(self, model, model_name, only_param=False): # 返回bool值指示是否成功reload模型 if self.save_path is not None: @@ -708,7 +733,7 @@ class Trainer(object): else: return False return True - + def _better_eval_result(self, metrics): """Check if the current epoch yields better validation results. @@ -759,7 +784,7 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ check_level=0): # check get_loss 方法 model_devcie = model.parameters().__next__().device - + batch = Batch(dataset=dataset, batch_size=batch_size, sampler=SequentialSampler()) for batch_count, (batch_x, batch_y) in enumerate(batch): _move_dict_value_to_device(batch_x, batch_y, device=model_devcie) @@ -783,13 +808,13 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ print(info_str) _check_forward_error(forward_func=model.forward, dataset=dataset, batch_x=batch_x, check_level=check_level) - + refined_batch_x = _build_args(model.forward, **batch_x) pred_dict = model(**refined_batch_x) func_signature = _get_func_signature(model.forward) if not isinstance(pred_dict, dict): raise TypeError(f"The return value of {func_signature} should be `dict`, not `{type(pred_dict)}`.") - + # loss check try: loss = losser(pred_dict, batch_y) @@ -813,7 +838,7 @@ def _check_code(dataset, model, losser, metrics, batch_size=DEFAULT_CHECK_BATCH_ model.zero_grad() if batch_count + 1 >= DEFAULT_CHECK_NUM_BATCH: break - + if dev_data is not None: tester = Tester(data=dev_data[:batch_size * DEFAULT_CHECK_NUM_BATCH], model=model, metrics=metrics, batch_size=batch_size, verbose=-1) @@ -827,7 +852,7 @@ def _check_eval_results(metrics, metric_key, metric_list): # metric_list: 多个用来做评价的指标,来自Trainer的初始化 if isinstance(metrics, tuple): loss, metrics = metrics - + if isinstance(metrics, dict): if len(metrics) == 1: # only single metric, just use it @@ -838,7 +863,7 @@ def _check_eval_results(metrics, metric_key, metric_list): if metrics_name not in metrics: raise RuntimeError(f"{metrics_name} is chosen to do validation, but got {metrics}") metric_dict = metrics[metrics_name] - + if len(metric_dict) == 1: indicator_val, indicator = list(metric_dict.values())[0], list(metric_dict.keys())[0] elif len(metric_dict) > 1 and metric_key is None: diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index cc9e8164..23743ecf 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -1,3 +1,7 @@ +""" +utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户可以使用的是 :func:`cache_results` 修饰器。 +""" +__all__ = ["cache_results"] import _pickle import inspect import os @@ -29,6 +33,8 @@ def _prepare_cache_filepath(filepath): # TODO 可以保存下缓存时的参数,如果load的时候发现参数不一致,发出警告。 def cache_results(_cache_fp, _refresh=False, _verbose=1): """ + 别名::class:`fastNLP.cache_results` :class:`fastNLP.core.uitls.cache_results` + cache_results是fastNLP中用于cache数据的装饰器。通过下面的例子看一下如何使用 Example:: From a1f8cdec48a61202d44c801f85da5f8a867091fc Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 11:29:24 +0800 Subject: [PATCH 075/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20io=20?= =?UTF-8?q?=E9=83=A8=E5=88=86=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/__init__.py | 15 +++-- fastNLP/io/base_loader.py | 17 +++-- fastNLP/io/config_io.py | 55 +++++++++------ fastNLP/io/dataset_loader.py | 126 ++++++++++++++++++++++------------- fastNLP/io/embed_loader.py | 28 ++++---- fastNLP/io/file_reader.py | 3 + fastNLP/io/model_io.py | 35 ++++++---- 7 files changed, 172 insertions(+), 107 deletions(-) diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index e8ccca30..dc1e3d15 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -12,13 +12,14 @@ 这些类的使用方法可以在对应module的文档下查看. """ from .embed_loader import EmbedLoader -from .dataset_loader import * -from .config_io import * -from .model_io import * +from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ + PeopleDailyCorpusLoader, Conll2003Loader +from .config_io import ConfigLoader, ConfigSection, ConfigSaver +from .model_io import ModelLoader as ModelLoader, ModelSaver as ModelSaver __all__ = [ 'EmbedLoader', - + 'DataSetLoader', 'CSVLoader', 'JsonLoader', @@ -27,11 +28,11 @@ __all__ = [ 'SSTLoader', 'PeopleDailyCorpusLoader', 'Conll2003Loader', - + 'ConfigLoader', 'ConfigSection', 'ConfigSaver', - + 'ModelLoader', 'ModelSaver', -] \ No newline at end of file +] diff --git a/fastNLP/io/base_loader.py b/fastNLP/io/base_loader.py index 5d5fe63a..569f7e2e 100644 --- a/fastNLP/io/base_loader.py +++ b/fastNLP/io/base_loader.py @@ -3,7 +3,8 @@ import os class BaseLoader(object): - """Base loader for all loaders. + """ + 各个 Loader 的基类,提供了 API 的参考。 """ def __init__(self): @@ -11,7 +12,10 @@ class BaseLoader(object): @staticmethod def load_lines(data_path): - """按行读取,舍弃每行两侧空白字符,返回list of str + """ + 按行读取,舍弃每行两侧空白字符,返回list of str + + :param data_path: 读取数据的路径 """ with open(data_path, "r", encoding="utf=8") as f: text = f.readlines() @@ -19,7 +23,10 @@ class BaseLoader(object): @classmethod def load(cls, data_path): - """先按行读取,去除一行两侧空白,再提取每行的字符。返回list of list of str + """ + 先按行读取,去除一行两侧空白,再提取每行的字符。返回list of list of str + + :param data_path: """ with open(data_path, "r", encoding="utf-8") as f: text = f.readlines() @@ -40,9 +47,7 @@ class BaseLoader(object): class DataLoaderRegister: - """Register for all data sets. - - """ + # TODO 这个类使用在何处? _readers = {} @classmethod diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index f303f0e9..8fa30dd4 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -1,19 +1,22 @@ """ -.. _config-io: 用于读入和处理和保存 config 文件 """ +__all__ = ["ConfigLoader","ConfigSection","ConfigSaver"] import configparser import json import os -from fastNLP.io.base_loader import BaseLoader +from .base_loader import BaseLoader class ConfigLoader(BaseLoader): - """Loader for configuration. + """ + 别名::class:`fastNLP.io.ConfigLoader` :class:`fastNLP.io.config_io.ConfigLoader` + + 读取配置文件的Loader - :param str data_path: path to the config + :param str data_path: 配置文件的路径 """ def __init__(self, data_path=None): @@ -27,14 +30,16 @@ class ConfigLoader(BaseLoader): @staticmethod def load_config(file_path, sections): - """Load section(s) of configuration into the ``sections`` provided. No returns. + """ + 把配置文件的section 存入提供的 ``sections`` 中 - :param str file_path: the path of config file - :param dict sections: the dict of ``{section_name(string): ConfigSection object}`` - Example:: - - test_args = ConfigSection() - ConfigLoader("config.cfg").load_config("./data_for_tests/config", {"POS_test": test_args}) + :param str file_path: 配置文件的路径 + :param dict sections: 符合如下键值对组成的字典 `section_name(string)` : :class:`~fastNLP.io.ConfigSection` + + Example:: + + test_args = ConfigSection() + ConfigLoader("config.cfg").load_config("./data_for_tests/config", {"POS_test": test_args}) """ assert isinstance(sections, dict) @@ -70,7 +75,10 @@ class ConfigLoader(BaseLoader): class ConfigSection(object): - """ConfigSection is the data structure storing all key-value pairs in one section in a config file. + """ + 别名::class:`fastNLP.io.ConfigSection` :class:`fastNLP.io.config_io.ConfigSection` + + ConfigSection是一个存储了一个section中所有键值对的数据结构,推荐使用此类的实例来配合 :meth:`ConfigLoader.load_config` 使用 """ @@ -146,9 +154,12 @@ class ConfigSection(object): class ConfigSaver(object): - """ConfigSaver is used to save config file and solve related conflicts. + """ + 别名::class:`fastNLP.io.ConfigSaver` :class:`fastNLP.io.config_io.ConfigSaver` + + ConfigSaver 是用来存储配置文件并解决相关冲突的类 - :param str file_path: path to the config file + :param str file_path: 配置文件的路径 """ def __init__(self, file_path): @@ -157,7 +168,8 @@ class ConfigSaver(object): raise FileNotFoundError("file {} NOT found!".__format__(self.file_path)) def _get_section(self, sect_name): - """This is the function to get the section with the section name. + """ + This is the function to get the section with the section name. :param sect_name: The name of section what wants to load. :return: The section. @@ -167,7 +179,8 @@ class ConfigSaver(object): return sect def _read_section(self): - """This is the function to read sections from the config file. + """ + This is the function to read sections from the config file. :return: sect_list, sect_key_list sect_list: A list of ConfigSection(). @@ -219,7 +232,8 @@ class ConfigSaver(object): return sect_list, sect_key_list def _write_section(self, sect_list, sect_key_list): - """This is the function to write config file with section list and name list. + """ + This is the function to write config file with section list and name list. :param sect_list: A list of ConfigSection() need to be writen into file. :param sect_key_list: A list of name of sect_list. @@ -240,10 +254,11 @@ class ConfigSaver(object): f.write('\n') def save_config_file(self, section_name, section): - """This is the function to be called to change the config file with a single section and its name. + """ + 这个方法可以用来修改并保存配置文件中单独的一个 section - :param str section_name: The name of section what needs to be changed and saved. - :param ConfigSection section: The section with key and value what needs to be changed and saved. + :param str section_name: 需要保存的 section 的名字. + :param section: 你需要修改并保存的 section, :class:`~fastNLP.io.ConfigSaver` 类型 """ section_file = self._get_section(section_name) if len(section_file.__dict__.keys()) == 0: # the section not in the file before diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 0fcdcf29..6d64ede2 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,8 +1,6 @@ """ -.. _dataset-loader: - -DataSetLoader 的 API, 用于读取不同格式的数据, 并返回 `DataSet` , -得到的 `DataSet` 对象可以直接传入 `Trainer`, `Tester`, 用于模型的训练和测试 +dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的数据, 并返回 `DataSet` , +得到的 :class:`~fastNLP.DataSet` 对象可以直接传入 :class:`~fastNLP.Trainer`, :class:`~fastNLP.Tester`, 用于模型的训练和测试 Example:: @@ -13,50 +11,50 @@ Example:: # ... do stuff """ -import os -import json + from nltk.tree import Tree -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance -from fastNLP.io.file_reader import _read_csv, _read_json, _read_conll +from ..core.dataset import DataSet +from ..core.instance import Instance +from .file_reader import _read_csv, _read_json, _read_conll def _download_from_url(url, path): from tqdm import tqdm import requests - + """Download file""" r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) chunk_size = 16 * 1024 total_size = int(r.headers.get('Content-length', 0)) - with open(path, "wb") as file ,\ - tqdm(total=total_size, unit='B', unit_scale=1, desc=path.split('/')[-1]) as t: + with open(path, "wb") as file, \ + tqdm(total=total_size, unit='B', unit_scale=1, desc=path.split('/')[-1]) as t: for chunk in r.iter_content(chunk_size): if chunk: file.write(chunk) t.update(len(chunk)) return + def _uncompress(src, dst): import zipfile, gzip, tarfile, os - + def unzip(src, dst): with zipfile.ZipFile(src, 'r') as f: f.extractall(dst) - + def ungz(src, dst): with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: - length = 16 * 1024 # 16KB + length = 16 * 1024 # 16KB buf = f.read(length) while buf: uf.write(buf) buf = f.read(length) - + def untar(src, dst): with tarfile.open(src, 'r:gz') as f: f.extractall(dst) - + fn, ext = os.path.splitext(src) _, ext_2 = os.path.splitext(fn) if ext == '.zip': @@ -71,42 +69,48 @@ def _uncompress(src, dst): class DataSetLoader: """ + 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` - 所有`DataSetLoader`的接口 + 所有 DataSetLoader 的 API 接口,你可以继承它实现自己的 DataSetLoader """ - + def load(self, path): """从指定 ``path`` 的文件中读取数据,返回DataSet - :param str path: file path - :return: a DataSet object + :param str path: 文件路径 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ raise NotImplementedError - + def convert(self, data): - """用Python数据对象创建DataSet + """ + 用Python数据对象创建DataSet,各个子类需要自行实现这个方法 - :param data: inner data structure (user-defined) to represent the data. - :return: a DataSet object + :param data: Python 内置的数据结构 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ raise NotImplementedError class PeopleDailyCorpusLoader(DataSetLoader): - """读取人民日报数据集 """ + 别名::class:`fastNLP.io.PeopleDailyCorpusLoader` :class:`fastNLP.io.dataset_loader.PeopleDailyCorpusLoader` + + 读取人民日报数据集 + """ + def __init__(self): super(PeopleDailyCorpusLoader, self).__init__() self.pos = True self.ner = True - + def load(self, data_path, pos=True, ner=True): """ :param str data_path: 数据路径 :param bool pos: 是否使用词性标签 :param bool ner: 是否使用命名实体标签 - :return: a DataSet object + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ self.pos, self.ner = pos, ner with open(data_path, "r", encoding="utf-8") as f: @@ -152,8 +156,13 @@ class PeopleDailyCorpusLoader(DataSetLoader): example.append(sent_ner) examples.append(example) return self.convert(examples) - + def convert(self, data): + """ + + :param data: python 内置对象 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 + """ data_set = DataSet() for item in data: sent_words = item[0] @@ -172,6 +181,8 @@ class PeopleDailyCorpusLoader(DataSetLoader): class ConllLoader(DataSetLoader): """ + 别名::class:`fastNLP.io.ConllLoader` :class:`fastNLP.io.dataset_loader.ConllLoader` + 读取Conll格式的数据. 数据格式详见 http://conll.cemantix.org/2012/data.html 列号从0开始, 每列对应内容为:: @@ -195,6 +206,7 @@ class ConllLoader(DataSetLoader): :param indexs: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ + def __init__(self, headers, indexs=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): @@ -207,21 +219,25 @@ class ConllLoader(DataSetLoader): if len(indexs) != len(headers): raise ValueError self.indexs = indexs - + def load(self, path): ds = DataSet() for idx, data in _read_conll(path, indexes=self.indexs, dropna=self.dropna): - ins = {h:data[i] for i, h in enumerate(self.headers)} + ins = {h: data[i] for i, h in enumerate(self.headers)} ds.append(Instance(**ins)) return ds class Conll2003Loader(ConllLoader): - """读取Conll2003数据 + """ + 别名::class:`fastNLP.io.Conll2003Loader` :class:`fastNLP.io.dataset_loader.Conll2003Loader` + + 读取Conll2003数据 关于数据集的更多信息,参考: https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ + def __init__(self): headers = [ 'tokens', 'pos', 'chunks', 'ner', @@ -260,7 +276,10 @@ def _cut_long_sentence(sent, max_sample_length=200): class SSTLoader(DataSetLoader): - """读取SST数据集, DataSet包含fields:: + """ + 别名::class:`fastNLP.io.SSTLoader` :class:`fastNLP.io.dataset_loader.SSTLoader` + + 读取SST数据集, DataSet包含fields:: words: list(str) 需要分类的文本 target: str 文本的标签 @@ -270,21 +289,22 @@ class SSTLoader(DataSetLoader): :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` """ + def __init__(self, subtree=False, fine_grained=False): self.subtree = subtree - - tag_v = {'0':'very negative', '1':'negative', '2':'neutral', - '3':'positive', '4':'very positive'} + + tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', + '3': 'positive', '4': 'very positive'} if not fine_grained: tag_v['0'] = tag_v['1'] tag_v['4'] = tag_v['3'] self.tag_v = tag_v - + def load(self, path): """ - :param path: str,存储数据的路径 - :return: DataSet。 + :param str path: 存储数据的路径 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ datalist = [] with open(path, 'r', encoding='utf-8') as f: @@ -296,7 +316,7 @@ class SSTLoader(DataSetLoader): for words, tag in datas: ds.append(Instance(words=words, target=tag)) return ds - + @staticmethod def _get_one(data, subtree): tree = Tree.fromstring(data) @@ -307,15 +327,18 @@ class SSTLoader(DataSetLoader): class JsonLoader(DataSetLoader): """ + 别名::class:`fastNLP.io.JsonLoader` :class:`fastNLP.io.dataset_loader.JsonLoader` + 读取json格式数据.数据必须按行存储,每行是一个包含各类属性的json对象 :param dict fields: 需要读入的json属性名称, 和读入后在DataSet中存储的field_name - ``fields`` 的`key`必须是json对象的属性名. ``fields`` 的`value`为读入后在DataSet存储的`field_name`, - `value`也可为 ``None`` , 这时读入后的`field_name`与json对象对应属性同名 + ``fields`` 的 `key` 必须是json对象的属性名. ``fields`` 的 `value` 为读入后在DataSet存储的 `field_name` , + `value` 也可为 ``None`` , 这时读入后的 `field_name` 与json对象对应属性同名 ``fields`` 可为 ``None`` , 这时,json对象所有属性都保存在DataSet中. Default: ``None`` :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ + def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() self.dropna = dropna @@ -326,12 +349,12 @@ class JsonLoader(DataSetLoader): for k, v in fields.items(): self.fields[k] = k if v is None else v self.fields_list = list(self.fields.keys()) - + def load(self, path): ds = DataSet() for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): if self.fields: - ins = {self.fields[k]:v for k,v in d.items()} + ins = {self.fields[k]: v for k, v in d.items()} else: ins = d ds.append(Instance(**ins)) @@ -340,6 +363,8 @@ class JsonLoader(DataSetLoader): class SNLILoader(JsonLoader): """ + 别名::class:`fastNLP.io.SNLILoader` :class:`fastNLP.io.dataset_loader.SNLILoader` + 读取SNLI数据集,读取的DataSet包含fields:: words1: list(str),第一句文本, premise @@ -348,6 +373,7 @@ class SNLILoader(JsonLoader): 数据来源: https://nlp.stanford.edu/projects/snli/snli_1.0.zip """ + def __init__(self): fields = { 'sentence1_parse': 'words1', @@ -355,12 +381,14 @@ class SNLILoader(JsonLoader): 'gold_label': 'target', } super(SNLILoader, self).__init__(fields=fields) - + def load(self, path): ds = super(SNLILoader, self).load(path) + def parse_tree(x): t = Tree.fromstring(x) return t.leaves() + ds.apply(lambda ins: parse_tree(ins['words1']), new_field_name='words1') ds.apply(lambda ins: parse_tree(ins['words2']), new_field_name='words2') ds.drop(lambda x: x['target'] == '-') @@ -369,6 +397,8 @@ class SNLILoader(JsonLoader): class CSVLoader(DataSetLoader): """ + 别名::class:`fastNLP.io.CSVLoader` :class:`fastNLP.io.dataset_loader.CSVLoader` + 读取CSV格式的数据集。返回 ``DataSet`` :param List[str] headers: CSV文件的文件头.定义每一列的属性名称,即返回的DataSet中`field`的名称 @@ -377,11 +407,12 @@ class CSVLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ + def __init__(self, headers=None, sep=",", dropna=False): self.headers = headers self.sep = sep self.dropna = dropna - + def load(self, path): ds = DataSet() for idx, data in _read_csv(path, headers=self.headers, @@ -396,7 +427,7 @@ def _add_seg_tag(data): :param data: list of ([word], [pos], [heads], [head_tags]) :return: list of ([word], [pos]) """ - + _processed = [] for word_list, pos_list, _, _ in data: new_sample = [] @@ -410,4 +441,3 @@ def _add_seg_tag(data): new_sample.append((word[-1], 'E-' + pos)) _processed.append(list(map(list, zip(*new_sample)))) return _processed - diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 39d93fab..4cc8f596 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -7,13 +7,17 @@ import os import numpy as np -from fastNLP.core.vocabulary import Vocabulary -from fastNLP.io.base_loader import BaseLoader +from ..core.vocabulary import Vocabulary +from .base_loader import BaseLoader import warnings class EmbedLoader(BaseLoader): - """这个类用于从预训练的Embedding中load数据。""" + """ + 别名::class:`fastNLP.io.EmbedLoader` :class:`fastNLP.io.embed_loader.EmbedLoader` + + 这个类用于从预训练的Embedding中load数据。 + """ def __init__(self): super(EmbedLoader, self).__init__() @@ -25,13 +29,13 @@ class EmbedLoader(BaseLoader): word2vec(第一行只有两个元素)还是glove格式的数据。 :param str embed_filepath: 预训练的embedding的路径。 - :param Vocabulary vocab: 词表,读取出现在vocab中的词的embedding。没有出现在vocab中的词的embedding将通过找到的词的 - embedding的正态分布采样出来,以使得整个Embedding是同分布的。 + :param vocab: 词表 :class:`~fastNLP.Vocabulary` 类型,读取出现在vocab中的词的embedding。 + 没有出现在vocab中的词的embedding将通过找到的词的embedding的正态分布采样出来,以使得整个Embedding是同分布的。 :param dtype: 读出的embedding的类型 :param bool normalize: 是否将每个vector归一化到norm为1 - :param str error: 'ignore', 'strict'; 如果'ignore',错误将自动跳过; 如果strict, 错误将抛出。这里主要可能出错的地 - 方在于词表有空行或者词表出现了维度不一致。 - :return: numpy.ndarray, shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 + :param str error: `ignore` , `strict` ; 如果 `ignore` ,错误将自动跳过; 如果 `strict` , 错误将抛出。 + 这里主要可能出错的地方在于词表有空行或者词表出现了维度不一致。 + :return numpy.ndarray: shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 """ assert isinstance(vocab, Vocabulary), "Only fastNLP.Vocabulary is supported." if not os.path.exists(embed_filepath): @@ -87,11 +91,11 @@ class EmbedLoader(BaseLoader): :param str padding: the padding tag for vocabulary. :param str unknown: the unknown tag for vocabulary. :param bool normalize: 是否将每个vector归一化到norm为1 - :param str error: 'ignore', 'strict'; 如果'ignore',错误将自动跳过; 如果strict, 错误将抛出。这里主要可能出错的地 + :param str error: `ignore` , `strict` ; 如果 `ignore` ,错误将自动跳过; 如果 `strict` , 错误将抛出。这里主要可能出错的地 方在于词表有空行或者词表出现了维度不一致。 - :return: numpy.ndarray, shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 - :return: numpy.ndarray,Vocabulary embedding的shape是[词表大小+x, 词表维度], "词表大小+x"是由于最终的大小还取决与 - 是否使用padding, 以及unknown有没有在词表中找到对应的词。Vocabulary中的词的顺序与Embedding的顺序是一一对应的。 + :return numpy.ndarray: shape为 [len(vocab), dimension], dimension由pretrain的embedding决定。 + :return numpy.ndarray: Vocabulary Embedding的shape是[词表大小+x, 词表维度], "词表大小+x"是由于最终的大小还取决与 + 是否使用padding, 以及unknown有没有在词表中找到对应的词。 Vocabulary中的词的顺序与Embedding的顺序是一一对应的。 """ vocab = Vocabulary(padding=padding, unknown=unknown) vec_dict = {} diff --git a/fastNLP/io/file_reader.py b/fastNLP/io/file_reader.py index ffbab510..5963bb56 100644 --- a/fastNLP/io/file_reader.py +++ b/fastNLP/io/file_reader.py @@ -1,3 +1,6 @@ +""" +此模块用于给其它模块提供读取文件的函数,没有为用户提供 API +""" import json diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index d28034c8..48e53ab3 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -1,16 +1,16 @@ """ -.. _model-io: - 用于载入和保存模型 """ import torch -from fastNLP.io.base_loader import BaseLoader +from .base_loader import BaseLoader class ModelLoader(BaseLoader): """ - Loader for models. + 别名::class:`fastNLP.io.ModelLoader` :class:`fastNLP.io.model_io.ModelLoader` + + 用于读取模型 """ def __init__(self): @@ -18,24 +18,30 @@ class ModelLoader(BaseLoader): @staticmethod def load_pytorch(empty_model, model_path): - """Load model parameters from ".pkl" files into the empty PyTorch model. + """ + 从 ".pkl" 文件读取 PyTorch 模型 - :param empty_model: a PyTorch model with initialized parameters. - :param str model_path: the path to the saved model. + :param empty_model: 初始化参数的 PyTorch 模型 + :param str model_path: 模型保存的路径 """ empty_model.load_state_dict(torch.load(model_path)) @staticmethod def load_pytorch_model(model_path): - """Load the entire model. + """ + 读取整个模型 - :param str model_path: the path to the saved model. + :param str model_path: 模型保存的路径 """ return torch.load(model_path) class ModelSaver(object): - """Save a model + """ + 别名::class:`fastNLP.io.ModelSaver` :class:`fastNLP.io.model_io.ModelSaver` + + 用于保存模型 + Example:: saver = ModelSaver("./save/model_ckpt_100.pkl") @@ -46,15 +52,16 @@ class ModelSaver(object): def __init__(self, save_path): """ - :param save_path: the path to the saving directory. + :param save_path: 模型保存的路径 """ self.save_path = save_path def save_pytorch(self, model, param_only=True): - """Save a pytorch model into ".pkl" file. + """ + 把 PyTorch 模型存入 ".pkl" 文件 - :param model: a PyTorch model - :param bool param_only: whether only to save the model parameters or the entire model. + :param model: PyTorch 模型 + :param bool param_only: 是否只保存模型的参数(否则保存整个模型) """ if param_only is True: From f66012a6403915a5a6b708e1656be177f0ec9bb1 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 13:20:49 +0800 Subject: [PATCH 076/173] =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E8=B7=AF=E5=BE=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/api/__init__.py | 1 + fastNLP/api/api.py | 129 ++++++++++----------- fastNLP/api/examples.py | 2 +- fastNLP/api/pipeline.py | 2 +- fastNLP/api/processor.py | 8 +- fastNLP/automl/enas_trainer.py | 14 +-- fastNLP/models/base_model.py | 2 +- fastNLP/models/bert.py | 2 +- fastNLP/models/char_language_model.py | 2 +- fastNLP/models/enas_controller.py | 5 +- fastNLP/models/enas_model.py | 5 +- fastNLP/models/enas_trainer.py | 21 ++-- fastNLP/models/enas_utils.py | 11 +- fastNLP/models/sequence_modeling.py | 8 +- fastNLP/models/snli.py | 10 +- fastNLP/models/star_transformer.py | 1 - fastNLP/modules/aggregator/attention.py | 6 +- fastNLP/modules/aggregator/pooling.py | 30 ++--- fastNLP/modules/decoder/CRF.py | 4 +- fastNLP/modules/decoder/MLP.py | 2 +- fastNLP/modules/encoder/char_encoder.py | 2 +- fastNLP/modules/encoder/conv_maxpool.py | 2 +- fastNLP/modules/encoder/embedding.py | 2 +- fastNLP/modules/encoder/linear.py | 2 +- fastNLP/modules/encoder/lstm.py | 2 +- fastNLP/modules/encoder/variational_rnn.py | 2 +- 26 files changed, 131 insertions(+), 146 deletions(-) diff --git a/fastNLP/api/__init__.py b/fastNLP/api/__init__.py index a21a4c42..5171d8c2 100644 --- a/fastNLP/api/__init__.py +++ b/fastNLP/api/__init__.py @@ -1 +1,2 @@ +__all__ = ["CWS", "POS", "Parser"] from .api import CWS, POS, Parser diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index 88f1755a..c72f3690 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -1,6 +1,3 @@ -""" -api.api的介绍文档 -""" import warnings import torch @@ -8,15 +5,14 @@ import torch warnings.filterwarnings('ignore') import os -from fastNLP.core.dataset import DataSet - -from fastNLP.api.utils import load_url -from fastNLP.api.processor import ModelProcessor -from fastNLP.io.dataset_loader import _cut_long_sentence, ConllLoader -from fastNLP.core.instance import Instance -from fastNLP.api.pipeline import Pipeline -from fastNLP.core.metrics import SpanFPreRecMetric -from fastNLP.api.processor import IndexerProcessor +from ..core.dataset import DataSet +from .utils import load_url +from .processor import ModelProcessor +from ..io.dataset_loader import _cut_long_sentence, ConllLoader +from ..core.instance import Instance +from ..api.pipeline import Pipeline +from ..core.metrics import SpanFPreRecMetric +from .processor import IndexerProcessor # TODO add pretrain urls model_urls = { @@ -28,9 +24,10 @@ model_urls = { class ConllCWSReader(object): """Deprecated. Use ConllLoader for all types of conll-format files.""" + def __init__(self): pass - + def load(self, path, cut_long_sent=False): """ 返回的DataSet只包含raw_sentence这个field,内容为str。 @@ -63,7 +60,7 @@ class ConllCWSReader(object): sample.append(line.strip().split()) if len(sample) > 0: datalist.append(sample) - + ds = DataSet() for sample in datalist: # print(sample) @@ -78,7 +75,7 @@ class ConllCWSReader(object): for raw_sentence in sents: ds.append(Instance(raw_sentence=raw_sentence)) return ds - + def get_char_lst(self, sample): if len(sample) == 0: return None @@ -90,11 +87,13 @@ class ConllCWSReader(object): text.append(t1) return text + class ConllxDataLoader(ConllLoader): """返回“词级别”的标签信息,包括词、词性、(句法)头依赖、(句法)边标签。跟``ZhConllPOSReader``完全不同。 Deprecated. Use ConllLoader for all types of conll-format files. """ + def __init__(self): headers = [ 'words', 'pos_tags', 'heads', 'labels', @@ -106,18 +105,15 @@ class ConllxDataLoader(ConllLoader): class API: - """ - 这是 API 类的文档 - """ def __init__(self): self.pipeline = None self._dict = None - + def predict(self, *args, **kwargs): """Do prediction for the given input. """ raise NotImplementedError - + def test(self, file_path): """Test performance over the given data set. @@ -125,7 +121,7 @@ class API: :return: a dictionary of metric values """ raise NotImplementedError - + def load(self, path, device): if os.path.exists(os.path.expanduser(path)): _dict = torch.load(path, map_location='cpu') @@ -145,14 +141,14 @@ class POS(API): :param str device: device name such as "cpu" or "cuda:0". Use the same notation as PyTorch. """ - + def __init__(self, model_path=None, device='cpu'): super(POS, self).__init__() if model_path is None: model_path = model_urls['pos'] - + self.load(model_path, device) - + def predict(self, content): """predict函数的介绍, 函数介绍的第二句,这句话不会换行 @@ -162,48 +158,48 @@ class POS(API): """ if not hasattr(self, "pipeline"): raise ValueError("You have to load model first.") - + sentence_list = content # 1. 检查sentence的类型 for sentence in sentence_list: if not all((type(obj) == str for obj in sentence)): raise ValueError("Input must be list of list of string.") - + # 2. 组建dataset dataset = DataSet() dataset.add_field("words", sentence_list) - + # 3. 使用pipeline self.pipeline(dataset) - + def merge_tag(words_list, tags_list): rtn = [] for words, tags in zip(words_list, tags_list): rtn.append([w + "/" + t for w, t in zip(words, tags)]) return rtn - + output = dataset.field_arrays["tag"].content if isinstance(content, str): return output[0] elif isinstance(content, list): return merge_tag(content, output) - + def test(self, file_path): test_data = ConllxDataLoader().load(file_path) - + save_dict = self._dict tag_vocab = save_dict["tag_vocab"] pipeline = save_dict["pipeline"] index_tag = IndexerProcessor(vocab=tag_vocab, field_name="tag", new_added_field_name="truth", is_input=False) pipeline.pipeline = [index_tag] + pipeline.pipeline - + test_data.rename_field("pos_tags", "tag") pipeline(test_data) test_data.set_target("truth") prediction = test_data.field_arrays["predict"].content truth = test_data.field_arrays["truth"].content seq_len = test_data.field_arrays["word_seq_origin_len"].content - + # padding by hand max_length = max([len(seq) for seq in prediction]) for idx in range(len(prediction)): @@ -217,7 +213,7 @@ class POS(API): f1 = round(test_result['f'] * 100, 2) pre = round(test_result['pre'] * 100, 2) rec = round(test_result['rec'] * 100, 2) - + return {"F1": f1, "precision": pre, "recall": rec} @@ -228,14 +224,15 @@ class CWS(API): :param model_path: 当model_path为None,使用默认位置的model。如果默认位置不存在,则自动下载模型 :param device: str,可以为'cpu', 'cuda'或'cuda:0'等。会将模型load到相应device进行推断。 """ + def __init__(self, model_path=None, device='cpu'): super(CWS, self).__init__() if model_path is None: model_path = model_urls['cws'] - + self.load(model_path, device) - + def predict(self, content): """ 分词接口。 @@ -246,27 +243,27 @@ class CWS(API): """ if not hasattr(self, 'pipeline'): raise ValueError("You have to load model first.") - + sentence_list = [] # 1. 检查sentence的类型 if isinstance(content, str): sentence_list.append(content) elif isinstance(content, list): sentence_list = content - + # 2. 组建dataset dataset = DataSet() dataset.add_field('raw_sentence', sentence_list) - + # 3. 使用pipeline self.pipeline(dataset) - + output = dataset.get_field('output').content if isinstance(content, str): return output[0] elif isinstance(content, list): return output - + def test(self, filepath): """ 传入一个分词文件路径,返回该数据集上分词f1, precision, recall。 @@ -292,28 +289,28 @@ class CWS(API): tag_proc = self._dict['tag_proc'] cws_model = self.pipeline.pipeline[-2].model pipeline = self.pipeline.pipeline[:-2] - + pipeline.insert(1, tag_proc) pp = Pipeline(pipeline) - + reader = ConllCWSReader() - + # te_filename = '/home/hyan/ctb3/test.conllx' te_dataset = reader.load(filepath) pp(te_dataset) - + from fastNLP.core.tester import Tester from fastNLP.core.metrics import BMESF1PreRecMetric - + tester = Tester(data=te_dataset, model=cws_model, metrics=BMESF1PreRecMetric(target='target'), batch_size=64, verbose=0) eval_res = tester.test() - + f1 = eval_res['BMESF1PreRecMetric']['f'] pre = eval_res['BMESF1PreRecMetric']['pre'] rec = eval_res['BMESF1PreRecMetric']['rec'] # print("f1:{:.2f}, pre:{:.2f}, rec:{:.2f}".format(f1, pre, rec)) - + return {"F1": f1, "precision": pre, "recall": rec} @@ -322,25 +319,25 @@ class Parser(API): super(Parser, self).__init__() if model_path is None: model_path = model_urls['parser'] - + self.pos_tagger = POS(device=device) self.load(model_path, device) - + def predict(self, content): if not hasattr(self, 'pipeline'): raise ValueError("You have to load model first.") - + # 1. 利用POS得到分词和pos tagging结果 pos_out = self.pos_tagger.predict(content) # pos_out = ['这里/NN 是/VB 分词/NN 结果/NN'.split()] - + # 2. 组建dataset dataset = DataSet() dataset.add_field('wp', pos_out) dataset.apply(lambda x: [''] + [w.split('/')[0] for w in x['wp']], new_field_name='words') dataset.apply(lambda x: [''] + [w.split('/')[1] for w in x['wp']], new_field_name='pos') dataset.rename_field("words", "raw_words") - + # 3. 使用pipeline self.pipeline(dataset) dataset.apply(lambda x: [str(arc) for arc in x['arc_pred']], new_field_name='arc_pred') @@ -348,7 +345,7 @@ class Parser(API): zip(x['arc_pred'], x['label_pred_seq'])][1:], new_field_name='output') # output like: [['2/top', '0/root', '4/nn', '2/dep']] return dataset.field_arrays['output'].content - + def load_test_file(self, path): def get_one(sample): sample = list(map(list, zip(*sample))) @@ -360,7 +357,7 @@ class Parser(API): return None # return word_seq, pos_seq, head_seq, head_tag_seq return sample[1], sample[3], list(map(int, sample[6])), sample[7] - + datalist = [] with open(path, 'r', encoding='utf-8') as f: sample = [] @@ -374,14 +371,14 @@ class Parser(API): sample.append(line.split('\t')) if len(sample) > 0: datalist.append(sample) - + data = [get_one(sample) for sample in datalist] data_list = list(filter(lambda x: x is not None, data)) return data_list - + def test(self, filepath): data = self.load_test_file(filepath) - + def convert(data): BOS = '' dataset = DataSet() @@ -396,7 +393,7 @@ class Parser(API): arc_true=heads, tags=head_tags)) return dataset - + ds = convert(data) pp = self.pipeline for p in pp: @@ -417,23 +414,23 @@ class Parser(API): head_cor += 1 if head_pred[i] == head_gold[i] else 0 uas = head_cor / total # print('uas:{:.2f}'.format(uas)) - + for p in pp: if p.field_name == 'gold_words': p.field_name = 'word_list' elif p.field_name == 'gold_pos': p.field_name = 'pos_list' - + return {"USA": round(uas, 5)} class Analyzer: def __init__(self, device='cpu'): - + self.cws = CWS(device=device) self.pos = POS(device=device) self.parser = Parser(device=device) - + def predict(self, content, seg=False, pos=False, parser=False): if seg is False and pos is False and parser is False: seg = True @@ -447,9 +444,9 @@ class Analyzer: if parser: parser_output = self.parser.predict(content) output_dict['parser'] = parser_output - + return output_dict - + def test(self, filepath): output_dict = {} if self.cws: @@ -461,5 +458,5 @@ class Analyzer: if self.parser: parser_output = self.parser.test(filepath) output_dict['parser'] = parser_output - + return output_dict diff --git a/fastNLP/api/examples.py b/fastNLP/api/examples.py index a85e7c30..c1b2e155 100644 --- a/fastNLP/api/examples.py +++ b/fastNLP/api/examples.py @@ -3,7 +3,7 @@ api/example.py contains all API examples provided by fastNLP. It is used as a tutorial for API or a test script since it is difficult to test APIs in travis. """ -from fastNLP.api import CWS, POS, Parser +from . import CWS, POS, Parser text = ['编者按:7月12日,英国航空航天系统公司公布了该公司研制的第一款高科技隐形无人机雷电之神。', '这款飞行从外型上来看酷似电影中的太空飞行器,据英国方面介绍,可以实现洲际远程打击。', diff --git a/fastNLP/api/pipeline.py b/fastNLP/api/pipeline.py index 0c567678..2cec16b3 100644 --- a/fastNLP/api/pipeline.py +++ b/fastNLP/api/pipeline.py @@ -1,4 +1,4 @@ -from fastNLP.api.processor import Processor +from ..api.processor import Processor class Pipeline: diff --git a/fastNLP/api/processor.py b/fastNLP/api/processor.py index 0bba96c0..be111cd0 100644 --- a/fastNLP/api/processor.py +++ b/fastNLP/api/processor.py @@ -3,10 +3,10 @@ from collections import defaultdict import torch -from fastNLP.core.batch import Batch -from fastNLP.core.dataset import DataSet -from fastNLP.core.sampler import SequentialSampler -from fastNLP.core.vocabulary import Vocabulary +from ..core.batch import Batch +from ..core.dataset import DataSet +from ..core.sampler import SequentialSampler +from ..core.vocabulary import Vocabulary class Processor(object): diff --git a/fastNLP/automl/enas_trainer.py b/fastNLP/automl/enas_trainer.py index a6316341..a9b1b8c3 100644 --- a/fastNLP/automl/enas_trainer.py +++ b/fastNLP/automl/enas_trainer.py @@ -11,15 +11,15 @@ import torch try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import _pseudo_tqdm as tqdm + from ..core.utils import _pseudo_tqdm as tqdm -from fastNLP.core.batch import Batch -from fastNLP.core.callback import CallbackException -from fastNLP.core.dataset import DataSet -from fastNLP.core.utils import _move_dict_value_to_device +from ..core.batch import Batch +from ..core.callback import CallbackException +from ..core.dataset import DataSet +from ..core.utils import _move_dict_value_to_device import fastNLP -import fastNLP.automl.enas_utils as utils -from fastNLP.core.utils import _build_args +from . import enas_utils as utils +from ..core.utils import _build_args from torch.optim import Adam diff --git a/fastNLP/models/base_model.py b/fastNLP/models/base_model.py index ec532014..39ac99a0 100644 --- a/fastNLP/models/base_model.py +++ b/fastNLP/models/base_model.py @@ -1,6 +1,6 @@ import torch -from fastNLP.modules.decoder.MLP import MLP +from ..modules.decoder.MLP import MLP class BaseModel(torch.nn.Module): diff --git a/fastNLP/models/bert.py b/fastNLP/models/bert.py index 42626934..7934b435 100644 --- a/fastNLP/models/bert.py +++ b/fastNLP/models/bert.py @@ -6,7 +6,7 @@ import torch from torch import nn from .base_model import BaseModel -from fastNLP.modules.encoder import BertModel +from ..modules.encoder import BertModel class BertForSequenceClassification(BaseModel): diff --git a/fastNLP/models/char_language_model.py b/fastNLP/models/char_language_model.py index d5e3359d..d0b4c426 100644 --- a/fastNLP/models/char_language_model.py +++ b/fastNLP/models/char_language_model.py @@ -2,7 +2,7 @@ import torch import torch.nn as nn import torch.nn.functional as F -from fastNLP.modules.encoder.lstm import LSTM +from ..modules.encoder.lstm import LSTM class Highway(nn.Module): diff --git a/fastNLP/models/enas_controller.py b/fastNLP/models/enas_controller.py index ae9bcfd2..16b970e6 100644 --- a/fastNLP/models/enas_controller.py +++ b/fastNLP/models/enas_controller.py @@ -5,9 +5,8 @@ import os import torch import torch.nn.functional as F -import fastNLP -import fastNLP.models.enas_utils as utils -from fastNLP.models.enas_utils import Node +from . import enas_utils as utils +from .enas_utils import Node def _construct_dags(prev_nodes, activations, func_names, num_blocks): diff --git a/fastNLP/models/enas_model.py b/fastNLP/models/enas_model.py index cc91e675..5c667927 100644 --- a/fastNLP/models/enas_model.py +++ b/fastNLP/models/enas_model.py @@ -9,9 +9,8 @@ from torch import nn import torch.nn.functional as F from torch.autograd import Variable -import fastNLP.models.enas_utils as utils -from fastNLP.models.base_model import BaseModel -import fastNLP.modules.encoder as encoder +from . import enas_utils as utils +from .base_model import BaseModel def _get_dropped_weights(w_raw, dropout_p, is_training): """Drops out weights to implement DropConnect. diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index d8110db0..824b8184 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -1,6 +1,5 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch -import os import time from datetime import datetime from datetime import timedelta @@ -8,21 +7,19 @@ from datetime import timedelta import numpy as np import torch import math -from torch import nn try: from tqdm.autonotebook import tqdm except: - from fastNLP.core.utils import _pseudo_tqdm as tqdm + from ..core.utils import _pseudo_tqdm as tqdm -from fastNLP.core.batch import Batch -from fastNLP.core.callback import CallbackManager, CallbackException -from fastNLP.core.dataset import DataSet -from fastNLP.core.utils import _CheckError -from fastNLP.core.utils import _move_dict_value_to_device -import fastNLP -import fastNLP.models.enas_utils as utils -from fastNLP.core.utils import _build_args +from ..core.trainer import Trainer +from ..core.batch import Batch +from ..core.callback import CallbackManager, CallbackException +from ..core.dataset import DataSet +from ..core.utils import _move_dict_value_to_device +from . import enas_utils as utils +from ..core.utils import _build_args from torch.optim import Adam @@ -34,7 +31,7 @@ def _get_no_grad_ctx_mgr(): return torch.no_grad() -class ENASTrainer(fastNLP.Trainer): +class ENASTrainer(Trainer): """A class to wrap training code.""" def __init__(self, train_data, model, controller, **kwargs): """Constructor for training algorithm. diff --git a/fastNLP/models/enas_utils.py b/fastNLP/models/enas_utils.py index e5027d81..aafcb3a7 100644 --- a/fastNLP/models/enas_utils.py +++ b/fastNLP/models/enas_utils.py @@ -4,21 +4,20 @@ from __future__ import print_function from collections import defaultdict import collections -from datetime import datetime -import os -import json import numpy as np import torch from torch.autograd import Variable + def detach(h): if type(h) == Variable: return Variable(h.data) else: return tuple(detach(v) for v in h) + def get_variable(inputs, cuda=False, **kwargs): if type(inputs) in [list, np.ndarray]: inputs = torch.Tensor(inputs) @@ -28,10 +27,12 @@ def get_variable(inputs, cuda=False, **kwargs): out = Variable(inputs, **kwargs) return out + def update_lr(optimizer, lr): for param_group in optimizer.param_groups: param_group['lr'] = lr + Node = collections.namedtuple('Node', ['id', 'name']) @@ -48,9 +49,9 @@ def to_item(x): """Converts x, possibly scalar and possibly tensor, to a Python scalar.""" if isinstance(x, (float, int)): return x - + if float(torch.__version__[0:3]) < 0.4: assert (x.dim() == 1) and (len(x) == 1) return x[0] - + return x.item() diff --git a/fastNLP/models/sequence_modeling.py b/fastNLP/models/sequence_modeling.py index b9b0677d..e076910f 100644 --- a/fastNLP/models/sequence_modeling.py +++ b/fastNLP/models/sequence_modeling.py @@ -1,9 +1,9 @@ import torch -from fastNLP.models.base_model import BaseModel -from fastNLP.modules import decoder, encoder -from fastNLP.modules.decoder.CRF import allowed_transitions -from fastNLP.modules.utils import seq_mask +from .base_model import BaseModel +from ..modules import decoder, encoder +from ..modules.decoder.CRF import allowed_transitions +from ..modules.utils import seq_mask class SeqLabeling(BaseModel): diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index d4bf3d59..6b54bee6 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -1,11 +1,11 @@ import torch import torch.nn as nn -from fastNLP.models.base_model import BaseModel -from fastNLP.modules import decoder as Decoder -from fastNLP.modules import encoder as Encoder -from fastNLP.modules import aggregator as Aggregator -from fastNLP.modules.utils import seq_mask +from .base_model import BaseModel +from ..modules import decoder as Decoder +from ..modules import encoder as Encoder +from ..modules import aggregator as Aggregator +from ..modules.utils import seq_mask my_inf = 10e12 diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index c3247333..93ee72f6 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -7,7 +7,6 @@ from ..core.const import Const import torch from torch import nn -import torch.nn.functional as F class StarTransEnc(nn.Module): diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index f2f2ac68..67f68ff2 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -4,10 +4,10 @@ import torch import torch.nn.functional as F from torch import nn -from fastNLP.modules.dropout import TimestepDropout -from fastNLP.modules.utils import mask_softmax +from ..dropout import TimestepDropout +from ..utils import mask_softmax -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter class Attention(torch.nn.Module): diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index 9961b87f..fd4414b7 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -1,17 +1,12 @@ -# python: 3.6 -# encoding: utf-8 - import torch import torch.nn as nn class MaxPool(nn.Module): """Max-pooling模块。""" - - def __init__( - self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, - return_indices=False, ceil_mode=False - ): + + def __init__(self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, + return_indices=False, ceil_mode=False): """ :param stride: 窗口移动大小,默认为kernel_size :param padding: padding的内容,默认为0 @@ -30,7 +25,7 @@ class MaxPool(nn.Module): self.kernel_size = kernel_size self.return_indices = return_indices self.ceil_mode = ceil_mode - + def forward(self, x): if self.dimension == 1: pooling = nn.MaxPool1d( @@ -57,10 +52,11 @@ class MaxPool(nn.Module): class MaxPoolWithMask(nn.Module): """带mask矩阵的1维max pooling""" + def __init__(self): super(MaxPoolWithMask, self).__init__() self.inf = 10e12 - + def forward(self, tensor, mask, dim=1): """ :param torch.FloatTensor tensor: [batch_size, seq_len, channels] 初始tensor @@ -75,11 +71,11 @@ class MaxPoolWithMask(nn.Module): class KMaxPool(nn.Module): """K max-pooling module.""" - + def __init__(self, k=1): super(KMaxPool, self).__init__() self.k = k - + def forward(self, x): """ :param torch.Tensor x: [N, C, L] 初始tensor @@ -92,12 +88,12 @@ class KMaxPool(nn.Module): class AvgPool(nn.Module): """1-d average pooling module.""" - + def __init__(self, stride=None, padding=0): super(AvgPool, self).__init__() self.stride = stride self.padding = padding - + def forward(self, x): """ :param torch.Tensor x: [N, C, L] 初始tensor @@ -117,7 +113,7 @@ class MeanPoolWithMask(nn.Module): def __init__(self): super(MeanPoolWithMask, self).__init__() self.inf = 10e12 - + def forward(self, tensor, mask, dim=1): """ :param torch.FloatTensor tensor: [batch_size, seq_len, channels] 初始tensor @@ -127,7 +123,3 @@ class MeanPoolWithMask(nn.Module): """ masks = mask.view(mask.size(0), mask.size(1), -1).float() return torch.sum(tensor * masks.float(), dim=dim) / torch.sum(masks.float(), dim=1) - - - - diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index cc713bc6..4c3ac122 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -1,8 +1,8 @@ import torch from torch import nn -from fastNLP.modules.utils import initial_parameter -from fastNLP.modules.decoder.utils import log_sum_exp +from ..utils import initial_parameter +from ..decoder.utils import log_sum_exp def seq_len_to_byte_mask(seq_lens): diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index e7fafd68..35484932 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -1,7 +1,7 @@ import torch import torch.nn as nn -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter class MLP(nn.Module): diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index 39e4b43e..54b702ea 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -1,7 +1,7 @@ import torch from torch import nn -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter # from torch.nn.init import xavier_uniform diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index d7a8b286..d01eddea 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -5,7 +5,7 @@ import torch import torch.nn as nn import torch.nn.functional as F -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter class ConvMaxpool(nn.Module): diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index 098788a8..8cc53b0b 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -1,5 +1,5 @@ import torch.nn as nn -from fastNLP.modules.utils import get_embeddings +from ..utils import get_embeddings class Embedding(nn.Embedding): """Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" diff --git a/fastNLP/modules/encoder/linear.py b/fastNLP/modules/encoder/linear.py index 2dc31eea..06edf81b 100644 --- a/fastNLP/modules/encoder/linear.py +++ b/fastNLP/modules/encoder/linear.py @@ -1,6 +1,6 @@ import torch.nn as nn -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter class Linear(nn.Module): diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index cff39c84..cc6b1183 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -5,7 +5,7 @@ import torch import torch.nn as nn import torch.nn.utils.rnn as rnn -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter class LSTM(nn.Module): diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index 89ab44d9..2657ebf4 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -3,7 +3,7 @@ import torch import torch.nn as nn from torch.nn.utils.rnn import PackedSequence, pack_padded_sequence, pad_packed_sequence -from fastNLP.modules.utils import initial_parameter +from ..utils import initial_parameter try: from torch import flip From cf08e00c69e3c568b5415c1ecad95aa4cc2c3de9 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 13:22:32 +0800 Subject: [PATCH 077/173] =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E7=9B=B8=E5=AF=B9=E8=B7=AF=E5=BE=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/api/api.py | 4 ++-- fastNLP/api/processor.py | 2 +- fastNLP/core/vocabulary.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index c72f3690..351b210d 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -299,8 +299,8 @@ class CWS(API): te_dataset = reader.load(filepath) pp(te_dataset) - from fastNLP.core.tester import Tester - from fastNLP.core.metrics import BMESF1PreRecMetric + from ..core.tester import Tester + from ..core.metrics import BMESF1PreRecMetric tester = Tester(data=te_dataset, model=cws_model, metrics=BMESF1PreRecMetric(target='target'), batch_size=64, verbose=0) diff --git a/fastNLP/api/processor.py b/fastNLP/api/processor.py index be111cd0..3c60e621 100644 --- a/fastNLP/api/processor.py +++ b/fastNLP/api/processor.py @@ -232,7 +232,7 @@ class SeqLenProcessor(Processor): return dataset -from fastNLP.core.utils import _build_args +from ..core.utils import _build_args class ModelProcessor(Processor): diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index aaaf84be..c82c316e 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -1,6 +1,6 @@ from functools import wraps from collections import Counter -from fastNLP.core.dataset import DataSet +from .dataset import DataSet def _check_build_vocab(func): """A decorator to make sure the indexing is built before used. From 4c6c4f68c79e275018a6f0974c638c3f7ca90071 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 5 May 2019 18:55:13 +0800 Subject: [PATCH 078/173] small change --- fastNLP/core/trainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index b6c282b4..7ace58d9 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -366,7 +366,7 @@ class Trainer(object): 2. torch.device:将模型装载到torch.device上。 - 3. int: 将使用device_id为该值的gpu进行训练 + 3. int: 将使用该device的gpu进行训练 4. list(int):如果多于1个device,将使用torch.nn.DataParallel包裹model, 并使用传入的device。 From b9558e21e6710885c4206aa6f8abd75d6275002e Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 5 May 2019 19:33:30 +0800 Subject: [PATCH 079/173] =?UTF-8?q?1.=20=E4=BB=8E=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=AD=E5=88=A0=E9=99=A4api/automl?= =?UTF-8?q?=E7=9A=84=E5=AE=89=E8=A3=85=202.=20metric=E4=B8=AD=E5=AD=98?= =?UTF-8?q?=E5=9C=A8seq=5Flen=E7=9A=84bug=203.=20sampler=E4=B8=AD=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E5=91=BD=E5=90=8D=E9=94=99=E8=AF=AF=EF=BC=8C=E5=B7=B2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 2 ++ fastNLP/core/metrics.py | 9 ++++----- fastNLP/core/sampler.py | 8 ++++---- test/core/test_sampler.py | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index f04509c1..d893b45a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,5 @@ include LICENSE include README.md prune test/ prune reproduction/ +prune fastNLP/api +prune fastNLP/automl \ No newline at end of file diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 829684d3..be9a3c48 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -269,7 +269,7 @@ class AccuracyMetric(MetricBase): :param pred: 参数映射表中 `pred` 的映射关系,None表示映射关系为 `pred` -> `pred` :param target: 参数映射表中 `target` 的映射关系,None表示映射关系为 `target` -> `target` - :param seq_len: 参数映射表中 `seq_lens` 的映射关系,None表示映射关系为 `seq_len` -> `seq_len` + :param seq_len: 参数映射表中 `seq_len` 的映射关系,None表示映射关系为 `seq_len` -> `seq_len` """ def __init__(self, pred=None, target=None, seq_len=None): @@ -458,7 +458,7 @@ class SpanFPreRecMetric(MetricBase): 在解码时,会将相同xxx的认为是同一个label,比如['B-NN', 'E-NN']会被合并为一个'NN'. :param str pred: 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 :param str target: 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_lens'取数据。 + :param str seq_len: 用该key在evaluate()时从传入dict中取出sequence length数据。为None,则使用'seq_len'取数据。 :param str encoding_type: 目前支持bio, bmes :param list ignore_labels: str 组成的list. 这个list中的class不会被用于计算。例如在POS tagging时传入['NN'],则不会计算'NN'这 个label @@ -729,9 +729,8 @@ class BMESF1PreRecMetric(MetricBase): f"{pred.size()[:-1]}, got {target.size()}.") for idx in range(len(pred)): - seq_len = seq_len[idx] - target_tags = target[idx][:seq_len].tolist() - pred_tags = pred[idx][:seq_len] + target_tags = target[idx][:seq_len[idx]].tolist() + pred_tags = pred[idx][:seq_len[idx]] pred_tags = self._validate_tags(pred_tags) start_idx = 0 for t_idx, (t_tag, p_tag) in enumerate(zip(target_tags, pred_tags)): diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 2182ae1c..e270dac1 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -59,16 +59,16 @@ class BucketSampler(Sampler): :param int num_buckets: bucket的数量 :param int batch_size: batch的大小 - :param str seq_lens_field_name: 对应序列长度的 `field` 的名字 + :param str seq_len_field_name: 对应序列长度的 `field` 的名字 """ - def __init__(self, num_buckets=10, batch_size=32, seq_lens_field_name='seq_len'): + def __init__(self, num_buckets=10, batch_size=32, seq_len_field_name='seq_len'): self.num_buckets = num_buckets self.batch_size = batch_size - self.seq_lens_field_name = seq_lens_field_name + self.seq_len_field_name = seq_len_field_name def __call__(self, data_set): - seq_lens = data_set.get_all_fields()[self.seq_lens_field_name].content + seq_lens = data_set.get_all_fields()[self.seq_len_field_name].content total_sample_num = len(seq_lens) bucket_indexes = [] diff --git a/test/core/test_sampler.py b/test/core/test_sampler.py index f3cbb77f..ba43a973 100644 --- a/test/core/test_sampler.py +++ b/test/core/test_sampler.py @@ -38,7 +38,7 @@ class TestSampler(unittest.TestCase): assert len(_) == 10 def test_BucketSampler(self): - sampler = BucketSampler(num_buckets=3, batch_size=16, seq_lens_field_name="seq_len") + sampler = BucketSampler(num_buckets=3, batch_size=16, seq_len_field_name="seq_len") data_set = DataSet({"x": [[0] * random.randint(1, 10)] * 10, "y": [[5, 6]] * 10}) data_set.apply(lambda ins: len(ins["x"]), new_field_name="seq_len") indices = sampler(data_set) From 3a6a1882181bb0020760123e9f56d15ec123b6c0 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 22:21:19 +0800 Subject: [PATCH 080/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20bug=20=EF=BC=9A?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=20cpu=20=E7=89=88=E6=9C=AC=E7=9A=84=20PyTorc?= =?UTF-8?q?h=20TODO=EF=BC=9A=E5=85=B6=E5=AE=83=E5=9C=B0=E6=96=B9=E5=8F=AF?= =?UTF-8?q?=E8=83=BD=E4=B9=9F=E5=AD=98=E5=9C=A8=E7=B1=BB=E4=BC=BC=E7=9A=84?= =?UTF-8?q?=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 23743ecf..33b69d7e 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -404,6 +404,9 @@ def _move_dict_value_to_device(*args, device: torch.device, non_blocking=False): :param args: :return: """ + if not torch.cuda.is_available(): + return + if not isinstance(device, torch.device): raise TypeError(f"device must be `torch.device`, got `{type(device)}`") From f466c502c5abae393bd4631c48b46cbcac1133f6 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 22:27:38 +0800 Subject: [PATCH 081/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=BC=95=E7=94=A8=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index dc1e3d15..e71c84ec 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -1,13 +1,13 @@ """ 用于IO的模块, 具体包括: -1. 用于读入 embedding 的 :ref:`EmbedLoader ` 类, +1. 用于读入 embedding 的 :doc:`EmbedLoader ` 类, -2. 用于读入数据的 :ref:`DataSetLoader ` 类 +2. 用于读入数据的 :doc:`DataSetLoader ` 类 -3. 用于读写config文件的类, 参考 :ref:`Config-io ` +3. 用于读写config文件的类, 参考 :doc:`Config-IO ` -4. 用于保存和载入模型的类, 参考 :ref:`Model-io ` +4. 用于保存和载入模型的类, 参考 :doc:`Model-IO ` 这些类的使用方法可以在对应module的文档下查看. """ From e209925256277dd4c58314729782800cba4c7a31 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 5 May 2019 22:28:39 +0800 Subject: [PATCH 082/173] =?UTF-8?q?=E6=8A=8A=20tqdm.autonotebook=20?= =?UTF-8?q?=E6=8D=A2=E6=88=90tqdm.auto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/api/utils.py | 8 ++++---- fastNLP/automl/enas_trainer.py | 2 +- fastNLP/core/trainer.py | 2 +- fastNLP/io/dataset_loader.py | 5 ++++- fastNLP/models/enas_trainer.py | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/fastNLP/api/utils.py b/fastNLP/api/utils.py index a54a53d9..e8e7c42a 100644 --- a/fastNLP/api/utils.py +++ b/fastNLP/api/utils.py @@ -20,10 +20,10 @@ except ImportError: from urllib.request import urlopen from urllib.parse import urlparse try: - from tqdm import tqdm -except ImportError: - tqdm = None # defined below - + from tqdm.auto import tqdm +except: + from ..core.utils import _pseudo_tqdm as tqdm + # matches bfd8deac from resnet18-bfd8deac.pth HASH_REGEX = re.compile(r'-([a-f0-9]*)\.') diff --git a/fastNLP/automl/enas_trainer.py b/fastNLP/automl/enas_trainer.py index a9b1b8c3..8f51c2cd 100644 --- a/fastNLP/automl/enas_trainer.py +++ b/fastNLP/automl/enas_trainer.py @@ -9,7 +9,7 @@ import numpy as np import torch try: - from tqdm.autonotebook import tqdm + from tqdm.auto import tqdm except: from ..core.utils import _pseudo_tqdm as tqdm diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 90aa0c19..40eea4a5 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -311,7 +311,7 @@ import torch from torch import nn try: - from tqdm.autonotebook import tqdm + from tqdm.auto import tqdm except: from .utils import _pseudo_tqdm as tqdm diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 6d64ede2..1fe92e23 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -20,7 +20,10 @@ from .file_reader import _read_csv, _read_json, _read_conll def _download_from_url(url, path): - from tqdm import tqdm + try: + from tqdm.auto import tqdm + except: + from ..core.utils import _pseudo_tqdm as tqdm import requests """Download file""" diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index 824b8184..cce93556 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -9,7 +9,7 @@ import torch import math try: - from tqdm.autonotebook import tqdm + from tqdm.auto import tqdm except: from ..core.utils import _pseudo_tqdm as tqdm From 443184f82e58af8940309b73b8ee6b2619ca679a Mon Sep 17 00:00:00 2001 From: yunfan Date: Mon, 6 May 2019 14:55:40 +0800 Subject: [PATCH 083/173] - fix batch & vocab --- fastNLP/core/batch.py | 98 +++++++++++++++++++++----------------- fastNLP/core/vocabulary.py | 4 +- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 4af6d651..235a9a3a 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -9,6 +9,7 @@ import atexit from .sampler import RandomSampler, Sampler import torch.multiprocessing as mp +from queue import Empty, Full _python_is_exit = False @@ -92,7 +93,7 @@ class Batch(object): :return: """ if self.prefetch: - return _run_batch_iter(self) + return self._run_batch_iter(self) def batch_iter(): self.init_iter() @@ -120,6 +121,57 @@ class Batch(object): """ return self.cur_batch_indices + @staticmethod + def _run_fetch(batch, q): + try: + global _python_is_exit + batch.init_iter() + # print('start fetch') + while 1: + res = batch.fetch_one() + # print('fetch one') + while 1: + try: + q.put(res, timeout=3) + break + except Full: + if _python_is_exit: + return + if res is None: + # print('fetch done, waiting processing') + break + # print('fetch exit') + except Exception as e: + q.put(e) + finally: + q.join() + + @staticmethod + def _run_batch_iter(batch): + q = mp.JoinableQueue(maxsize=10) + fetch_p = mp.Process(target=Batch._run_fetch, args=(batch, q)) + fetch_p.daemon = True + fetch_p.start() + # print('fork fetch process') + while 1: + try: + res = q.get(timeout=1) + q.task_done() + # print('get fetched') + if res is None: + break + elif isinstance(res, Exception): + raise res + yield res + except Empty as e: + if fetch_p.is_alive(): + continue + else: + break + fetch_p.terminate() + fetch_p.join() + # print('iter done') + def _to_tensor(batch, dtype): try: @@ -131,47 +183,3 @@ def _to_tensor(batch, dtype): pass return batch - -def _run_fetch(batch, q): - global _python_is_exit - batch.init_iter() - # print('start fetch') - while 1: - res = batch.fetch_one() - # print('fetch one') - while 1: - try: - q.put(res, timeout=3) - break - except: - if _python_is_exit: - return - if res is None: - # print('fetch done, waiting processing') - q.join() - break - # print('fetch exit') - - -def _run_batch_iter(batch): - q = mp.JoinableQueue(maxsize=10) - fetch_p = mp.Process(target=_run_fetch, args=(batch, q)) - fetch_p.daemon = True - fetch_p.start() - # print('fork fetch process') - while 1: - try: - res = q.get(timeout=1) - q.task_done() - # print('get fetched') - if res is None: - break - yield res - except Exception as e: - if fetch_p.is_alive(): - continue - else: - break - fetch_p.terminate() - fetch_p.join() - # print('iter done') diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index c82c316e..0dc232e4 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -110,7 +110,8 @@ class Vocabulary(object): 但已经记录在词典中的词, 不会改变对应的 `int` """ - self.word2idx = {} + if self.word2idx is None: + self.word2idx = {} if self.padding is not None: self.word2idx[self.padding] = len(self.word2idx) if self.unknown is not None: @@ -316,6 +317,7 @@ class Vocabulary(object): """Use to prepare data for pickle. """ + len(self) # make sure vocab has been built state = self.__dict__.copy() # no need to pickle idx2word as it can be constructed from word2idx del state['idx2word'] From c9dc7022e4e8142dd113632c52d218e5f70dcb8f Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 6 May 2019 15:16:11 +0800 Subject: [PATCH 084/173] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=96=87=E4=BB=B6=20*.rst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fastNLP.component.bert_tokenizer.rst | 7 +++ docs/source/fastNLP.component.rst | 15 ++++++ docs/source/fastNLP.core.batch.rst | 7 +++ docs/source/fastNLP.core.callback.rst | 7 +++ docs/source/fastNLP.core.const.rst | 7 +++ docs/source/fastNLP.core.dataset.rst | 7 +++ docs/source/fastNLP.core.field.rst | 7 +++ docs/source/fastNLP.core.instance.rst | 7 +++ docs/source/fastNLP.core.losses.rst | 7 +++ docs/source/fastNLP.core.metrics.rst | 7 +++ docs/source/fastNLP.core.optimizer.rst | 7 +++ docs/source/fastNLP.core.rst | 28 ++++++++++ docs/source/fastNLP.core.sampler.rst | 7 +++ docs/source/fastNLP.core.tester.rst | 7 +++ docs/source/fastNLP.core.trainer.rst | 7 +++ docs/source/fastNLP.core.utils.rst | 7 +++ docs/source/fastNLP.core.vocabulary.rst | 7 +++ docs/source/fastNLP.io.base_loader.rst | 7 +++ docs/source/fastNLP.io.config_io.rst | 7 +++ docs/source/fastNLP.io.dataset_loader.rst | 7 +++ docs/source/fastNLP.io.embed_loader.rst | 7 +++ docs/source/fastNLP.io.file_reader.rst | 7 +++ docs/source/fastNLP.io.model_io.rst | 7 +++ docs/source/fastNLP.io.rst | 20 +++++++ docs/source/fastNLP.models.base_model.rst | 7 +++ docs/source/fastNLP.models.bert.rst | 7 +++ .../source/fastNLP.models.biaffine_parser.rst | 7 +++ .../fastNLP.models.char_language_model.rst | 7 +++ ...fastNLP.models.cnn_text_classification.rst | 7 +++ .../source/fastNLP.models.enas_controller.rst | 7 +++ docs/source/fastNLP.models.enas_model.rst | 7 +++ docs/source/fastNLP.models.enas_trainer.rst | 7 +++ docs/source/fastNLP.models.enas_utils.rst | 7 +++ docs/source/fastNLP.models.rst | 26 ++++++++++ .../fastNLP.models.sequence_modeling.rst | 7 +++ docs/source/fastNLP.models.snli.rst | 7 +++ .../fastNLP.models.star_transformer.rst | 7 +++ .../fastNLP.modules.aggregator.attention.rst | 7 +++ .../fastNLP.modules.aggregator.pooling.rst | 7 +++ docs/source/fastNLP.modules.aggregator.rst | 16 ++++++ docs/source/fastNLP.modules.decoder.CRF.rst | 7 +++ docs/source/fastNLP.modules.decoder.MLP.rst | 7 +++ docs/source/fastNLP.modules.decoder.rst | 17 ++++++ docs/source/fastNLP.modules.decoder.utils.rst | 7 +++ docs/source/fastNLP.modules.dropout.rst | 7 +++ docs/source/fastNLP.modules.encoder.bert.rst | 7 +++ .../fastNLP.modules.encoder.char_encoder.rst | 7 +++ .../fastNLP.modules.encoder.conv_maxpool.rst | 7 +++ .../fastNLP.modules.encoder.embedding.rst | 7 +++ .../source/fastNLP.modules.encoder.linear.rst | 7 +++ docs/source/fastNLP.modules.encoder.lstm.rst | 7 +++ docs/source/fastNLP.modules.encoder.rst | 23 ++++++++ ...stNLP.modules.encoder.star_transformer.rst | 7 +++ .../fastNLP.modules.encoder.transformer.rst | 7 +++ ...astNLP.modules.encoder.variational_rnn.rst | 7 +++ docs/source/fastNLP.modules.other_modules.rst | 7 +++ docs/source/fastNLP.modules.rst | 18 +++++++ docs/source/fastNLP.modules.utils.rst | 7 +++ docs/source/fastNLP.rst | 19 +++++++ docs/source/index.rst | 52 +++++++++++++------ docs/source/modules.rst | 7 +++ docs/source/user/installation.rst | 22 ++++---- docs/source/user/quickstart.rst | 14 ++--- docs/source/user/task2.rst | 3 ++ fastNLP/__init__.py | 11 +++- fastNLP/models/__init__.py | 5 ++ fastNLP/models/cnn_text_classification.py | 18 +++---- fastNLP/modules/__init__.py | 14 +++-- fastNLP/modules/aggregator/__init__.py | 3 +- fastNLP/modules/aggregator/attention.py | 6 +-- fastNLP/modules/decoder/CRF.py | 21 ++++---- fastNLP/modules/decoder/__init__.py | 1 + 72 files changed, 643 insertions(+), 66 deletions(-) create mode 100644 docs/source/fastNLP.component.bert_tokenizer.rst create mode 100644 docs/source/fastNLP.component.rst create mode 100644 docs/source/fastNLP.core.batch.rst create mode 100644 docs/source/fastNLP.core.callback.rst create mode 100644 docs/source/fastNLP.core.const.rst create mode 100644 docs/source/fastNLP.core.dataset.rst create mode 100644 docs/source/fastNLP.core.field.rst create mode 100644 docs/source/fastNLP.core.instance.rst create mode 100644 docs/source/fastNLP.core.losses.rst create mode 100644 docs/source/fastNLP.core.metrics.rst create mode 100644 docs/source/fastNLP.core.optimizer.rst create mode 100644 docs/source/fastNLP.core.rst create mode 100644 docs/source/fastNLP.core.sampler.rst create mode 100644 docs/source/fastNLP.core.tester.rst create mode 100644 docs/source/fastNLP.core.trainer.rst create mode 100644 docs/source/fastNLP.core.utils.rst create mode 100644 docs/source/fastNLP.core.vocabulary.rst create mode 100644 docs/source/fastNLP.io.base_loader.rst create mode 100644 docs/source/fastNLP.io.config_io.rst create mode 100644 docs/source/fastNLP.io.dataset_loader.rst create mode 100644 docs/source/fastNLP.io.embed_loader.rst create mode 100644 docs/source/fastNLP.io.file_reader.rst create mode 100644 docs/source/fastNLP.io.model_io.rst create mode 100644 docs/source/fastNLP.io.rst create mode 100644 docs/source/fastNLP.models.base_model.rst create mode 100644 docs/source/fastNLP.models.bert.rst create mode 100644 docs/source/fastNLP.models.biaffine_parser.rst create mode 100644 docs/source/fastNLP.models.char_language_model.rst create mode 100644 docs/source/fastNLP.models.cnn_text_classification.rst create mode 100644 docs/source/fastNLP.models.enas_controller.rst create mode 100644 docs/source/fastNLP.models.enas_model.rst create mode 100644 docs/source/fastNLP.models.enas_trainer.rst create mode 100644 docs/source/fastNLP.models.enas_utils.rst create mode 100644 docs/source/fastNLP.models.rst create mode 100644 docs/source/fastNLP.models.sequence_modeling.rst create mode 100644 docs/source/fastNLP.models.snli.rst create mode 100644 docs/source/fastNLP.models.star_transformer.rst create mode 100644 docs/source/fastNLP.modules.aggregator.attention.rst create mode 100644 docs/source/fastNLP.modules.aggregator.pooling.rst create mode 100644 docs/source/fastNLP.modules.aggregator.rst create mode 100644 docs/source/fastNLP.modules.decoder.CRF.rst create mode 100644 docs/source/fastNLP.modules.decoder.MLP.rst create mode 100644 docs/source/fastNLP.modules.decoder.rst create mode 100644 docs/source/fastNLP.modules.decoder.utils.rst create mode 100644 docs/source/fastNLP.modules.dropout.rst create mode 100644 docs/source/fastNLP.modules.encoder.bert.rst create mode 100644 docs/source/fastNLP.modules.encoder.char_encoder.rst create mode 100644 docs/source/fastNLP.modules.encoder.conv_maxpool.rst create mode 100644 docs/source/fastNLP.modules.encoder.embedding.rst create mode 100644 docs/source/fastNLP.modules.encoder.linear.rst create mode 100644 docs/source/fastNLP.modules.encoder.lstm.rst create mode 100644 docs/source/fastNLP.modules.encoder.rst create mode 100644 docs/source/fastNLP.modules.encoder.star_transformer.rst create mode 100644 docs/source/fastNLP.modules.encoder.transformer.rst create mode 100644 docs/source/fastNLP.modules.encoder.variational_rnn.rst create mode 100644 docs/source/fastNLP.modules.other_modules.rst create mode 100644 docs/source/fastNLP.modules.rst create mode 100644 docs/source/fastNLP.modules.utils.rst create mode 100644 docs/source/fastNLP.rst create mode 100644 docs/source/modules.rst create mode 100644 docs/source/user/task2.rst diff --git a/docs/source/fastNLP.component.bert_tokenizer.rst b/docs/source/fastNLP.component.bert_tokenizer.rst new file mode 100644 index 00000000..ccfc50c6 --- /dev/null +++ b/docs/source/fastNLP.component.bert_tokenizer.rst @@ -0,0 +1,7 @@ +fastNLP.component.bert\_tokenizer module +======================================== + +.. automodule:: fastNLP.component.bert_tokenizer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.component.rst b/docs/source/fastNLP.component.rst new file mode 100644 index 00000000..3e15fa47 --- /dev/null +++ b/docs/source/fastNLP.component.rst @@ -0,0 +1,15 @@ +fastNLP.component package +========================= + +.. automodule:: fastNLP.component + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.component.bert_tokenizer + diff --git a/docs/source/fastNLP.core.batch.rst b/docs/source/fastNLP.core.batch.rst new file mode 100644 index 00000000..b0294bac --- /dev/null +++ b/docs/source/fastNLP.core.batch.rst @@ -0,0 +1,7 @@ +fastNLP.core.batch module +========================= + +.. automodule:: fastNLP.core.batch + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.callback.rst b/docs/source/fastNLP.core.callback.rst new file mode 100644 index 00000000..075712e8 --- /dev/null +++ b/docs/source/fastNLP.core.callback.rst @@ -0,0 +1,7 @@ +fastNLP.core.callback module +============================ + +.. automodule:: fastNLP.core.callback + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.const.rst b/docs/source/fastNLP.core.const.rst new file mode 100644 index 00000000..288fcf50 --- /dev/null +++ b/docs/source/fastNLP.core.const.rst @@ -0,0 +1,7 @@ +fastNLP.core.const module +========================= + +.. automodule:: fastNLP.core.const + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.dataset.rst b/docs/source/fastNLP.core.dataset.rst new file mode 100644 index 00000000..24e1ab4e --- /dev/null +++ b/docs/source/fastNLP.core.dataset.rst @@ -0,0 +1,7 @@ +fastNLP.core.dataset module +=========================== + +.. automodule:: fastNLP.core.dataset + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.field.rst b/docs/source/fastNLP.core.field.rst new file mode 100644 index 00000000..23eb47bc --- /dev/null +++ b/docs/source/fastNLP.core.field.rst @@ -0,0 +1,7 @@ +fastNLP.core.field module +========================= + +.. automodule:: fastNLP.core.field + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.instance.rst b/docs/source/fastNLP.core.instance.rst new file mode 100644 index 00000000..db731fe9 --- /dev/null +++ b/docs/source/fastNLP.core.instance.rst @@ -0,0 +1,7 @@ +fastNLP.core.instance module +============================ + +.. automodule:: fastNLP.core.instance + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.losses.rst b/docs/source/fastNLP.core.losses.rst new file mode 100644 index 00000000..7f4e02b6 --- /dev/null +++ b/docs/source/fastNLP.core.losses.rst @@ -0,0 +1,7 @@ +fastNLP.core.losses module +========================== + +.. automodule:: fastNLP.core.losses + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.metrics.rst b/docs/source/fastNLP.core.metrics.rst new file mode 100644 index 00000000..d700a525 --- /dev/null +++ b/docs/source/fastNLP.core.metrics.rst @@ -0,0 +1,7 @@ +fastNLP.core.metrics module +=========================== + +.. automodule:: fastNLP.core.metrics + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.optimizer.rst b/docs/source/fastNLP.core.optimizer.rst new file mode 100644 index 00000000..737fc430 --- /dev/null +++ b/docs/source/fastNLP.core.optimizer.rst @@ -0,0 +1,7 @@ +fastNLP.core.optimizer module +============================= + +.. automodule:: fastNLP.core.optimizer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst new file mode 100644 index 00000000..01c59e21 --- /dev/null +++ b/docs/source/fastNLP.core.rst @@ -0,0 +1,28 @@ +fastNLP.core package +==================== + +.. automodule:: fastNLP.core + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.core.batch + fastNLP.core.callback + fastNLP.core.const + fastNLP.core.dataset + fastNLP.core.field + fastNLP.core.instance + fastNLP.core.losses + fastNLP.core.metrics + fastNLP.core.optimizer + fastNLP.core.sampler + fastNLP.core.tester + fastNLP.core.trainer + fastNLP.core.utils + fastNLP.core.vocabulary + diff --git a/docs/source/fastNLP.core.sampler.rst b/docs/source/fastNLP.core.sampler.rst new file mode 100644 index 00000000..a827d49c --- /dev/null +++ b/docs/source/fastNLP.core.sampler.rst @@ -0,0 +1,7 @@ +fastNLP.core.sampler module +=========================== + +.. automodule:: fastNLP.core.sampler + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.tester.rst b/docs/source/fastNLP.core.tester.rst new file mode 100644 index 00000000..30cebe28 --- /dev/null +++ b/docs/source/fastNLP.core.tester.rst @@ -0,0 +1,7 @@ +fastNLP.core.tester module +========================== + +.. automodule:: fastNLP.core.tester + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.trainer.rst b/docs/source/fastNLP.core.trainer.rst new file mode 100644 index 00000000..648feb9d --- /dev/null +++ b/docs/source/fastNLP.core.trainer.rst @@ -0,0 +1,7 @@ +fastNLP.core.trainer module +=========================== + +.. automodule:: fastNLP.core.trainer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.utils.rst b/docs/source/fastNLP.core.utils.rst new file mode 100644 index 00000000..2bec7f62 --- /dev/null +++ b/docs/source/fastNLP.core.utils.rst @@ -0,0 +1,7 @@ +fastNLP.core.utils module +========================= + +.. automodule:: fastNLP.core.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.core.vocabulary.rst b/docs/source/fastNLP.core.vocabulary.rst new file mode 100644 index 00000000..98d8d24d --- /dev/null +++ b/docs/source/fastNLP.core.vocabulary.rst @@ -0,0 +1,7 @@ +fastNLP.core.vocabulary module +============================== + +.. automodule:: fastNLP.core.vocabulary + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.base_loader.rst b/docs/source/fastNLP.io.base_loader.rst new file mode 100644 index 00000000..b3375f74 --- /dev/null +++ b/docs/source/fastNLP.io.base_loader.rst @@ -0,0 +1,7 @@ +fastNLP.io.base\_loader module +============================== + +.. automodule:: fastNLP.io.base_loader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.config_io.rst b/docs/source/fastNLP.io.config_io.rst new file mode 100644 index 00000000..c6bf4bae --- /dev/null +++ b/docs/source/fastNLP.io.config_io.rst @@ -0,0 +1,7 @@ +fastNLP.io.config\_io module +============================ + +.. automodule:: fastNLP.io.config_io + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.dataset_loader.rst b/docs/source/fastNLP.io.dataset_loader.rst new file mode 100644 index 00000000..89f9b165 --- /dev/null +++ b/docs/source/fastNLP.io.dataset_loader.rst @@ -0,0 +1,7 @@ +fastNLP.io.dataset\_loader module +================================= + +.. automodule:: fastNLP.io.dataset_loader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.embed_loader.rst b/docs/source/fastNLP.io.embed_loader.rst new file mode 100644 index 00000000..1f135155 --- /dev/null +++ b/docs/source/fastNLP.io.embed_loader.rst @@ -0,0 +1,7 @@ +fastNLP.io.embed\_loader module +=============================== + +.. automodule:: fastNLP.io.embed_loader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.file_reader.rst b/docs/source/fastNLP.io.file_reader.rst new file mode 100644 index 00000000..233ee399 --- /dev/null +++ b/docs/source/fastNLP.io.file_reader.rst @@ -0,0 +1,7 @@ +fastNLP.io.file\_reader module +============================== + +.. automodule:: fastNLP.io.file_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.model_io.rst b/docs/source/fastNLP.io.model_io.rst new file mode 100644 index 00000000..75f1df69 --- /dev/null +++ b/docs/source/fastNLP.io.model_io.rst @@ -0,0 +1,7 @@ +fastNLP.io.model\_io module +=========================== + +.. automodule:: fastNLP.io.model_io + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst new file mode 100644 index 00000000..5af9d538 --- /dev/null +++ b/docs/source/fastNLP.io.rst @@ -0,0 +1,20 @@ +fastNLP.io package +================== + +.. automodule:: fastNLP.io + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.io.base_loader + fastNLP.io.config_io + fastNLP.io.dataset_loader + fastNLP.io.embed_loader + fastNLP.io.file_reader + fastNLP.io.model_io + diff --git a/docs/source/fastNLP.models.base_model.rst b/docs/source/fastNLP.models.base_model.rst new file mode 100644 index 00000000..2537f75f --- /dev/null +++ b/docs/source/fastNLP.models.base_model.rst @@ -0,0 +1,7 @@ +fastNLP.models.base\_model module +================================= + +.. automodule:: fastNLP.models.base_model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.bert.rst b/docs/source/fastNLP.models.bert.rst new file mode 100644 index 00000000..7ac64ad7 --- /dev/null +++ b/docs/source/fastNLP.models.bert.rst @@ -0,0 +1,7 @@ +fastNLP.models.bert module +========================== + +.. automodule:: fastNLP.models.bert + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.biaffine_parser.rst b/docs/source/fastNLP.models.biaffine_parser.rst new file mode 100644 index 00000000..448dff09 --- /dev/null +++ b/docs/source/fastNLP.models.biaffine_parser.rst @@ -0,0 +1,7 @@ +fastNLP.models.biaffine\_parser module +====================================== + +.. automodule:: fastNLP.models.biaffine_parser + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.char_language_model.rst b/docs/source/fastNLP.models.char_language_model.rst new file mode 100644 index 00000000..8cfc9e6e --- /dev/null +++ b/docs/source/fastNLP.models.char_language_model.rst @@ -0,0 +1,7 @@ +fastNLP.models.char\_language\_model module +=========================================== + +.. automodule:: fastNLP.models.char_language_model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.cnn_text_classification.rst b/docs/source/fastNLP.models.cnn_text_classification.rst new file mode 100644 index 00000000..31807494 --- /dev/null +++ b/docs/source/fastNLP.models.cnn_text_classification.rst @@ -0,0 +1,7 @@ +fastNLP.models.cnn\_text\_classification module +=============================================== + +.. automodule:: fastNLP.models.cnn_text_classification + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_controller.rst b/docs/source/fastNLP.models.enas_controller.rst new file mode 100644 index 00000000..7977de81 --- /dev/null +++ b/docs/source/fastNLP.models.enas_controller.rst @@ -0,0 +1,7 @@ +fastNLP.models.enas\_controller module +====================================== + +.. automodule:: fastNLP.models.enas_controller + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_model.rst b/docs/source/fastNLP.models.enas_model.rst new file mode 100644 index 00000000..518f56b7 --- /dev/null +++ b/docs/source/fastNLP.models.enas_model.rst @@ -0,0 +1,7 @@ +fastNLP.models.enas\_model module +================================= + +.. automodule:: fastNLP.models.enas_model + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_trainer.rst b/docs/source/fastNLP.models.enas_trainer.rst new file mode 100644 index 00000000..45f77f31 --- /dev/null +++ b/docs/source/fastNLP.models.enas_trainer.rst @@ -0,0 +1,7 @@ +fastNLP.models.enas\_trainer module +=================================== + +.. automodule:: fastNLP.models.enas_trainer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_utils.rst b/docs/source/fastNLP.models.enas_utils.rst new file mode 100644 index 00000000..5f05a4fc --- /dev/null +++ b/docs/source/fastNLP.models.enas_utils.rst @@ -0,0 +1,7 @@ +fastNLP.models.enas\_utils module +================================= + +.. automodule:: fastNLP.models.enas_utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst new file mode 100644 index 00000000..ae4abf7a --- /dev/null +++ b/docs/source/fastNLP.models.rst @@ -0,0 +1,26 @@ +fastNLP.models package +====================== + +.. automodule:: fastNLP.models + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.models.base_model + fastNLP.models.bert + fastNLP.models.biaffine_parser + fastNLP.models.char_language_model + fastNLP.models.cnn_text_classification + fastNLP.models.enas_controller + fastNLP.models.enas_model + fastNLP.models.enas_trainer + fastNLP.models.enas_utils + fastNLP.models.sequence_modeling + fastNLP.models.snli + fastNLP.models.star_transformer + diff --git a/docs/source/fastNLP.models.sequence_modeling.rst b/docs/source/fastNLP.models.sequence_modeling.rst new file mode 100644 index 00000000..7fa7e450 --- /dev/null +++ b/docs/source/fastNLP.models.sequence_modeling.rst @@ -0,0 +1,7 @@ +fastNLP.models.sequence\_modeling module +======================================== + +.. automodule:: fastNLP.models.sequence_modeling + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.snli.rst b/docs/source/fastNLP.models.snli.rst new file mode 100644 index 00000000..b24bc196 --- /dev/null +++ b/docs/source/fastNLP.models.snli.rst @@ -0,0 +1,7 @@ +fastNLP.models.snli module +========================== + +.. automodule:: fastNLP.models.snli + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.models.star_transformer.rst b/docs/source/fastNLP.models.star_transformer.rst new file mode 100644 index 00000000..f2185935 --- /dev/null +++ b/docs/source/fastNLP.models.star_transformer.rst @@ -0,0 +1,7 @@ +fastNLP.models.star\_transformer module +======================================= + +.. automodule:: fastNLP.models.star_transformer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.aggregator.attention.rst b/docs/source/fastNLP.modules.aggregator.attention.rst new file mode 100644 index 00000000..46251e73 --- /dev/null +++ b/docs/source/fastNLP.modules.aggregator.attention.rst @@ -0,0 +1,7 @@ +fastNLP.modules.aggregator.attention module +=========================================== + +.. automodule:: fastNLP.modules.aggregator.attention + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.aggregator.pooling.rst b/docs/source/fastNLP.modules.aggregator.pooling.rst new file mode 100644 index 00000000..f6730430 --- /dev/null +++ b/docs/source/fastNLP.modules.aggregator.pooling.rst @@ -0,0 +1,7 @@ +fastNLP.modules.aggregator.pooling module +========================================= + +.. automodule:: fastNLP.modules.aggregator.pooling + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst new file mode 100644 index 00000000..b9b331c3 --- /dev/null +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -0,0 +1,16 @@ +fastNLP.modules.aggregator package +================================== + +.. automodule:: fastNLP.modules.aggregator + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.modules.aggregator.attention + fastNLP.modules.aggregator.pooling + diff --git a/docs/source/fastNLP.modules.decoder.CRF.rst b/docs/source/fastNLP.modules.decoder.CRF.rst new file mode 100644 index 00000000..8d980bbd --- /dev/null +++ b/docs/source/fastNLP.modules.decoder.CRF.rst @@ -0,0 +1,7 @@ +fastNLP.modules.decoder.CRF module +================================== + +.. automodule:: fastNLP.modules.decoder.CRF + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.MLP.rst b/docs/source/fastNLP.modules.decoder.MLP.rst new file mode 100644 index 00000000..787a3c33 --- /dev/null +++ b/docs/source/fastNLP.modules.decoder.MLP.rst @@ -0,0 +1,7 @@ +fastNLP.modules.decoder.MLP module +================================== + +.. automodule:: fastNLP.modules.decoder.MLP + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst new file mode 100644 index 00000000..10fdbd90 --- /dev/null +++ b/docs/source/fastNLP.modules.decoder.rst @@ -0,0 +1,17 @@ +fastNLP.modules.decoder package +=============================== + +.. automodule:: fastNLP.modules.decoder + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.modules.decoder.CRF + fastNLP.modules.decoder.MLP + fastNLP.modules.decoder.utils + diff --git a/docs/source/fastNLP.modules.decoder.utils.rst b/docs/source/fastNLP.modules.decoder.utils.rst new file mode 100644 index 00000000..b829baf7 --- /dev/null +++ b/docs/source/fastNLP.modules.decoder.utils.rst @@ -0,0 +1,7 @@ +fastNLP.modules.decoder.utils module +==================================== + +.. automodule:: fastNLP.modules.decoder.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.dropout.rst b/docs/source/fastNLP.modules.dropout.rst new file mode 100644 index 00000000..f525e08f --- /dev/null +++ b/docs/source/fastNLP.modules.dropout.rst @@ -0,0 +1,7 @@ +fastNLP.modules.dropout module +============================== + +.. automodule:: fastNLP.modules.dropout + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.bert.rst b/docs/source/fastNLP.modules.encoder.bert.rst new file mode 100644 index 00000000..6f811792 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.bert.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.bert module +=================================== + +.. automodule:: fastNLP.modules.encoder.bert + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.char_encoder.rst b/docs/source/fastNLP.modules.encoder.char_encoder.rst new file mode 100644 index 00000000..12431c70 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.char_encoder.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.char\_encoder module +============================================ + +.. automodule:: fastNLP.modules.encoder.char_encoder + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.conv_maxpool.rst b/docs/source/fastNLP.modules.encoder.conv_maxpool.rst new file mode 100644 index 00000000..c40a5f39 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.conv_maxpool.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.conv\_maxpool module +============================================ + +.. automodule:: fastNLP.modules.encoder.conv_maxpool + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.embedding.rst b/docs/source/fastNLP.modules.encoder.embedding.rst new file mode 100644 index 00000000..abdd5fd2 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.embedding.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.embedding module +======================================== + +.. automodule:: fastNLP.modules.encoder.embedding + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.linear.rst b/docs/source/fastNLP.modules.encoder.linear.rst new file mode 100644 index 00000000..7a479ca2 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.linear.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.linear module +===================================== + +.. automodule:: fastNLP.modules.encoder.linear + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.lstm.rst b/docs/source/fastNLP.modules.encoder.lstm.rst new file mode 100644 index 00000000..897e7a5f --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.lstm.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.lstm module +=================================== + +.. automodule:: fastNLP.modules.encoder.lstm + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst new file mode 100644 index 00000000..3ee246c2 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.rst @@ -0,0 +1,23 @@ +fastNLP.modules.encoder package +=============================== + +.. automodule:: fastNLP.modules.encoder + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +.. toctree:: + + fastNLP.modules.encoder.bert + fastNLP.modules.encoder.char_encoder + fastNLP.modules.encoder.conv_maxpool + fastNLP.modules.encoder.embedding + fastNLP.modules.encoder.linear + fastNLP.modules.encoder.lstm + fastNLP.modules.encoder.star_transformer + fastNLP.modules.encoder.transformer + fastNLP.modules.encoder.variational_rnn + diff --git a/docs/source/fastNLP.modules.encoder.star_transformer.rst b/docs/source/fastNLP.modules.encoder.star_transformer.rst new file mode 100644 index 00000000..57cd6dcf --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.star_transformer.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.star\_transformer module +================================================ + +.. automodule:: fastNLP.modules.encoder.star_transformer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.transformer.rst b/docs/source/fastNLP.modules.encoder.transformer.rst new file mode 100644 index 00000000..4b63686e --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.transformer.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.transformer module +========================================== + +.. automodule:: fastNLP.modules.encoder.transformer + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.variational_rnn.rst b/docs/source/fastNLP.modules.encoder.variational_rnn.rst new file mode 100644 index 00000000..4472d5c2 --- /dev/null +++ b/docs/source/fastNLP.modules.encoder.variational_rnn.rst @@ -0,0 +1,7 @@ +fastNLP.modules.encoder.variational\_rnn module +=============================================== + +.. automodule:: fastNLP.modules.encoder.variational_rnn + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.other_modules.rst b/docs/source/fastNLP.modules.other_modules.rst new file mode 100644 index 00000000..c1bba5fb --- /dev/null +++ b/docs/source/fastNLP.modules.other_modules.rst @@ -0,0 +1,7 @@ +fastNLP.modules.other\_modules module +===================================== + +.. automodule:: fastNLP.modules.other_modules + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst new file mode 100644 index 00000000..23f5523a --- /dev/null +++ b/docs/source/fastNLP.modules.rst @@ -0,0 +1,18 @@ + + +fastNLP.modules package +======================= + +.. automodule:: fastNLP.modules + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + fastNLP.modules.aggregator + fastNLP.modules.decoder + fastNLP.modules.encoder \ No newline at end of file diff --git a/docs/source/fastNLP.modules.utils.rst b/docs/source/fastNLP.modules.utils.rst new file mode 100644 index 00000000..b7eb672e --- /dev/null +++ b/docs/source/fastNLP.modules.utils.rst @@ -0,0 +1,7 @@ +fastNLP.modules.utils module +============================ + +.. automodule:: fastNLP.modules.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst new file mode 100644 index 00000000..30f405f0 --- /dev/null +++ b/docs/source/fastNLP.rst @@ -0,0 +1,19 @@ +fastNLP package +=============== + +.. automodule:: fastNLP + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + + fastNLP.component + fastNLP.core + fastNLP.io + fastNLP.models + fastNLP.modules + diff --git a/docs/source/index.rst b/docs/source/index.rst index 9f410f41..b08c2b2d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,13 +1,21 @@ -fastNLP documentation +fastNLP 中文文档 ===================== -A Modularized and Extensible Toolkit for Natural Language Processing. Currently still in incubation. +fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地完成一个命名实体识别(NER)、中文分词或文本分类任务; +也可以使用他构建许多复杂的网络模型,进行科研。它具有如下的特性: -Introduction +- 代码简洁易懂,有着详尽的中文文档以供查阅; +- 深度学习的各个阶段划分明确,配合 fitlog 使用让科研更轻松; +- 内置多种常见模型 (TODO); +- 基于 PyTorch ,方便从原生 PyTorch 代码迁入,并能使用 PyTorch 中的各种组件; +- 便于 seq2seq; +- 便于 fine-tune + +内置的模块 ------------ -FastNLP is a modular Natural Language Processing system based on -PyTorch, built for fast development of NLP models. +(TODO) + A deep learning NLP model is the composition of three types of modules: @@ -33,31 +41,41 @@ For example: +各个任务上的结果 +----------------------- + +(TODO) + +快速入门 +------------- + +TODO + + +用户手册 +--------------- -User's Guide ------------- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - user/installation - user/quickstart + 安装指南 + 快速入门 + 使用 fastNLP 分类 + 使用 fastNLP 分词 -API Reference +API 文档 ------------- -If you are looking for information on a specific function, class or -method, this part of the documentation is for you. +除了用户手册之外,你还可以通过查阅 API 文档来找到你所需要的工具。 .. toctree:: :maxdepth: 2 - fastNLP API - - + fastNLP -Indices and tables +索引与搜索 ================== * :ref:`genindex` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 00000000..e9a92cb7 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +fastNLP +======= + +.. toctree:: + :maxdepth: 4 + + fastNLP diff --git a/docs/source/user/installation.rst b/docs/source/user/installation.rst index 5dfe4a11..e22c4202 100644 --- a/docs/source/user/installation.rst +++ b/docs/source/user/installation.rst @@ -1,17 +1,21 @@ -============ -Installation -============ +=============== +安装指南 +=============== .. contents:: :local: -Make sure your environment satisfies https://github.com/fastnlp/fastNLP/blob/master/requirements.txt . +fastNLP 依赖如下包:: -Run the following commands to install fastNLP package: + torch>=0.4.0 + numpy + tensorboardX + tqdm + nltk -.. code:: shell - - pip install fastNLP - +其中torch的安装可能与操作系统及 CUDA 的版本相关,请参见 `PyTorch 官网 `_ 。 +在依赖包安装完成的情况,您可以在命令行执行如下指令完成安装 +.. code:: shell + >>> pip install fitlog diff --git a/docs/source/user/quickstart.rst b/docs/source/user/quickstart.rst index a5eb9402..599254e9 100644 --- a/docs/source/user/quickstart.rst +++ b/docs/source/user/quickstart.rst @@ -1,11 +1,3 @@ -Quickstart -========== - -.. toctree:: - :maxdepth: 1 - - ../tutorials/fastnlp_1_minute_tutorial - ../tutorials/fastnlp_10tmin_tutorial - ../tutorials/fastnlp_advanced_tutorial - ../tutorials/fastnlp_developer_guide - +=============== +快速入门 +=============== \ No newline at end of file diff --git a/docs/source/user/task2.rst b/docs/source/user/task2.rst new file mode 100644 index 00000000..73ee014b --- /dev/null +++ b/docs/source/user/task2.rst @@ -0,0 +1,3 @@ +===================== +用 fastNLP 分词 +===================== \ No newline at end of file diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index e7975c9b..25d7ea82 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -1,5 +1,14 @@ """ -fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.modules` 等子模块组成,但常用的组件都可以直接 import ,常用组件如下: +fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.modules`、:mod:`~fastNLP.models` +和 :mod:`~fastNLP.component` 等子模块组成。 + +- :mod:`~fastNLP.core` fastNLP 的核心模块,包括 DataSet、 Trainer、 Tester 等组件 +- :mod:`~fastNLP.io` fastNLP 的输入输出模块,实现了数据集的读取,模型的存取等功能 +- :mod:`~fastNLP.modules` TODO 如何介绍 +- :mod:`~fastNLP.models` 使用 fastNLP 实现的一些常见模型,具体参见 :doc:`fastNLP.models` +- :mod:`~fastNLP.component` TODO + +fastNLP 中最常用的组件可以直接从 fastNLP 包中 import ,他们的文档如下: """ __all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", "Trainer", "Tester", "Callback", diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index a83c3936..657f67ec 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -1,3 +1,8 @@ +""" +使用 fastNLP 实现的一系列常见模型,具体有: +TODO 详细介绍的表格,与主页相对应 + +""" from .base_model import BaseModel from .biaffine_parser import BiaffineParser, GraphParser from .char_language_model import CharLM diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index 86848d0c..7d7c3878 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -12,6 +12,14 @@ class CNNText(torch.nn.Module): """ 使用CNN进行文本分类的模型 'Yoon Kim. 2014. Convolution Neural Networks for Sentence Classification.' + + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding + :param int num_classes: 一共有多少类 + :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 + :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 + :param int padding: + :param float dropout: Dropout的大小 """ def __init__(self, init_embed, @@ -20,16 +28,6 @@ class CNNText(torch.nn.Module): kernel_sizes=(3, 4, 5), padding=0, dropout=0.5): - """ - - :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), - 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding - :param int num_classes: 一共有多少类 - :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 - :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 - :param int padding: - :param float dropout: Dropout的大小 - """ super(CNNText, self).__init__() # no support for pre-trained embedding currently diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index 37223394..6ff356f1 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -1,13 +1,19 @@ +""" +modules 模块是 fastNLP 的重要组成部分,它实现了神经网络构建中常见的组件, +具体包括 TODO + +可以和 PyTorch 结合使用?TODO + +TODO __all__ 里面多暴露一些 + +""" from . import aggregator from . import decoder from . import encoder from .aggregator import * from .decoder import * +from .other_modules import * from .dropout import TimestepDropout from .encoder import * __version__ = '0.0.0' - -__all__ = ['encoder', - 'decoder', - 'aggregator'] diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 51106a76..dbf53662 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,3 +1,5 @@ +__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MeanPoolWithMask", "KMaxPool", "Attention", "BiAttention", + "SelfAttention"] from .pooling import MaxPool from .pooling import MaxPoolWithMask from .pooling import AvgPool @@ -7,4 +9,3 @@ from .pooling import KMaxPool from .attention import Attention from .attention import BiAttention from .attention import SelfAttention - diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index 67f68ff2..fb6f5dc5 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -117,11 +117,11 @@ class BiAttention(nn.Module): .. math:: - \begin{array}{ll} \\ - e_ij = {a}^{\mathbf{T}}_{i}{b}_{j} \\ + \\begin{array}{ll} \\\\ + e_ij = {a}^{\\mathbf{T}}_{i}{b}_{j} \\\\ a_i = b_j = - \end{array} + \\end{array} """ diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 4c3ac122..59efbd53 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -144,18 +144,19 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label class ConditionalRandomField(nn.Module): + """条件随机场。 + 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 + + :param int num_tags: 标签的数量 + :param bool include_start_end_trans: 是否考虑各个tag作为开始以及结尾的分数。 + :param List[Tuple[from_tag_id(int), to_tag_id(int)]] allowed_transitions: 内部的Tuple[from_tag_id(int), + to_tag_id(int)]视为允许发生的跃迁,其他没有包含的跃迁认为是禁止跃迁,可以通过 + allowed_transitions()函数得到;如果为None,则所有跃迁均为合法 + :param str initial_method: 初始化方法。见initial_parameter + """ def __init__(self, num_tags, include_start_end_trans=False, allowed_transitions=None, initial_method=None): - """条件随机场。 - 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 - - :param int num_tags: 标签的数量 - :param bool include_start_end_trans: 是否考虑各个tag作为开始以及结尾的分数。 - :param List[Tuple[from_tag_id(int), to_tag_id(int)]] allowed_transitions: 内部的Tuple[from_tag_id(int), - to_tag_id(int)]视为允许发生的跃迁,其他没有包含的跃迁认为是禁止跃迁,可以通过 - allowed_transitions()函数得到;如果为None,则所有跃迁均为合法 - :param str initial_method: 初始化方法。见initial_parameter - """ + super(ConditionalRandomField, self).__init__() self.include_start_end_trans = include_start_end_trans diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index a72b7cd0..7e247c70 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,2 +1,3 @@ +__all__ = ["MLP", "ConditionalRandomField"] from .CRF import ConditionalRandomField from .MLP import MLP From 584f3a615f8277cf9d483d6f00abdd15d51db21a Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 6 May 2019 15:16:45 +0800 Subject: [PATCH 085/173] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=96=87=E4=BB=B6=E5=92=8C=E8=8B=A5=E5=B9=B2=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/user/task1.rst | 3 + tutorials/fastnlp_10min_tutorial.ipynb | 113 +- tutorials/fastnlp_1min_tutorial.ipynb | 1615 +++++++++++++++++++++++- tutorials/fastnlp_test_tutorial.ipynb | 2 +- 4 files changed, 1626 insertions(+), 107 deletions(-) create mode 100644 docs/source/user/task1.rst diff --git a/docs/source/user/task1.rst b/docs/source/user/task1.rst new file mode 100644 index 00000000..0c346999 --- /dev/null +++ b/docs/source/user/task1.rst @@ -0,0 +1,3 @@ +===================== +用 fastNLP 分类 +===================== \ No newline at end of file diff --git a/tutorials/fastnlp_10min_tutorial.ipynb b/tutorials/fastnlp_10min_tutorial.ipynb index 534c4e49..526fd49f 100644 --- a/tutorials/fastnlp_10min_tutorial.ipynb +++ b/tutorials/fastnlp_10min_tutorial.ipynb @@ -41,7 +41,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": {}, "outputs": [ { @@ -107,7 +107,7 @@ "'label': 0 type=str}" ] }, - "execution_count": 8, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -128,7 +128,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -148,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -168,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 6, "metadata": {}, "outputs": [ { @@ -191,7 +191,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -221,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -263,7 +263,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -295,17 +295,17 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{'raw_sentence': a welcome relief from baseball movies that try too hard to be mythic , this one is a sweet and modest and ultimately winning story . type=str,\n", - "'label': 3 type=int,\n", - "'words': [4, 1, 1, 18, 1, 1, 13, 1, 1, 1, 8, 26, 1, 5, 35, 1, 11, 4, 1, 10, 1, 10, 1, 1, 1, 2] type=list,\n", - "'seq_len': 26 type=int}\n" + "{'raw_sentence': the performances are an absolute joy . type=str,\n", + "'label': 4 type=int,\n", + "'words': [3, 1, 1, 26, 1, 1, 2] type=list,\n", + "'seq_len': 7 type=int}\n" ] } ], @@ -327,9 +327,21 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "batch_x has: {'words': tensor([[ 15, 72, 15, 73, 74, 7, 3, 75, 6, 3, 16, 16,\n", + " 76, 2],\n", + " [ 15, 72, 15, 73, 74, 7, 3, 75, 6, 3, 16, 16,\n", + " 76, 2]])}\n", + "batch_y has: {'label': tensor([ 1, 1])}\n" + ] + } + ], "source": [ "# 如果你们需要做强化学习或者GAN之类的项目,你们也可以使用这些数据预处理的工具\n", "from fastNLP.core.batch import Batch\n", @@ -352,7 +364,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -360,7 +372,7 @@ "text/plain": [ "CNNText(\n", " (embed): Embedding(\n", - " (embed): Embedding(59, 50, padding_idx=0)\n", + " 77, 50\n", " (dropout): Dropout(p=0.0)\n", " )\n", " (conv_pool): ConvMaxpool(\n", @@ -377,14 +389,14 @@ ")" ] }, - "execution_count": 17, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from fastNLP.models import CNNText\n", - "model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1)\n", + "model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1)\n", "model" ] }, @@ -448,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -485,7 +497,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": {}, "outputs": [], "source": [ @@ -508,7 +520,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -517,7 +529,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -525,48 +537,25 @@ "output_type": "stream", "text": [ "input fields after batch(if batch size is 2):\n", - "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 26]) \n", + "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 11]) \n", "target fields after batch(if batch size is 2):\n", "\tlabel_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", - "\n", - "training epochs started 2019-01-12 17-07-51\n" + "\n" ] }, { - "data": { - "text/plain": [ - "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=10), HTML(value='')), layout=Layout(display='…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Evaluation at Epoch 1/5. Step:2/10. AccuracyMetric: acc=0.425926\n", - "Evaluation at Epoch 2/5. Step:4/10. AccuracyMetric: acc=0.425926\n", - "Evaluation at Epoch 3/5. Step:6/10. AccuracyMetric: acc=0.611111\n", - "Evaluation at Epoch 4/5. Step:8/10. AccuracyMetric: acc=0.648148\n", - "Evaluation at Epoch 5/5. Step:10/10. AccuracyMetric: acc=0.703704\n", - "\n", - "In Epoch:5/Step:10, got best dev performance:AccuracyMetric: acc=0.703704\n", - "Reloaded the best model.\n" + "ename": "NameError", + "evalue": "\nProblems occurred when calling CNNText.forward(self, words, seq_len=None)\n\tmissing param: ['words']\n\tunused field: ['word_seq']\n\tSuggestion: You need to provide ['words'] in DataSet and set it as input. ", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0msave_path\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mbatch_size\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m32\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m n_epochs=5)\n\u001b[0m\u001b[1;32m 10\u001b[0m \u001b[0moverfit_trainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, train_data, model, optimizer, loss, batch_size, sampler, update_every, n_epochs, print_every, dev_data, metrics, metric_key, validate_every, save_path, prefetch, use_tqdm, device, callbacks, check_code_level)\u001b[0m\n\u001b[1;32m 447\u001b[0m _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data,\n\u001b[1;32m 448\u001b[0m \u001b[0mmetric_key\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmetric_key\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcheck_level\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_code_level\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 449\u001b[0;31m batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE))\n\u001b[0m\u001b[1;32m 450\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 451\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtrain_data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_check_code\u001b[0;34m(dataset, model, losser, metrics, batch_size, dev_data, metric_key, check_level)\u001b[0m\n\u001b[1;32m 808\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minfo_str\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 809\u001b[0m _check_forward_error(forward_func=model.forward, dataset=dataset,\n\u001b[0;32m--> 810\u001b[0;31m batch_x=batch_x, check_level=check_level)\n\u001b[0m\u001b[1;32m 811\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 812\u001b[0m \u001b[0mrefined_batch_x\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/utils.py\u001b[0m in \u001b[0;36m_check_forward_error\u001b[0;34m(forward_func, batch_x, dataset, check_level)\u001b[0m\n\u001b[1;32m 594\u001b[0m \u001b[0msugg_str\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0msuggestions\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 595\u001b[0m \u001b[0merr_str\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'\\n'\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m'\\n'\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merrs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m'\\n\\tSuggestion: '\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msugg_str\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 596\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mNameError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merr_str\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 597\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m_unused\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 598\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcheck_level\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mWARNING_CHECK_LEVEL\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mNameError\u001b[0m: \nProblems occurred when calling CNNText.forward(self, words, seq_len=None)\n\tmissing param: ['words']\n\tunused field: ['word_seq']\n\tSuggestion: You need to provide ['words'] in DataSet and set it as input. " ] - }, - { - "data": { - "text/plain": [ - "{'best_eval': {'AccuracyMetric': {'acc': 0.703704}},\n", - " 'best_epoch': 5,\n", - " 'best_step': 10,\n", - " 'seconds': 0.62}" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" } ], "source": [ diff --git a/tutorials/fastnlp_1min_tutorial.ipynb b/tutorials/fastnlp_1min_tutorial.ipynb index 7a35d992..64d57bc4 100644 --- a/tutorials/fastnlp_1min_tutorial.ipynb +++ b/tutorials/fastnlp_1min_tutorial.ipynb @@ -21,22 +21,10 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "c:\\users\\zyfeng\\miniconda3\\envs\\fastnlp\\lib\\site-packages\\tqdm\\autonotebook\\__init__.py:14: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", - " \" (e.g. in jupyter console)\", TqdmExperimentalWarning)\n" - ] - } - ], + "outputs": [], "source": [ - "import sys\n", - "sys.path.append(\"../\")\n", - "\n", "from fastNLP import DataSet\n", - "\n", + " \n", "data_path = \"./sample_data/tutorial_sample_dataset.csv\"\n", "ds = DataSet.read_csv(data_path, headers=('raw_sentence', 'label'), sep='\\t')" ] @@ -77,7 +65,1370 @@ "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[['a',\n", + " 'series',\n", + " 'of',\n", + " 'escapades',\n", + " 'demonstrating',\n", + " 'the',\n", + " 'adage',\n", + " 'that',\n", + " 'what',\n", + " 'is',\n", + " 'good',\n", + " 'for',\n", + " 'the',\n", + " 'goose',\n", + " 'is',\n", + " 'also',\n", + " 'good',\n", + " 'for',\n", + " 'the',\n", + " 'gander',\n", + " ',',\n", + " 'some',\n", + " 'of',\n", + " 'which',\n", + " 'occasionally',\n", + " 'amuses',\n", + " 'but',\n", + " 'none',\n", + " 'of',\n", + " 'which',\n", + " 'amounts',\n", + " 'to',\n", + " 'much',\n", + " 'of',\n", + " 'a',\n", + " 'story',\n", + " '.'],\n", + " ['this',\n", + " 'quiet',\n", + " ',',\n", + " 'introspective',\n", + " 'and',\n", + " 'entertaining',\n", + " 'independent',\n", + " 'is',\n", + " 'worth',\n", + " 'seeking',\n", + " '.'],\n", + " ['even',\n", + " 'fans',\n", + " 'of',\n", + " 'ismail',\n", + " 'merchant',\n", + " \"'s\",\n", + " 'work',\n", + " ',',\n", + " 'i',\n", + " 'suspect',\n", + " ',',\n", + " 'would',\n", + " 'have',\n", + " 'a',\n", + " 'hard',\n", + " 'time',\n", + " 'sitting',\n", + " 'through',\n", + " 'this',\n", + " 'one',\n", + " '.'],\n", + " ['a',\n", + " 'positively',\n", + " 'thrilling',\n", + " 'combination',\n", + " 'of',\n", + " 'ethnography',\n", + " 'and',\n", + " 'all',\n", + " 'the',\n", + " 'intrigue',\n", + " ',',\n", + " 'betrayal',\n", + " ',',\n", + " 'deceit',\n", + " 'and',\n", + " 'murder',\n", + " 'of',\n", + " 'a',\n", + " 'shakespearean',\n", + " 'tragedy',\n", + " 'or',\n", + " 'a',\n", + " 'juicy',\n", + " 'soap',\n", + " 'opera',\n", + " '.'],\n", + " ['aggressive',\n", + " 'self-glorification',\n", + " 'and',\n", + " 'a',\n", + " 'manipulative',\n", + " 'whitewash',\n", + " '.'],\n", + " ['a',\n", + " 'comedy-drama',\n", + " 'of',\n", + " 'nearly',\n", + " 'epic',\n", + " 'proportions',\n", + " 'rooted',\n", + " 'in',\n", + " 'a',\n", + " 'sincere',\n", + " 'performance',\n", + " 'by',\n", + " 'the',\n", + " 'title',\n", + " 'character',\n", + " 'undergoing',\n", + " 'midlife',\n", + " 'crisis',\n", + " '.'],\n", + " ['narratively',\n", + " ',',\n", + " 'trouble',\n", + " 'every',\n", + " 'day',\n", + " 'is',\n", + " 'a',\n", + " 'plodding',\n", + " 'mess',\n", + " '.'],\n", + " ['the',\n", + " 'importance',\n", + " 'of',\n", + " 'being',\n", + " 'earnest',\n", + " ',',\n", + " 'so',\n", + " 'thick',\n", + " 'with',\n", + " 'wit',\n", + " 'it',\n", + " 'plays',\n", + " 'like',\n", + " 'a',\n", + " 'reading',\n", + " 'from',\n", + " 'bartlett',\n", + " \"'s\",\n", + " 'familiar',\n", + " 'quotations'],\n", + " ['but', 'it', 'does', \"n't\", 'leave', 'you', 'with', 'much', '.'],\n", + " ['you', 'could', 'hate', 'it', 'for', 'the', 'same', 'reason', '.'],\n", + " ['there',\n", + " \"'s\",\n", + " 'little',\n", + " 'to',\n", + " 'recommend',\n", + " 'snow',\n", + " 'dogs',\n", + " ',',\n", + " 'unless',\n", + " 'one',\n", + " 'considers',\n", + " 'cliched',\n", + " 'dialogue',\n", + " 'and',\n", + " 'perverse',\n", + " 'escapism',\n", + " 'a',\n", + " 'source',\n", + " 'of',\n", + " 'high',\n", + " 'hilarity',\n", + " '.'],\n", + " ['kung',\n", + " 'pow',\n", + " 'is',\n", + " 'oedekerk',\n", + " \"'s\",\n", + " 'realization',\n", + " 'of',\n", + " 'his',\n", + " 'childhood',\n", + " 'dream',\n", + " 'to',\n", + " 'be',\n", + " 'in',\n", + " 'a',\n", + " 'martial-arts',\n", + " 'flick',\n", + " ',',\n", + " 'and',\n", + " 'proves',\n", + " 'that',\n", + " 'sometimes',\n", + " 'the',\n", + " 'dreams',\n", + " 'of',\n", + " 'youth',\n", + " 'should',\n", + " 'remain',\n", + " 'just',\n", + " 'that',\n", + " '.'],\n", + " ['the', 'performances', 'are', 'an', 'absolute', 'joy', '.'],\n", + " ['fresnadillo',\n", + " 'has',\n", + " 'something',\n", + " 'serious',\n", + " 'to',\n", + " 'say',\n", + " 'about',\n", + " 'the',\n", + " 'ways',\n", + " 'in',\n", + " 'which',\n", + " 'extravagant',\n", + " 'chance',\n", + " 'can',\n", + " 'distort',\n", + " 'our',\n", + " 'perspective',\n", + " 'and',\n", + " 'throw',\n", + " 'us',\n", + " 'off',\n", + " 'the',\n", + " 'path',\n", + " 'of',\n", + " 'good',\n", + " 'sense',\n", + " '.'],\n", + " ['i',\n", + " 'still',\n", + " 'like',\n", + " 'moonlight',\n", + " 'mile',\n", + " ',',\n", + " 'better',\n", + " 'judgment',\n", + " 'be',\n", + " 'damned',\n", + " '.'],\n", + " ['a',\n", + " 'welcome',\n", + " 'relief',\n", + " 'from',\n", + " 'baseball',\n", + " 'movies',\n", + " 'that',\n", + " 'try',\n", + " 'too',\n", + " 'hard',\n", + " 'to',\n", + " 'be',\n", + " 'mythic',\n", + " ',',\n", + " 'this',\n", + " 'one',\n", + " 'is',\n", + " 'a',\n", + " 'sweet',\n", + " 'and',\n", + " 'modest',\n", + " 'and',\n", + " 'ultimately',\n", + " 'winning',\n", + " 'story',\n", + " '.'],\n", + " ['a',\n", + " 'bilingual',\n", + " 'charmer',\n", + " ',',\n", + " 'just',\n", + " 'like',\n", + " 'the',\n", + " 'woman',\n", + " 'who',\n", + " 'inspired',\n", + " 'it'],\n", + " ['like',\n", + " 'a',\n", + " 'less',\n", + " 'dizzily',\n", + " 'gorgeous',\n", + " 'companion',\n", + " 'to',\n", + " 'mr.',\n", + " 'wong',\n", + " \"'s\",\n", + " 'in',\n", + " 'the',\n", + " 'mood',\n", + " 'for',\n", + " 'love',\n", + " '--',\n", + " 'very',\n", + " 'much',\n", + " 'a',\n", + " 'hong',\n", + " 'kong',\n", + " 'movie',\n", + " 'despite',\n", + " 'its',\n", + " 'mainland',\n", + " 'setting',\n", + " '.'],\n", + " ['as',\n", + " 'inept',\n", + " 'as',\n", + " 'big-screen',\n", + " 'remakes',\n", + " 'of',\n", + " 'the',\n", + " 'avengers',\n", + " 'and',\n", + " 'the',\n", + " 'wild',\n", + " 'wild',\n", + " 'west',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'everything',\n", + " 'you',\n", + " \"'d\",\n", + " 'expect',\n", + " '--',\n", + " 'but',\n", + " 'nothing',\n", + " 'more',\n", + " '.'],\n", + " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", + " ['hatfield',\n", + " 'and',\n", + " 'hicks',\n", + " 'make',\n", + " 'the',\n", + " 'oddest',\n", + " 'of',\n", + " 'couples',\n", + " ',',\n", + " 'and',\n", + " 'in',\n", + " 'this',\n", + " 'sense',\n", + " 'the',\n", + " 'movie',\n", + " 'becomes',\n", + " 'a',\n", + " 'study',\n", + " 'of',\n", + " 'the',\n", + " 'gambles',\n", + " 'of',\n", + " 'the',\n", + " 'publishing',\n", + " 'world',\n", + " ',',\n", + " 'offering',\n", + " 'a',\n", + " 'case',\n", + " 'study',\n", + " 'that',\n", + " 'exists',\n", + " 'apart',\n", + " 'from',\n", + " 'all',\n", + " 'the',\n", + " 'movie',\n", + " \"'s\",\n", + " 'political',\n", + " 'ramifications',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'like',\n", + " 'going',\n", + " 'to',\n", + " 'a',\n", + " 'house',\n", + " 'party',\n", + " 'and',\n", + " 'watching',\n", + " 'the',\n", + " 'host',\n", + " 'defend',\n", + " 'himself',\n", + " 'against',\n", + " 'a',\n", + " 'frothing',\n", + " 'ex-girlfriend',\n", + " '.'],\n", + " ['that',\n", + " 'the',\n", + " 'chuck',\n", + " 'norris',\n", + " '``',\n", + " 'grenade',\n", + " 'gag',\n", + " \"''\",\n", + " 'occurs',\n", + " 'about',\n", + " '7',\n", + " 'times',\n", + " 'during',\n", + " 'windtalkers',\n", + " 'is',\n", + " 'a',\n", + " 'good',\n", + " 'indication',\n", + " 'of',\n", + " 'how',\n", + " 'serious-minded',\n", + " 'the',\n", + " 'film',\n", + " 'is',\n", + " '.'],\n", + " ['the',\n", + " 'plot',\n", + " 'is',\n", + " 'romantic',\n", + " 'comedy',\n", + " 'boilerplate',\n", + " 'from',\n", + " 'start',\n", + " 'to',\n", + " 'finish',\n", + " '.'],\n", + " ['it',\n", + " 'arrives',\n", + " 'with',\n", + " 'an',\n", + " 'impeccable',\n", + " 'pedigree',\n", + " ',',\n", + " 'mongrel',\n", + " 'pep',\n", + " ',',\n", + " 'and',\n", + " 'almost',\n", + " 'indecipherable',\n", + " 'plot',\n", + " 'complications',\n", + " '.'],\n", + " ['a',\n", + " 'film',\n", + " 'that',\n", + " 'clearly',\n", + " 'means',\n", + " 'to',\n", + " 'preach',\n", + " 'exclusively',\n", + " 'to',\n", + " 'the',\n", + " 'converted',\n", + " '.'],\n", + " ['while',\n", + " 'the',\n", + " 'importance',\n", + " 'of',\n", + " 'being',\n", + " 'earnest',\n", + " 'offers',\n", + " 'opportunities',\n", + " 'for',\n", + " 'occasional',\n", + " 'smiles',\n", + " 'and',\n", + " 'chuckles',\n", + " ',',\n", + " 'it',\n", + " 'does',\n", + " \"n't\",\n", + " 'give',\n", + " 'us',\n", + " 'a',\n", + " 'reason',\n", + " 'to',\n", + " 'be',\n", + " 'in',\n", + " 'the',\n", + " 'theater',\n", + " 'beyond',\n", + " 'wilde',\n", + " \"'s\",\n", + " 'wit',\n", + " 'and',\n", + " 'the',\n", + " 'actors',\n", + " \"'\",\n", + " 'performances',\n", + " '.'],\n", + " ['the',\n", + " 'latest',\n", + " 'vapid',\n", + " 'actor',\n", + " \"'s\",\n", + " 'exercise',\n", + " 'to',\n", + " 'appropriate',\n", + " 'the',\n", + " 'structure',\n", + " 'of',\n", + " 'arthur',\n", + " 'schnitzler',\n", + " \"'s\",\n", + " 'reigen',\n", + " '.'],\n", + " ['more',\n", + " 'vaudeville',\n", + " 'show',\n", + " 'than',\n", + " 'well-constructed',\n", + " 'narrative',\n", + " ',',\n", + " 'but',\n", + " 'on',\n", + " 'those',\n", + " 'terms',\n", + " 'it',\n", + " \"'s\",\n", + " 'inoffensive',\n", + " 'and',\n", + " 'actually',\n", + " 'rather',\n", + " 'sweet',\n", + " '.'],\n", + " ['nothing', 'more', 'than', 'a', 'run-of-the-mill', 'action', 'flick', '.'],\n", + " ['hampered',\n", + " '--',\n", + " 'no',\n", + " ',',\n", + " 'paralyzed',\n", + " '--',\n", + " 'by',\n", + " 'a',\n", + " 'self-indulgent',\n", + " 'script',\n", + " '...',\n", + " 'that',\n", + " 'aims',\n", + " 'for',\n", + " 'poetry',\n", + " 'and',\n", + " 'ends',\n", + " 'up',\n", + " 'sounding',\n", + " 'like',\n", + " 'satire',\n", + " '.'],\n", + " ['ice',\n", + " 'age',\n", + " 'is',\n", + " 'the',\n", + " 'first',\n", + " 'computer-generated',\n", + " 'feature',\n", + " 'cartoon',\n", + " 'to',\n", + " 'feel',\n", + " 'like',\n", + " 'other',\n", + " 'movies',\n", + " ',',\n", + " 'and',\n", + " 'that',\n", + " 'makes',\n", + " 'for',\n", + " 'some',\n", + " 'glacial',\n", + " 'pacing',\n", + " 'early',\n", + " 'on',\n", + " '.'],\n", + " ['there',\n", + " \"'s\",\n", + " 'very',\n", + " 'little',\n", + " 'sense',\n", + " 'to',\n", + " 'what',\n", + " \"'s\",\n", + " 'going',\n", + " 'on',\n", + " 'here',\n", + " ',',\n", + " 'but',\n", + " 'the',\n", + " 'makers',\n", + " 'serve',\n", + " 'up',\n", + " 'the',\n", + " 'cliches',\n", + " 'with',\n", + " 'considerable',\n", + " 'dash',\n", + " '.'],\n", + " ['cattaneo',\n", + " 'should',\n", + " 'have',\n", + " 'followed',\n", + " 'the',\n", + " 'runaway',\n", + " 'success',\n", + " 'of',\n", + " 'his',\n", + " 'first',\n", + " 'film',\n", + " ',',\n", + " 'the',\n", + " 'full',\n", + " 'monty',\n", + " ',',\n", + " 'with',\n", + " 'something',\n", + " 'different',\n", + " '.'],\n", + " ['they',\n", + " \"'re\",\n", + " 'the',\n", + " 'unnamed',\n", + " ',',\n", + " 'easily',\n", + " 'substitutable',\n", + " 'forces',\n", + " 'that',\n", + " 'serve',\n", + " 'as',\n", + " 'whatever',\n", + " 'terror',\n", + " 'the',\n", + " 'heroes',\n", + " 'of',\n", + " 'horror',\n", + " 'movies',\n", + " 'try',\n", + " 'to',\n", + " 'avoid',\n", + " '.'],\n", + " ['it',\n", + " 'almost',\n", + " 'feels',\n", + " 'as',\n", + " 'if',\n", + " 'the',\n", + " 'movie',\n", + " 'is',\n", + " 'more',\n", + " 'interested',\n", + " 'in',\n", + " 'entertaining',\n", + " 'itself',\n", + " 'than',\n", + " 'in',\n", + " 'amusing',\n", + " 'us',\n", + " '.'],\n", + " ['the',\n", + " 'movie',\n", + " \"'s\",\n", + " 'progression',\n", + " 'into',\n", + " 'rambling',\n", + " 'incoherence',\n", + " 'gives',\n", + " 'new',\n", + " 'meaning',\n", + " 'to',\n", + " 'the',\n", + " 'phrase',\n", + " '`',\n", + " 'fatal',\n", + " 'script',\n", + " 'error',\n", + " '.',\n", + " \"'\"],\n", + " ['i',\n", + " 'still',\n", + " 'like',\n", + " 'moonlight',\n", + " 'mile',\n", + " ',',\n", + " 'better',\n", + " 'judgment',\n", + " 'be',\n", + " 'damned',\n", + " '.'],\n", + " ['a',\n", + " 'welcome',\n", + " 'relief',\n", + " 'from',\n", + " 'baseball',\n", + " 'movies',\n", + " 'that',\n", + " 'try',\n", + " 'too',\n", + " 'hard',\n", + " 'to',\n", + " 'be',\n", + " 'mythic',\n", + " ',',\n", + " 'this',\n", + " 'one',\n", + " 'is',\n", + " 'a',\n", + " 'sweet',\n", + " 'and',\n", + " 'modest',\n", + " 'and',\n", + " 'ultimately',\n", + " 'winning',\n", + " 'story',\n", + " '.'],\n", + " ['a',\n", + " 'bilingual',\n", + " 'charmer',\n", + " ',',\n", + " 'just',\n", + " 'like',\n", + " 'the',\n", + " 'woman',\n", + " 'who',\n", + " 'inspired',\n", + " 'it'],\n", + " ['like',\n", + " 'a',\n", + " 'less',\n", + " 'dizzily',\n", + " 'gorgeous',\n", + " 'companion',\n", + " 'to',\n", + " 'mr.',\n", + " 'wong',\n", + " \"'s\",\n", + " 'in',\n", + " 'the',\n", + " 'mood',\n", + " 'for',\n", + " 'love',\n", + " '--',\n", + " 'very',\n", + " 'much',\n", + " 'a',\n", + " 'hong',\n", + " 'kong',\n", + " 'movie',\n", + " 'despite',\n", + " 'its',\n", + " 'mainland',\n", + " 'setting',\n", + " '.'],\n", + " ['as',\n", + " 'inept',\n", + " 'as',\n", + " 'big-screen',\n", + " 'remakes',\n", + " 'of',\n", + " 'the',\n", + " 'avengers',\n", + " 'and',\n", + " 'the',\n", + " 'wild',\n", + " 'wild',\n", + " 'west',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'everything',\n", + " 'you',\n", + " \"'d\",\n", + " 'expect',\n", + " '--',\n", + " 'but',\n", + " 'nothing',\n", + " 'more',\n", + " '.'],\n", + " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", + " ['hatfield',\n", + " 'and',\n", + " 'hicks',\n", + " 'make',\n", + " 'the',\n", + " 'oddest',\n", + " 'of',\n", + " 'couples',\n", + " ',',\n", + " 'and',\n", + " 'in',\n", + " 'this',\n", + " 'sense',\n", + " 'the',\n", + " 'movie',\n", + " 'becomes',\n", + " 'a',\n", + " 'study',\n", + " 'of',\n", + " 'the',\n", + " 'gambles',\n", + " 'of',\n", + " 'the',\n", + " 'publishing',\n", + " 'world',\n", + " ',',\n", + " 'offering',\n", + " 'a',\n", + " 'case',\n", + " 'study',\n", + " 'that',\n", + " 'exists',\n", + " 'apart',\n", + " 'from',\n", + " 'all',\n", + " 'the',\n", + " 'movie',\n", + " \"'s\",\n", + " 'political',\n", + " 'ramifications',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'like',\n", + " 'going',\n", + " 'to',\n", + " 'a',\n", + " 'house',\n", + " 'party',\n", + " 'and',\n", + " 'watching',\n", + " 'the',\n", + " 'host',\n", + " 'defend',\n", + " 'himself',\n", + " 'against',\n", + " 'a',\n", + " 'frothing',\n", + " 'ex-girlfriend',\n", + " '.'],\n", + " ['that',\n", + " 'the',\n", + " 'chuck',\n", + " 'norris',\n", + " '``',\n", + " 'grenade',\n", + " 'gag',\n", + " \"''\",\n", + " 'occurs',\n", + " 'about',\n", + " '7',\n", + " 'times',\n", + " 'during',\n", + " 'windtalkers',\n", + " 'is',\n", + " 'a',\n", + " 'good',\n", + " 'indication',\n", + " 'of',\n", + " 'how',\n", + " 'serious-minded',\n", + " 'the',\n", + " 'film',\n", + " 'is',\n", + " '.'],\n", + " ['the',\n", + " 'plot',\n", + " 'is',\n", + " 'romantic',\n", + " 'comedy',\n", + " 'boilerplate',\n", + " 'from',\n", + " 'start',\n", + " 'to',\n", + " 'finish',\n", + " '.'],\n", + " ['it',\n", + " 'arrives',\n", + " 'with',\n", + " 'an',\n", + " 'impeccable',\n", + " 'pedigree',\n", + " ',',\n", + " 'mongrel',\n", + " 'pep',\n", + " ',',\n", + " 'and',\n", + " 'almost',\n", + " 'indecipherable',\n", + " 'plot',\n", + " 'complications',\n", + " '.'],\n", + " ['a',\n", + " 'film',\n", + " 'that',\n", + " 'clearly',\n", + " 'means',\n", + " 'to',\n", + " 'preach',\n", + " 'exclusively',\n", + " 'to',\n", + " 'the',\n", + " 'converted',\n", + " '.'],\n", + " ['i',\n", + " 'still',\n", + " 'like',\n", + " 'moonlight',\n", + " 'mile',\n", + " ',',\n", + " 'better',\n", + " 'judgment',\n", + " 'be',\n", + " 'damned',\n", + " '.'],\n", + " ['a',\n", + " 'welcome',\n", + " 'relief',\n", + " 'from',\n", + " 'baseball',\n", + " 'movies',\n", + " 'that',\n", + " 'try',\n", + " 'too',\n", + " 'hard',\n", + " 'to',\n", + " 'be',\n", + " 'mythic',\n", + " ',',\n", + " 'this',\n", + " 'one',\n", + " 'is',\n", + " 'a',\n", + " 'sweet',\n", + " 'and',\n", + " 'modest',\n", + " 'and',\n", + " 'ultimately',\n", + " 'winning',\n", + " 'story',\n", + " '.'],\n", + " ['a',\n", + " 'bilingual',\n", + " 'charmer',\n", + " ',',\n", + " 'just',\n", + " 'like',\n", + " 'the',\n", + " 'woman',\n", + " 'who',\n", + " 'inspired',\n", + " 'it'],\n", + " ['like',\n", + " 'a',\n", + " 'less',\n", + " 'dizzily',\n", + " 'gorgeous',\n", + " 'companion',\n", + " 'to',\n", + " 'mr.',\n", + " 'wong',\n", + " \"'s\",\n", + " 'in',\n", + " 'the',\n", + " 'mood',\n", + " 'for',\n", + " 'love',\n", + " '--',\n", + " 'very',\n", + " 'much',\n", + " 'a',\n", + " 'hong',\n", + " 'kong',\n", + " 'movie',\n", + " 'despite',\n", + " 'its',\n", + " 'mainland',\n", + " 'setting',\n", + " '.'],\n", + " ['as',\n", + " 'inept',\n", + " 'as',\n", + " 'big-screen',\n", + " 'remakes',\n", + " 'of',\n", + " 'the',\n", + " 'avengers',\n", + " 'and',\n", + " 'the',\n", + " 'wild',\n", + " 'wild',\n", + " 'west',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'everything',\n", + " 'you',\n", + " \"'d\",\n", + " 'expect',\n", + " '--',\n", + " 'but',\n", + " 'nothing',\n", + " 'more',\n", + " '.'],\n", + " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", + " ['hatfield',\n", + " 'and',\n", + " 'hicks',\n", + " 'make',\n", + " 'the',\n", + " 'oddest',\n", + " 'of',\n", + " 'couples',\n", + " ',',\n", + " 'and',\n", + " 'in',\n", + " 'this',\n", + " 'sense',\n", + " 'the',\n", + " 'movie',\n", + " 'becomes',\n", + " 'a',\n", + " 'study',\n", + " 'of',\n", + " 'the',\n", + " 'gambles',\n", + " 'of',\n", + " 'the',\n", + " 'publishing',\n", + " 'world',\n", + " ',',\n", + " 'offering',\n", + " 'a',\n", + " 'case',\n", + " 'study',\n", + " 'that',\n", + " 'exists',\n", + " 'apart',\n", + " 'from',\n", + " 'all',\n", + " 'the',\n", + " 'movie',\n", + " \"'s\",\n", + " 'political',\n", + " 'ramifications',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'like',\n", + " 'going',\n", + " 'to',\n", + " 'a',\n", + " 'house',\n", + " 'party',\n", + " 'and',\n", + " 'watching',\n", + " 'the',\n", + " 'host',\n", + " 'defend',\n", + " 'himself',\n", + " 'against',\n", + " 'a',\n", + " 'frothing',\n", + " 'ex-girlfriend',\n", + " '.'],\n", + " ['that',\n", + " 'the',\n", + " 'chuck',\n", + " 'norris',\n", + " '``',\n", + " 'grenade',\n", + " 'gag',\n", + " \"''\",\n", + " 'occurs',\n", + " 'about',\n", + " '7',\n", + " 'times',\n", + " 'during',\n", + " 'windtalkers',\n", + " 'is',\n", + " 'a',\n", + " 'good',\n", + " 'indication',\n", + " 'of',\n", + " 'how',\n", + " 'serious-minded',\n", + " 'the',\n", + " 'film',\n", + " 'is',\n", + " '.'],\n", + " ['the',\n", + " 'plot',\n", + " 'is',\n", + " 'romantic',\n", + " 'comedy',\n", + " 'boilerplate',\n", + " 'from',\n", + " 'start',\n", + " 'to',\n", + " 'finish',\n", + " '.'],\n", + " ['it',\n", + " 'arrives',\n", + " 'with',\n", + " 'an',\n", + " 'impeccable',\n", + " 'pedigree',\n", + " ',',\n", + " 'mongrel',\n", + " 'pep',\n", + " ',',\n", + " 'and',\n", + " 'almost',\n", + " 'indecipherable',\n", + " 'plot',\n", + " 'complications',\n", + " '.'],\n", + " ['a',\n", + " 'film',\n", + " 'that',\n", + " 'clearly',\n", + " 'means',\n", + " 'to',\n", + " 'preach',\n", + " 'exclusively',\n", + " 'to',\n", + " 'the',\n", + " 'converted',\n", + " '.'],\n", + " ['i',\n", + " 'still',\n", + " 'like',\n", + " 'moonlight',\n", + " 'mile',\n", + " ',',\n", + " 'better',\n", + " 'judgment',\n", + " 'be',\n", + " 'damned',\n", + " '.'],\n", + " ['a',\n", + " 'welcome',\n", + " 'relief',\n", + " 'from',\n", + " 'baseball',\n", + " 'movies',\n", + " 'that',\n", + " 'try',\n", + " 'too',\n", + " 'hard',\n", + " 'to',\n", + " 'be',\n", + " 'mythic',\n", + " ',',\n", + " 'this',\n", + " 'one',\n", + " 'is',\n", + " 'a',\n", + " 'sweet',\n", + " 'and',\n", + " 'modest',\n", + " 'and',\n", + " 'ultimately',\n", + " 'winning',\n", + " 'story',\n", + " '.'],\n", + " ['a',\n", + " 'bilingual',\n", + " 'charmer',\n", + " ',',\n", + " 'just',\n", + " 'like',\n", + " 'the',\n", + " 'woman',\n", + " 'who',\n", + " 'inspired',\n", + " 'it'],\n", + " ['like',\n", + " 'a',\n", + " 'less',\n", + " 'dizzily',\n", + " 'gorgeous',\n", + " 'companion',\n", + " 'to',\n", + " 'mr.',\n", + " 'wong',\n", + " \"'s\",\n", + " 'in',\n", + " 'the',\n", + " 'mood',\n", + " 'for',\n", + " 'love',\n", + " '--',\n", + " 'very',\n", + " 'much',\n", + " 'a',\n", + " 'hong',\n", + " 'kong',\n", + " 'movie',\n", + " 'despite',\n", + " 'its',\n", + " 'mainland',\n", + " 'setting',\n", + " '.'],\n", + " ['as',\n", + " 'inept',\n", + " 'as',\n", + " 'big-screen',\n", + " 'remakes',\n", + " 'of',\n", + " 'the',\n", + " 'avengers',\n", + " 'and',\n", + " 'the',\n", + " 'wild',\n", + " 'wild',\n", + " 'west',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'everything',\n", + " 'you',\n", + " \"'d\",\n", + " 'expect',\n", + " '--',\n", + " 'but',\n", + " 'nothing',\n", + " 'more',\n", + " '.'],\n", + " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", + " ['hatfield',\n", + " 'and',\n", + " 'hicks',\n", + " 'make',\n", + " 'the',\n", + " 'oddest',\n", + " 'of',\n", + " 'couples',\n", + " ',',\n", + " 'and',\n", + " 'in',\n", + " 'this',\n", + " 'sense',\n", + " 'the',\n", + " 'movie',\n", + " 'becomes',\n", + " 'a',\n", + " 'study',\n", + " 'of',\n", + " 'the',\n", + " 'gambles',\n", + " 'of',\n", + " 'the',\n", + " 'publishing',\n", + " 'world',\n", + " ',',\n", + " 'offering',\n", + " 'a',\n", + " 'case',\n", + " 'study',\n", + " 'that',\n", + " 'exists',\n", + " 'apart',\n", + " 'from',\n", + " 'all',\n", + " 'the',\n", + " 'movie',\n", + " \"'s\",\n", + " 'political',\n", + " 'ramifications',\n", + " '.'],\n", + " ['it',\n", + " \"'s\",\n", + " 'like',\n", + " 'going',\n", + " 'to',\n", + " 'a',\n", + " 'house',\n", + " 'party',\n", + " 'and',\n", + " 'watching',\n", + " 'the',\n", + " 'host',\n", + " 'defend',\n", + " 'himself',\n", + " 'against',\n", + " 'a',\n", + " 'frothing',\n", + " 'ex-girlfriend',\n", + " '.'],\n", + " ['that',\n", + " 'the',\n", + " 'chuck',\n", + " 'norris',\n", + " '``',\n", + " 'grenade',\n", + " 'gag',\n", + " \"''\",\n", + " 'occurs',\n", + " 'about',\n", + " '7',\n", + " 'times',\n", + " 'during',\n", + " 'windtalkers',\n", + " 'is',\n", + " 'a',\n", + " 'good',\n", + " 'indication',\n", + " 'of',\n", + " 'how',\n", + " 'serious-minded',\n", + " 'the',\n", + " 'film',\n", + " 'is',\n", + " '.'],\n", + " ['the',\n", + " 'plot',\n", + " 'is',\n", + " 'romantic',\n", + " 'comedy',\n", + " 'boilerplate',\n", + " 'from',\n", + " 'start',\n", + " 'to',\n", + " 'finish',\n", + " '.'],\n", + " ['it',\n", + " 'arrives',\n", + " 'with',\n", + " 'an',\n", + " 'impeccable',\n", + " 'pedigree',\n", + " ',',\n", + " 'mongrel',\n", + " 'pep',\n", + " ',',\n", + " 'and',\n", + " 'almost',\n", + " 'indecipherable',\n", + " 'plot',\n", + " 'complications',\n", + " '.'],\n", + " ['a',\n", + " 'film',\n", + " 'that',\n", + " 'clearly',\n", + " 'means',\n", + " 'to',\n", + " 'preach',\n", + " 'exclusively',\n", + " 'to',\n", + " 'the',\n", + " 'converted',\n", + " '.']]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# 将所有数字转为小写\n", "ds.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence')\n", @@ -114,7 +1465,192 @@ "cell_type": "code", "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "[[120, 121, 6, 2, 122, 5, 72, 123, 3],\n", + " [14,\n", + " 4,\n", + " 152,\n", + " 153,\n", + " 154,\n", + " 155,\n", + " 8,\n", + " 156,\n", + " 157,\n", + " 9,\n", + " 16,\n", + " 2,\n", + " 158,\n", + " 21,\n", + " 159,\n", + " 30,\n", + " 98,\n", + " 57,\n", + " 4,\n", + " 160,\n", + " 161,\n", + " 13,\n", + " 162,\n", + " 163,\n", + " 164,\n", + " 165,\n", + " 3],\n", + " [4,\n", + " 112,\n", + " 113,\n", + " 15,\n", + " 114,\n", + " 35,\n", + " 10,\n", + " 68,\n", + " 115,\n", + " 69,\n", + " 8,\n", + " 23,\n", + " 116,\n", + " 5,\n", + " 18,\n", + " 36,\n", + " 11,\n", + " 4,\n", + " 70,\n", + " 7,\n", + " 117,\n", + " 7,\n", + " 118,\n", + " 119,\n", + " 71,\n", + " 3],\n", + " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", + " [2, 27, 11, 139, 140, 141, 15, 142, 8, 143, 3],\n", + " [12, 9, 14, 32, 8, 4, 59, 60, 7, 61, 2, 62, 63, 64, 65, 4, 66, 67, 3],\n", + " [97, 145, 14, 146, 147, 5, 148, 149, 23, 150, 3],\n", + " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", + " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", + " [14,\n", + " 4,\n", + " 152,\n", + " 153,\n", + " 154,\n", + " 155,\n", + " 8,\n", + " 156,\n", + " 157,\n", + " 9,\n", + " 16,\n", + " 2,\n", + " 158,\n", + " 21,\n", + " 159,\n", + " 30,\n", + " 98,\n", + " 57,\n", + " 4,\n", + " 160,\n", + " 161,\n", + " 13,\n", + " 162,\n", + " 163,\n", + " 164,\n", + " 165,\n", + " 3],\n", + " [10,\n", + " 2,\n", + " 82,\n", + " 83,\n", + " 84,\n", + " 85,\n", + " 86,\n", + " 87,\n", + " 88,\n", + " 89,\n", + " 90,\n", + " 91,\n", + " 92,\n", + " 93,\n", + " 11,\n", + " 4,\n", + " 28,\n", + " 94,\n", + " 6,\n", + " 95,\n", + " 96,\n", + " 2,\n", + " 17,\n", + " 11,\n", + " 3],\n", + " [12, 73, 20, 33, 74, 75, 5, 76, 77, 5, 7, 78, 79, 27, 80, 3],\n", + " [12, 78, 1, 24, 1, 2, 13, 11, 31, 1, 16, 1, 1, 133, 16, 1, 1, 3],\n", + " [24, 107, 24, 108, 109, 6, 2, 110, 7, 2, 34, 34, 111, 3],\n", + " [2, 27, 11, 139, 140, 141, 15, 142, 8, 143, 3],\n", + " [24, 107, 24, 108, 109, 6, 2, 110, 7, 2, 34, 34, 111, 3],\n", + " [97, 145, 14, 146, 147, 5, 148, 149, 23, 150, 3],\n", + " [4,\n", + " 112,\n", + " 113,\n", + " 15,\n", + " 114,\n", + " 35,\n", + " 10,\n", + " 68,\n", + " 115,\n", + " 69,\n", + " 8,\n", + " 23,\n", + " 116,\n", + " 5,\n", + " 18,\n", + " 36,\n", + " 11,\n", + " 4,\n", + " 70,\n", + " 7,\n", + " 117,\n", + " 7,\n", + " 118,\n", + " 119,\n", + " 71,\n", + " 3],\n", + " [12, 9, 99, 29, 100, 101, 30, 22, 58, 31, 3],\n", + " [12, 9, 99, 29, 100, 101, 30, 22, 58, 31, 3],\n", + " [120, 121, 6, 2, 122, 5, 72, 123, 3],\n", + " [1, 30, 1, 5, 1, 30, 1, 4, 1, 1, 1, 10, 1, 21, 1, 7, 1, 1, 1, 14, 1, 3],\n", + " [1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 8,\n", + " 1,\n", + " 89,\n", + " 2,\n", + " 1,\n", + " 16,\n", + " 151,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 7,\n", + " 1,\n", + " 1,\n", + " 1,\n", + " 2,\n", + " 1,\n", + " 6,\n", + " 28,\n", + " 25,\n", + " 3]]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from fastNLP import Vocabulary\n", "vocab = Vocabulary(min_freq=2)\n", @@ -140,7 +1676,7 @@ "outputs": [], "source": [ "from fastNLP.models import CNNText\n", - "model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1)\n" + "model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n" ] }, { @@ -162,38 +1698,29 @@ "text": [ "input fields after batch(if batch size is 2):\n", "\twords: (1)type:numpy.ndarray (2)dtype:object, (3)shape:(2,) \n", - "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 25]) \n", + "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 11]) \n", "target fields after batch(if batch size is 2):\n", "\tlabel_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", - "\n", - "training epochs started 2019-01-12 17-00-48\n" + "\n" ] }, { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "23979df0f63e446fbb0406b919b91dd3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=6), HTML(value='')), layout=Layout(display='i…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Evaluation at Epoch 1/3. Step:2/6. AccuracyMetric: acc=0.173913\n", - "Evaluation at Epoch 2/3. Step:4/6. AccuracyMetric: acc=0.26087\n", - "Evaluation at Epoch 3/3. Step:6/6. AccuracyMetric: acc=0.304348\n", - "\n", - "In Epoch:3/Step:6, got best dev performance:AccuracyMetric: acc=0.304348\n", - "Reloaded the best model.\n", - "Train finished!\n" + "ename": "AttributeError", + "evalue": "'numpy.ndarray' object has no attribute 'contiguous'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdev_data\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdev_data\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mloss\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mCrossEntropyLoss\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mmetrics\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mAccuracyMetric\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m )\n\u001b[1;32m 8\u001b[0m \u001b[0mtrainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, train_data, model, optimizer, loss, batch_size, sampler, update_every, n_epochs, print_every, dev_data, metrics, metric_key, validate_every, save_path, prefetch, use_tqdm, device, callbacks, check_code_level)\u001b[0m\n\u001b[1;32m 447\u001b[0m _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data,\n\u001b[1;32m 448\u001b[0m \u001b[0mmetric_key\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmetric_key\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcheck_level\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_code_level\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 449\u001b[0;31m batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE))\n\u001b[0m\u001b[1;32m 450\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 451\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtrain_data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_check_code\u001b[0;34m(dataset, model, losser, metrics, batch_size, dev_data, metric_key, check_level)\u001b[0m\n\u001b[1;32m 811\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 812\u001b[0m \u001b[0mrefined_batch_x\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 813\u001b[0;31m \u001b[0mpred_dict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mrefined_batch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 814\u001b[0m \u001b[0mfunc_signature\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_get_func_signature\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 815\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpred_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/models/cnn_text_classification.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, words, seq_len)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[0;34m:\u001b[0m\u001b[0;32mreturn\u001b[0m \u001b[0moutput\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdict\u001b[0m \u001b[0mof\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mLongTensor\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mbatch_size\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnum_classes\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m \"\"\"\n\u001b[0;32m---> 60\u001b[0;31m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0membed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mwords\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# [N,L] -> [N,L,C]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 61\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconv_pool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# [N,L,C] -> [N,C]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdropout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/modules/encoder/embedding.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m 33\u001b[0m \u001b[0;34m:\u001b[0m\u001b[0;32mreturn\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTensor\u001b[0m \u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mbatch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0membed_dim\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 34\u001b[0m \"\"\"\n\u001b[0;32m---> 35\u001b[0;31m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 36\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdropout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/sparse.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, input)\u001b[0m\n\u001b[1;32m 106\u001b[0m return F.embedding(\n\u001b[1;32m 107\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mweight\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpadding_idx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax_norm\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 108\u001b[0;31m self.norm_type, self.scale_grad_by_freq, self.sparse)\n\u001b[0m\u001b[1;32m 109\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 110\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mextra_repr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/functional.py\u001b[0m in \u001b[0;36membedding\u001b[0;34m(input, weight, padding_idx, max_norm, norm_type, scale_grad_by_freq, sparse)\u001b[0m\n\u001b[1;32m 1062\u001b[0m [ 0.6262, 0.2438, 0.7471]]])\n\u001b[1;32m 1063\u001b[0m \"\"\"\n\u001b[0;32m-> 1064\u001b[0;31m \u001b[0minput\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcontiguous\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1065\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpadding_idx\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1066\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpadding_idx\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAttributeError\u001b[0m: 'numpy.ndarray' object has no attribute 'contiguous'" ] } ], diff --git a/tutorials/fastnlp_test_tutorial.ipynb b/tutorials/fastnlp_test_tutorial.ipynb index 9b0c1b2e..fb87606e 100644 --- a/tutorials/fastnlp_test_tutorial.ipynb +++ b/tutorials/fastnlp_test_tutorial.ipynb @@ -89,7 +89,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.4" + "version": "3.6.7" } }, "nbformat": 4, From 8039f4dd45f80f62c05ec1491f27d768ea441f20 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 6 May 2019 20:46:02 +0800 Subject: [PATCH 086/173] =?UTF-8?q?=E8=AE=A8=E8=AE=BA=E5=B9=B6=E6=95=B4?= =?UTF-8?q?=E5=90=88=E4=BA=86=E8=8B=A5=E5=B9=B2=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.io.config_io.rst | 7 - docs/source/fastNLP.io.file_reader.rst | 7 - docs/source/fastNLP.io.rst | 2 - docs/source/user/installation.rst | 3 +- fastNLP/io/__init__.py | 9 +- fastNLP/models/__init__.py | 1 - fastNLP/models/char_language_model.py | 138 ------------------ fastNLP/models/snli.py | 20 +-- fastNLP/modules/__init__.py | 2 +- fastNLP/modules/aggregator/__init__.py | 8 +- fastNLP/modules/aggregator/attention.py | 49 +++---- fastNLP/modules/aggregator/pooling.py | 23 +-- fastNLP/modules/decoder/__init__.py | 3 +- fastNLP/modules/decoder/utils.py | 2 +- fastNLP/modules/dropout.py | 2 +- fastNLP/modules/encoder/__init__.py | 2 - fastNLP/modules/encoder/char_encoder.py | 17 +-- fastNLP/modules/encoder/linear.py | 21 --- fastNLP/modules/encoder/lstm.py | 19 +-- fastNLP/modules/other_modules.py | 186 ------------------------ fastNLP/modules/utils.py | 14 +- requirements.txt | 1 - test/io/test_config_saver.py | 56 +++---- test/io/test_dataset_loader.py | 27 ++-- test/io/test_embed_loader.py | 12 +- 25 files changed, 114 insertions(+), 517 deletions(-) delete mode 100644 docs/source/fastNLP.io.config_io.rst delete mode 100644 docs/source/fastNLP.io.file_reader.rst delete mode 100644 fastNLP/models/char_language_model.py delete mode 100644 fastNLP/modules/encoder/linear.py delete mode 100644 fastNLP/modules/other_modules.py diff --git a/docs/source/fastNLP.io.config_io.rst b/docs/source/fastNLP.io.config_io.rst deleted file mode 100644 index c6bf4bae..00000000 --- a/docs/source/fastNLP.io.config_io.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.io.config\_io module -============================ - -.. automodule:: fastNLP.io.config_io - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.io.file_reader.rst b/docs/source/fastNLP.io.file_reader.rst deleted file mode 100644 index 233ee399..00000000 --- a/docs/source/fastNLP.io.file_reader.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.io.file\_reader module -============================== - -.. automodule:: fastNLP.io.file_reader - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index 5af9d538..610b1ce6 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -12,9 +12,7 @@ Submodules .. toctree:: fastNLP.io.base_loader - fastNLP.io.config_io fastNLP.io.dataset_loader fastNLP.io.embed_loader - fastNLP.io.file_reader fastNLP.io.model_io diff --git a/docs/source/user/installation.rst b/docs/source/user/installation.rst index e22c4202..c218b3e1 100644 --- a/docs/source/user/installation.rst +++ b/docs/source/user/installation.rst @@ -9,7 +9,6 @@ fastNLP 依赖如下包:: torch>=0.4.0 numpy - tensorboardX tqdm nltk @@ -18,4 +17,4 @@ fastNLP 依赖如下包:: .. code:: shell - >>> pip install fitlog + >>> pip install fastNLP diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index e71c84ec..b855a1bb 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -5,16 +5,13 @@ 2. 用于读入数据的 :doc:`DataSetLoader ` 类 -3. 用于读写config文件的类, 参考 :doc:`Config-IO ` - -4. 用于保存和载入模型的类, 参考 :doc:`Model-IO ` +3. 用于保存和载入模型的类, 参考 :doc:`Model-IO ` 这些类的使用方法可以在对应module的文档下查看. """ from .embed_loader import EmbedLoader from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ PeopleDailyCorpusLoader, Conll2003Loader -from .config_io import ConfigLoader, ConfigSection, ConfigSaver from .model_io import ModelLoader as ModelLoader, ModelSaver as ModelSaver __all__ = [ @@ -29,10 +26,6 @@ __all__ = [ 'PeopleDailyCorpusLoader', 'Conll2003Loader', - 'ConfigLoader', - 'ConfigSection', - 'ConfigSaver', - 'ModelLoader', 'ModelSaver', ] diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index 657f67ec..e8818492 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -5,7 +5,6 @@ TODO 详细介绍的表格,与主页相对应 """ from .base_model import BaseModel from .biaffine_parser import BiaffineParser, GraphParser -from .char_language_model import CharLM from .cnn_text_classification import CNNText from .sequence_modeling import SeqLabeling, AdvSeqLabel from .snli import ESIM diff --git a/fastNLP/models/char_language_model.py b/fastNLP/models/char_language_model.py deleted file mode 100644 index d0b4c426..00000000 --- a/fastNLP/models/char_language_model.py +++ /dev/null @@ -1,138 +0,0 @@ -import torch -import torch.nn as nn -import torch.nn.functional as F - -from ..modules.encoder.lstm import LSTM - - -class Highway(nn.Module): - """Highway network""" - - def __init__(self, input_size): - super(Highway, self).__init__() - self.fc1 = nn.Linear(input_size, input_size, bias=True) - self.fc2 = nn.Linear(input_size, input_size, bias=True) - - def forward(self, x): - t = F.sigmoid(self.fc1(x)) - return torch.mul(t, F.relu(self.fc2(x))) + torch.mul(1 - t, x) - - -class CharLM(nn.Module): - """CNN + highway network + LSTM - - # Input:: - - 4D tensor with shape [batch_size, in_channel, height, width] - - # Output:: - - 2D Tensor with shape [batch_size, vocab_size] - - # Arguments:: - - char_emb_dim: the size of each character's attention - word_emb_dim: the size of each word's attention - vocab_size: num of unique words - num_char: num of characters - use_gpu: True or False - - """ - - def __init__(self, char_emb_dim, word_emb_dim, - vocab_size, num_char): - super(CharLM, self).__init__() - self.char_emb_dim = char_emb_dim - self.word_emb_dim = word_emb_dim - self.vocab_size = vocab_size - - # char attention layer - self.char_embed = nn.Embedding(num_char, char_emb_dim) - - # convolutions of filters with different sizes - self.convolutions = [] - - # list of tuples: (the number of filter, width) - self.filter_num_width = [(25, 1), (50, 2), (75, 3), (100, 4), (125, 5), (150, 6)] - - for out_channel, filter_width in self.filter_num_width: - self.convolutions.append( - nn.Conv2d( - 1, # in_channel - out_channel, # out_channel - kernel_size=(char_emb_dim, filter_width), # (height, width) - bias=True - ) - ) - - self.highway_input_dim = sum([x for x, y in self.filter_num_width]) - - self.batch_norm = nn.BatchNorm1d(self.highway_input_dim, affine=False) - - # highway net - self.highway1 = Highway(self.highway_input_dim) - self.highway2 = Highway(self.highway_input_dim) - - # LSTM - self.lstm_num_layers = 2 - - self.lstm = LSTM(self.highway_input_dim, hidden_size=self.word_emb_dim, num_layers=self.lstm_num_layers, - dropout=0.5) - # output layer - self.dropout = nn.Dropout(p=0.5) - self.linear = nn.Linear(self.word_emb_dim, self.vocab_size) - - def forward(self, x): - # Input: Variable of Tensor with shape [num_seq, seq_len, max_word_len+2] - # Return: Variable of Tensor with shape [num_words, len(word_dict)] - lstm_batch_size = x.size()[0] - lstm_seq_len = x.size()[1] - - x = x.contiguous().view(-1, x.size()[2]) - # [num_seq*seq_len, max_word_len+2] - - x = self.char_embed(x) - # [num_seq*seq_len, max_word_len+2, char_emb_dim] - - x = torch.transpose(x.view(x.size()[0], 1, x.size()[1], -1), 2, 3) - # [num_seq*seq_len, 1, max_word_len+2, char_emb_dim] - - x = self.conv_layers(x) - # [num_seq*seq_len, total_num_filters] - - x = self.batch_norm(x) - # [num_seq*seq_len, total_num_filters] - - x = self.highway1(x) - x = self.highway2(x) - # [num_seq*seq_len, total_num_filters] - - x = x.contiguous().view(lstm_batch_size, lstm_seq_len, -1) - # [num_seq, seq_len, total_num_filters] - - x = self.lstm(x) - # [seq_len, num_seq, hidden_size] - - x = self.dropout(x) - # [seq_len, num_seq, hidden_size] - - x = x.contiguous().view(lstm_batch_size * lstm_seq_len, -1) - # [num_seq*seq_len, hidden_size] - - x = self.linear(x) - # [num_seq*seq_len, vocab_size] - return x - - def conv_layers(self, x): - chosen_list = list() - for conv in self.convolutions: - feature_map = F.tanh(conv(x)) - # (batch_size, out_channel, 1, max_word_len-width+1) - chosen = torch.max(feature_map, 3)[0] - # (batch_size, out_channel, 1) - chosen = chosen.squeeze() - # (batch_size, out_channel) - chosen_list.append(chosen) - - # (batch_size, total_num_filers) - return torch.cat(chosen_list, 1) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 6b54bee6..25b8a36e 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -12,19 +12,21 @@ my_inf = 10e12 class ESIM(BaseModel): - """ESIM模型的一个PyTorch实现。 + """ + ESIM模型的一个PyTorch实现。 + ESIM模型的论文: Enhanced LSTM for Natural Language Inference (arXiv: 1609.06038) + + :param int vocab_size: 词表大小 + :param int embed_dim: 词嵌入维度 + :param int hidden_size: LSTM隐层大小 + :param float dropout: dropout大小,默认为0 + :param int num_classes: 标签数目,默认为3 + :param numpy.array init_embedding: 初始词嵌入矩阵,形状为(vocab_size, embed_dim),默认为None,即随机初始化词嵌入矩阵 """ def __init__(self, vocab_size, embed_dim, hidden_size, dropout=0.0, num_classes=3, init_embedding=None): - """ - :param int vocab_size: 词表大小 - :param int embed_dim: 词嵌入维度 - :param int hidden_size: LSTM隐层大小 - :param float dropout: dropout大小,默认为0 - :param int num_classes: 标签数目,默认为3 - :param numpy.array init_embedding: 初始词嵌入矩阵,形状为(vocab_size, embed_dim),默认为None,即随机初始化词嵌入矩阵 - """ + super(ESIM, self).__init__() self.vocab_size = vocab_size self.embed_dim = embed_dim diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index 6ff356f1..f1e4c9e6 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -12,8 +12,8 @@ from . import decoder from . import encoder from .aggregator import * from .decoder import * -from .other_modules import * from .dropout import TimestepDropout from .encoder import * +from .utils import get_embeddings __version__ = '0.0.0' diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index dbf53662..c1e6cd17 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,11 +1,7 @@ -__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MeanPoolWithMask", "KMaxPool", "Attention", "BiAttention", - "SelfAttention"] +__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention"] from .pooling import MaxPool from .pooling import MaxPoolWithMask from .pooling import AvgPool from .pooling import MeanPoolWithMask -from .pooling import KMaxPool -from .attention import Attention -from .attention import BiAttention -from .attention import SelfAttention +from .attention import MultiHeadAttention diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index fb6f5dc5..b926fb12 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -1,3 +1,4 @@ +__all__ =["MultiHeadAttention"] import math import torch @@ -5,27 +6,14 @@ import torch.nn.functional as F from torch import nn from ..dropout import TimestepDropout -from ..utils import mask_softmax from ..utils import initial_parameter -class Attention(torch.nn.Module): - def __init__(self, normalize=False): - super(Attention, self).__init__() - self.normalize = normalize - - def forward(self, query, memory, mask): - similarities = self._atten_forward(query, memory) - if self.normalize: - return mask_softmax(similarities, mask) - return similarities - - def _atten_forward(self, query, memory): - raise NotImplementedError - - class DotAttention(nn.Module): + """ + TODO + """ def __init__(self, key_size, value_size, dropout=0.1): super(DotAttention, self).__init__() self.key_size = key_size @@ -51,15 +39,15 @@ class DotAttention(nn.Module): class MultiHeadAttention(nn.Module): - def __init__(self, input_size, key_size, value_size, num_head, dropout=0.1): - """ + """ - :param input_size: int, 输入维度的大小。同时也是输出维度的大小。 - :param key_size: int, 每个head的维度大小。 - :param value_size: int,每个head中value的维度。 - :param num_head: int,head的数量。 - :param dropout: float。 - """ + :param input_size: int, 输入维度的大小。同时也是输出维度的大小。 + :param key_size: int, 每个head的维度大小。 + :param value_size: int,每个head中value的维度。 + :param num_head: int,head的数量。 + :param dropout: float。 + """ + def __init__(self, input_size, key_size, value_size, num_head, dropout=0.1): super(MultiHeadAttention, self).__init__() self.input_size = input_size self.key_size = key_size @@ -112,16 +100,16 @@ class MultiHeadAttention(nn.Module): class BiAttention(nn.Module): - """Bi Attention module + r"""Bi Attention module Calculate Bi Attention matrix `e` .. math:: - \\begin{array}{ll} \\\\ - e_ij = {a}^{\\mathbf{T}}_{i}{b}_{j} \\\\ + \begin{array}{ll} \\ + e_ij = {a}^{\mathbf{T}}_{i}{b}_{j} \\ a_i = b_j = - \\end{array} + \end{array} """ @@ -171,8 +159,11 @@ class BiAttention(nn.Module): return out_x1, out_x2 + class SelfAttention(nn.Module): - """Self Attention Module. + """ + Self Attention Module. + :param int input_size: 输入tensor的hidden维度 :param int attention_unit: 输出tensor的hidden维度 :param int attention_hops: diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index fd4414b7..47050355 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -1,21 +1,23 @@ +__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool"] import torch import torch.nn as nn class MaxPool(nn.Module): - """Max-pooling模块。""" + """ + Max-pooling模块。 + :param stride: 窗口移动大小,默认为kernel_size + :param padding: padding的内容,默认为0 + :param dilation: 控制窗口内元素移动距离的大小 + :param dimension: MaxPool的维度,支持1,2,3维。 + :param kernel_size: max pooling的窗口大小,默认为tensor最后k维,其中k为dimension + :param return_indices: + :param ceil_mode: + """ def __init__(self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, return_indices=False, ceil_mode=False): - """ - :param stride: 窗口移动大小,默认为kernel_size - :param padding: padding的内容,默认为0 - :param dilation: 控制窗口内元素移动距离的大小 - :param dimension: MaxPool的维度,支持1,2,3维。 - :param kernel_size: max pooling的窗口大小,默认为tensor最后k维,其中k为dimension - :param return_indices: - :param ceil_mode: - """ + super(MaxPool, self).__init__() assert (1 <= dimension) and (dimension <= 3) self.dimension = dimension @@ -110,6 +112,7 @@ class AvgPool(nn.Module): class MeanPoolWithMask(nn.Module): + def __init__(self): super(MeanPoolWithMask, self).__init__() self.inf = 10e12 diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index 7e247c70..ec864a14 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,3 +1,4 @@ -__all__ = ["MLP", "ConditionalRandomField"] +__all__ = ["MLP", "ConditionalRandomField","viterbi_decode"] from .CRF import ConditionalRandomField from .MLP import MLP +from .utils import viterbi_decode \ No newline at end of file diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 67db08f7..12e6893b 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -1,4 +1,4 @@ - +__all__ = ["viterbi_decode"] import torch diff --git a/fastNLP/modules/dropout.py b/fastNLP/modules/dropout.py index 34cf9e90..97745c00 100644 --- a/fastNLP/modules/dropout.py +++ b/fastNLP/modules/dropout.py @@ -1,5 +1,5 @@ import torch - +__all__ = [] class TimestepDropout(torch.nn.Dropout): """This module accepts a ``[batch_size, num_timesteps, embedding_dim)]`` and use a single diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index 06d8b86a..a1cd910b 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -1,11 +1,9 @@ from .conv_maxpool import ConvMaxpool from .embedding import Embedding -from .linear import Linear from .lstm import LSTM from .bert import BertModel __all__ = ["LSTM", "Embedding", - "Linear", "ConvMaxpool", "BertModel"] diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index 54b702ea..c3886c86 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -6,16 +6,15 @@ from ..utils import initial_parameter # from torch.nn.init import xavier_uniform class ConvolutionCharEncoder(nn.Module): - """char级别的卷积编码器.""" - + """ + char级别的卷积编码器. + :param int char_emb_size: char级别embedding的维度. Default: 50 + 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. + :param tuple feature_maps: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的filter. + :param tuple kernels: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的卷积核. + :param initial_method: 初始化参数的方式, 默认为`xavier normal` + """ def __init__(self, char_emb_size=50, feature_maps=(40, 30, 30), kernels=(3, 4, 5), initial_method=None): - """ - :param int char_emb_size: char级别embedding的维度. Default: 50 - 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. - :param tuple feature_maps: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的filter. - :param tuple kernels: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的卷积核. - :param initial_method: 初始化参数的方式, 默认为`xavier normal` - """ super(ConvolutionCharEncoder, self).__init__() self.convs = nn.ModuleList([ nn.Conv2d(1, feature_maps[i], kernel_size=(char_emb_size, kernels[i]), bias=True, padding=(0, 4)) diff --git a/fastNLP/modules/encoder/linear.py b/fastNLP/modules/encoder/linear.py deleted file mode 100644 index 06edf81b..00000000 --- a/fastNLP/modules/encoder/linear.py +++ /dev/null @@ -1,21 +0,0 @@ -import torch.nn as nn - -from ..utils import initial_parameter - - -class Linear(nn.Module): - """ - - :param int input_size: input size - :param int output_size: output size - :param bool bias: - :param str initial_method: - """ - def __init__(self, input_size, output_size, bias=True, initial_method=None): - super(Linear, self).__init__() - self.linear = nn.Linear(input_size, output_size, bias) - initial_parameter(self, initial_method) - - def forward(self, x): - x = self.linear(x) - return x diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index cc6b1183..b11a84eb 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -19,15 +19,13 @@ class LSTM(nn.Module): :param batch_first: 若为 ``True``, 输入和输出 ``Tensor`` 形状为 :(batch, seq, feature). Default: ``False`` :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` - :param get_hidden: 是否返回隐状态 `h` . Default: ``False`` """ def __init__(self, input_size, hidden_size=100, num_layers=1, dropout=0.0, batch_first=True, - bidirectional=False, bias=True, initial_method=None, get_hidden=False): + bidirectional=False, bias=True, initial_method=None): super(LSTM, self).__init__() self.batch_first = batch_first self.lstm = nn.LSTM(input_size, hidden_size, num_layers, bias=bias, batch_first=batch_first, dropout=dropout, bidirectional=bidirectional) - self.get_hidden = get_hidden initial_parameter(self, initial_method) def forward(self, x, seq_len=None, h0=None, c0=None): @@ -39,7 +37,6 @@ class LSTM(nn.Module): :param c0: [batch, hidden_size] 初始Cell状态, 若为 ``None`` , 设为全1向量. Default: ``None`` :return (output, ht) 或 output: 若 ``get_hidden=True`` [batch, seq_len, hidden_size*num_direction] 输出序列 和 [batch, hidden_size*num_direction] 最后时刻隐状态. - 若 ``get_hidden=False`` 仅返回输出序列. """ if h0 is not None and c0 is not None: hx = (h0, c0) @@ -61,16 +58,4 @@ class LSTM(nn.Module): output = output[:, unsort_idx] else: output, hx = self.lstm(x, hx) - if self.get_hidden: - return output, hx - return output - - -if __name__ == "__main__": - lstm = LSTM(input_size=2, hidden_size=2, get_hidden=False) - x = torch.randn((3, 5, 2)) - seq_lens = torch.tensor([5,1,2]) - y = lstm(x, seq_lens) - print(x) - print(y) - print(x.size(), y.size(), ) + return output, hx diff --git a/fastNLP/modules/other_modules.py b/fastNLP/modules/other_modules.py deleted file mode 100644 index 92a08ba1..00000000 --- a/fastNLP/modules/other_modules.py +++ /dev/null @@ -1,186 +0,0 @@ -import numpy as np -import torch -import torch.nn as nn -import torch.nn.functional as F -import torch.utils.data -from torch.nn import Parameter - - -class GroupNorm(nn.Module): - def __init__(self, num_features, num_groups=20, eps=1e-5): - super(GroupNorm, self).__init__() - self.weight = nn.Parameter(torch.ones(1, num_features, 1)) - self.bias = nn.Parameter(torch.zeros(1, num_features, 1)) - self.num_groups = num_groups - self.eps = eps - - def forward(self, x): - N, C, H = x.size() - G = self.num_groups - assert C % G == 0 - - x = x.view(N, G, -1) - mean = x.mean(-1, keepdim=True) - var = x.var(-1, keepdim=True) - - x = (x - mean) / (var + self.eps).sqrt() - x = x.view(N, C, H) - return x * self.weight + self.bias - - -class LayerNormalization(nn.Module): - """ - - :param int layer_size: - :param float eps: default=1e-3 - """ - def __init__(self, layer_size, eps=1e-3): - super(LayerNormalization, self).__init__() - - self.eps = eps - self.a_2 = nn.Parameter(torch.ones(1, layer_size, requires_grad=True)) - self.b_2 = nn.Parameter(torch.zeros(1, layer_size, requires_grad=True)) - - def forward(self, z): - if z.size(1) == 1: - return z - - mu = torch.mean(z, keepdim=True, dim=-1) - sigma = torch.std(z, keepdim=True, dim=-1) - ln_out = (z - mu) / (sigma + self.eps) - ln_out = ln_out * self.a_2 + self.b_2 - return ln_out - - -class BiLinear(nn.Module): - def __init__(self, n_left, n_right, n_out, bias=True): - """ - - :param int n_left: size of left input - :param int n_right: size of right input - :param int n_out: size of output - :param bool bias: If set to False, the layer will not learn an additive bias. Default: True - """ - super(BiLinear, self).__init__() - self.n_left = n_left - self.n_right = n_right - self.n_out = n_out - - self.U = Parameter(torch.Tensor(self.n_out, self.n_left, self.n_right)) - self.W_l = Parameter(torch.Tensor(self.n_out, self.n_left)) - self.W_r = Parameter(torch.Tensor(self.n_out, self.n_left)) - - if bias: - self.bias = Parameter(torch.Tensor(n_out)) - else: - self.register_parameter('bias', None) - - self.reset_parameters() - - def reset_parameters(self): - nn.init.xavier_uniform_(self.W_l) - nn.init.xavier_uniform_(self.W_r) - nn.init.constant_(self.bias, 0.) - nn.init.xavier_uniform_(self.U) - - def forward(self, input_left, input_right): - """ - :param Tensor input_left: the left input tensor with shape = [batch1, batch2, ..., left_features] - :param Tensor input_right: the right input tensor with shape = [batch1, batch2, ..., right_features] - - """ - left_size = input_left.size() - right_size = input_right.size() - assert left_size[:-1] == right_size[:-1], \ - "batch size of left and right inputs mis-match: (%s, %s)" % (left_size[:-1], right_size[:-1]) - batch = int(np.prod(left_size[:-1])) - - # convert left and right input to matrices [batch, left_features], [batch, right_features] - input_left = input_left.view(batch, self.n_left) - input_right = input_right.view(batch, self.n_right) - - # output [batch, out_features] - output = F.bilinear(input_left, input_right, self.U, self.bias) - output = output + \ - F.linear(input_left, self.W_l, None) + \ - F.linear(input_right, self.W_r, None) - # convert back to [batch1, batch2, ..., out_features] - return output.view(left_size[:-1] + (self.n_out,)) - - def __repr__(self): - return self.__class__.__name__ + ' (' \ - + 'in1_features=' + str(self.n_left) \ - + ', in2_features=' + str(self.n_right) \ - + ', out_features=' + str(self.n_out) + ')' - - -class BiAffine(nn.Module): - def __init__(self, n_enc, n_dec, n_labels, biaffine=True, **kwargs): - """ - - :param int n_enc: the dimension of the encoder input. - :param int n_dec: the dimension of the decoder input. - :param int n_labels: the number of labels of the crf layer - :param bool biaffine: if apply bi-affine parameter. - """ - super(BiAffine, self).__init__() - self.n_enc = n_enc - self.n_dec = n_dec - self.num_labels = n_labels - self.biaffine = biaffine - - self.W_d = Parameter(torch.Tensor(self.num_labels, self.n_dec)) - self.W_e = Parameter(torch.Tensor(self.num_labels, self.n_enc)) - self.b = Parameter(torch.Tensor(self.num_labels, 1, 1)) - if self.biaffine: - self.U = Parameter(torch.Tensor(self.num_labels, self.n_dec, self.n_enc)) - else: - self.register_parameter('U', None) - - self.reset_parameters() - - def reset_parameters(self): - nn.init.xavier_uniform_(self.W_d) - nn.init.xavier_uniform_(self.W_e) - nn.init.constant_(self.b, 0.) - if self.biaffine: - nn.init.xavier_uniform_(self.U) - - def forward(self, input_d, input_e, mask_d=None, mask_e=None): - """ - - :param Tensor input_d: the decoder input tensor with shape = [batch, length_decoder, input_size] - :param Tensor input_e: the child input tensor with shape = [batch, length_encoder, input_size] - :param mask_d: Tensor or None, the mask tensor for decoder with shape = [batch, length_decoder] - :param mask_e: Tensor or None, the mask tensor for encoder with shape = [batch, length_encoder] - :returns: Tensor, the energy tensor with shape = [batch, num_label, length, length] - """ - assert input_d.size(0) == input_e.size(0), 'batch sizes of encoder and decoder are requires to be equal.' - batch, length_decoder, _ = input_d.size() - _, length_encoder, _ = input_e.size() - - # compute decoder part: [num_label, input_size_decoder] * [batch, input_size_decoder, length_decoder] - # the output shape is [batch, num_label, length_decoder] - out_d = torch.matmul(self.W_d, input_d.transpose(1, 2)).unsqueeze(3) - # compute decoder part: [num_label, input_size_encoder] * [batch, input_size_encoder, length_encoder] - # the output shape is [batch, num_label, length_encoder] - out_e = torch.matmul(self.W_e, input_e.transpose(1, 2)).unsqueeze(2) - - # output shape [batch, num_label, length_decoder, length_encoder] - if self.biaffine: - # compute bi-affine part - # [batch, 1, length_decoder, input_size_decoder] * [num_labels, input_size_decoder, input_size_encoder] - # output shape [batch, num_label, length_decoder, input_size_encoder] - output = torch.matmul(input_d.unsqueeze(1), self.U) - # [batch, num_label, length_decoder, input_size_encoder] * [batch, 1, input_size_encoder, length_encoder] - # output shape [batch, num_label, length_decoder, length_encoder] - output = torch.matmul(output, input_e.unsqueeze(1).transpose(2, 3)) - - output = output + out_d + out_e + self.b - else: - output = out_d + out_d + self.b - - if mask_d is not None: - output = output * mask_d.unsqueeze(1).unsqueeze(3) * mask_e.unsqueeze(1).unsqueeze(2) - - return output diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index c6d8be9d..337be64d 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -4,14 +4,6 @@ import torch.nn as nn import torch.nn.init as init -def mask_softmax(matrix, mask): - if mask is None: - result = torch.nn.functional.softmax(matrix, dim=-1) - else: - raise NotImplementedError - return result - - def initial_parameter(net, initial_method=None): """A method used to initialize the weights of PyTorch models. @@ -77,7 +69,8 @@ def initial_parameter(net, initial_method=None): def seq_mask(seq_len, max_len): - """Create sequence mask. + """ + Create sequence mask. :param seq_len: list or torch.Tensor, the lengths of sequences in a batch. :param max_len: int, the maximum sequence length in a batch. @@ -92,7 +85,8 @@ def seq_mask(seq_len, max_len): def get_embeddings(init_embed): - """得到词嵌入 + """ + 得到词嵌入 TODO :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, diff --git a/requirements.txt b/requirements.txt index d763ea1b..dfd2b16e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ numpy torch>=0.4.0 -tensorboardX tqdm nltk \ No newline at end of file diff --git a/test/io/test_config_saver.py b/test/io/test_config_saver.py index a71419e5..978eb80f 100644 --- a/test/io/test_config_saver.py +++ b/test/io/test_config_saver.py @@ -1,112 +1,112 @@ import os import unittest -from fastNLP.io.config_io import ConfigSection, ConfigLoader, ConfigSaver +from fastNLP.io import ConfigSection, ConfigLoader, ConfigSaver class TestConfigSaver(unittest.TestCase): def test_case_1(self): - config_file_dir = "test/io" + config_file_dir = "." config_file_name = "config" config_file_path = os.path.join(config_file_dir, config_file_name) - + tmp_config_file_path = os.path.join(config_file_dir, "tmp_config") - + with open(config_file_path, "r") as f: lines = f.readlines() - + standard_section = ConfigSection() t_section = ConfigSection() ConfigLoader().load_config(config_file_path, {"test": standard_section, "t": t_section}) - + config_saver = ConfigSaver(config_file_path) - + section = ConfigSection() section["doubles"] = 0.8 section["tt"] = 0.5 section["test"] = 105 section["str"] = "this is a str" - + test_case_2_section = section test_case_2_section["double"] = 0.5 - + for k in section.__dict__.keys(): standard_section[k] = section[k] - + config_saver.save_config_file("test", section) config_saver.save_config_file("another-test", section) config_saver.save_config_file("one-another-test", section) config_saver.save_config_file("test-case-2", section) - + test_section = ConfigSection() at_section = ConfigSection() another_test_section = ConfigSection() one_another_test_section = ConfigSection() a_test_case_2_section = ConfigSection() - + ConfigLoader().load_config(config_file_path, {"test": test_section, "another-test": another_test_section, "t": at_section, "one-another-test": one_another_test_section, "test-case-2": a_test_case_2_section}) - + assert test_section == standard_section assert at_section == t_section assert another_test_section == section assert one_another_test_section == section assert a_test_case_2_section == test_case_2_section - + config_saver.save_config_file("test", section) - + with open(config_file_path, "w") as f: f.writelines(lines) - + with open(tmp_config_file_path, "w") as f: f.write('[test]\n') f.write('this is an fault example\n') - + tmp_config_saver = ConfigSaver(tmp_config_file_path) try: tmp_config_saver._read_section() except Exception as e: pass os.remove(tmp_config_file_path) - + try: tmp_config_saver = ConfigSaver("file-NOT-exist") except Exception as e: pass - + def test_case_2(self): config = "[section_A]\n[section_B]\n" - + with open("./test.cfg", "w", encoding="utf-8") as f: f.write(config) saver = ConfigSaver("./test.cfg") - + section = ConfigSection() section["doubles"] = 0.8 section["tt"] = [1, 2, 3] section["test"] = 105 section["str"] = "this is a str" - + saver.save_config_file("section_A", section) - + os.system("rm ./test.cfg") - + def test_case_3(self): config = "[section_A]\ndoubles = 0.9\ntt = [1, 2, 3]\n[section_B]\n" - + with open("./test.cfg", "w", encoding="utf-8") as f: f.write(config) saver = ConfigSaver("./test.cfg") - + section = ConfigSection() section["doubles"] = 0.8 section["tt"] = [1, 2, 3] section["test"] = 105 section["str"] = "this is a str" - + saver.save_config_file("section_A", section) - + os.system("rm ./test.cfg") diff --git a/test/io/test_dataset_loader.py b/test/io/test_dataset_loader.py index 2e367567..3c9d7c07 100644 --- a/test/io/test_dataset_loader.py +++ b/test/io/test_dataset_loader.py @@ -1,31 +1,30 @@ import unittest -from fastNLP.io.dataset_loader import Conll2003Loader, PeopleDailyCorpusLoader, \ - CSVLoader, SNLILoader, JsonLoader +from fastNLP.io import Conll2003Loader, PeopleDailyCorpusLoader, CSVLoader, SNLILoader, JsonLoader -class TestDatasetLoader(unittest.TestCase): +class TestDatasetLoader(unittest.TestCase): + def test_Conll2003Loader(self): """ Test the the loader of Conll2003 dataset """ - dataset_path = "test/data_for_tests/conll_2003_example.txt" + dataset_path = "../data_for_tests/conll_2003_example.txt" loader = Conll2003Loader() dataset_2003 = loader.load(dataset_path) - + def test_PeopleDailyCorpusLoader(self): - data_set = PeopleDailyCorpusLoader().load("test/data_for_tests/people_daily_raw.txt") - + data_set = PeopleDailyCorpusLoader().load("../data_for_tests/people_daily_raw.txt") + def test_CSVLoader(self): - ds = CSVLoader(sep='\t', headers=['words', 'label'])\ - .load('test/data_for_tests/tutorial_sample_dataset.csv') + ds = CSVLoader(sep='\t', headers=['words', 'label']) \ + .load('../data_for_tests/tutorial_sample_dataset.csv') assert len(ds) > 0 - + def test_SNLILoader(self): - ds = SNLILoader().load('test/data_for_tests/sample_snli.jsonl') + ds = SNLILoader().load('../data_for_tests/sample_snli.jsonl') assert len(ds) == 3 - + def test_JsonLoader(self): - ds = JsonLoader().load('test/data_for_tests/sample_snli.jsonl') + ds = JsonLoader().load('../data_for_tests/sample_snli.jsonl') assert len(ds) == 3 - diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index c41aec96..05a127a9 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -1,15 +1,15 @@ import unittest import numpy as np -from fastNLP.core.vocabulary import Vocabulary -from fastNLP.io.embed_loader import EmbedLoader +from fastNLP import Vocabulary +from fastNLP.io import EmbedLoader class TestEmbedLoader(unittest.TestCase): def test_load_with_vocab(self): vocab = Vocabulary() - glove = "test/data_for_tests/glove.6B.50d_test.txt" - word2vec = "test/data_for_tests/word2vec_test.txt" + glove = "../data_for_tests/glove.6B.50d_test.txt" + word2vec = "../data_for_tests/word2vec_test.txt" vocab.add_word('the') vocab.add_word('none') g_m = EmbedLoader.load_with_vocab(glove, vocab) @@ -20,8 +20,8 @@ class TestEmbedLoader(unittest.TestCase): def test_load_without_vocab(self): words = ['the', 'of', 'in', 'a', 'to', 'and'] - glove = "test/data_for_tests/glove.6B.50d_test.txt" - word2vec = "test/data_for_tests/word2vec_test.txt" + glove = "../data_for_tests/glove.6B.50d_test.txt" + word2vec = "../data_for_tests/word2vec_test.txt" g_m, vocab = EmbedLoader.load_without_vocab(glove) self.assertEqual(g_m.shape, (8, 50)) for word in words: From 4926b33df0094ead9b8f868e75c14ce425de54e7 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 7 May 2019 13:12:38 +0800 Subject: [PATCH 087/173] =?UTF-8?q?core=E9=83=A8=E5=88=86=E7=9A=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=92=8C=E4=B8=80=E4=BA=9B=E5=B0=8F=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 3 +- fastNLP/core/__init__.py | 2 +- fastNLP/core/callback.py | 15 ++- fastNLP/core/field.py | 1 + fastNLP/core/metrics.py | 5 + fastNLP/core/trainer.py | 6 +- fastNLP/core/utils.py | 2 +- fastNLP/core/vocabulary.py | 69 +++++----- test/core/test_batch.py | 41 +++--- test/core/test_callbacks.py | 54 +++----- test/core/test_dataset.py | 15 +-- .../{test_fieldarray.py => test_field.py} | 10 +- test/core/test_instance.py | 12 +- test/core/test_loss.py | 24 ++-- test/core/test_metrics.py | 124 +++++++++--------- test/core/test_optimizer.py | 18 +-- test/core/test_sampler.py | 6 +- test/core/test_tester.py | 30 ++--- test/core/test_trainer.py | 71 +++++----- test/core/test_utils.py | 48 ++++--- test/core/test_vocabulary.py | 73 ++++++----- 21 files changed, 324 insertions(+), 305 deletions(-) rename test/core/{test_fieldarray.py => test_field.py} (97%) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 25d7ea82..330ad9c2 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -13,7 +13,8 @@ fastNLP 中最常用的组件可以直接从 fastNLP 包中 import ,他们的 __all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", "Trainer", "Tester", "Callback", "Padder", "AutoPadder", "EngChar2DPadder", - "AccuracyMetric", "Optimizer", "SGD", "Adam", + "AccuracyMetric", "BMESF1PreRecMetric", "SpanFPreRecMetric", "SQuADMetric", + "Optimizer", "SGD", "Adam", "Sampler", "SequentialSampler", "BucketSampler", "RandomSampler", "LossFunc", "CrossEntropyLoss", "L1Loss", "BCELoss", "NLLLoss", "LossInForward", "cache_results"] diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index f11f0e12..4e5f8527 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -17,7 +17,7 @@ from .dataset import DataSet from .field import FieldArray, Padder, AutoPadder, EngChar2DPadder from .instance import Instance from .losses import LossFunc, CrossEntropyLoss, L1Loss, BCELoss, NLLLoss, LossInForward -from .metrics import AccuracyMetric +from .metrics import AccuracyMetric, BMESF1PreRecMetric, SpanFPreRecMetric, SQuADMetric from .optimizer import Optimizer, SGD, Adam from .sampler import SequentialSampler, BucketSampler, RandomSampler, Sampler from .tester import Tester diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index b3aaffaa..a6cc3402 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -236,6 +236,7 @@ class CallbackManager(Callback): for env_name, env_val in env.items(): for callback in self.callbacks: + print(callback, env_name, env_val ) setattr(callback, '_' + env_name, env_val) # Callback.trainer @_transfer @@ -425,19 +426,25 @@ class LRFinder(Callback): super(LRFinder, self).__init__() self.start_lr, self.end_lr = start_lr, end_lr - self.num_it = self.batch_per_epoch + self.stop = False self.best_loss = 0. self.best_lr = None self.loss_history = [] self.smooth_value = SmoothValue(0.8) self.opt = None - scale = (self.end_lr - self.start_lr) / self.num_it - - self.lr_gen = (self.start_lr + scale * (step + 1) for step in range(self.num_it)) self.find = None self.loader = ModelLoader() + @property + def lr_gen(self): + scale = (self.end_lr - self.start_lr) / self.batch_per_epoch + return (self.start_lr + scale * (step + 1) for step in range(self.batch_per_epoch)) + + @property + def num_it(self): + return self.batch_per_epoch + def on_epoch_begin(self): if self.epoch == 1: # first epoch self.opt = self.trainer.optimizer # pytorch optimizer diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index b4858c2e..a355c4d2 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -418,6 +418,7 @@ class AutoPadder(Padder): return False def __call__(self, contents, field_name, field_ele_dtype): + if not _is_iterable(contents[0]): array = np.array([content for content in contents], dtype=field_ele_dtype) elif field_ele_dtype in (np.int64, np.float64) and self._is_two_dimension(contents): diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index be9a3c48..c9ba7f35 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -430,6 +430,7 @@ def _bio_tag_to_spans(tags, ignore_labels=None): class SpanFPreRecMetric(MetricBase): """ + 别名::class:`fastNLP.SpanFPreRecMetric` :class:`fastNLP.core.metrics.SpanFPreRecMetric` 在序列标注问题中,以span的方式计算F, pre, rec. 比如中文Part of speech中,会以character的方式进行标注,句子'中国在亚洲'对应的POS可能为(以BMES为例) @@ -619,6 +620,8 @@ class SpanFPreRecMetric(MetricBase): class BMESF1PreRecMetric(MetricBase): """ + 别名::class:`fastNLP.BMESF1PreRecMetric` :class:`fastNLP.core.metrics.BMESF1PreRecMetric` + 按照BMES标注方式计算f1, precision, recall。由于可能存在非法tag,比如"BS",所以需要用以下的表格做转换,cur_B意思是当前tag是B, next_B意思是后一个tag是B。则cur_B=S,即将当前被predict是B的tag标为S;next_M=B, 即将后一个被predict是M的tag标为B @@ -826,6 +829,8 @@ def _pred_topk(y_prob, k=1): class SQuADMetric(MetricBase): """ + 别名::class:`fastNLP.SQuADMetric` :class:`fastNLP.core.metrics.SQuADMetric` + SQuAD数据集metric :param pred1: 参数映射表中`pred1`的映射关系,None表示映射关系为`pred1`->`pred1` diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 42f957b1..cb2ff821 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -350,7 +350,7 @@ class Trainer(object): :param train_data: 训练集, :class:`~fastNLP.DataSet` 类型。 :param nn.modules model: 待训练的模型 - :param torch.optim.Optimizer optimizer: 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器 + :param optimizer: `torch.optim.Optimizer` 优化器。如果为None,则Trainer使用默认的Adam(model.parameters(), lr=4e-3)这个优化器 :param int batch_size: 训练和验证的时候的batch大小。 :param loss: 使用的 :class:`~fastNLP.core.losses.LossBase` 对象。当为None时,默认使用 :class:`~fastNLP.LossInForward` :param sampler: Batch数据生成的顺序, :class:`~fastNLP.Sampler` 类型。如果为None,默认使用 :class:`~fastNLP.RandomSampler` @@ -403,7 +403,6 @@ class Trainer(object): callbacks=None, check_code_level=0): super(Trainer, self).__init__() - if not isinstance(train_data, DataSet): raise TypeError(f"The type of train_data must be fastNLP.DataSet, got {type(train_data)}.") if not isinstance(model, nn.Module): @@ -468,7 +467,7 @@ class Trainer(object): len(self.train_data) % self.batch_size != 0)) * self.n_epochs self.model = _move_model_to_device(self.model, device=device) - + if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer elif isinstance(optimizer, Optimizer): @@ -493,6 +492,7 @@ class Trainer(object): self.step = 0 self.start_time = None # start timestamp + print("callback_manager") self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 1864f39e..09a3a4c5 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -616,7 +616,7 @@ def seq_lens_to_masks(seq_lens, float=False): assert len(seq_lens.size()) == 1, f"seq_lens can only have one dimension, got {len(seq_lens.size())==1}." batch_size = seq_lens.size(0) max_len = seq_lens.max() - indexes = torch.arange(max_len).view(1, -1).repeat(batch_size, 1).to(seq_lens.device) + indexes = torch.arange(max_len).view(1, -1).repeat(batch_size, 1).to(seq_lens.device).long() masks = indexes.lt(seq_lens.unsqueeze(1)) if float: diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 0dc232e4..e9f2cb61 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -2,16 +2,18 @@ from functools import wraps from collections import Counter from .dataset import DataSet + def _check_build_vocab(func): """A decorator to make sure the indexing is built before used. """ - @wraps(func) # to solve missing docstring + + @wraps(func) # to solve missing docstring def _wrapper(self, *args, **kwargs): if self.word2idx is None or self.rebuild is True: self.build_vocab() return func(self, *args, **kwargs) - + return _wrapper @@ -19,7 +21,8 @@ def _check_build_status(func): """A decorator to check whether the vocabulary updates after the last build. """ - @wraps(func) # to solve missing docstring + + @wraps(func) # to solve missing docstring def _wrapper(self, *args, **kwargs): if self.rebuild is False: self.rebuild = True @@ -28,7 +31,7 @@ def _check_build_status(func): "Adding more words may cause unexpected behaviour of Vocabulary. ".format( self.max_size, func.__name__)) return func(self, *args, **kwargs) - + return _wrapper @@ -50,15 +53,15 @@ class Vocabulary(object): 若为 ``None`` , 则不限制大小. Default: ``None`` :param int min_freq: 能被记录下的词在文本中的最小出现频率, 应大于或等于 1. 若小于该频率, 词语将被视为 `unknown`. 若为 ``None`` , 所有文本中的词都被记录. Default: ``None`` - :param str padding: padding的字符. 如果设置为 ``None`` , + :param str optional padding: padding的字符. 如果设置为 ``None`` , 则vocabulary中不考虑padding, 也不计入词表大小,为 ``None`` 的情况多在为label建立Vocabulary的情况. Default: '' - :param str unknow: unknow的字符,所有未被记录的词在转为 `int` 时将被视为unknown. + :param str optional unknown: unknown的字符,所有未被记录的词在转为 `int` 时将被视为unknown. 如果设置为 ``None`` ,则vocabulary中不考虑unknow, 也不计入词表大小. 为 ``None`` 的情况多在为label建立Vocabulary的情况. Default: '' """ - + def __init__(self, max_size=None, min_freq=None, padding='', unknown=''): self.max_size = max_size self.min_freq = min_freq @@ -68,7 +71,7 @@ class Vocabulary(object): self.word2idx = None self.idx2word = None self.rebuild = True - + @_check_build_status def update(self, word_lst): """依次增加序列中词在词典中的出现频率 @@ -76,7 +79,7 @@ class Vocabulary(object): :param list word_lst: a list of strings """ self.word_count.update(word_lst) - + @_check_build_status def add(self, word): """ @@ -85,7 +88,7 @@ class Vocabulary(object): :param str word: 新词 """ self.word_count[word] += 1 - + @_check_build_status def add_word(self, word): """ @@ -94,7 +97,7 @@ class Vocabulary(object): :param str word: 新词 """ self.add(word) - + @_check_build_status def add_word_lst(self, word_lst): """ @@ -103,7 +106,7 @@ class Vocabulary(object): :param list[str] word_lst: 词的序列 """ self.update(word_lst) - + def build_vocab(self): """ 根据已经出现的词和出现频率构建词典. 注意: 重复构建可能会改变词典的大小, @@ -116,7 +119,7 @@ class Vocabulary(object): self.word2idx[self.padding] = len(self.word2idx) if self.unknown is not None: self.word2idx[self.unknown] = len(self.word2idx) - + max_size = min(self.max_size, len(self.word_count)) if self.max_size else None words = self.word_count.most_common(max_size) if self.min_freq is not None: @@ -127,18 +130,18 @@ class Vocabulary(object): self.word2idx.update({w: i + start_idx for i, (w, _) in enumerate(words)}) self.build_reverse_vocab() self.rebuild = False - + def build_reverse_vocab(self): """ 基于 "word to index" dict, 构建 "index to word" dict. """ self.idx2word = {i: w for w, i in self.word2idx.items()} - + @_check_build_vocab def __len__(self): return len(self.word2idx) - + @_check_build_vocab def __contains__(self, item): """ @@ -148,7 +151,7 @@ class Vocabulary(object): :return: True or False """ return item in self.word2idx - + def has_word(self, w): """ 检查词是否被记录 @@ -163,7 +166,7 @@ class Vocabulary(object): :return: ``True`` or ``False`` """ return self.__contains__(w) - + @_check_build_vocab def __getitem__(self, w): """ @@ -177,7 +180,7 @@ class Vocabulary(object): return self.word2idx[self.unknown] else: raise ValueError("word {} not in vocabulary".format(w)) - + @_check_build_vocab def index_dataset(self, *datasets, field_name, new_field_name=None): """ @@ -194,6 +197,7 @@ class Vocabulary(object): :param str new_field_name: 保存结果的field_name. 若为 ``None`` , 将覆盖原field. Default: ``None`` """ + def index_instance(ins): """ 有几种情况, str, 1d-list, 2d-list @@ -209,8 +213,8 @@ class Vocabulary(object): else: if isinstance(field[0][0], list): raise RuntimeError("Only support field with 2 dimensions.") - return[[self.to_index(c) for c in w] for w in field] - + return [[self.to_index(c) for c in w] for w in field] + if new_field_name is None: new_field_name = field_name for idx, dataset in enumerate(datasets): @@ -222,7 +226,7 @@ class Vocabulary(object): raise e else: raise RuntimeError("Only DataSet type is allowed.") - + def from_dataset(self, *datasets, field_name): """ 使用dataset的对应field中词构建词典 @@ -243,7 +247,7 @@ class Vocabulary(object): field_name = [field_name] elif not isinstance(field_name, list): raise TypeError('invalid argument field_name: {}'.format(field_name)) - + def construct_vocab(ins): for fn in field_name: field = ins[fn] @@ -256,6 +260,7 @@ class Vocabulary(object): if isinstance(field[0][0], list): raise RuntimeError("Only support field with 2 dimensions.") [self.add_word_lst(w) for w in field] + for idx, dataset in enumerate(datasets): if isinstance(dataset, DataSet): try: @@ -266,7 +271,7 @@ class Vocabulary(object): else: raise RuntimeError("Only DataSet type is allowed.") return self - + def to_index(self, w): """ 将词转为数字. 若词不再词典中被记录, 将视为 unknown, 若 ``unknown=None`` , 将抛出 @@ -282,7 +287,7 @@ class Vocabulary(object): :return int index: the number """ return self.__getitem__(w) - + @property @_check_build_vocab def unknown_idx(self): @@ -292,7 +297,7 @@ class Vocabulary(object): if self.unknown is None: return None return self.word2idx[self.unknown] - + @property @_check_build_vocab def padding_idx(self): @@ -302,7 +307,7 @@ class Vocabulary(object): if self.padding is None: return None return self.word2idx[self.padding] - + @_check_build_vocab def to_word(self, idx): """ @@ -312,26 +317,26 @@ class Vocabulary(object): :return str word: the word """ return self.idx2word[idx] - + def __getstate__(self): """Use to prepare data for pickle. """ - len(self) # make sure vocab has been built + len(self) # make sure vocab has been built state = self.__dict__.copy() # no need to pickle idx2word as it can be constructed from word2idx del state['idx2word'] return state - + def __setstate__(self, state): """Use to restore state from pickle. """ self.__dict__.update(state) self.build_reverse_vocab() - + def __repr__(self): return "Vocabulary({}...)".format(list(self.word_count.keys())[:5]) - + def __iter__(self): return iter(list(self.word_count.keys())) diff --git a/test/core/test_batch.py b/test/core/test_batch.py index 072f7c83..d1f93b9c 100644 --- a/test/core/test_batch.py +++ b/test/core/test_batch.py @@ -1,13 +1,12 @@ -import time import unittest import numpy as np import torch -from fastNLP.core.batch import Batch -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance -from fastNLP.core.sampler import SequentialSampler +from fastNLP import Batch +from fastNLP import DataSet +from fastNLP import Instance +from fastNLP import SequentialSampler def generate_fake_dataset(num_samples=1000): @@ -16,11 +15,11 @@ def generate_fake_dataset(num_samples=1000): :param num_samples: sample的数量 :return: """ - + max_len = 50 min_len = 10 num_features = 4 - + data_dict = {} for i in range(num_features): data = [] @@ -28,9 +27,9 @@ def generate_fake_dataset(num_samples=1000): for length in lengths: data.append(np.random.randint(100, size=length)) data_dict[str(i)] = data - + dataset = DataSet(data_dict) - + for i in range(num_features): if np.random.randint(2) == 0: dataset.set_input(str(i)) @@ -38,6 +37,7 @@ 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. @@ -51,18 +51,19 @@ def construct_dataset(sentences): dataset.append(instance) return dataset + class TestCase1(unittest.TestCase): def test_simple(self): dataset = construct_dataset( [["FastNLP", "is", "the", "most", "beautiful", "tool", "in", "the", "world"] for _ in range(40)]) dataset.set_target() batch = Batch(dataset, batch_size=4, sampler=SequentialSampler(), as_numpy=True) - + cnt = 0 for _, _ in batch: cnt += 1 self.assertEqual(cnt, 10) - + def test_dataset_batching(self): ds = DataSet({"x": [[1, 2, 3, 4]] * 40, "y": [[5, 6]] * 40}) ds.set_input("x") @@ -74,7 +75,7 @@ class TestCase1(unittest.TestCase): self.assertEqual(len(y["y"]), 4) self.assertListEqual(list(x["x"][-1]), [1, 2, 3, 4]) self.assertListEqual(list(y["y"][-1]), [5, 6]) - + def test_list_padding(self): ds = DataSet({"x": [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] * 10, "y": [[4, 3, 2, 1], [3, 2, 1], [2, 1], [1]] * 10}) @@ -84,7 +85,7 @@ class TestCase1(unittest.TestCase): for x, y in iter: self.assertEqual(x["x"].shape, (4, 4)) self.assertEqual(y["y"].shape, (4, 4)) - + def test_numpy_padding(self): ds = DataSet({"x": np.array([[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] * 10), "y": np.array([[4, 3, 2, 1], [3, 2, 1], [2, 1], [1]] * 10)}) @@ -94,7 +95,7 @@ class TestCase1(unittest.TestCase): for x, y in iter: self.assertEqual(x["x"].shape, (4, 4)) self.assertEqual(y["y"].shape, (4, 4)) - + def test_list_to_tensor(self): ds = DataSet({"x": [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] * 10, "y": [[4, 3, 2, 1], [3, 2, 1], [2, 1], [1]] * 10}) @@ -106,7 +107,7 @@ class TestCase1(unittest.TestCase): self.assertEqual(tuple(x["x"].shape), (4, 4)) self.assertTrue(isinstance(y["y"], torch.Tensor)) self.assertEqual(tuple(y["y"].shape), (4, 4)) - + def test_numpy_to_tensor(self): ds = DataSet({"x": np.array([[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]] * 10), "y": np.array([[4, 3, 2, 1], [3, 2, 1], [2, 1], [1]] * 10)}) @@ -118,7 +119,7 @@ class TestCase1(unittest.TestCase): self.assertEqual(tuple(x["x"].shape), (4, 4)) self.assertTrue(isinstance(y["y"], torch.Tensor)) self.assertEqual(tuple(y["y"].shape), (4, 4)) - + def test_list_of_list_to_tensor(self): ds = DataSet([Instance(x=[1, 2], y=[3, 4]) for _ in range(2)] + [Instance(x=[1, 2, 3, 4], y=[3, 4, 5, 6]) for _ in range(2)]) @@ -130,7 +131,7 @@ class TestCase1(unittest.TestCase): self.assertEqual(tuple(x["x"].shape), (4, 4)) self.assertTrue(isinstance(y["y"], torch.Tensor)) self.assertEqual(tuple(y["y"].shape), (4, 4)) - + def test_list_of_numpy_to_tensor(self): ds = DataSet([Instance(x=np.array([1, 2]), y=np.array([3, 4])) for _ in range(2)] + [Instance(x=np.array([1, 2, 3, 4]), y=np.array([3, 4, 5, 6])) for _ in range(2)]) @@ -139,16 +140,16 @@ class TestCase1(unittest.TestCase): iter = Batch(ds, batch_size=4, sampler=SequentialSampler(), as_numpy=False) for x, y in iter: print(x, y) - + def test_sequential_batch(self): batch_size = 32 num_samples = 1000 dataset = generate_fake_dataset(num_samples) - + batch = Batch(dataset, batch_size=batch_size, sampler=SequentialSampler()) for batch_x, batch_y in batch: pass - + """ def test_multi_workers_batch(self): batch_size = 32 diff --git a/test/core/test_callbacks.py b/test/core/test_callbacks.py index cf3e2fff..db640eb1 100644 --- a/test/core/test_callbacks.py +++ b/test/core/test_callbacks.py @@ -4,14 +4,13 @@ import numpy as np import torch from fastNLP.core.callback import EarlyStopCallback, GradientClipCallback, LRScheduler, ControlC, \ - LRFinder, \ - TensorboardCallback -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance -from fastNLP.core.losses import BCELoss -from fastNLP.core.metrics import AccuracyMetric -from fastNLP.core.optimizer import SGD -from fastNLP.core.trainer import Trainer + LRFinder, TensorboardCallback +from fastNLP import DataSet +from fastNLP import Instance +from fastNLP import BCELoss +from fastNLP import AccuracyMetric +from fastNLP import SGD +from fastNLP import Trainer from fastNLP.models.base_model import NaiveClassifier @@ -20,15 +19,15 @@ def prepare_env(): mean = np.array([-3, -3]) cov = np.array([[1, 0], [0, 1]]) class_A = np.random.multivariate_normal(mean, cov, size=(1000,)) - + mean = np.array([3, 3]) cov = np.array([[1, 0], [0, 1]]) class_B = np.random.multivariate_normal(mean, cov, size=(1000,)) - + data_set = DataSet([Instance(x=[float(item[0]), float(item[1])], y=[0.0]) for item in class_A] + [Instance(x=[float(item[0]), float(item[1])], y=[1.0]) for item in class_B]) return data_set - + data_set = prepare_fake_dataset() data_set.set_input("x") data_set.set_target("y") @@ -37,19 +36,7 @@ def prepare_env(): class TestCallback(unittest.TestCase): - def test_echo_callback(self): - data_set, model = prepare_env() - trainer = Trainer(data_set, model, - loss=BCELoss(pred="predict", target="y"), - n_epochs=2, - batch_size=32, - print_every=50, - optimizer=SGD(lr=0.1), - check_code_level=2, - use_tqdm=False, - callbacks=[EchoCallback()]) - trainer.train() - + def test_gradient_clip(self): data_set, model = prepare_env() trainer = Trainer(data_set, model, @@ -64,7 +51,7 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[GradientClipCallback(model.parameters(), clip_value=2)]) trainer.train() - + def test_early_stop(self): data_set, model = prepare_env() trainer = Trainer(data_set, model, @@ -79,7 +66,7 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[EarlyStopCallback(5)]) trainer.train() - + def test_lr_scheduler(self): data_set, model = prepare_env() optimizer = torch.optim.SGD(model.parameters(), lr=0.01) @@ -95,7 +82,7 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[LRScheduler(torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1))]) trainer.train() - + def test_KeyBoardInterrupt(self): data_set, model = prepare_env() trainer = Trainer(data_set, model, @@ -108,7 +95,7 @@ class TestCallback(unittest.TestCase): use_tqdm=False, callbacks=[ControlC(False)]) trainer.train() - + def test_LRFinder(self): data_set, model = prepare_env() trainer = Trainer(data_set, model, @@ -121,7 +108,7 @@ class TestCallback(unittest.TestCase): use_tqdm=False, callbacks=[LRFinder(len(data_set) // 32)]) trainer.train() - + def test_TensorboardCallback(self): data_set, model = prepare_env() trainer = Trainer(data_set, model, @@ -136,21 +123,22 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[TensorboardCallback("loss", "metric")]) trainer.train() - + def test_readonly_property(self): from fastNLP.core.callback import Callback passed_epochs = [] total_epochs = 5 + class MyCallback(Callback): def __init__(self): super(MyCallback, self).__init__() - + def on_epoch_begin(self): passed_epochs.append(self.epoch) print(self.n_epochs, self.n_steps, self.batch_size) print(self.model) print(self.optimizer) - + data_set, model = prepare_env() trainer = Trainer(data_set, model, loss=BCELoss(pred="predict", target="y"), @@ -164,4 +152,4 @@ class TestCallback(unittest.TestCase): metrics=AccuracyMetric(pred="predict", target="y"), callbacks=[MyCallback()]) trainer.train() - assert passed_epochs == list(range(1, total_epochs+1)) + assert passed_epochs == list(range(1, total_epochs + 1)) diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 833ee9ce..69548e73 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -1,9 +1,10 @@ import os import unittest -from fastNLP.core.dataset import DataSet -from fastNLP.core.fieldarray import FieldArray -from fastNLP.core.instance import Instance +from fastNLP import DataSet +from fastNLP import FieldArray +from fastNLP import Instance +from fastNLP.io import CSVLoader class TestDataSetInit(unittest.TestCase): @@ -167,13 +168,11 @@ class TestDataSetMethods(unittest.TestCase): ds = DataSet({"x": [[1, 2, 3, 4]] * 10, "y": [[5, 6]] * 10}) d1, d2 = ds.split(0.1) - def test_apply2(self): def split_sent(ins): return ins['raw_sentence'].split() - - dataset = DataSet.read_csv('test/data_for_tests/tutorial_sample_dataset.csv', headers=('raw_sentence', 'label'), - sep='\t') + csv_loader = CSVLoader(headers=['raw_sentence', 'label'],sep='\t') + dataset = csv_loader.load('../data_for_tests/tutorial_sample_dataset.csv') dataset.drop(lambda x: len(x['raw_sentence'].split()) == 0, inplace=True) dataset.apply(split_sent, new_field_name='words', is_input=True) # print(dataset) @@ -208,7 +207,7 @@ class TestDataSetMethods(unittest.TestCase): self.assertEqual(ans.content, [[5, 6]] * 10) def test_add_null(self): - # TODO test failed because 'fastNLP\core\fieldarray.py:143: RuntimeError' + # TODO test failed because 'fastNLP\core\field.py:143: RuntimeError' ds = DataSet() with self.assertRaises(RuntimeError) as RE: ds.add_field('test', []) diff --git a/test/core/test_fieldarray.py b/test/core/test_field.py similarity index 97% rename from test/core/test_fieldarray.py rename to test/core/test_field.py index ff1a8314..1f6580c1 100644 --- a/test/core/test_fieldarray.py +++ b/test/core/test_field.py @@ -2,7 +2,7 @@ import unittest import numpy as np -from fastNLP.core.fieldarray import FieldArray +from fastNLP import FieldArray class TestFieldArrayInit(unittest.TestCase): @@ -170,7 +170,7 @@ class TestPadder(unittest.TestCase): 测试AutoPadder能否正常工作 :return: """ - from fastNLP.core.fieldarray import AutoPadder + from fastNLP import AutoPadder padder = AutoPadder() content = ['This is a str', 'this is another str'] self.assertListEqual(content, padder(content, None, np.str).tolist()) @@ -194,7 +194,7 @@ class TestPadder(unittest.TestCase): 测试EngChar2DPadder能不能正确使用 :return: """ - from fastNLP.core.fieldarray import EngChar2DPadder + from fastNLP import EngChar2DPadder padder = EngChar2DPadder(pad_length=0) contents = [1, 2] @@ -225,11 +225,11 @@ class TestPadder(unittest.TestCase): ) def test_None_dtype(self): - from fastNLP.core.fieldarray import AutoPadder + from fastNLP import AutoPadder padder = AutoPadder() content = [ [[1, 2, 3], [4, 5], [7, 8, 9, 10]], [[1]] ] - ans = padder(content, None, None) + ans = padder(content, None, None).tolist() self.assertListEqual(content, ans) diff --git a/test/core/test_instance.py b/test/core/test_instance.py index 1342ba2c..207b44e9 100644 --- a/test/core/test_instance.py +++ b/test/core/test_instance.py @@ -1,33 +1,33 @@ import unittest -from fastNLP.core.instance import Instance +from fastNLP import Instance class TestCase(unittest.TestCase): - + def test_init(self): fields = {"x": [1, 2, 3], "y": [4, 5, 6]} ins = Instance(x=[1, 2, 3], y=[4, 5, 6]) self.assertTrue(isinstance(ins.fields, dict)) self.assertEqual(ins.fields, fields) - + ins = Instance(**fields) self.assertEqual(ins.fields, fields) - + def test_add_field(self): fields = {"x": [1, 2, 3], "y": [4, 5, 6]} ins = Instance(**fields) ins.add_field("z", [1, 1, 1]) fields.update({"z": [1, 1, 1]}) self.assertEqual(ins.fields, fields) - + def test_get_item(self): fields = {"x": [1, 2, 3], "y": [4, 5, 6], "z": [1, 1, 1]} ins = Instance(**fields) self.assertEqual(ins["x"], [1, 2, 3]) self.assertEqual(ins["y"], [4, 5, 6]) self.assertEqual(ins["z"], [1, 1, 1]) - + def test_repr(self): fields = {"x": [1, 2, 3], "y": [4, 5, 6], "z": [1, 1, 1]} ins = Instance(**fields) diff --git a/test/core/test_loss.py b/test/core/test_loss.py index a6d542fa..8db54615 100644 --- a/test/core/test_loss.py +++ b/test/core/test_loss.py @@ -3,7 +3,7 @@ import unittest import torch import torch.nn.functional as F -import fastNLP.core.losses as loss +import fastNLP as loss from fastNLP.core.losses import squash, unpad @@ -14,21 +14,21 @@ class TestLoss(unittest.TestCase): b = torch.empty(3, dtype=torch.long).random_(5) ans = ce({"my_predict": a}, {"my_truth": b}) self.assertEqual(ans, torch.nn.functional.cross_entropy(a, b)) - + def test_BCELoss(self): bce = loss.BCELoss(pred="my_predict", target="my_truth") a = torch.sigmoid(torch.randn((3, 5), requires_grad=False)) b = torch.randn((3, 5), requires_grad=False) ans = bce({"my_predict": a}, {"my_truth": b}) self.assertEqual(ans, torch.nn.functional.binary_cross_entropy(a, b)) - + def test_L1Loss(self): l1 = loss.L1Loss(pred="my_predict", target="my_truth") a = torch.randn(3, 5, requires_grad=False) b = torch.randn(3, 5) ans = l1({"my_predict": a}, {"my_truth": b}) self.assertEqual(ans, torch.nn.functional.l1_loss(a, b)) - + def test_NLLLoss(self): l1 = loss.NLLLoss(pred="my_predict", target="my_truth") a = F.log_softmax(torch.randn(3, 5, requires_grad=False), dim=0) @@ -43,34 +43,34 @@ class TestLosserError(unittest.TestCase): pred_dict = {"pred": torch.zeros(4, 3)} target_dict = {'target': torch.zeros(4).long()} los = loss.CrossEntropyLoss() - + print(los(pred_dict=pred_dict, target_dict=target_dict)) - + # def test_losser2(self): # (2) with corrupted size pred_dict = {"pred": torch.zeros(16, 3)} target_dict = {'target': torch.zeros(16, 3).long()} los = loss.CrossEntropyLoss() - + with self.assertRaises(RuntimeError): print(los(pred_dict=pred_dict, target_dict=target_dict)) - + def test_losser3(self): # (2) with corrupted size pred_dict = {"pred": torch.zeros(16, 3), 'stop_fast_param': 0} target_dict = {'target': torch.zeros(16).long()} los = loss.CrossEntropyLoss() - + print(los(pred_dict=pred_dict, target_dict=target_dict)) - + def test_check_error(self): l1 = loss.NLLLoss(pred="my_predict", target="my_truth") a = F.log_softmax(torch.randn(3, 5, requires_grad=False), dim=0) b = torch.tensor([1, 0, 4]) with self.assertRaises(Exception): ans = l1({"wrong_predict": a, "my": b}, {"my_truth": b}) - + with self.assertRaises(Exception): ans = l1({"my_predict": a}, {"truth": b, "my": a}) @@ -80,7 +80,7 @@ class TestLossUtils(unittest.TestCase): a, b = squash(torch.randn(3, 5), torch.randn(3, 5)) self.assertEqual(tuple(a.size()), (3, 5)) self.assertEqual(tuple(b.size()), (15,)) - + def test_unpad(self): a, b = unpad(torch.randn(5, 8, 3), torch.randn(5, 8)) self.assertEqual(tuple(a.size()), (5, 8, 3)) diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index d4422ec4..babfb4aa 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -3,8 +3,8 @@ import unittest import numpy as np import torch -from fastNLP.core.metrics import AccuracyMetric -from fastNLP.core.metrics import BMESF1PreRecMetric +from fastNLP import AccuracyMetric +from fastNLP import BMESF1PreRecMetric from fastNLP.core.metrics import _pred_topk, _accuracy_topk @@ -14,24 +14,24 @@ class TestAccuracyMetric(unittest.TestCase): pred_dict = {"pred": torch.zeros(4, 3)} target_dict = {'target': torch.zeros(4)} metric = AccuracyMetric() - + metric(pred_dict=pred_dict, target_dict=target_dict) print(metric.get_metric()) - + def test_AccuracyMetric2(self): # (2) with corrupted size try: pred_dict = {"pred": torch.zeros(4, 3, 2)} target_dict = {'target': torch.zeros(4)} metric = AccuracyMetric() - + metric(pred_dict=pred_dict, target_dict=target_dict, ) print(metric.get_metric()) except Exception as e: print(e) return print("No exception catches.") - + def test_AccuracyMetric3(self): # (3) the second batch is corrupted size try: @@ -39,17 +39,17 @@ class TestAccuracyMetric(unittest.TestCase): pred_dict = {"pred": torch.zeros(4, 3, 2)} target_dict = {'target': torch.zeros(4, 3)} metric(pred_dict=pred_dict, target_dict=target_dict) - + pred_dict = {"pred": torch.zeros(4, 3, 2)} target_dict = {'target': torch.zeros(4)} metric(pred_dict=pred_dict, target_dict=target_dict) - + print(metric.get_metric()) except Exception as e: print(e) return self.assertTrue(True, False), "No exception catches." - + def test_AccuaryMetric4(self): # (5) check reset metric = AccuracyMetric() @@ -61,7 +61,7 @@ class TestAccuracyMetric(unittest.TestCase): self.assertTrue(isinstance(res, dict)) self.assertTrue("acc" in res) self.assertAlmostEqual(res["acc"], float(ans.float().mean()), places=3) - + def test_AccuaryMetric5(self): # (5) check reset metric = AccuracyMetric() @@ -71,7 +71,7 @@ class TestAccuracyMetric(unittest.TestCase): res = metric.get_metric(reset=False) ans = (torch.argmax(pred_dict["pred"], dim=2).float() == target_dict["target"]).float().mean() self.assertAlmostEqual(res["acc"], float(ans), places=4) - + def test_AccuaryMetric6(self): # (6) check numpy array is not acceptable try: @@ -83,7 +83,7 @@ class TestAccuracyMetric(unittest.TestCase): print(e) return self.assertTrue(True, False), "No exception catches." - + def test_AccuaryMetric7(self): # (7) check map, match metric = AccuracyMetric(pred='predictions', target='targets') @@ -93,7 +93,7 @@ class TestAccuracyMetric(unittest.TestCase): res = metric.get_metric() ans = (torch.argmax(pred_dict["predictions"], dim=2).float() == target_dict["targets"]).float().mean() self.assertAlmostEqual(res["acc"], float(ans), places=4) - + def test_AccuaryMetric8(self): try: metric = AccuracyMetric(pred='predictions', target='targets') @@ -105,7 +105,7 @@ class TestAccuracyMetric(unittest.TestCase): print(e) return self.assertTrue(True, False), "No exception catches." - + def test_AccuaryMetric9(self): # (9) check map, include unused try: @@ -118,12 +118,12 @@ class TestAccuracyMetric(unittest.TestCase): print(e) return self.assertTrue(True, False), "No exception catches." - + def test_AccuaryMetric10(self): # (10) check _fast_metric try: metric = AccuracyMetric() - pred_dict = {"predictions": torch.zeros(4, 3, 2), "seq_len": torch.ones(3)*3} + pred_dict = {"predictions": torch.zeros(4, 3, 2), "seq_len": torch.ones(3) * 3} target_dict = {'targets': torch.zeros(4, 3)} metric(pred_dict=pred_dict, target_dict=target_dict) self.assertDictEqual(metric.get_metric(), {'acc': 1}) @@ -131,7 +131,7 @@ class TestAccuracyMetric(unittest.TestCase): print(e) return self.assertTrue(True, False), "No exception catches." - + def test_seq_len(self): N = 256 seq_len = torch.zeros(N).long() @@ -145,20 +145,21 @@ class TestAccuracyMetric(unittest.TestCase): metric(pred_dict=pred, target_dict=target) self.assertDictEqual(metric.get_metric(), {'acc': 1.}) + class SpanF1PreRecMetric(unittest.TestCase): def test_case1(self): from fastNLP.core.metrics import _bmes_tag_to_spans from fastNLP.core.metrics import _bio_tag_to_spans - + bmes_lst = ['M-8', 'S-2', 'S-0', 'B-9', 'B-6', 'E-5', 'B-7', 'S-2', 'E-7', 'S-8'] bio_lst = ['O-8', 'O-2', 'B-0', 'O-9', 'I-6', 'I-5', 'I-7', 'I-2', 'I-7', 'O-8'] expect_bmes_res = set() expect_bmes_res.update([('8', (0, 1)), ('2', (1, 2)), ('0', (2, 3)), ('9', (3, 4)), ('6', (4, 5)), - ('5', (5, 6)), ('7', (6, 7)), ('2', (7, 8)), ('7', (8, 9)), ('8', (9, 10))]) + ('5', (5, 6)), ('7', (6, 7)), ('2', (7, 8)), ('7', (8, 9)), ('8', (9, 10))]) expect_bio_res = set() expect_bio_res.update([('7', (8, 9)), ('0', (2, 3)), ('2', (7, 8)), ('5', (5, 6)), - ('6', (4, 5)), ('7', (6, 7))]) - self.assertSetEqual(expect_bmes_res,set(_bmes_tag_to_spans(bmes_lst))) + ('6', (4, 5)), ('7', (6, 7))]) + self.assertSetEqual(expect_bmes_res, set(_bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans @@ -171,19 +172,19 @@ class SpanF1PreRecMetric(unittest.TestCase): # bio_strs = [str_ + '-' + tag for tag, str_ in zip(strs, np.random.choice(bio, size=len(strs)))] # self.assertSetEqual(set(allen_bmes_tags_to_spans(bmes_strs)),set(bmes_tag_to_spans(bmes_strs))) # self.assertSetEqual(set(allen_bio_tags_to_spans(bio_strs)), set(bio_tag_to_spans(bio_strs))) - + def test_case2(self): # 测试不带label的 from fastNLP.core.metrics import _bmes_tag_to_spans from fastNLP.core.metrics import _bio_tag_to_spans - + bmes_lst = ['B', 'E', 'B', 'S', 'B', 'M', 'E', 'M', 'B', 'E'] bio_lst = ['I', 'B', 'O', 'O', 'I', 'O', 'I', 'B', 'O', 'O'] expect_bmes_res = set() expect_bmes_res.update([('', (0, 2)), ('', (2, 3)), ('', (3, 4)), ('', (4, 7)), ('', (7, 8)), ('', (8, 10))]) expect_bio_res = set() expect_bio_res.update([('', (7, 8)), ('', (6, 7)), ('', (4, 5)), ('', (0, 1)), ('', (1, 2))]) - self.assertSetEqual(expect_bmes_res,set(_bmes_tag_to_spans(bmes_lst))) + self.assertSetEqual(expect_bmes_res, set(_bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans @@ -195,7 +196,7 @@ class SpanF1PreRecMetric(unittest.TestCase): # bio_strs = np.random.choice(bio, size=100) # self.assertSetEqual(set(allen_bmes_tags_to_spans(bmes_strs)),set(bmes_tag_to_spans(bmes_strs))) # self.assertSetEqual(set(allen_bio_tags_to_spans(bio_strs)), set(bio_tag_to_spans(bio_strs))) - + def tese_case3(self): from fastNLP.core.vocabulary import Vocabulary from collections import Counter @@ -213,7 +214,7 @@ class SpanF1PreRecMetric(unittest.TestCase): continue vocab['{}-{}'.format(tag, label)] = len(vocab) + 1 # 其实表达的是这个的count return vocab - + number_labels = 4 # bio tag fastnlp_bio_vocab = Vocabulary(unknown=None, padding=None) @@ -221,26 +222,26 @@ class SpanF1PreRecMetric(unittest.TestCase): fastnlp_bio_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bio_vocab, only_gross=False) bio_sequence = torch.FloatTensor( [[[-0.9543, -1.4357, -0.2365, 0.2438, 1.0312, -1.4302, 0.3011, - 0.0470, 0.0971], - [-0.6638, -0.7116, -1.9804, 0.2787, -0.2732, -0.9501, -1.4523, - 0.7987, -0.3970], - [0.2939, 0.8132, -0.0903, -2.8296, 0.2080, -0.9823, -0.1898, - 0.6880, 1.4348], - [-0.1886, 0.0067, -0.6862, -0.4635, 2.2776, 0.0710, -1.6793, - -1.6876, -0.8917], - [-0.7663, 0.6377, 0.8669, 0.1237, 1.7628, 0.0313, -1.0824, - 1.4217, 0.2622]], - - [[0.1529, 0.7474, -0.9037, 1.5287, 0.2771, 0.2223, 0.8136, - 1.3592, -0.8973], - [0.4515, -0.5235, 0.3265, -1.1947, 0.8308, 1.8754, -0.4887, - -0.4025, -0.3417], - [-0.7855, 0.1615, -0.1272, -1.9289, -0.5181, 1.9742, -0.9698, - 0.2861, -0.3966], - [-0.8291, -0.8823, -1.1496, 0.2164, 1.3390, -0.3964, -0.5275, - 0.0213, 1.4777], - [-1.1299, 0.0627, -0.1358, -1.5951, 0.4484, -0.6081, -1.9566, - 1.3024, 0.2001]]] + 0.0470, 0.0971], + [-0.6638, -0.7116, -1.9804, 0.2787, -0.2732, -0.9501, -1.4523, + 0.7987, -0.3970], + [0.2939, 0.8132, -0.0903, -2.8296, 0.2080, -0.9823, -0.1898, + 0.6880, 1.4348], + [-0.1886, 0.0067, -0.6862, -0.4635, 2.2776, 0.0710, -1.6793, + -1.6876, -0.8917], + [-0.7663, 0.6377, 0.8669, 0.1237, 1.7628, 0.0313, -1.0824, + 1.4217, 0.2622]], + + [[0.1529, 0.7474, -0.9037, 1.5287, 0.2771, 0.2223, 0.8136, + 1.3592, -0.8973], + [0.4515, -0.5235, 0.3265, -1.1947, 0.8308, 1.8754, -0.4887, + -0.4025, -0.3417], + [-0.7855, 0.1615, -0.1272, -1.9289, -0.5181, 1.9742, -0.9698, + 0.2861, -0.3966], + [-0.8291, -0.8823, -1.1496, 0.2164, 1.3390, -0.3964, -0.5275, + 0.0213, 1.4777], + [-1.1299, 0.0627, -0.1358, -1.5951, 0.4484, -0.6081, -1.9566, + 1.3024, 0.2001]]] ) bio_target = torch.LongTensor([[5., 0., 3., 3., 3.], [5., 6., 8., 6., 0.]]) @@ -250,8 +251,8 @@ class SpanF1PreRecMetric(unittest.TestCase): 'rec-0': 0.0, 'f-0': 0.0, 'pre': 0.12499999999999845, 'rec': 0.12499999999999845, 'f': 0.12499999999994846} self.assertDictEqual(expect_bio_res, fastnlp_bio_metric.get_metric()) - - #bmes tag + + # bmes tag bmes_sequence = torch.FloatTensor( [[[0.6536, -0.7179, 0.6579, 1.2503, 0.4176, 0.6696, 0.2352, -0.4085, 0.4084, -0.4185, 1.4172, -0.9162, -0.2679, 0.3332, @@ -268,7 +269,7 @@ class SpanF1PreRecMetric(unittest.TestCase): [0.9088, -0.4955, -0.5076, 0.3732, 0.0283, -0.0263, -1.0393, 0.7734, 1.0968, 0.4132, -1.3647, -0.5762, 0.6678, 0.8809, -0.3779, -0.3195]], - + [[-0.4638, -0.5939, -0.1052, -0.5573, 0.4600, -1.3484, 0.1753, 0.0685, 0.3663, -0.6789, 0.0097, 1.0327, -0.0212, -0.9957, -0.1103, 0.4417], @@ -285,22 +286,22 @@ class SpanF1PreRecMetric(unittest.TestCase): 2.6973, -0.8308, -1.4939, 0.9865, -0.3935, 0.2743, 0.1142, -0.7344, -1.2046]]] ) - bmes_target = torch.LongTensor([[ 9., 6., 1., 9., 15.], - [ 6., 15., 6., 15., 5.]]) - + bmes_target = torch.LongTensor([[9., 6., 1., 9., 15.], + [6., 15., 6., 15., 5.]]) + fastnlp_bmes_vocab = Vocabulary(unknown=None, padding=None) fastnlp_bmes_vocab.word_count = Counter(generate_allen_tags('BMES', number_labels)) fastnlp_bmes_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bmes_vocab, only_gross=False, encoding_type='bmes') fastnlp_bmes_metric({'pred': bmes_sequence, 'seq_lens': torch.LongTensor([20, 20])}, {'target': bmes_target}) - + expect_bmes_res = {'f-3': 0.6666666666665778, 'pre-3': 0.499999999999975, 'rec-3': 0.9999999999999001, 'f-0': 0.0, 'pre-0': 0.0, 'rec-0': 0.0, 'f-1': 0.33333333333327775, 'pre-1': 0.24999999999999373, 'rec-1': 0.499999999999975, 'f-2': 0.7499999999999314, 'pre-2': 0.7499999999999812, 'rec-2': 0.7499999999999812, 'f': 0.49999999999994504, 'pre': 0.499999999999995, 'rec': 0.499999999999995} - + self.assertDictEqual(fastnlp_bmes_metric.get_metric(), expect_bmes_res) - + # 已经和allennlp做过验证,但由于不能依赖allennlp,所以注释了以下代码 # from allennlp.data.vocabulary import Vocabulary as allen_Vocabulary # from allennlp.training.metrics import SpanBasedF1Measure @@ -349,6 +350,7 @@ class SpanF1PreRecMetric(unittest.TestCase): # self.assertDictEqual(convert_allen_res_to_fastnlp_res(allen_bmes_metric.get_metric()), # fastnlp_bmes_metric.get_metric()) + class TestBMESF1PreRecMetric(unittest.TestCase): def test_case1(self): seq_lens = torch.LongTensor([4, 2]) @@ -356,20 +358,20 @@ class TestBMESF1PreRecMetric(unittest.TestCase): target = torch.LongTensor([[0, 1, 2, 3], [3, 3, 0, 0]]) pred_dict = {'pred': pred} - target_dict = {'target': target, 'seq_lens': seq_lens} - + target_dict = {'target': target, 'seq_len': seq_lens} + metric = BMESF1PreRecMetric() metric(pred_dict, target_dict) metric.get_metric() - + def test_case2(self): # 测试相同两个seqence,应该给出{f1: 1, precision:1, recall:1} seq_lens = torch.LongTensor([4, 2]) target = torch.LongTensor([[0, 1, 2, 3], [3, 3, 0, 0]]) pred_dict = {'pred': target} - target_dict = {'target': target, 'seq_lens': seq_lens} - + target_dict = {'target': target, 'seq_len': seq_lens} + metric = BMESF1PreRecMetric() metric(pred_dict, target_dict) self.assertDictEqual(metric.get_metric(), {'f': 1.0, 'pre': 1.0, 'rec': 1.0}) @@ -381,5 +383,5 @@ class TestUsefulFunctions(unittest.TestCase): # multi-class _ = _accuracy_topk(np.random.randint(0, 3, size=(10, 1)), np.random.randint(0, 3, size=(10, 1)), k=3) _ = _pred_topk(np.random.randint(0, 3, size=(10, 1))) - + # 跑通即可 diff --git a/test/core/test_optimizer.py b/test/core/test_optimizer.py index 83ed6000..b9a1c271 100644 --- a/test/core/test_optimizer.py +++ b/test/core/test_optimizer.py @@ -2,7 +2,7 @@ import unittest import torch -from fastNLP.core.optimizer import SGD, Adam +from fastNLP import SGD, Adam class TestOptim(unittest.TestCase): @@ -12,42 +12,42 @@ class TestOptim(unittest.TestCase): self.assertTrue("momentum" in optim.__dict__["settings"]) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) self.assertTrue(isinstance(res, torch.optim.SGD)) - + optim = SGD(lr=0.001) self.assertEqual(optim.__dict__["settings"]["lr"], 0.001) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) self.assertTrue(isinstance(res, torch.optim.SGD)) - + optim = SGD(lr=0.002, momentum=0.989) self.assertEqual(optim.__dict__["settings"]["lr"], 0.002) self.assertEqual(optim.__dict__["settings"]["momentum"], 0.989) - + optim = SGD(0.001) self.assertEqual(optim.__dict__["settings"]["lr"], 0.001) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) self.assertTrue(isinstance(res, torch.optim.SGD)) - + with self.assertRaises(TypeError): _ = SGD("???") with self.assertRaises(TypeError): _ = SGD(0.001, lr=0.002) - + def test_Adam(self): optim = Adam(model_params=torch.nn.Linear(10, 3).parameters()) self.assertTrue("lr" in optim.__dict__["settings"]) self.assertTrue("weight_decay" in optim.__dict__["settings"]) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) self.assertTrue(isinstance(res, torch.optim.Adam)) - + optim = Adam(lr=0.001) self.assertEqual(optim.__dict__["settings"]["lr"], 0.001) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) self.assertTrue(isinstance(res, torch.optim.Adam)) - + optim = Adam(lr=0.002, weight_decay=0.989) self.assertEqual(optim.__dict__["settings"]["lr"], 0.002) self.assertEqual(optim.__dict__["settings"]["weight_decay"], 0.989) - + optim = Adam(0.001) self.assertEqual(optim.__dict__["settings"]["lr"], 0.001) res = optim.construct_from_pytorch(torch.nn.Linear(10, 3).parameters()) diff --git a/test/core/test_sampler.py b/test/core/test_sampler.py index ba43a973..703a9428 100644 --- a/test/core/test_sampler.py +++ b/test/core/test_sampler.py @@ -3,9 +3,9 @@ import unittest import torch -from fastNLP.core.dataset import DataSet -from fastNLP.core.sampler import SequentialSampler, RandomSampler, \ - k_means_1d, k_means_bucketing, simple_sort_bucketing, BucketSampler +from fastNLP import DataSet +from fastNLP import SequentialSampler, RandomSampler, BucketSampler +from fastNLP.core.sampler import k_means_1d, k_means_bucketing, simple_sort_bucketing class TestSampler(unittest.TestCase): diff --git a/test/core/test_tester.py b/test/core/test_tester.py index d78377aa..d0267cce 100644 --- a/test/core/test_tester.py +++ b/test/core/test_tester.py @@ -1,32 +1,25 @@ import unittest +import numpy as np +from torch import nn +import time +from fastNLP import DataSet +from fastNLP import Instance +from fastNLP import AccuracyMetric +from fastNLP import Tester data_name = "pku_training.utf8" pickle_path = "data_for_tests" -import numpy as np -import torch.nn.functional as F -from torch import nn -import time -from fastNLP.core.utils import _CheckError -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance -from fastNLP.core.losses import BCELoss -from fastNLP.core.losses import CrossEntropyLoss -from fastNLP.core.metrics import AccuracyMetric -from fastNLP.core.optimizer import SGD -from fastNLP.core.tester import Tester -from fastNLP.models.base_model import NaiveClassifier - def prepare_fake_dataset(): mean = np.array([-3, -3]) cov = np.array([[1, 0], [0, 1]]) class_A = np.random.multivariate_normal(mean, cov, size=(1000,)) - + mean = np.array([3, 3]) cov = np.array([[1, 0], [0, 1]]) class_B = np.random.multivariate_normal(mean, cov, size=(1000,)) - + data_set = DataSet([Instance(x=[float(item[0]), float(item[1])], y=[0.0]) for item in class_A] + [Instance(x=[float(item[0]), float(item[1])], y=[1.0]) for item in class_B]) return data_set @@ -39,6 +32,7 @@ def prepare_fake_dataset2(*args, size=100): data[arg] = np.random.randn(size, 5) return DataSet(data=data) + class TestTester(unittest.TestCase): def test_case_1(self): # 检查报错提示能否正确提醒用户 @@ -46,10 +40,12 @@ class TestTester(unittest.TestCase): dataset.rename_field('x_unused', 'x2') dataset.set_input('x1', 'x2') dataset.set_target('y', 'x1') + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) + def forward(self, x1, x2): x1 = self.fc(x1) x2 = self.fc(x2) @@ -57,7 +53,7 @@ class TestTester(unittest.TestCase): time.sleep(0.1) # loss = F.cross_entropy(x, y) return {'preds': x} - + model = Model() with self.assertRaises(NameError): tester = Tester( diff --git a/test/core/test_trainer.py b/test/core/test_trainer.py index 36062ef7..f559eac5 100644 --- a/test/core/test_trainer.py +++ b/test/core/test_trainer.py @@ -5,25 +5,24 @@ import numpy as np import torch.nn.functional as F from torch import nn -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance -from fastNLP.core.losses import BCELoss -from fastNLP.core.losses import CrossEntropyLoss -from fastNLP.core.metrics import AccuracyMetric -from fastNLP.core.optimizer import SGD -from fastNLP.core.trainer import Trainer +from fastNLP import DataSet +from fastNLP import Instance +from fastNLP import BCELoss +from fastNLP import CrossEntropyLoss +from fastNLP import AccuracyMetric +from fastNLP import SGD +from fastNLP import Trainer from fastNLP.models.base_model import NaiveClassifier - def prepare_fake_dataset(): mean = np.array([-3, -3]) cov = np.array([[1, 0], [0, 1]]) class_A = np.random.multivariate_normal(mean, cov, size=(1000,)) - + mean = np.array([3, 3]) cov = np.array([[1, 0], [0, 1]]) class_B = np.random.multivariate_normal(mean, cov, size=(1000,)) - + data_set = DataSet([Instance(x=[float(item[0]), float(item[1])], y=[0.0]) for item in class_A] + [Instance(x=[float(item[0]), float(item[1])], y=[1.0]) for item in class_B]) return data_set @@ -42,11 +41,11 @@ class TrainerTestGround(unittest.TestCase): data_set = prepare_fake_dataset() data_set.set_input("x", flag=True) data_set.set_target("y", flag=True) - + train_set, dev_set = data_set.split(0.3) - + model = NaiveClassifier(2, 1) - + trainer = Trainer(train_set, model, loss=BCELoss(pred="predict", target="y"), metrics=AccuracyMetric(pred="predict", target="y"), @@ -63,26 +62,26 @@ class TrainerTestGround(unittest.TestCase): """ # 应该正确运行 """ - + def test_trainer_suggestion1(self): # 检查报错提示能否正确提醒用户。 # 这里没有传入forward需要的数据。需要trainer提醒用户如何设置。 dataset = prepare_fake_dataset2('x') - + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) - + def forward(self, x1, x2, y): x1 = self.fc(x1) x2 = self.fc(x2) x = x1 + x2 loss = F.cross_entropy(x, y) return {'loss': loss} - + model = Model() - + with self.assertRaises(RuntimeError): trainer = Trainer( train_data=dataset, @@ -97,25 +96,25 @@ class TrainerTestGround(unittest.TestCase): (2). You need to provide ['x1', 'x2'] in DataSet and set it as input. """ - + def test_trainer_suggestion2(self): # 检查报错提示能否正确提醒用户 # 这里传入forward需要的数据,看是否可以运行 dataset = prepare_fake_dataset2('x1', 'x2') dataset.set_input('x1', 'x2', 'y', flag=True) - + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) - + def forward(self, x1, x2, y): x1 = self.fc(x1) x2 = self.fc(x2) x = x1 + x2 loss = F.cross_entropy(x, y) return {'loss': loss} - + model = Model() trainer = Trainer( train_data=dataset, @@ -127,25 +126,25 @@ class TrainerTestGround(unittest.TestCase): """ # 应该正确运行 """ - + def test_trainer_suggestion3(self): # 检查报错提示能否正确提醒用户 # 这里传入forward需要的数据,但是forward没有返回loss这个key dataset = prepare_fake_dataset2('x1', 'x2') dataset.set_input('x1', 'x2', 'y', flag=True) - + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) - + def forward(self, x1, x2, y): x1 = self.fc(x1) x2 = self.fc(x2) x = x1 + x2 loss = F.cross_entropy(x, y) return {'wrong_loss_key': loss} - + model = Model() with self.assertRaises(NameError): trainer = Trainer( @@ -155,23 +154,25 @@ class TrainerTestGround(unittest.TestCase): print_every=2 ) trainer.train() - + def test_trainer_suggestion4(self): # 检查报错提示能否正确提醒用户 # 这里传入forward需要的数据,是否可以正确提示unused dataset = prepare_fake_dataset2('x1', 'x2') dataset.set_input('x1', 'x2', 'y', flag=True) + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) + def forward(self, x1, x2, y): x1 = self.fc(x1) x2 = self.fc(x2) x = x1 + x2 loss = F.cross_entropy(x, y) return {'losses': loss} - + model = Model() with self.assertRaises(NameError): trainer = Trainer( @@ -180,7 +181,7 @@ class TrainerTestGround(unittest.TestCase): use_tqdm=False, print_every=2 ) - + def test_trainer_suggestion5(self): # 检查报错提示能否正确提醒用户 # 这里传入多余参数,让其duplicate, 但这里因为y不会被调用,所以其实不会报错 @@ -188,17 +189,19 @@ class TrainerTestGround(unittest.TestCase): dataset.rename_field('x_unused', 'x2') dataset.set_input('x1', 'x2', 'y') dataset.set_target('y') + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) + def forward(self, x1, x2, y): x1 = self.fc(x1) x2 = self.fc(x2) x = x1 + x2 loss = F.cross_entropy(x, y) return {'loss': loss} - + model = Model() trainer = Trainer( train_data=dataset, @@ -206,7 +209,7 @@ class TrainerTestGround(unittest.TestCase): use_tqdm=False, print_every=2 ) - + def test_trainer_suggestion6(self): # 检查报错提示能否正确提醒用户 # 这里传入多余参数,让其duplicate @@ -214,10 +217,12 @@ class TrainerTestGround(unittest.TestCase): dataset.rename_field('x_unused', 'x2') dataset.set_input('x1', 'x2') dataset.set_target('y', 'x1') + class Model(nn.Module): def __init__(self): super().__init__() self.fc = nn.Linear(5, 4) + def forward(self, x1, x2): x1 = self.fc(x1) x2 = self.fc(x2) @@ -225,7 +230,7 @@ class TrainerTestGround(unittest.TestCase): time.sleep(0.1) # loss = F.cross_entropy(x, y) return {'preds': x} - + model = Model() with self.assertRaises(NameError): trainer = Trainer( @@ -236,7 +241,7 @@ class TrainerTestGround(unittest.TestCase): metrics=AccuracyMetric(), use_tqdm=False, print_every=2) - + """ def test_trainer_multiprocess(self): dataset = prepare_fake_dataset2('x1', 'x2') diff --git a/test/core/test_utils.py b/test/core/test_utils.py index d676ceed..7f218db0 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -1,8 +1,7 @@ - import unittest import _pickle from fastNLP import cache_results -from fastNLP.io.embed_loader import EmbedLoader +from fastNLP.io import EmbedLoader from fastNLP import DataSet from fastNLP import Instance import time @@ -11,11 +10,13 @@ import torch from torch import nn from fastNLP.core.utils import _move_model_to_device, _get_model_device + class Model(nn.Module): def __init__(self): super().__init__() self.param = nn.Parameter(torch.zeros(0)) + class TestMoveModelDeivce(unittest.TestCase): def test_case1(self): # 测试str @@ -35,36 +36,36 @@ class TestMoveModelDeivce(unittest.TestCase): _move_model_to_device(model, 'cuda:1000') # 测试None model = _move_model_to_device(model, None) - + def test_case2(self): # 测试使用int初始化 model = Model() if torch.cuda.is_available(): model = _move_model_to_device(model, 0) assert model.param.device == torch.device('cuda:0') - assert model.param.device==torch.device('cuda:0'), "The model should be in " + assert model.param.device == torch.device('cuda:0'), "The model should be in " with self.assertRaises(Exception): _move_model_to_device(model, 100) with self.assertRaises(Exception): _move_model_to_device(model, -1) - + def test_case3(self): # 测试None model = Model() device = _get_model_device(model) model = _move_model_to_device(model, None) - assert device==_get_model_device(model), "The device should not change." + assert device == _get_model_device(model), "The device should not change." if torch.cuda.is_available(): model.cuda() device = _get_model_device(model) model = _move_model_to_device(model, None) - assert device==_get_model_device(model), "The device should not change." - + assert device == _get_model_device(model), "The device should not change." + model = nn.DataParallel(model, device_ids=[0]) _move_model_to_device(model, None) with self.assertRaises(Exception): _move_model_to_device(model, 'cpu') - + def test_case4(self): # 测试传入list的内容 model = Model() @@ -78,15 +79,17 @@ class TestMoveModelDeivce(unittest.TestCase): device = [torch.device('cuda:0'), torch.device('cuda:0')] with self.assertRaises(Exception): _model = _move_model_to_device(model, device) - if torch.cuda.device_count()>1: + if torch.cuda.device_count() > 1: device = [0, 1] _model = _move_model_to_device(model, device) assert isinstance(_model, nn.DataParallel) device = ['cuda', 'cuda:1'] with self.assertRaises(Exception): _move_model_to_device(model, device) - + def test_case5(self): + if not torch.cuda.is_available(): + return # torch.device() device = torch.device('cpu') model = Model() @@ -106,10 +109,11 @@ def process_data_1(embed_file, cws_train): d = DataSet() for line in f: line = line.strip() - if len(line)>0: + if len(line) > 0: d.append(Instance(raw=line)) return embed, vocab, d + class TestCache(unittest.TestCase): def test_cache_save(self): try: @@ -127,10 +131,10 @@ class TestCache(unittest.TestCase): end_time = time.time() read_time = end_time - start_time print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) - self.assertGreater(pre_time-0.5, read_time) + self.assertGreater(pre_time - 0.5, read_time) finally: os.remove('test/demo1.pkl') - + def test_cache_save_overwrite_path(self): try: start_time = time.time() @@ -149,10 +153,10 @@ class TestCache(unittest.TestCase): end_time = time.time() read_time = end_time - start_time print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) - self.assertGreater(pre_time-0.5, read_time) + self.assertGreater(pre_time - 0.5, read_time) finally: os.remove('test/demo_overwrite.pkl') - + def test_cache_refresh(self): try: start_time = time.time() @@ -171,34 +175,38 @@ class TestCache(unittest.TestCase): end_time = time.time() read_time = end_time - start_time print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) - self.assertGreater(0.1, pre_time-read_time) + self.assertGreater(0.1, pre_time - read_time) finally: os.remove('test/demo1.pkl') - + def test_duplicate_keyword(self): with self.assertRaises(RuntimeError): @cache_results(None) def func_verbose(a, _verbose): pass + func_verbose(0, 1) with self.assertRaises(RuntimeError): @cache_results(None) def func_cache(a, _cache_fp): pass + func_cache(1, 2) with self.assertRaises(RuntimeError): @cache_results(None) def func_refresh(a, _refresh): pass + func_refresh(1, 2) - + def test_create_cache_dir(self): @cache_results('test/demo1/demo.pkl') def cache(): return 1, 2 + try: results = cache() print(results) finally: os.remove('test/demo1/demo.pkl') - os.rmdir('test/demo1') \ No newline at end of file + os.rmdir('test/demo1') diff --git a/test/core/test_vocabulary.py b/test/core/test_vocabulary.py index 0f13b935..df0ebb1a 100644 --- a/test/core/test_vocabulary.py +++ b/test/core/test_vocabulary.py @@ -1,9 +1,9 @@ import unittest from collections import Counter -from fastNLP.core.vocabulary import Vocabulary -from fastNLP.core.dataset import DataSet -from fastNLP.core.instance import Instance +from fastNLP import Vocabulary +from fastNLP import DataSet +from fastNLP import Instance text = ["FastNLP", "works", "well", "in", "most", "cases", "and", "scales", "well", "in", "works", "well", "in", "most", "cases", "scales", "well"] @@ -12,92 +12,93 @@ counter = Counter(text) class TestAdd(unittest.TestCase): def test_add(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() for word in text: vocab.add(word) self.assertEqual(vocab.word_count, counter) - + def test_add_word(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() for word in text: vocab.add_word(word) self.assertEqual(vocab.word_count, counter) - + def test_add_word_lst(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() vocab.add_word_lst(text) self.assertEqual(vocab.word_count, counter) - + def test_update(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() vocab.update(text) self.assertEqual(vocab.word_count, counter) - + def test_from_dataset(self): start_char = 65 num_samples = 10 - + # 0 dim dataset = DataSet() for i in range(num_samples): - ins = Instance(char=chr(start_char+i)) + ins = Instance(char=chr(start_char + i)) dataset.append(ins) vocab = Vocabulary() vocab.from_dataset(dataset, field_name='char') for i in range(num_samples): - self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + self.assertEqual(vocab.to_index(chr(start_char + i)), i + 2) vocab.index_dataset(dataset, field_name='char') - + # 1 dim dataset = DataSet() for i in range(num_samples): - ins = Instance(char=[chr(start_char+i)]*6) + ins = Instance(char=[chr(start_char + i)] * 6) dataset.append(ins) vocab = Vocabulary() vocab.from_dataset(dataset, field_name='char') for i in range(num_samples): - self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + self.assertEqual(vocab.to_index(chr(start_char + i)), i + 2) vocab.index_dataset(dataset, field_name='char') - + # 2 dim dataset = DataSet() for i in range(num_samples): - ins = Instance(char=[[chr(start_char+i) for _ in range(6)] for _ in range(6)]) + ins = Instance(char=[[chr(start_char + i) for _ in range(6)] for _ in range(6)]) dataset.append(ins) vocab = Vocabulary() vocab.from_dataset(dataset, field_name='char') for i in range(num_samples): - self.assertEqual(vocab.to_index(chr(start_char+i)), i+2) + self.assertEqual(vocab.to_index(chr(start_char + i)), i + 2) vocab.index_dataset(dataset, field_name='char') + class TestIndexing(unittest.TestCase): def test_len(self): - vocab = Vocabulary(max_size=None, min_freq=None, unknown=None, padding=None) + vocab = Vocabulary(unknown=None, padding=None) vocab.update(text) self.assertEqual(len(vocab), len(counter)) - + def test_contains(self): - vocab = Vocabulary(max_size=None, min_freq=None, unknown=None, padding=None) + vocab = Vocabulary(unknown=None) vocab.update(text) self.assertTrue(text[-1] in vocab) self.assertFalse("~!@#" in vocab) self.assertEqual(text[-1] in vocab, vocab.has_word(text[-1])) self.assertEqual("~!@#" in vocab, vocab.has_word("~!@#")) - + def test_index(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() vocab.update(text) res = [vocab[w] for w in set(text)] self.assertEqual(len(res), len(set(res))) - + res = [vocab.to_index(w) for w in set(text)] self.assertEqual(len(res), len(set(res))) - + def test_to_word(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() vocab.update(text) self.assertEqual(text, [vocab.to_word(idx) for idx in [vocab[w] for w in text]]) - + def test_iteration(self): vocab = Vocabulary() text = ["FastNLP", "works", "well", "in", "most", "cases", "and", "scales", "well", "in", @@ -110,26 +111,26 @@ class TestIndexing(unittest.TestCase): class TestOther(unittest.TestCase): def test_additional_update(self): - vocab = Vocabulary(max_size=None, min_freq=None) + vocab = Vocabulary() vocab.update(text) - + _ = vocab["well"] self.assertEqual(vocab.rebuild, False) - + vocab.add("hahaha") self.assertEqual(vocab.rebuild, True) - + _ = vocab["hahaha"] self.assertEqual(vocab.rebuild, False) self.assertTrue("hahaha" in vocab) - + def test_warning(self): - vocab = Vocabulary(max_size=len(set(text)), min_freq=None) + vocab = Vocabulary(max_size=len(set(text))) vocab.update(text) self.assertEqual(vocab.rebuild, True) print(len(vocab)) self.assertEqual(vocab.rebuild, False) - + vocab.update(["hahahha", "hhh", "vvvv", "ass", "asss", "jfweiong", "eqgfeg", "feqfw"]) # this will print a warning self.assertEqual(vocab.rebuild, True) From 0860e0a81c42ac09fe8fe3b3e6f820bd23e5ef52 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 7 May 2019 14:03:19 +0800 Subject: [PATCH 088/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=86=97=E4=BD=99=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.models.char_language_model.rst | 7 ------- docs/source/fastNLP.models.rst | 1 - docs/source/fastNLP.modules.dropout.rst | 7 ------- docs/source/fastNLP.modules.encoder.linear.rst | 7 ------- docs/source/fastNLP.modules.encoder.rst | 1 - docs/source/fastNLP.modules.other_modules.rst | 7 ------- docs/source/fastNLP.modules.utils.rst | 7 ------- fastNLP/core/callback.py | 4 +++- 8 files changed, 3 insertions(+), 38 deletions(-) delete mode 100644 docs/source/fastNLP.models.char_language_model.rst delete mode 100644 docs/source/fastNLP.modules.dropout.rst delete mode 100644 docs/source/fastNLP.modules.encoder.linear.rst delete mode 100644 docs/source/fastNLP.modules.other_modules.rst delete mode 100644 docs/source/fastNLP.modules.utils.rst diff --git a/docs/source/fastNLP.models.char_language_model.rst b/docs/source/fastNLP.models.char_language_model.rst deleted file mode 100644 index 8cfc9e6e..00000000 --- a/docs/source/fastNLP.models.char_language_model.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.char\_language\_model module -=========================================== - -.. automodule:: fastNLP.models.char_language_model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index ae4abf7a..24fca203 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -14,7 +14,6 @@ Submodules fastNLP.models.base_model fastNLP.models.bert fastNLP.models.biaffine_parser - fastNLP.models.char_language_model fastNLP.models.cnn_text_classification fastNLP.models.enas_controller fastNLP.models.enas_model diff --git a/docs/source/fastNLP.modules.dropout.rst b/docs/source/fastNLP.modules.dropout.rst deleted file mode 100644 index f525e08f..00000000 --- a/docs/source/fastNLP.modules.dropout.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.modules.dropout module -============================== - -.. automodule:: fastNLP.modules.dropout - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.linear.rst b/docs/source/fastNLP.modules.encoder.linear.rst deleted file mode 100644 index 7a479ca2..00000000 --- a/docs/source/fastNLP.modules.encoder.linear.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.modules.encoder.linear module -===================================== - -.. automodule:: fastNLP.modules.encoder.linear - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index 3ee246c2..e571331d 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -15,7 +15,6 @@ Submodules fastNLP.modules.encoder.char_encoder fastNLP.modules.encoder.conv_maxpool fastNLP.modules.encoder.embedding - fastNLP.modules.encoder.linear fastNLP.modules.encoder.lstm fastNLP.modules.encoder.star_transformer fastNLP.modules.encoder.transformer diff --git a/docs/source/fastNLP.modules.other_modules.rst b/docs/source/fastNLP.modules.other_modules.rst deleted file mode 100644 index c1bba5fb..00000000 --- a/docs/source/fastNLP.modules.other_modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.modules.other\_modules module -===================================== - -.. automodule:: fastNLP.modules.other_modules - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.modules.utils.rst b/docs/source/fastNLP.modules.utils.rst deleted file mode 100644 index b7eb672e..00000000 --- a/docs/source/fastNLP.modules.utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.modules.utils module -============================ - -.. automodule:: fastNLP.modules.utils - :members: - :undoc-members: - :show-inheritance: diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index a6cc3402..0a2d052f 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -293,7 +293,8 @@ class CallbackManager(Callback): class GradientClipCallback(Callback): - """每次backward前,将parameter的gradient clip到某个范围。 + """ + 每次backward前,将parameter的gradient clip到某个范围。 :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer 的model中所有参数进行clip @@ -304,6 +305,7 @@ class GradientClipCallback(Callback): 2 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; 大于clip_value的gradient被赋值为clip_value. + """ def __init__(self, parameters=None, clip_value=1, clip_type='norm'): From 0eed25387ed091f736a67b8b2f18daa8bafefd24 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 7 May 2019 14:45:45 +0800 Subject: [PATCH 089/173] update init files --- fastNLP/__init__.py | 2 +- fastNLP/core/__init__.py | 5 +++-- fastNLP/models/__init__.py | 3 +++ fastNLP/modules/__init__.py | 1 - fastNLP/modules/aggregator/__init__.py | 4 ++-- fastNLP/modules/decoder/__init__.py | 4 ++-- 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 330ad9c2..54d96959 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -10,7 +10,7 @@ fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.module fastNLP 中最常用的组件可以直接从 fastNLP 包中 import ,他们的文档如下: """ -__all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", +__all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", "Const", "Trainer", "Tester", "Callback", "Padder", "AutoPadder", "EngChar2DPadder", "AccuracyMetric", "BMESF1PreRecMetric", "SpanFPreRecMetric", "SQuADMetric", diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 4e5f8527..97afa364 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -13,6 +13,8 @@ core 模块里实现了 fastNLP 的核心框架,常用的组件都可以从 fa """ from .batch import Batch +from .callback import Callback +from .const import Const from .dataset import DataSet from .field import FieldArray, Padder, AutoPadder, EngChar2DPadder from .instance import Instance @@ -22,6 +24,5 @@ from .optimizer import Optimizer, SGD, Adam from .sampler import SequentialSampler, BucketSampler, RandomSampler, Sampler from .tester import Tester from .trainer import Trainer +from .utils import cache_results from .vocabulary import Vocabulary -from .callback import Callback -from .utils import cache_results \ No newline at end of file diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index e8818492..59200773 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -4,7 +4,10 @@ TODO 详细介绍的表格,与主页相对应 """ from .base_model import BaseModel +from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequenceClassification, \ + BertForTokenClassification from .biaffine_parser import BiaffineParser, GraphParser from .cnn_text_classification import CNNText from .sequence_modeling import SeqLabeling, AdvSeqLabel from .snli import ESIM +from .star_transformer import STSeqCls, STNLICls, STSeqLabel diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index f1e4c9e6..4022de9d 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -16,4 +16,3 @@ from .dropout import TimestepDropout from .encoder import * from .utils import get_embeddings -__version__ = '0.0.0' diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index c1e6cd17..bfb7579b 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,7 +1,7 @@ -__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention"] +__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention", "BiAttention"] from .pooling import MaxPool from .pooling import MaxPoolWithMask from .pooling import AvgPool from .pooling import MeanPoolWithMask -from .attention import MultiHeadAttention +from .attention import MultiHeadAttention, BiAttention diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index ec864a14..dc05fce2 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["MLP", "ConditionalRandomField","viterbi_decode"] +__all__ = ["MLP", "ConditionalRandomField", "viterbi_decode"] from .CRF import ConditionalRandomField from .MLP import MLP -from .utils import viterbi_decode \ No newline at end of file +from .utils import viterbi_decode From ae8f74b31d07a36f0ebe73395fa497d07c9841ed Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 7 May 2019 14:46:11 +0800 Subject: [PATCH 090/173] update const files --- fastNLP/core/const.py | 8 ++++++++ fastNLP/models/snli.py | 22 +++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/fastNLP/core/const.py b/fastNLP/core/const.py index dcb0a786..f3022db2 100644 --- a/fastNLP/core/const.py +++ b/fastNLP/core/const.py @@ -7,6 +7,7 @@ class Const: INPUT_LEN 序列长度 seq_len(复数seq_len1,seq_len2) OUTPUT 模型输出 pred(复数pred1, pred2) TARGET 真实目标 target(复数target1,target2) + LOSS 损失函数 loss (复数loss1,loss2) """ INPUT = 'words' @@ -14,6 +15,7 @@ class Const: INPUT_LEN = 'seq_len' OUTPUT = 'pred' TARGET = 'target' + LOSS = 'loss' @staticmethod def INPUTS(i): @@ -44,3 +46,9 @@ class Const: """得到第 i 个 ``TARGET`` 的命名""" i = int(i) + 1 return Const.TARGET + str(i) + + @staticmethod + def LOSSES(i): + """得到第 i 个 ``LOSS`` 的命名""" + i = int(i) + 1 + return Const.LOSS + str(i) diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 25b8a36e..1224c4b3 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -2,6 +2,7 @@ import torch import torch.nn as nn from .base_model import BaseModel +from ..core.const import Const from ..modules import decoder as Decoder from ..modules import encoder as Encoder from ..modules import aggregator as Aggregator @@ -40,7 +41,7 @@ class ESIM(BaseModel): (self.vocab_size, self.embed_dim), dropout=self.dropout, ) - self.embedding_layer = Encoder.Linear(self.embed_dim, self.hidden_size) + self.embedding_layer = nn.Linear(self.embed_dim, self.hidden_size) self.encoder = Encoder.LSTM( input_size=self.embed_dim, hidden_size=self.hidden_size, num_layers=1, bias=True, @@ -51,7 +52,7 @@ class ESIM(BaseModel): self.mean_pooling = Aggregator.MeanPoolWithMask() self.max_pooling = Aggregator.MaxPoolWithMask() - self.inference_layer = Encoder.Linear(self.hidden_size * 4, self.hidden_size) + self.inference_layer = nn.Linear(self.hidden_size * 4, self.hidden_size) self.decoder = Encoder.LSTM( input_size=self.hidden_size, hidden_size=self.hidden_size, num_layers=1, bias=True, @@ -60,12 +61,13 @@ class ESIM(BaseModel): self.output = Decoder.MLP([4 * self.hidden_size, self.hidden_size, self.n_labels], 'tanh', dropout=self.dropout) - def forward(self, words1, words2, seq_len1=None, seq_len2=None): + def forward(self, words1, words2, seq_len1=None, seq_len2=None, target=None): """ Forward function :param torch.Tensor words1: [batch size(B), premise seq len(PL)] premise的token表示 :param torch.Tensor words2: [B, hypothesis seq len(HL)] hypothesis的token表示 :param torch.LongTensor seq_len1: [B] premise的长度 :param torch.LongTensor seq_len2: [B] hypothesis的长度 + :param torch.LongTensor target: [B] 真实目标值 :return: dict prediction: [B, n_labels(N)] 预测结果 """ @@ -124,17 +126,23 @@ class ESIM(BaseModel): prediction = torch.tanh(self.output(v)) # prediction: [B, N] - return {'pred': prediction} + if target is not None: + func = nn.CrossEntropyLoss() + loss = func(prediction, target) + return {Const.OUTPUT: prediction, Const.LOSS: loss} - def predict(self, words1, words2, seq_len1, seq_len2): + return {Const.OUTPUT: prediction} + + def predict(self, words1, words2, seq_len1=None, seq_len2=None, target=None): """ Predict function :param torch.Tensor words1: [batch size(B), premise seq len(PL)] premise的token表示 :param torch.Tensor words2: [B, hypothesis seq len(HL)] hypothesis的token表示 :param torch.LongTensor seq_len1: [B] premise的长度 :param torch.LongTensor seq_len2: [B] hypothesis的长度 + :param torch.LongTensor target: [B] 真实目标值 :return: dict prediction: [B, n_labels(N)] 预测结果 """ - prediction = self.forward(words1, words2, seq_len1, seq_len2)['pred'] - return {'pred': torch.argmax(prediction, dim=-1)} + prediction = self.forward(words1, words2, seq_len1, seq_len2)[Const.OUTPUT] + return {Const.OUTPUT: torch.argmax(prediction, dim=-1)} From 1d46ece3264198cacc008a8e6c0998162240845c Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 7 May 2019 14:51:45 +0800 Subject: [PATCH 091/173] update const files --- fastNLP/models/bert.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/fastNLP/models/bert.py b/fastNLP/models/bert.py index 7934b435..960132ad 100644 --- a/fastNLP/models/bert.py +++ b/fastNLP/models/bert.py @@ -6,6 +6,7 @@ import torch from torch import nn from .base_model import BaseModel +from ..core.const import Const from ..modules.encoder import BertModel @@ -62,13 +63,13 @@ class BertForSequenceClassification(BaseModel): if labels is not None: loss_fct = nn.CrossEntropyLoss() loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) - return {"pred": logits, "loss": loss} + return {Const.OUTPUT: logits, Const.LOSS: loss} else: - return {"pred": logits} + return {Const.OUTPUT: logits} def predict(self, input_ids, token_type_ids=None, attention_mask=None): logits = self.forward(input_ids, token_type_ids, attention_mask) - return {"pred": torch.argmax(logits, dim=-1)} + return {Const.OUTPUT: torch.argmax(logits, dim=-1)} class BertForMultipleChoice(BaseModel): @@ -128,13 +129,13 @@ class BertForMultipleChoice(BaseModel): if labels is not None: loss_fct = nn.CrossEntropyLoss() loss = loss_fct(reshaped_logits, labels) - return {"pred": reshaped_logits, "loss": loss} + return {Const.OUTPUT: reshaped_logits, Const.LOSS: loss} else: - return {"pred": reshaped_logits} + return {Const.OUTPUT: reshaped_logits} def predict(self, input_ids, token_type_ids=None, attention_mask=None): - logits = self.forward(input_ids, token_type_ids, attention_mask)["pred"] - return {"pred": torch.argmax(logits, dim=-1)} + logits = self.forward(input_ids, token_type_ids, attention_mask)[Const.OUTPUT] + return {Const.OUTPUT: torch.argmax(logits, dim=-1)} class BertForTokenClassification(BaseModel): @@ -199,13 +200,13 @@ class BertForTokenClassification(BaseModel): loss = loss_fct(active_logits, active_labels) else: loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) - return {"pred": logits, "loss": loss} + return {Const.OUTPUT: logits, Const.LOSS: loss} else: - return {"pred": logits} + return {Const.OUTPUT: logits} def predict(self, input_ids, token_type_ids=None, attention_mask=None): - logits = self.forward(input_ids, token_type_ids, attention_mask)["pred"] - return {"pred": torch.argmax(logits, dim=-1)} + logits = self.forward(input_ids, token_type_ids, attention_mask)[Const.OUTPUT] + return {Const.OUTPUT: torch.argmax(logits, dim=-1)} class BertForQuestionAnswering(BaseModel): @@ -280,12 +281,13 @@ class BertForQuestionAnswering(BaseModel): start_loss = loss_fct(start_logits, start_positions) end_loss = loss_fct(end_logits, end_positions) total_loss = (start_loss + end_loss) / 2 - return {"pred1": start_logits, "pred2": end_logits, "loss": total_loss} + return {Const.OUTPUTS(0): start_logits, Const.OUTPUTS(1): end_logits, Const.LOSS: total_loss} else: - return {"pred1": start_logits, "pred2": end_logits} + return {Const.OUTPUTS(0): start_logits, Const.OUTPUTS(1): end_logits} def predict(self, input_ids, token_type_ids=None, attention_mask=None, **kwargs): logits = self.forward(input_ids, token_type_ids, attention_mask) - start_logits = logits["pred1"] - end_logits = logits["pred2"] - return {"pred1": torch.argmax(start_logits, dim=-1), "pred2": torch.argmax(end_logits, dim=-1)} + start_logits = logits[Const.OUTPUTS(0)] + end_logits = logits[Const.OUTPUTS(1)] + return {Const.OUTPUTS(0): torch.argmax(start_logits, dim=-1), + Const.OUTPUTS(1): torch.argmax(end_logits, dim=-1)} From 25565fe0c931d732d2243222d07fb6d82845d9ae Mon Sep 17 00:00:00 2001 From: yh Date: Tue, 7 May 2019 20:42:00 +0800 Subject: [PATCH 092/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0cnn=E7=9A=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/tester.py | 14 +++++++++++-- fastNLP/core/trainer.py | 2 -- fastNLP/models/cnn_text_classification.py | 24 +++++++---------------- fastNLP/models/sequence_modeling.py | 9 +++++---- test/models/test_cnn.py | 22 +++++++++++++++++++++ 5 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 test/models/test_cnn.py diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 6eaa5add..7b6fdda5 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -27,11 +27,13 @@ Example:: tester = Tester(dataset, model, metrics=AccuracyMetric()) eval_results = tester.test() -这里Metric的映射规律是和 :class:`fastNLP.Trainer` 中一致的,具体使用请参考 :doc:`trainer 模块` 的1.3部分 - +这里Metric的映射规律是和 :class:`fastNLP.Trainer` 中一致的,具体使用请参考 :doc:`trainer 模块` 的1.3部分。 +Tester在验证进行之前会调用model.eval()提示当前进入了evaluation阶段,即会关闭nn.Dropout()等,在验证结束之后会调用model.train()恢复到训练状态。 """ +import warnings + import torch from torch import nn @@ -72,6 +74,7 @@ class Tester(object): 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + 如果模型是通过predict()进行预测的话,那么将不能使用多卡(DataParallel)进行验证,只会使用第一张卡上的模型。 :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 """ @@ -90,6 +93,13 @@ class Tester(object): self.batch_size = batch_size self.verbose = verbose + # 如果是DataParallel将没有办法使用predict方法 + if isinstance(self._model, nn.DataParallel): + if hasattr(self._model.module, 'predict') and not hasattr(self._model, 'predict'): + warnings.warn("Cannot use DataParallel to test your model, because your model offer predict() function," + " while DataParallel has no predict() function.") + self._model = self._model.module + # check predict if hasattr(self._model, 'predict'): self._predict_func = self._model.predict diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index cb2ff821..ed76ee8e 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -462,7 +462,6 @@ class Trainer(object): self.best_dev_perf = None self.sampler = sampler if sampler is not None else RandomSampler() self.prefetch = prefetch - self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) self.n_steps = (len(self.train_data) // self.batch_size + int( len(self.train_data) % self.batch_size != 0)) * self.n_epochs @@ -492,7 +491,6 @@ class Trainer(object): self.step = 0 self.start_time = None # start timestamp - print("callback_manager") self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index 7d7c3878..5df4e62a 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -3,9 +3,9 @@ import torch import torch.nn as nn -import numpy as np +from ..core.const import Const as C -import fastNLP.modules.encoder as encoder +from ..modules import encoder class CNNText(torch.nn.Module): @@ -18,7 +18,7 @@ class CNNText(torch.nn.Module): :param int num_classes: 一共有多少类 :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 :param int,tuple(int) kernel_sizes: 输出channel的kernel大小。 - :param int padding: + :param int padding: 对句子前后的pad的大小, 用0填充。 :param float dropout: Dropout的大小 """ @@ -38,17 +38,7 @@ class CNNText(torch.nn.Module): kernel_sizes=kernel_sizes, padding=padding) self.dropout = nn.Dropout(dropout) - self.fc = encoder.Linear(sum(kernel_nums), num_classes) - - def init_embed(self, embed): - """ - 加载预训练的模型 - :param numpy.ndarray embed: vocab_size x embed_dim的embedding - :return: - """ - assert isinstance(embed, np.ndarray) - assert embed.shape == self.embed.embed.weight.shape - self.embed.embed.weight.data = torch.from_numpy(embed) + self.fc = nn.Linear(sum(kernel_nums), num_classes) def forward(self, words, seq_len=None): """ @@ -61,7 +51,7 @@ class CNNText(torch.nn.Module): x = self.conv_pool(x) # [N,L,C] -> [N,C] x = self.dropout(x) x = self.fc(x) # [N,C] -> [N, N_class] - return {'pred': x} + return {C.OUTPUT: x} def predict(self, words, seq_len=None): """ @@ -71,5 +61,5 @@ class CNNText(torch.nn.Module): :return predict: dict of torch.LongTensor, [batch_size, ] """ output = self(words, seq_len) - _, predict = output['pred'].max(dim=1) - return {'pred': predict} + _, predict = output[C.OUTPUT].max(dim=1) + return {C.OUTPUT: predict} diff --git a/fastNLP/models/sequence_modeling.py b/fastNLP/models/sequence_modeling.py index e076910f..ffa24940 100644 --- a/fastNLP/models/sequence_modeling.py +++ b/fastNLP/models/sequence_modeling.py @@ -4,7 +4,8 @@ from .base_model import BaseModel from ..modules import decoder, encoder from ..modules.decoder.CRF import allowed_transitions from ..modules.utils import seq_mask - +from ..core.const import Const as C +from torch import nn class SeqLabeling(BaseModel): """ @@ -24,7 +25,7 @@ class SeqLabeling(BaseModel): self.Embedding = encoder.embedding.Embedding(init_embed) self.Rnn = encoder.lstm.LSTM(self.Embedding.embedding_dim, hidden_size) - self.Linear = encoder.linear.Linear(hidden_size, num_classes) + self.Linear = nn.Linear(hidden_size, num_classes) self.Crf = decoder.CRF.ConditionalRandomField(num_classes) self.mask = None @@ -46,7 +47,7 @@ class SeqLabeling(BaseModel): # [batch_size, max_len, hidden_size * direction] x = self.Linear(x) # [batch_size, max_len, num_classes] - return {"loss": self._internal_loss(x, target)} + return {C.LOSS: self._internal_loss(x, target)} def predict(self, words, seq_len): """ @@ -65,7 +66,7 @@ class SeqLabeling(BaseModel): x = self.Linear(x) # [batch_size, max_len, num_classes] pred = self._decode(x) - return {'pred': pred} + return {C.OUTPUT: pred} def _internal_loss(self, x, y): """ diff --git a/test/models/test_cnn.py b/test/models/test_cnn.py new file mode 100644 index 00000000..61b75703 --- /dev/null +++ b/test/models/test_cnn.py @@ -0,0 +1,22 @@ + +import unittest + +from test.models.model_runner import * +from fastNLP.models.cnn_text_classification import CNNText + + +class TestCNNText(unittest.TestCase): + def test_case1(self): + # 测试能否正常运行CNN + init_emb = (VOCAB_SIZE, 30) + model = CNNText(init_emb, + NUM_CLS, + kernel_nums=(1, 3, 5), + kernel_sizes=(2, 2, 2), + padding=0, + dropout=0.5) + RUNNER.run_model_with_task(TEXT_CLS, model) + + +if __name__ == '__main__': + TestCNNText().test_case1() \ No newline at end of file From fbe7b7175baece5e31bdf28298da78db119d15f6 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 8 May 2019 14:38:44 +0800 Subject: [PATCH 093/173] fix a little bug --- fastNLP/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 013f7602..aa3d2d82 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -97,7 +97,7 @@ # 将句子分成单词形式, 详见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') + dataset.apply_field(lambda sent:sent.split(), field_name='sentence', new_field_name='words') # 除了匿名函数,也可以定义函数传递进去 def get_words(instance): sentence = instance['sentence'] From 702fa1d95cca1584730b005ccba5c912c4e720d4 Mon Sep 17 00:00:00 2001 From: yunfan Date: Wed, 8 May 2019 23:36:46 +0800 Subject: [PATCH 094/173] - update attention - fix tests --- fastNLP/modules/aggregator/attention.py | 40 +++++++++++++------------ test/core/test_predictor.py | 3 +- test/io/test_config_saver.py | 2 +- test/models/model_runner.py | 6 +++- test/models/test_biaffine_parser.py | 18 +++++++++-- test/models/test_star_trans.py | 6 ++-- test/modules/test_other_modules.py | 2 +- 7 files changed, 48 insertions(+), 29 deletions(-) diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index b926fb12..233dcb55 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -14,7 +14,7 @@ class DotAttention(nn.Module): """ TODO """ - def __init__(self, key_size, value_size, dropout=0.1): + def __init__(self, key_size, value_size, dropout=0): super(DotAttention, self).__init__() self.key_size = key_size self.value_size = value_size @@ -25,14 +25,14 @@ class DotAttention(nn.Module): def forward(self, Q, K, V, mask_out=None): """ - :param Q: [batch, seq_len, key_size] - :param K: [batch, seq_len, key_size] - :param V: [batch, seq_len, value_size] - :param mask_out: [batch, seq_len] + :param Q: [batch, seq_len_q, key_size] + :param K: [batch, seq_len_k, key_size] + :param V: [batch, seq_len_k, value_size] + :param mask_out: [batch, 1, seq_len] or [batch, seq_len_q, seq_len_k] """ output = torch.matmul(Q, K.transpose(1, 2)) / self.scale if mask_out is not None: - output.masked_fill_(mask_out, -float('inf')) + output.masked_fill_(mask_out, -1e8) output = self.softmax(output) output = self.drop(output) return torch.matmul(output, V) @@ -58,7 +58,8 @@ class MultiHeadAttention(nn.Module): self.q_in = nn.Linear(input_size, in_size) self.k_in = nn.Linear(input_size, in_size) self.v_in = nn.Linear(input_size, in_size) - self.attention = DotAttention(key_size=key_size, value_size=value_size) + # follow the paper, do not apply dropout within dot-product + self.attention = DotAttention(key_size=key_size, value_size=value_size, dropout=0) self.out = nn.Linear(value_size * num_head, input_size) self.drop = TimestepDropout(dropout) self.reset_parameters() @@ -73,28 +74,29 @@ class MultiHeadAttention(nn.Module): def forward(self, Q, K, V, atte_mask_out=None): """ - :param Q: [batch, seq_len, model_size] - :param K: [batch, seq_len, model_size] - :param V: [batch, seq_len, model_size] + :param Q: [batch, seq_len_q, model_size] + :param K: [batch, seq_len_k, model_size] + :param V: [batch, seq_len_k, model_size] :param seq_mask: [batch, seq_len] """ - batch, seq_len, _ = Q.size() + batch, sq, _ = Q.size() + sk = K.size(1) d_k, d_v, n_head = self.key_size, self.value_size, self.num_head # input linear - q = self.q_in(Q).view(batch, seq_len, n_head, d_k) - k = self.k_in(K).view(batch, seq_len, n_head, d_k) - v = self.v_in(V).view(batch, seq_len, n_head, d_k) + q = self.q_in(Q).view(batch, sq, n_head, d_k) + k = self.k_in(K).view(batch, sk, n_head, d_k) + v = self.v_in(V).view(batch, sk, n_head, d_v) # transpose q, k and v to do batch attention - q = q.permute(2, 0, 1, 3).contiguous().view(-1, seq_len, d_k) - k = k.permute(2, 0, 1, 3).contiguous().view(-1, seq_len, d_k) - v = v.permute(2, 0, 1, 3).contiguous().view(-1, seq_len, d_v) + q = q.permute(2, 0, 1, 3).contiguous().view(-1, sq, d_k) + k = k.permute(2, 0, 1, 3).contiguous().view(-1, sk, d_k) + v = v.permute(2, 0, 1, 3).contiguous().view(-1, sk, d_v) if atte_mask_out is not None: atte_mask_out = atte_mask_out.repeat(n_head, 1, 1) - atte = self.attention(q, k, v, atte_mask_out).view(n_head, batch, seq_len, d_v) + atte = self.attention(q, k, v, atte_mask_out).view(n_head, batch, sq, d_v) # concat all heads, do output linear - atte = atte.permute(1, 2, 0, 3).contiguous().view(batch, seq_len, -1) + atte = atte.permute(1, 2, 0, 3).contiguous().view(batch, sq, -1) output = self.drop(self.out(atte)) return output diff --git a/test/core/test_predictor.py b/test/core/test_predictor.py index c779e3ac..701353dc 100644 --- a/test/core/test_predictor.py +++ b/test/core/test_predictor.py @@ -7,7 +7,6 @@ import torch from fastNLP.core.dataset import DataSet from fastNLP.core.instance import Instance from fastNLP.core.predictor import Predictor -from fastNLP.modules.encoder.linear import Linear def prepare_fake_dataset(): @@ -27,7 +26,7 @@ def prepare_fake_dataset(): class LinearModel(torch.nn.Module): def __init__(self): super(LinearModel, self).__init__() - self.linear = Linear(2, 1) + self.linear = torch.nn.Linear(2, 1) def forward(self, x): return {"predict": self.linear(x)} diff --git a/test/io/test_config_saver.py b/test/io/test_config_saver.py index 978eb80f..e5341d63 100644 --- a/test/io/test_config_saver.py +++ b/test/io/test_config_saver.py @@ -1,7 +1,7 @@ import os import unittest -from fastNLP.io import ConfigSection, ConfigLoader, ConfigSaver +# from fastNLP.io import ConfigSection, ConfigLoader, ConfigSaver class TestConfigSaver(unittest.TestCase): diff --git a/test/models/model_runner.py b/test/models/model_runner.py index 7a8d0593..3f4e1200 100644 --- a/test/models/model_runner.py +++ b/test/models/model_runner.py @@ -24,7 +24,7 @@ Example:: RUNNER.run_model(model, data=get_mydata(), loss=Myloss(), metrics=Mymetric()) """ -from fastNLP import Trainer, Tester, DataSet +from fastNLP import Trainer, Tester, DataSet, Callback from fastNLP import AccuracyMetric from fastNLP import CrossEntropyLoss from fastNLP.core.const import Const as C @@ -42,6 +42,10 @@ POS_TAGGING = 'pos_tagging' NLI = 'nli' class ModelRunner(): + class Checker(Callback): + def on_backward_begin(self, loss): + assert loss.to('cpu').numpy().isfinate() + def gen_seq(self, length, vocab_size): """generate fake sequence indexes with given length""" # reserve 0 for padding diff --git a/test/models/test_biaffine_parser.py b/test/models/test_biaffine_parser.py index e4746391..e6fca6a8 100644 --- a/test/models/test_biaffine_parser.py +++ b/test/models/test_biaffine_parser.py @@ -25,10 +25,24 @@ def prepare_parser_data(): is_input=True, is_target=True) return ds + class TestBiaffineParser(unittest.TestCase): def test_train(self): - model = BiaffineParser(init_embed=(VOCAB_SIZE, 30), - pos_vocab_size=VOCAB_SIZE, pos_emb_dim=30, + model = BiaffineParser(init_embed=(VOCAB_SIZE, 10), + pos_vocab_size=VOCAB_SIZE, pos_emb_dim=10, + rnn_hidden_size=10, + arc_mlp_size=10, + label_mlp_size=10, num_label=NUM_CLS, encoder='var-lstm') ds = prepare_parser_data() RUNNER.run_model(model, ds, loss=ParserLoss(), metrics=ParserMetric()) + + def test_train2(self): + model = BiaffineParser(init_embed=(VOCAB_SIZE, 10), + pos_vocab_size=VOCAB_SIZE, pos_emb_dim=10, + rnn_hidden_size=16, + arc_mlp_size=10, + label_mlp_size=10, + num_label=NUM_CLS, encoder='transformer') + ds = prepare_parser_data() + RUNNER.run_model(model, ds, loss=ParserLoss(), metrics=ParserMetric()) diff --git a/test/models/test_star_trans.py b/test/models/test_star_trans.py index b08e2efe..eae19b24 100644 --- a/test/models/test_star_trans.py +++ b/test/models/test_star_trans.py @@ -4,13 +4,13 @@ from fastNLP.models.star_transformer import STNLICls, STSeqCls, STSeqLabel # add star-transformer tests, for 3 kinds of tasks. def test_cls(): - model = STSeqCls((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + model = STSeqCls((VOCAB_SIZE, 10), NUM_CLS, dropout=0) RUNNER.run_model_with_task(TEXT_CLS, model) def test_nli(): - model = STNLICls((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + model = STNLICls((VOCAB_SIZE, 10), NUM_CLS, dropout=0) RUNNER.run_model_with_task(NLI, model) def test_seq_label(): - model = STSeqLabel((VOCAB_SIZE, 100), NUM_CLS, dropout=0) + model = STSeqLabel((VOCAB_SIZE, 10), NUM_CLS, dropout=0) RUNNER.run_model_with_task(POS_TAGGING, model) diff --git a/test/modules/test_other_modules.py b/test/modules/test_other_modules.py index 4e0fb838..ef5020c1 100644 --- a/test/modules/test_other_modules.py +++ b/test/modules/test_other_modules.py @@ -2,7 +2,7 @@ import unittest import torch -from fastNLP.modules.other_modules import GroupNorm, LayerNormalization, BiLinear, BiAffine +# from fastNLP.modules.other_modules import GroupNorm, LayerNormalization, BiLinear, BiAffine from fastNLP.modules.encoder.star_transformer import StarTransformer From 6d36dbe7fb0358e70b87f343b5533a964245563d Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 9 May 2019 00:15:25 +0800 Subject: [PATCH 095/173] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/models/__init__.py | 2 +- ...uence_modeling.py => sequence_labeling.py} | 44 ++++++++----------- reproduction/POS_tagging/train_pos_tag.py | 2 +- test/data_for_tests/word2vec_test.txt | 7 +++ test/io/test_embed_loader.py | 16 ++++++- ...cnn.py => test_cnn_text_classification.py} | 6 +-- test/models/test_sequence_labeling.py | 36 +++++++++++++++ 7 files changed, 78 insertions(+), 35 deletions(-) rename fastNLP/models/{sequence_modeling.py => sequence_labeling.py} (85%) create mode 100644 test/data_for_tests/word2vec_test.txt rename test/models/{test_cnn.py => test_cnn_text_classification.py} (83%) create mode 100644 test/models/test_sequence_labeling.py diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index 59200773..f0d84b1c 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -8,6 +8,6 @@ from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequen BertForTokenClassification from .biaffine_parser import BiaffineParser, GraphParser from .cnn_text_classification import CNNText -from .sequence_modeling import SeqLabeling, AdvSeqLabel +from .sequence_labeling import SeqLabeling, AdvSeqLabel from .snli import ESIM from .star_transformer import STSeqCls, STNLICls, STSeqLabel diff --git a/fastNLP/models/sequence_modeling.py b/fastNLP/models/sequence_labeling.py similarity index 85% rename from fastNLP/models/sequence_modeling.py rename to fastNLP/models/sequence_labeling.py index ffa24940..880bd8a8 100644 --- a/fastNLP/models/sequence_modeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -43,7 +43,7 @@ class SeqLabeling(BaseModel): x = self.Embedding(words) # [batch_size, max_len, word_emb_dim] - x = self.Rnn(x) + x,_ = self.Rnn(x, seq_len) # [batch_size, max_len, hidden_size * direction] x = self.Linear(x) # [batch_size, max_len, num_classes] @@ -55,13 +55,13 @@ class SeqLabeling(BaseModel): :param torch.LongTensor words: [batch_size, max_len] :param torch.LongTensor seq_len: [batch_size,] - :return: + :return: {'pred': xx}, [batch_size, max_len] """ self.mask = self._make_mask(words, seq_len) x = self.Embedding(words) # [batch_size, max_len, word_emb_dim] - x = self.Rnn(x) + x, _ = self.Rnn(x, seq_len) # [batch_size, max_len, hidden_size * direction] x = self.Linear(x) # [batch_size, max_len, num_classes] @@ -93,13 +93,13 @@ class SeqLabeling(BaseModel): def _decode(self, x): """ :param torch.FloatTensor x: [batch_size, max_len, tag_size] - :return prediction: list of [decode path(list)] + :return prediction: [batch_size, max_len] """ - tag_seq, _ = self.Crf.viterbi_decode(x, self.mask, unpad=True) + tag_seq, _ = self.Crf.viterbi_decode(x, self.mask) return tag_seq -class AdvSeqLabel: +class AdvSeqLabel(nn.Module): """ 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 """ @@ -115,17 +115,19 @@ class AdvSeqLabel: :param dict id2words: tag id转为其tag word的表。用于在CRF解码时防止解出非法的顺序,比如'BMES'这个标签规范中,'S' 不能出现在'B'之后。这里也支持类似与'B-NN',即'-'前为标签类型的指示,后面为具体的tag的情况。这里不但会保证 'B-NN'后面不为'S-NN'还会保证'B-NN'后面不会出现'M-xx'(任何非'M-NN'和'E-NN'的情况。) - :param str encoding_type: 支持"BIO", "BMES", "BEMSO"。 + :param str encoding_type: 支持"BIO", "BMES", "BEMSO", 只有在id2words不为None的情况游泳。 """ + super().__init__() + self.Embedding = encoder.embedding.Embedding(init_embed) self.norm1 = torch.nn.LayerNorm(self.Embedding.embedding_dim) - self.Rnn = torch.nn.LSTM(input_size=self.Embedding.embedding_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, + self.Rnn = encoder.LSTM(input_size=self.Embedding.embedding_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, bidirectional=True, batch_first=True) - self.Linear1 = encoder.Linear(hidden_size * 2, hidden_size * 2 // 3) + self.Linear1 = nn.Linear(hidden_size * 2, hidden_size * 2 // 3) self.norm2 = torch.nn.LayerNorm(hidden_size * 2 // 3) self.relu = torch.nn.LeakyReLU() self.drop = torch.nn.Dropout(dropout) - self.Linear2 = encoder.Linear(hidden_size * 2 // 3, num_classes) + self.Linear2 = nn.Linear(hidden_size * 2 // 3, num_classes) if id2words is None: self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False) @@ -137,9 +139,9 @@ class AdvSeqLabel: def _decode(self, x): """ :param torch.FloatTensor x: [batch_size, max_len, tag_size] - :return prediction: list of [decode path(list)] + :return torch.LongTensor, [batch_size, max_len] """ - tag_seq, _ = self.Crf.viterbi_decode(x, self.mask, unpad=True) + tag_seq, _ = self.Crf.viterbi_decode(x, self.mask) return tag_seq def _internal_loss(self, x, y): @@ -176,31 +178,20 @@ class AdvSeqLabel: words = words.long() seq_len = seq_len.long() self.mask = self._make_mask(words, seq_len) - sent_len, idx_sort = torch.sort(seq_len, descending=True) - _, idx_unsort = torch.sort(idx_sort, descending=False) # seq_len = seq_len.long() target = target.long() if target is not None else None if next(self.parameters()).is_cuda: words = words.cuda() - idx_sort = idx_sort.cuda() - idx_unsort = idx_unsort.cuda() self.mask = self.mask.cuda() x = self.Embedding(words) x = self.norm1(x) # [batch_size, max_len, word_emb_dim] - sent_variable = x[idx_sort] - sent_packed = torch.nn.utils.rnn.pack_padded_sequence(sent_variable, sent_len, batch_first=True) - - x, _ = self.Rnn(sent_packed) + x, _ = self.Rnn(x, seq_len=seq_len) - sent_output = torch.nn.utils.rnn.pad_packed_sequence(x, batch_first=True)[0] - x = sent_output[idx_unsort] - - x = x.contiguous() x = self.Linear1(x) x = self.norm2(x) x = self.relu(x) @@ -225,6 +216,7 @@ class AdvSeqLabel: :param torch.LongTensor words: [batch_size, mex_len] :param torch.LongTensor seq_len:[batch_size, ] - :return: [list1, list2, ...], 内部每个list为一个路径,已经unpad了。 + :return {'pred':}, value是torch.LongTensor, [batch_size, max_len] + """ - return self._forward(words, seq_len, ) + return self._forward(words, seq_len) diff --git a/reproduction/POS_tagging/train_pos_tag.py b/reproduction/POS_tagging/train_pos_tag.py index 06547701..ccf7aa1e 100644 --- a/reproduction/POS_tagging/train_pos_tag.py +++ b/reproduction/POS_tagging/train_pos_tag.py @@ -13,7 +13,7 @@ from fastNLP.api.processor import SeqLenProcessor, VocabIndexerProcessor, SetInp from fastNLP.core.metrics import SpanFPreRecMetric from fastNLP.core.trainer import Trainer from fastNLP.io.config_io import ConfigLoader, ConfigSection -from fastNLP.models.sequence_modeling import AdvSeqLabel +from fastNLP.models.sequence_labeling import AdvSeqLabel from fastNLP.io.dataset_loader import ConllxDataLoader from fastNLP.api.processor import ModelProcessor, Index2WordProcessor diff --git a/test/data_for_tests/word2vec_test.txt b/test/data_for_tests/word2vec_test.txt new file mode 100644 index 00000000..c16170f2 --- /dev/null +++ b/test/data_for_tests/word2vec_test.txt @@ -0,0 +1,7 @@ +5 50 +the 0.418 0.24968 -0.41242 0.1217 0.34527 -0.044457 -0.49688 -0.17862 -0.00066023 -0.6566 0.27843 -0.14767 -0.55677 0.14658 -0.0095095 0.011658 0.10204 -0.12792 -0.8443 -0.12181 -0.016801 -0.33279 -0.1552 -0.23131 -0.19181 -1.8823 -0.76746 0.099051 -0.42125 -0.19526 4.0071 -0.18594 -0.52287 -0.31681 0.00059213 0.0074449 0.17778 -0.15897 0.012041 -0.054223 -0.29871 -0.15749 -0.34758 -0.045637 -0.44251 0.18785 0.0027849 -0.18411 -0.11514 -0.78581 +of 0.70853 0.57088 -0.4716 0.18048 0.54449 0.72603 0.18157 -0.52393 0.10381 -0.17566 0.078852 -0.36216 -0.11829 -0.83336 0.11917 -0.16605 0.061555 -0.012719 -0.56623 0.013616 0.22851 -0.14396 -0.067549 -0.38157 -0.23698 -1.7037 -0.86692 -0.26704 -0.2589 0.1767 3.8676 -0.1613 -0.13273 -0.68881 0.18444 0.0052464 -0.33874 -0.078956 0.24185 0.36576 -0.34727 0.28483 0.075693 -0.062178 -0.38988 0.22902 -0.21617 -0.22562 -0.093918 -0.80375 +to 0.68047 -0.039263 0.30186 -0.17792 0.42962 0.032246 -0.41376 0.13228 -0.29847 -0.085253 0.17118 0.22419 -0.10046 -0.43653 0.33418 0.67846 0.057204 -0.34448 -0.42785 -0.43275 0.55963 0.10032 0.18677 -0.26854 0.037334 -2.0932 0.22171 -0.39868 0.20912 -0.55725 3.8826 0.47466 -0.95658 -0.37788 0.20869 -0.32752 0.12751 0.088359 0.16351 -0.21634 -0.094375 0.018324 0.21048 -0.03088 -0.19722 0.082279 -0.09434 -0.073297 -0.064699 -0.26044 +and 0.26818 0.14346 -0.27877 0.016257 0.11384 0.69923 -0.51332 -0.47368 -0.33075 -0.13834 0.2702 0.30938 -0.45012 -0.4127 -0.09932 0.038085 0.029749 0.10076 -0.25058 -0.51818 0.34558 0.44922 0.48791 -0.080866 -0.10121 -1.3777 -0.10866 -0.23201 0.012839 -0.46508 3.8463 0.31362 0.13643 -0.52244 0.3302 0.33707 -0.35601 0.32431 0.12041 0.3512 -0.069043 0.36885 0.25168 -0.24517 0.25381 0.1367 -0.31178 -0.6321 -0.25028 -0.38097 +in 0.33042 0.24995 -0.60874 0.10923 0.036372 0.151 -0.55083 -0.074239 -0.092307 -0.32821 0.09598 -0.82269 -0.36717 -0.67009 0.42909 0.016496 -0.23573 0.12864 -1.0953 0.43334 0.57067 -0.1036 0.20422 0.078308 -0.42795 -1.7984 -0.27865 0.11954 -0.12689 0.031744 3.8631 -0.17786 -0.082434 -0.62698 0.26497 -0.057185 -0.073521 0.46103 0.30862 0.12498 -0.48609 -0.0080272 0.031184 -0.36576 -0.42699 0.42164 -0.11666 -0.50703 -0.027273 -0.53285 +a 0.21705 0.46515 -0.46757 0.10082 1.0135 0.74845 -0.53104 -0.26256 0.16812 0.13182 -0.24909 -0.44185 -0.21739 0.51004 0.13448 -0.43141 -0.03123 0.20674 -0.78138 -0.20148 -0.097401 0.16088 -0.61836 -0.18504 -0.12461 -2.2526 -0.22321 0.5043 0.32257 0.15313 3.9636 -0.71365 -0.67012 0.28388 0.21738 0.14433 0.25926 0.23434 0.4274 -0.44451 0.13813 0.36973 -0.64289 0.024142 -0.039315 -0.26037 0.12017 -0.043782 0.41013 0.1796 \ No newline at end of file diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index 05a127a9..d43a00fe 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -3,7 +3,9 @@ import numpy as np from fastNLP import Vocabulary from fastNLP.io import EmbedLoader - +import os +from fastNLP.io.dataset_loader import SSTLoader +from fastNLP.core.const import Const as C class TestEmbedLoader(unittest.TestCase): def test_load_with_vocab(self): @@ -36,4 +38,14 @@ class TestEmbedLoader(unittest.TestCase): self.assertEqual(w_m.shape, (7, 50)) self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 7) for word in words: - self.assertIn(word, vocab) \ No newline at end of file + self.assertIn(word, vocab) + + def test_read_all_glove(self): + pass + # 这是可以运行的,但是总数少于行数,应该是由于glove有重复的word + # path = '/where/to/read/full/glove' + # init_embed, vocab = EmbedLoader.load_without_vocab(path, error='strict') + # print(init_embed.shape) + # print(init_embed.mean()) + # print(np.isnan(init_embed).sum()) + # print(len(vocab)) diff --git a/test/models/test_cnn.py b/test/models/test_cnn_text_classification.py similarity index 83% rename from test/models/test_cnn.py rename to test/models/test_cnn_text_classification.py index 61b75703..b83b7bad 100644 --- a/test/models/test_cnn.py +++ b/test/models/test_cnn_text_classification.py @@ -1,7 +1,7 @@ import unittest -from test.models.model_runner import * +from .model_runner import * from fastNLP.models.cnn_text_classification import CNNText @@ -16,7 +16,3 @@ class TestCNNText(unittest.TestCase): padding=0, dropout=0.5) RUNNER.run_model_with_task(TEXT_CLS, model) - - -if __name__ == '__main__': - TestCNNText().test_case1() \ No newline at end of file diff --git a/test/models/test_sequence_labeling.py b/test/models/test_sequence_labeling.py new file mode 100644 index 00000000..3a70e381 --- /dev/null +++ b/test/models/test_sequence_labeling.py @@ -0,0 +1,36 @@ + + +import unittest + +from .model_runner import * +from fastNLP.models.sequence_labeling import SeqLabeling, AdvSeqLabel +from fastNLP.core.losses import LossInForward + +class TesSeqLabel(unittest.TestCase): + def test_case1(self): + # 测试能否正常运行CNN + init_emb = (VOCAB_SIZE, 30) + model = SeqLabeling(init_emb, + hidden_size=30, + num_classes=NUM_CLS) + + data = RUNNER.prepare_pos_tagging_data() + data.set_input('target') + loss = LossInForward() + metric = AccuracyMetric(pred=C.OUTPUT, target=C.TARGET, seq_len=C.INPUT_LEN) + RUNNER.run_model(model, data, loss, metric) + + +class TesAdvSeqLabel(unittest.TestCase): + def test_case1(self): + # 测试能否正常运行CNN + init_emb = (VOCAB_SIZE, 30) + model = AdvSeqLabel(init_emb, + hidden_size=30, + num_classes=NUM_CLS) + + data = RUNNER.prepare_pos_tagging_data() + data.set_input('target') + loss = LossInForward() + metric = AccuracyMetric(pred=C.OUTPUT, target=C.TARGET, seq_len=C.INPUT_LEN) + RUNNER.run_model(model, data, loss, metric) \ No newline at end of file From 8dbbc8caf0b42f6824129972ad4872f460533513 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 9 May 2019 14:18:26 +0800 Subject: [PATCH 096/173] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E5=85=A5=E9=97=A8=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/user/quickstart.rst | 123 +++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/docs/source/user/quickstart.rst b/docs/source/user/quickstart.rst index 599254e9..0e5c053e 100644 --- a/docs/source/user/quickstart.rst +++ b/docs/source/user/quickstart.rst @@ -1,3 +1,124 @@ =============== 快速入门 -=============== \ No newline at end of file +=============== + +这是一个简单的分类任务 (数据来源 `kaggle `_ )。 +给出一段文字,预测它的标签是0~4中的哪一个。 + +我们可以使用 fastNLP 中 io 模块中的 :class:`~fastNLP.io.CSVLoader` 类,轻松地从 csv 文件读取我们的数据。 + +.. code-block:: python + + from fastNLP.io import CSVLoader + + loader = CSVLoader(headers=('raw_sentence', 'label'), sep='\t') + dataset = loader.load("./sample_data/tutorial_sample_dataset.csv") + +此时的 `dataset[0]` 的值如下,可以看到,数据集中的每个数据包含 ``raw_sentence`` 和 ``label`` 两个字段,他们的类型都是 ``str``:: + + {'raw_sentence': A series of escapades demonstrating the adage that what is good for the + goose is also good for the gander , some of which occasionally amuses but none of which + amounts to much of a story . type=str, + 'label': 1 type=str} + + +我们使用 :class:`~fastNLP.DataSet` 类的 :meth:`~fastNLP.DataSet.apply` 方法将 ``raw_sentence`` 中字母变成小写,并将句子分词。 + +.. code-block:: python + + dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='sentence') + dataset.apply(lambda x: x['sentence'].split(), new_field_name='words', is_input=True) + +然后我们再用 :class:`~fastNLP.Vocabulary` 类来统计数据中出现的单词,并将单词序列转化为训练可用的数字序列。 + +.. code-block:: python + + from fastNLP import Vocabulary + vocab = Vocabulary(min_freq=2).from_dataset(dataset, field_name='words') + vocab.index_dataset(dataset, field_name='words',new_field_name='words') + +同时,我们也将原来 str 类型的标签转化为数字,并设置为训练中的标准答案 ``target`` + +.. code-block:: python + + dataset.apply(lambda x: int(x['label']), new_field_name='target', is_target=True) + +现在我们可以导入 fastNLP 内置的文本分类模型 :class:`~fastNLP.models.CNNText` , + + +.. code-block:: python + + from fastNLP.models import CNNText + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + +:class:`~fastNLP.models.CNNText` 的网络结构如下:: + + CNNText( + (embed): Embedding( + 177, 50 + (dropout): Dropout(p=0.0) + ) + (conv_pool): ConvMaxpool( + (convs): ModuleList( + (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,)) + (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,)) + (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,)) + ) + ) + (dropout): Dropout(p=0.1) + (fc): Linear(in_features=12, out_features=5, bias=True) + ) + +下面我们用 :class:`~fastNLP.DataSet` 类的 :meth:`~fastNLP.DataSet.split` 方法将数据集划分为 ``train_data`` 和 ``dev_data`` +两个部分,分别用于训练和验证 + +.. code-block:: python + + train_data, dev_data = dataset.split(0.2) + +最后我们用 fastNLP 的 :class:`~fastNLP.Trainer` 进行训练,训练的过程中需要传入模型 ``model`` ,训练数据集 ``train_data`` , +验证数据集 ``dev_data`` ,损失函数 ``loss`` 和衡量标准 ``metrics`` 。 +其中损失函数使用的是 fastNLP 提供的 :class:`~fastNLP.CrossEntropyLoss` 损失函数; +衡量标准使用的是 fastNLP 提供的 :class:`~fastNLP.AccuracyMetric` 正确率指标。 + +.. code-block:: python + + from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric + + trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, + loss=CrossEntropyLoss(), metrics=AccuracyMetric()) + trainer.train() + +训练过程的输出如下:: + + input fields after batch(if batch size is 2): + words: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 26]) + target fields after batch(if batch size is 2): + target: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + + training epochs started 2019-05-09-10-59-39 + Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.333333 + + Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.6 + + Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.8 + + Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.8 + + Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.733333 + + Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.733333 + + Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.733333 + + + In Epoch:6/Step:12, got best dev performance:AccuracyMetric: acc=0.8 + Reloaded the best model. + +这份教程只是简单地介绍了使用 fastNLP 工作的流程,具体的细节分析见 :doc:`/user/tutorials` \ No newline at end of file From cdbb687d34de5d4e960c12765149e87047dcef41 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 9 May 2019 14:21:25 +0800 Subject: [PATCH 097/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86sequence=5Fm?= =?UTF-8?q?odeling=20=E5=91=BD=E5=90=8D=E4=B8=BA=20sequence=5Flabeling=20?= =?UTF-8?q?=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.models.rst | 2 +- ...ence_modeling.rst => fastNLP.models.sequence_labeling.rst} | 0 docs/source/index.rst | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/source/{fastNLP.models.sequence_modeling.rst => fastNLP.models.sequence_labeling.rst} (100%) diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index 24fca203..accfc3bb 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -19,7 +19,7 @@ Submodules fastNLP.models.enas_model fastNLP.models.enas_trainer fastNLP.models.enas_utils - fastNLP.models.sequence_modeling + fastNLP.models.sequence_labeling fastNLP.models.snli fastNLP.models.star_transformer diff --git a/docs/source/fastNLP.models.sequence_modeling.rst b/docs/source/fastNLP.models.sequence_labeling.rst similarity index 100% rename from docs/source/fastNLP.models.sequence_modeling.rst rename to docs/source/fastNLP.models.sequence_labeling.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index b08c2b2d..23b14877 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -60,8 +60,8 @@ TODO 安装指南 快速入门 - 使用 fastNLP 分类 - 使用 fastNLP 分词 + 详细指南 + 科研向导 API 文档 From e557da61a888a62e8de063fb77aa1993cc5ad474 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 9 May 2019 14:31:59 +0800 Subject: [PATCH 098/173] =?UTF-8?q?=E9=87=8D=E6=96=B0=20apidoc=20=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E6=94=B9=E5=90=8D=E7=9A=84=E9=81=97=E7=95=99=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.models.sequence_labeling.rst | 4 ++-- docs/source/fastNLP.modules.rst | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/source/fastNLP.models.sequence_labeling.rst b/docs/source/fastNLP.models.sequence_labeling.rst index 7fa7e450..b8568be7 100644 --- a/docs/source/fastNLP.models.sequence_labeling.rst +++ b/docs/source/fastNLP.models.sequence_labeling.rst @@ -1,7 +1,7 @@ -fastNLP.models.sequence\_modeling module +fastNLP.models.sequence\_labeling module ======================================== -.. automodule:: fastNLP.models.sequence_modeling +.. automodule:: fastNLP.models.sequence_labeling :members: :undoc-members: :show-inheritance: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 23f5523a..2057613a 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -1,5 +1,3 @@ - - fastNLP.modules package ======================= From bbab238bb0cb0fff0a4aac33f1e5a29f53afe3c0 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 9 May 2019 15:40:25 +0800 Subject: [PATCH 099/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/models/sequence_labeling.py | 109 ++++++++++++++-------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 880bd8a8..015ae24a 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -7,28 +7,27 @@ from ..modules.utils import seq_mask from ..core.const import Const as C from torch import nn + class SeqLabeling(BaseModel): """ - 一个基础的Sequence labeling的模型 + 一个基础的Sequence labeling的模型。 + 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 + + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding + :param int hidden_size: LSTM隐藏层的大小 + :param int num_classes: 一共有多少类 """ - + def __init__(self, init_embed, hidden_size, num_classes): - """ - 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 - - :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), - 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding - :param int hidden_size: LSTM隐藏层的大小 - :param int num_classes: 一共有多少类 - """ super(SeqLabeling, self).__init__() - + self.Embedding = encoder.embedding.Embedding(init_embed) self.Rnn = encoder.lstm.LSTM(self.Embedding.embedding_dim, hidden_size) self.Linear = nn.Linear(hidden_size, num_classes) self.Crf = decoder.CRF.ConditionalRandomField(num_classes) self.mask = None - + def forward(self, words, seq_len, target): """ :param torch.LongTensor words: [batch_size, max_len],序列的index @@ -40,15 +39,15 @@ class SeqLabeling(BaseModel): assert words.shape[0] == seq_len.shape[0] assert target.shape == words.shape self.mask = self._make_mask(words, seq_len) - + x = self.Embedding(words) # [batch_size, max_len, word_emb_dim] - x,_ = self.Rnn(x, seq_len) + x, _ = self.Rnn(x, seq_len) # [batch_size, max_len, hidden_size * direction] x = self.Linear(x) # [batch_size, max_len, num_classes] return {C.LOSS: self._internal_loss(x, target)} - + def predict(self, words, seq_len): """ 用于在预测时使用 @@ -58,7 +57,7 @@ class SeqLabeling(BaseModel): :return: {'pred': xx}, [batch_size, max_len] """ self.mask = self._make_mask(words, seq_len) - + x = self.Embedding(words) # [batch_size, max_len, word_emb_dim] x, _ = self.Rnn(x, seq_len) @@ -67,7 +66,7 @@ class SeqLabeling(BaseModel): # [batch_size, max_len, num_classes] pred = self._decode(x) return {C.OUTPUT: pred} - + def _internal_loss(self, x, y): """ Negative log likelihood loss. @@ -82,14 +81,14 @@ class SeqLabeling(BaseModel): assert y.shape == self.mask.shape total_loss = self.Crf(x, y, self.mask) return torch.mean(total_loss) - + def _make_mask(self, x, seq_len): batch_size, max_len = x.size(0), x.size(1) mask = seq_mask(seq_len, max_len) mask = mask.view(batch_size, max_len) mask = mask.to(x).float() return mask - + def _decode(self, x): """ :param torch.FloatTensor x: [batch_size, max_len, tag_size] @@ -102,40 +101,40 @@ class SeqLabeling(BaseModel): class AdvSeqLabel(nn.Module): """ 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 + + :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), + 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding + :param int hidden_size: LSTM的隐层大小 + :param int num_classes: 有多少个类 + :param float dropout: LSTM中以及DropOut层的drop概率 + :param dict id2words: tag id转为其tag word的表。用于在CRF解码时防止解出非法的顺序,比如'BMES'这个标签规范中,'S' + 不能出现在'B'之后。这里也支持类似与'B-NN',即'-'前为标签类型的指示,后面为具体的tag的情况。这里不但会保证 + 'B-NN'后面不为'S-NN'还会保证'B-NN'后面不会出现'M-xx'(任何非'M-NN'和'E-NN'的情况。) + :param str encoding_type: 支持"BIO", "BMES", "BEMSO", 只有在id2words不为None的情况有用。 """ - + def __init__(self, init_embed, hidden_size, num_classes, dropout=0.3, id2words=None, encoding_type='bmes'): - """ - - :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), - 第一个int为vocab_zie, 第二个int为embed_dim); 如果为Tensor, Embedding, ndarray等则直接使用该值初始化Embedding - :param int hidden_size: LSTM的隐层大小 - :param int num_classes: 有多少个类 - :param float dropout: LSTM中以及DropOut层的drop概率 - :param dict id2words: tag id转为其tag word的表。用于在CRF解码时防止解出非法的顺序,比如'BMES'这个标签规范中,'S' - 不能出现在'B'之后。这里也支持类似与'B-NN',即'-'前为标签类型的指示,后面为具体的tag的情况。这里不但会保证 - 'B-NN'后面不为'S-NN'还会保证'B-NN'后面不会出现'M-xx'(任何非'M-NN'和'E-NN'的情况。) - :param str encoding_type: 支持"BIO", "BMES", "BEMSO", 只有在id2words不为None的情况游泳。 - """ + super().__init__() - + self.Embedding = encoder.embedding.Embedding(init_embed) self.norm1 = torch.nn.LayerNorm(self.Embedding.embedding_dim) - self.Rnn = encoder.LSTM(input_size=self.Embedding.embedding_dim, hidden_size=hidden_size, num_layers=2, dropout=dropout, - bidirectional=True, batch_first=True) + self.Rnn = encoder.LSTM(input_size=self.Embedding.embedding_dim, hidden_size=hidden_size, num_layers=2, + dropout=dropout, + bidirectional=True, batch_first=True) self.Linear1 = nn.Linear(hidden_size * 2, hidden_size * 2 // 3) self.norm2 = torch.nn.LayerNorm(hidden_size * 2 // 3) self.relu = torch.nn.LeakyReLU() self.drop = torch.nn.Dropout(dropout) self.Linear2 = nn.Linear(hidden_size * 2 // 3, num_classes) - + if id2words is None: self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False) else: self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False, allowed_transitions=allowed_transitions(id2words, - encoding_type=encoding_type)) - + encoding_type=encoding_type)) + def _decode(self, x): """ :param torch.FloatTensor x: [batch_size, max_len, tag_size] @@ -143,7 +142,7 @@ class AdvSeqLabel(nn.Module): """ tag_seq, _ = self.Crf.viterbi_decode(x, self.mask) return tag_seq - + def _internal_loss(self, x, y): """ Negative log likelihood loss. @@ -158,14 +157,14 @@ class AdvSeqLabel(nn.Module): assert y.shape == self.mask.shape total_loss = self.Crf(x, y, self.mask) return torch.mean(total_loss) - + def _make_mask(self, x, seq_len): batch_size, max_len = x.size(0), x.size(1) mask = seq_mask(seq_len, max_len) mask = mask.view(batch_size, max_len) mask = mask.to(x).float() return mask - + def _forward(self, words, seq_len, target=None): """ :param torch.LongTensor words: [batch_size, mex_len] @@ -174,24 +173,24 @@ class AdvSeqLabel(nn.Module): :return y: If truth is None, return list of [decode path(list)]. Used in testing and predicting. If truth is not None, return loss, a scalar. Used in training. """ - + words = words.long() seq_len = seq_len.long() self.mask = self._make_mask(words, seq_len) - + # seq_len = seq_len.long() target = target.long() if target is not None else None - + if next(self.parameters()).is_cuda: words = words.cuda() self.mask = self.mask.cuda() - + x = self.Embedding(words) x = self.norm1(x) # [batch_size, max_len, word_emb_dim] - + x, _ = self.Rnn(x, seq_len=seq_len) - + x = self.Linear1(x) x = self.norm2(x) x = self.relu(x) @@ -201,22 +200,22 @@ class AdvSeqLabel(nn.Module): return {"loss": self._internal_loss(x, target)} else: return {"pred": self._decode(x)} - + def forward(self, words, seq_len, target): """ + :param torch.LongTensor words: [batch_size, mex_len] - :param torch.LongTensor seq_len:[batch_size, ] + :param torch.LongTensor seq_len: [batch_size, ] :param torch.LongTensor target: [batch_size, max_len], 目标 - :return torch.Tensor, a scalar loss + :return torch.Tensor: a scalar loss """ return self._forward(words, seq_len, target) - + def predict(self, words, seq_len): """ - + :param torch.LongTensor words: [batch_size, mex_len] - :param torch.LongTensor seq_len:[batch_size, ] - :return {'pred':}, value是torch.LongTensor, [batch_size, max_len] - + :param torch.LongTensor seq_len: [batch_size, ] + :return torch.LongTensor: [batch_size, max_len] """ return self._forward(words, seq_len) From 56ff4ac7a1bd3afb0fa5ba89b3726c46b8580a63 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Fri, 10 May 2019 16:34:35 +0800 Subject: [PATCH 100/173] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=E7=9A=84seq=5Flen=5Fto=5Fmask,=20=E7=8E=B0?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=88=B0core.utils.seq=5Flen=5Fto=5Fmask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/__init__.py | 2 +- fastNLP/core/metrics.py | 4 +- fastNLP/core/utils.py | 69 +++++++++++++---------------- fastNLP/models/biaffine_parser.py | 9 ++-- fastNLP/models/sequence_labeling.py | 6 +-- fastNLP/models/snli.py | 6 +-- fastNLP/models/star_transformer.py | 10 ++--- fastNLP/modules/decoder/CRF.py | 15 +------ fastNLP/modules/decoder/__init__.py | 3 +- fastNLP/modules/decoder/utils.py | 6 --- fastNLP/modules/utils.py | 16 ------- test/core/test_utils.py | 42 +++++++++++++++++- test/modules/decoder/test_CRF.py | 6 +-- 13 files changed, 97 insertions(+), 97 deletions(-) diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 97afa364..ed1bd0c9 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -24,5 +24,5 @@ from .optimizer import Optimizer, SGD, Adam from .sampler import SequentialSampler, BucketSampler, RandomSampler, Sampler from .tester import Tester from .trainer import Trainer -from .utils import cache_results +from .utils import cache_results, seq_len_to_mask from .vocabulary import Vocabulary diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index c9ba7f35..5cdfdc44 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -13,7 +13,7 @@ from .utils import _CheckRes from .utils import _build_args from .utils import _check_arg_dict_list from .utils import _get_func_signature -from .utils import seq_lens_to_masks +from .utils import seq_len_to_mask from .vocabulary import Vocabulary @@ -305,7 +305,7 @@ class AccuracyMetric(MetricBase): f"got {type(seq_len)}.") if seq_len is not None: - masks = seq_lens_to_masks(seq_lens=seq_len) + masks = seq_len_to_mask(seq_len=seq_len) else: masks = None diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 09a3a4c5..f7539fd7 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -1,7 +1,7 @@ """ utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户可以使用的是 :func:`cache_results` 修饰器。 """ -__all__ = ["cache_results"] +__all__ = ["cache_results", "seq_len_to_mask"] import _pickle import inspect import os @@ -600,48 +600,41 @@ def _check_forward_error(forward_func, batch_x, dataset, check_level): warnings.warn(message=_unused_warn) -def seq_lens_to_masks(seq_lens, float=False): +def seq_len_to_mask(seq_len): """ - Convert seq_lens to masks. - :param seq_lens: list, np.ndarray, or torch.LongTensor, shape should all be (B,) - :param float: if True, the return masks is in float type, otherwise it is byte. - :return: list, np.ndarray or torch.Tensor, shape will be (B, max_length) + 将一个表示sequence length的一维数组转换为二维的mask,不包含的位置为0。 + 转变 1-d seq_len到2-d mask. + + Example:: + >>> seq_len = torch.arange(2, 16) + >>> mask = seq_len_to_mask(seq_len) + >>> print(mask.size()) + torch.Size([14, 15]) + >>> seq_len = np.arange(2, 16) + >>> mask = seq_len_to_mask(seq_len) + >>> print(mask.shape) + (14, 15) + + :param np.ndarray,torch.LongTensor seq_len: shape将是(B,) + :return: np.ndarray or torch.Tensor, shape将是(B, max_length)。 元素类似为bool或torch.uint8 """ - if isinstance(seq_lens, np.ndarray): - assert len(np.shape(seq_lens)) == 1, f"seq_lens can only have one dimension, got {len(np.shape(seq_lens))}." - assert seq_lens.dtype in (int, np.int32, np.int64), f"seq_lens can only be integer, not {seq_lens.dtype}." - raise NotImplemented - elif isinstance(seq_lens, torch.Tensor): - assert len(seq_lens.size()) == 1, f"seq_lens can only have one dimension, got {len(seq_lens.size())==1}." - batch_size = seq_lens.size(0) - max_len = seq_lens.max() - indexes = torch.arange(max_len).view(1, -1).repeat(batch_size, 1).to(seq_lens.device).long() - masks = indexes.lt(seq_lens.unsqueeze(1)) - - if float: - masks = masks.float() - - return masks - elif isinstance(seq_lens, list): - raise NotImplemented + if isinstance(seq_len, np.ndarray): + assert len(np.shape(seq_len)) == 1, f"seq_len can only have one dimension, got {len(np.shape(seq_len))}." + max_len = int(seq_len.max()) + broad_cast_seq_len = np.tile(np.arange(max_len), (len(seq_len), 1)) + mask = broad_cast_seq_len [N,L,C_0] pos = self.pos_embedding(words2) # [N,L] -> [N,L,C_1] @@ -418,7 +417,7 @@ class BiaffineParser(GraphParser): """ batch_size, length, _ = pred1.shape - mask = seq_mask(seq_len, length) + mask = seq_len_to_mask(seq_len) flip_mask = (mask == 0) _arc_pred = pred1.clone() _arc_pred.masked_fill_(flip_mask.unsqueeze(1), -float('inf')) @@ -514,7 +513,7 @@ class ParserMetric(MetricBase): if seq_len is None: seq_mask = pred1.new_ones(pred1.size(), dtype=torch.long) else: - seq_mask = seq_lens_to_masks(seq_len.long(), float=False).long() + seq_mask = seq_len_to_mask(seq_len.long()).long() # mask out tag seq_mask[:,0] = 0 head_pred_correct = (pred1 == target1).long() * seq_mask diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 015ae24a..98badd56 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -3,7 +3,7 @@ import torch from .base_model import BaseModel from ..modules import decoder, encoder from ..modules.decoder.CRF import allowed_transitions -from ..modules.utils import seq_mask +from ..core.utils import seq_len_to_mask from ..core.const import Const as C from torch import nn @@ -84,7 +84,7 @@ class SeqLabeling(BaseModel): def _make_mask(self, x, seq_len): batch_size, max_len = x.size(0), x.size(1) - mask = seq_mask(seq_len, max_len) + mask = seq_len_to_mask(seq_len) mask = mask.view(batch_size, max_len) mask = mask.to(x).float() return mask @@ -160,7 +160,7 @@ class AdvSeqLabel(nn.Module): def _make_mask(self, x, seq_len): batch_size, max_len = x.size(0), x.size(1) - mask = seq_mask(seq_len, max_len) + mask = seq_len_to_mask(seq_len) mask = mask.view(batch_size, max_len) mask = mask.to(x).float() return mask diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 1224c4b3..ac0a2e47 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -6,7 +6,7 @@ from ..core.const import Const from ..modules import decoder as Decoder from ..modules import encoder as Encoder from ..modules import aggregator as Aggregator -from ..modules.utils import seq_mask +from ..core.utils import seq_len_to_mask my_inf = 10e12 @@ -75,12 +75,12 @@ class ESIM(BaseModel): hypothesis0 = self.embedding_layer(self.embedding(words2)) if seq_len1 is not None: - seq_len1 = seq_mask(seq_len1, premise0.size(1)) + seq_len1 = seq_len_to_mask(seq_len1) else: seq_len1 = torch.ones(premise0.size(0), premise0.size(1)) seq_len1 = (seq_len1.long()).to(device=premise0.device) if seq_len2 is not None: - seq_len2 = seq_mask(seq_len2, hypothesis0.size(1)) + seq_len2 = seq_len_to_mask(seq_len2) else: seq_len2 = torch.ones(hypothesis0.size(0), hypothesis0.size(1)) seq_len2 = (seq_len2.long()).to(device=hypothesis0.device) diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index 93ee72f6..f7b9028e 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,7 +1,7 @@ """Star-Transformer 的 一个 Pytorch 实现. """ from ..modules.encoder.star_transformer import StarTransformer -from ..core.utils import seq_lens_to_masks +from ..core.utils import seq_len_to_mask from ..modules.utils import get_embeddings from ..core.const import Const @@ -134,7 +134,7 @@ class STSeqLabel(nn.Module): :param seq_len: [batch,] 输入序列的长度 :return output: [batch, num_cls, seq_len] 输出序列中每个元素的分类的概率 """ - mask = seq_lens_to_masks(seq_len) + mask = seq_len_to_mask(seq_len) nodes, _ = self.enc(words, mask) output = self.cls(nodes) output = output.transpose(1,2) # make hidden to be dim 1 @@ -195,7 +195,7 @@ class STSeqCls(nn.Module): :param seq_len: [batch,] 输入序列的长度 :return output: [batch, num_cls] 输出序列的分类的概率 """ - mask = seq_lens_to_masks(seq_len) + mask = seq_len_to_mask(seq_len) nodes, relay = self.enc(words, mask) y = 0.5 * (relay + nodes.max(1)[0]) output = self.cls(y) # [bsz, n_cls] @@ -258,8 +258,8 @@ class STNLICls(nn.Module): :param seq_len2: [batch,] 输入序列2的长度 :return output: [batch, num_cls] 输出分类的概率 """ - mask1 = seq_lens_to_masks(seq_len1) - mask2 = seq_lens_to_masks(seq_len2) + mask1 = seq_len_to_mask(seq_len1) + mask2 = seq_len_to_mask(seq_len2) def enc(seq, mask): nodes, relay = self.enc(seq, mask) return 0.5 * (relay + nodes.max(1)[0]) diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 59efbd53..0d8ec25a 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -2,17 +2,6 @@ import torch from torch import nn from ..utils import initial_parameter -from ..decoder.utils import log_sum_exp - - -def seq_len_to_byte_mask(seq_lens): - # usually seq_lens: LongTensor, batch_size - # return value: ByteTensor, batch_size x max_len - batch_size = seq_lens.size(0) - max_len = seq_lens.max() - broadcast_arange = torch.arange(max_len).view(1, -1).repeat(batch_size, 1).to(seq_lens.device) - mask = broadcast_arange.float().lt(seq_lens.float().view(-1, 1)) - return mask def allowed_transitions(id2label, encoding_type='bio', include_start_end=True): @@ -197,13 +186,13 @@ class ConditionalRandomField(nn.Module): emit_score = logits[i].view(batch_size, 1, n_tags) trans_score = self.trans_m.view(1, n_tags, n_tags) tmp = alpha.view(batch_size, n_tags, 1) + emit_score + trans_score - alpha = log_sum_exp(tmp, 1).masked_fill(flip_mask[i].view(batch_size, 1), 0) + \ + alpha = torch.logsumexp(tmp, 1).masked_fill(flip_mask[i].view(batch_size, 1), 0) + \ alpha.masked_fill(mask[i].byte().view(batch_size, 1), 0) if self.include_start_end_trans: alpha = alpha + self.end_scores.view(1, -1) - return log_sum_exp(alpha, 1) + return torch.logsumexp(alpha, 1) def _gold_score(self, logits, tags, mask): """ diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index dc05fce2..84763e03 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,4 +1,5 @@ -__all__ = ["MLP", "ConditionalRandomField", "viterbi_decode"] +__all__ = ["MLP", "ConditionalRandomField", "viterbi_decode", "allowed_transitions"] from .CRF import ConditionalRandomField from .MLP import MLP from .utils import viterbi_decode +from .CRF import allowed_transitions diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 12e6893b..95b25767 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -2,12 +2,6 @@ __all__ = ["viterbi_decode"] import torch -def log_sum_exp(x, dim=-1): - max_value, _ = x.max(dim=dim, keepdim=True) - res = torch.log(torch.sum(torch.exp(x - max_value), dim=dim, keepdim=True)) + max_value - return res.squeeze(dim) - - def viterbi_decode(logits, transitions, mask=None, unpad=False): """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 337be64d..78851587 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -68,22 +68,6 @@ def initial_parameter(net, initial_method=None): net.apply(weights_init) -def seq_mask(seq_len, max_len): - """ - Create sequence mask. - - :param seq_len: list or torch.Tensor, the lengths of sequences in a batch. - :param max_len: int, the maximum sequence length in a batch. - :return: mask, torch.LongTensor, [batch_size, max_len] - - """ - if not isinstance(seq_len, torch.Tensor): - seq_len = torch.LongTensor(seq_len) - seq_len = seq_len.view(-1, 1).long() # [batch_size, 1] - seq_range = torch.arange(start=0, end=max_len, dtype=torch.long, device=seq_len.device).view(1, -1) # [1, max_len] - return torch.gt(seq_len, seq_range) # [batch_size, max_len] - - def get_embeddings(init_embed): """ 得到词嵌入 TODO diff --git a/test/core/test_utils.py b/test/core/test_utils.py index 7f218db0..b846a32d 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -9,7 +9,8 @@ import os import torch from torch import nn from fastNLP.core.utils import _move_model_to_device, _get_model_device - +import numpy as np +from fastNLP.core.utils import seq_len_to_mask class Model(nn.Module): def __init__(self): @@ -210,3 +211,42 @@ class TestCache(unittest.TestCase): finally: os.remove('test/demo1/demo.pkl') os.rmdir('test/demo1') + + +class TestSeqLenToMask(unittest.TestCase): + + def evaluate_mask_seq_len(self, seq_len, mask): + max_len = int(max(seq_len)) + for i in range(len(seq_len)): + length = seq_len[i] + mask_i = mask[i] + for j in range(max_len): + self.assertEqual(mask_i[j], j0, "CRF loss cannot be less than 0." + self.assertGreater(loss.item(), 0, "CRF loss cannot be less than 0.") From dc855d0ac524e58221571b40a808bd0b1bec430c Mon Sep 17 00:00:00 2001 From: ChenXin Date: Fri, 10 May 2019 20:18:23 +0800 Subject: [PATCH 101/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E8=A1=8C=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/trainer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index ed76ee8e..99003ae7 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -445,6 +445,7 @@ class Trainer(object): _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data, metric_key=metric_key, check_level=check_code_level, batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE)) + # _check_code 是 fastNLP 帮助你检查代码是否正确的方法 。如果你在错误栈中看到这行注释,请认真检查你的代码 self.train_data = train_data self.dev_data = dev_data # If None, No validation. From 96f3fed618d85b1542a03ace010e5c4b6812b72f Mon Sep 17 00:00:00 2001 From: ChenXin Date: Fri, 10 May 2019 20:19:14 +0800 Subject: [PATCH 102/173] =?UTF-8?q?=E5=9C=A8=E6=96=87=E6=A1=A3=E4=B8=AD?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=20dataset=5Floader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/dataset_loader.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 1fe92e23..5df48d71 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -11,7 +11,16 @@ Example:: # ... do stuff """ - +__all__ = [ + 'DataSetLoader', + 'CSVLoader', + 'JsonLoader', + 'ConllLoader', + 'SNLILoader', + 'SSTLoader', + 'PeopleDailyCorpusLoader', + 'Conll2003Loader', +] from nltk.tree import Tree from ..core.dataset import DataSet From 9da72d22ec587694d38d228cddb8a1ef85b0f6c4 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Fri, 10 May 2019 20:20:02 +0800 Subject: [PATCH 103/173] =?UTF-8?q?=E6=8F=90=E7=A4=BA=20Dataset.read=5Fcsv?= =?UTF-8?q?=20=E4=BC=9A=E8=A2=AB=20CSVLoader=20=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index aa3d2d82..d527bf76 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -798,7 +798,10 @@ class DataSet(object): @classmethod def read_csv(cls, csv_path, headers=None, sep=",", dropna=True): """ - 从csv_path路径下以csv的格式读取数据. + .. warning:: + 此方法会在下个版本移除,请使用 :class:`fastNLP.io.CSVLoader` + + 从csv_path路径下以csv的格式读取数据。 :param str csv_path: 从哪里读取csv文件 :param list[str] headers: 如果为None,则使用csv文件的第一行作为header; 如果传入list(str), 则元素的个数必须 From 96e2a6f370ffa2ce33782b952d5514c978edda55 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 10:43:49 +0800 Subject: [PATCH 104/173] =?UTF-8?q?=E5=AE=8C=E6=88=90=20Callback=20?= =?UTF-8?q?=E5=92=8C=20Trainer=20=E4=B9=8B=E9=97=B4=E7=9A=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 52 +++++++++++++++++++---- fastNLP/core/__init__.py | 3 +- fastNLP/core/callback.py | 92 +++++++++++++++++++++++++++++++++++----- fastNLP/core/trainer.py | 61 ++++++++++++-------------- 4 files changed, 155 insertions(+), 53 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 54d96959..9873be72 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -10,14 +10,50 @@ fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.module fastNLP 中最常用的组件可以直接从 fastNLP 包中 import ,他们的文档如下: """ -__all__ = ["Instance", "FieldArray", "Batch", "Vocabulary", "DataSet", "Const", - "Trainer", "Tester", "Callback", - "Padder", "AutoPadder", "EngChar2DPadder", - "AccuracyMetric", "BMESF1PreRecMetric", "SpanFPreRecMetric", "SQuADMetric", - "Optimizer", "SGD", "Adam", - "Sampler", "SequentialSampler", "BucketSampler", "RandomSampler", - "LossFunc", "CrossEntropyLoss", "L1Loss", "BCELoss", "NLLLoss", "LossInForward", - "cache_results"] +__all__ = [ + "Instance", + "FieldArray", + "Batch", + "Vocabulary", + "DataSet", + "Const", + + "Trainer", + "Tester", + + "Callback", + "GradientClipCallback", + "EarlyStopCallback", + "TensorboardCallback", + "LRScheduler", + "ControlC", + + "Padder", + "AutoPadder", + "EngChar2DPadder", + + "AccuracyMetric", + "BMESF1PreRecMetric", + "SpanFPreRecMetric", + "SQuADMetric", + + "Optimizer", + "SGD", + "Adam", + + "Sampler", + "SequentialSampler", + "BucketSampler", + "RandomSampler", + + "LossFunc", + "CrossEntropyLoss", + "L1Loss", "BCELoss", + "NLLLoss", + "LossInForward", + + "cache_results" +] from .core import * from . import models from . import modules diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index ed1bd0c9..52e716f6 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -10,10 +10,11 @@ core 模块里实现了 fastNLP 的核心框架,常用的组件都可以从 fa 对于常用的功能,你只需要在 :doc:`fastNLP` 中查看即可。如果想了解各个子模块的分工,您可以阅读以下文档: + TODO 向导 """ from .batch import Batch -from .callback import Callback +from .callback import Callback, GradientClipCallback, EarlyStopCallback, TensorboardCallback, LRScheduler, ControlC from .const import Const from .dataset import DataSet from .field import FieldArray, Padder, AutoPadder, EngChar2DPadder diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 0a2d052f..abba2790 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -1,7 +1,62 @@ -""" -callback模块实现了 fastNLP 中的Callback类,用于增强 :class:`~fastNLP.Trainer` 类, +r""" +callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class:`~fastNLP.Trainer` 类, + +我们将 :meth:`~fastNLP.Train.train` 这个函数内部分为以下的阶段,在对应阶段会触发相应的调用:: + + callback.on_train_begin() # 开始进行训练 + for i in range(1, n_epochs+1): + callback.on_epoch_begin() # 开始新的epoch + for batch_x, batch_y in Batch: + callback.on_batch_begin(batch_x, batch_y, indices) # batch_x是设置为input的field,batch_y是设置为target的field + 获取模型输出 + callback.on_loss_begin() + 计算loss + callback.on_backward_begin() # 可以进行一些检查,比如loss是否为None + 反向梯度回传 + callback.on_backward_end() # 进行梯度截断等 + 进行参数更新 + callback.on_step_end() + callback.on_batch_end() + # 根据设置进行evaluation,比如这是本epoch最后一个batch或者达到一定step + if do evaluation: + callback.on_valid_begin() + 进行dev data上的验证 + callback.on_valid_end() # 可以进行在其它数据集上进行验证 + callback.on_epoch_end() # epoch结束调用 + callback.on_train_end() # 训练结束 + callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 + 关于Trainer的详细文档,请参见 :doc:`trainer 模块` + +如下面的例子所示,我们可以使用内置的 callback 类,或者继承 :class:`~fastNLP.core.callback.Callback` +定义自己的 callback 类:: + + from fastNLP import Callback, EarlyStopCallback, Trainer, CrossEntropyLoss, AccuracyMetric + from fastNLP.models import CNNText + + start_time = time.time() + + class MyCallback(Callback): + def on_epoch_end(self): + print('{:d}ms\n\n'.format(round((time.time()-start_time)*1000))) + + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, loss=CrossEntropyLoss(), + metrics=AccuracyMetric(), callbacks=[MyCallback(),EarlyStopCallback(10)]) + trainer.train() + """ +__all__ = [ + "Callback", + "GradientClipCallback", + "EarlyStopCallback", + "TensorboardCallback", + "LRScheduler", + "ControlC", + + "CallbackException", + "EarlyStopError" +] import os import torch from ..io.model_io import ModelSaver, ModelLoader @@ -19,7 +74,7 @@ class Callback(object): Callback是fastNLP中被设计用于增强 :class:`~fastNLP.Trainer` 的类。 如果Callback被传递给了 Trainer , 则 Trainer 会在对应的阶段调用Callback的函数, 具体调用时机可以通过 :doc:`trainer 模块` 查看。 - 这是Callback的基类,所有的callback必须继承自这个类(参见 :doc:`callback 模块 ` ) + 这是Callback的基类,所有的callback必须继承自这个类 """ @@ -236,7 +291,7 @@ class CallbackManager(Callback): for env_name, env_val in env.items(): for callback in self.callbacks: - print(callback, env_name, env_val ) + print(callback, env_name, env_val) setattr(callback, '_' + env_name, env_val) # Callback.trainer @_transfer @@ -294,12 +349,15 @@ class CallbackManager(Callback): class GradientClipCallback(Callback): """ + 别名::class:`fastNLP.GradientClipCallback` :class:`fastNLP.core.callback.GradientClipCallback` + 每次backward前,将parameter的gradient clip到某个范围。 :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer 的model中所有参数进行clip :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 - :param str clip_type: 支持'norm', 'value'两种:: + :param str clip_type: 支持'norm', 'value' + 两种:: 1 'norm', 将gradient的norm rescale到[-clip_value, clip_value] @@ -331,8 +389,11 @@ class GradientClipCallback(Callback): class EarlyStopCallback(Callback): """ + 别名::class:`fastNLP.EarlyStopCallback` :class:`fastNLP.core.callback.EarlyStopCallback` + + 多少个epoch没有变好就停止训练,相关类 :class:`EarlyStopError` - :param int patience: 多少个epoch没有变好就停止训练 + :param int patience: epoch的数量 """ def __init__(self, patience): @@ -358,11 +419,10 @@ class EarlyStopCallback(Callback): class LRScheduler(Callback): - """对PyTorch LR Scheduler的包装以使得其可以被Trainer所使用 - - Example:: + """ + 别名::class:`fastNLP.LRScheduler` :class:`fastNLP.core.callback.LRScheduler` - from fastNLP import LRScheduler + 对PyTorch LR Scheduler的包装以使得其可以被Trainer所使用 :param torch.optim.lr_scheduler._LRScheduler lr_scheduler: PyTorch的lr_scheduler """ @@ -382,6 +442,7 @@ class LRScheduler(Callback): class ControlC(Callback): """ + 别名::class:`fastNLP.ControlC` :class:`fastNLP.core.callback.ControlC` :param bool quit_all: 若为True,则检测到control+C 直接退出程序;否则只退出Trainer """ @@ -418,6 +479,8 @@ class SmoothValue(object): class LRFinder(Callback): """ + 别名::class:`fastNLP.LRFinder` :class:`fastNLP.core.callback.LRFinder` + 用第一个 epoch 找最佳的学习率,从第二个epoch开始应用它 :param float start_lr: 学习率下界 @@ -442,7 +505,7 @@ class LRFinder(Callback): def lr_gen(self): scale = (self.end_lr - self.start_lr) / self.batch_per_epoch return (self.start_lr + scale * (step + 1) for step in range(self.batch_per_epoch)) - + @property def num_it(self): return self.batch_per_epoch @@ -487,10 +550,17 @@ class LRFinder(Callback): class TensorboardCallback(Callback): """ + 别名::class:`fastNLP.TensorboardCallback` :class:`fastNLP.core.callback.TensorboardCallback` + 接受以下一个或多个字符串作为参数: - "model" - "loss" - "metric" + + .. warning:: + fastNLP 已停止对此功能的维护,请等待 fastNLP 兼容 PyTorch1.1 的下一个版本。 + 或者使用和 fastNLP 高度配合的 fitlog(参见 :doc:`/user/with_fitlog` )。 + """ def __init__(self, *options): diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 99003ae7..2ad92f6b 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -1,4 +1,4 @@ -""" +r""" Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在不同训练任务中重复撰以下步骤的代码 (1) epoch循环; @@ -93,7 +93,10 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 尽管fastNLP使用了映射机制来使得loss的计算变得比较灵活,但有些情况下loss必须在模型中进行计算,比如使用了CRF的模型。 fastNLP中提供了 :class:`~fastNLP.LossInForward` 这个loss。 这个loss的原理是直接在forward()的返回结果中找到loss_key(默认寻找'loss')指定的那个tensor,并使用它作为loss。 - 如果Trainer初始化没有提供loss则默认使用 :class:`~fastNLP.LossInForward` 。TODO 补充一个例子 详细例子可以参照 + 如果Trainer初始化没有提供loss则默认使用 :class:`~fastNLP.LossInForward` 。 + + .. todo:: + 补充一个例子 详细例子可以参照 1.3 Metric :mod:`Metric` 使用了与上述Loss一样的策略,即使用名称进行匹配。 @@ -102,7 +105,10 @@ Trainer在fastNLP中用于组织单任务的训练过程,可以避免用户在 在进行验证时,可能用到的计算与forward()中不太一致,没有办法直接从forward()的结果中得到预测值,这时模型可以提供一个predict()方法, 如果提供的模型具有predict方法,则在模型验证时将调用predict()方法获取预测结果, 传入到predict()的参数也是从DataSet中被设置为input的field中选择出来的; - 与forward()一样,返回值需要为一个dict。 TODO 补充一个例子 具体例子可以参考 + 与forward()一样,返回值需要为一个dict。 + + .. todo:: + 补充一个例子 具体例子可以参考 2 Trainer的代码检查 由于在fastNLP中采取了映射的机制,所以难免可能存在对应出错的情况。Trainer提供一种映射检查机制,可以通过check_code_level来进行控制 @@ -267,37 +273,26 @@ Example2.3 虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能,比如负采样,learning rate decay, Early Stop等。 为了解决这个问题fastNLP引入了callback的机制,:class:`~fastNLP.Callback` 是一种在Trainer训练过程中特定阶段会运行的函数集合, 所有的 :class:`~fastNLP.Callback` 都具有on_*(比如on_train_start, on_backward_begin)等函数。 - 如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用。 - - 我们将Train.train()这个函数内部分为以下的阶段,在对应阶段会触发相应的调用。 - - Example:: + 如果 Callback 实现了该函数,则Trainer运行至对应阶段,会进行调用,例如:: + + from fastNLP import Callback, EarlyStopCallback, Trainer, CrossEntropyLoss, AccuracyMetric + from fastNLP.models import CNNText - callback.on_train_begin() # 开始进行训练 - for i in range(1, n_epochs+1): - callback.on_epoch_begin() # 开始新的epoch - for batch_x, batch_y in Batch: - callback.on_batch_begin(batch_x, batch_y, indices) # batch_x是设置为input的field,batch_y是设置为target的field - 获取模型输出 - callback.on_loss_begin() - 计算loss - callback.on_backward_begin() # 可以进行一些检查,比如loss是否为None - 反向梯度回传 - callback.on_backward_end() # 进行梯度截断等 - 进行参数更新 - callback.on_step_end() - callback.on_batch_end() - # 根据设置进行evaluation,比如这是本epoch最后一个batch或者达到一定step - if do evaluation: - callback.on_valid_begin() - 进行dev data上的验证 - callback.on_valid_end() # 可以进行在其它数据集上进行验证 - callback.on_epoch_end() # epoch结束调用 - callback.on_train_end() # 训练结束 - callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 - - fastNLP已经自带了很多callback函数供使用,可以参考 :class:`~fastNLP.Callback` 。 - TODO callback的例子 一些关于callback的例子,请参考 + start_time = time.time() + + class MyCallback(Callback): + def on_epoch_end(self): + print('{:d}ms\n\n'.format(round((time.time()-start_time)*1000))) + + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, loss=CrossEntropyLoss(), + metrics=AccuracyMetric(), callbacks=[MyCallback(),EarlyStopCallback(10)]) + trainer.train() + + 这里,我们通过继承 :class:`~fastNLP.Callback` 类定义了自己的 callback 的,并和内置的 :class:`~fastNLP.EarlyStopCallback` + 一起传给了 :class:`~fastNLP.Trainer` ,增强了 :class:`~fastNLP.Trainer` 的功能 + + fastNLP已经自带了很多callback函数供使用,可以参考 :doc:`fastNLP.core.callback` 。 """ From a79a966df47d7b306b617e1d24ad05d6e01d3e9b Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 12 May 2019 11:00:07 +0800 Subject: [PATCH 105/173] =?UTF-8?q?index=E6=9B=B4=E6=96=B0=E4=BA=86?= =?UTF-8?q?=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 23b14877..d441601e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,12 +4,13 @@ fastNLP 中文文档 fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地完成一个命名实体识别(NER)、中文分词或文本分类任务; 也可以使用他构建许多复杂的网络模型,进行科研。它具有如下的特性: -- 代码简洁易懂,有着详尽的中文文档以供查阅; -- 深度学习的各个阶段划分明确,配合 fitlog 使用让科研更轻松; -- 内置多种常见模型 (TODO); -- 基于 PyTorch ,方便从原生 PyTorch 代码迁入,并能使用 PyTorch 中的各种组件; -- 便于 seq2seq; -- 便于 fine-tune +- 统一的Tabular式数据容器,让数据预处理过程简洁明了。内置多种数据集的DataSet Loader,省去预处理代码。 +- 各种方便的NLP工具,例如预处理embedding加载; 中间数据cache等; +- 详尽的中文文档以供查阅; +- 提供诸多高级模块,例如Variational LSTM, Transformer, CRF等; +- 封装CNNText,Biaffine等模型可供直接使用; +- 便捷且具有扩展性的训练器; 提供多种内置callback函数,方便实验记录、异常捕获等。 + 内置的模块 ------------ @@ -17,21 +18,20 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 (TODO) -A deep learning NLP model is the composition of three types of modules: +主要包含了以下的三大模块: +-----------------------+-----------------------+-----------------------+ | module type | functionality | example | +=======================+=======================+=======================+ -| encoder | encode the input into | embedding, RNN, CNN, | -| | some abstract | transformer | -| | representation | | +| encoder | 将输入编码为具有具 | embedding, RNN, CNN, | +| | 有表示能力的向量 | transformer | +-----------------------+-----------------------+-----------------------+ -| aggregator | aggregate and reduce | self-attention, | -| | information | max-pooling | +| aggregator | 从多个向量中聚合信息 | self-attention, | +| | | max-pooling | +-----------------------+-----------------------+-----------------------+ -| decoder | decode the | MLP, CRF | -| | representation into | | -| | the output | | +| decoder | 将具有某种表示意义的 | MLP, CRF | +| | 向量解码为需要的输出 | | +| | 形式 | | +-----------------------+-----------------------+-----------------------+ From 519c8e8a869624422e73a2948fe12d8c4b2ab7ed Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 12 May 2019 15:37:18 +0800 Subject: [PATCH 106/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99?= =?UTF-8?q?=E7=9A=84print?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index abba2790..ba73f101 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -291,7 +291,6 @@ class CallbackManager(Callback): for env_name, env_val in env.items(): for callback in self.callbacks: - print(callback, env_name, env_val) setattr(callback, '_' + env_name, env_val) # Callback.trainer @_transfer From 349194fe85e8069ed7fa29028003fa43dea073c6 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sun, 12 May 2019 16:47:44 +0800 Subject: [PATCH 107/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E7=94=A8=E4=BA=8E?= =?UTF-8?q?=E5=88=86=E8=AF=8D=E7=9A=84metric=EF=BC=8C=E5=9B=A0=E4=B8=BA?= =?UTF-8?q?=E6=9C=89=E5=8F=AF=E8=83=BD=E5=BC=95=E8=B5=B7=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/api/api.py | 12 +-- fastNLP/core/__init__.py | 2 +- fastNLP/core/metrics.py | 143 --------------------------------- fastNLP/modules/decoder/CRF.py | 12 +-- test/core/test_metrics.py | 25 ------ 5 files changed, 13 insertions(+), 181 deletions(-) diff --git a/fastNLP/api/api.py b/fastNLP/api/api.py index 351b210d..2e7cbfcf 100644 --- a/fastNLP/api/api.py +++ b/fastNLP/api/api.py @@ -206,7 +206,7 @@ class POS(API): prediction[idx] = list(prediction[idx]) + ([0] * (max_length - len(prediction[idx]))) truth[idx] = list(truth[idx]) + ([0] * (max_length - len(truth[idx]))) evaluator = SpanFPreRecMetric(tag_vocab=tag_vocab, pred="predict", target="truth", - seq_lens="word_seq_origin_len") + seq_len="word_seq_origin_len") evaluator({"predict": torch.Tensor(prediction), "word_seq_origin_len": torch.Tensor(seq_len)}, {"truth": torch.Tensor(truth)}) test_result = evaluator.get_metric() @@ -300,15 +300,15 @@ class CWS(API): pp(te_dataset) from ..core.tester import Tester - from ..core.metrics import BMESF1PreRecMetric + from ..core.metrics import SpanFPreRecMetric - tester = Tester(data=te_dataset, model=cws_model, metrics=BMESF1PreRecMetric(target='target'), batch_size=64, + tester = Tester(data=te_dataset, model=cws_model, metrics=SpanFPreRecMetric(tag_proc.get_vocab()), batch_size=64, verbose=0) eval_res = tester.test() - f1 = eval_res['BMESF1PreRecMetric']['f'] - pre = eval_res['BMESF1PreRecMetric']['pre'] - rec = eval_res['BMESF1PreRecMetric']['rec'] + f1 = eval_res['SpanFPreRecMetric']['f'] + pre = eval_res['SpanFPreRecMetric']['pre'] + rec = eval_res['SpanFPreRecMetric']['rec'] # print("f1:{:.2f}, pre:{:.2f}, rec:{:.2f}".format(f1, pre, rec)) return {"F1": f1, "precision": pre, "recall": rec} diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 52e716f6..3c5b3f42 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -20,7 +20,7 @@ from .dataset import DataSet from .field import FieldArray, Padder, AutoPadder, EngChar2DPadder from .instance import Instance from .losses import LossFunc, CrossEntropyLoss, L1Loss, BCELoss, NLLLoss, LossInForward -from .metrics import AccuracyMetric, BMESF1PreRecMetric, SpanFPreRecMetric, SQuADMetric +from .metrics import AccuracyMetric, SpanFPreRecMetric, SQuADMetric from .optimizer import Optimizer, SGD, Adam from .sampler import SequentialSampler, BucketSampler, RandomSampler, Sampler from .tester import Tester diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 5cdfdc44..0354e7cc 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -618,149 +618,6 @@ class SpanFPreRecMetric(MetricBase): return f, pre, rec -class BMESF1PreRecMetric(MetricBase): - """ - 别名::class:`fastNLP.BMESF1PreRecMetric` :class:`fastNLP.core.metrics.BMESF1PreRecMetric` - - 按照BMES标注方式计算f1, precision, recall。由于可能存在非法tag,比如"BS",所以需要用以下的表格做转换,cur_B意思是当前tag是B, - next_B意思是后一个tag是B。则cur_B=S,即将当前被predict是B的tag标为S;next_M=B, 即将后一个被predict是M的tag标为B - - +-------+---------+----------+----------+---------+---------+ - | | next_B | next_M | next_E | next_S | end | - +=======+=========+==========+==========+=========+=========+ - | start | 合法 | next_M=B | next_E=S | 合法 | -- | - +-------+---------+----------+----------+---------+---------+ - | cur_B | cur_B=S | 合法 | 合法 | cur_B=S | cur_B=S | - +-------+---------+----------+----------+---------+---------+ - | cur_M | cur_M=E | 合法 | 合法 | cur_M=E | cur_M=E | - +-------+---------+----------+----------+---------+---------+ - | cur_E | 合法 | next_M=B | next_E=S | 合法 | 合法 | - +-------+---------+----------+----------+---------+---------+ - | cur_S | 合法 | next_M=B | next_E=S | 合法 | 合法 | - +-------+---------+----------+----------+---------+---------+ - - 举例: - prediction为BSEMS,会被认为是SSSSS. - - 本Metric不检验target的合法性,请务必保证target的合法性。 - pred的形状应该为(batch_size, max_len) 或 (batch_size, max_len, 4)。 - target形状为 (batch_size, max_len) - seq_lens形状为 (batch_size, ) - - 需要申明BMES这四种tag中,各种tag对应的idx。所有不为b_idx, m_idx, e_idx, s_idx的数字都认为是s_idx。 - - :param b_idx: int, Begin标签所对应的tag idx. - :param m_idx: int, Middle标签所对应的tag idx. - :param e_idx: int, End标签所对应的tag idx. - :param s_idx: int, Single标签所对应的tag idx - :param pred: str, 用该key在evaluate()时从传入dict中取出prediction数据。 为None,则使用'pred'取数据 - :param target: str, 用该key在evaluate()时从传入dict中取出target数据。 为None,则使用'target'取数据 - :param seq_len: str, 用该key在evaluate()时从传入dict中取出seqence length数据。为None,则使用'seq_len'取数据。 - """ - - def __init__(self, b_idx=0, m_idx=1, e_idx=2, s_idx=3, pred=None, target=None, seq_len=None): - super().__init__() - - self._init_param_map(pred=pred, target=target, seq_len=seq_len) - - self.yt_wordnum = 0 - self.yp_wordnum = 0 - self.corr_num = 0 - - self.b_idx = b_idx - self.m_idx = m_idx - self.e_idx = e_idx - self.s_idx = s_idx - # 还原init处介绍的矩阵 - self._valida_matrix = { - -1: [(-1, -1), (1, self.b_idx), (1, self.s_idx), (-1, -1)], # magic start idx - self.b_idx:[(0, self.s_idx), (-1, -1), (-1, -1), (0, self.s_idx), (0, self.s_idx)], - self.m_idx:[(0, self.e_idx), (-1, -1), (-1, -1), (0, self.e_idx), (0, self.e_idx)], - self.e_idx:[(-1, -1), (1, self.b_idx), (1, self.s_idx), (-1, -1), (-1, -1)], - self.s_idx:[(-1, -1), (1, self.b_idx), (1, self.s_idx), (-1, -1), (-1, -1)], - } - - def _validate_tags(self, tags): - """ - 给定一个tag的Tensor,返回合法tag - - :param torch.Tensor tags: [seq_len] - :return: 返回修改为合法tag的list - """ - assert len(tags)!=0 - assert isinstance(tags, torch.Tensor) and len(tags.size())==1 - padded_tags = [-1, *tags.tolist(), -1] - for idx in range(len(padded_tags)-1): - cur_tag = padded_tags[idx] - if cur_tag not in self._valida_matrix: - cur_tag = self.s_idx - if padded_tags[idx+1] not in self._valida_matrix: - padded_tags[idx+1] = self.s_idx - next_tag = padded_tags[idx+1] - shift_tag = self._valida_matrix[cur_tag][next_tag] - if shift_tag[0]!=-1: - padded_tags[idx+shift_tag[0]] = shift_tag[1] - - return padded_tags[1:-1] - - def evaluate(self, pred, target, seq_len): - """evaluate函数将针对一个批次的预测结果做评价指标的累计 - - :param pred: [batch, seq_len] 或者 [batch, seq_len, 4] - :param target: [batch, seq_len] - :param seq_len: [batch] - :return: - """ - if not isinstance(pred, torch.Tensor): - raise TypeError(f"`pred` in {_get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(pred)}.") - if not isinstance(target, torch.Tensor): - raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(target)}.") - - if not isinstance(seq_len, torch.Tensor): - raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," - f"got {type(seq_len)}.") - - if pred.size() == target.size() and len(target.size()) == 2: - pass - elif len(pred.size()) == len(target.size()) + 1 and len(target.size()) == 2: - pred = pred.argmax(dim=-1) - else: - raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " - f"size:{pred.size()}, target should have size: {pred.size()} or " - f"{pred.size()[:-1]}, got {target.size()}.") - - for idx in range(len(pred)): - target_tags = target[idx][:seq_len[idx]].tolist() - pred_tags = pred[idx][:seq_len[idx]] - pred_tags = self._validate_tags(pred_tags) - start_idx = 0 - for t_idx, (t_tag, p_tag) in enumerate(zip(target_tags, pred_tags)): - if t_tag in (self.s_idx, self.e_idx): - self.yt_wordnum += 1 - corr_flag = True - for i in range(start_idx, t_idx+1): - if target_tags[i]!=pred_tags[i]: - corr_flag = False - if corr_flag: - self.corr_num += 1 - start_idx = t_idx + 1 - if p_tag in (self.s_idx, self.e_idx): - self.yp_wordnum += 1 - - def get_metric(self, reset=True): - """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" - P = self.corr_num / (self.yp_wordnum + 1e-12) - R = self.corr_num / (self.yt_wordnum + 1e-12) - F = 2 * P * R / (P + R + 1e-12) - evaluate_result = {'f': round(F, 6), 'pre':round(P, 6), 'rec': round(R, 6)} - if reset: - self.yp_wordnum = 0 - self.yt_wordnum = 0 - self.corr_num = 0 - return evaluate_result - def _prepare_metrics(metrics): """ diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 0d8ec25a..2c9080b2 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -4,12 +4,12 @@ from torch import nn from ..utils import initial_parameter -def allowed_transitions(id2label, encoding_type='bio', include_start_end=True): +def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): """ 给定一个id到label的映射表,返回所有可以跳转的(from_tag_id, to_tag_id)列表。 - :param dict id2label: key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 - "B-NN", "M-NN", tag和label之间一定要用"-"隔开。一般可以通过Vocabulary.get_id2word()得到id2label。 + :param dict id2target: key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 + "B-NN", "M-NN", tag和label之间一定要用"-"隔开。一般可以通过Vocabulary.idx2word得到id2label。 :param str encoding_type: 支持"bio", "bmes", "bmeso"。 :param bool include_start_end: 是否包含开始与结尾的转换。比如在bio中,b/o可以在开头,但是i不能在开头; 为True,返回的结果中会包含(start_idx, b_idx), (start_idx, o_idx), 但是不包含(start_idx, i_idx); @@ -17,12 +17,12 @@ def allowed_transitions(id2label, encoding_type='bio', include_start_end=True): 为False, 返回的结果中不含与开始结尾相关的内容 :return: List[Tuple(int, int)]], 内部的Tuple是可以进行跳转的(from_tag_id, to_tag_id)。 """ - num_tags = len(id2label) + num_tags = len(id2target) start_idx = num_tags end_idx = num_tags + 1 encoding_type = encoding_type.lower() allowed_trans = [] - id_label_lst = list(id2label.items()) + id_label_lst = list(id2target.items()) if include_start_end: id_label_lst += [(start_idx, 'start'), (end_idx, 'end')] def split_tag_label(from_label): @@ -160,7 +160,7 @@ class ConditionalRandomField(nn.Module): if allowed_transitions is None: constrain = torch.zeros(num_tags + 2, num_tags + 2) else: - constrain = torch.new_full((num_tags+2, num_tags+2), fill_value=-10000.0, dtype=torch.float) + constrain = torch.full((num_tags+2, num_tags+2), fill_value=-10000.0, dtype=torch.float) for from_tag_id, to_tag_id in allowed_transitions: constrain[from_tag_id, to_tag_id] = 0 self._constrain = nn.Parameter(constrain, requires_grad=False) diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index babfb4aa..db508e39 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -351,31 +351,6 @@ class SpanF1PreRecMetric(unittest.TestCase): # fastnlp_bmes_metric.get_metric()) -class TestBMESF1PreRecMetric(unittest.TestCase): - def test_case1(self): - seq_lens = torch.LongTensor([4, 2]) - pred = torch.randn(2, 4, 4) - target = torch.LongTensor([[0, 1, 2, 3], - [3, 3, 0, 0]]) - pred_dict = {'pred': pred} - target_dict = {'target': target, 'seq_len': seq_lens} - - metric = BMESF1PreRecMetric() - metric(pred_dict, target_dict) - metric.get_metric() - - def test_case2(self): - # 测试相同两个seqence,应该给出{f1: 1, precision:1, recall:1} - seq_lens = torch.LongTensor([4, 2]) - target = torch.LongTensor([[0, 1, 2, 3], - [3, 3, 0, 0]]) - pred_dict = {'pred': target} - target_dict = {'target': target, 'seq_len': seq_lens} - - metric = BMESF1PreRecMetric() - metric(pred_dict, target_dict) - self.assertDictEqual(metric.get_metric(), {'f': 1.0, 'pre': 1.0, 'rec': 1.0}) - class TestUsefulFunctions(unittest.TestCase): # 测试metrics.py中一些看上去挺有用的函数 From 58e73631147e5a4a495025c2cb6f2518d81951d2 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 17:55:54 +0800 Subject: [PATCH 108/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E4=B8=AD=E6=96=87=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.component.bert_tokenizer.rst | 7 ------- docs/source/fastNLP.component.rst | 15 --------------- docs/source/fastNLP.core.batch.rst | 4 ++-- docs/source/fastNLP.core.callback.rst | 4 ++-- docs/source/fastNLP.core.const.rst | 4 ++-- docs/source/fastNLP.core.dataset.rst | 4 ++-- docs/source/fastNLP.core.field.rst | 4 ++-- docs/source/fastNLP.core.instance.rst | 4 ++-- docs/source/fastNLP.core.losses.rst | 4 ++-- docs/source/fastNLP.core.metrics.rst | 4 ++-- docs/source/fastNLP.core.optimizer.rst | 4 ++-- docs/source/fastNLP.core.rst | 4 ++-- docs/source/fastNLP.core.sampler.rst | 4 ++-- docs/source/fastNLP.core.tester.rst | 4 ++-- docs/source/fastNLP.core.trainer.rst | 4 ++-- docs/source/fastNLP.core.utils.rst | 4 ++-- docs/source/fastNLP.core.vocabulary.rst | 4 ++-- docs/source/fastNLP.io.base_loader.rst | 4 ++-- docs/source/fastNLP.io.dataset_loader.rst | 4 ++-- docs/source/fastNLP.io.embed_loader.rst | 4 ++-- docs/source/fastNLP.io.model_io.rst | 4 ++-- docs/source/fastNLP.io.rst | 4 ++-- docs/source/fastNLP.models.base_model.rst | 4 ++-- docs/source/fastNLP.models.bert.rst | 4 ++-- docs/source/fastNLP.models.biaffine_parser.rst | 4 ++-- .../fastNLP.models.cnn_text_classification.rst | 4 ++-- docs/source/fastNLP.models.enas_controller.rst | 4 ++-- docs/source/fastNLP.models.enas_model.rst | 4 ++-- docs/source/fastNLP.models.enas_trainer.rst | 4 ++-- docs/source/fastNLP.models.enas_utils.rst | 4 ++-- docs/source/fastNLP.models.rst | 4 ++-- docs/source/fastNLP.models.sequence_labeling.rst | 4 ++-- docs/source/fastNLP.models.snli.rst | 4 ++-- docs/source/fastNLP.models.star_transformer.rst | 4 ++-- .../fastNLP.modules.aggregator.attention.rst | 4 ++-- .../source/fastNLP.modules.aggregator.pooling.rst | 4 ++-- docs/source/fastNLP.modules.aggregator.rst | 6 +++--- docs/source/fastNLP.modules.decoder.CRF.rst | 4 ++-- docs/source/fastNLP.modules.decoder.MLP.rst | 4 ++-- docs/source/fastNLP.modules.decoder.rst | 6 +++--- docs/source/fastNLP.modules.decoder.utils.rst | 4 ++-- docs/source/fastNLP.modules.encoder.bert.rst | 4 ++-- .../fastNLP.modules.encoder.char_encoder.rst | 4 ++-- .../fastNLP.modules.encoder.conv_maxpool.rst | 4 ++-- docs/source/fastNLP.modules.encoder.embedding.rst | 4 ++-- docs/source/fastNLP.modules.encoder.lstm.rst | 4 ++-- docs/source/fastNLP.modules.encoder.rst | 6 +++--- .../fastNLP.modules.encoder.star_transformer.rst | 4 ++-- .../fastNLP.modules.encoder.transformer.rst | 4 ++-- .../fastNLP.modules.encoder.variational_rnn.rst | 4 ++-- docs/source/fastNLP.modules.rst | 4 ++-- docs/source/fastNLP.rst | 8 ++++---- docs/source/index.rst | 7 +------ 53 files changed, 106 insertions(+), 133 deletions(-) delete mode 100644 docs/source/fastNLP.component.bert_tokenizer.rst delete mode 100644 docs/source/fastNLP.component.rst diff --git a/docs/source/fastNLP.component.bert_tokenizer.rst b/docs/source/fastNLP.component.bert_tokenizer.rst deleted file mode 100644 index ccfc50c6..00000000 --- a/docs/source/fastNLP.component.bert_tokenizer.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.component.bert\_tokenizer module -======================================== - -.. automodule:: fastNLP.component.bert_tokenizer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.component.rst b/docs/source/fastNLP.component.rst deleted file mode 100644 index 3e15fa47..00000000 --- a/docs/source/fastNLP.component.rst +++ /dev/null @@ -1,15 +0,0 @@ -fastNLP.component package -========================= - -.. automodule:: fastNLP.component - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - - fastNLP.component.bert_tokenizer - diff --git a/docs/source/fastNLP.core.batch.rst b/docs/source/fastNLP.core.batch.rst index b0294bac..33a5b730 100644 --- a/docs/source/fastNLP.core.batch.rst +++ b/docs/source/fastNLP.core.batch.rst @@ -1,5 +1,5 @@ -fastNLP.core.batch module -========================= +fastNLP.core.batch +================== .. automodule:: fastNLP.core.batch :members: diff --git a/docs/source/fastNLP.core.callback.rst b/docs/source/fastNLP.core.callback.rst index 075712e8..31ec627b 100644 --- a/docs/source/fastNLP.core.callback.rst +++ b/docs/source/fastNLP.core.callback.rst @@ -1,5 +1,5 @@ -fastNLP.core.callback module -============================ +fastNLP.core.callback +===================== .. automodule:: fastNLP.core.callback :members: diff --git a/docs/source/fastNLP.core.const.rst b/docs/source/fastNLP.core.const.rst index 288fcf50..c9e3bd97 100644 --- a/docs/source/fastNLP.core.const.rst +++ b/docs/source/fastNLP.core.const.rst @@ -1,5 +1,5 @@ -fastNLP.core.const module -========================= +fastNLP.core.const +================== .. automodule:: fastNLP.core.const :members: diff --git a/docs/source/fastNLP.core.dataset.rst b/docs/source/fastNLP.core.dataset.rst index 24e1ab4e..b377cb0f 100644 --- a/docs/source/fastNLP.core.dataset.rst +++ b/docs/source/fastNLP.core.dataset.rst @@ -1,5 +1,5 @@ -fastNLP.core.dataset module -=========================== +fastNLP.core.dataset +==================== .. automodule:: fastNLP.core.dataset :members: diff --git a/docs/source/fastNLP.core.field.rst b/docs/source/fastNLP.core.field.rst index 23eb47bc..7686e79a 100644 --- a/docs/source/fastNLP.core.field.rst +++ b/docs/source/fastNLP.core.field.rst @@ -1,5 +1,5 @@ -fastNLP.core.field module -========================= +fastNLP.core.field +================== .. automodule:: fastNLP.core.field :members: diff --git a/docs/source/fastNLP.core.instance.rst b/docs/source/fastNLP.core.instance.rst index db731fe9..14393a91 100644 --- a/docs/source/fastNLP.core.instance.rst +++ b/docs/source/fastNLP.core.instance.rst @@ -1,5 +1,5 @@ -fastNLP.core.instance module -============================ +fastNLP.core.instance +===================== .. automodule:: fastNLP.core.instance :members: diff --git a/docs/source/fastNLP.core.losses.rst b/docs/source/fastNLP.core.losses.rst index 7f4e02b6..d2dd492b 100644 --- a/docs/source/fastNLP.core.losses.rst +++ b/docs/source/fastNLP.core.losses.rst @@ -1,5 +1,5 @@ -fastNLP.core.losses module -========================== +fastNLP.core.losses +=================== .. automodule:: fastNLP.core.losses :members: diff --git a/docs/source/fastNLP.core.metrics.rst b/docs/source/fastNLP.core.metrics.rst index d700a525..69afff36 100644 --- a/docs/source/fastNLP.core.metrics.rst +++ b/docs/source/fastNLP.core.metrics.rst @@ -1,5 +1,5 @@ -fastNLP.core.metrics module -=========================== +fastNLP.core.metrics +==================== .. automodule:: fastNLP.core.metrics :members: diff --git a/docs/source/fastNLP.core.optimizer.rst b/docs/source/fastNLP.core.optimizer.rst index 737fc430..e2100d2e 100644 --- a/docs/source/fastNLP.core.optimizer.rst +++ b/docs/source/fastNLP.core.optimizer.rst @@ -1,5 +1,5 @@ -fastNLP.core.optimizer module -============================= +fastNLP.core.optimizer +====================== .. automodule:: fastNLP.core.optimizer :members: diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst index 01c59e21..932c6b21 100644 --- a/docs/source/fastNLP.core.rst +++ b/docs/source/fastNLP.core.rst @@ -1,4 +1,4 @@ -fastNLP.core package +fastNLP.core 模块 ==================== .. automodule:: fastNLP.core @@ -6,7 +6,7 @@ fastNLP.core package :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.core.sampler.rst b/docs/source/fastNLP.core.sampler.rst index a827d49c..1810d59c 100644 --- a/docs/source/fastNLP.core.sampler.rst +++ b/docs/source/fastNLP.core.sampler.rst @@ -1,5 +1,5 @@ -fastNLP.core.sampler module -=========================== +fastNLP.core.sampler +==================== .. automodule:: fastNLP.core.sampler :members: diff --git a/docs/source/fastNLP.core.tester.rst b/docs/source/fastNLP.core.tester.rst index 30cebe28..a9e7e09f 100644 --- a/docs/source/fastNLP.core.tester.rst +++ b/docs/source/fastNLP.core.tester.rst @@ -1,5 +1,5 @@ -fastNLP.core.tester module -========================== +fastNLP.core.tester +=================== .. automodule:: fastNLP.core.tester :members: diff --git a/docs/source/fastNLP.core.trainer.rst b/docs/source/fastNLP.core.trainer.rst index 648feb9d..9e518d4b 100644 --- a/docs/source/fastNLP.core.trainer.rst +++ b/docs/source/fastNLP.core.trainer.rst @@ -1,5 +1,5 @@ -fastNLP.core.trainer module -=========================== +fastNLP.core.trainer +==================== .. automodule:: fastNLP.core.trainer :members: diff --git a/docs/source/fastNLP.core.utils.rst b/docs/source/fastNLP.core.utils.rst index 2bec7f62..fcd3f50c 100644 --- a/docs/source/fastNLP.core.utils.rst +++ b/docs/source/fastNLP.core.utils.rst @@ -1,5 +1,5 @@ -fastNLP.core.utils module -========================= +fastNLP.core.utils +================== .. automodule:: fastNLP.core.utils :members: diff --git a/docs/source/fastNLP.core.vocabulary.rst b/docs/source/fastNLP.core.vocabulary.rst index 98d8d24d..b3bf4bac 100644 --- a/docs/source/fastNLP.core.vocabulary.rst +++ b/docs/source/fastNLP.core.vocabulary.rst @@ -1,5 +1,5 @@ -fastNLP.core.vocabulary module -============================== +fastNLP.core.vocabulary +======================= .. automodule:: fastNLP.core.vocabulary :members: diff --git a/docs/source/fastNLP.io.base_loader.rst b/docs/source/fastNLP.io.base_loader.rst index b3375f74..c1f9ac14 100644 --- a/docs/source/fastNLP.io.base_loader.rst +++ b/docs/source/fastNLP.io.base_loader.rst @@ -1,5 +1,5 @@ -fastNLP.io.base\_loader module -============================== +fastNLP.io.base\_loader +======================= .. automodule:: fastNLP.io.base_loader :members: diff --git a/docs/source/fastNLP.io.dataset_loader.rst b/docs/source/fastNLP.io.dataset_loader.rst index 89f9b165..d6663e59 100644 --- a/docs/source/fastNLP.io.dataset_loader.rst +++ b/docs/source/fastNLP.io.dataset_loader.rst @@ -1,5 +1,5 @@ -fastNLP.io.dataset\_loader module -================================= +fastNLP.io.dataset\_loader +========================== .. automodule:: fastNLP.io.dataset_loader :members: diff --git a/docs/source/fastNLP.io.embed_loader.rst b/docs/source/fastNLP.io.embed_loader.rst index 1f135155..7a8e730c 100644 --- a/docs/source/fastNLP.io.embed_loader.rst +++ b/docs/source/fastNLP.io.embed_loader.rst @@ -1,5 +1,5 @@ -fastNLP.io.embed\_loader module -=============================== +fastNLP.io.embed\_loader +======================== .. automodule:: fastNLP.io.embed_loader :members: diff --git a/docs/source/fastNLP.io.model_io.rst b/docs/source/fastNLP.io.model_io.rst index 75f1df69..50d4c25a 100644 --- a/docs/source/fastNLP.io.model_io.rst +++ b/docs/source/fastNLP.io.model_io.rst @@ -1,5 +1,5 @@ -fastNLP.io.model\_io module -=========================== +fastNLP.io.model\_io +==================== .. automodule:: fastNLP.io.model_io :members: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index 610b1ce6..ae28573d 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -1,4 +1,4 @@ -fastNLP.io package +fastNLP.io 模块 ================== .. automodule:: fastNLP.io @@ -6,7 +6,7 @@ fastNLP.io package :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.models.base_model.rst b/docs/source/fastNLP.models.base_model.rst index 2537f75f..e1d4d64f 100644 --- a/docs/source/fastNLP.models.base_model.rst +++ b/docs/source/fastNLP.models.base_model.rst @@ -1,5 +1,5 @@ -fastNLP.models.base\_model module -================================= +fastNLP.models.base\_model +========================== .. automodule:: fastNLP.models.base_model :members: diff --git a/docs/source/fastNLP.models.bert.rst b/docs/source/fastNLP.models.bert.rst index 7ac64ad7..bba323df 100644 --- a/docs/source/fastNLP.models.bert.rst +++ b/docs/source/fastNLP.models.bert.rst @@ -1,5 +1,5 @@ -fastNLP.models.bert module -========================== +fastNLP.models.bert +=================== .. automodule:: fastNLP.models.bert :members: diff --git a/docs/source/fastNLP.models.biaffine_parser.rst b/docs/source/fastNLP.models.biaffine_parser.rst index 448dff09..a3dd1836 100644 --- a/docs/source/fastNLP.models.biaffine_parser.rst +++ b/docs/source/fastNLP.models.biaffine_parser.rst @@ -1,5 +1,5 @@ -fastNLP.models.biaffine\_parser module -====================================== +fastNLP.models.biaffine\_parser +=============================== .. automodule:: fastNLP.models.biaffine_parser :members: diff --git a/docs/source/fastNLP.models.cnn_text_classification.rst b/docs/source/fastNLP.models.cnn_text_classification.rst index 31807494..a935d0bf 100644 --- a/docs/source/fastNLP.models.cnn_text_classification.rst +++ b/docs/source/fastNLP.models.cnn_text_classification.rst @@ -1,5 +1,5 @@ -fastNLP.models.cnn\_text\_classification module -=============================================== +fastNLP.models.cnn\_text\_classification +======================================== .. automodule:: fastNLP.models.cnn_text_classification :members: diff --git a/docs/source/fastNLP.models.enas_controller.rst b/docs/source/fastNLP.models.enas_controller.rst index 7977de81..28655bd7 100644 --- a/docs/source/fastNLP.models.enas_controller.rst +++ b/docs/source/fastNLP.models.enas_controller.rst @@ -1,5 +1,5 @@ -fastNLP.models.enas\_controller module -====================================== +fastNLP.models.enas\_controller +=============================== .. automodule:: fastNLP.models.enas_controller :members: diff --git a/docs/source/fastNLP.models.enas_model.rst b/docs/source/fastNLP.models.enas_model.rst index 518f56b7..35fbe495 100644 --- a/docs/source/fastNLP.models.enas_model.rst +++ b/docs/source/fastNLP.models.enas_model.rst @@ -1,5 +1,5 @@ -fastNLP.models.enas\_model module -================================= +fastNLP.models.enas\_model +========================== .. automodule:: fastNLP.models.enas_model :members: diff --git a/docs/source/fastNLP.models.enas_trainer.rst b/docs/source/fastNLP.models.enas_trainer.rst index 45f77f31..7e0ef462 100644 --- a/docs/source/fastNLP.models.enas_trainer.rst +++ b/docs/source/fastNLP.models.enas_trainer.rst @@ -1,5 +1,5 @@ -fastNLP.models.enas\_trainer module -=================================== +fastNLP.models.enas\_trainer +============================ .. automodule:: fastNLP.models.enas_trainer :members: diff --git a/docs/source/fastNLP.models.enas_utils.rst b/docs/source/fastNLP.models.enas_utils.rst index 5f05a4fc..0a049706 100644 --- a/docs/source/fastNLP.models.enas_utils.rst +++ b/docs/source/fastNLP.models.enas_utils.rst @@ -1,5 +1,5 @@ -fastNLP.models.enas\_utils module -================================= +fastNLP.models.enas\_utils +========================== .. automodule:: fastNLP.models.enas_utils :members: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index accfc3bb..c1be3a4c 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -1,4 +1,4 @@ -fastNLP.models package +fastNLP.models 模块 ====================== .. automodule:: fastNLP.models @@ -6,7 +6,7 @@ fastNLP.models package :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.models.sequence_labeling.rst b/docs/source/fastNLP.models.sequence_labeling.rst index b8568be7..6d569fe1 100644 --- a/docs/source/fastNLP.models.sequence_labeling.rst +++ b/docs/source/fastNLP.models.sequence_labeling.rst @@ -1,5 +1,5 @@ -fastNLP.models.sequence\_labeling module -======================================== +fastNLP.models.sequence\_labeling +================================= .. automodule:: fastNLP.models.sequence_labeling :members: diff --git a/docs/source/fastNLP.models.snli.rst b/docs/source/fastNLP.models.snli.rst index b24bc196..24c2cc53 100644 --- a/docs/source/fastNLP.models.snli.rst +++ b/docs/source/fastNLP.models.snli.rst @@ -1,5 +1,5 @@ -fastNLP.models.snli module -========================== +fastNLP.models.snli +=================== .. automodule:: fastNLP.models.snli :members: diff --git a/docs/source/fastNLP.models.star_transformer.rst b/docs/source/fastNLP.models.star_transformer.rst index f2185935..c93fb8cd 100644 --- a/docs/source/fastNLP.models.star_transformer.rst +++ b/docs/source/fastNLP.models.star_transformer.rst @@ -1,5 +1,5 @@ -fastNLP.models.star\_transformer module -======================================= +fastNLP.models.star\_transformer +================================ .. automodule:: fastNLP.models.star_transformer :members: diff --git a/docs/source/fastNLP.modules.aggregator.attention.rst b/docs/source/fastNLP.modules.aggregator.attention.rst index 46251e73..dc9c2b53 100644 --- a/docs/source/fastNLP.modules.aggregator.attention.rst +++ b/docs/source/fastNLP.modules.aggregator.attention.rst @@ -1,5 +1,5 @@ -fastNLP.modules.aggregator.attention module -=========================================== +fastNLP.modules.aggregator.attention +==================================== .. automodule:: fastNLP.modules.aggregator.attention :members: diff --git a/docs/source/fastNLP.modules.aggregator.pooling.rst b/docs/source/fastNLP.modules.aggregator.pooling.rst index f6730430..162f889d 100644 --- a/docs/source/fastNLP.modules.aggregator.pooling.rst +++ b/docs/source/fastNLP.modules.aggregator.pooling.rst @@ -1,5 +1,5 @@ -fastNLP.modules.aggregator.pooling module -========================================= +fastNLP.modules.aggregator.pooling +================================== .. automodule:: fastNLP.modules.aggregator.pooling :members: diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst index b9b331c3..4f8baabd 100644 --- a/docs/source/fastNLP.modules.aggregator.rst +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -1,12 +1,12 @@ -fastNLP.modules.aggregator package -================================== +fastNLP.modules.aggregator +========================== .. automodule:: fastNLP.modules.aggregator :members: :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.modules.decoder.CRF.rst b/docs/source/fastNLP.modules.decoder.CRF.rst index 8d980bbd..fc643fef 100644 --- a/docs/source/fastNLP.modules.decoder.CRF.rst +++ b/docs/source/fastNLP.modules.decoder.CRF.rst @@ -1,5 +1,5 @@ -fastNLP.modules.decoder.CRF module -================================== +fastNLP.modules.decoder.CRF +=========================== .. automodule:: fastNLP.modules.decoder.CRF :members: diff --git a/docs/source/fastNLP.modules.decoder.MLP.rst b/docs/source/fastNLP.modules.decoder.MLP.rst index 787a3c33..feb5c228 100644 --- a/docs/source/fastNLP.modules.decoder.MLP.rst +++ b/docs/source/fastNLP.modules.decoder.MLP.rst @@ -1,5 +1,5 @@ -fastNLP.modules.decoder.MLP module -================================== +fastNLP.modules.decoder.MLP +=========================== .. automodule:: fastNLP.modules.decoder.MLP :members: diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index 10fdbd90..fbda11d9 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -1,12 +1,12 @@ -fastNLP.modules.decoder package -=============================== +fastNLP.modules.decoder +======================= .. automodule:: fastNLP.modules.decoder :members: :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.modules.decoder.utils.rst b/docs/source/fastNLP.modules.decoder.utils.rst index b829baf7..da979d99 100644 --- a/docs/source/fastNLP.modules.decoder.utils.rst +++ b/docs/source/fastNLP.modules.decoder.utils.rst @@ -1,5 +1,5 @@ -fastNLP.modules.decoder.utils module -==================================== +fastNLP.modules.decoder.utils +============================= .. automodule:: fastNLP.modules.decoder.utils :members: diff --git a/docs/source/fastNLP.modules.encoder.bert.rst b/docs/source/fastNLP.modules.encoder.bert.rst index 6f811792..66bd0bbd 100644 --- a/docs/source/fastNLP.modules.encoder.bert.rst +++ b/docs/source/fastNLP.modules.encoder.bert.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.bert module -=================================== +fastNLP.modules.encoder.bert +============================ .. automodule:: fastNLP.modules.encoder.bert :members: diff --git a/docs/source/fastNLP.modules.encoder.char_encoder.rst b/docs/source/fastNLP.modules.encoder.char_encoder.rst index 12431c70..61ea3340 100644 --- a/docs/source/fastNLP.modules.encoder.char_encoder.rst +++ b/docs/source/fastNLP.modules.encoder.char_encoder.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.char\_encoder module -============================================ +fastNLP.modules.encoder.char\_encoder +===================================== .. automodule:: fastNLP.modules.encoder.char_encoder :members: diff --git a/docs/source/fastNLP.modules.encoder.conv_maxpool.rst b/docs/source/fastNLP.modules.encoder.conv_maxpool.rst index c40a5f39..7058a723 100644 --- a/docs/source/fastNLP.modules.encoder.conv_maxpool.rst +++ b/docs/source/fastNLP.modules.encoder.conv_maxpool.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.conv\_maxpool module -============================================ +fastNLP.modules.encoder.conv\_maxpool +===================================== .. automodule:: fastNLP.modules.encoder.conv_maxpool :members: diff --git a/docs/source/fastNLP.modules.encoder.embedding.rst b/docs/source/fastNLP.modules.encoder.embedding.rst index abdd5fd2..4427b3bf 100644 --- a/docs/source/fastNLP.modules.encoder.embedding.rst +++ b/docs/source/fastNLP.modules.encoder.embedding.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.embedding module -======================================== +fastNLP.modules.encoder.embedding +================================= .. automodule:: fastNLP.modules.encoder.embedding :members: diff --git a/docs/source/fastNLP.modules.encoder.lstm.rst b/docs/source/fastNLP.modules.encoder.lstm.rst index 897e7a5f..f9cbea88 100644 --- a/docs/source/fastNLP.modules.encoder.lstm.rst +++ b/docs/source/fastNLP.modules.encoder.lstm.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.lstm module -=================================== +fastNLP.modules.encoder.lstm +============================ .. automodule:: fastNLP.modules.encoder.lstm :members: diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index e571331d..9a11fe74 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -1,12 +1,12 @@ -fastNLP.modules.encoder package -=============================== +fastNLP.modules.encoder +======================= .. automodule:: fastNLP.modules.encoder :members: :undoc-members: :show-inheritance: -Submodules +子模块 ---------- .. toctree:: diff --git a/docs/source/fastNLP.modules.encoder.star_transformer.rst b/docs/source/fastNLP.modules.encoder.star_transformer.rst index 57cd6dcf..0c406782 100644 --- a/docs/source/fastNLP.modules.encoder.star_transformer.rst +++ b/docs/source/fastNLP.modules.encoder.star_transformer.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.star\_transformer module -================================================ +fastNLP.modules.encoder.star\_transformer +========================================= .. automodule:: fastNLP.modules.encoder.star_transformer :members: diff --git a/docs/source/fastNLP.modules.encoder.transformer.rst b/docs/source/fastNLP.modules.encoder.transformer.rst index 4b63686e..6a40c597 100644 --- a/docs/source/fastNLP.modules.encoder.transformer.rst +++ b/docs/source/fastNLP.modules.encoder.transformer.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.transformer module -========================================== +fastNLP.modules.encoder.transformer +=================================== .. automodule:: fastNLP.modules.encoder.transformer :members: diff --git a/docs/source/fastNLP.modules.encoder.variational_rnn.rst b/docs/source/fastNLP.modules.encoder.variational_rnn.rst index 4472d5c2..348fb3d8 100644 --- a/docs/source/fastNLP.modules.encoder.variational_rnn.rst +++ b/docs/source/fastNLP.modules.encoder.variational_rnn.rst @@ -1,5 +1,5 @@ -fastNLP.modules.encoder.variational\_rnn module -=============================================== +fastNLP.modules.encoder.variational\_rnn +======================================== .. automodule:: fastNLP.modules.encoder.variational_rnn :members: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 2057613a..263005f0 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -1,4 +1,4 @@ -fastNLP.modules package +fastNLP.modules 模块 ======================= .. automodule:: fastNLP.modules @@ -6,7 +6,7 @@ fastNLP.modules package :undoc-members: :show-inheritance: -Subpackages +子模块 ----------- .. toctree:: diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst index 30f405f0..eaa06ea8 100644 --- a/docs/source/fastNLP.rst +++ b/docs/source/fastNLP.rst @@ -1,4 +1,4 @@ -fastNLP package +用户 API =============== .. automodule:: fastNLP @@ -6,14 +6,14 @@ fastNLP package :undoc-members: :show-inheritance: -Subpackages +内部模块 ----------- .. toctree:: + :maxdepth: 3 - fastNLP.component fastNLP.core fastNLP.io - fastNLP.models fastNLP.modules + fastNLP.models diff --git a/docs/source/index.rst b/docs/source/index.rst index d441601e..4bb44773 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -46,11 +46,6 @@ For example: (TODO) -快速入门 -------------- - -TODO - 用户手册 --------------- @@ -60,7 +55,7 @@ TODO 安装指南 快速入门 - 详细指南 + 详细指南 科研向导 From ce8d629fb1d21edc7e5f36d4ef0d1b964431f464 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 22:10:11 +0800 Subject: [PATCH 109/173] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E4=BB=8B=E7=BB=8D=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 13 +- .../tutorials/fastnlp_10tmin_tutorial.rst | 376 ------------------ .../tutorials/fastnlp_1_minute_tutorial.rst | 113 ------ .../tutorials/fastnlp_advanced_tutorial.rst | 5 - .../tutorials/fastnlp_developer_guide.rst | 5 - docs/source/user/quickstart.rst | 2 +- docs/source/user/task1.rst | 3 - docs/source/user/task2.rst | 3 - docs/source/user/tutorial_one.rst | 371 +++++++++++++++++ docs/source/user/with_fitlog.rst | 5 + 10 files changed, 383 insertions(+), 513 deletions(-) delete mode 100644 docs/source/tutorials/fastnlp_10tmin_tutorial.rst delete mode 100644 docs/source/tutorials/fastnlp_1_minute_tutorial.rst delete mode 100644 docs/source/tutorials/fastnlp_advanced_tutorial.rst delete mode 100644 docs/source/tutorials/fastnlp_developer_guide.rst delete mode 100644 docs/source/user/task1.rst delete mode 100644 docs/source/user/task2.rst create mode 100644 docs/source/user/tutorial_one.rst create mode 100644 docs/source/user/with_fitlog.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 4bb44773..10bab0eb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,7 +24,7 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 | module type | functionality | example | +=======================+=======================+=======================+ | encoder | 将输入编码为具有具 | embedding, RNN, CNN, | -| | 有表示能力的向量 | transformer | +| | 有表示能力的向量 | transformer | +-----------------------+-----------------------+-----------------------+ | aggregator | 从多个向量中聚合信息 | self-attention, | | | | max-pooling | @@ -39,16 +39,15 @@ For example: .. image:: figures/text_classification.png +.. todo:: + 各个任务上的结果 - -各个任务上的结果 ------------------------ - -(TODO) +内置的模型 +---------------- 用户手册 ---------------- +---------------- .. toctree:: :maxdepth: 1 diff --git a/docs/source/tutorials/fastnlp_10tmin_tutorial.rst b/docs/source/tutorials/fastnlp_10tmin_tutorial.rst deleted file mode 100644 index 4c5fc65e..00000000 --- a/docs/source/tutorials/fastnlp_10tmin_tutorial.rst +++ /dev/null @@ -1,376 +0,0 @@ -fastNLP 10分钟上手教程 -=============== - -教程原文见 https://github.com/fastnlp/fastNLP/blob/master/tutorials/fastnlp_10min_tutorial.ipynb - -fastNLP提供方便的数据预处理,训练和测试模型的功能 - -DataSet & Instance ------------------- - -fastNLP用DataSet和Instance保存和处理数据。每个DataSet表示一个数据集,每个Instance表示一个数据样本。一个DataSet存有多个Instance,每个Instance可以自定义存哪些内容。 - -有一些read\_\*方法,可以轻松从文件读取数据,存成DataSet。 - -.. code:: ipython3 - - from fastNLP import DataSet - from fastNLP import Instance - - # 从csv读取数据到DataSet - win_path = "C:\\Users\zyfeng\Desktop\FudanNLP\\fastNLP\\test\\data_for_tests\\tutorial_sample_dataset.csv" - dataset = DataSet.read_csv(win_path, headers=('raw_sentence', 'label'), sep='\t') - print(dataset[0]) - - -.. parsed-literal:: - - {'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story ., - 'label': 1} - - -.. code:: ipython3 - - # DataSet.append(Instance)加入新数据 - - dataset.append(Instance(raw_sentence='fake data', label='0')) - dataset[-1] - - - - -.. parsed-literal:: - - {'raw_sentence': fake data, - 'label': 0} - - - -.. code:: ipython3 - - # DataSet.apply(func, new_field_name)对数据预处理 - - # 将所有数字转为小写 - dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') - # label转int - dataset.apply(lambda x: int(x['label']), new_field_name='label_seq', is_target=True) - # 使用空格分割句子 - dataset.drop(lambda x: len(x['raw_sentence'].split()) == 0) - def split_sent(ins): - return ins['raw_sentence'].split() - dataset.apply(split_sent, new_field_name='words', is_input=True) - -.. code:: ipython3 - - # DataSet.drop(func)筛除数据 - # 删除低于某个长度的词语 - dataset.drop(lambda x: len(x['words']) <= 3) - -.. code:: ipython3 - - # 分出测试集、训练集 - - test_data, train_data = dataset.split(0.3) - print("Train size: ", len(test_data)) - print("Test size: ", len(train_data)) - - -.. parsed-literal:: - - Train size: 54 - Test size: - -Vocabulary ----------- - -fastNLP中的Vocabulary轻松构建词表,将词转成数字 - -.. code:: ipython3 - - from fastNLP import Vocabulary - - # 构建词表, Vocabulary.add(word) - vocab = Vocabulary(min_freq=2) - train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) - vocab.build_vocab() - - # index句子, Vocabulary.to_index(word) - train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True) - test_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True) - - - print(test_data[0]) - - -.. parsed-literal:: - - {'raw_sentence': the plot is romantic comedy boilerplate from start to finish ., - 'label': 2, - 'label_seq': 2, - 'words': ['the', 'plot', 'is', 'romantic', 'comedy', 'boilerplate', 'from', 'start', 'to', 'finish', '.'], - 'word_seq': [2, 13, 9, 24, 25, 26, 15, 27, 11, 28, 3]} - - -.. code:: ipython3 - - # 假设你们需要做强化学习或者gan之类的项目,也许你们可以使用这里的dataset - from fastNLP.core.batch import Batch - from fastNLP.core.sampler import RandomSampler - - batch_iterator = Batch(dataset=train_data, batch_size=2, sampler=RandomSampler()) - for batch_x, batch_y in batch_iterator: - print("batch_x has: ", batch_x) - print("batch_y has: ", batch_y) - break - - -.. parsed-literal:: - - batch_x has: {'words': array([list(['this', 'kind', 'of', 'hands-on', 'storytelling', 'is', 'ultimately', 'what', 'makes', 'shanghai', 'ghetto', 'move', 'beyond', 'a', 'good', ',', 'dry', ',', 'reliable', 'textbook', 'and', 'what', 'allows', 'it', 'to', 'rank', 'with', 'its', 'worthy', 'predecessors', '.']), - list(['the', 'entire', 'movie', 'is', 'filled', 'with', 'deja', 'vu', 'moments', '.'])], - dtype=object), 'word_seq': tensor([[ 19, 184, 6, 1, 481, 9, 206, 50, 91, 1210, 1609, 1330, - 495, 5, 63, 4, 1269, 4, 1, 1184, 7, 50, 1050, 10, - 8, 1611, 16, 21, 1039, 1, 2], - [ 3, 711, 22, 9, 1282, 16, 2482, 2483, 200, 2, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0]])} - batch_y has: {'label_seq': tensor([3, 2])} - - -Model ------ - -.. code:: ipython3 - - # 定义一个简单的Pytorch模型 - - from fastNLP.models import CNNText - model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) - model - - - - -.. parsed-literal:: - - CNNText( - (embed): Embedding( - (embed): Embedding(77, 50, padding_idx=0) - (dropout): Dropout(p=0.0) - ) - (conv_pool): ConvMaxpool( - (convs): ModuleList( - (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,)) - (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,)) - (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,)) - ) - ) - (dropout): Dropout(p=0.1) - (fc): Linear( - (linear): Linear(in_features=12, out_features=5, bias=True) - ) - ) - - - -Trainer & Tester ----------------- - -使用fastNLP的Trainer训练模型 - -.. code:: ipython3 - - from fastNLP import Trainer - from copy import deepcopy - from fastNLP import CrossEntropyLoss - from fastNLP import AccuracyMetric - -.. code:: ipython3 - - # 进行overfitting测试 - copy_model = deepcopy(model) - overfit_trainer = Trainer(model=copy_model, - train_data=test_data, - dev_data=test_data, - loss=CrossEntropyLoss(pred="output", target="label_seq"), - metrics=AccuracyMetric(), - n_epochs=10, - save_path=None) - overfit_trainer.train() - - -.. parsed-literal:: - - training epochs started 2018-12-07 14:07:20 - - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=20), HTML(value='')), layout=Layout(display='… - - - -.. parsed-literal:: - - Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.037037 - Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.296296 - Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.333333 - Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.555556 - Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.611111 - Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.481481 - Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.62963 - Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.685185 - Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.722222 - Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.777778 - - -.. code:: ipython3 - - # 实例化Trainer,传入模型和数据,进行训练 - trainer = Trainer(model=model, - train_data=train_data, - dev_data=test_data, - loss=CrossEntropyLoss(pred="output", target="label_seq"), - metrics=AccuracyMetric(), - n_epochs=5) - trainer.train() - print('Train finished!') - - -.. parsed-literal:: - - training epochs started 2018-12-07 14:08:10 - - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=5), HTML(value='')), layout=Layout(display='i… - - - -.. parsed-literal:: - - Epoch 1/5. Step:1/5. AccuracyMetric: acc=0.037037 - Epoch 2/5. Step:2/5. AccuracyMetric: acc=0.037037 - Epoch 3/5. Step:3/5. AccuracyMetric: acc=0.037037 - Epoch 4/5. Step:4/5. AccuracyMetric: acc=0.185185 - Epoch 5/5. Step:5/5. AccuracyMetric: acc=0.240741 - Train finished! - - -.. code:: ipython3 - - from fastNLP import Tester - - tester = Tester(data=test_data, model=model, metrics=AccuracyMetric()) - acc = tester.test() - - -.. parsed-literal:: - - [tester] - AccuracyMetric: acc=0.240741 - - -In summary ----------- - -fastNLP Trainer的伪代码逻辑 ---------------------------- - -1. 准备DataSet,假设DataSet中共有如下的fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - ['raw_sentence', 'word_seq1', 'word_seq2', 'raw_label','label'] - 通过 - DataSet.set_input('word_seq1', word_seq2', flag=True)将'word_seq1', 'word_seq2'设置为input - 通过 - DataSet.set_target('label', flag=True)将'label'设置为target - -2. 初始化模型 -~~~~~~~~~~~~~ - -:: - - class Model(nn.Module): - def __init__(self): - xxx - def forward(self, word_seq1, word_seq2): - # (1) 这里使用的形参名必须和DataSet中的input field的名称对应。因为我们是通过形参名, 进行赋值的 - # (2) input field的数量可以多于这里的形参数量。但是不能少于。 - xxxx - # 输出必须是一个dict - -3. Trainer的训练过程 -~~~~~~~~~~~~~~~~~~~~ - -:: - - (1) 从DataSet中按照batch_size取出一个batch,调用Model.forward - (2) 将 Model.forward的结果 与 标记为target的field 传入Losser当中。 - 由于每个人写的Model.forward的output的dict可能key并不一样,比如有人是{'pred':xxx}, {'output': xxx}; - 另外每个人将target可能也会设置为不同的名称, 比如有人是label, 有人设置为target; - 为了解决以上的问题,我们的loss提供映射机制 - 比如CrossEntropyLosser的需要的输入是(prediction, target)。但是forward的output是{'output': xxx}; 'label'是target - 那么初始化losser的时候写为CrossEntropyLosser(prediction='output', target='label')即可 - (3) 对于Metric是同理的 - Metric计算也是从 forward的结果中取值 与 设置target的field中取值。 也是可以通过映射找到对应的值 - -一些问题. ---------- - -1. DataSet中为什么需要设置input和target -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - 只有被设置为input或者target的数据才会在train的过程中被取出来 - (1.1) 我们只会在设置为input的field中寻找传递给Model.forward的参数。 - (1.2) 我们在传递值给losser或者metric的时候会使用来自: - (a)Model.forward的output - (b)被设置为target的field - - -2. 我们是通过forwad中的形参名将DataSet中的field赋值给对应的参数 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - (1.1) 构建模型过程中, - 例如: - DataSet中x,seq_lens是input,那么forward就应该是 - def forward(self, x, seq_lens): - pass - 我们是通过形参名称进行匹配的field的 - - -1. 加载数据到DataSet -~~~~~~~~~~~~~~~~~~~~ - -2. 使用apply操作对DataSet进行预处理 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -:: - - (2.1) 处理过程中将某些field设置为input,某些field设置为target - -3. 构建模型 -~~~~~~~~~~~ - -:: - - (3.1) 构建模型过程中,需要注意forward函数的形参名需要和DataSet中设置为input的field名称是一致的。 - 例如: - DataSet中x,seq_lens是input,那么forward就应该是 - def forward(self, x, seq_lens): - pass - 我们是通过形参名称进行匹配的field的 - (3.2) 模型的forward的output需要是dict类型的。 - 建议将输出设置为{"pred": xx}. - diff --git a/docs/source/tutorials/fastnlp_1_minute_tutorial.rst b/docs/source/tutorials/fastnlp_1_minute_tutorial.rst deleted file mode 100644 index b4c6c8c4..00000000 --- a/docs/source/tutorials/fastnlp_1_minute_tutorial.rst +++ /dev/null @@ -1,113 +0,0 @@ - -FastNLP 1分钟上手教程 -===================== - -教程原文见 https://github.com/fastnlp/fastNLP/blob/master/tutorials/fastnlp_1min_tutorial.ipynb - -step 1 ------- - -读取数据集 - -.. code:: ipython3 - - from fastNLP import DataSet - # linux_path = "../test/data_for_tests/tutorial_sample_dataset.csv" - win_path = "C:\\Users\zyfeng\Desktop\FudanNLP\\fastNLP\\test\\data_for_tests\\tutorial_sample_dataset.csv" - ds = DataSet.read_csv(win_path, headers=('raw_sentence', 'label'), sep='\t') - -step 2 ------- - -数据预处理 1. 类型转换 2. 切分验证集 3. 构建词典 - -.. code:: ipython3 - - # 将所有数字转为小写 - ds.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') - # label转int - ds.apply(lambda x: int(x['label']), new_field_name='label_seq', is_target=True) - - def split_sent(ins): - return ins['raw_sentence'].split() - ds.apply(split_sent, new_field_name='words', is_input=True) - - -.. code:: ipython3 - - # 分割训练集/验证集 - train_data, dev_data = ds.split(0.3) - print("Train size: ", len(train_data)) - print("Test size: ", len(dev_data)) - - -.. parsed-literal:: - - Train size: 54 - Test size: 23 - - -.. code:: ipython3 - - from fastNLP import Vocabulary - vocab = Vocabulary(min_freq=2) - train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) - - # index句子, Vocabulary.to_index(word) - train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True) - dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True) - - -step 3 ------- - -定义模型 - -.. code:: ipython3 - - from fastNLP.models import CNNText - model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1) - - -step 4 ------- - -开始训练 - -.. code:: ipython3 - - from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric - trainer = Trainer(model=model, - train_data=train_data, - dev_data=dev_data, - loss=CrossEntropyLoss(), - metrics=AccuracyMetric() - ) - trainer.train() - print('Train finished!') - - - -.. parsed-literal:: - - training epochs started 2018-12-07 14:03:41 - - - - -.. parsed-literal:: - - HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=6), HTML(value='')), layout=Layout(display='i… - - - -.. parsed-literal:: - - Epoch 1/3. Step:2/6. AccuracyMetric: acc=0.26087 - Epoch 2/3. Step:4/6. AccuracyMetric: acc=0.347826 - Epoch 3/3. Step:6/6. AccuracyMetric: acc=0.608696 - Train finished! - - -本教程结束。更多操作请参考进阶教程。 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/tutorials/fastnlp_advanced_tutorial.rst b/docs/source/tutorials/fastnlp_advanced_tutorial.rst deleted file mode 100644 index d788e9d6..00000000 --- a/docs/source/tutorials/fastnlp_advanced_tutorial.rst +++ /dev/null @@ -1,5 +0,0 @@ -fastNLP 进阶教程 -=============== - -教程原文见 https://github.com/fastnlp/fastNLP/blob/master/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb - diff --git a/docs/source/tutorials/fastnlp_developer_guide.rst b/docs/source/tutorials/fastnlp_developer_guide.rst deleted file mode 100644 index 73b75f02..00000000 --- a/docs/source/tutorials/fastnlp_developer_guide.rst +++ /dev/null @@ -1,5 +0,0 @@ -fastNLP 开发者指南 -=============== - -原文见 https://github.com/fastnlp/fastNLP/blob/master/tutorials/tutorial_for_developer.md - diff --git a/docs/source/user/quickstart.rst b/docs/source/user/quickstart.rst index 0e5c053e..43056a26 100644 --- a/docs/source/user/quickstart.rst +++ b/docs/source/user/quickstart.rst @@ -121,4 +121,4 @@ In Epoch:6/Step:12, got best dev performance:AccuracyMetric: acc=0.8 Reloaded the best model. -这份教程只是简单地介绍了使用 fastNLP 工作的流程,具体的细节分析见 :doc:`/user/tutorials` \ No newline at end of file +这份教程只是简单地介绍了使用 fastNLP 工作的流程,具体的细节分析见 :doc:`/user/tutorial_one` \ No newline at end of file diff --git a/docs/source/user/task1.rst b/docs/source/user/task1.rst deleted file mode 100644 index 0c346999..00000000 --- a/docs/source/user/task1.rst +++ /dev/null @@ -1,3 +0,0 @@ -===================== -用 fastNLP 分类 -===================== \ No newline at end of file diff --git a/docs/source/user/task2.rst b/docs/source/user/task2.rst deleted file mode 100644 index 73ee014b..00000000 --- a/docs/source/user/task2.rst +++ /dev/null @@ -1,3 +0,0 @@ -===================== -用 fastNLP 分词 -===================== \ No newline at end of file diff --git a/docs/source/user/tutorial_one.rst b/docs/source/user/tutorial_one.rst new file mode 100644 index 00000000..0c7be77d --- /dev/null +++ b/docs/source/user/tutorial_one.rst @@ -0,0 +1,371 @@ +=============== +详细指南 +=============== + +我们使用和 :doc:`/user/quickstart` 中一样的任务来进行详细的介绍。给出一段文字,预测它的标签是0~4中的哪一个 +(数据来源 `kaggle `_ )。 + +-------------- +数据处理 +-------------- + +数据读入 + 我们可以使用 fastNLP :mod:`fastNLP.io` 模块中的 :class:`~fastNLP.io.CSVLoader` 类,轻松地从 csv 文件读取我们的数据。 + 这里的 dataset 是 fastNLP 中 :class:`~fastNLP.DataSet` 类的对象 + + .. code-block:: python + + from fastNLP.io import CSVLoader + + loader = CSVLoader(headers=('raw_sentence', 'label'), sep='\t') + dataset = loader.load("./sample_data/tutorial_sample_dataset.csv") + + 除了读取数据外,fastNLP 还提供了读取其它文件类型的 Loader 类、读取 Embedding的 Loader 等。详见 :doc:`/fastNLP.io` 。 + +Instance 和 DataSet + fastNLP 中的 :class:`~fastNLP.DataSet` 类对象类似于二维表格,它的每一列是一个 :mod:`~fastNLP.core.field` + 每一行是一个 :mod:`~fastNLP.core.instance` 。我们可以手动向数据集中添加 :class:`~fastNLP.Instance` 类的对象 + + .. code-block:: python + + from fastNLP import Instance + + dataset.append(Instance(raw_sentence='fake data', label='0')) + + 此时的 ``dataset[-1]`` 的值如下,可以看到,数据集中的每个数据包含 ``raw_sentence`` 和 ``label`` 两个 + :mod:`~fastNLP.core.field` ,他们的类型都是 ``str`` :: + + {'raw_sentence': fake data type=str, 'label': 0 type=str} + +field 的修改 + 我们使用 :class:`~fastNLP.DataSet` 类的 :meth:`~fastNLP.DataSet.apply` 方法将 ``raw_sentence`` 中字母变成小写,并将句子分词。 + 同时也将 ``label`` :mod:`~fastNLP.core.field` 转化为整数并改名为 ``target`` + + .. code-block:: python + + dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='sentence') + dataset.apply_field(lambda x: x.split(), field_name='sentence', new_field_name='words') + dataset.apply(lambda x: int(x['label']), new_field_name='target') + + ``words`` 和 ``target`` 已经足够用于 :class:`~fastNLP.models.CNNText` 的训练了,但我们从其文档 + :class:`~fastNLP.models.CNNText` 中看到,在 :meth:`~fastNLP.models.CNNText.forward` 的时候,还可以传入可选参数 ``seq_len`` 。 + 所以,我们再使用 :meth:`~fastNLP.DataSet.apply_field` 方法增加一个名为 ``seq_len`` 的 :mod:`~fastNLP.core.field` 。 + + .. code-block:: python + + dataset.apply_field(lambda x: len(x), field_name='words', new_field_name='seq_len') + + 观察可知: :meth:`~fastNLP.DataSet.apply_field` 与 :meth:`~fastNLP.DataSet.apply` 类似, + 但所传入的 `lambda` 函数是针对一个 :class:`~fastNLP.Instance` 中的一个 :mod:`~fastNLP.core.field` 的; + 而 :meth:`~fastNLP.DataSet.apply` 所传入的 `lambda` 函数是针对整个 :class:`~fastNLP.Instance` 的。 + + .. note:: + `lambda` 函数即匿名函数,是 Python 的重要特性。 ``lambda x: len(x)`` 和下面的这个函数的作用相同:: + + def func_lambda(x): + return len(x) + + 你也可以编写复杂的函数做为 :meth:`~fastNLP.DataSet.apply_field` 与 :meth:`~fastNLP.DataSet.apply` 的参数 + +Vocabulary 的使用 + 我们再用 :class:`~fastNLP.Vocabulary` 类来统计数据中出现的单词,并使用 :meth:`~fastNLP.Vocabularyindex_dataset` + 将单词序列转化为训练可用的数字序列。 + + .. code-block:: python + + from fastNLP import Vocabulary + + vocab = Vocabulary(min_freq=2).from_dataset(dataset, field_name='words') + vocab.index_dataset(dataset, field_name='words',new_field_name='words') + +数据集分割 + 除了修改 :mod:`~fastNLP.core.field` 之外,我们还可以对 :class:`~fastNLP.DataSet` 进行分割,以供训练、开发和测试使用。 + 下面这段代码展示了 :meth:`~fastNLP.DataSet.split` 的使用方法(但实际应该放在后面两段改名和设置输入的代码之后) + + .. code-block:: python + + train_dev_data, test_data = dataset.split(0.1) + train_data, dev_data = train_dev_data.split(0.1) + len(train_data), len(dev_data), len(test_data) + +--------------------- +使用内置模型训练 +--------------------- + +内置模型的输入输出命名 + fastNLP内置了一些完整的神经网络模型,详见 :doc:`/fastNLP.models` , 我们使用其中的 :class:`~fastNLP.models.CNNText` 模型进行训练。 + 为了使用内置的 :class:`~fastNLP.models.CNNText`,我们必须修改 :class:`~fastNLP.DataSet` 中 :mod:`~fastNLP.core.field` 的名称。 + 在这个例子中模型输入 (forward方法的参数) 为 ``words`` 和 ``seq_len`` ; 预测输出为 ``pred`` ;标准答案为 ``target`` 。 + 具体的命名规范可以参考 :doc:`/fastNLP.core.const` 。 + + 如果不想查看文档,您也可以使用 :class:`~fastNLP.Const` 类进行命名。下面的代码展示了给 :class:`~fastNLP.DataSet` 中 + :mod:`~fastNLP.core.field` 改名的 :meth:`~fastNLP.DataSet.rename_field` 方法,以及 :class:`~fastNLP.Const` 类的使用方法。 + + .. code-block:: python + + from fastNLP import Const + + dataset.rename_field('words', Const.INPUT) + dataset.rename_field('seq_len', Const.INPUT_LEN) + dataset.rename_field('target', Const.TARGET) + + 在给 :class:`~fastNLP.DataSet` 中 :mod:`~fastNLP.core.field` 改名后,我们还需要设置训练所需的输入和目标,这里使用的是 + :meth:`~fastNLP.DataSet.set_input` 和 :meth:`~fastNLP.DataSet.set_target` 两个函数。 + + .. code-block:: python + + dataset.set_input(Const.INPUT, Const.INPUT_LEN) + dataset.set_target(Const.TARGET) + +快速训练 + 现在我们可以导入 fastNLP 内置的文本分类模型 :class:`~fastNLP.models.CNNText` ,并使用 :class:`~fastNLP.Trainer` 进行训练了 + (其中 ``loss`` 和 ``metrics`` 的定义,我们将在后续两段代码中给出)。 + + .. code-block:: python + + from fastNLP.models import CNNText + from fastNLP import Trainer + + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + + trainer = Trainer(model=model_cnn, train_data=train_data, dev_data=dev_data, + loss=loss, metrics=metrics) + trainer.train() + + 训练过程的输出如下:: + + input fields after batch(if batch size is 2): + words: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 26]) + target fields after batch(if batch size is 2): + target: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + + training epochs started 2019-05-09-10-59-39 + Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.333333 + + Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.533333 + + Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.6 + + Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.8 + + Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.8 + + Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.733333 + + Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.733333 + + Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.733333 + + + In Epoch:6/Step:12, got best dev performance:AccuracyMetric: acc=0.8 + Reloaded the best model. + +损失函数 + 训练模型需要提供一个损失函数, 下面提供了一个在分类问题中常用的交叉熵损失。注意它的 **初始化参数** 。 + ``pred`` 参数对应的是模型的 forward 方法返回的 dict 中的一个 key 的名字。 + ``target`` 参数对应的是 :class:`~fastNLP.DataSet` 中作为标签的 :mod:`~fastNLP.core.field` 的名字。 + 这里我们用 :class:`~fastNLP.Const` 来辅助命名,如果你自己编写模型中 forward 方法的返回值或 + 数据集中 :mod:`~fastNLP.core.field` 的名字与本例不同, 你可以把 ``pred`` 参数和 ``target`` 参数设定符合自己代码的值。 + + .. code-block:: python + + from fastNLP import CrossEntropyLoss + + # loss = CrossEntropyLoss() 在本例中与下面这行代码等价 + loss = CrossEntropyLoss(pred=Const.OUTPUT, target=Const.TARGET) + +评价指标 + 训练模型需要提供一个评价指标。这里使用准确率做为评价指标。参数的 `命名规则` 跟上面类似。 + ``pred`` 参数对应的是模型的 forward 方法返回的 dict 中的一个 key 的名字。 + ``target`` 参数对应的是 :class:`~fastNLP.DataSet` 中作为标签的 :mod:`~fastNLP.core.field` 的名字。 + + .. code-block:: python + + from fastNLP import AccuracyMetric + + # metrics=AccuracyMetric() 在本例中与下面这行代码等价 + metrics=AccuracyMetric(pred=Const.OUTPUT, target=Const.TARGET) + +快速测试 + 与 :class:`~fastNLP.Trainer` 对应,fastNLP 也提供了 :class:`~fastNLP.Tester` 用于快速测试,用法如下 + + .. code-block:: python + + from fastNLP import Tester + + tester = Tester(test_data, model_cnn, metrics=AccuracyMetric()) + tester.test() + +--------------------- +编写自己的模型 +--------------------- + +因为 fastNLP 是基于 `PyTorch `_ 开发的框架,所以我们可以基于 PyTorch 模型编写自己的神经网络模型。 +与标准的 PyTorch 模型不同,fastNLP 模型中 forward 方法返回的是一个字典,字典中至少需要包含 "pred" 这个字段。 +而 forward 方法的参数名称必须与 :class:`~fastNLP.DataSet` 中用 :meth:`~fastNLP.DataSet.set_input` 设定的名称一致。 +模型定义的代码如下: + +.. code-block:: python + + import torch + import torch.nn as nn + + class LSTMText(nn.Module): + def __init__(self, vocab_size, embedding_dim, output_dim, hidden_dim=64, num_layers=2, dropout=0.5): + super().__init__() + + self.embedding = nn.Embedding(vocab_size, embedding_dim) + self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True, dropout=dropout) + self.fc = nn.Linear(hidden_dim * 2, output_dim) + self.dropout = nn.Dropout(dropout) + + def forward(self, words): + # (input) words : (batch_size, seq_len) + words = words.permute(1,0) + # words : (seq_len, batch_size) + + embedded = self.dropout(self.embedding(words)) + # embedded : (seq_len, batch_size, embedding_dim) + output, (hidden, cell) = self.lstm(embedded) + # output: (seq_len, batch_size, hidden_dim * 2) + # hidden: (num_layers * 2, batch_size, hidden_dim) + # cell: (num_layers * 2, batch_size, hidden_dim) + + hidden = torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1) + hidden = self.dropout(hidden) + # hidden: (batch_size, hidden_dim * 2) + + pred = self.fc(hidden.squeeze(0)) + # result: (batch_size, output_dim) + return {"pred":pred} + +模型的使用方法与内置模型 :class:`~fastNLP.models.CNNText` 一致 + +.. code-block:: python + + model_lstm = LSTMText(len(vocab),50,5) + + trainer = Trainer(model=model_lstm, train_data=train_data, dev_data=dev_data, + loss=loss, metrics=metrics) + trainer.train() + + tester = Tester(test_data, model_lstm, metrics=AccuracyMetric()) + tester.test() + +.. todo:: + 使用 :doc:`/fastNLP.modules` 编写模型 + +-------------------------- +自己编写训练过程 +-------------------------- + +如果你想用类似 PyTorch 的使用方法,自己编写训练过程,你可以参考下面这段代码。其中使用了 fastNLP 提供的 :class:`~fastNLP.Batch` +来获得小批量训练的小批量数据,使用 :class:`~fastNLP.BucketSampler` 做为 :class:`~fastNLP.Batch` 的参数来选择采样的方式。 +这段代码中使用了 PyTorch 的 `torch.optim.Adam` 优化器 和 `torch.nn.CrossEntropyLoss` 损失函数,并自己计算了正确率 + +.. code-block:: python + + from fastNLP import BucketSampler + from fastNLP import Batch + import torch + import time + + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + + def train(epoch, data): + optim = torch.optim.Adam(model.parameters(), lr=0.001) + lossfunc = torch.nn.CrossEntropyLoss() + batch_size = 32 + + train_sampler = BucketSampler(batch_size=batch_size, seq_len_field_name='seq_len') + train_batch = Batch(batch_size=batch_size, dataset=data, sampler=train_sampler) + + start_time = time.time() + for i in range(epoch): + loss_list = [] + for batch_x, batch_y in train_batch: + optim.zero_grad() + output = model(batch_x['words']) + loss = lossfunc(output['pred'], batch_y['target']) + loss.backward() + optim.step() + loss_list.append(loss.item()) + print('Epoch {:d} Avg Loss: {:.2f}'.format(i, sum(loss_list) / len(loss_list)),end=" ") + print('{:d}ms'.format(round((time.time()-start_time)*1000))) + loss_list.clear() + + train(10, train_data) + + tester = Tester(test_data, model, metrics=AccuracyMetric()) + tester.test() + +这段代码的输出如下:: + + Epoch 0 Avg Loss: 2.76 17ms + Epoch 1 Avg Loss: 2.55 29ms + Epoch 2 Avg Loss: 2.37 41ms + Epoch 3 Avg Loss: 2.30 53ms + Epoch 4 Avg Loss: 2.12 65ms + Epoch 5 Avg Loss: 2.16 76ms + Epoch 6 Avg Loss: 1.88 88ms + Epoch 7 Avg Loss: 1.84 99ms + Epoch 8 Avg Loss: 1.71 111ms + Epoch 9 Avg Loss: 1.62 122ms + [tester] + AccuracyMetric: acc=0.142857 + +---------------------------------- +使用 Callback 增强 Trainer +---------------------------------- + +如果你不想自己实现繁琐的训练过程,只希望在训练过程中实现一些自己的功能(比如:输出从训练开始到当前 batch 结束的总时间), +你可以使用 fastNLP 提供的 :class:`~fastNLP.Callback` 类。下面的例子中,我们继承 :class:`~fastNLP.Callback` 类实现了这个功能。 + +.. code-block:: python + + from fastNLP import Callback + + start_time = time.time() + + class MyCallback(Callback): + def on_epoch_end(self): + print('Sum Time: {:d}ms\n\n'.format(round((time.time()-start_time)*1000))) + + + model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1) + trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, + loss=CrossEntropyLoss(), metrics=AccuracyMetric(), callbacks=[MyCallback()]) + trainer.train() + +训练输出如下:: + + input fields after batch(if batch size is 2): + words: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 16]) + seq_len: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + target fields after batch(if batch size is 2): + target: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + + training epochs started 2019-05-12-21-38-40 + Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.285714 + + Sum Time: 51ms + + + ………………………… + + + Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.857143 + + Sum Time: 212ms + + + + In Epoch:10/Step:20, got best dev performance:AccuracyMetric: acc=0.857143 + Reloaded the best model. + +这个例子只是介绍了 :class:`~fastNLP.Callback` 类的使用方法。实际应用(比如:负采样、Learning Rate Decay、Early Stop 等)中 +很多功能已经被 fastNLP 实现了。你可以直接 import 它们使用,详细请查看文档 :doc:`/fastNLP.core.callback` 。 \ No newline at end of file diff --git a/docs/source/user/with_fitlog.rst b/docs/source/user/with_fitlog.rst new file mode 100644 index 00000000..97c3ea71 --- /dev/null +++ b/docs/source/user/with_fitlog.rst @@ -0,0 +1,5 @@ +================= +科研向导 +================= + +本文介绍使用 fastNLP 和 fitlog 进行科学研究的方法 \ No newline at end of file From 674c3d10281d1f5e0587d59808f58f779d87f055 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 22:11:22 +0800 Subject: [PATCH 110/173] =?UTF-8?q?tutorial=20=E7=9A=84=20ipynb=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tutorials/fastnlp_1min_tutorial.ipynb | 1775 ------------------------- tutorials/quickstart.ipynb | 280 ++++ tutorials/tutorial_one.ipynb | 831 ++++++++++++ 3 files changed, 1111 insertions(+), 1775 deletions(-) delete mode 100644 tutorials/fastnlp_1min_tutorial.ipynb create mode 100644 tutorials/quickstart.ipynb create mode 100644 tutorials/tutorial_one.ipynb diff --git a/tutorials/fastnlp_1min_tutorial.ipynb b/tutorials/fastnlp_1min_tutorial.ipynb deleted file mode 100644 index 64d57bc4..00000000 --- a/tutorials/fastnlp_1min_tutorial.ipynb +++ /dev/null @@ -1,1775 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "# fastNLP 1分钟上手教程" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## step 1\n", - "读取数据集" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from fastNLP import DataSet\n", - " \n", - "data_path = \"./sample_data/tutorial_sample_dataset.csv\"\n", - "ds = DataSet.read_csv(data_path, headers=('raw_sentence', 'label'), sep='\\t')" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_sentence': This quiet , introspective and entertaining independent is worth seeking . type=str,\n", - "'label': 4 type=str}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ds[1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## step 2\n", - "数据预处理\n", - "1. 类型转换\n", - "2. 切分验证集\n", - "3. 构建词典" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[['a',\n", - " 'series',\n", - " 'of',\n", - " 'escapades',\n", - " 'demonstrating',\n", - " 'the',\n", - " 'adage',\n", - " 'that',\n", - " 'what',\n", - " 'is',\n", - " 'good',\n", - " 'for',\n", - " 'the',\n", - " 'goose',\n", - " 'is',\n", - " 'also',\n", - " 'good',\n", - " 'for',\n", - " 'the',\n", - " 'gander',\n", - " ',',\n", - " 'some',\n", - " 'of',\n", - " 'which',\n", - " 'occasionally',\n", - " 'amuses',\n", - " 'but',\n", - " 'none',\n", - " 'of',\n", - " 'which',\n", - " 'amounts',\n", - " 'to',\n", - " 'much',\n", - " 'of',\n", - " 'a',\n", - " 'story',\n", - " '.'],\n", - " ['this',\n", - " 'quiet',\n", - " ',',\n", - " 'introspective',\n", - " 'and',\n", - " 'entertaining',\n", - " 'independent',\n", - " 'is',\n", - " 'worth',\n", - " 'seeking',\n", - " '.'],\n", - " ['even',\n", - " 'fans',\n", - " 'of',\n", - " 'ismail',\n", - " 'merchant',\n", - " \"'s\",\n", - " 'work',\n", - " ',',\n", - " 'i',\n", - " 'suspect',\n", - " ',',\n", - " 'would',\n", - " 'have',\n", - " 'a',\n", - " 'hard',\n", - " 'time',\n", - " 'sitting',\n", - " 'through',\n", - " 'this',\n", - " 'one',\n", - " '.'],\n", - " ['a',\n", - " 'positively',\n", - " 'thrilling',\n", - " 'combination',\n", - " 'of',\n", - " 'ethnography',\n", - " 'and',\n", - " 'all',\n", - " 'the',\n", - " 'intrigue',\n", - " ',',\n", - " 'betrayal',\n", - " ',',\n", - " 'deceit',\n", - " 'and',\n", - " 'murder',\n", - " 'of',\n", - " 'a',\n", - " 'shakespearean',\n", - " 'tragedy',\n", - " 'or',\n", - " 'a',\n", - " 'juicy',\n", - " 'soap',\n", - " 'opera',\n", - " '.'],\n", - " ['aggressive',\n", - " 'self-glorification',\n", - " 'and',\n", - " 'a',\n", - " 'manipulative',\n", - " 'whitewash',\n", - " '.'],\n", - " ['a',\n", - " 'comedy-drama',\n", - " 'of',\n", - " 'nearly',\n", - " 'epic',\n", - " 'proportions',\n", - " 'rooted',\n", - " 'in',\n", - " 'a',\n", - " 'sincere',\n", - " 'performance',\n", - " 'by',\n", - " 'the',\n", - " 'title',\n", - " 'character',\n", - " 'undergoing',\n", - " 'midlife',\n", - " 'crisis',\n", - " '.'],\n", - " ['narratively',\n", - " ',',\n", - " 'trouble',\n", - " 'every',\n", - " 'day',\n", - " 'is',\n", - " 'a',\n", - " 'plodding',\n", - " 'mess',\n", - " '.'],\n", - " ['the',\n", - " 'importance',\n", - " 'of',\n", - " 'being',\n", - " 'earnest',\n", - " ',',\n", - " 'so',\n", - " 'thick',\n", - " 'with',\n", - " 'wit',\n", - " 'it',\n", - " 'plays',\n", - " 'like',\n", - " 'a',\n", - " 'reading',\n", - " 'from',\n", - " 'bartlett',\n", - " \"'s\",\n", - " 'familiar',\n", - " 'quotations'],\n", - " ['but', 'it', 'does', \"n't\", 'leave', 'you', 'with', 'much', '.'],\n", - " ['you', 'could', 'hate', 'it', 'for', 'the', 'same', 'reason', '.'],\n", - " ['there',\n", - " \"'s\",\n", - " 'little',\n", - " 'to',\n", - " 'recommend',\n", - " 'snow',\n", - " 'dogs',\n", - " ',',\n", - " 'unless',\n", - " 'one',\n", - " 'considers',\n", - " 'cliched',\n", - " 'dialogue',\n", - " 'and',\n", - " 'perverse',\n", - " 'escapism',\n", - " 'a',\n", - " 'source',\n", - " 'of',\n", - " 'high',\n", - " 'hilarity',\n", - " '.'],\n", - " ['kung',\n", - " 'pow',\n", - " 'is',\n", - " 'oedekerk',\n", - " \"'s\",\n", - " 'realization',\n", - " 'of',\n", - " 'his',\n", - " 'childhood',\n", - " 'dream',\n", - " 'to',\n", - " 'be',\n", - " 'in',\n", - " 'a',\n", - " 'martial-arts',\n", - " 'flick',\n", - " ',',\n", - " 'and',\n", - " 'proves',\n", - " 'that',\n", - " 'sometimes',\n", - " 'the',\n", - " 'dreams',\n", - " 'of',\n", - " 'youth',\n", - " 'should',\n", - " 'remain',\n", - " 'just',\n", - " 'that',\n", - " '.'],\n", - " ['the', 'performances', 'are', 'an', 'absolute', 'joy', '.'],\n", - " ['fresnadillo',\n", - " 'has',\n", - " 'something',\n", - " 'serious',\n", - " 'to',\n", - " 'say',\n", - " 'about',\n", - " 'the',\n", - " 'ways',\n", - " 'in',\n", - " 'which',\n", - " 'extravagant',\n", - " 'chance',\n", - " 'can',\n", - " 'distort',\n", - " 'our',\n", - " 'perspective',\n", - " 'and',\n", - " 'throw',\n", - " 'us',\n", - " 'off',\n", - " 'the',\n", - " 'path',\n", - " 'of',\n", - " 'good',\n", - " 'sense',\n", - " '.'],\n", - " ['i',\n", - " 'still',\n", - " 'like',\n", - " 'moonlight',\n", - " 'mile',\n", - " ',',\n", - " 'better',\n", - " 'judgment',\n", - " 'be',\n", - " 'damned',\n", - " '.'],\n", - " ['a',\n", - " 'welcome',\n", - " 'relief',\n", - " 'from',\n", - " 'baseball',\n", - " 'movies',\n", - " 'that',\n", - " 'try',\n", - " 'too',\n", - " 'hard',\n", - " 'to',\n", - " 'be',\n", - " 'mythic',\n", - " ',',\n", - " 'this',\n", - " 'one',\n", - " 'is',\n", - " 'a',\n", - " 'sweet',\n", - " 'and',\n", - " 'modest',\n", - " 'and',\n", - " 'ultimately',\n", - " 'winning',\n", - " 'story',\n", - " '.'],\n", - " ['a',\n", - " 'bilingual',\n", - " 'charmer',\n", - " ',',\n", - " 'just',\n", - " 'like',\n", - " 'the',\n", - " 'woman',\n", - " 'who',\n", - " 'inspired',\n", - " 'it'],\n", - " ['like',\n", - " 'a',\n", - " 'less',\n", - " 'dizzily',\n", - " 'gorgeous',\n", - " 'companion',\n", - " 'to',\n", - " 'mr.',\n", - " 'wong',\n", - " \"'s\",\n", - " 'in',\n", - " 'the',\n", - " 'mood',\n", - " 'for',\n", - " 'love',\n", - " '--',\n", - " 'very',\n", - " 'much',\n", - " 'a',\n", - " 'hong',\n", - " 'kong',\n", - " 'movie',\n", - " 'despite',\n", - " 'its',\n", - " 'mainland',\n", - " 'setting',\n", - " '.'],\n", - " ['as',\n", - " 'inept',\n", - " 'as',\n", - " 'big-screen',\n", - " 'remakes',\n", - " 'of',\n", - " 'the',\n", - " 'avengers',\n", - " 'and',\n", - " 'the',\n", - " 'wild',\n", - " 'wild',\n", - " 'west',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'everything',\n", - " 'you',\n", - " \"'d\",\n", - " 'expect',\n", - " '--',\n", - " 'but',\n", - " 'nothing',\n", - " 'more',\n", - " '.'],\n", - " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", - " ['hatfield',\n", - " 'and',\n", - " 'hicks',\n", - " 'make',\n", - " 'the',\n", - " 'oddest',\n", - " 'of',\n", - " 'couples',\n", - " ',',\n", - " 'and',\n", - " 'in',\n", - " 'this',\n", - " 'sense',\n", - " 'the',\n", - " 'movie',\n", - " 'becomes',\n", - " 'a',\n", - " 'study',\n", - " 'of',\n", - " 'the',\n", - " 'gambles',\n", - " 'of',\n", - " 'the',\n", - " 'publishing',\n", - " 'world',\n", - " ',',\n", - " 'offering',\n", - " 'a',\n", - " 'case',\n", - " 'study',\n", - " 'that',\n", - " 'exists',\n", - " 'apart',\n", - " 'from',\n", - " 'all',\n", - " 'the',\n", - " 'movie',\n", - " \"'s\",\n", - " 'political',\n", - " 'ramifications',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'like',\n", - " 'going',\n", - " 'to',\n", - " 'a',\n", - " 'house',\n", - " 'party',\n", - " 'and',\n", - " 'watching',\n", - " 'the',\n", - " 'host',\n", - " 'defend',\n", - " 'himself',\n", - " 'against',\n", - " 'a',\n", - " 'frothing',\n", - " 'ex-girlfriend',\n", - " '.'],\n", - " ['that',\n", - " 'the',\n", - " 'chuck',\n", - " 'norris',\n", - " '``',\n", - " 'grenade',\n", - " 'gag',\n", - " \"''\",\n", - " 'occurs',\n", - " 'about',\n", - " '7',\n", - " 'times',\n", - " 'during',\n", - " 'windtalkers',\n", - " 'is',\n", - " 'a',\n", - " 'good',\n", - " 'indication',\n", - " 'of',\n", - " 'how',\n", - " 'serious-minded',\n", - " 'the',\n", - " 'film',\n", - " 'is',\n", - " '.'],\n", - " ['the',\n", - " 'plot',\n", - " 'is',\n", - " 'romantic',\n", - " 'comedy',\n", - " 'boilerplate',\n", - " 'from',\n", - " 'start',\n", - " 'to',\n", - " 'finish',\n", - " '.'],\n", - " ['it',\n", - " 'arrives',\n", - " 'with',\n", - " 'an',\n", - " 'impeccable',\n", - " 'pedigree',\n", - " ',',\n", - " 'mongrel',\n", - " 'pep',\n", - " ',',\n", - " 'and',\n", - " 'almost',\n", - " 'indecipherable',\n", - " 'plot',\n", - " 'complications',\n", - " '.'],\n", - " ['a',\n", - " 'film',\n", - " 'that',\n", - " 'clearly',\n", - " 'means',\n", - " 'to',\n", - " 'preach',\n", - " 'exclusively',\n", - " 'to',\n", - " 'the',\n", - " 'converted',\n", - " '.'],\n", - " ['while',\n", - " 'the',\n", - " 'importance',\n", - " 'of',\n", - " 'being',\n", - " 'earnest',\n", - " 'offers',\n", - " 'opportunities',\n", - " 'for',\n", - " 'occasional',\n", - " 'smiles',\n", - " 'and',\n", - " 'chuckles',\n", - " ',',\n", - " 'it',\n", - " 'does',\n", - " \"n't\",\n", - " 'give',\n", - " 'us',\n", - " 'a',\n", - " 'reason',\n", - " 'to',\n", - " 'be',\n", - " 'in',\n", - " 'the',\n", - " 'theater',\n", - " 'beyond',\n", - " 'wilde',\n", - " \"'s\",\n", - " 'wit',\n", - " 'and',\n", - " 'the',\n", - " 'actors',\n", - " \"'\",\n", - " 'performances',\n", - " '.'],\n", - " ['the',\n", - " 'latest',\n", - " 'vapid',\n", - " 'actor',\n", - " \"'s\",\n", - " 'exercise',\n", - " 'to',\n", - " 'appropriate',\n", - " 'the',\n", - " 'structure',\n", - " 'of',\n", - " 'arthur',\n", - " 'schnitzler',\n", - " \"'s\",\n", - " 'reigen',\n", - " '.'],\n", - " ['more',\n", - " 'vaudeville',\n", - " 'show',\n", - " 'than',\n", - " 'well-constructed',\n", - " 'narrative',\n", - " ',',\n", - " 'but',\n", - " 'on',\n", - " 'those',\n", - " 'terms',\n", - " 'it',\n", - " \"'s\",\n", - " 'inoffensive',\n", - " 'and',\n", - " 'actually',\n", - " 'rather',\n", - " 'sweet',\n", - " '.'],\n", - " ['nothing', 'more', 'than', 'a', 'run-of-the-mill', 'action', 'flick', '.'],\n", - " ['hampered',\n", - " '--',\n", - " 'no',\n", - " ',',\n", - " 'paralyzed',\n", - " '--',\n", - " 'by',\n", - " 'a',\n", - " 'self-indulgent',\n", - " 'script',\n", - " '...',\n", - " 'that',\n", - " 'aims',\n", - " 'for',\n", - " 'poetry',\n", - " 'and',\n", - " 'ends',\n", - " 'up',\n", - " 'sounding',\n", - " 'like',\n", - " 'satire',\n", - " '.'],\n", - " ['ice',\n", - " 'age',\n", - " 'is',\n", - " 'the',\n", - " 'first',\n", - " 'computer-generated',\n", - " 'feature',\n", - " 'cartoon',\n", - " 'to',\n", - " 'feel',\n", - " 'like',\n", - " 'other',\n", - " 'movies',\n", - " ',',\n", - " 'and',\n", - " 'that',\n", - " 'makes',\n", - " 'for',\n", - " 'some',\n", - " 'glacial',\n", - " 'pacing',\n", - " 'early',\n", - " 'on',\n", - " '.'],\n", - " ['there',\n", - " \"'s\",\n", - " 'very',\n", - " 'little',\n", - " 'sense',\n", - " 'to',\n", - " 'what',\n", - " \"'s\",\n", - " 'going',\n", - " 'on',\n", - " 'here',\n", - " ',',\n", - " 'but',\n", - " 'the',\n", - " 'makers',\n", - " 'serve',\n", - " 'up',\n", - " 'the',\n", - " 'cliches',\n", - " 'with',\n", - " 'considerable',\n", - " 'dash',\n", - " '.'],\n", - " ['cattaneo',\n", - " 'should',\n", - " 'have',\n", - " 'followed',\n", - " 'the',\n", - " 'runaway',\n", - " 'success',\n", - " 'of',\n", - " 'his',\n", - " 'first',\n", - " 'film',\n", - " ',',\n", - " 'the',\n", - " 'full',\n", - " 'monty',\n", - " ',',\n", - " 'with',\n", - " 'something',\n", - " 'different',\n", - " '.'],\n", - " ['they',\n", - " \"'re\",\n", - " 'the',\n", - " 'unnamed',\n", - " ',',\n", - " 'easily',\n", - " 'substitutable',\n", - " 'forces',\n", - " 'that',\n", - " 'serve',\n", - " 'as',\n", - " 'whatever',\n", - " 'terror',\n", - " 'the',\n", - " 'heroes',\n", - " 'of',\n", - " 'horror',\n", - " 'movies',\n", - " 'try',\n", - " 'to',\n", - " 'avoid',\n", - " '.'],\n", - " ['it',\n", - " 'almost',\n", - " 'feels',\n", - " 'as',\n", - " 'if',\n", - " 'the',\n", - " 'movie',\n", - " 'is',\n", - " 'more',\n", - " 'interested',\n", - " 'in',\n", - " 'entertaining',\n", - " 'itself',\n", - " 'than',\n", - " 'in',\n", - " 'amusing',\n", - " 'us',\n", - " '.'],\n", - " ['the',\n", - " 'movie',\n", - " \"'s\",\n", - " 'progression',\n", - " 'into',\n", - " 'rambling',\n", - " 'incoherence',\n", - " 'gives',\n", - " 'new',\n", - " 'meaning',\n", - " 'to',\n", - " 'the',\n", - " 'phrase',\n", - " '`',\n", - " 'fatal',\n", - " 'script',\n", - " 'error',\n", - " '.',\n", - " \"'\"],\n", - " ['i',\n", - " 'still',\n", - " 'like',\n", - " 'moonlight',\n", - " 'mile',\n", - " ',',\n", - " 'better',\n", - " 'judgment',\n", - " 'be',\n", - " 'damned',\n", - " '.'],\n", - " ['a',\n", - " 'welcome',\n", - " 'relief',\n", - " 'from',\n", - " 'baseball',\n", - " 'movies',\n", - " 'that',\n", - " 'try',\n", - " 'too',\n", - " 'hard',\n", - " 'to',\n", - " 'be',\n", - " 'mythic',\n", - " ',',\n", - " 'this',\n", - " 'one',\n", - " 'is',\n", - " 'a',\n", - " 'sweet',\n", - " 'and',\n", - " 'modest',\n", - " 'and',\n", - " 'ultimately',\n", - " 'winning',\n", - " 'story',\n", - " '.'],\n", - " ['a',\n", - " 'bilingual',\n", - " 'charmer',\n", - " ',',\n", - " 'just',\n", - " 'like',\n", - " 'the',\n", - " 'woman',\n", - " 'who',\n", - " 'inspired',\n", - " 'it'],\n", - " ['like',\n", - " 'a',\n", - " 'less',\n", - " 'dizzily',\n", - " 'gorgeous',\n", - " 'companion',\n", - " 'to',\n", - " 'mr.',\n", - " 'wong',\n", - " \"'s\",\n", - " 'in',\n", - " 'the',\n", - " 'mood',\n", - " 'for',\n", - " 'love',\n", - " '--',\n", - " 'very',\n", - " 'much',\n", - " 'a',\n", - " 'hong',\n", - " 'kong',\n", - " 'movie',\n", - " 'despite',\n", - " 'its',\n", - " 'mainland',\n", - " 'setting',\n", - " '.'],\n", - " ['as',\n", - " 'inept',\n", - " 'as',\n", - " 'big-screen',\n", - " 'remakes',\n", - " 'of',\n", - " 'the',\n", - " 'avengers',\n", - " 'and',\n", - " 'the',\n", - " 'wild',\n", - " 'wild',\n", - " 'west',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'everything',\n", - " 'you',\n", - " \"'d\",\n", - " 'expect',\n", - " '--',\n", - " 'but',\n", - " 'nothing',\n", - " 'more',\n", - " '.'],\n", - " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", - " ['hatfield',\n", - " 'and',\n", - " 'hicks',\n", - " 'make',\n", - " 'the',\n", - " 'oddest',\n", - " 'of',\n", - " 'couples',\n", - " ',',\n", - " 'and',\n", - " 'in',\n", - " 'this',\n", - " 'sense',\n", - " 'the',\n", - " 'movie',\n", - " 'becomes',\n", - " 'a',\n", - " 'study',\n", - " 'of',\n", - " 'the',\n", - " 'gambles',\n", - " 'of',\n", - " 'the',\n", - " 'publishing',\n", - " 'world',\n", - " ',',\n", - " 'offering',\n", - " 'a',\n", - " 'case',\n", - " 'study',\n", - " 'that',\n", - " 'exists',\n", - " 'apart',\n", - " 'from',\n", - " 'all',\n", - " 'the',\n", - " 'movie',\n", - " \"'s\",\n", - " 'political',\n", - " 'ramifications',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'like',\n", - " 'going',\n", - " 'to',\n", - " 'a',\n", - " 'house',\n", - " 'party',\n", - " 'and',\n", - " 'watching',\n", - " 'the',\n", - " 'host',\n", - " 'defend',\n", - " 'himself',\n", - " 'against',\n", - " 'a',\n", - " 'frothing',\n", - " 'ex-girlfriend',\n", - " '.'],\n", - " ['that',\n", - " 'the',\n", - " 'chuck',\n", - " 'norris',\n", - " '``',\n", - " 'grenade',\n", - " 'gag',\n", - " \"''\",\n", - " 'occurs',\n", - " 'about',\n", - " '7',\n", - " 'times',\n", - " 'during',\n", - " 'windtalkers',\n", - " 'is',\n", - " 'a',\n", - " 'good',\n", - " 'indication',\n", - " 'of',\n", - " 'how',\n", - " 'serious-minded',\n", - " 'the',\n", - " 'film',\n", - " 'is',\n", - " '.'],\n", - " ['the',\n", - " 'plot',\n", - " 'is',\n", - " 'romantic',\n", - " 'comedy',\n", - " 'boilerplate',\n", - " 'from',\n", - " 'start',\n", - " 'to',\n", - " 'finish',\n", - " '.'],\n", - " ['it',\n", - " 'arrives',\n", - " 'with',\n", - " 'an',\n", - " 'impeccable',\n", - " 'pedigree',\n", - " ',',\n", - " 'mongrel',\n", - " 'pep',\n", - " ',',\n", - " 'and',\n", - " 'almost',\n", - " 'indecipherable',\n", - " 'plot',\n", - " 'complications',\n", - " '.'],\n", - " ['a',\n", - " 'film',\n", - " 'that',\n", - " 'clearly',\n", - " 'means',\n", - " 'to',\n", - " 'preach',\n", - " 'exclusively',\n", - " 'to',\n", - " 'the',\n", - " 'converted',\n", - " '.'],\n", - " ['i',\n", - " 'still',\n", - " 'like',\n", - " 'moonlight',\n", - " 'mile',\n", - " ',',\n", - " 'better',\n", - " 'judgment',\n", - " 'be',\n", - " 'damned',\n", - " '.'],\n", - " ['a',\n", - " 'welcome',\n", - " 'relief',\n", - " 'from',\n", - " 'baseball',\n", - " 'movies',\n", - " 'that',\n", - " 'try',\n", - " 'too',\n", - " 'hard',\n", - " 'to',\n", - " 'be',\n", - " 'mythic',\n", - " ',',\n", - " 'this',\n", - " 'one',\n", - " 'is',\n", - " 'a',\n", - " 'sweet',\n", - " 'and',\n", - " 'modest',\n", - " 'and',\n", - " 'ultimately',\n", - " 'winning',\n", - " 'story',\n", - " '.'],\n", - " ['a',\n", - " 'bilingual',\n", - " 'charmer',\n", - " ',',\n", - " 'just',\n", - " 'like',\n", - " 'the',\n", - " 'woman',\n", - " 'who',\n", - " 'inspired',\n", - " 'it'],\n", - " ['like',\n", - " 'a',\n", - " 'less',\n", - " 'dizzily',\n", - " 'gorgeous',\n", - " 'companion',\n", - " 'to',\n", - " 'mr.',\n", - " 'wong',\n", - " \"'s\",\n", - " 'in',\n", - " 'the',\n", - " 'mood',\n", - " 'for',\n", - " 'love',\n", - " '--',\n", - " 'very',\n", - " 'much',\n", - " 'a',\n", - " 'hong',\n", - " 'kong',\n", - " 'movie',\n", - " 'despite',\n", - " 'its',\n", - " 'mainland',\n", - " 'setting',\n", - " '.'],\n", - " ['as',\n", - " 'inept',\n", - " 'as',\n", - " 'big-screen',\n", - " 'remakes',\n", - " 'of',\n", - " 'the',\n", - " 'avengers',\n", - " 'and',\n", - " 'the',\n", - " 'wild',\n", - " 'wild',\n", - " 'west',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'everything',\n", - " 'you',\n", - " \"'d\",\n", - " 'expect',\n", - " '--',\n", - " 'but',\n", - " 'nothing',\n", - " 'more',\n", - " '.'],\n", - " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", - " ['hatfield',\n", - " 'and',\n", - " 'hicks',\n", - " 'make',\n", - " 'the',\n", - " 'oddest',\n", - " 'of',\n", - " 'couples',\n", - " ',',\n", - " 'and',\n", - " 'in',\n", - " 'this',\n", - " 'sense',\n", - " 'the',\n", - " 'movie',\n", - " 'becomes',\n", - " 'a',\n", - " 'study',\n", - " 'of',\n", - " 'the',\n", - " 'gambles',\n", - " 'of',\n", - " 'the',\n", - " 'publishing',\n", - " 'world',\n", - " ',',\n", - " 'offering',\n", - " 'a',\n", - " 'case',\n", - " 'study',\n", - " 'that',\n", - " 'exists',\n", - " 'apart',\n", - " 'from',\n", - " 'all',\n", - " 'the',\n", - " 'movie',\n", - " \"'s\",\n", - " 'political',\n", - " 'ramifications',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'like',\n", - " 'going',\n", - " 'to',\n", - " 'a',\n", - " 'house',\n", - " 'party',\n", - " 'and',\n", - " 'watching',\n", - " 'the',\n", - " 'host',\n", - " 'defend',\n", - " 'himself',\n", - " 'against',\n", - " 'a',\n", - " 'frothing',\n", - " 'ex-girlfriend',\n", - " '.'],\n", - " ['that',\n", - " 'the',\n", - " 'chuck',\n", - " 'norris',\n", - " '``',\n", - " 'grenade',\n", - " 'gag',\n", - " \"''\",\n", - " 'occurs',\n", - " 'about',\n", - " '7',\n", - " 'times',\n", - " 'during',\n", - " 'windtalkers',\n", - " 'is',\n", - " 'a',\n", - " 'good',\n", - " 'indication',\n", - " 'of',\n", - " 'how',\n", - " 'serious-minded',\n", - " 'the',\n", - " 'film',\n", - " 'is',\n", - " '.'],\n", - " ['the',\n", - " 'plot',\n", - " 'is',\n", - " 'romantic',\n", - " 'comedy',\n", - " 'boilerplate',\n", - " 'from',\n", - " 'start',\n", - " 'to',\n", - " 'finish',\n", - " '.'],\n", - " ['it',\n", - " 'arrives',\n", - " 'with',\n", - " 'an',\n", - " 'impeccable',\n", - " 'pedigree',\n", - " ',',\n", - " 'mongrel',\n", - " 'pep',\n", - " ',',\n", - " 'and',\n", - " 'almost',\n", - " 'indecipherable',\n", - " 'plot',\n", - " 'complications',\n", - " '.'],\n", - " ['a',\n", - " 'film',\n", - " 'that',\n", - " 'clearly',\n", - " 'means',\n", - " 'to',\n", - " 'preach',\n", - " 'exclusively',\n", - " 'to',\n", - " 'the',\n", - " 'converted',\n", - " '.'],\n", - " ['i',\n", - " 'still',\n", - " 'like',\n", - " 'moonlight',\n", - " 'mile',\n", - " ',',\n", - " 'better',\n", - " 'judgment',\n", - " 'be',\n", - " 'damned',\n", - " '.'],\n", - " ['a',\n", - " 'welcome',\n", - " 'relief',\n", - " 'from',\n", - " 'baseball',\n", - " 'movies',\n", - " 'that',\n", - " 'try',\n", - " 'too',\n", - " 'hard',\n", - " 'to',\n", - " 'be',\n", - " 'mythic',\n", - " ',',\n", - " 'this',\n", - " 'one',\n", - " 'is',\n", - " 'a',\n", - " 'sweet',\n", - " 'and',\n", - " 'modest',\n", - " 'and',\n", - " 'ultimately',\n", - " 'winning',\n", - " 'story',\n", - " '.'],\n", - " ['a',\n", - " 'bilingual',\n", - " 'charmer',\n", - " ',',\n", - " 'just',\n", - " 'like',\n", - " 'the',\n", - " 'woman',\n", - " 'who',\n", - " 'inspired',\n", - " 'it'],\n", - " ['like',\n", - " 'a',\n", - " 'less',\n", - " 'dizzily',\n", - " 'gorgeous',\n", - " 'companion',\n", - " 'to',\n", - " 'mr.',\n", - " 'wong',\n", - " \"'s\",\n", - " 'in',\n", - " 'the',\n", - " 'mood',\n", - " 'for',\n", - " 'love',\n", - " '--',\n", - " 'very',\n", - " 'much',\n", - " 'a',\n", - " 'hong',\n", - " 'kong',\n", - " 'movie',\n", - " 'despite',\n", - " 'its',\n", - " 'mainland',\n", - " 'setting',\n", - " '.'],\n", - " ['as',\n", - " 'inept',\n", - " 'as',\n", - " 'big-screen',\n", - " 'remakes',\n", - " 'of',\n", - " 'the',\n", - " 'avengers',\n", - " 'and',\n", - " 'the',\n", - " 'wild',\n", - " 'wild',\n", - " 'west',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'everything',\n", - " 'you',\n", - " \"'d\",\n", - " 'expect',\n", - " '--',\n", - " 'but',\n", - " 'nothing',\n", - " 'more',\n", - " '.'],\n", - " ['best', 'indie', 'of', 'the', 'year', ',', 'so', 'far', '.'],\n", - " ['hatfield',\n", - " 'and',\n", - " 'hicks',\n", - " 'make',\n", - " 'the',\n", - " 'oddest',\n", - " 'of',\n", - " 'couples',\n", - " ',',\n", - " 'and',\n", - " 'in',\n", - " 'this',\n", - " 'sense',\n", - " 'the',\n", - " 'movie',\n", - " 'becomes',\n", - " 'a',\n", - " 'study',\n", - " 'of',\n", - " 'the',\n", - " 'gambles',\n", - " 'of',\n", - " 'the',\n", - " 'publishing',\n", - " 'world',\n", - " ',',\n", - " 'offering',\n", - " 'a',\n", - " 'case',\n", - " 'study',\n", - " 'that',\n", - " 'exists',\n", - " 'apart',\n", - " 'from',\n", - " 'all',\n", - " 'the',\n", - " 'movie',\n", - " \"'s\",\n", - " 'political',\n", - " 'ramifications',\n", - " '.'],\n", - " ['it',\n", - " \"'s\",\n", - " 'like',\n", - " 'going',\n", - " 'to',\n", - " 'a',\n", - " 'house',\n", - " 'party',\n", - " 'and',\n", - " 'watching',\n", - " 'the',\n", - " 'host',\n", - " 'defend',\n", - " 'himself',\n", - " 'against',\n", - " 'a',\n", - " 'frothing',\n", - " 'ex-girlfriend',\n", - " '.'],\n", - " ['that',\n", - " 'the',\n", - " 'chuck',\n", - " 'norris',\n", - " '``',\n", - " 'grenade',\n", - " 'gag',\n", - " \"''\",\n", - " 'occurs',\n", - " 'about',\n", - " '7',\n", - " 'times',\n", - " 'during',\n", - " 'windtalkers',\n", - " 'is',\n", - " 'a',\n", - " 'good',\n", - " 'indication',\n", - " 'of',\n", - " 'how',\n", - " 'serious-minded',\n", - " 'the',\n", - " 'film',\n", - " 'is',\n", - " '.'],\n", - " ['the',\n", - " 'plot',\n", - " 'is',\n", - " 'romantic',\n", - " 'comedy',\n", - " 'boilerplate',\n", - " 'from',\n", - " 'start',\n", - " 'to',\n", - " 'finish',\n", - " '.'],\n", - " ['it',\n", - " 'arrives',\n", - " 'with',\n", - " 'an',\n", - " 'impeccable',\n", - " 'pedigree',\n", - " ',',\n", - " 'mongrel',\n", - " 'pep',\n", - " ',',\n", - " 'and',\n", - " 'almost',\n", - " 'indecipherable',\n", - " 'plot',\n", - " 'complications',\n", - " '.'],\n", - " ['a',\n", - " 'film',\n", - " 'that',\n", - " 'clearly',\n", - " 'means',\n", - " 'to',\n", - " 'preach',\n", - " 'exclusively',\n", - " 'to',\n", - " 'the',\n", - " 'converted',\n", - " '.']]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 将所有数字转为小写\n", - "ds.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence')\n", - "# label转int\n", - "ds.apply(lambda x: int(x['label']), new_field_name='label_seq', is_target=True)\n", - "\n", - "def split_sent(ins):\n", - " return ins['raw_sentence'].split()\n", - "ds.apply(split_sent, new_field_name='words', is_input=True)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Train size: 54\n", - "Test size: 23\n" - ] - } - ], - "source": [ - "# 分割训练集/验证集\n", - "train_data, dev_data = ds.split(0.3)\n", - "print(\"Train size: \", len(train_data))\n", - "print(\"Test size: \", len(dev_data))" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[[120, 121, 6, 2, 122, 5, 72, 123, 3],\n", - " [14,\n", - " 4,\n", - " 152,\n", - " 153,\n", - " 154,\n", - " 155,\n", - " 8,\n", - " 156,\n", - " 157,\n", - " 9,\n", - " 16,\n", - " 2,\n", - " 158,\n", - " 21,\n", - " 159,\n", - " 30,\n", - " 98,\n", - " 57,\n", - " 4,\n", - " 160,\n", - " 161,\n", - " 13,\n", - " 162,\n", - " 163,\n", - " 164,\n", - " 165,\n", - " 3],\n", - " [4,\n", - " 112,\n", - " 113,\n", - " 15,\n", - " 114,\n", - " 35,\n", - " 10,\n", - " 68,\n", - " 115,\n", - " 69,\n", - " 8,\n", - " 23,\n", - " 116,\n", - " 5,\n", - " 18,\n", - " 36,\n", - " 11,\n", - " 4,\n", - " 70,\n", - " 7,\n", - " 117,\n", - " 7,\n", - " 118,\n", - " 119,\n", - " 71,\n", - " 3],\n", - " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", - " [2, 27, 11, 139, 140, 141, 15, 142, 8, 143, 3],\n", - " [12, 9, 14, 32, 8, 4, 59, 60, 7, 61, 2, 62, 63, 64, 65, 4, 66, 67, 3],\n", - " [97, 145, 14, 146, 147, 5, 148, 149, 23, 150, 3],\n", - " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", - " [4, 1, 1, 5, 138, 14, 2, 1, 1, 1, 12],\n", - " [14,\n", - " 4,\n", - " 152,\n", - " 153,\n", - " 154,\n", - " 155,\n", - " 8,\n", - " 156,\n", - " 157,\n", - " 9,\n", - " 16,\n", - " 2,\n", - " 158,\n", - " 21,\n", - " 159,\n", - " 30,\n", - " 98,\n", - " 57,\n", - " 4,\n", - " 160,\n", - " 161,\n", - " 13,\n", - " 162,\n", - " 163,\n", - " 164,\n", - " 165,\n", - " 3],\n", - " [10,\n", - " 2,\n", - " 82,\n", - " 83,\n", - " 84,\n", - " 85,\n", - " 86,\n", - " 87,\n", - " 88,\n", - " 89,\n", - " 90,\n", - " 91,\n", - " 92,\n", - " 93,\n", - " 11,\n", - " 4,\n", - " 28,\n", - " 94,\n", - " 6,\n", - " 95,\n", - " 96,\n", - " 2,\n", - " 17,\n", - " 11,\n", - " 3],\n", - " [12, 73, 20, 33, 74, 75, 5, 76, 77, 5, 7, 78, 79, 27, 80, 3],\n", - " [12, 78, 1, 24, 1, 2, 13, 11, 31, 1, 16, 1, 1, 133, 16, 1, 1, 3],\n", - " [24, 107, 24, 108, 109, 6, 2, 110, 7, 2, 34, 34, 111, 3],\n", - " [2, 27, 11, 139, 140, 141, 15, 142, 8, 143, 3],\n", - " [24, 107, 24, 108, 109, 6, 2, 110, 7, 2, 34, 34, 111, 3],\n", - " [97, 145, 14, 146, 147, 5, 148, 149, 23, 150, 3],\n", - " [4,\n", - " 112,\n", - " 113,\n", - " 15,\n", - " 114,\n", - " 35,\n", - " 10,\n", - " 68,\n", - " 115,\n", - " 69,\n", - " 8,\n", - " 23,\n", - " 116,\n", - " 5,\n", - " 18,\n", - " 36,\n", - " 11,\n", - " 4,\n", - " 70,\n", - " 7,\n", - " 117,\n", - " 7,\n", - " 118,\n", - " 119,\n", - " 71,\n", - " 3],\n", - " [12, 9, 99, 29, 100, 101, 30, 22, 58, 31, 3],\n", - " [12, 9, 99, 29, 100, 101, 30, 22, 58, 31, 3],\n", - " [120, 121, 6, 2, 122, 5, 72, 123, 3],\n", - " [1, 30, 1, 5, 1, 30, 1, 4, 1, 1, 1, 10, 1, 21, 1, 7, 1, 1, 1, 14, 1, 3],\n", - " [1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 8,\n", - " 1,\n", - " 89,\n", - " 2,\n", - " 1,\n", - " 16,\n", - " 151,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 7,\n", - " 1,\n", - " 1,\n", - " 1,\n", - " 2,\n", - " 1,\n", - " 6,\n", - " 28,\n", - " 25,\n", - " 3]]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from fastNLP import Vocabulary\n", - "vocab = Vocabulary(min_freq=2)\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['words']])\n", - "\n", - "# index句子, Vocabulary.to_index(word)\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True)\n", - "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', is_input=True)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## step 3\n", - " 定义模型" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "from fastNLP.models import CNNText\n", - "model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## step 4\n", - "开始训练" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "input fields after batch(if batch size is 2):\n", - "\twords: (1)type:numpy.ndarray (2)dtype:object, (3)shape:(2,) \n", - "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 11]) \n", - "target fields after batch(if batch size is 2):\n", - "\tlabel_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", - "\n" - ] - }, - { - "ename": "AttributeError", - "evalue": "'numpy.ndarray' object has no attribute 'contiguous'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAttributeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0mdev_data\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mdev_data\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mloss\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mCrossEntropyLoss\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m \u001b[0mmetrics\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mAccuracyMetric\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 7\u001b[0m )\n\u001b[1;32m 8\u001b[0m \u001b[0mtrainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, train_data, model, optimizer, loss, batch_size, sampler, update_every, n_epochs, print_every, dev_data, metrics, metric_key, validate_every, save_path, prefetch, use_tqdm, device, callbacks, check_code_level)\u001b[0m\n\u001b[1;32m 447\u001b[0m _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data,\n\u001b[1;32m 448\u001b[0m \u001b[0mmetric_key\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmetric_key\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcheck_level\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_code_level\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 449\u001b[0;31m batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE))\n\u001b[0m\u001b[1;32m 450\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 451\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtrain_data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_check_code\u001b[0;34m(dataset, model, losser, metrics, batch_size, dev_data, metric_key, check_level)\u001b[0m\n\u001b[1;32m 811\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 812\u001b[0m \u001b[0mrefined_batch_x\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 813\u001b[0;31m \u001b[0mpred_dict\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmodel\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mrefined_batch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 814\u001b[0m \u001b[0mfunc_signature\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_get_func_signature\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 815\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpred_dict\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/models/cnn_text_classification.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, words, seq_len)\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[0;34m:\u001b[0m\u001b[0;32mreturn\u001b[0m \u001b[0moutput\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mdict\u001b[0m \u001b[0mof\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mLongTensor\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mbatch_size\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnum_classes\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 59\u001b[0m \"\"\"\n\u001b[0;32m---> 60\u001b[0;31m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0membed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mwords\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# [N,L] -> [N,L,C]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 61\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mconv_pool\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;31m# [N,L,C] -> [N,C]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdropout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/modules/encoder/embedding.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, x)\u001b[0m\n\u001b[1;32m 33\u001b[0m \u001b[0;34m:\u001b[0m\u001b[0;32mreturn\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTensor\u001b[0m \u001b[0;34m:\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mbatch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0membed_dim\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 34\u001b[0m \"\"\"\n\u001b[0;32m---> 35\u001b[0;31m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msuper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 36\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdropout\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/sparse.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, input)\u001b[0m\n\u001b[1;32m 106\u001b[0m return F.embedding(\n\u001b[1;32m 107\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mweight\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpadding_idx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax_norm\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 108\u001b[0;31m self.norm_type, self.scale_grad_by_freq, self.sparse)\n\u001b[0m\u001b[1;32m 109\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 110\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mextra_repr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/functional.py\u001b[0m in \u001b[0;36membedding\u001b[0;34m(input, weight, padding_idx, max_norm, norm_type, scale_grad_by_freq, sparse)\u001b[0m\n\u001b[1;32m 1062\u001b[0m [ 0.6262, 0.2438, 0.7471]]])\n\u001b[1;32m 1063\u001b[0m \"\"\"\n\u001b[0;32m-> 1064\u001b[0;31m \u001b[0minput\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcontiguous\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1065\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpadding_idx\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1066\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mpadding_idx\u001b[0m \u001b[0;34m>\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mAttributeError\u001b[0m: 'numpy.ndarray' object has no attribute 'contiguous'" - ] - } - ], - "source": [ - "from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric\n", - "trainer = Trainer(model=model, \n", - " train_data=train_data, \n", - " dev_data=dev_data,\n", - " loss=CrossEntropyLoss(),\n", - " metrics=AccuracyMetric()\n", - " )\n", - "trainer.train()\n", - "print('Train finished!')\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 本教程结束。更多操作请参考进阶教程。" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/tutorials/quickstart.ipynb b/tutorials/quickstart.ipynb new file mode 100644 index 00000000..00c30c93 --- /dev/null +++ b/tutorials/quickstart.ipynb @@ -0,0 +1,280 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "# 快速入门" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP.io import CSVLoader\n", + "\n", + "loader = CSVLoader(headers=('raw_sentence', 'label'), sep='\\t')\n", + "dataset = loader.load(\"./sample_data/tutorial_sample_dataset.csv\")\n", + "dataset[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str,\n", + "'sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'words': ['a', 'series', 'of', 'escapades', 'demonstrating', 'the', 'adage', 'that', 'what', 'is', 'good', 'for', 'the', 'goose', 'is', 'also', 'good', 'for', 'the', 'gander', ',', 'some', 'of', 'which', 'occasionally', 'amuses', 'but', 'none', 'of', 'which', 'amounts', 'to', 'much', 'of', 'a', 'story', '.'] type=list}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 将所有字母转为小写, 并所有句子变成单词序列\n", + "dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='sentence')\n", + "dataset.apply(lambda x: x['sentence'].split(), new_field_name='words', is_input=True)\n", + "dataset[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str,\n", + "'sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'words': [4, 1, 6, 1, 1, 2, 1, 11, 153, 10, 28, 17, 2, 1, 10, 1, 28, 17, 2, 1, 5, 154, 6, 149, 1, 1, 23, 1, 6, 149, 1, 8, 30, 6, 4, 35, 3] type=list}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Vocabulary\n", + "\n", + "# 使用Vocabulary类统计单词,并将单词序列转化为数字序列\n", + "vocab = Vocabulary(min_freq=2).from_dataset(dataset, field_name='words')\n", + "vocab.index_dataset(dataset, field_name='words',new_field_name='words')\n", + "dataset[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str,\n", + "'sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'words': [4, 1, 6, 1, 1, 2, 1, 11, 153, 10, 28, 17, 2, 1, 10, 1, 28, 17, 2, 1, 5, 154, 6, 149, 1, 1, 23, 1, 6, 149, 1, 8, 30, 6, 4, 35, 3] type=list,\n", + "'target': 1 type=int}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 将label转为整数,并设置为 target\n", + "dataset.apply(lambda x: int(x['label']), new_field_name='target', is_target=True)\n", + "dataset[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "CNNText(\n", + " (embed): Embedding(\n", + " 177, 50\n", + " (dropout): Dropout(p=0.0)\n", + " )\n", + " (conv_pool): ConvMaxpool(\n", + " (convs): ModuleList(\n", + " (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,))\n", + " (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,))\n", + " (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,))\n", + " )\n", + " )\n", + " (dropout): Dropout(p=0.1)\n", + " (fc): Linear(in_features=12, out_features=5, bias=True)\n", + ")" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP.models import CNNText\n", + "model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n", + "model" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(62, 15)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# 分割训练集/验证集\n", + "train_data, dev_data = dataset.split(0.2)\n", + "len(train_data), len(dev_data)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input fields after batch(if batch size is 2):\n", + "\twords: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 26]) \n", + "target fields after batch(if batch size is 2):\n", + "\ttarget: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "\n", + "training epochs started 2019-05-09-10-59-39\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=20), HTML(value='')), layout=Layout(display='…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.333333\n", + "\n", + "Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.533333\n", + "\n", + "Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.533333\n", + "\n", + "Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.533333\n", + "\n", + "Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.6\n", + "\n", + "Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.8\n", + "\n", + "Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.8\n", + "\n", + "Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.733333\n", + "\n", + "Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.733333\n", + "\n", + "Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.733333\n", + "\n", + "\n", + "In Epoch:6/Step:12, got best dev performance:AccuracyMetric: acc=0.8\n", + "Reloaded the best model.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'best_eval': {'AccuracyMetric': {'acc': 0.8}},\n", + " 'best_epoch': 6,\n", + " 'best_step': 12,\n", + " 'seconds': 0.22}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric\n", + "\n", + "# 定义trainer并进行训练\n", + "trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data,\n", + " loss=CrossEntropyLoss(), metrics=AccuracyMetric())\n", + "trainer.train()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/tutorials/tutorial_one.ipynb b/tutorials/tutorial_one.ipynb new file mode 100644 index 00000000..db302238 --- /dev/null +++ b/tutorials/tutorial_one.ipynb @@ -0,0 +1,831 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "# 详细指南" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 数据读入" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str}" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP.io import CSVLoader\n", + "\n", + "loader = CSVLoader(headers=('raw_sentence', 'label'), sep='\\t')\n", + "dataset = loader.load(\"./sample_data/tutorial_sample_dataset.csv\")\n", + "dataset[0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instance表示一个样本,由一个或多个field(域,属性,特征)组成,每个field有名字和值。\n", + "\n", + "在初始化Instance时即可定义它包含的域,使用 \"field_name=field_value\"的写法。" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': fake data type=str,\n", + "'label': 0 type=str}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Instance\n", + "\n", + "dataset.append(Instance(raw_sentence='fake data', label='0'))\n", + "dataset[-1]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 数据处理" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str,\n", + "'sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'words': [4, 1, 6, 1, 1, 2, 1, 11, 153, 10, 28, 17, 2, 1, 10, 1, 28, 17, 2, 1, 5, 154, 6, 149, 1, 1, 23, 1, 6, 149, 1, 8, 30, 6, 4, 35, 3] type=list,\n", + "'target': 1 type=int}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Vocabulary\n", + "\n", + "# 将所有字母转为小写, 并所有句子变成单词序列\n", + "dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='sentence')\n", + "dataset.apply_field(lambda x: x.split(), field_name='sentence', new_field_name='words')\n", + "\n", + "# 使用Vocabulary类统计单词,并将单词序列转化为数字序列\n", + "vocab = Vocabulary(min_freq=2).from_dataset(dataset, field_name='words')\n", + "vocab.index_dataset(dataset, field_name='words',new_field_name='words')\n", + "\n", + "# 将label转为整数\n", + "dataset.apply(lambda x: int(x['label']), new_field_name='target')\n", + "dataset[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'label': 1 type=str,\n", + "'sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", + "'words': [4, 1, 6, 1, 1, 2, 1, 11, 153, 10, 28, 17, 2, 1, 10, 1, 28, 17, 2, 1, 5, 154, 6, 149, 1, 1, 23, 1, 6, 149, 1, 8, 30, 6, 4, 35, 3] type=list,\n", + "'target': 1 type=int,\n", + "'seq_len': 37 type=int}\n" + ] + } + ], + "source": [ + "# 增加长度信息\n", + "dataset.apply_field(lambda x: len(x), field_name='words', new_field_name='seq_len')\n", + "print(dataset[0])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 使用内置模块CNNText\n", + "设置为符合内置模块的名称" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "CNNText(\n", + " (embed): Embedding(\n", + " 177, 50\n", + " (dropout): Dropout(p=0.0)\n", + " )\n", + " (conv_pool): ConvMaxpool(\n", + " (convs): ModuleList(\n", + " (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,))\n", + " (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,))\n", + " (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,))\n", + " )\n", + " )\n", + " (dropout): Dropout(p=0.1)\n", + " (fc): Linear(in_features=12, out_features=5, bias=True)\n", + ")" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP.models import CNNText\n", + "\n", + "model_cnn = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n", + "model_cnn" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "我们在使用内置模块的时候,还应该使用应该注意把 field 设定成符合内置模型输入输出的名字。" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "words\n", + "seq_len\n", + "target\n" + ] + } + ], + "source": [ + "from fastNLP import Const\n", + "\n", + "dataset.rename_field('words', Const.INPUT)\n", + "dataset.rename_field('seq_len', Const.INPUT_LEN)\n", + "dataset.rename_field('target', Const.TARGET)\n", + "\n", + "dataset.set_input(Const.INPUT, Const.INPUT_LEN)\n", + "dataset.set_target(Const.TARGET)\n", + "\n", + "print(Const.INPUT)\n", + "print(Const.INPUT_LEN)\n", + "print(Const.TARGET)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 分割训练集/验证集/测试集" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(64, 7, 7)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "train_dev_data, test_data = dataset.split(0.1)\n", + "train_data, dev_data = train_dev_data.split(0.1)\n", + "len(train_data), len(dev_data), len(test_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 训练(model_cnn)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### loss\n", + "训练模型需要提供一个损失函数\n", + "\n", + "下面提供了一个在分类问题中常用的交叉熵损失。注意它的**初始化参数**。\n", + "\n", + "pred参数对应的是模型的forward返回的dict的一个key的名字,这里是\"output\"。\n", + "\n", + "target参数对应的是dataset作为标签的field的名字,这里是\"label_seq\"。" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from fastNLP import CrossEntropyLoss\n", + "\n", + "# loss = CrossEntropyLoss()\n", + "# 等价于\n", + "loss = CrossEntropyLoss(pred=Const.OUTPUT, target=Const.TARGET)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Metric\n", + "定义评价指标\n", + "\n", + "这里使用准确率。参数的“命名规则”跟上面类似。\n", + "\n", + "pred参数对应的是模型的predict方法返回的dict的一个key的名字,这里是\"predict\"。\n", + "\n", + "target参数对应的是dataset作为标签的field的名字,这里是\"label_seq\"。" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from fastNLP import AccuracyMetric\n", + "\n", + "# metrics=AccuracyMetric()\n", + "# 等价于\n", + "metrics=AccuracyMetric(pred=Const.OUTPUT, target=Const.TARGET)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input fields after batch(if batch size is 2):\n", + "\twords: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 16]) \n", + "\tseq_len: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "target fields after batch(if batch size is 2):\n", + "\ttarget: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "\n", + "training epochs started 2019-05-12-21-38-34\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=20), HTML(value='')), layout=Layout(display='…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.285714\n", + "\n", + "Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.428571\n", + "\n", + "Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.857143\n", + "\n", + "\n", + "In Epoch:8/Step:16, got best dev performance:AccuracyMetric: acc=0.857143\n", + "Reloaded the best model.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'best_eval': {'AccuracyMetric': {'acc': 0.857143}},\n", + " 'best_epoch': 8,\n", + " 'best_step': 16,\n", + " 'seconds': 0.21}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Trainer\n", + "\n", + "trainer = Trainer(model=model_cnn, train_data=train_data, dev_data=dev_data, loss=loss, metrics=metrics)\n", + "trainer.train()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 测试(model_cnn)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[tester] \n", + "AccuracyMetric: acc=0.857143\n" + ] + }, + { + "data": { + "text/plain": [ + "{'AccuracyMetric': {'acc': 0.857143}}" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Tester\n", + "\n", + "tester = Tester(test_data, model_cnn, metrics=AccuracyMetric())\n", + "tester.test()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 编写自己的模型\n", + "\n", + "完全支持 pytorch 的模型,与 pytorch 唯一不同的是返回结果是一个字典,字典中至少需要包含 \"pred\" 这个字段" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "\n", + "class LSTMText(nn.Module):\n", + " def __init__(self, vocab_size, embedding_dim, output_dim, hidden_dim=64, num_layers=2, dropout=0.5):\n", + " super().__init__()\n", + "\n", + " self.embedding = nn.Embedding(vocab_size, embedding_dim)\n", + " self.lstm = nn.LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True, dropout=dropout)\n", + " self.fc = nn.Linear(hidden_dim * 2, output_dim)\n", + " self.dropout = nn.Dropout(dropout)\n", + "\n", + " def forward(self, words):\n", + " # (input) words : (batch_size, seq_len)\n", + " words = words.permute(1,0)\n", + " # words : (seq_len, batch_size)\n", + "\n", + " embedded = self.dropout(self.embedding(words))\n", + " # embedded : (seq_len, batch_size, embedding_dim)\n", + " output, (hidden, cell) = self.lstm(embedded)\n", + " # output: (seq_len, batch_size, hidden_dim * 2)\n", + " # hidden: (num_layers * 2, batch_size, hidden_dim)\n", + " # cell: (num_layers * 2, batch_size, hidden_dim)\n", + "\n", + " hidden = torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)\n", + " hidden = self.dropout(hidden)\n", + " # hidden: (batch_size, hidden_dim * 2)\n", + "\n", + " pred = self.fc(hidden.squeeze(0))\n", + " # result: (batch_size, output_dim)\n", + " return {\"pred\":pred}" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input fields after batch(if batch size is 2):\n", + "\twords: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 16]) \n", + "\tseq_len: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "target fields after batch(if batch size is 2):\n", + "\ttarget: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "\n", + "training epochs started 2019-05-12-21-38-36\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=20), HTML(value='')), layout=Layout(display='…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.714286\n", + "\n", + "Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.857143\n", + "\n", + "\n", + "In Epoch:6/Step:12, got best dev performance:AccuracyMetric: acc=0.857143\n", + "Reloaded the best model.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'best_eval': {'AccuracyMetric': {'acc': 0.857143}},\n", + " 'best_epoch': 6,\n", + " 'best_step': 12,\n", + " 'seconds': 2.15}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model_lstm = LSTMText(len(vocab),50,5)\n", + "trainer = Trainer(model=model_lstm, train_data=train_data, dev_data=dev_data, loss=loss, metrics=metrics)\n", + "trainer.train()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[tester] \n", + "AccuracyMetric: acc=0.857143\n" + ] + }, + { + "data": { + "text/plain": [ + "{'AccuracyMetric': {'acc': 0.857143}}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tester = Tester(test_data, model_lstm, metrics=AccuracyMetric())\n", + "tester.test()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 使用 Batch编写自己的训练过程" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Epoch 0 Avg Loss: 3.11 18ms\n", + "Epoch 1 Avg Loss: 2.88 30ms\n", + "Epoch 2 Avg Loss: 2.69 42ms\n", + "Epoch 3 Avg Loss: 2.47 54ms\n", + "Epoch 4 Avg Loss: 2.38 67ms\n", + "Epoch 5 Avg Loss: 2.10 78ms\n", + "Epoch 6 Avg Loss: 2.06 91ms\n", + "Epoch 7 Avg Loss: 1.92 103ms\n", + "Epoch 8 Avg Loss: 1.91 114ms\n", + "Epoch 9 Avg Loss: 1.76 126ms\n", + "[tester] \n", + "AccuracyMetric: acc=0.571429\n" + ] + }, + { + "data": { + "text/plain": [ + "{'AccuracyMetric': {'acc': 0.571429}}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import BucketSampler\n", + "from fastNLP import Batch\n", + "import torch\n", + "import time\n", + "\n", + "model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n", + "\n", + "def train(epoch, data):\n", + " optim = torch.optim.Adam(model.parameters(), lr=0.001)\n", + " lossfunc = torch.nn.CrossEntropyLoss()\n", + " batch_size = 32\n", + "\n", + " # 定义一个Batch,传入DataSet,规定batch_size和去batch的规则。\n", + " # 顺序(Sequential),随机(Random),相似长度组成一个batch(Bucket)\n", + " train_sampler = BucketSampler(batch_size=batch_size, seq_len_field_name='seq_len')\n", + " train_batch = Batch(batch_size=batch_size, dataset=data, sampler=train_sampler)\n", + " \n", + " start_time = time.time()\n", + " for i in range(epoch):\n", + " loss_list = []\n", + " for batch_x, batch_y in train_batch:\n", + " optim.zero_grad()\n", + " output = model(batch_x['words'])\n", + " loss = lossfunc(output['pred'], batch_y['target'])\n", + " loss.backward()\n", + " optim.step()\n", + " loss_list.append(loss.item())\n", + " print('Epoch {:d} Avg Loss: {:.2f}'.format(i, sum(loss_list) / len(loss_list)),end=\" \")\n", + " print('{:d}ms'.format(round((time.time()-start_time)*1000)))\n", + " loss_list.clear()\n", + " \n", + "train(10, train_data)\n", + "tester = Tester(test_data, model, metrics=AccuracyMetric())\n", + "tester.test()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 使用 Callback 实现自己想要的效果" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "input fields after batch(if batch size is 2):\n", + "\twords: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 16]) \n", + "\tseq_len: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "target fields after batch(if batch size is 2):\n", + "\ttarget: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", + "\n", + "training epochs started 2019-05-12-21-38-40\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=20), HTML(value='')), layout=Layout(display='…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.285714\n", + "\n", + "Sum Time: 51ms\n", + "\n", + "\n", + "Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.285714\n", + "\n", + "Sum Time: 69ms\n", + "\n", + "\n", + "Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.285714\n", + "\n", + "Sum Time: 91ms\n", + "\n", + "\n", + "Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Sum Time: 107ms\n", + "\n", + "\n", + "Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Sum Time: 125ms\n", + "\n", + "\n", + "Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Sum Time: 142ms\n", + "\n", + "\n", + "Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Sum Time: 158ms\n", + "\n", + "\n", + "Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.571429\n", + "\n", + "Sum Time: 176ms\n", + "\n", + "\n", + "Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.714286\n", + "\n", + "Sum Time: 193ms\n", + "\n", + "\n", + "Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.857143\n", + "\n", + "Sum Time: 212ms\n", + "\n", + "\n", + "\n", + "In Epoch:10/Step:20, got best dev performance:AccuracyMetric: acc=0.857143\n", + "Reloaded the best model.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'best_eval': {'AccuracyMetric': {'acc': 0.857143}},\n", + " 'best_epoch': 10,\n", + " 'best_step': 20,\n", + " 'seconds': 0.2}" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from fastNLP import Callback\n", + "\n", + "start_time = time.time()\n", + "\n", + "class MyCallback(Callback):\n", + " def on_epoch_end(self):\n", + " print('Sum Time: {:d}ms\\n\\n'.format(round((time.time()-start_time)*1000)))\n", + " \n", + "\n", + "model = CNNText((len(vocab),50), num_classes=5, padding=2, dropout=0.1)\n", + "trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data,\n", + " loss=CrossEntropyLoss(), metrics=AccuracyMetric(), callbacks=[MyCallback()])\n", + "trainer.train()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.7" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} From fa246f5ab28049f3896f384baf9ac67b1a990fb0 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 22:11:46 +0800 Subject: [PATCH 111/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E4=BB=8B=E7=BB=8D=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 1 - fastNLP/core/callback.py | 9 ++++++--- fastNLP/core/const.py | 7 ++++++- fastNLP/models/__init__.py | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 9873be72..bb9dfb4a 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -33,7 +33,6 @@ __all__ = [ "EngChar2DPadder", "AccuracyMetric", - "BMESF1PreRecMetric", "SpanFPreRecMetric", "SQuADMetric", diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index ba73f101..76a9e2e2 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -1,5 +1,10 @@ r""" -callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class:`~fastNLP.Trainer` 类, +callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class:`~fastNLP.Trainer` 类。 + +虽然Trainer本身已经集成了一些功能,但仍然不足以囊括训练过程中可能需要到的功能, +比如负采样,learning rate decay, Early Stop等。 +为了解决这个问题fastNLP引入了callback的机制,Callback 是一种在Trainer训练过程中特定阶段会运行的函数集合。 +关于Trainer的详细文档,请参见 :doc:`trainer 模块` 我们将 :meth:`~fastNLP.Train.train` 这个函数内部分为以下的阶段,在对应阶段会触发相应的调用:: @@ -26,8 +31,6 @@ callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class: callback.on_train_end() # 训练结束 callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 -关于Trainer的详细文档,请参见 :doc:`trainer 模块` - 如下面的例子所示,我们可以使用内置的 callback 类,或者继承 :class:`~fastNLP.core.callback.Callback` 定义自己的 callback 类:: diff --git a/fastNLP/core/const.py b/fastNLP/core/const.py index f3022db2..89ff51a2 100644 --- a/fastNLP/core/const.py +++ b/fastNLP/core/const.py @@ -1,5 +1,10 @@ class Const: - """fastNLP中field命名常量。 + """ + fastNLP中field命名常量。 + + .. todo:: + 把下面这段改成表格 + 具体列表:: INPUT 模型的序列输入 words(复数words1, words2) diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index f0d84b1c..bad96cf9 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -3,6 +3,7 @@ TODO 详细介绍的表格,与主页相对应 """ +__all__ = ["CNNText", "SeqLabeling", "ESIM", "STSeqLabel", "AdvSeqLabel", "STNLICls", "STSeqCls"] from .base_model import BaseModel from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequenceClassification, \ BertForTokenClassification From e3d6560132ef95d8100c8fd97ee1b189964b142c Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 12 May 2019 23:54:10 +0800 Subject: [PATCH 112/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20models=20?= =?UTF-8?q?=E5=92=8C=20modules=20=E7=9A=84=E4=B8=BB=E9=A1=B5=E4=BB=8B?= =?UTF-8?q?=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 10bab0eb..687b1c33 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,13 +12,14 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 - 便捷且具有扩展性的训练器; 提供多种内置callback函数,方便实验记录、异常捕获等。 -内置的模块 +内置组件 ------------ -(TODO) +大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三个阶段组成。 +.. image:: figures/text_classification.png -主要包含了以下的三大模块: +三大模块功能和例子如下: +-----------------------+-----------------------+-----------------------+ | module type | functionality | example | @@ -34,17 +35,17 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 | | 形式 | | +-----------------------+-----------------------+-----------------------+ +fastNLP 在 :mod:`~fastNLP.module` 模块中内置了大量的组件,可以帮助用户快速搭建自己所需的网络 -For example: - -.. image:: figures/text_classification.png -.. todo:: - 各个任务上的结果 - -内置的模型 +内置模型 ---------------- +fastNLP 在 :mod:`~fastNLP.models` 模块中内置了如 :class:`~fastNLP.models.CNNText` 、 +:class:`~fastNLP.models.SeqLabeling` 等完整的模型,以供用户直接使用。 + +.. todo:: + 这些模型的介绍如下表所示:(模型名称 + 介绍 + 任务上的结果) 用户手册 ---------------- From 269e28cc795eb22ba4414a37c916b7a8c174e5d8 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 13 May 2019 00:23:55 +0800 Subject: [PATCH 113/173] =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E4=BA=86=20titlesonl?= =?UTF-8?q?y=20=E8=BF=99=E4=B8=AA=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.core.rst | 1 + docs/source/fastNLP.io.rst | 1 + docs/source/fastNLP.models.rst | 1 + docs/source/fastNLP.modules.aggregator.rst | 1 + docs/source/fastNLP.modules.decoder.rst | 1 + docs/source/fastNLP.modules.encoder.rst | 1 + docs/source/fastNLP.modules.rst | 1 + docs/source/fastNLP.rst | 1 + docs/source/index.rst | 9 ++++----- docs/source/modules.rst | 1 + 10 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst index 932c6b21..ba784a59 100644 --- a/docs/source/fastNLP.core.rst +++ b/docs/source/fastNLP.core.rst @@ -10,6 +10,7 @@ fastNLP.core 模块 ---------- .. toctree:: + :titlesonly: fastNLP.core.batch fastNLP.core.callback diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index ae28573d..88498d9a 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -10,6 +10,7 @@ fastNLP.io 模块 ---------- .. toctree:: + :titlesonly: fastNLP.io.base_loader fastNLP.io.dataset_loader diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index c1be3a4c..2c243295 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -10,6 +10,7 @@ fastNLP.models 模块 ---------- .. toctree:: + :titlesonly: fastNLP.models.base_model fastNLP.models.bert diff --git a/docs/source/fastNLP.modules.aggregator.rst b/docs/source/fastNLP.modules.aggregator.rst index 4f8baabd..44398325 100644 --- a/docs/source/fastNLP.modules.aggregator.rst +++ b/docs/source/fastNLP.modules.aggregator.rst @@ -10,6 +10,7 @@ fastNLP.modules.aggregator ---------- .. toctree:: + :titlesonly: fastNLP.modules.aggregator.attention fastNLP.modules.aggregator.pooling diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index fbda11d9..1c28740b 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -10,6 +10,7 @@ fastNLP.modules.decoder ---------- .. toctree:: + :titlesonly: fastNLP.modules.decoder.CRF fastNLP.modules.decoder.MLP diff --git a/docs/source/fastNLP.modules.encoder.rst b/docs/source/fastNLP.modules.encoder.rst index 9a11fe74..b15232fa 100644 --- a/docs/source/fastNLP.modules.encoder.rst +++ b/docs/source/fastNLP.modules.encoder.rst @@ -10,6 +10,7 @@ fastNLP.modules.encoder ---------- .. toctree:: + :titlesonly: fastNLP.modules.encoder.bert fastNLP.modules.encoder.char_encoder diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 263005f0..4f05ae7b 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -10,6 +10,7 @@ fastNLP.modules 模块 ----------- .. toctree:: + :titlesonly: fastNLP.modules.aggregator fastNLP.modules.decoder diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst index eaa06ea8..a795045a 100644 --- a/docs/source/fastNLP.rst +++ b/docs/source/fastNLP.rst @@ -10,6 +10,7 @@ ----------- .. toctree:: + :titlesonly: :maxdepth: 3 fastNLP.core diff --git a/docs/source/index.rst b/docs/source/index.rst index 687b1c33..d77ae1c8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,11 +15,12 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 内置组件 ------------ -大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三个阶段组成。 +大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三中模块组成。 .. image:: figures/text_classification.png -三大模块功能和例子如下: +fastNLP 在 :mod:`~fastNLP.modules` 模块中内置了三个模块的诸多组件,可以帮助用户快速搭建自己所需的网络。 +三个模块的功能和常见组件如下: +-----------------------+-----------------------+-----------------------+ | module type | functionality | example | @@ -35,8 +36,6 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 | | 形式 | | +-----------------------+-----------------------+-----------------------+ -fastNLP 在 :mod:`~fastNLP.module` 模块中内置了大量的组件,可以帮助用户快速搭建自己所需的网络 - 内置模型 ---------------- @@ -65,7 +64,7 @@ API 文档 除了用户手册之外,你还可以通过查阅 API 文档来找到你所需要的工具。 .. toctree:: - :maxdepth: 2 + :titlesonly: fastNLP diff --git a/docs/source/modules.rst b/docs/source/modules.rst index e9a92cb7..9ca3c7f3 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -2,6 +2,7 @@ fastNLP ======= .. toctree:: + :titlesonly: :maxdepth: 4 fastNLP From 39a1fe9567beb467ec5bd24cc8817b3f98e76ff4 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 13 May 2019 00:49:06 +0800 Subject: [PATCH 114/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=96=87=E6=A1=A3=E5=B1=95=E7=A4=BA=E7=9A=84=E6=A0=87?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.core.rst | 4 ++-- docs/source/fastNLP.io.rst | 4 ++-- docs/source/fastNLP.models.rst | 4 ++-- docs/source/fastNLP.modules.rst | 4 ++-- docs/source/fastNLP.rst | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/fastNLP.core.rst b/docs/source/fastNLP.core.rst index ba784a59..82c13e46 100644 --- a/docs/source/fastNLP.core.rst +++ b/docs/source/fastNLP.core.rst @@ -1,5 +1,5 @@ -fastNLP.core 模块 -==================== +fastNLP.core +============ .. automodule:: fastNLP.core :members: diff --git a/docs/source/fastNLP.io.rst b/docs/source/fastNLP.io.rst index 88498d9a..fad05a21 100644 --- a/docs/source/fastNLP.io.rst +++ b/docs/source/fastNLP.io.rst @@ -1,5 +1,5 @@ -fastNLP.io 模块 -================== +fastNLP.io +========== .. automodule:: fastNLP.io :members: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index 2c243295..57592bf4 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -1,5 +1,5 @@ -fastNLP.models 模块 -====================== +fastNLP.models +============== .. automodule:: fastNLP.models :members: diff --git a/docs/source/fastNLP.modules.rst b/docs/source/fastNLP.modules.rst index 4f05ae7b..d04ccdcf 100644 --- a/docs/source/fastNLP.modules.rst +++ b/docs/source/fastNLP.modules.rst @@ -1,5 +1,5 @@ -fastNLP.modules 模块 -======================= +fastNLP.modules +=============== .. automodule:: fastNLP.modules :members: diff --git a/docs/source/fastNLP.rst b/docs/source/fastNLP.rst index a795045a..f0c3d41c 100644 --- a/docs/source/fastNLP.rst +++ b/docs/source/fastNLP.rst @@ -1,4 +1,4 @@ -用户 API +API 文档 =============== .. automodule:: fastNLP From 0a014fe3aeffafa473aaa79c8f0d99b5641e74f1 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 13 May 2019 00:49:54 +0800 Subject: [PATCH 115/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20core=20?= =?UTF-8?q?=E5=92=8C=20io=20=E7=9A=84=E5=BC=80=E7=AF=87=E4=BB=8B=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 11 +++++------ fastNLP/core/__init__.py | 9 +++++---- fastNLP/io/__init__.py | 13 ++++++------- fastNLP/io/dataset_loader.py | 5 ++--- fastNLP/io/embed_loader.py | 7 +------ 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index bb9dfb4a..5dd5fd54 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -1,12 +1,11 @@ """ fastNLP 由 :mod:`~fastNLP.core` 、 :mod:`~fastNLP.io` 、:mod:`~fastNLP.modules`、:mod:`~fastNLP.models` -和 :mod:`~fastNLP.component` 等子模块组成。 +等子模块组成,你可以点进去查看每个模块的文档。 -- :mod:`~fastNLP.core` fastNLP 的核心模块,包括 DataSet、 Trainer、 Tester 等组件 -- :mod:`~fastNLP.io` fastNLP 的输入输出模块,实现了数据集的读取,模型的存取等功能 -- :mod:`~fastNLP.modules` TODO 如何介绍 -- :mod:`~fastNLP.models` 使用 fastNLP 实现的一些常见模型,具体参见 :doc:`fastNLP.models` -- :mod:`~fastNLP.component` TODO +- :mod:`~fastNLP.core` 是fastNLP 的核心模块,包括 DataSet、 Trainer、 Tester 等组件。详见文档 :doc:`/fastNLP.core` +- :mod:`~fastNLP.io` 是实现输入输出的模块,包括了数据集的读取,模型的存取等功能。详见文档 :doc:`/fastNLP.io` +- :mod:`~fastNLP.modules` 包含了用于搭建神经网络模型的诸多组件,可以帮助用户快速搭建自己所需的网络。详见文档 :doc:`/fastNLP.modules` +- :mod:`~fastNLP.models` 包含了一些使用 fastNLP 实现的完整网络模型,包括CNNText、SeqLabeling等常见模型。详见文档 :doc:`/fastNLP.models` fastNLP 中最常用的组件可以直接从 fastNLP 包中 import ,他们的文档如下: """ diff --git a/fastNLP/core/__init__.py b/fastNLP/core/__init__.py index 3c5b3f42..d6ab8983 100644 --- a/fastNLP/core/__init__.py +++ b/fastNLP/core/__init__.py @@ -1,5 +1,5 @@ """ -core 模块里实现了 fastNLP 的核心框架,常用的组件都可以从 fastNLP 包中直接 import。当然你也同样可以从 core 模块的子模块中 import, +core 模块里实现了 fastNLP 的核心框架,常用的功能都可以从 fastNLP 包中直接 import。当然你也同样可以从 core 模块的子模块中 import, 例如 Batch 组件有两种 import 的方式:: # 直接从 fastNLP 中 import @@ -8,10 +8,11 @@ core 模块里实现了 fastNLP 的核心框架,常用的组件都可以从 fa # 从 core 模块的子模块 batch 中 import from fastNLP.core.batch import Batch -对于常用的功能,你只需要在 :doc:`fastNLP` 中查看即可。如果想了解各个子模块的分工,您可以阅读以下文档: - - TODO 向导 +对于常用的功能,你只需要在 :doc:`fastNLP` 中查看即可。如果想了解各个子模块的具体作用,您可以在下面找到每个子模块的具体文档。 +.. todo:: + 介绍core 的子模块的分工,好像必要性不大 + """ from .batch import Batch from .callback import Callback, GradientClipCallback, EarlyStopCallback, TensorboardCallback, LRScheduler, ControlC diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index b855a1bb..3baf878c 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -5,15 +5,10 @@ 2. 用于读入数据的 :doc:`DataSetLoader ` 类 -3. 用于保存和载入模型的类, 参考 :doc:`Model-IO ` +3. 用于保存和载入模型的类, 参考 :doc:`/fastNLP.io.model_io` -这些类的使用方法可以在对应module的文档下查看. +这些类的使用方法如下: """ -from .embed_loader import EmbedLoader -from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ - PeopleDailyCorpusLoader, Conll2003Loader -from .model_io import ModelLoader as ModelLoader, ModelSaver as ModelSaver - __all__ = [ 'EmbedLoader', @@ -29,3 +24,7 @@ __all__ = [ 'ModelLoader', 'ModelSaver', ] +from .embed_loader import EmbedLoader +from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ + PeopleDailyCorpusLoader, Conll2003Loader +from .model_io import ModelLoader as ModelLoader, ModelSaver as ModelSaver \ No newline at end of file diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 5df48d71..3cd475a5 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,8 +1,7 @@ """ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的数据, 并返回 `DataSet` , -得到的 :class:`~fastNLP.DataSet` 对象可以直接传入 :class:`~fastNLP.Trainer`, :class:`~fastNLP.Tester`, 用于模型的训练和测试 - -Example:: +得到的 :class:`~fastNLP.DataSet` 对象可以直接传入 :class:`~fastNLP.Trainer`, :class:`~fastNLP.Tester`, 用于模型的训练和测试。 +以SNLI数据集为例:: loader = SNLILoader() train_ds = loader.load('path/to/train') diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 4cc8f596..9f3a73dd 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,8 +1,3 @@ -""" -.. _embed-loader: - -用于读取预训练的embedding, 读取结果可直接载入为模型参数 -""" import os import numpy as np @@ -16,7 +11,7 @@ class EmbedLoader(BaseLoader): """ 别名::class:`fastNLP.io.EmbedLoader` :class:`fastNLP.io.embed_loader.EmbedLoader` - 这个类用于从预训练的Embedding中load数据。 + 用于读取预训练的embedding, 读取结果可直接载入为模型参数。 """ def __init__(self): From 208cf5facb8b5644516ced00df3251100c8275d0 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 13 May 2019 10:08:27 +0800 Subject: [PATCH 116/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20modules?= =?UTF-8?q?=20=E5=92=8C=20models=20=E5=BC=80=E7=AF=87=E4=BB=8B=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 6 ++-- fastNLP/models/__init__.py | 8 ++++-- fastNLP/modules/__init__.py | 40 +++++++++++++++++++++++--- fastNLP/modules/aggregator/__init__.py | 9 +++++- fastNLP/modules/decoder/__init__.py | 8 +++++- fastNLP/modules/encoder/__init__.py | 10 ++++--- 6 files changed, 66 insertions(+), 15 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index d77ae1c8..554b1afc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,12 +15,12 @@ fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地 内置组件 ------------ -大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三中模块组成。 +大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三种模块组成。 .. image:: figures/text_classification.png -fastNLP 在 :mod:`~fastNLP.modules` 模块中内置了三个模块的诸多组件,可以帮助用户快速搭建自己所需的网络。 -三个模块的功能和常见组件如下: +fastNLP 在 :mod:`~fastNLP.modules` 模块中内置了三种模块的诸多组件,可以帮助用户快速搭建自己所需的网络。 +三种模块的功能和常见组件如下: +-----------------------+-----------------------+-----------------------+ | module type | functionality | example | diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index bad96cf9..66af3a46 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -1,6 +1,10 @@ """ -使用 fastNLP 实现的一系列常见模型,具体有: -TODO 详细介绍的表格,与主页相对应 +fastNLP 在 :mod:`~fastNLP.models` 模块中内置了如 :class:`~fastNLP.models.CNNText` 、 +:class:`~fastNLP.models.SeqLabeling` 等完整的模型,以供用户直接使用。 + +.. todo:: + 这些模型的介绍(与主页一致) + """ __all__ = ["CNNText", "SeqLabeling", "ESIM", "STSeqLabel", "AdvSeqLabel", "STNLICls", "STSeqCls"] diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index 4022de9d..53d44f47 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -1,10 +1,25 @@ """ -modules 模块是 fastNLP 的重要组成部分,它实现了神经网络构建中常见的组件, -具体包括 TODO +大部分用于的 NLP 任务神经网络都可以看做由编码 :mod:`~fastNLP.modules.encoder` 、 +聚合 :mod:`~fastNLP.modules.aggregator` 、解码 :mod:`~fastNLP.modules.decoder` 三种模块组成。 -可以和 PyTorch 结合使用?TODO +.. image:: figures/text_classification.png -TODO __all__ 里面多暴露一些 +:mod:`~fastNLP.modules` 中实现了 fastNLP 提供的诸多模块组件,可以帮助用户快速搭建自己所需的网络。 +三种模块的功能和常见组件如下: + ++-----------------------+-----------------------+-----------------------+ +| module type | functionality | example | ++=======================+=======================+=======================+ +| encoder | 将输入编码为具有具 | embedding, RNN, CNN, | +| | 有表示能力的向量 | transformer | ++-----------------------+-----------------------+-----------------------+ +| aggregator | 从多个向量中聚合信息 | self-attention, | +| | | max-pooling | ++-----------------------+-----------------------+-----------------------+ +| decoder | 将具有某种表示意义的 | MLP, CRF | +| | 向量解码为需要的输出 | | +| | 形式 | | ++-----------------------+-----------------------+-----------------------+ """ from . import aggregator @@ -16,3 +31,20 @@ from .dropout import TimestepDropout from .encoder import * from .utils import get_embeddings +__all__ = [ + "LSTM", + "Embedding", + "ConvMaxpool", + "BertModel", + + "MaxPool", + "MaxPoolWithMask", + "AvgPool", + "MultiHeadAttention", + "BiAttention", + + "MLP", + "ConditionalRandomField", + "viterbi_decode", + "allowed_transitions", +] \ No newline at end of file diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index bfb7579b..4a76cf5b 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,7 +1,14 @@ -__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention", "BiAttention"] from .pooling import MaxPool from .pooling import MaxPoolWithMask from .pooling import AvgPool from .pooling import MeanPoolWithMask from .attention import MultiHeadAttention, BiAttention +__all__ = [ + "MaxPool", + "MaxPoolWithMask", + "AvgPool", + + "MultiHeadAttention", + "BiAttention" +] diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index 84763e03..516b687a 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,5 +1,11 @@ -__all__ = ["MLP", "ConditionalRandomField", "viterbi_decode", "allowed_transitions"] from .CRF import ConditionalRandomField from .MLP import MLP from .utils import viterbi_decode from .CRF import allowed_transitions + +__all__ = [ + "MLP", + "ConditionalRandomField", + "viterbi_decode", + "allowed_transitions" +] diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index a1cd910b..67f69850 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -3,7 +3,9 @@ from .embedding import Embedding from .lstm import LSTM from .bert import BertModel -__all__ = ["LSTM", - "Embedding", - "ConvMaxpool", - "BertModel"] +__all__ = [ + "LSTM", + "Embedding", + "ConvMaxpool", + "BertModel" +] From fec3216a0eba641764bc65971fd2c34720f2b022 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Mon, 13 May 2019 10:40:06 +0800 Subject: [PATCH 117/173] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20..=20todo::=20?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E4=BA=86=E5=8F=AF=E8=83=BD=E8=A2=AB=E6=8A=BD?= =?UTF-8?q?=E5=88=B0=E6=96=87=E6=A1=A3=E4=B8=AD=E7=9A=84=20TODO=20?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 5 ++++- fastNLP/core/losses.py | 3 +-- fastNLP/io/base_loader.py | 3 ++- fastNLP/modules/aggregator/attention.py | 3 ++- fastNLP/modules/utils.py | 5 ++++- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index d527bf76..7228842f 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -58,7 +58,10 @@ 2 DataSet与预处理 常见的预处理有如下几种 -2.1 从某个文本文件读取内容 # TODO 引用DataLoader +2.1 从某个文本文件读取内容 # + + .. todo:: + 引用DataLoader Example:: diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index ac08b46f..7a5fdf9d 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -221,8 +221,7 @@ class CrossEntropyLoss(LossBase): """ def __init__(self, pred=None, target=None, padding_idx=-100): - # TODO 需要做一些检查,F.cross_entropy在计算时,如果pred是(16, 10 ,4), target的形状按道理应该是(16, 10), 但实际却需要 - # TODO (16, 4) + # TODO 需要做一些检查,F.cross_entropy在计算时,如果pred是(16, 10 ,4), target的形状按道理应该是(16, 10), 但实际需要(16,4) super(CrossEntropyLoss, self).__init__() self._init_param_map(pred=pred, target=target) self.padding_idx = padding_idx diff --git a/fastNLP/io/base_loader.py b/fastNLP/io/base_loader.py index 569f7e2e..051de281 100644 --- a/fastNLP/io/base_loader.py +++ b/fastNLP/io/base_loader.py @@ -47,7 +47,6 @@ class BaseLoader(object): class DataLoaderRegister: - # TODO 这个类使用在何处? _readers = {} @classmethod @@ -64,3 +63,5 @@ class DataLoaderRegister: if read_fn_name in cls._readers: return cls._readers[read_fn_name] raise AttributeError('no read function: {}'.format(read_fn_name)) + + # TODO 这个类使用在何处? diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index 233dcb55..cea9c405 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -12,7 +12,8 @@ from ..utils import initial_parameter class DotAttention(nn.Module): """ - TODO + .. todo:: + 补上文档 """ def __init__(self, key_size, value_size, dropout=0): super(DotAttention, self).__init__() diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 78851587..047ebb78 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -70,7 +70,10 @@ def initial_parameter(net, initial_method=None): def get_embeddings(init_embed): """ - 得到词嵌入 TODO + 得到词嵌入 + + .. todo:: + 补上文档 :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, From 902a3a6bcd0b2f5667fdab387f9f550d1b8068bc Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 16:48:32 +0800 Subject: [PATCH 118/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/dataset.py | 46 ++++++++++++++++--------------------- fastNLP/core/utils.py | 5 ++-- test/models/model_runner.py | 1 + 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 7228842f..b506dfae 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -212,39 +212,33 @@ target和input,这种情况下,fastNLP默认不进行pad。另外,当某个field已经被设置为了target或者input后,之后append的 instance对应的field必须要和前面已有的内容一致,否则会报错。 - 可以查看field的dtype - - Example:: + 可以查看field的dtype:: - from fastNLP import DataSet + 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。 + 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进行类型检查 + from fastNLP import DataSet + d = DataSet({'data': [1, 'a']}) + d.set_input('data') + >> RuntimeError: Mixed data types in Field data: [, ] - Example:: + 可以通过设置以忽略对该field进行类型检查:: - from fastNLP import DataSet - d = DataSet({'data': [1, 'a']}) - d.set_ignore_type('data') - d.set_input('data') + from fastNLP import DataSet + d = DataSet({'data': [1, 'a']}) + d.set_ignore_type('data') + d.set_input('data') 当某个field被设置为忽略type之后,fastNLP将不对其进行pad。 diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index f7539fd7..a9a7ac0c 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -35,9 +35,7 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): """ 别名::class:`fastNLP.cache_results` :class:`fastNLP.core.uitls.cache_results` - cache_results是fastNLP中用于cache数据的装饰器。通过下面的例子看一下如何使用 - - Example:: + cache_results是fastNLP中用于cache数据的装饰器。通过下面的例子看一下如何使用:: import time import numpy as np @@ -607,6 +605,7 @@ def seq_len_to_mask(seq_len): 转变 1-d seq_len到2-d mask. Example:: + >>> seq_len = torch.arange(2, 16) >>> mask = seq_len_to_mask(seq_len) >>> print(mask.size()) diff --git a/test/models/model_runner.py b/test/models/model_runner.py index 3f4e1200..405aa7d6 100644 --- a/test/models/model_runner.py +++ b/test/models/model_runner.py @@ -6,6 +6,7 @@ 此模块的测试仅保证模型能使用fastNLP进行训练和测试,不测试模型实际性能 Example:: + # import 全大写变量... from model_runner import * From 8a7bf582445f35e0f839ef5cd2d63108b5b60fa0 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 16:59:18 +0800 Subject: [PATCH 119/173] delete an old metric in test --- test/core/test_metrics.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index db508e39..a5f7c0c3 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -4,7 +4,6 @@ import numpy as np import torch from fastNLP import AccuracyMetric -from fastNLP import BMESF1PreRecMetric from fastNLP.core.metrics import _pred_topk, _accuracy_topk From 6cb75104b80c2ccb0a4d17c1c61e6a4ef7b50bd6 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 19:27:02 +0800 Subject: [PATCH 120/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20tutorials=20?= =?UTF-8?q?=E7=9A=84=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_tutorials.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 8c0e37bf..4b1889d4 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -10,7 +10,7 @@ from fastNLP.core.metrics import AccuracyMetric class TestTutorial(unittest.TestCase): def test_fastnlp_10min_tutorial(self): # 从csv读取数据到DataSet - sample_path = "tutorials/sample_data/tutorial_sample_dataset.csv" + sample_path = "data_for_tests/tutorial_sample_dataset.csv" dataset = DataSet.read_csv(sample_path, headers=('raw_sentence', 'label'), sep='\t') print(len(dataset)) @@ -76,9 +76,7 @@ class TestTutorial(unittest.TestCase): from copy import deepcopy # 更改DataSet中对应field的名称,要以模型的forward等参数名一致 - train_data.rename_field('words', 'word_seq') # input field 与 forward 参数一致 train_data.rename_field('label', 'label_seq') - test_data.rename_field('words', 'word_seq') test_data.rename_field('label', 'label_seq') loss = CrossEntropyLoss(pred="output", target="label_seq") From 1c9a0b5875bd48e7b6ad3fec268a8f34772e7245 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 20:59:52 +0800 Subject: [PATCH 121/173] =?UTF-8?q?=E6=8A=8A=E6=9A=82=E4=B8=8D=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=9A=84=E5=8A=9F=E8=83=BD=E7=A7=BB=E5=88=B0=20legacy?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E5=A4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 6 ++++-- fastNLP/models/enas_trainer.py | 4 ++-- {fastNLP => legacy}/api/README.md | 0 {fastNLP => legacy}/api/__init__.py | 0 {fastNLP => legacy}/api/api.py | 8 ++++---- {fastNLP => legacy}/api/converter.py | 0 {fastNLP => legacy}/api/examples.py | 0 {fastNLP => legacy}/api/pipeline.py | 0 {fastNLP => legacy}/api/processor.py | 15 ++++++--------- {fastNLP => legacy}/api/utils.py | 2 +- {fastNLP => legacy}/automl/__init__.py | 0 {fastNLP => legacy}/automl/enas_controller.py | 0 {fastNLP => legacy}/automl/enas_model.py | 0 {fastNLP => legacy}/automl/enas_trainer.py | 12 ++++++------ {fastNLP => legacy}/automl/enas_utils.py | 0 {fastNLP => legacy}/component/__init__.py | 0 {fastNLP => legacy}/component/bert_tokenizer.py | 0 {test => legacy/test}/api/test_pipeline.py | 0 {test => legacy/test}/api/test_processor.py | 0 {test => legacy/test}/automl/test_enas.py | 0 20 files changed, 23 insertions(+), 24 deletions(-) rename {fastNLP => legacy}/api/README.md (100%) rename {fastNLP => legacy}/api/__init__.py (100%) rename {fastNLP => legacy}/api/api.py (98%) rename {fastNLP => legacy}/api/converter.py (100%) rename {fastNLP => legacy}/api/examples.py (100%) rename {fastNLP => legacy}/api/pipeline.py (100%) rename {fastNLP => legacy}/api/processor.py (98%) rename {fastNLP => legacy}/api/utils.py (98%) rename {fastNLP => legacy}/automl/__init__.py (100%) rename {fastNLP => legacy}/automl/enas_controller.py (100%) rename {fastNLP => legacy}/automl/enas_model.py (100%) rename {fastNLP => legacy}/automl/enas_trainer.py (98%) rename {fastNLP => legacy}/automl/enas_utils.py (100%) rename {fastNLP => legacy}/component/__init__.py (100%) rename {fastNLP => legacy}/component/bert_tokenizer.py (100%) rename {test => legacy/test}/api/test_pipeline.py (100%) rename {test => legacy/test}/api/test_processor.py (100%) rename {test => legacy/test}/automl/test_enas.py (100%) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 76a9e2e2..c944ec96 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -66,8 +66,9 @@ from ..io.model_io import ModelSaver, ModelLoader try: from tensorboardX import SummaryWriter + tensorboardX_flag = True except: - pass + tensorboardX_flag = False class Callback(object): @@ -581,7 +582,8 @@ class TensorboardCallback(Callback): path = os.path.join("./", 'tensorboard_logs_{}'.format(self.trainer.start_time)) else: path = os.path.join(save_dir, 'tensorboard_logs_{}'.format(self.trainer.start_time)) - self._summary_writer = SummaryWriter(path) + if tensorboardX_flag: + self._summary_writer = SummaryWriter(path) def on_batch_begin(self, batch_x, batch_y, indices): if "model" in self.options and self.graph_added is False: diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index cce93556..9cd7d8d0 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -78,7 +78,7 @@ class ENASTrainer(Trainer): results['seconds'] = 0. return results try: - if torch.cuda.is_available() and self.use_cuda: + if torch.cuda.is_available() and "cuda" in self.device: self.model = self.model.cuda() self._model_device = self.model.parameters().__next__().device self._mode(self.model, is_test=False) @@ -337,7 +337,7 @@ class ENASTrainer(Trainer): # policy loss loss = -log_probs*utils.get_variable(adv, - self.use_cuda, + 'cuda' in self.device, requires_grad=False) loss = loss.sum() # or loss.mean() diff --git a/fastNLP/api/README.md b/legacy/api/README.md similarity index 100% rename from fastNLP/api/README.md rename to legacy/api/README.md diff --git a/fastNLP/api/__init__.py b/legacy/api/__init__.py similarity index 100% rename from fastNLP/api/__init__.py rename to legacy/api/__init__.py diff --git a/fastNLP/api/api.py b/legacy/api/api.py similarity index 98% rename from fastNLP/api/api.py rename to legacy/api/api.py index 2e7cbfcf..0b5d3cd3 100644 --- a/fastNLP/api/api.py +++ b/legacy/api/api.py @@ -5,13 +5,13 @@ import torch warnings.filterwarnings('ignore') import os -from ..core.dataset import DataSet +from fastNLP.core.dataset import DataSet from .utils import load_url from .processor import ModelProcessor -from ..io.dataset_loader import _cut_long_sentence, ConllLoader -from ..core.instance import Instance +from fastNLP.io.dataset_loader import _cut_long_sentence, ConllLoader +from fastNLP.core.instance import Instance from ..api.pipeline import Pipeline -from ..core.metrics import SpanFPreRecMetric +from fastNLP.core.metrics import SpanFPreRecMetric from .processor import IndexerProcessor # TODO add pretrain urls diff --git a/fastNLP/api/converter.py b/legacy/api/converter.py similarity index 100% rename from fastNLP/api/converter.py rename to legacy/api/converter.py diff --git a/fastNLP/api/examples.py b/legacy/api/examples.py similarity index 100% rename from fastNLP/api/examples.py rename to legacy/api/examples.py diff --git a/fastNLP/api/pipeline.py b/legacy/api/pipeline.py similarity index 100% rename from fastNLP/api/pipeline.py rename to legacy/api/pipeline.py diff --git a/fastNLP/api/processor.py b/legacy/api/processor.py similarity index 98% rename from fastNLP/api/processor.py rename to legacy/api/processor.py index 3c60e621..4c442ed2 100644 --- a/fastNLP/api/processor.py +++ b/legacy/api/processor.py @@ -3,10 +3,10 @@ from collections import defaultdict import torch -from ..core.batch import Batch -from ..core.dataset import DataSet -from ..core.sampler import SequentialSampler -from ..core.vocabulary import Vocabulary +from fastNLP.core.batch import Batch +from fastNLP.core.dataset import DataSet +from fastNLP.core.sampler import SequentialSampler +from fastNLP.core.vocabulary import Vocabulary class Processor(object): @@ -232,7 +232,7 @@ class SeqLenProcessor(Processor): return dataset -from ..core.utils import _build_args +from fastNLP.core.utils import _build_args class ModelProcessor(Processor): @@ -257,10 +257,7 @@ class ModelProcessor(Processor): data_iterator = Batch(dataset, batch_size=self.batch_size, sampler=SequentialSampler()) batch_output = defaultdict(list) - if hasattr(self.model, "predict"): - predict_func = self.model.predict - else: - predict_func = self.model.forward + predict_func = self.model.forward with torch.no_grad(): for batch_x, _ in data_iterator: refined_batch_x = _build_args(predict_func, **batch_x) diff --git a/fastNLP/api/utils.py b/legacy/api/utils.py similarity index 98% rename from fastNLP/api/utils.py rename to legacy/api/utils.py index e8e7c42a..184e5fe6 100644 --- a/fastNLP/api/utils.py +++ b/legacy/api/utils.py @@ -22,7 +22,7 @@ except ImportError: try: from tqdm.auto import tqdm except: - from ..core.utils import _pseudo_tqdm as tqdm + from fastNLP.core.utils import _pseudo_tqdm as tqdm # matches bfd8deac from resnet18-bfd8deac.pth HASH_REGEX = re.compile(r'-([a-f0-9]*)\.') diff --git a/fastNLP/automl/__init__.py b/legacy/automl/__init__.py similarity index 100% rename from fastNLP/automl/__init__.py rename to legacy/automl/__init__.py diff --git a/fastNLP/automl/enas_controller.py b/legacy/automl/enas_controller.py similarity index 100% rename from fastNLP/automl/enas_controller.py rename to legacy/automl/enas_controller.py diff --git a/fastNLP/automl/enas_model.py b/legacy/automl/enas_model.py similarity index 100% rename from fastNLP/automl/enas_model.py rename to legacy/automl/enas_model.py diff --git a/fastNLP/automl/enas_trainer.py b/legacy/automl/enas_trainer.py similarity index 98% rename from fastNLP/automl/enas_trainer.py rename to legacy/automl/enas_trainer.py index 8f51c2cd..e3524aa9 100644 --- a/fastNLP/automl/enas_trainer.py +++ b/legacy/automl/enas_trainer.py @@ -11,15 +11,15 @@ import torch try: from tqdm.auto import tqdm except: - from ..core.utils import _pseudo_tqdm as tqdm + from fastNLP.core.utils import _pseudo_tqdm as tqdm -from ..core.batch import Batch -from ..core.callback import CallbackException -from ..core.dataset import DataSet -from ..core.utils import _move_dict_value_to_device +from fastNLP.core.batch import Batch +from fastNLP.core.callback import CallbackException +from fastNLP.core.dataset import DataSet +from fastNLP.core.utils import _move_dict_value_to_device import fastNLP from . import enas_utils as utils -from ..core.utils import _build_args +from fastNLP.core.utils import _build_args from torch.optim import Adam diff --git a/fastNLP/automl/enas_utils.py b/legacy/automl/enas_utils.py similarity index 100% rename from fastNLP/automl/enas_utils.py rename to legacy/automl/enas_utils.py diff --git a/fastNLP/component/__init__.py b/legacy/component/__init__.py similarity index 100% rename from fastNLP/component/__init__.py rename to legacy/component/__init__.py diff --git a/fastNLP/component/bert_tokenizer.py b/legacy/component/bert_tokenizer.py similarity index 100% rename from fastNLP/component/bert_tokenizer.py rename to legacy/component/bert_tokenizer.py diff --git a/test/api/test_pipeline.py b/legacy/test/api/test_pipeline.py similarity index 100% rename from test/api/test_pipeline.py rename to legacy/test/api/test_pipeline.py diff --git a/test/api/test_processor.py b/legacy/test/api/test_processor.py similarity index 100% rename from test/api/test_processor.py rename to legacy/test/api/test_processor.py diff --git a/test/automl/test_enas.py b/legacy/test/automl/test_enas.py similarity index 100% rename from test/automl/test_enas.py rename to legacy/test/automl/test_enas.py From 14fe885ecfa522b003b7439f31b294c4ec7ceb7d Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 22:05:52 +0800 Subject: [PATCH 122/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E4=B8=8D?= =?UTF-8?q?=E8=83=BD=E8=BF=90=E8=A1=8C=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- legacy/test/api/test_pipeline.py | 6 -- legacy/test/api/test_processor.py | 101 --------------------------- legacy/test/automl/test_enas.py | 111 ------------------------------ 3 files changed, 218 deletions(-) delete mode 100644 legacy/test/api/test_pipeline.py delete mode 100644 legacy/test/api/test_processor.py delete mode 100644 legacy/test/automl/test_enas.py diff --git a/legacy/test/api/test_pipeline.py b/legacy/test/api/test_pipeline.py deleted file mode 100644 index c7094790..00000000 --- a/legacy/test/api/test_pipeline.py +++ /dev/null @@ -1,6 +0,0 @@ -import unittest - - -class TestPipeline(unittest.TestCase): - def test_case(self): - pass diff --git a/legacy/test/api/test_processor.py b/legacy/test/api/test_processor.py deleted file mode 100644 index 9611e458..00000000 --- a/legacy/test/api/test_processor.py +++ /dev/null @@ -1,101 +0,0 @@ -import random -import unittest - -import numpy as np - -from fastNLP import Vocabulary, Instance -from fastNLP.api.processor import FullSpaceToHalfSpaceProcessor, PreAppendProcessor, SliceProcessor, Num2TagProcessor, \ - IndexerProcessor, VocabProcessor, SeqLenProcessor, ModelProcessor, Index2WordProcessor, SetTargetProcessor, \ - SetInputProcessor, VocabIndexerProcessor -from fastNLP.core.dataset import DataSet - - -class TestProcessor(unittest.TestCase): - def test_FullSpaceToHalfSpaceProcessor(self): - ds = DataSet({"word": ["00, u1, u), (u2, u2"]}) - proc = FullSpaceToHalfSpaceProcessor("word") - ds = proc(ds) - self.assertEqual(ds.field_arrays["word"].content, ["00, u1, u), (u2, u2"]) - - def test_PreAppendProcessor(self): - ds = DataSet({"word": [["1234", "3456"], ["8789", "3464"]]}) - proc = PreAppendProcessor(data="abc", field_name="word") - ds = proc(ds) - self.assertEqual(ds.field_arrays["word"].content, [["abc", "1234", "3456"], ["abc", "8789", "3464"]]) - - def test_SliceProcessor(self): - ds = DataSet({"xx": [[random.randint(0, 10) for _ in range(30)]] * 40}) - proc = SliceProcessor(10, 20, 2, "xx", new_added_field_name="yy") - ds = proc(ds) - self.assertEqual(len(ds.field_arrays["yy"].content[0]), 5) - - def test_Num2TagProcessor(self): - ds = DataSet({"num": [["99.9982", "2134.0"], ["0.002", "234"]]}) - proc = Num2TagProcessor("", "num") - ds = proc(ds) - for data in ds.field_arrays["num"].content: - for d in data: - self.assertEqual(d, "") - - def test_VocabProcessor_and_IndexerProcessor(self): - ds = DataSet({"xx": [[str(random.randint(0, 10)) for _ in range(30)]] * 40}) - vocab_proc = VocabProcessor("xx") - vocab_proc(ds) - vocab = vocab_proc.vocab - self.assertTrue(isinstance(vocab, Vocabulary)) - self.assertTrue(len(vocab) > 5) - - proc = IndexerProcessor(vocab, "xx", "yy") - ds = proc(ds) - for data in ds.field_arrays["yy"].content[0]: - self.assertTrue(isinstance(data, int)) - - def test_SeqLenProcessor(self): - ds = DataSet({"xx": [[str(random.randint(0, 10)) for _ in range(30)]] * 10}) - proc = SeqLenProcessor("xx", "len") - ds = proc(ds) - for data in ds.field_arrays["len"].content: - self.assertEqual(data, 30) - - def test_ModelProcessor(self): - from fastNLP.models.cnn_text_classification import CNNText - model = CNNText((100, 100), 5) - ins_list = [] - for _ in range(64): - seq_len = np.random.randint(5, 30) - ins_list.append(Instance(word_seq=[np.random.randint(0, 100) for _ in range(seq_len)], seq_lens=seq_len)) - data_set = DataSet(ins_list) - data_set.set_input("word_seq", "seq_lens") - proc = ModelProcessor(model) - data_set = proc(data_set) - self.assertTrue("pred" in data_set) - - def test_Index2WordProcessor(self): - vocab = Vocabulary() - vocab.add_word_lst(["a", "b", "c", "d", "e"]) - proc = Index2WordProcessor(vocab, "tag_id", "tag") - data_set = DataSet([Instance(tag_id=[np.random.randint(0, 7) for _ in range(32)])]) - data_set = proc(data_set) - self.assertTrue("tag" in data_set) - - def test_SetTargetProcessor(self): - proc = SetTargetProcessor("a", "b", "c") - data_set = DataSet({"a": [1, 2, 3], "b": [1, 2, 3], "c": [1, 2, 3]}) - data_set = proc(data_set) - self.assertTrue(data_set["a"].is_target) - self.assertTrue(data_set["b"].is_target) - self.assertTrue(data_set["c"].is_target) - - def test_SetInputProcessor(self): - proc = SetInputProcessor("a", "b", "c") - data_set = DataSet({"a": [1, 2, 3], "b": [1, 2, 3], "c": [1, 2, 3]}) - data_set = proc(data_set) - self.assertTrue(data_set["a"].is_input) - self.assertTrue(data_set["b"].is_input) - self.assertTrue(data_set["c"].is_input) - - def test_VocabIndexerProcessor(self): - proc = VocabIndexerProcessor("word_seq", "word_ids") - data_set = DataSet([Instance(word_seq=["a", "b", "c", "d", "e"])]) - data_set = proc(data_set) - self.assertTrue("word_ids" in data_set) diff --git a/legacy/test/automl/test_enas.py b/legacy/test/automl/test_enas.py deleted file mode 100644 index 4fea1063..00000000 --- a/legacy/test/automl/test_enas.py +++ /dev/null @@ -1,111 +0,0 @@ -import unittest - -from fastNLP import DataSet -from fastNLP import Instance -from fastNLP import Vocabulary -from fastNLP.core.losses import CrossEntropyLoss -from fastNLP.core.metrics import AccuracyMetric - - -class TestENAS(unittest.TestCase): - def testENAS(self): - # 从csv读取数据到DataSet - sample_path = "tutorials/sample_data/tutorial_sample_dataset.csv" - dataset = DataSet.read_csv(sample_path, headers=('raw_sentence', 'label'), - sep='\t') - print(len(dataset)) - print(dataset[0]) - print(dataset[-3]) - - dataset.append(Instance(raw_sentence='fake data', label='0')) - # 将所有数字转为小写 - dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') - # label转int - dataset.apply(lambda x: int(x['label']), new_field_name='label') - - # 使用空格分割句子 - def split_sent(ins): - return ins['raw_sentence'].split() - - dataset.apply(split_sent, new_field_name='words') - - # 增加长度信息 - dataset.apply(lambda x: len(x['words']), new_field_name='seq_len') - print(len(dataset)) - print(dataset[0]) - - # DataSet.drop(func)筛除数据 - dataset.drop(lambda x: x['seq_len'] <= 3, inplace=True) - print(len(dataset)) - - # 设置DataSet中,哪些field要转为tensor - # set target,loss或evaluate中的golden,计算loss,模型评估时使用 - dataset.set_target("label") - # set input,模型forward时使用 - dataset.set_input("words", "seq_len") - - # 分出测试集、训练集 - test_data, train_data = dataset.split(0.5) - print(len(test_data)) - print(len(train_data)) - - # 构建词表, Vocabulary.add(word) - vocab = Vocabulary(min_freq=2) - train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) - vocab.build_vocab() - - # index句子, Vocabulary.to_index(word) - train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') - test_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words') - print(test_data[0]) - - # 如果你们需要做强化学习或者GAN之类的项目,你们也可以使用这些数据预处理的工具 - from fastNLP.core.batch import Batch - from fastNLP.core.sampler import RandomSampler - - batch_iterator = Batch(dataset=train_data, batch_size=2, sampler=RandomSampler()) - for batch_x, batch_y in batch_iterator: - print("batch_x has: ", batch_x) - print("batch_y has: ", batch_y) - break - - from fastNLP.automl.enas_model import ENASModel - from fastNLP.automl.enas_controller import Controller - model = ENASModel(embed_num=len(vocab), num_classes=5) - controller = Controller() - - from fastNLP.automl.enas_trainer import ENASTrainer - - # 更改DataSet中对应field的名称,要以模型的forward等参数名一致 - train_data.rename_field('words', 'word_seq') # input field 与 forward 参数一致 - train_data.rename_field('label', 'label_seq') - test_data.rename_field('words', 'word_seq') - test_data.rename_field('label', 'label_seq') - - loss = CrossEntropyLoss(pred="output", target="label_seq") - metric = AccuracyMetric(pred="predict", target="label_seq") - - trainer = ENASTrainer(model=model, controller=controller, train_data=train_data, dev_data=test_data, - loss=CrossEntropyLoss(pred="output", target="label_seq"), - metrics=AccuracyMetric(pred="predict", target="label_seq"), - check_code_level=-1, - save_path=None, - batch_size=32, - print_every=1, - n_epochs=3, - final_epochs=1) - trainer.train() - print('Train finished!') - - # 调用Tester在test_data上评价效果 - from fastNLP import Tester - - tester = Tester(data=test_data, model=model, metrics=AccuracyMetric(pred="predict", target="label_seq"), - batch_size=4) - - acc = tester.test() - print(acc) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From 38c2ef7d74a54fbcaafa2e6a18cce8299a13d13d Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 22:49:49 +0800 Subject: [PATCH 123/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20callback=20?= =?UTF-8?q?=E7=9A=84=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 15 +- test/core/test_dataset.py | 2 +- test/test_tutorials.py | 14 +- .../advance_tutorial.ipynb | 235 +++++++----------- 4 files changed, 100 insertions(+), 166 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index c944ec96..f337975a 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -584,7 +584,9 @@ class TensorboardCallback(Callback): path = os.path.join(save_dir, 'tensorboard_logs_{}'.format(self.trainer.start_time)) if tensorboardX_flag: self._summary_writer = SummaryWriter(path) - + else: + self._summary_writer = None + def on_batch_begin(self, batch_x, batch_y, indices): if "model" in self.options and self.graph_added is False: # tesorboardX 这里有大bug,暂时没法画模型图 @@ -596,10 +598,10 @@ class TensorboardCallback(Callback): self.graph_added = True def on_backward_begin(self, loss): - if "loss" in self.options: + if "loss" in self.options and self._summary_writer: self._summary_writer.add_scalar("loss", loss.item(), global_step=self.trainer.step) - if "model" in self.options: + if "model" in self.options and self._summary_writer: for name, param in self.trainer.model.named_parameters(): if param.requires_grad: self._summary_writer.add_scalar(name + "_mean", param.mean(), global_step=self.trainer.step) @@ -608,15 +610,16 @@ class TensorboardCallback(Callback): global_step=self.trainer.step) def on_valid_end(self, eval_result, metric_key, optimizer, is_better_eval): - if "metric" in self.options: + if "metric" in self.options and self._summary_writer: for name, metric in eval_result.items(): for metric_key, metric_val in metric.items(): self._summary_writer.add_scalar("valid_{}_{}".format(name, metric_key), metric_val, global_step=self.trainer.step) def on_train_end(self): - self._summary_writer.close() - del self._summary_writer + if self._summary_writer: + self._summary_writer.close() + del self._summary_writer def on_exception(self, exception): if hasattr(self, "_summary_writer"): diff --git a/test/core/test_dataset.py b/test/core/test_dataset.py index 69548e73..0228f207 100644 --- a/test/core/test_dataset.py +++ b/test/core/test_dataset.py @@ -172,7 +172,7 @@ class TestDataSetMethods(unittest.TestCase): def split_sent(ins): return ins['raw_sentence'].split() csv_loader = CSVLoader(headers=['raw_sentence', 'label'],sep='\t') - dataset = csv_loader.load('../data_for_tests/tutorial_sample_dataset.csv') + dataset = csv_loader.load('test/data_for_tests/tutorial_sample_dataset.csv') dataset.drop(lambda x: len(x['raw_sentence'].split()) == 0, inplace=True) dataset.apply(split_sent, new_field_name='words', is_input=True) # print(dataset) diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 4b1889d4..255b391e 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -10,7 +10,7 @@ from fastNLP.core.metrics import AccuracyMetric class TestTutorial(unittest.TestCase): def test_fastnlp_10min_tutorial(self): # 从csv读取数据到DataSet - sample_path = "data_for_tests/tutorial_sample_dataset.csv" + sample_path = "test/data_for_tests/tutorial_sample_dataset.csv" dataset = DataSet.read_csv(sample_path, headers=('raw_sentence', 'label'), sep='\t') print(len(dataset)) @@ -113,14 +113,14 @@ class TestTutorial(unittest.TestCase): def test_fastnlp_1min_tutorial(self): # tutorials/fastnlp_1min_tutorial.ipynb - data_path = "tutorials/sample_data/tutorial_sample_dataset.csv" + data_path = "test/data_for_tests/tutorial_sample_dataset.csv" ds = DataSet.read_csv(data_path, headers=('raw_sentence', 'label'), sep='\t') print(ds[1]) # 将所有数字转为小写 ds.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence') # label转int - ds.apply(lambda x: int(x['label']), new_field_name='label_seq', is_target=True) + ds.apply(lambda x: int(x['label']), new_field_name='target', is_target=True) def split_sent(ins): return ins['raw_sentence'].split() @@ -137,9 +137,9 @@ class TestTutorial(unittest.TestCase): train_data.apply(lambda x: [vocab.add(word) for word in x['words']]) # index句子, Vocabulary.to_index(word) - train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', + train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words', is_input=True) - dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='word_seq', + dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words', is_input=True) from fastNLP.models import CNNText @@ -152,14 +152,14 @@ class TestTutorial(unittest.TestCase): dev_data=dev_data, loss=CrossEntropyLoss(), optimizer= Adam(), - metrics=AccuracyMetric(target='label_seq') + metrics=AccuracyMetric(target='target') ) trainer.train() print('Train finished!') def test_fastnlp_advanced_tutorial(self): import os - os.chdir("tutorials/fastnlp_advanced_tutorial") + os.chdir("test/tutorials/fastnlp_advanced_tutorial") from fastNLP import DataSet from fastNLP import Instance diff --git a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb b/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb index 64eb3462..7e487933 100644 --- a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb +++ b/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb @@ -170,11 +170,11 @@ { "data": { "text/plain": [ - "DataSet({'image': tensor([[ 4.7106e-01, -1.2246e+00, 3.1234e-01, -1.6781e+00, -8.7967e-01],\n", - " [ 1.1454e+00, 1.2236e-01, 3.0258e-01, -1.5454e+00, 8.9201e-01],\n", - " [-5.7143e-03, 3.9488e-01, 2.0287e-01, -1.5726e+00, 9.3171e-01],\n", - " [ 6.8914e-01, -2.6302e-01, -8.2694e-01, 9.5942e-01, -5.2589e-01],\n", - " [-5.7798e-03, -9.1621e-03, 1.0077e-03, 9.1716e-02, 1.0565e+00]]) type=torch.Tensor,\n", + "DataSet({'image': tensor([[ 0.3582, -1.0358, 1.4785, -1.5288, -0.9982],\n", + " [-0.3973, -0.4294, 0.9215, -1.9631, -1.6556],\n", + " [ 0.3313, -1.7714, 0.8729, 0.6976, -1.3172],\n", + " [-0.6403, 0.5023, -0.9919, 1.1178, -0.3710],\n", + " [-0.3692, 1.8631, -1.3646, -0.7290, -1.0774]]) type=torch.Tensor,\n", "'label': 0 type=int})" ] }, @@ -524,7 +524,11 @@ "outputs": [], "source": [ "# 设定特征域、标签域\n", - "data_set.set_input(\"premise\", \"premise_len\", \"hypothesis\", \"hypothesis_len\")\n", + "data_set.rename_field(\"premise\",\"words1\")\n", + "data_set.rename_field(\"premise_len\",\"seq_len1\")\n", + "data_set.rename_field(\"hypothesis\",\"words2\")\n", + "data_set.rename_field(\"hypothesis_len\",\"seq_len2\")\n", + "data_set.set_input(\"words1\", \"seq_len1\", \"words2\", \"seq_len2\")\n", "data_set.set_target(\"truth\")" ] }, @@ -536,10 +540,10 @@ { "data": { "text/plain": [ - "{'premise': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", - "'hypothesis': ['a', 'woman', 'eating', 'a', 'banana', 'crosses', 'a', 'street'] type=list,\n", - "'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - "'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + "{'words1': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", + "'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + "'words2': ['a', 'woman', 'eating', 'a', 'banana', 'crosses', 'a', 'street'] type=list,\n", + "'seq_len2': [1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", "'label': 0 type=int}" ] }, @@ -613,7 +617,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ @@ -622,49 +626,49 @@ "vocab = Vocabulary(max_size=10000, min_freq=2, unknown='', padding='')\n", "\n", "# 构建词表\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['premise']])\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['hypothesis']])\n", + "train_data.apply(lambda x: [vocab.add(word) for word in x['words1']])\n", + "train_data.apply(lambda x: [vocab.add(word) for word in x['words2']])\n", "vocab.build_vocab()" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "({'premise': [2, 10, 9, 2, 15, 115, 6, 11, 5, 132, 17, 2, 76, 9, 77, 55, 3] type=list,\n", - " 'hypothesis': [1, 2, 56, 17, 1, 4, 13, 49, 123, 12, 6, 11, 3] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 0 type=int},\n", - " {'premise': [50, 124, 10, 7, 68, 91, 92, 38, 2, 55, 3] type=list,\n", - " 'hypothesis': [21, 10, 5, 2, 55, 7, 99, 64, 48, 1, 22, 1, 3] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + "({'words1': [2, 9, 4, 2, 75, 85, 7, 86, 76, 77, 87, 88, 89, 2, 90, 3] type=list,\n", + " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'words2': [18, 9, 10, 1, 3] type=list,\n", + " 'seq_len2': [1, 1, 1, 1, 1] type=list,\n", " 'label': 1 type=int},\n", - " {'premise': [13, 24, 4, 14, 29, 5, 25, 4, 8, 39, 9, 14, 34, 4, 40, 41, 4, 16, 12, 2, 11, 4, 30, 28, 2, 42, 8, 2, 43, 44, 17, 2, 45, 35, 26, 31, 27, 5, 6, 32, 3] type=list,\n", - " 'hypothesis': [37, 49, 123, 30, 28, 2, 55, 12, 2, 11, 3] type=list,\n", - " 'premise_len': [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, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 0 type=int})" + " {'words1': [22, 32, 5, 110, 81, 111, 112, 5, 82, 3] type=list,\n", + " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'words2': [64, 32, 82, 133, 84, 3] type=list,\n", + " 'seq_len2': [1, 1, 1, 1, 1, 1] type=list,\n", + " 'label': 0 type=int},\n", + " {'words1': [2, 9, 97, 1, 20, 7, 54, 5, 1, 1, 70, 2, 11, 110, 2, 62, 3] type=list,\n", + " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'words2': [23, 1, 58, 10, 12, 1, 70, 133, 84, 3] type=list,\n", + " 'seq_len2': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'label': 1 type=int})" ] }, - "execution_count": 23, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 根据词表index句子\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise')\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis')\n", - "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise')\n", - "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis')\n", - "test_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise')\n", - "test_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis')\n", + "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", + "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", + "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", + "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", + "test_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", + "test_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", "train_data[-1], dev_data[-1], test_data[-1]" ] }, @@ -679,7 +683,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ @@ -703,35 +707,35 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "({'premise': [1037, 2158, 1998, 1037, 2450, 2892, 1996, 2395, 1999, 2392, 1997, 1037, 10733, 1998, 100, 4825, 1012] type=list,\n", - " 'hypothesis': [100, 1037, 3232, 1997, 7884, 1010, 2048, 2111, 3328, 2408, 1996, 2395, 1012] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 0 type=int},\n", - " {'premise': [2019, 3080, 2158, 2003, 5948, 4589, 10869, 2012, 1037, 4825, 1012] type=list,\n", - " 'hypothesis': [100, 2158, 1999, 1037, 4825, 2003, 3403, 2005, 2010, 7954, 2000, 7180, 1012] type=list,\n", - " 'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 1 type=int})" + "({'words1': [1037, 2450, 1999, 1037, 2665, 6598, 1998, 7415, 2058, 2014, 2132, 2559, 2875, 1037, 3028, 1012] type=list,\n", + " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'words2': [100, 2450, 2003, 3147, 1012] type=list,\n", + " 'seq_len2': [1, 1, 1, 1, 1] type=list,\n", + " 'label': 1 type=int},\n", + " {'words1': [2048, 2308, 1010, 3173, 2833, 100, 16143, 1010, 8549, 1012] type=list,\n", + " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", + " 'words2': [100, 2308, 8549, 2169, 2060, 1012] type=list,\n", + " 'seq_len2': [1, 1, 1, 1, 1, 1] type=list,\n", + " 'label': 0 type=int})" ] }, - "execution_count": 25, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 根据词表index句子\n", - "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['premise']], new_field_name='premise')\n", - "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis')\n", - "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['premise']], new_field_name='premise')\n", - "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis')\n", + "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words1']], new_field_name='words1')\n", + "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words2']], new_field_name='words2')\n", + "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words1']], new_field_name='words1')\n", + "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words2']], new_field_name='words2')\n", "train_data_2[-1], dev_data_2[-1]" ] }, @@ -747,7 +751,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -760,10 +764,10 @@ " 'num_classes': 3,\n", " 'gpu': True,\n", " 'batch_size': 32,\n", - " 'vocab_size': 156}" + " 'vocab_size': 143}" ] }, - "execution_count": 26, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -779,7 +783,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -788,21 +792,17 @@ "ESIM(\n", " (drop): Dropout(p=0.3)\n", " (embedding): Embedding(\n", - " (embed): Embedding(156, 300, padding_idx=0)\n", + " 143, 300\n", " (dropout): Dropout(p=0.3)\n", " )\n", - " (embedding_layer): Linear(\n", - " (linear): Linear(in_features=300, out_features=300, bias=True)\n", - " )\n", + " (embedding_layer): Linear(in_features=300, out_features=300, bias=True)\n", " (encoder): LSTM(\n", " (lstm): LSTM(300, 300, batch_first=True, bidirectional=True)\n", " )\n", - " (bi_attention): Bi_Attention()\n", + " (bi_attention): BiAttention()\n", " (mean_pooling): MeanPoolWithMask()\n", " (max_pooling): MaxPoolWithMask()\n", - " (inference_layer): Linear(\n", - " (linear): Linear(in_features=1200, out_features=300, bias=True)\n", - " )\n", + " (inference_layer): Linear(in_features=1200, out_features=300, bias=True)\n", " (decoder): LSTM(\n", " (lstm): LSTM(300, 300, batch_first=True, bidirectional=True)\n", " )\n", @@ -816,7 +816,7 @@ ")" ] }, - "execution_count": 27, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -824,49 +824,10 @@ "source": [ "# step 2:加载ESIM模型\n", "from fastNLP.models import ESIM\n", - "model = ESIM(**args.data)\n", + "model = ESIM(args[\"vocab_size\"], args[\"embed_dim\"], args[\"hidden_size\"], args[\"dropout\"], args[\"num_classes\"])\n", "model" ] }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "CNNText(\n", - " (embed): Embedding(\n", - " (embed): Embedding(156, 50, padding_idx=0)\n", - " (dropout): Dropout(p=0.0)\n", - " )\n", - " (conv_pool): ConvMaxpool(\n", - " (convs): ModuleList(\n", - " (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,))\n", - " (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,))\n", - " (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,))\n", - " )\n", - " )\n", - " (dropout): Dropout(p=0.1)\n", - " (fc): Linear(\n", - " (linear): Linear(in_features=12, out_features=5, bias=True)\n", - " )\n", - ")" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 另一个例子:加载CNN文本分类模型\n", - "from fastNLP.models import CNNText\n", - "cnn_text_model = CNNText(embed_num=len(vocab), embed_dim=50, num_classes=5, padding=2, dropout=0.1)\n", - "cnn_text_model" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -1009,54 +970,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "training epochs started 2019-04-14-23-22-28\n", - "[epoch: 1 step: 1] train loss: 1.51372 time: 0:00:00\n", - "[epoch: 1 step: 2] train loss: 1.26874 time: 0:00:00\n", - "[epoch: 1 step: 3] train loss: 1.49786 time: 0:00:00\n", - "[epoch: 1 step: 4] train loss: 1.37505 time: 0:00:00\n", - "Evaluation at Epoch 1/5. Step:4/20. AccuracyMetric: acc=0.344828\n", - "\n", - "[epoch: 2 step: 5] train loss: 1.21877 time: 0:00:00\n", - "[epoch: 2 step: 6] train loss: 1.14183 time: 0:00:00\n", - "[epoch: 2 step: 7] train loss: 1.15934 time: 0:00:00\n", - "[epoch: 2 step: 8] train loss: 1.55148 time: 0:00:00\n", - "Evaluation at Epoch 2/5. Step:8/20. AccuracyMetric: acc=0.344828\n", - "\n", - "[epoch: 3 step: 9] train loss: 1.1457 time: 0:00:00\n", - "[epoch: 3 step: 10] train loss: 1.0547 time: 0:00:00\n", - "[epoch: 3 step: 11] train loss: 1.40139 time: 0:00:00\n", - "[epoch: 3 step: 12] train loss: 0.551445 time: 0:00:00\n", - "Evaluation at Epoch 3/5. Step:12/20. AccuracyMetric: acc=0.275862\n", - "\n", - "[epoch: 4 step: 13] train loss: 1.07965 time: 0:00:00\n", - "[epoch: 4 step: 14] train loss: 1.04118 time: 0:00:00\n", - "[epoch: 4 step: 15] train loss: 1.11719 time: 0:00:00\n", - "[epoch: 4 step: 16] train loss: 1.09861 time: 0:00:00\n", - "Evaluation at Epoch 4/5. Step:16/20. AccuracyMetric: acc=0.275862\n", - "\n", - "[epoch: 5 step: 17] train loss: 1.10795 time: 0:00:00\n", - "[epoch: 5 step: 18] train loss: 1.26715 time: 0:00:00\n", - "[epoch: 5 step: 19] train loss: 1.19875 time: 0:00:00\n", - "[epoch: 5 step: 20] train loss: 1.09862 time: 0:00:00\n", - "Evaluation at Epoch 5/5. Step:20/20. AccuracyMetric: acc=0.37931\n", - "\n", - "\n", - "In Epoch:5/Step:20, got best dev performance:AccuracyMetric: acc=0.37931\n", - "Reloaded the best model.\n" + "training epochs started 2019-05-14-19-49-25\n" ] }, { - "data": { - "text/plain": [ - "{'best_eval': {'AccuracyMetric': {'acc': 0.37931}},\n", - " 'best_epoch': 5,\n", - " 'best_step': 20,\n", - " 'seconds': 0.5}" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" + "ename": "AssertionError", + "evalue": "seq_len can only have one dimension, got False.", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0muse_tqdm\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m )\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0mtrainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36mtrain\u001b[0;34m(self, load_best_model)\u001b[0m\n\u001b[1;32m 522\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 523\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_train_begin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 524\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_train\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 525\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_train_end\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 526\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mCallbackException\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mKeyboardInterrupt\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_train\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 573\u001b[0m \u001b[0;31m# negative sampling; replace unknown; re-weight batch_y\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 574\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_batch_begin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbatch_y\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindices\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 575\u001b[0;31m \u001b[0mprediction\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_data_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 576\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 577\u001b[0m \u001b[0;31m# edit prediction\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_data_forward\u001b[0;34m(self, network, x)\u001b[0m\n\u001b[1;32m 661\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_data_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnetwork\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 662\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 663\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnetwork\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 664\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 665\u001b[0m raise TypeError(\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/models/snli.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, words1, words2, seq_len1, seq_len2, target)\u001b[0m\n\u001b[1;32m 76\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 77\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mseq_len1\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 78\u001b[0;31m \u001b[0mseq_len1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len_to_mask\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseq_len1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 79\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 80\u001b[0m \u001b[0mseq_len1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mones\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpremise0\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpremise0\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/utils.py\u001b[0m in \u001b[0;36mseq_len_to_mask\u001b[0;34m(seq_len)\u001b[0m\n\u001b[1;32m 626\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 627\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseq_len\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTensor\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 628\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf\"seq_len can only have one dimension, got {seq_len.dim() == 1}.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 629\u001b[0m \u001b[0mbatch_size\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 630\u001b[0m \u001b[0mmax_len\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlong\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mAssertionError\u001b[0m: seq_len can only have one dimension, got False." + ] } ], "source": [ @@ -1073,7 +1005,6 @@ " print_every=-1,\n", " validate_every=-1,\n", " dev_data=dev_data,\n", - " use_cuda=True,\n", " optimizer=Adam(lr=1e-3, weight_decay=0),\n", " check_code_level=-1,\n", " metric_key='acc',\n", @@ -1178,7 +1109,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.6.7" } }, "nbformat": 4, From 63e023b2fad41b833ddf56ceb1443367043e3310 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 23:08:00 +0800 Subject: [PATCH 124/173] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E8=BF=87?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E6=95=99=E7=A8=8B=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_tutorials.py | 278 - .../advance_tutorial.ipynb | 1117 - .../fastnlp_advanced_tutorial/data/config | 8 - .../fastnlp_advanced_tutorial/hypothesis | 100 - tutorials/fastnlp_advanced_tutorial/label | 100 - tutorials/fastnlp_advanced_tutorial/premise | 100 - .../tutorial_sample_dataset.csv | 77 - tutorials/fastnlp_advanced_tutorial/vocab.txt | 30522 ---------------- 8 files changed, 32302 deletions(-) delete mode 100644 tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb delete mode 100644 tutorials/fastnlp_advanced_tutorial/data/config delete mode 100644 tutorials/fastnlp_advanced_tutorial/hypothesis delete mode 100644 tutorials/fastnlp_advanced_tutorial/label delete mode 100644 tutorials/fastnlp_advanced_tutorial/premise delete mode 100644 tutorials/fastnlp_advanced_tutorial/tutorial_sample_dataset.csv delete mode 100644 tutorials/fastnlp_advanced_tutorial/vocab.txt diff --git a/test/test_tutorials.py b/test/test_tutorials.py index 255b391e..128e4235 100644 --- a/test/test_tutorials.py +++ b/test/test_tutorials.py @@ -157,284 +157,6 @@ class TestTutorial(unittest.TestCase): trainer.train() print('Train finished!') - def test_fastnlp_advanced_tutorial(self): - import os - os.chdir("test/tutorials/fastnlp_advanced_tutorial") - - from fastNLP import DataSet - from fastNLP import Instance - from fastNLP import Vocabulary - from fastNLP import Trainer - from fastNLP import Tester - - # ### Instance - # Instance表示一个样本,由一个或者多个field(域、属性、特征)组成,每个field具有自己的名字以及值 - # 在初始化Instance的时候可以定义它包含的field,使用"field_name=field_value"的写法 - - # In[2]: - - # 组织一个Instance,这个Instance由premise、hypothesis、label三个field组成 - instance = Instance(premise='an premise example .', hypothesis='an hypothesis example.', label=1) - instance - - # In[3]: - - data_set = DataSet([instance] * 5) - data_set.append(instance) - data_set[-2:] - - # In[4]: - - # 如果某一个field的类型与dataset对应的field类型不一样仍可被加入dataset中 - instance2 = Instance(premise='the second premise example .', hypothesis='the second hypothesis example.', - label='1') - try: - data_set.append(instance2) - except: - pass - data_set[-2:] - - # In[5]: - - # 如果某一个field的名字不对,则该instance不能被append到dataset中 - instance3 = Instance(premises='the third premise example .', hypothesis='the third hypothesis example.', - label=1) - try: - data_set.append(instance3) - except: - print('cannot append instance') - pass - data_set[-2:] - - # In[6]: - - # 除了文本以外,还可以将tensor作为其中一个field的value - import torch - tensor_ins = Instance(image=torch.randn(5, 5), label=0) - ds = DataSet() - ds.append(tensor_ins) - ds - - from fastNLP import DataSet - from fastNLP import Instance - - # 从csv读取数据到DataSet - # 类csv文件,即每一行为一个example的文件,都可以使用这种方法进行数据读取 - dataset = DataSet.read_csv('tutorial_sample_dataset.csv', headers=('raw_sentence', 'label'), sep='\t') - # 查看DataSet的大小 - len(dataset) - - # In[8]: - - # 使用数字索引[k],获取第k个样本 - dataset[0] - - # In[9]: - - # 获取的样本是一个Instance - type(dataset[0]) - - # In[10]: - - # 使用数字索引[a: b],获取第a到第b个样本 - dataset[0: 3] - - # In[11]: - - # 索引也可以是负数 - dataset[-1] - - data_path = ['premise', 'hypothesis', 'label'] - - # 读入文件 - with open(data_path[0]) as f: - premise = f.readlines() - - with open(data_path[1]) as f: - hypothesis = f.readlines() - - with open(data_path[2]) as f: - label = f.readlines() - - assert len(premise) == len(hypothesis) and len(hypothesis) == len(label) - - # 组织DataSet - data_set = DataSet() - for p, h, l in zip(premise, hypothesis, label): - p = p.strip() # 将行末空格去除 - h = h.strip() # 将行末空格去除 - data_set.append(Instance(premise=p, hypothesis=h, truth=l)) - - data_set[0] - - # ### DataSet的其他操作 - # 在构建完毕DataSet后,仍然可以对DataSet的内容进行操作,函数接口为DataSet.apply() - - # In[13]: - - # 将premise域的所有文本转成小写 - data_set.apply(lambda x: x['premise'].lower(), new_field_name='premise') - data_set[-2:] - - # In[14]: - - # label转int - data_set.apply(lambda x: int(x['truth']), new_field_name='truth') - data_set[-2:] - - # In[15]: - - # 使用空格分割句子 - def split_sent(ins): - return ins['premise'].split() - - data_set.apply(split_sent, new_field_name='premise') - data_set.apply(lambda x: x['hypothesis'].split(), new_field_name='hypothesis') - data_set[-2:] - - # In[16]: - - # 筛选数据 - origin_data_set_len = len(data_set) - data_set.drop(lambda x: len(x['premise']) <= 6, inplace=True) - origin_data_set_len, len(data_set) - - # In[17]: - - # 增加长度信息 - data_set.apply(lambda x: [1] * len(x['premise']), new_field_name='premise_len') - data_set.apply(lambda x: [1] * len(x['hypothesis']), new_field_name='hypothesis_len') - data_set[-1] - - # In[18]: - - # 设定特征域、标签域 - data_set.set_input("premise", "premise_len", "hypothesis", "hypothesis_len") - data_set.set_target("truth") - - # In[19]: - - # 重命名field - data_set.rename_field('truth', 'label') - data_set[-1] - - # In[20]: - - # 切分训练、验证集、测试集 - train_data, vad_data = data_set.split(0.5) - dev_data, test_data = vad_data.split(0.4) - len(train_data), len(dev_data), len(test_data) - - # In[21]: - - # 深拷贝一个数据集 - import copy - train_data_2, dev_data_2 = copy.deepcopy(train_data), copy.deepcopy(dev_data) - del copy - - # 初始化词表,该词表最大的vocab_size为10000,词表中每个词出现的最低频率为2,''表示未知词语,''表示padding词语 - # Vocabulary默认初始化参数为max_size=None, min_freq=None, unknown='', padding='' - vocab = Vocabulary(max_size=10000, min_freq=2, unknown='', padding='') - - # 构建词表 - train_data.apply(lambda x: [vocab.add(word) for word in x['premise']]) - train_data.apply(lambda x: [vocab.add(word) for word in x['hypothesis']]) - vocab.build_vocab() - - # In[23]: - - # 根据词表index句子 - train_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise') - train_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis') - dev_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise') - dev_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis') - test_data.apply(lambda x: [vocab.to_index(word) for word in x['premise']], new_field_name='premise') - test_data.apply(lambda x: [vocab.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis') - train_data[-1], dev_data[-1], test_data[-1] - - # 读入vocab文件 - with open('vocab.txt', encoding='utf-8') as f: - lines = f.readlines() - vocabs = [] - for line in lines: - vocabs.append(line.strip()) - - # 实例化Vocabulary - vocab_bert = Vocabulary(unknown=None, padding=None) - # 将vocabs列表加入Vocabulary - vocab_bert.add_word_lst(vocabs) - # 构建词表 - vocab_bert.build_vocab() - # 更新unknown与padding的token文本 - vocab_bert.unknown = '[UNK]' - vocab_bert.padding = '[PAD]' - - # In[25]: - - # 根据词表index句子 - train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['premise']], new_field_name='premise') - train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['hypothesis']], - new_field_name='hypothesis') - dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['premise']], new_field_name='premise') - dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['hypothesis']], new_field_name='hypothesis') - train_data_2[-1], dev_data_2[-1] - - for data in [train_data, dev_data, test_data]: - data.rename_field('premise', 'words1') - data.rename_field('hypothesis', 'words2') - data.rename_field('premise_len', 'seq_len1') - data.rename_field('hypothesis_len', 'seq_len2') - data.set_input('words1', 'words2', 'seq_len1', 'seq_len2') - - - # step 1:加载模型参数(非必选) - from fastNLP.io.config_io import ConfigSection, ConfigLoader - args = ConfigSection() - ConfigLoader().load_config("./data/config", {"esim_model": args}) - args["vocab_size"] = len(vocab) - args.data - - # In[27]: - - # step 2:加载ESIM模型 - from fastNLP.models import ESIM - model = ESIM(**args.data) - model - - # In[28]: - - # 另一个例子:加载CNN文本分类模型 - from fastNLP.models import CNNText - cnn_text_model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1) - - from fastNLP import CrossEntropyLoss - from fastNLP import Adam - from fastNLP import AccuracyMetric - trainer = Trainer( - train_data=train_data, - model=model, - loss=CrossEntropyLoss(pred='pred', target='label'), - metrics=AccuracyMetric(target='label'), - n_epochs=3, - batch_size=16, - print_every=-1, - validate_every=-1, - dev_data=dev_data, - optimizer=Adam(lr=1e-3, weight_decay=0), - check_code_level=-1, - metric_key='acc', - use_tqdm=False, - ) - trainer.train() - - tester = Tester( - data=test_data, - model=model, - metrics=AccuracyMetric(target='label'), - batch_size=args["batch_size"], - ) - tester.test() - def setUp(self): import os self._init_wd = os.path.abspath(os.curdir) diff --git a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb b/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb deleted file mode 100644 index 7e487933..00000000 --- a/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb +++ /dev/null @@ -1,1117 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# fastNLP开发进阶教程\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 组织数据部分\n", - "## DataSet & Instance\n", - "fastNLP用DataSet和Instance保存和处理数据。每个DataSet表示一个数据集,每个Instance表示一个数据样本。一个DataSet存有多个Instance,每个Instance可以自定义存哪些内容。" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "# 声明部件\n", - "import torch\n", - "import fastNLP\n", - "from fastNLP import DataSet\n", - "from fastNLP import Instance\n", - "from fastNLP import Vocabulary\n", - "from fastNLP import Trainer\n", - "from fastNLP import Tester" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Instance\n", - "Instance表示一个样本,由一个或者多个field(域、属性、特征)组成,每个field具有自己的名字以及值\n", - "在初始化Instance的时候可以定义它包含的field,使用\"field_name=field_value\"的写法" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'premise': an premise example . type=str,\n", - "'hypothesis': an hypothesis example. type=str,\n", - "'label': 1 type=int}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 组织一个Instance,这个Instance由premise、hypothesis、label三个field组成\n", - "instance = Instance(premise='an premise example .', hypothesis='an hypothesis example.', label=1)\n", - "instance" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'premise': an premise example . type=str,\n", - "'hypothesis': an hypothesis example. type=str,\n", - "'label': 1 type=int},\n", - "{'premise': an premise example . type=str,\n", - "'hypothesis': an hypothesis example. type=str,\n", - "'label': 1 type=int})" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data_set = DataSet([instance] * 5)\n", - "data_set.append(instance)\n", - "data_set[-2: ]" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'premise': an premise example . type=str,\n", - "'hypothesis': an hypothesis example. type=str,\n", - "'label': 1 type=int},\n", - "{'premise': the second premise example . type=str,\n", - "'hypothesis': the second hypothesis example. type=str,\n", - "'label': 1 type=str})" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 如果某一个field的类型与dataset对应的field类型不一样仍可被加入dataset中\n", - "instance2 = Instance(premise='the second premise example .', hypothesis='the second hypothesis example.', label='1')\n", - "try:\n", - " data_set.append(instance2)\n", - "except:\n", - " pass\n", - "data_set[-2: ]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "cannot append instance\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet({'premise': an premise example . type=str,\n", - "'hypothesis': an hypothesis example. type=str,\n", - "'label': 1 type=int},\n", - "{'premise': the second premise example . type=str,\n", - "'hypothesis': the second hypothesis example. type=str,\n", - "'label': 1 type=str})" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 如果某一个field的名字不对,则该instance不能被append到dataset中\n", - "instance3 = Instance(premises='the third premise example .', hypothesis='the third hypothesis example.', label=1)\n", - "try:\n", - " data_set.append(instance3)\n", - "except:\n", - " print('cannot append instance')\n", - " pass\n", - "data_set[-2: ]" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'image': tensor([[ 0.3582, -1.0358, 1.4785, -1.5288, -0.9982],\n", - " [-0.3973, -0.4294, 0.9215, -1.9631, -1.6556],\n", - " [ 0.3313, -1.7714, 0.8729, 0.6976, -1.3172],\n", - " [-0.6403, 0.5023, -0.9919, 1.1178, -0.3710],\n", - " [-0.3692, 1.8631, -1.3646, -0.7290, -1.0774]]) type=torch.Tensor,\n", - "'label': 0 type=int})" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 除了文本以外,还可以将tensor作为其中一个field的value\n", - "import torch\n", - "tensor_ins = Instance(image=torch.randn(5, 5), label=0)\n", - "ds = DataSet()\n", - "ds.append(tensor_ins)\n", - "ds" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### DataSet\n", - "### 使用现有代码读取并组织DataSet\n", - "在DataSet类当中有一些read_* 方法,可以从文件中读取数据并组织DataSet" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "77" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import fastNLP\n", - "from fastNLP import DataSet\n", - "from fastNLP import Instance\n", - "\n", - "# 从csv读取数据到DataSet\n", - "# 类csv文件,即每一行为一个example的文件,都可以使用这种方法进行数据读取\n", - "dataset = DataSet.read_csv('tutorial_sample_dataset.csv', headers=('raw_sentence', 'label'), sep='\\t')\n", - "# 查看DataSet的大小\n", - "len(dataset)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=str}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 使用数字索引[k],获取第k个样本\n", - "dataset[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "fastNLP.core.instance.Instance" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 获取的样本是一个Instance\n", - "type(dataset[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=str},\n", - "{'raw_sentence': This quiet , introspective and entertaining independent is worth seeking . type=str,\n", - "'label': 4 type=str},\n", - "{'raw_sentence': Even fans of Ismail Merchant 's work , I suspect , would have a hard time sitting through this one . type=str,\n", - "'label': 1 type=str})" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 使用数字索引[a: b],获取第a到第b个样本\n", - "dataset[0: 3]" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_sentence': A film that clearly means to preach exclusively to the converted . type=str,\n", - "'label': 2 type=str}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 索引也可以是负数\n", - "dataset[-1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 自行读取并组织DataSet\n", - "以SNLI数据集为例,\n", - "SNLI数据集的训练、验证、测试集分别三个文件组成:第一个文件每一行是一句话,代表一个example当中的premise;第二个文件每一行也是一句话,代表一个example当中的hypothesis;第三个文件每一行是一个label" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'premise': A person on a horse jumps over a broken down airplane . type=str,\n", - "'hypothesis': A person is training his horse for a competition . type=str,\n", - "'truth': 1\n", - " type=str}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data_path = ['premise', 'hypothesis', 'label']\n", - "\n", - "# 读入文件\n", - "with open(data_path[0]) as f:\n", - " premise = f.readlines()\n", - "\n", - "with open(data_path[1]) as f:\n", - " hypothesis = f.readlines()\n", - "\n", - "with open(data_path[2]) as f:\n", - " label = f.readlines()\n", - "\n", - "assert len(premise) == len(hypothesis) and len(hypothesis) == len(label)\n", - "\n", - "# 组织DataSet\n", - "data_set = DataSet()\n", - "for p, h, l in zip(premise, hypothesis, label):\n", - " p = p.strip() # 将行末空格去除\n", - " h = h.strip() # 将行末空格去除\n", - " data_set.append(Instance(premise=p, hypothesis=h, truth=l))\n", - "\n", - "data_set[0]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### DataSet的其他操作\n", - "在构建完毕DataSet后,仍然可以对DataSet的内容进行操作,函数接口为DataSet.apply()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'premise': a woman is walking across the street eating a banana , while a man is following with his briefcase . type=str,\n", - "'hypothesis': An actress and her favorite assistant talk a walk in the city . type=str,\n", - "'truth': 1\n", - " type=str},\n", - "{'premise': a woman is walking across the street eating a banana , while a man is following with his briefcase . type=str,\n", - "'hypothesis': a woman eating a banana crosses a street type=str,\n", - "'truth': 0\n", - " type=str})" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 将premise域的所有文本转成小写\n", - "data_set.apply(lambda x: x['premise'].lower(), new_field_name='premise')\n", - "data_set[-2: ]" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'premise': a woman is walking across the street eating a banana , while a man is following with his briefcase . type=str,\n", - "'hypothesis': An actress and her favorite assistant talk a walk in the city . type=str,\n", - "'truth': 1 type=int},\n", - "{'premise': a woman is walking across the street eating a banana , while a man is following with his briefcase . type=str,\n", - "'hypothesis': a woman eating a banana crosses a street type=str,\n", - "'truth': 0 type=int})" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# label转int\n", - "data_set.apply(lambda x: int(x['truth']), new_field_name='truth')\n", - "data_set[-2: ]" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'premise': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", - "'hypothesis': ['An', 'actress', 'and', 'her', 'favorite', 'assistant', 'talk', 'a', 'walk', 'in', 'the', 'city', '.'] type=list,\n", - "'truth': 1 type=int},\n", - "{'premise': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", - "'hypothesis': ['a', 'woman', 'eating', 'a', 'banana', 'crosses', 'a', 'street'] type=list,\n", - "'truth': 0 type=int})" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 使用空格分割句子\n", - "def split_sent(ins):\n", - " return ins['premise'].split()\n", - "data_set.apply(split_sent, new_field_name='premise')\n", - "data_set.apply(lambda x: x['hypothesis'].split(), new_field_name='hypothesis')\n", - "data_set[-2:]" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(100, 97)" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 筛选数据\n", - "origin_data_set_len = len(data_set)\n", - "data_set.drop(lambda x: len(x['premise']) <= 6)\n", - "origin_data_set_len, len(data_set)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'premise': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", - "'hypothesis': ['a', 'woman', 'eating', 'a', 'banana', 'crosses', 'a', 'street'] type=list,\n", - "'truth': 0 type=int,\n", - "'premise_len': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - "'hypothesis_len': [1, 1, 1, 1, 1, 1, 1, 1] type=list}" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 增加长度信息\n", - "data_set.apply(lambda x: [1] * len(x['premise']), new_field_name='premise_len')\n", - "data_set.apply(lambda x: [1] * len(x['hypothesis']), new_field_name='hypothesis_len')\n", - "data_set[-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "# 设定特征域、标签域\n", - "data_set.rename_field(\"premise\",\"words1\")\n", - "data_set.rename_field(\"premise_len\",\"seq_len1\")\n", - "data_set.rename_field(\"hypothesis\",\"words2\")\n", - "data_set.rename_field(\"hypothesis_len\",\"seq_len2\")\n", - "data_set.set_input(\"words1\", \"seq_len1\", \"words2\", \"seq_len2\")\n", - "data_set.set_target(\"truth\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'words1': ['a', 'woman', 'is', 'walking', 'across', 'the', 'street', 'eating', 'a', 'banana', ',', 'while', 'a', 'man', 'is', 'following', 'with', 'his', 'briefcase', '.'] type=list,\n", - "'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - "'words2': ['a', 'woman', 'eating', 'a', 'banana', 'crosses', 'a', 'street'] type=list,\n", - "'seq_len2': [1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - "'label': 0 type=int}" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 重命名field\n", - "data_set.rename_field('truth', 'label')\n", - "data_set[-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(49, 29, 19)" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 切分训练、验证集、测试集\n", - "train_data, vad_data = data_set.split(0.5)\n", - "dev_data, test_data = vad_data.split(0.4)\n", - "len(train_data), len(dev_data), len(test_data)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "# 深拷贝一个数据集\n", - "import copy\n", - "train_data_2, dev_data_2 = copy.deepcopy(train_data), copy.deepcopy(dev_data)\n", - "del copy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### DataSet的总结:\n", - "将DataSet的ield设置为input和target后,这些field在接下来的代码中将被使用。其中被设置为input的field会被传递给Model.forward,这个过程是通过键匹配的方式进行的。举例如下: \n", - "假设DataSet中有'x1', 'x2', 'x3'被设置为了input,而 \n", - "   (1)函数是Model.forward(self, x1, x3), 那么DataSet中'x1', 'x3'会被传递给forward函数。多余的'x2'会被忽略 \n", - "   (2)函数是Model.forward(self, x1, x4), 这里多需要了一个'x4', 但是DataSet的input field中没有这个field,会报错。 \n", - "   (3)函数是Model.forward(self, x1, kwargs), 会把'x1', 'x2', 'x3'都传入。但如果是Model.forward(self, x4, kwargs)就会发生报错,因为没有'x4'。 \n", - "   (4)对于设置为target的field的名称,我们建议取名为'target'(如果只有一个需要predict的值),但是不强制。如果这个名称不是target,那么在加载loss函数的时候需要手动添加名称转换map" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Vocabulary\n", - "fastNLP中的Vocabulary轻松构建词表,并将词转成数字。构建词表有两种方式:根据数据集构建词表;载入现有词表\n", - "### 根据数据集构建词表" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "# 初始化词表,该词表最大的vocab_size为10000,词表中每个词出现的最低频率为2,''表示未知词语,''表示padding词语\n", - "# Vocabulary默认初始化参数为max_size=None, min_freq=None, unknown='', padding=''\n", - "vocab = Vocabulary(max_size=10000, min_freq=2, unknown='', padding='')\n", - "\n", - "# 构建词表\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['words1']])\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['words2']])\n", - "vocab.build_vocab()" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "({'words1': [2, 9, 4, 2, 75, 85, 7, 86, 76, 77, 87, 88, 89, 2, 90, 3] type=list,\n", - " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'words2': [18, 9, 10, 1, 3] type=list,\n", - " 'seq_len2': [1, 1, 1, 1, 1] type=list,\n", - " 'label': 1 type=int},\n", - " {'words1': [22, 32, 5, 110, 81, 111, 112, 5, 82, 3] type=list,\n", - " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'words2': [64, 32, 82, 133, 84, 3] type=list,\n", - " 'seq_len2': [1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 0 type=int},\n", - " {'words1': [2, 9, 97, 1, 20, 7, 54, 5, 1, 1, 70, 2, 11, 110, 2, 62, 3] type=list,\n", - " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'words2': [23, 1, 58, 10, 12, 1, 70, 133, 84, 3] type=list,\n", - " 'seq_len2': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 1 type=int})" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 根据词表index句子\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", - "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", - "dev_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", - "test_data.apply(lambda x: [vocab.to_index(word) for word in x['words1']], new_field_name='words1')\n", - "test_data.apply(lambda x: [vocab.to_index(word) for word in x['words2']], new_field_name='words2')\n", - "train_data[-1], dev_data[-1], test_data[-1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 载入现有词表\n", - "以BERT pretrained model为例,词表由一个vocab.txt文件来保存\n", - "用以下方法可以载入现有词表,并保证词表顺序不变" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "# 读入vocab文件\n", - "with open('vocab.txt') as f:\n", - " lines = f.readlines()\n", - "vocabs = []\n", - "for line in lines:\n", - " vocabs.append(line.strip())\n", - "\n", - "# 实例化Vocabulary\n", - "vocab_bert = Vocabulary(unknown=None, padding=None)\n", - "# 将vocabs列表加入Vocabulary\n", - "vocab_bert.add_word_lst(vocabs)\n", - "# 构建词表\n", - "vocab_bert.build_vocab()\n", - "# 更新unknown与padding的token文本\n", - "vocab_bert.unknown = '[UNK]'\n", - "vocab_bert.padding = '[PAD]'" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "({'words1': [1037, 2450, 1999, 1037, 2665, 6598, 1998, 7415, 2058, 2014, 2132, 2559, 2875, 1037, 3028, 1012] type=list,\n", - " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'words2': [100, 2450, 2003, 3147, 1012] type=list,\n", - " 'seq_len2': [1, 1, 1, 1, 1] type=list,\n", - " 'label': 1 type=int},\n", - " {'words1': [2048, 2308, 1010, 3173, 2833, 100, 16143, 1010, 8549, 1012] type=list,\n", - " 'seq_len1': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] type=list,\n", - " 'words2': [100, 2308, 8549, 2169, 2060, 1012] type=list,\n", - " 'seq_len2': [1, 1, 1, 1, 1, 1] type=list,\n", - " 'label': 0 type=int})" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 根据词表index句子\n", - "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words1']], new_field_name='words1')\n", - "train_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words2']], new_field_name='words2')\n", - "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words1']], new_field_name='words1')\n", - "dev_data_2.apply(lambda x: [vocab_bert.to_index(word) for word in x['words2']], new_field_name='words2')\n", - "train_data_2[-1], dev_data_2[-1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 模型部分\n", - "## Model\n", - "模型部分fastNLP提供两种使用方式:调用fastNLP现有模型;开发者自行搭建模型\n", - "### 调用fastNLP现有模型" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'embed_dim': 300,\n", - " 'hidden_size': 300,\n", - " 'batch_first': True,\n", - " 'dropout': 0.3,\n", - " 'num_classes': 3,\n", - " 'gpu': True,\n", - " 'batch_size': 32,\n", - " 'vocab_size': 143}" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# step 1:加载模型参数(非必选)\n", - "from fastNLP.io.config_io import ConfigSection, ConfigLoader\n", - "args = ConfigSection()\n", - "ConfigLoader().load_config(\"./data/config\", {\"esim_model\": args})\n", - "args[\"vocab_size\"] = len(vocab)\n", - "args.data" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ESIM(\n", - " (drop): Dropout(p=0.3)\n", - " (embedding): Embedding(\n", - " 143, 300\n", - " (dropout): Dropout(p=0.3)\n", - " )\n", - " (embedding_layer): Linear(in_features=300, out_features=300, bias=True)\n", - " (encoder): LSTM(\n", - " (lstm): LSTM(300, 300, batch_first=True, bidirectional=True)\n", - " )\n", - " (bi_attention): BiAttention()\n", - " (mean_pooling): MeanPoolWithMask()\n", - " (max_pooling): MaxPoolWithMask()\n", - " (inference_layer): Linear(in_features=1200, out_features=300, bias=True)\n", - " (decoder): LSTM(\n", - " (lstm): LSTM(300, 300, batch_first=True, bidirectional=True)\n", - " )\n", - " (output): MLP(\n", - " (hiddens): ModuleList(\n", - " (0): Linear(in_features=1200, out_features=300, bias=True)\n", - " )\n", - " (output): Linear(in_features=300, out_features=3, bias=True)\n", - " (dropout): Dropout(p=0.3)\n", - " )\n", - ")" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# step 2:加载ESIM模型\n", - "from fastNLP.models import ESIM\n", - "model = ESIM(args[\"vocab_size\"], args[\"embed_dim\"], args[\"hidden_size\"], args[\"dropout\"], args[\"num_classes\"])\n", - "model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "这是上述模型的forward方法。如果你不知道什么是forward方法,请参考我们的PyTorch教程。\n", - "\n", - "注意两点:\n", - "1. forward参数名字叫**word_seq**,请记住。\n", - "2. forward的返回值是一个**dict**,其中有个key的名字叫**pred**。\n", - "\n", - "```Python\n", - " def forward(self, word_seq):\n", - " \"\"\"\n", - "\n", - " :param word_seq: torch.LongTensor, [batch_size, seq_len]\n", - " :return output: dict of torch.LongTensor, [batch_size, num_classes]\n", - " \"\"\"\n", - " x = self.embed(word_seq) # [N,L] -> [N,L,C]\n", - " x = self.conv_pool(x) # [N,L,C] -> [N,C]\n", - " x = self.dropout(x)\n", - " x = self.fc(x) # [N,C] -> [N, N_class]\n", - " return {'pred': x}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 自行搭载模型\n", - "自行搭载的模型必须是nn.Module的子类, \n", - "(1)必须实现forward方法,并且forward方法不能出现**\\*arg**这种参数,例如\n", - "```Python\n", - " def forword(self, word_seq, *args): # 这是不允许的\n", - " xxx\n", - "```\n", - "forward函数的返回值必须是一个**dict**。 \n", - "dict当中模型预测的值所对应的key建议用**'pred'**,这里不做强制限制,但是如果不是pred的话,在加载loss函数的时候需要手动添加名称转换map \n", - "(2)如果实现了predict方法,在做evaluation的时候将调用predict方法而不是forward。如果没有predict方法,则在evaluation时调用forward方法。predict方法也不能使用\\*args这种参数形式,同时结果也必须返回一个dict,同样推荐key为'pred'。 \n", - "(3)forward函数可以计算loss并返回结果,在dict中的key为'loss',如: \n", - "```Python\n", - " def forword(self, word_seq, *args): \n", - " xxx\n", - " return {'pred': pred, 'loss': loss}\n", - "```\n", - "当loss函数没有在trainer里被定义的时候,trainer将会根据forward函数返回的dict中key为'loss'的值来进行反向传播,具体见loss部分" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 训练测试部分\n", - "## Loss\n", - "### 键映射\n", - "根据上文DataSet与Model部分可以知道,fastNLP并不限制Model.forward()的返回值,也不限制DataSet中target field的key。因此在计算loss的时候,需要通过键映射的方式来完成取值。 \n", - "这里以CrossEntropyLoss为例,我们的交叉熵函数部分如下:\n", - "```Python\n", - " def get_loss(self, pred, target):\n", - " return F.cross_entropy(input=pred, target=target,\n", - " ignore_index=self.padding_idx)\n", - "```\n", - "这里接收的两个参数名字分别为pred和target,其中pred是从model的forward函数返回值中取得,target是从DataSet的is_target的field当中取得。在没有设置键映射的基础上,pred从model的forward函数返回的dict中取'pred'键得到;target从DataSet的'target'field中得到。\n", - "。" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 修改键映射\n", - "在初始化CrossEntropyLoss的时候,可以传入两个参数(pred=None, target=None), 这两个参数接受的类型是str,假设(pred='output', target='label'),那么CrossEntropyLoss会使用'output'这个key在forward的output与batch_y中寻找值;'label'也是在forward的output与d ataset的is target field中寻找值。注意这里pred或target的来源并不一定非要来自于model.forward与dataset的is target field,也可以只来自于forward的结果" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 创建一个自己的loss\n", - "    (1)采用fastNLP.LossInForward,在model.forward()的结果中包含一个'loss'的key,具体见**自行搭载模型**部分 \n", - "    (2)自己定义一个继承于fastNLP.core.loss.LossBase的class。重写get_loss方法。 \n", - "      (2.1)在初始化自己的loss class的时候,需要初始化需要映射的值 \n", - "      (2.2)在get_loss函数中,参数的名字需要与初始化时的映射是一致的 \n", - "以L1Loss为例子:\n", - "```Python\n", - "class L1Loss(LossBase):\n", - " def __init__(self, pred=None, target=None):\n", - " super(L1Loss, self).__init__()\n", - " \"\"\"\n", - " 这里传入_init_param_map以使得pred和target被正确注册,但这一步不是必须的, 建议调用。传入_init_param_map的是用于\n", - " \"键映射\"的键值对。假设初始化__init__(pred=None, target=None, threshold=0.1)中threshold是用于控制loss计算的,\n", - " 则\\不要将threshold传入_init_param_map.\n", - " \"\"\"\n", - " self._init_param_map(pred=pred, target=target)\n", - "\n", - " def get_loss(self, pred, target):\n", - " # 这里'pred', 'target'必须和初始化的映射是一致的。\n", - " return F.l1_loss(input=pred, target=target)\n", - "```\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Trainer\n", - "trainer的作用是训练模型,是一个对训练过程的封装。trainer当中比较重要的函数是trainer.train(),train函数的主要步骤包括: \n", - "(1)创建batch \n", - "```Python\n", - "batch = Batch(dataset, batch_size, sampler=sampler)\n", - "```\n", - "(2)for batch_x, batch_y in batch: (batch_x, batch_y的内容分别为dataset中is input和is target的部分,这两个dict的key就是DataSet中的key,value会根据情况做好padding及tensor) \n", - "  (2.1)将batch_x, batch_y中的tensor移动到model所在的device \n", - "  (2.2)根据model.forward的参数列表,从batch_x中取出需要传递给forward的数据 \n", - "  (2.3)获取model.forward返回的dict,并与batch_y一起传递给loss函数,求得loss \n", - "  (2.4)对loss进行反向梯度传播并更新参数 \n", - "(3)如果有验证集,则进行验证 \n", - "(4)如果验证集的结果是当前最佳结果,则保存模型" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "除了以上的内容, Trainer中还提供了\"预跑\"的功能。该功能通过check_code_level管理,如果check_code_level为-1,则不进行\"预跑\"。 check_code_level=0,1,2代表不同的提醒级别。目前不同提醒级别对应的是对DataSet中设置为input或target但又没有使用的field的提醒级别。 0是忽略(默认);1是会warning发生了未使用field的情况;2是出现了unused会直接报错并退出运行 \"预跑\"的主要目的有两个: \n", - "(1)防止train完了之后进行evaluation的时候出现错误。之前的train就白费了 \n", - "(2)由于存在\"键映射\",直接运行导致的报错可能不太容易debug,通过\"预跑\"过程的报错会有一些debug提示 \"预跑\"会进行以下的操作: \n", - "  (i) 使用很小的batch_size, 检查batch_x中是否包含Model.forward所需要的参数。只会运行两个循环。 \n", - "  (ii)将Model.foward的输出pred_dict与batch_y输入到loss中,并尝试backward。不会更新参数,而且grad会被清零。如果传入了dev_data,还将进行metric的测试 \n", - "  (iii)创建Tester,并传入少量数据,检测是否可以正常运行 \n", - "\"预跑\"操作是在Trainer初始化的时候执行的。正常情况下,应该不需要改动\"预跑\"的代码。但如果遇到bug或者有什么好的建议,欢迎在开发群或者github提交issue。" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "training epochs started 2019-05-14-19-49-25\n" - ] - }, - { - "ename": "AssertionError", - "evalue": "seq_len can only have one dimension, got False.", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0muse_tqdm\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 18\u001b[0m )\n\u001b[0;32m---> 19\u001b[0;31m \u001b[0mtrainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36mtrain\u001b[0;34m(self, load_best_model)\u001b[0m\n\u001b[1;32m 522\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 523\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_train_begin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 524\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_train\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 525\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_train_end\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 526\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mCallbackException\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mKeyboardInterrupt\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_train\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 573\u001b[0m \u001b[0;31m# negative sampling; replace unknown; re-weight batch_y\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 574\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcallback_manager\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mon_batch_begin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbatch_y\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mindices\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 575\u001b[0;31m \u001b[0mprediction\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_data_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 576\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 577\u001b[0m \u001b[0;31m# edit prediction\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_data_forward\u001b[0;34m(self, network, x)\u001b[0m\n\u001b[1;32m 661\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_data_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mnetwork\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 662\u001b[0m \u001b[0mx\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnetwork\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 663\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mnetwork\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 664\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0my\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 665\u001b[0m raise TypeError(\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/torch/nn/modules/module.py\u001b[0m in \u001b[0;36m__call__\u001b[0;34m(self, *input, **kwargs)\u001b[0m\n\u001b[1;32m 489\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_slow_forward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 490\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 491\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 492\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mhook\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_forward_hooks\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 493\u001b[0m \u001b[0mhook_result\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mhook\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minput\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/models/snli.py\u001b[0m in \u001b[0;36mforward\u001b[0;34m(self, words1, words2, seq_len1, seq_len2, target)\u001b[0m\n\u001b[1;32m 76\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 77\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mseq_len1\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 78\u001b[0;31m \u001b[0mseq_len1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len_to_mask\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseq_len1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 79\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 80\u001b[0m \u001b[0mseq_len1\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mones\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpremise0\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpremise0\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/utils.py\u001b[0m in \u001b[0;36mseq_len_to_mask\u001b[0;34m(seq_len)\u001b[0m\n\u001b[1;32m 626\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 627\u001b[0m \u001b[0;32melif\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseq_len\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtorch\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mTensor\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 628\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdim\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34mf\"seq_len can only have one dimension, got {seq_len.dim() == 1}.\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 629\u001b[0m \u001b[0mbatch_size\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 630\u001b[0m \u001b[0mmax_len\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mseq_len\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mlong\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mAssertionError\u001b[0m: seq_len can only have one dimension, got False." - ] - } - ], - "source": [ - "from fastNLP import CrossEntropyLoss\n", - "from fastNLP import Adam\n", - "from fastNLP import AccuracyMetric\n", - "trainer = Trainer(\n", - " train_data=train_data,\n", - " model=model,\n", - " loss=CrossEntropyLoss(pred='pred', target='label'), # 模型预测值通过'pred'来取得,目标值(ground truth)由'label'取得\n", - " metrics=AccuracyMetric(target='label'), # 目标值(ground truth)由'label'取得\n", - " n_epochs=5,\n", - " batch_size=16,\n", - " print_every=-1,\n", - " validate_every=-1,\n", - " dev_data=dev_data,\n", - " optimizer=Adam(lr=1e-3, weight_decay=0),\n", - " check_code_level=-1,\n", - " metric_key='acc',\n", - " use_tqdm=False,\n", - ")\n", - "trainer.train()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tester\n", - "tester的作用是训练模型,是一个对训练过程的封装。tester当中比较重要的函数是tester.test(),test函数的主要步骤包括:\n", - "(1)创建batch\n", - "```Python\n", - "batch = Batch(dataset, batch_size, sampler=sampler)\n", - "```\n", - "(2)for batch_x, batch_y in batch: (batch_x, batch_y的内容分别为dataset中is input和is target的部分,这两个dict的key就是DataSet中的key,value会根据情况做好padding及tensor) \n", - "  (2.1)同步数据与model将batch_x, batch_y中的tensor移动到model所在的device \n", - "  (2.2)根据predict_func的参数列表,从batch_x中取出需要传递给predict_func的数据,得到结果pred_dict \n", - "  (2.3)调用metric(pred_dict, batch_y) \n", - "  (2.4)当所有batch都运行完毕,会调用metric的get_metric方法,并且以返回的值作为evaluation的结果 " - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[tester] \n", - "AccuracyMetric: acc=0.368421\n" - ] - }, - { - "data": { - "text/plain": [ - "{'AccuracyMetric': {'acc': 0.368421}}" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "tester = Tester(\n", - " data=test_data,\n", - " model=model,\n", - " metrics=AccuracyMetric(target='label'),\n", - " batch_size=args[\"batch_size\"],\n", - ")\n", - "tester.test()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/fastnlp_advanced_tutorial/data/config b/tutorials/fastnlp_advanced_tutorial/data/config deleted file mode 100644 index 87348b72..00000000 --- a/tutorials/fastnlp_advanced_tutorial/data/config +++ /dev/null @@ -1,8 +0,0 @@ -[esim_model] -embed_dim = 300 -hidden_size = 300 -batch_first = true -dropout = 0.3 -num_classes = 3 -gpu = true -batch_size = 32 diff --git a/tutorials/fastnlp_advanced_tutorial/hypothesis b/tutorials/fastnlp_advanced_tutorial/hypothesis deleted file mode 100644 index 07aec3c9..00000000 --- a/tutorials/fastnlp_advanced_tutorial/hypothesis +++ /dev/null @@ -1,100 +0,0 @@ -A person is training his horse for a competition . -A person is at a diner , ordering an omelette . -A person is outdoors , on a horse . -They are smiling at their parents -There are children present -The kids are frowning -The boy skates down the sidewalk . -The boy does a skateboarding trick . -The boy is wearing safety equipment . -An older man drinks his juice as he waits for his daughter to get off work . -A boy flips a burger . -An elderly man sits in a small shop . -Some women are hugging on vacation . -The women are sleeping . -There are women showing affection . -The people are eating omelettes . -The people are sitting at desks in school . -The diners are at a restaurant . -A man is drinking juice . -Two women are at a restaurant drinking wine . -A man in a restaurant is waiting for his meal to arrive . -A blond man getting a drink of water from a fountain in the park . -A blond man wearing a brown shirt is reading a book on a bench in the park -A blond man drinking water from a fountain . -The friends scowl at each other over a full dinner table . -There are two woman in this picture . -The friends have just met for the first time in 20 years , and have had a great time catching up . -The two sisters saw each other across the crowded diner and shared a hug , both clutching their doggie bags . -Two groups of rival gang members flipped each other off . -Two women hug each other . -A team is trying to score the games winning out . -A team is trying to tag a runner out . -A team is playing baseball on Saturn . -A school hosts a basketball game . -A high school is hosting an event . -A school is hosting an event . -The women do not care what clothes they wear . -Women are waiting by a tram . -The women enjoy having a good fashion sense . -A child with mom and dad , on summer vacation at the beach . -A family of three is at the beach . -A family of three is at the mall shopping . -The people waiting on the train are sitting . -There are people just getting on a train -There are people waiting on a train . -A couple are playing with a young child outside . -A couple are playing frisbee with a young child at the beach . -A couple watch a little girl play by herself on the beach . -The family is sitting down for dinner . -The family is outside . -The family is on vacation . -The people are standing still on the curb . -Near a couple of restaurants , two people walk across the street . -The couple are walking across the street together . -The woman is nake . -The woman is cold . -The woman is wearing green . -The man with the sign is caucasian . -They are protesting outside the capital . -A woman in white . -A man is advertising for a restaurant . -The woman is wearing black . -A man and a woman walk down a crowded city street . -The woman is wearing white . -They are working for John 's Pizza . -Olympic swimming . -A man and a soman are eating together at John 's Pizza and Gyro . -They are walking with a sign . -The woman is waiting for a friend . -The man is sitting down while he has a sign for John 's Pizza and Gyro in his arms . -The woman and man are outdoors . -A woman ordering pizza . -The people are related . -Two adults run across the street to get away from a red shirted person chasing them . -The adults are both male and female . -Two people walk home after a tasty steak dinner . -Two adults swimming in water -Two adults walk across a street . -Two people ride bicycles into a tunnel . -Two people walk away from a restaurant across a street . -Two adults walking across a road near the convicted prisoner dressed in red -Two friends cross a street . -Some people board a train . -Two adults walk across the street . -Two adults walking across a road -There are no women in the picture . -Two adults walk across the street to get away from a red shirted person who is chasing them . -A married couple is sleeping . -A female is next to a man . -A married couple is walking next to each other . -Nobody has food . -A woman eats a banana and walks across a street , and there is a man trailing behind her . -The woman and man are playing baseball together . -two coworkers cross pathes on a street -A woman eats ice cream walking down the sidewalk , and there is another woman in front of her with a purse . -The mans briefcase is for work . -A person eating . -A person that is hungry . -An actress and her favorite assistant talk a walk in the city . -a woman eating a banana crosses a street diff --git a/tutorials/fastnlp_advanced_tutorial/label b/tutorials/fastnlp_advanced_tutorial/label deleted file mode 100644 index e28836df..00000000 --- a/tutorials/fastnlp_advanced_tutorial/label +++ /dev/null @@ -1,100 +0,0 @@ -1 -2 -0 -1 -0 -2 -2 -0 -1 -1 -2 -1 -1 -2 -0 -1 -2 -0 -0 -2 -1 -1 -2 -0 -2 -0 -1 -1 -2 -0 -1 -0 -2 -2 -1 -0 -2 -0 -1 -1 -0 -2 -1 -0 -0 -0 -1 -2 -2 -0 -1 -2 -0 -1 -2 -1 -0 -1 -2 -0 -0 -2 -1 -0 -1 -2 -2 -0 -1 -2 -0 -1 -1 -2 -0 -1 -2 -0 -2 -0 -1 -1 -2 -0 -0 -2 -1 -2 -0 -1 -2 -0 -2 -1 -2 -1 -0 -1 -1 -0 diff --git a/tutorials/fastnlp_advanced_tutorial/premise b/tutorials/fastnlp_advanced_tutorial/premise deleted file mode 100644 index 0c9af30e..00000000 --- a/tutorials/fastnlp_advanced_tutorial/premise +++ /dev/null @@ -1,100 +0,0 @@ -A person on a horse jumps over a broken down airplane . -A person on a horse jumps over a broken down airplane . -A person on a horse jumps over a broken down airplane . -Children smiling and waving at camera -Children smiling and waving at camera -Children smiling and waving at camera -A boy is jumping on skateboard in the middle of a red bridge . -A boy is jumping on skateboard in the middle of a red bridge . -A boy is jumping on skateboard in the middle of a red bridge . -An older man sits with his orange juice at a small table in a coffee shop while employees in bright colored shirts smile in the background . -An older man sits with his orange juice at a small table in a coffee shop while employees in bright colored shirts smile in the background . -An older man sits with his orange juice at a small table in a coffee shop while employees in bright colored shirts smile in the background . -Two blond women are hugging one another . -Two blond women are hugging one another . -Two blond women are hugging one another . -A few people in a restaurant setting , one of them is drinking orange juice . -A few people in a restaurant setting , one of them is drinking orange juice . -A few people in a restaurant setting , one of them is drinking orange juice . -An older man is drinking orange juice at a restaurant . -An older man is drinking orange juice at a restaurant . -An older man is drinking orange juice at a restaurant . -A man with blond-hair , and a brown shirt drinking out of a public water fountain . -A man with blond-hair , and a brown shirt drinking out of a public water fountain . -A man with blond-hair , and a brown shirt drinking out of a public water fountain . -Two women who just had lunch hugging and saying goodbye . -Two women who just had lunch hugging and saying goodbye . -Two women who just had lunch hugging and saying goodbye . -Two women , holding food carryout containers , hug . -Two women , holding food carryout containers , hug . -Two women , holding food carryout containers , hug . -A Little League team tries to catch a runner sliding into a base in an afternoon game . -A Little League team tries to catch a runner sliding into a base in an afternoon game . -A Little League team tries to catch a runner sliding into a base in an afternoon game . -The school is having a special event in order to show the american culture on how other cultures are dealt with in parties . -The school is having a special event in order to show the american culture on how other cultures are dealt with in parties . -The school is having a special event in order to show the american culture on how other cultures are dealt with in parties . -High fashion ladies wait outside a tram beside a crowd of people in the city . -High fashion ladies wait outside a tram beside a crowd of people in the city . -High fashion ladies wait outside a tram beside a crowd of people in the city . -A man , woman , and child enjoying themselves on a beach . -A man , woman , and child enjoying themselves on a beach . -A man , woman , and child enjoying themselves on a beach . -People waiting to get on a train or just getting off . -People waiting to get on a train or just getting off . -People waiting to get on a train or just getting off . -A couple playing with a little boy on the beach . -A couple playing with a little boy on the beach . -A couple playing with a little boy on the beach . -A couple play in the tide with their young son . -A couple play in the tide with their young son . -A couple play in the tide with their young son . -A man and a woman cross the street in front of a pizza and gyro restaurant . -A man and a woman cross the street in front of a pizza and gyro restaurant . -A man and a woman cross the street in front of a pizza and gyro restaurant . -A woman in a green jacket and hood over her head looking towards a valley . -A woman in a green jacket and hood over her head looking towards a valley . -A woman in a green jacket and hood over her head looking towards a valley . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Woman in white in foreground and a man slightly behind walking with a sign for John 's Pizza and Gyro in the background . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -Two adults , one female in white , with shades and one male , gray clothes , walking across a street , away from a eatery with a blurred image of a dark colored red shirted person in the foreground . -A woman wearing all white and eating , walks next to a man holding a briefcase . -A woman wearing all white and eating , walks next to a man holding a briefcase . -A woman wearing all white and eating , walks next to a man holding a briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . -A woman is walking across the street eating a banana , while a man is following with his briefcase . diff --git a/tutorials/fastnlp_advanced_tutorial/tutorial_sample_dataset.csv b/tutorials/fastnlp_advanced_tutorial/tutorial_sample_dataset.csv deleted file mode 100644 index e5c0a74f..00000000 --- a/tutorials/fastnlp_advanced_tutorial/tutorial_sample_dataset.csv +++ /dev/null @@ -1,77 +0,0 @@ -A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . 1 -This quiet , introspective and entertaining independent is worth seeking . 4 -Even fans of Ismail Merchant 's work , I suspect , would have a hard time sitting through this one . 1 -A positively thrilling combination of ethnography and all the intrigue , betrayal , deceit and murder of a Shakespearean tragedy or a juicy soap opera . 3 -Aggressive self-glorification and a manipulative whitewash . 1 -A comedy-drama of nearly epic proportions rooted in a sincere performance by the title character undergoing midlife crisis . 4 -Narratively , Trouble Every Day is a plodding mess . 1 -The Importance of Being Earnest , so thick with wit it plays like a reading from Bartlett 's Familiar Quotations 3 -But it does n't leave you with much . 1 -You could hate it for the same reason . 1 -There 's little to recommend Snow Dogs , unless one considers cliched dialogue and perverse escapism a source of high hilarity . 1 -Kung Pow is Oedekerk 's realization of his childhood dream to be in a martial-arts flick , and proves that sometimes the dreams of youth should remain just that . 1 -The performances are an absolute joy . 4 -Fresnadillo has something serious to say about the ways in which extravagant chance can distort our perspective and throw us off the path of good sense . 3 -I still like Moonlight Mile , better judgment be damned . 3 -A welcome relief from baseball movies that try too hard to be mythic , this one is a sweet and modest and ultimately winning story . 3 -a bilingual charmer , just like the woman who inspired it 3 -Like a less dizzily gorgeous companion to Mr. Wong 's In the Mood for Love -- very much a Hong Kong movie despite its mainland setting . 2 -As inept as big-screen remakes of The Avengers and The Wild Wild West . 1 -It 's everything you 'd expect -- but nothing more . 2 -Best indie of the year , so far . 4 -Hatfield and Hicks make the oddest of couples , and in this sense the movie becomes a study of the gambles of the publishing world , offering a case study that exists apart from all the movie 's political ramifications . 3 -It 's like going to a house party and watching the host defend himself against a frothing ex-girlfriend . 1 -That the Chuck Norris `` grenade gag '' occurs about 7 times during Windtalkers is a good indication of how serious-minded the film is . 2 -The plot is romantic comedy boilerplate from start to finish . 2 -It arrives with an impeccable pedigree , mongrel pep , and almost indecipherable plot complications . 2 -A film that clearly means to preach exclusively to the converted . 2 -While The Importance of Being Earnest offers opportunities for occasional smiles and chuckles , it does n't give us a reason to be in the theater beyond Wilde 's wit and the actors ' performances . 1 -The latest vapid actor 's exercise to appropriate the structure of Arthur Schnitzler 's Reigen . 1 -More vaudeville show than well-constructed narrative , but on those terms it 's inoffensive and actually rather sweet . 2 -Nothing more than a run-of-the-mill action flick . 2 -Hampered -- no , paralyzed -- by a self-indulgent script ... that aims for poetry and ends up sounding like satire . 0 -Ice Age is the first computer-generated feature cartoon to feel like other movies , and that makes for some glacial pacing early on . 2 -There 's very little sense to what 's going on here , but the makers serve up the cliches with considerable dash . 2 -Cattaneo should have followed the runaway success of his first film , The Full Monty , with something different . 2 -They 're the unnamed , easily substitutable forces that serve as whatever terror the heroes of horror movies try to avoid . 1 -It almost feels as if the movie is more interested in entertaining itself than in amusing us . 1 -The movie 's progression into rambling incoherence gives new meaning to the phrase ` fatal script error . ' 0 -I still like Moonlight Mile , better judgment be damned . 3 -A welcome relief from baseball movies that try too hard to be mythic , this one is a sweet and modest and ultimately winning story . 3 -a bilingual charmer , just like the woman who inspired it 3 -Like a less dizzily gorgeous companion to Mr. Wong 's In the Mood for Love -- very much a Hong Kong movie despite its mainland setting . 2 -As inept as big-screen remakes of The Avengers and The Wild Wild West . 1 -It 's everything you 'd expect -- but nothing more . 2 -Best indie of the year , so far . 4 -Hatfield and Hicks make the oddest of couples , and in this sense the movie becomes a study of the gambles of the publishing world , offering a case study that exists apart from all the movie 's political ramifications . 3 -It 's like going to a house party and watching the host defend himself against a frothing ex-girlfriend . 1 -That the Chuck Norris `` grenade gag '' occurs about 7 times during Windtalkers is a good indication of how serious-minded the film is . 2 -The plot is romantic comedy boilerplate from start to finish . 2 -It arrives with an impeccable pedigree , mongrel pep , and almost indecipherable plot complications . 2 -A film that clearly means to preach exclusively to the converted . 2 -I still like Moonlight Mile , better judgment be damned . 3 -A welcome relief from baseball movies that try too hard to be mythic , this one is a sweet and modest and ultimately winning story . 3 -a bilingual charmer , just like the woman who inspired it 3 -Like a less dizzily gorgeous companion to Mr. Wong 's In the Mood for Love -- very much a Hong Kong movie despite its mainland setting . 2 -As inept as big-screen remakes of The Avengers and The Wild Wild West . 1 -It 's everything you 'd expect -- but nothing more . 2 -Best indie of the year , so far . 4 -Hatfield and Hicks make the oddest of couples , and in this sense the movie becomes a study of the gambles of the publishing world , offering a case study that exists apart from all the movie 's political ramifications . 3 -It 's like going to a house party and watching the host defend himself against a frothing ex-girlfriend . 1 -That the Chuck Norris `` grenade gag '' occurs about 7 times during Windtalkers is a good indication of how serious-minded the film is . 2 -The plot is romantic comedy boilerplate from start to finish . 2 -It arrives with an impeccable pedigree , mongrel pep , and almost indecipherable plot complications . 2 -A film that clearly means to preach exclusively to the converted . 2 -I still like Moonlight Mile , better judgment be damned . 3 -A welcome relief from baseball movies that try too hard to be mythic , this one is a sweet and modest and ultimately winning story . 3 -a bilingual charmer , just like the woman who inspired it 3 -Like a less dizzily gorgeous companion to Mr. Wong 's In the Mood for Love -- very much a Hong Kong movie despite its mainland setting . 2 -As inept as big-screen remakes of The Avengers and The Wild Wild West . 1 -It 's everything you 'd expect -- but nothing more . 2 -Best indie of the year , so far . 4 -Hatfield and Hicks make the oddest of couples , and in this sense the movie becomes a study of the gambles of the publishing world , offering a case study that exists apart from all the movie 's political ramifications . 3 -It 's like going to a house party and watching the host defend himself against a frothing ex-girlfriend . 1 -That the Chuck Norris `` grenade gag '' occurs about 7 times during Windtalkers is a good indication of how serious-minded the film is . 2 -The plot is romantic comedy boilerplate from start to finish . 2 -It arrives with an impeccable pedigree , mongrel pep , and almost indecipherable plot complications . 2 -A film that clearly means to preach exclusively to the converted . 2 \ No newline at end of file diff --git a/tutorials/fastnlp_advanced_tutorial/vocab.txt b/tutorials/fastnlp_advanced_tutorial/vocab.txt deleted file mode 100644 index fb140275..00000000 --- a/tutorials/fastnlp_advanced_tutorial/vocab.txt +++ /dev/null @@ -1,30522 +0,0 @@ -[PAD] -[unused0] -[unused1] -[unused2] -[unused3] -[unused4] -[unused5] -[unused6] -[unused7] -[unused8] -[unused9] -[unused10] -[unused11] -[unused12] -[unused13] -[unused14] -[unused15] -[unused16] -[unused17] -[unused18] -[unused19] -[unused20] -[unused21] -[unused22] -[unused23] -[unused24] -[unused25] -[unused26] -[unused27] -[unused28] -[unused29] -[unused30] -[unused31] -[unused32] -[unused33] -[unused34] -[unused35] -[unused36] -[unused37] -[unused38] -[unused39] -[unused40] -[unused41] -[unused42] -[unused43] -[unused44] -[unused45] -[unused46] -[unused47] -[unused48] -[unused49] -[unused50] -[unused51] -[unused52] -[unused53] -[unused54] -[unused55] -[unused56] -[unused57] -[unused58] -[unused59] -[unused60] -[unused61] -[unused62] -[unused63] -[unused64] -[unused65] -[unused66] -[unused67] -[unused68] -[unused69] -[unused70] -[unused71] -[unused72] -[unused73] -[unused74] -[unused75] -[unused76] -[unused77] -[unused78] -[unused79] -[unused80] -[unused81] -[unused82] -[unused83] -[unused84] -[unused85] -[unused86] -[unused87] -[unused88] -[unused89] -[unused90] -[unused91] -[unused92] -[unused93] -[unused94] -[unused95] -[unused96] -[unused97] -[unused98] -[UNK] -[CLS] -[SEP] -[MASK] -[unused99] -[unused100] -[unused101] -[unused102] -[unused103] -[unused104] -[unused105] -[unused106] -[unused107] -[unused108] -[unused109] -[unused110] -[unused111] -[unused112] -[unused113] -[unused114] -[unused115] -[unused116] -[unused117] -[unused118] -[unused119] -[unused120] -[unused121] -[unused122] -[unused123] -[unused124] -[unused125] -[unused126] -[unused127] -[unused128] -[unused129] -[unused130] -[unused131] -[unused132] -[unused133] -[unused134] -[unused135] -[unused136] -[unused137] -[unused138] -[unused139] -[unused140] -[unused141] -[unused142] -[unused143] -[unused144] -[unused145] -[unused146] -[unused147] -[unused148] -[unused149] -[unused150] -[unused151] -[unused152] -[unused153] -[unused154] -[unused155] -[unused156] -[unused157] -[unused158] -[unused159] -[unused160] -[unused161] -[unused162] -[unused163] -[unused164] -[unused165] -[unused166] -[unused167] -[unused168] -[unused169] -[unused170] -[unused171] -[unused172] -[unused173] -[unused174] -[unused175] -[unused176] -[unused177] -[unused178] -[unused179] -[unused180] -[unused181] -[unused182] -[unused183] -[unused184] -[unused185] -[unused186] -[unused187] -[unused188] -[unused189] -[unused190] -[unused191] -[unused192] -[unused193] -[unused194] -[unused195] -[unused196] -[unused197] -[unused198] -[unused199] -[unused200] -[unused201] -[unused202] -[unused203] -[unused204] -[unused205] -[unused206] -[unused207] -[unused208] -[unused209] -[unused210] -[unused211] -[unused212] -[unused213] -[unused214] -[unused215] -[unused216] -[unused217] -[unused218] -[unused219] -[unused220] -[unused221] -[unused222] -[unused223] -[unused224] -[unused225] -[unused226] -[unused227] -[unused228] -[unused229] -[unused230] -[unused231] -[unused232] -[unused233] -[unused234] -[unused235] -[unused236] -[unused237] -[unused238] -[unused239] -[unused240] -[unused241] -[unused242] -[unused243] -[unused244] -[unused245] -[unused246] -[unused247] -[unused248] -[unused249] -[unused250] -[unused251] -[unused252] -[unused253] -[unused254] -[unused255] -[unused256] -[unused257] -[unused258] -[unused259] -[unused260] -[unused261] -[unused262] -[unused263] -[unused264] -[unused265] -[unused266] -[unused267] -[unused268] -[unused269] -[unused270] -[unused271] -[unused272] -[unused273] -[unused274] -[unused275] -[unused276] -[unused277] -[unused278] -[unused279] -[unused280] -[unused281] -[unused282] -[unused283] -[unused284] -[unused285] -[unused286] -[unused287] -[unused288] -[unused289] -[unused290] -[unused291] -[unused292] -[unused293] -[unused294] -[unused295] -[unused296] -[unused297] -[unused298] -[unused299] -[unused300] -[unused301] -[unused302] -[unused303] -[unused304] -[unused305] -[unused306] -[unused307] -[unused308] -[unused309] -[unused310] -[unused311] -[unused312] -[unused313] -[unused314] -[unused315] -[unused316] -[unused317] -[unused318] -[unused319] -[unused320] -[unused321] -[unused322] -[unused323] -[unused324] -[unused325] -[unused326] -[unused327] -[unused328] -[unused329] -[unused330] -[unused331] -[unused332] -[unused333] -[unused334] -[unused335] -[unused336] -[unused337] -[unused338] -[unused339] -[unused340] -[unused341] -[unused342] -[unused343] -[unused344] -[unused345] -[unused346] -[unused347] -[unused348] -[unused349] -[unused350] -[unused351] -[unused352] -[unused353] -[unused354] -[unused355] -[unused356] -[unused357] -[unused358] -[unused359] -[unused360] -[unused361] -[unused362] -[unused363] -[unused364] -[unused365] -[unused366] -[unused367] -[unused368] -[unused369] -[unused370] -[unused371] -[unused372] -[unused373] -[unused374] -[unused375] -[unused376] -[unused377] -[unused378] -[unused379] -[unused380] -[unused381] -[unused382] -[unused383] -[unused384] -[unused385] -[unused386] -[unused387] -[unused388] -[unused389] -[unused390] -[unused391] -[unused392] -[unused393] -[unused394] -[unused395] -[unused396] -[unused397] -[unused398] -[unused399] -[unused400] -[unused401] -[unused402] -[unused403] -[unused404] -[unused405] -[unused406] -[unused407] -[unused408] -[unused409] -[unused410] -[unused411] -[unused412] -[unused413] -[unused414] -[unused415] -[unused416] -[unused417] -[unused418] -[unused419] -[unused420] -[unused421] -[unused422] -[unused423] -[unused424] -[unused425] -[unused426] -[unused427] -[unused428] -[unused429] -[unused430] -[unused431] -[unused432] -[unused433] -[unused434] -[unused435] -[unused436] -[unused437] -[unused438] -[unused439] -[unused440] -[unused441] -[unused442] -[unused443] -[unused444] -[unused445] -[unused446] -[unused447] -[unused448] -[unused449] -[unused450] -[unused451] -[unused452] -[unused453] -[unused454] -[unused455] -[unused456] -[unused457] -[unused458] -[unused459] -[unused460] -[unused461] -[unused462] -[unused463] -[unused464] -[unused465] -[unused466] -[unused467] -[unused468] -[unused469] -[unused470] -[unused471] -[unused472] -[unused473] -[unused474] -[unused475] -[unused476] -[unused477] -[unused478] -[unused479] -[unused480] -[unused481] -[unused482] -[unused483] -[unused484] -[unused485] -[unused486] -[unused487] -[unused488] -[unused489] -[unused490] -[unused491] -[unused492] -[unused493] -[unused494] -[unused495] -[unused496] -[unused497] -[unused498] -[unused499] -[unused500] -[unused501] -[unused502] -[unused503] -[unused504] -[unused505] -[unused506] -[unused507] -[unused508] -[unused509] -[unused510] -[unused511] -[unused512] -[unused513] -[unused514] -[unused515] -[unused516] -[unused517] -[unused518] -[unused519] -[unused520] -[unused521] -[unused522] -[unused523] -[unused524] -[unused525] -[unused526] -[unused527] -[unused528] -[unused529] -[unused530] -[unused531] -[unused532] -[unused533] -[unused534] -[unused535] -[unused536] -[unused537] -[unused538] -[unused539] -[unused540] -[unused541] -[unused542] -[unused543] -[unused544] -[unused545] -[unused546] -[unused547] -[unused548] -[unused549] -[unused550] -[unused551] -[unused552] -[unused553] -[unused554] -[unused555] -[unused556] -[unused557] -[unused558] -[unused559] -[unused560] -[unused561] -[unused562] -[unused563] -[unused564] -[unused565] -[unused566] -[unused567] -[unused568] -[unused569] -[unused570] -[unused571] -[unused572] -[unused573] -[unused574] -[unused575] -[unused576] -[unused577] -[unused578] -[unused579] -[unused580] -[unused581] -[unused582] -[unused583] -[unused584] -[unused585] -[unused586] -[unused587] -[unused588] -[unused589] -[unused590] -[unused591] -[unused592] -[unused593] -[unused594] -[unused595] -[unused596] -[unused597] -[unused598] -[unused599] -[unused600] -[unused601] -[unused602] -[unused603] -[unused604] -[unused605] -[unused606] -[unused607] -[unused608] -[unused609] -[unused610] -[unused611] -[unused612] -[unused613] -[unused614] -[unused615] -[unused616] -[unused617] -[unused618] -[unused619] -[unused620] -[unused621] -[unused622] -[unused623] -[unused624] -[unused625] -[unused626] -[unused627] -[unused628] -[unused629] -[unused630] -[unused631] -[unused632] -[unused633] -[unused634] -[unused635] -[unused636] -[unused637] -[unused638] -[unused639] -[unused640] -[unused641] -[unused642] -[unused643] -[unused644] -[unused645] -[unused646] -[unused647] -[unused648] -[unused649] -[unused650] -[unused651] -[unused652] -[unused653] -[unused654] -[unused655] -[unused656] -[unused657] -[unused658] -[unused659] -[unused660] -[unused661] -[unused662] -[unused663] -[unused664] -[unused665] -[unused666] -[unused667] -[unused668] -[unused669] -[unused670] -[unused671] -[unused672] -[unused673] -[unused674] -[unused675] -[unused676] -[unused677] -[unused678] -[unused679] -[unused680] -[unused681] -[unused682] -[unused683] -[unused684] -[unused685] -[unused686] -[unused687] -[unused688] -[unused689] -[unused690] -[unused691] -[unused692] -[unused693] -[unused694] -[unused695] -[unused696] -[unused697] -[unused698] -[unused699] -[unused700] -[unused701] -[unused702] -[unused703] -[unused704] -[unused705] -[unused706] -[unused707] -[unused708] -[unused709] -[unused710] -[unused711] -[unused712] -[unused713] -[unused714] -[unused715] -[unused716] -[unused717] -[unused718] -[unused719] -[unused720] -[unused721] -[unused722] -[unused723] -[unused724] -[unused725] -[unused726] -[unused727] -[unused728] -[unused729] -[unused730] -[unused731] -[unused732] -[unused733] -[unused734] -[unused735] -[unused736] -[unused737] -[unused738] -[unused739] -[unused740] -[unused741] -[unused742] -[unused743] -[unused744] -[unused745] -[unused746] -[unused747] -[unused748] -[unused749] -[unused750] -[unused751] -[unused752] -[unused753] -[unused754] -[unused755] -[unused756] -[unused757] -[unused758] -[unused759] -[unused760] -[unused761] -[unused762] -[unused763] -[unused764] -[unused765] -[unused766] -[unused767] -[unused768] -[unused769] -[unused770] -[unused771] -[unused772] -[unused773] -[unused774] -[unused775] -[unused776] -[unused777] -[unused778] -[unused779] -[unused780] -[unused781] -[unused782] -[unused783] -[unused784] -[unused785] -[unused786] -[unused787] -[unused788] -[unused789] -[unused790] -[unused791] -[unused792] -[unused793] -[unused794] -[unused795] -[unused796] -[unused797] -[unused798] -[unused799] -[unused800] -[unused801] -[unused802] -[unused803] -[unused804] -[unused805] -[unused806] -[unused807] -[unused808] -[unused809] -[unused810] -[unused811] -[unused812] -[unused813] -[unused814] -[unused815] -[unused816] -[unused817] -[unused818] -[unused819] -[unused820] -[unused821] -[unused822] -[unused823] -[unused824] -[unused825] -[unused826] -[unused827] -[unused828] -[unused829] -[unused830] -[unused831] -[unused832] -[unused833] -[unused834] -[unused835] -[unused836] -[unused837] -[unused838] -[unused839] -[unused840] -[unused841] -[unused842] -[unused843] -[unused844] -[unused845] -[unused846] -[unused847] -[unused848] -[unused849] -[unused850] -[unused851] -[unused852] -[unused853] -[unused854] -[unused855] -[unused856] -[unused857] -[unused858] -[unused859] -[unused860] -[unused861] -[unused862] -[unused863] -[unused864] -[unused865] -[unused866] -[unused867] -[unused868] -[unused869] -[unused870] -[unused871] -[unused872] -[unused873] -[unused874] -[unused875] -[unused876] -[unused877] -[unused878] -[unused879] -[unused880] -[unused881] -[unused882] -[unused883] -[unused884] -[unused885] -[unused886] -[unused887] -[unused888] -[unused889] -[unused890] -[unused891] -[unused892] -[unused893] -[unused894] -[unused895] -[unused896] -[unused897] -[unused898] -[unused899] -[unused900] -[unused901] -[unused902] -[unused903] -[unused904] -[unused905] -[unused906] -[unused907] -[unused908] -[unused909] -[unused910] -[unused911] -[unused912] -[unused913] -[unused914] -[unused915] -[unused916] -[unused917] -[unused918] -[unused919] -[unused920] -[unused921] -[unused922] -[unused923] -[unused924] -[unused925] -[unused926] -[unused927] -[unused928] -[unused929] -[unused930] -[unused931] -[unused932] -[unused933] -[unused934] -[unused935] -[unused936] -[unused937] -[unused938] -[unused939] -[unused940] -[unused941] -[unused942] -[unused943] -[unused944] -[unused945] -[unused946] -[unused947] -[unused948] -[unused949] -[unused950] -[unused951] -[unused952] -[unused953] -[unused954] -[unused955] -[unused956] -[unused957] -[unused958] -[unused959] -[unused960] -[unused961] -[unused962] -[unused963] -[unused964] -[unused965] -[unused966] -[unused967] -[unused968] -[unused969] -[unused970] -[unused971] -[unused972] -[unused973] -[unused974] -[unused975] -[unused976] -[unused977] -[unused978] -[unused979] -[unused980] -[unused981] -[unused982] -[unused983] -[unused984] -[unused985] -[unused986] -[unused987] -[unused988] -[unused989] -[unused990] -[unused991] -[unused992] -[unused993] -! -" -# -$ -% -& -' -( -) -* -+ -, -- -. -/ -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -: -; -< -= -> -? -@ -[ -\ -] -^ -_ -` -a -b -c -d -e -f -g -h -i -j -k -l -m -n -o -p -q -r -s -t -u -v -w -x -y -z -{ -| -} -~ -¡ -¢ -£ -¤ -¥ -¦ -§ -¨ -© -ª -« -¬ -® -° -± -² -³ -´ -µ -¶ -· -¹ -º -» -¼ -½ -¾ -¿ -× -ß -æ -ð -÷ -ø -þ -đ -ħ -ı -ł -ŋ -œ -ƒ -ɐ -ɑ -ɒ -ɔ -ɕ -ə -ɛ -ɡ -ɣ -ɨ -ɪ -ɫ -ɬ -ɯ -ɲ -ɴ -ɹ -ɾ -ʀ -ʁ -ʂ -ʃ -ʉ -ʊ -ʋ -ʌ -ʎ -ʐ -ʑ -ʒ -ʔ -ʰ -ʲ -ʳ -ʷ -ʸ -ʻ -ʼ -ʾ -ʿ -ˈ -ː -ˡ -ˢ -ˣ -ˤ -α -β -γ -δ -ε -ζ -η -θ -ι -κ -λ -μ -ν -ξ -ο -π -ρ -ς -σ -τ -υ -φ -χ -ψ -ω -а -б -в -г -д -е -ж -з -и -к -л -м -н -о -п -р -с -т -у -ф -х -ц -ч -ш -щ -ъ -ы -ь -э -ю -я -ђ -є -і -ј -љ -њ -ћ -ӏ -ա -բ -գ -դ -ե -թ -ի -լ -կ -հ -մ -յ -ն -ո -պ -ս -վ -տ -ր -ւ -ք -־ -א -ב -ג -ד -ה -ו -ז -ח -ט -י -ך -כ -ל -ם -מ -ן -נ -ס -ע -ף -פ -ץ -צ -ק -ר -ש -ת -، -ء -ا -ب -ة -ت -ث -ج -ح -خ -د -ذ -ر -ز -س -ش -ص -ض -ط -ظ -ع -غ -ـ -ف -ق -ك -ل -م -ن -ه -و -ى -ي -ٹ -پ -چ -ک -گ -ں -ھ -ہ -ی -ے -अ -आ -उ -ए -क -ख -ग -च -ज -ट -ड -ण -त -थ -द -ध -न -प -ब -भ -म -य -र -ल -व -श -ष -स -ह -ा -ि -ी -ो -। -॥ -ং -অ -আ -ই -উ -এ -ও -ক -খ -গ -চ -ছ -জ -ট -ড -ণ -ত -থ -দ -ধ -ন -প -ব -ভ -ম -য -র -ল -শ -ষ -স -হ -া -ি -ী -ে -க -ச -ட -த -ந -ன -ப -ம -ய -ர -ல -ள -வ -ா -ி -ு -ே -ை -ನ -ರ -ಾ -ක -ය -ර -ල -ව -ා -ก -ง -ต -ท -น -พ -ม -ย -ร -ล -ว -ส -อ -า -เ -་ -། -ག -ང -ད -ན -པ -བ -མ -འ -ར -ལ -ས -မ -ა -ბ -გ -დ -ე -ვ -თ -ი -კ -ლ -მ -ნ -ო -რ -ს -ტ -უ -ᄀ -ᄂ -ᄃ -ᄅ -ᄆ -ᄇ -ᄉ -ᄊ -ᄋ -ᄌ -ᄎ -ᄏ -ᄐ -ᄑ -ᄒ -ᅡ -ᅢ -ᅥ -ᅦ -ᅧ -ᅩ -ᅪ -ᅭ -ᅮ -ᅯ -ᅲ -ᅳ -ᅴ -ᅵ -ᆨ -ᆫ -ᆯ -ᆷ -ᆸ -ᆼ -ᴬ -ᴮ -ᴰ -ᴵ -ᴺ -ᵀ -ᵃ -ᵇ -ᵈ -ᵉ -ᵍ -ᵏ -ᵐ -ᵒ -ᵖ -ᵗ -ᵘ -ᵢ -ᵣ -ᵤ -ᵥ -ᶜ -ᶠ -‐ -‑ -‒ -– -— -― -‖ -‘ -’ -‚ -“ -” -„ -† -‡ -• -… -‰ -′ -″ -› -‿ -⁄ -⁰ -ⁱ -⁴ -⁵ -⁶ -⁷ -⁸ -⁹ -⁺ -⁻ -ⁿ -₀ -₁ -₂ -₃ -₄ -₅ -₆ -₇ -₈ -₉ -₊ -₍ -₎ -ₐ -ₑ -ₒ -ₓ -ₕ -ₖ -ₗ -ₘ -ₙ -ₚ -ₛ -ₜ -₤ -₩ -€ -₱ -₹ -ℓ -№ -ℝ -™ -⅓ -⅔ -← -↑ -→ -↓ -↔ -↦ -⇄ -⇌ -⇒ -∂ -∅ -∆ -∇ -∈ -− -∗ -∘ -√ -∞ -∧ -∨ -∩ -∪ -≈ -≡ -≤ -≥ -⊂ -⊆ -⊕ -⊗ -⋅ -─ -│ -■ -▪ -● -★ -☆ -☉ -♠ -♣ -♥ -♦ -♭ -♯ -⟨ -⟩ -ⱼ -⺩ -⺼ -⽥ -、 -。 -〈 -〉 -《 -》 -「 -」 -『 -』 -〜 -あ -い -う -え -お -か -き -く -け -こ -さ -し -す -せ -そ -た -ち -っ -つ -て -と -な -に -ぬ -ね -の -は -ひ -ふ -へ -ほ -ま -み -む -め -も -や -ゆ -よ -ら -り -る -れ -ろ -を -ん -ァ -ア -ィ -イ -ウ -ェ -エ -オ -カ -キ -ク -ケ -コ -サ -シ -ス -セ -タ -チ -ッ -ツ -テ -ト -ナ -ニ -ノ -ハ -ヒ -フ -ヘ -ホ -マ -ミ -ム -メ -モ -ャ -ュ -ョ -ラ -リ -ル -レ -ロ -ワ -ン -・ -ー -一 -三 -上 -下 -不 -世 -中 -主 -久 -之 -也 -事 -二 -五 -井 -京 -人 -亻 -仁 -介 -代 -仮 -伊 -会 -佐 -侍 -保 -信 -健 -元 -光 -八 -公 -内 -出 -分 -前 -劉 -力 -加 -勝 -北 -区 -十 -千 -南 -博 -原 -口 -古 -史 -司 -合 -吉 -同 -名 -和 -囗 -四 -国 -國 -土 -地 -坂 -城 -堂 -場 -士 -夏 -外 -大 -天 -太 -夫 -奈 -女 -子 -学 -宀 -宇 -安 -宗 -定 -宣 -宮 -家 -宿 -寺 -將 -小 -尚 -山 -岡 -島 -崎 -川 -州 -巿 -帝 -平 -年 -幸 -广 -弘 -張 -彳 -後 -御 -德 -心 -忄 -志 -忠 -愛 -成 -我 -戦 -戸 -手 -扌 -政 -文 -新 -方 -日 -明 -星 -春 -昭 -智 -曲 -書 -月 -有 -朝 -木 -本 -李 -村 -東 -松 -林 -森 -楊 -樹 -橋 -歌 -止 -正 -武 -比 -氏 -民 -水 -氵 -氷 -永 -江 -沢 -河 -治 -法 -海 -清 -漢 -瀬 -火 -版 -犬 -王 -生 -田 -男 -疒 -発 -白 -的 -皇 -目 -相 -省 -真 -石 -示 -社 -神 -福 -禾 -秀 -秋 -空 -立 -章 -竹 -糹 -美 -義 -耳 -良 -艹 -花 -英 -華 -葉 -藤 -行 -街 -西 -見 -訁 -語 -谷 -貝 -貴 -車 -軍 -辶 -道 -郎 -郡 -部 -都 -里 -野 -金 -鈴 -镇 -長 -門 -間 -阝 -阿 -陳 -陽 -雄 -青 -面 -風 -食 -香 -馬 -高 -龍 -龸 -fi -fl -! -( -) -, -- -. -/ -: -? -~ -the -of -and -in -to -was -he -is -as -for -on -with -that -it -his -by -at -from -her -##s -she -you -had -an -were -but -be -this -are -not -my -they -one -which -or -have -him -me -first -all -also -their -has -up -who -out -been -when -after -there -into -new -two -its -##a -time -would -no -what -about -said -we -over -then -other -so -more -##e -can -if -like -back -them -only -some -could -##i -where -just -##ing -during -before -##n -do -##o -made -school -through -than -now -years -most -world -may -between -down -well -three -##d -year -while -will -##ed -##r -##y -later -##t -city -under -around -did -such -being -used -state -people -part -know -against -your -many -second -university -both -national -##er -these -don -known -off -way -until -re -how -even -get -head -... -didn -##ly -team -american -because -de -##l -born -united -film -since -still -long -work -south -us -became -any -high -again -day -family -see -right -man -eyes -house -season -war -states -including -took -life -north -same -each -called -name -much -place -however -go -four -group -another -found -won -area -here -going -10 -away -series -left -home -music -best -make -hand -number -company -several -never -last -john -000 -very -album -take -end -good -too -following -released -game -played -little -began -district -##m -old -want -those -side -held -own -early -county -ll -league -use -west -##u -face -think -##es -2010 -government -##h -march -came -small -general -town -june -##on -line -based -something -##k -september -thought -looked -along -international -2011 -air -july -club -went -january -october -our -august -april -york -12 -few -2012 -2008 -east -show -member -college -2009 -father -public -##us -come -men -five -set -station -church -##c -next -former -november -room -party -located -december -2013 -age -got -2007 -##g -system -let -love -2006 -though -every -2014 -look -song -water -century -without -body -black -night -within -great -women -single -ve -building -large -population -river -named -band -white -started -##an -once -15 -20 -should -18 -2015 -service -top -built -british -open -death -king -moved -local -times -children -february -book -why -11 -door -need -president -order -final -road -wasn -although -due -major -died -village -third -knew -2016 -asked -turned -st -wanted -say -##p -together -received -main -son -served -different -##en -behind -himself -felt -members -power -football -law -voice -play -##in -near -park -history -30 -having -2005 -16 -##man -saw -mother -##al -army -point -front -help -english -street -art -late -hands -games -award -##ia -young -14 -put -published -country -division -across -told -13 -often -ever -french -london -center -six -red -2017 -led -days -include -light -25 -find -tell -among -species -really -according -central -half -2004 -form -original -gave -office -making -enough -lost -full -opened -must -included -live -given -german -player -run -business -woman -community -cup -might -million -land -2000 -court -development -17 -short -round -ii -km -seen -class -story -always -become -sure -research -almost -director -council -la -##2 -career -things -using -island -##z -couldn -car -##is -24 -close -force -##1 -better -free -support -control -field -students -2003 -education -married -##b -nothing -worked -others -record -big -inside -level -anything -continued -give -james -##3 -military -established -non -returned -feel -does -title -written -thing -feet -william -far -co -association -hard -already -2002 -##ra -championship -human -western -100 -##na -department -hall -role -various -production -21 -19 -heart -2001 -living -fire -version -##ers -##f -television -royal -##4 -produced -working -act -case -society -region -present -radio -period -looking -least -total -keep -england -wife -program -per -brother -mind -special -22 -##le -am -works -soon -##6 -political -george -services -taken -created -##7 -further -able -reached -david -union -joined -upon -done -important -social -information -either -##ic -##x -appeared -position -ground -lead -rock -dark -election -23 -board -france -hair -course -arms -site -police -girl -instead -real -sound -##v -words -moment -##te -someone -##8 -summer -project -announced -san -less -wrote -past -followed -##5 -blue -founded -al -finally -india -taking -records -america -##ne -1999 -design -considered -northern -god -stop -battle -toward -european -outside -described -track -today -playing -language -28 -call -26 -heard -professional -low -australia -miles -california -win -yet -green -##ie -trying -blood -##ton -southern -science -maybe -everything -match -square -27 -mouth -video -race -recorded -leave -above -##9 -daughter -points -space -1998 -museum -change -middle -common -##0 -move -tv -post -##ta -lake -seven -tried -elected -closed -ten -paul -minister -##th -months -start -chief -return -canada -person -sea -release -similar -modern -brought -rest -hit -formed -mr -##la -1997 -floor -event -doing -thomas -1996 -robert -care -killed -training -star -week -needed -turn -finished -railway -rather -news -health -sent -example -ran -term -michael -coming -currently -yes -forces -despite -gold -areas -50 -stage -fact -29 -dead -says -popular -2018 -originally -germany -probably -developed -result -pulled -friend -stood -money -running -mi -signed -word -songs -child -eventually -met -tour -average -teams -minutes -festival -current -deep -kind -1995 -decided -usually -eastern -seemed -##ness -episode -bed -added -table -indian -private -charles -route -available -idea -throughout -centre -addition -appointed -style -1994 -books -eight -construction -press -mean -wall -friends -remained -schools -study -##ch -##um -institute -oh -chinese -sometimes -events -possible -1992 -australian -type -brown -forward -talk -process -food -debut -seat -performance -committee -features -character -arts -herself -else -lot -strong -russian -range -hours -peter -arm -##da -morning -dr -sold -##ry -quickly -directed -1993 -guitar -china -##w -31 -list -##ma -performed -media -uk -players -smile -##rs -myself -40 -placed -coach -province -towards -wouldn -leading -whole -boy -official -designed -grand -census -##el -europe -attack -japanese -henry -1991 -##re -##os -cross -getting -alone -action -lower -network -wide -washington -japan -1990 -hospital -believe -changed -sister -##ar -hold -gone -sir -hadn -ship -##ka -studies -academy -shot -rights -below -base -bad -involved -kept -largest -##ist -bank -future -especially -beginning -mark -movement -section -female -magazine -plan -professor -lord -longer -##ian -sat -walked -hill -actually -civil -energy -model -families -size -thus -aircraft -completed -includes -data -captain -##or -fight -vocals -featured -richard -bridge -fourth -1989 -officer -stone -hear -##ism -means -medical -groups -management -self -lips -competition -entire -lived -technology -leaving -federal -tournament -bit -passed -hot -independent -awards -kingdom -mary -spent -fine -doesn -reported -##ling -jack -fall -raised -itself -stay -true -studio -1988 -sports -replaced -paris -systems -saint -leader -theatre -whose -market -capital -parents -spanish -canadian -earth -##ity -cut -degree -writing -bay -christian -awarded -natural -higher -bill -##as -coast -provided -previous -senior -ft -valley -organization -stopped -onto -countries -parts -conference -queen -security -interest -saying -allowed -master -earlier -phone -matter -smith -winning -try -happened -moving -campaign -los -##ley -breath -nearly -mid -1987 -certain -girls -date -italian -african -standing -fell -artist -##ted -shows -deal -mine -industry -1986 -##ng -everyone -republic -provide -collection -library -student -##ville -primary -owned -older -via -heavy -1st -makes -##able -attention -anyone -africa -##ri -stated -length -ended -fingers -command -staff -skin -foreign -opening -governor -okay -medal -kill -sun -cover -job -1985 -introduced -chest -hell -feeling -##ies -success -meet -reason -standard -meeting -novel -1984 -trade -source -buildings -##land -rose -guy -goal -##ur -chapter -native -husband -previously -unit -limited -entered -weeks -producer -operations -mountain -takes -covered -forced -related -roman -complete -successful -key -texas -cold -##ya -channel -1980 -traditional -films -dance -clear -approximately -500 -nine -van -prince -question -active -tracks -ireland -regional -silver -author -personal -sense -operation -##ine -economic -1983 -holding -twenty -isbn -additional -speed -hour -edition -regular -historic -places -whom -shook -movie -km² -secretary -prior -report -chicago -read -foundation -view -engine -scored -1982 -units -ask -airport -property -ready -immediately -lady -month -listed -contract -##de -manager -themselves -lines -##ki -navy -writer -meant -##ts -runs -##ro -practice -championships -singer -glass -commission -required -forest -starting -culture -generally -giving -access -attended -test -couple -stand -catholic -martin -caught -executive -##less -eye -##ey -thinking -chair -quite -shoulder -1979 -hope -decision -plays -defeated -municipality -whether -structure -offered -slowly -pain -ice -direction -##ion -paper -mission -1981 -mostly -200 -noted -individual -managed -nature -lives -plant -##ha -helped -except -studied -computer -figure -relationship -issue -significant -loss -die -smiled -gun -ago -highest -1972 -##am -male -bring -goals -mexico -problem -distance -commercial -completely -location -annual -famous -drive -1976 -neck -1978 -surface -caused -italy -understand -greek -highway -wrong -hotel -comes -appearance -joseph -double -issues -musical -companies -castle -income -review -assembly -bass -initially -parliament -artists -experience -1974 -particular -walk -foot -engineering -talking -window -dropped -##ter -miss -baby -boys -break -1975 -stars -edge -remember -policy -carried -train -stadium -bar -sex -angeles -evidence -##ge -becoming -assistant -soviet -1977 -upper -step -wing -1970 -youth -financial -reach -##ll -actor -numerous -##se -##st -nodded -arrived -##ation -minute -##nt -believed -sorry -complex -beautiful -victory -associated -temple -1968 -1973 -chance -perhaps -metal -##son -1945 -bishop -##et -lee -launched -particularly -tree -le -retired -subject -prize -contains -yeah -theory -empire -##ce -suddenly -waiting -trust -recording -##to -happy -terms -camp -champion -1971 -religious -pass -zealand -names -2nd -port -ancient -tom -corner -represented -watch -legal -anti -justice -cause -watched -brothers -45 -material -changes -simply -response -louis -fast -##ting -answer -60 -historical -1969 -stories -straight -create -feature -increased -rate -administration -virginia -el -activities -cultural -overall -winner -programs -basketball -legs -guard -beyond -cast -doctor -mm -flight -results -remains -cost -effect -winter -##ble -larger -islands -problems -chairman -grew -commander -isn -1967 -pay -failed -selected -hurt -fort -box -regiment -majority -journal -35 -edward -plans -##ke -##ni -shown -pretty -irish -characters -directly -scene -likely -operated -allow -spring -##j -junior -matches -looks -mike -houses -fellow -##tion -beach -marriage -##ham -##ive -rules -oil -65 -florida -expected -nearby -congress -sam -peace -recent -iii -wait -subsequently -cell -##do -variety -serving -agreed -please -poor -joe -pacific -attempt -wood -democratic -piece -prime -##ca -rural -mile -touch -appears -township -1964 -1966 -soldiers -##men -##ized -1965 -pennsylvania -closer -fighting -claimed -score -jones -physical -editor -##ous -filled -genus -specific -sitting -super -mom -##va -therefore -supported -status -fear -cases -store -meaning -wales -minor -spain -tower -focus -vice -frank -follow -parish -separate -golden -horse -fifth -remaining -branch -32 -presented -stared -##id -uses -secret -forms -##co -baseball -exactly -##ck -choice -note -discovered -travel -composed -truth -russia -ball -color -kiss -dad -wind -continue -ring -referred -numbers -digital -greater -##ns -metres -slightly -direct -increase -1960 -responsible -crew -rule -trees -troops -##no -broke -goes -individuals -hundred -weight -creek -sleep -memory -defense -provides -ordered -code -value -jewish -windows -1944 -safe -judge -whatever -corps -realized -growing -pre -##ga -cities -alexander -gaze -lies -spread -scott -letter -showed -situation -mayor -transport -watching -workers -extended -##li -expression -normal -##ment -chart -multiple -border -##ba -host -##ner -daily -mrs -walls -piano -##ko -heat -cannot -##ate -earned -products -drama -era -authority -seasons -join -grade -##io -sign -difficult -machine -1963 -territory -mainly -##wood -stations -squadron -1962 -stepped -iron -19th -##led -serve -appear -sky -speak -broken -charge -knowledge -kilometres -removed -ships -article -campus -simple -##ty -pushed -britain -##ve -leaves -recently -cd -soft -boston -latter -easy -acquired -poland -##sa -quality -officers -presence -planned -nations -mass -broadcast -jean -share -image -influence -wild -offer -emperor -electric -reading -headed -ability -promoted -yellow -ministry -1942 -throat -smaller -politician -##by -latin -spoke -cars -williams -males -lack -pop -80 -##ier -acting -seeing -consists -##ti -estate -1961 -pressure -johnson -newspaper -jr -chris -olympics -online -conditions -beat -elements -walking -vote -##field -needs -carolina -text -featuring -global -block -shirt -levels -francisco -purpose -females -et -dutch -duke -ahead -gas -twice -safety -serious -turning -highly -lieutenant -firm -maria -amount -mixed -daniel -proposed -perfect -agreement -affairs -3rd -seconds -contemporary -paid -1943 -prison -save -kitchen -label -administrative -intended -constructed -academic -nice -teacher -races -1956 -formerly -corporation -ben -nation -issued -shut -1958 -drums -housing -victoria -seems -opera -1959 -graduated -function -von -mentioned -picked -build -recognized -shortly -protection -picture -notable -exchange -elections -1980s -loved -percent -racing -fish -elizabeth -garden -volume -hockey -1941 -beside -settled -##ford -1940 -competed -replied -drew -1948 -actress -marine -scotland -steel -glanced -farm -steve -1957 -risk -tonight -positive -magic -singles -effects -gray -screen -dog -##ja -residents -bus -sides -none -secondary -literature -polish -destroyed -flying -founder -households -1939 -lay -reserve -usa -gallery -##ler -1946 -industrial -younger -approach -appearances -urban -ones -1950 -finish -avenue -powerful -fully -growth -page -honor -jersey -projects -advanced -revealed -basic -90 -infantry -pair -equipment -visit -33 -evening -search -grant -effort -solo -treatment -buried -republican -primarily -bottom -owner -1970s -israel -gives -jim -dream -bob -remain -spot -70 -notes -produce -champions -contact -ed -soul -accepted -ways -del -##ally -losing -split -price -capacity -basis -trial -questions -##ina -1955 -20th -guess -officially -memorial -naval -initial -##ization -whispered -median -engineer -##ful -sydney -##go -columbia -strength -300 -1952 -tears -senate -00 -card -asian -agent -1947 -software -44 -draw -warm -supposed -com -pro -##il -transferred -leaned -##at -candidate -escape -mountains -asia -potential -activity -entertainment -seem -traffic -jackson -murder -36 -slow -product -orchestra -haven -agency -bbc -taught -website -comedy -unable -storm -planning -albums -rugby -environment -scientific -grabbed -protect -##hi -boat -typically -1954 -1953 -damage -principal -divided -dedicated -mount -ohio -##berg -pick -fought -driver -##der -empty -shoulders -sort -thank -berlin -prominent -account -freedom -necessary -efforts -alex -headquarters -follows -alongside -des -simon -andrew -suggested -operating -learning -steps -1949 -sweet -technical -begin -easily -34 -teeth -speaking -settlement -scale -##sh -renamed -ray -max -enemy -semi -joint -compared -##rd -scottish -leadership -analysis -offers -georgia -pieces -captured -animal -deputy -guest -organized -##lin -tony -combined -method -challenge -1960s -huge -wants -battalion -sons -rise -crime -types -facilities -telling -path -1951 -platform -sit -1990s -##lo -tells -assigned -rich -pull -##ot -commonly -alive -##za -letters -concept -conducted -wearing -happen -bought -becomes -holy -gets -ocean -defeat -languages -purchased -coffee -occurred -titled -##q -declared -applied -sciences -concert -sounds -jazz -brain -##me -painting -fleet -tax -nick -##ius -michigan -count -animals -leaders -episodes -##line -content -##den -birth -##it -clubs -64 -palace -critical -refused -fair -leg -laughed -returning -surrounding -participated -formation -lifted -pointed -connected -rome -medicine -laid -taylor -santa -powers -adam -tall -shared -focused -knowing -yards -entrance -falls -##wa -calling -##ad -sources -chosen -beneath -resources -yard -##ite -nominated -silence -zone -defined -##que -gained -thirty -38 -bodies -moon -##ard -adopted -christmas -widely -register -apart -iran -premier -serves -du -unknown -parties -##les -generation -##ff -continues -quick -fields -brigade -quiet -teaching -clothes -impact -weapons -partner -flat -theater -supreme -1938 -37 -relations -##tor -plants -suffered -1936 -wilson -kids -begins -##age -1918 -seats -armed -internet -models -worth -laws -400 -communities -classes -background -knows -thanks -quarter -reaching -humans -carry -killing -format -kong -hong -setting -75 -architecture -disease -railroad -inc -possibly -wish -arthur -thoughts -harry -doors -density -##di -crowd -illinois -stomach -tone -unique -reports -anyway -##ir -liberal -der -vehicle -thick -dry -drug -faced -largely -facility -theme -holds -creation -strange -colonel -##mi -revolution -bell -politics -turns -silent -rail -relief -independence -combat -shape -write -determined -sales -learned -4th -finger -oxford -providing -1937 -heritage -fiction -situated -designated -allowing -distribution -hosted -##est -sight -interview -estimated -reduced -##ria -toronto -footballer -keeping -guys -damn -claim -motion -sport -sixth -stayed -##ze -en -rear -receive -handed -twelve -dress -audience -granted -brazil -##well -spirit -##ated -noticed -etc -olympic -representative -eric -tight -trouble -reviews -drink -vampire -missing -roles -ranked -newly -household -finals -wave -critics -##ee -phase -massachusetts -pilot -unlike -philadelphia -bright -guns -crown -organizations -roof -42 -respectively -clearly -tongue -marked -circle -fox -korea -bronze -brian -expanded -sexual -supply -yourself -inspired -labour -fc -##ah -reference -vision -draft -connection -brand -reasons -1935 -classic -driving -trip -jesus -cells -entry -1920 -neither -trail -claims -atlantic -orders -labor -nose -afraid -identified -intelligence -calls -cancer -attacked -passing -stephen -positions -imperial -grey -jason -39 -sunday -48 -swedish -avoid -extra -uncle -message -covers -allows -surprise -materials -fame -hunter -##ji -1930 -citizens -figures -davis -environmental -confirmed -shit -titles -di -performing -difference -acts -attacks -##ov -existing -votes -opportunity -nor -shop -entirely -trains -opposite -pakistan -##pa -develop -resulted -representatives -actions -reality -pressed -##ish -barely -wine -conversation -faculty -northwest -ends -documentary -nuclear -stock -grace -sets -eat -alternative -##ps -bag -resulting -creating -surprised -cemetery -1919 -drop -finding -sarah -cricket -streets -tradition -ride -1933 -exhibition -target -ear -explained -rain -composer -injury -apartment -municipal -educational -occupied -netherlands -clean -billion -constitution -learn -1914 -maximum -classical -francis -lose -opposition -jose -ontario -bear -core -hills -rolled -ending -drawn -permanent -fun -##tes -##lla -lewis -sites -chamber -ryan -##way -scoring -height -1934 -##house -lyrics -staring -55 -officials -1917 -snow -oldest -##tic -orange -##ger -qualified -interior -apparently -succeeded -thousand -dinner -lights -existence -fans -heavily -41 -greatest -conservative -send -bowl -plus -enter -catch -##un -economy -duty -1929 -speech -authorities -princess -performances -versions -shall -graduate -pictures -effective -remembered -poetry -desk -crossed -starring -starts -passenger -sharp -##ant -acres -ass -weather -falling -rank -fund -supporting -check -adult -publishing -heads -cm -southeast -lane -##burg -application -bc -##ura -les -condition -transfer -prevent -display -ex -regions -earl -federation -cool -relatively -answered -besides -1928 -obtained -portion -##town -mix -##ding -reaction -liked -dean -express -peak -1932 -##tte -counter -religion -chain -rare -miller -convention -aid -lie -vehicles -mobile -perform -squad -wonder -lying -crazy -sword -##ping -attempted -centuries -weren -philosophy -category -##ize -anna -interested -47 -sweden -wolf -frequently -abandoned -kg -literary -alliance -task -entitled -##ay -threw -promotion -factory -tiny -soccer -visited -matt -fm -achieved -52 -defence -internal -persian -43 -methods -##ging -arrested -otherwise -cambridge -programming -villages -elementary -districts -rooms -criminal -conflict -worry -trained -1931 -attempts -waited -signal -bird -truck -subsequent -programme -##ol -ad -49 -communist -details -faith -sector -patrick -carrying -laugh -##ss -controlled -korean -showing -origin -fuel -evil -1927 -##ent -brief -identity -darkness -address -pool -missed -publication -web -planet -ian -anne -wings -invited -##tt -briefly -standards -kissed -##be -ideas -climate -causing -walter -worse -albert -articles -winners -desire -aged -northeast -dangerous -gate -doubt -1922 -wooden -multi -##ky -poet -rising -funding -46 -communications -communication -violence -copies -prepared -ford -investigation -skills -1924 -pulling -electronic -##ak -##ial -##han -containing -ultimately -offices -singing -understanding -restaurant -tomorrow -fashion -christ -ward -da -pope -stands -5th -flow -studios -aired -commissioned -contained -exist -fresh -americans -##per -wrestling -approved -kid -employed -respect -suit -1925 -angel -asking -increasing -frame -angry -selling -1950s -thin -finds -##nd -temperature -statement -ali -explain -inhabitants -towns -extensive -narrow -51 -jane -flowers -images -promise -somewhere -object -fly -closely -##ls -1912 -bureau -cape -1926 -weekly -presidential -legislative -1921 -##ai -##au -launch -founding -##ny -978 -##ring -artillery -strike -un -institutions -roll -writers -landing -chose -kevin -anymore -pp -##ut -attorney -fit -dan -billboard -receiving -agricultural -breaking -sought -dave -admitted -lands -mexican -##bury -charlie -specifically -hole -iv -howard -credit -moscow -roads -accident -1923 -proved -wear -struck -hey -guards -stuff -slid -expansion -1915 -cat -anthony -##kin -melbourne -opposed -sub -southwest -architect -failure -plane -1916 -##ron -map -camera -tank -listen -regarding -wet -introduction -metropolitan -link -ep -fighter -inch -grown -gene -anger -fixed -buy -dvd -khan -domestic -worldwide -chapel -mill -functions -examples -##head -developing -1910 -turkey -hits -pocket -antonio -papers -grow -unless -circuit -18th -concerned -attached -journalist -selection -journey -converted -provincial -painted -hearing -aren -bands -negative -aside -wondered -knight -lap -survey -ma -##ow -noise -billy -##ium -shooting -guide -bedroom -priest -resistance -motor -homes -sounded -giant -##mer -150 -scenes -equal -comic -patients -hidden -solid -actual -bringing -afternoon -touched -funds -wedding -consisted -marie -canal -sr -kim -treaty -turkish -recognition -residence -cathedral -broad -knees -incident -shaped -fired -norwegian -handle -cheek -contest -represent -##pe -representing -beauty -##sen -birds -advantage -emergency -wrapped -drawing -notice -pink -broadcasting -##ong -somehow -bachelor -seventh -collected -registered -establishment -alan -assumed -chemical -personnel -roger -retirement -jeff -portuguese -wore -tied -device -threat -progress -advance -##ised -banks -hired -manchester -nfl -teachers -structures -forever -##bo -tennis -helping -saturday -sale -applications -junction -hip -incorporated -neighborhood -dressed -ceremony -##ds -influenced -hers -visual -stairs -decades -inner -kansas -hung -hoped -gain -scheduled -downtown -engaged -austria -clock -norway -certainly -pale -protected -1913 -victor -employees -plate -putting -surrounded -##ists -finishing -blues -tropical -##ries -minnesota -consider -philippines -accept -54 -retrieved -1900 -concern -anderson -properties -institution -gordon -successfully -vietnam -##dy -backing -outstanding -muslim -crossing -folk -producing -usual -demand -occurs -observed -lawyer -educated -##ana -kelly -string -pleasure -budget -items -quietly -colorado -philip -typical -##worth -derived -600 -survived -asks -mental -##ide -56 -jake -jews -distinguished -ltd -1911 -sri -extremely -53 -athletic -loud -thousands -worried -shadow -transportation -horses -weapon -arena -importance -users -tim -objects -contributed -dragon -douglas -aware -senator -johnny -jordan -sisters -engines -flag -investment -samuel -shock -capable -clark -row -wheel -refers -session -familiar -biggest -wins -hate -maintained -drove -hamilton -request -expressed -injured -underground -churches -walker -wars -tunnel -passes -stupid -agriculture -softly -cabinet -regarded -joining -indiana -##ea -##ms -push -dates -spend -behavior -woods -protein -gently -chase -morgan -mention -burning -wake -combination -occur -mirror -leads -jimmy -indeed -impossible -singapore -paintings -covering -##nes -soldier -locations -attendance -sell -historian -wisconsin -invasion -argued -painter -diego -changing -egypt -##don -experienced -inches -##ku -missouri -vol -grounds -spoken -switzerland -##gan -reform -rolling -ha -forget -massive -resigned -burned -allen -tennessee -locked -values -improved -##mo -wounded -universe -sick -dating -facing -pack -purchase -user -##pur -moments -##ul -merged -anniversary -1908 -coal -brick -understood -causes -dynasty -queensland -establish -stores -crisis -promote -hoping -views -cards -referee -extension -##si -raise -arizona -improve -colonial -formal -charged -##rt -palm -lucky -hide -rescue -faces -95 -feelings -candidates -juan -##ell -goods -6th -courses -weekend -59 -luke -cash -fallen -##om -delivered -affected -installed -carefully -tries -swiss -hollywood -costs -lincoln -responsibility -##he -shore -file -proper -normally -maryland -assistance -jump -constant -offering -friendly -waters -persons -realize -contain -trophy -800 -partnership -factor -58 -musicians -cry -bound -oregon -indicated -hero -houston -medium -##ure -consisting -somewhat -##ara -57 -cycle -##che -beer -moore -frederick -gotten -eleven -worst -weak -approached -arranged -chin -loan -universal -bond -fifteen -pattern -disappeared -##ney -translated -##zed -lip -arab -capture -interests -insurance -##chi -shifted -cave -prix -warning -sections -courts -coat -plot -smell -feed -golf -favorite -maintain -knife -vs -voted -degrees -finance -quebec -opinion -translation -manner -ruled -operate -productions -choose -musician -discovery -confused -tired -separated -stream -techniques -committed -attend -ranking -kings -throw -passengers -measure -horror -fan -mining -sand -danger -salt -calm -decade -dam -require -runner -##ik -rush -associate -greece -##ker -rivers -consecutive -matthew -##ski -sighed -sq -documents -steam -edited -closing -tie -accused -1905 -##ini -islamic -distributed -directors -organisation -bruce -7th -breathing -mad -lit -arrival -concrete -taste -08 -composition -shaking -faster -amateur -adjacent -stating -1906 -twin -flew -##ran -tokyo -publications -##tone -obviously -ridge -storage -1907 -carl -pages -concluded -desert -driven -universities -ages -terminal -sequence -borough -250 -constituency -creative -cousin -economics -dreams -margaret -notably -reduce -montreal -mode -17th -ears -saved -jan -vocal -##ica -1909 -andy -##jo -riding -roughly -threatened -##ise -meters -meanwhile -landed -compete -repeated -grass -czech -regularly -charges -tea -sudden -appeal -##ung -solution -describes -pierre -classification -glad -parking -##ning -belt -physics -99 -rachel -add -hungarian -participate -expedition -damaged -gift -childhood -85 -fifty -##red -mathematics -jumped -letting -defensive -mph -##ux -##gh -testing -##hip -hundreds -shoot -owners -matters -smoke -israeli -kentucky -dancing -mounted -grandfather -emma -designs -profit -argentina -##gs -truly -li -lawrence -cole -begun -detroit -willing -branches -smiling -decide -miami -enjoyed -recordings -##dale -poverty -ethnic -gay -##bi -gary -arabic -09 -accompanied -##one -##ons -fishing -determine -residential -acid -##ary -alice -returns -starred -mail -##ang -jonathan -strategy -##ue -net -forty -cook -businesses -equivalent -commonwealth -distinct -ill -##cy -seriously -##ors -##ped -shift -harris -replace -rio -imagine -formula -ensure -##ber -additionally -scheme -conservation -occasionally -purposes -feels -favor -##and -##ore -1930s -contrast -hanging -hunt -movies -1904 -instruments -victims -danish -christopher -busy -demon -sugar -earliest -colony -studying -balance -duties -##ks -belgium -slipped -carter -05 -visible -stages -iraq -fifa -##im -commune -forming -zero -07 -continuing -talked -counties -legend -bathroom -option -tail -clay -daughters -afterwards -severe -jaw -visitors -##ded -devices -aviation -russell -kate -##vi -entering -subjects -##ino -temporary -swimming -forth -smooth -ghost -audio -bush -operates -rocks -movements -signs -eddie -##tz -ann -voices -honorary -06 -memories -dallas -pure -measures -racial -promised -66 -harvard -ceo -16th -parliamentary -indicate -benefit -flesh -dublin -louisiana -1902 -1901 -patient -sleeping -1903 -membership -coastal -medieval -wanting -element -scholars -rice -62 -limit -survive -makeup -rating -definitely -collaboration -obvious -##tan -boss -ms -baron -birthday -linked -soil -diocese -##lan -ncaa -##mann -offensive -shell -shouldn -waist -##tus -plain -ross -organ -resolution -manufacturing -adding -relative -kennedy -98 -whilst -moth -marketing -gardens -crash -72 -heading -partners -credited -carlos -moves -cable -##zi -marshall -##out -depending -bottle -represents -rejected -responded -existed -04 -jobs -denmark -lock -##ating -treated -graham -routes -talent -commissioner -drugs -secure -tests -reign -restored -photography -##gi -contributions -oklahoma -designer -disc -grin -seattle -robin -paused -atlanta -unusual -##gate -praised -las -laughing -satellite -hungary -visiting -##sky -interesting -factors -deck -poems -norman -##water -stuck -speaker -rifle -domain -premiered -##her -dc -comics -actors -01 -reputation -eliminated -8th -ceiling -prisoners -script -##nce -leather -austin -mississippi -rapidly -admiral -parallel -charlotte -guilty -tools -gender -divisions -fruit -##bs -laboratory -nelson -fantasy -marry -rapid -aunt -tribe -requirements -aspects -suicide -amongst -adams -bone -ukraine -abc -kick -sees -edinburgh -clothing -column -rough -gods -hunting -broadway -gathered -concerns -##ek -spending -ty -12th -snapped -requires -solar -bones -cavalry -##tta -iowa -drinking -waste -index -franklin -charity -thompson -stewart -tip -flash -landscape -friday -enjoy -singh -poem -listening -##back -eighth -fred -differences -adapted -bomb -ukrainian -surgery -corporate -masters -anywhere -##more -waves -odd -sean -portugal -orleans -dick -debate -kent -eating -puerto -cleared -96 -expect -cinema -97 -guitarist -blocks -electrical -agree -involving -depth -dying -panel -struggle -##ged -peninsula -adults -novels -emerged -vienna -metro -debuted -shoes -tamil -songwriter -meets -prove -beating -instance -heaven -scared -sending -marks -artistic -passage -superior -03 -significantly -shopping -##tive -retained -##izing -malaysia -technique -cheeks -##ola -warren -maintenance -destroy -extreme -allied -120 -appearing -##yn -fill -advice -alabama -qualifying -policies -cleveland -hat -battery -smart -authors -10th -soundtrack -acted -dated -lb -glance -equipped -coalition -funny -outer -ambassador -roy -possibility -couples -campbell -dna -loose -ethan -supplies -1898 -gonna -88 -monster -##res -shake -agents -frequency -springs -dogs -practices -61 -gang -plastic -easier -suggests -gulf -blade -exposed -colors -industries -markets -pan -nervous -electoral -charts -legislation -ownership -##idae -mac -appointment -shield -copy -assault -socialist -abbey -monument -license -throne -employment -jay -93 -replacement -charter -cloud -powered -suffering -accounts -oak -connecticut -strongly -wright -colour -crystal -13th -context -welsh -networks -voiced -gabriel -jerry -##cing -forehead -mp -##ens -manage -schedule -totally -remix -##ii -forests -occupation -print -nicholas -brazilian -strategic -vampires -engineers -76 -roots -seek -correct -instrumental -und -alfred -backed -hop -##des -stanley -robinson -traveled -wayne -welcome -austrian -achieve -67 -exit -rates -1899 -strip -whereas -##cs -sing -deeply -adventure -bobby -rick -jamie -careful -components -cap -useful -personality -knee -##shi -pushing -hosts -02 -protest -ca -ottoman -symphony -##sis -63 -boundary -1890 -processes -considering -considerable -tons -##work -##ft -##nia -cooper -trading -dear -conduct -91 -illegal -apple -revolutionary -holiday -definition -harder -##van -jacob -circumstances -destruction -##lle -popularity -grip -classified -liverpool -donald -baltimore -flows -seeking -honour -approval -92 -mechanical -till -happening -statue -critic -increasingly -immediate -describe -commerce -stare -##ster -indonesia -meat -rounds -boats -baker -orthodox -depression -formally -worn -naked -claire -muttered -sentence -11th -emily -document -77 -criticism -wished -vessel -spiritual -bent -virgin -parker -minimum -murray -lunch -danny -printed -compilation -keyboards -false -blow -belonged -68 -raising -78 -cutting -##board -pittsburgh -##up -9th -shadows -81 -hated -indigenous -jon -15th -barry -scholar -ah -##zer -oliver -##gy -stick -susan -meetings -attracted -spell -romantic -##ver -ye -1895 -photo -demanded -customers -##ac -1896 -logan -revival -keys -modified -commanded -jeans -##ious -upset -raw -phil -detective -hiding -resident -vincent -##bly -experiences -diamond -defeating -coverage -lucas -external -parks -franchise -helen -bible -successor -percussion -celebrated -il -lift -profile -clan -romania -##ied -mills -##su -nobody -achievement -shrugged -fault -1897 -rhythm -initiative -breakfast -carbon -700 -69 -lasted -violent -74 -wound -ken -killer -gradually -filmed -°c -dollars -processing -94 -remove -criticized -guests -sang -chemistry -##vin -legislature -disney -##bridge -uniform -escaped -integrated -proposal -purple -denied -liquid -karl -influential -morris -nights -stones -intense -experimental -twisted -71 -84 -##ld -pace -nazi -mitchell -ny -blind -reporter -newspapers -14th -centers -burn -basin -forgotten -surviving -filed -collections -monastery -losses -manual -couch -description -appropriate -merely -tag -missions -sebastian -restoration -replacing -triple -73 -elder -julia -warriors -benjamin -julian -convinced -stronger -amazing -declined -versus -merchant -happens -output -finland -bare -barbara -absence -ignored -dawn -injuries -##port -producers -##ram -82 -luis -##ities -kw -admit -expensive -electricity -nba -exception -symbol -##ving -ladies -shower -sheriff -characteristics -##je -aimed -button -ratio -effectively -summit -angle -jury -bears -foster -vessels -pants -executed -evans -dozen -advertising -kicked -patrol -1889 -competitions -lifetime -principles -athletics -##logy -birmingham -sponsored -89 -rob -nomination -1893 -acoustic -##sm -creature -longest -##tra -credits -harbor -dust -josh -##so -territories -milk -infrastructure -completion -thailand -indians -leon -archbishop -##sy -assist -pitch -blake -arrangement -girlfriend -serbian -operational -hence -sad -scent -fur -dj -sessions -hp -refer -rarely -##ora -exists -1892 -##ten -scientists -dirty -penalty -burst -portrait -seed -79 -pole -limits -rival -1894 -stable -alpha -grave -constitutional -alcohol -arrest -flower -mystery -devil -architectural -relationships -greatly -habitat -##istic -larry -progressive -remote -cotton -##ics -##ok -preserved -reaches -##ming -cited -86 -vast -scholarship -decisions -cbs -joy -teach -1885 -editions -knocked -eve -searching -partly -participation -gap -animated -fate -excellent -##ett -na -87 -alternate -saints -youngest -##ily -climbed -##ita -##tors -suggest -##ct -discussion -staying -choir -lakes -jacket -revenue -nevertheless -peaked -instrument -wondering -annually -managing -neil -1891 -signing -terry -##ice -apply -clinical -brooklyn -aim -catherine -fuck -farmers -figured -ninth -pride -hugh -evolution -ordinary -involvement -comfortable -shouted -tech -encouraged -taiwan -representation -sharing -##lia -##em -panic -exact -cargo -competing -fat -cried -83 -1920s -occasions -pa -cabin -borders -utah -marcus -##isation -badly -muscles -##ance -victorian -transition -warner -bet -permission -##rin -slave -terrible -similarly -shares -seth -uefa -possession -medals -benefits -colleges -lowered -perfectly -mall -transit -##ye -##kar -publisher -##ened -harrison -deaths -elevation -##ae -asleep -machines -sigh -ash -hardly -argument -occasion -parent -leo -decline -1888 -contribution -##ua -concentration -1000 -opportunities -hispanic -guardian -extent -emotions -hips -mason -volumes -bloody -controversy -diameter -steady -mistake -phoenix -identify -violin -##sk -departure -richmond -spin -funeral -enemies -1864 -gear -literally -connor -random -sergeant -grab -confusion -1865 -transmission -informed -op -leaning -sacred -suspended -thinks -gates -portland -luck -agencies -yours -hull -expert -muscle -layer -practical -sculpture -jerusalem -latest -lloyd -statistics -deeper -recommended -warrior -arkansas -mess -supports -greg -eagle -1880 -recovered -rated -concerts -rushed -##ano -stops -eggs -files -premiere -keith -##vo -delhi -turner -pit -affair -belief -paint -##zing -mate -##ach -##ev -victim -##ology -withdrew -bonus -styles -fled -##ud -glasgow -technologies -funded -nbc -adaptation -##ata -portrayed -cooperation -supporters -judges -bernard -justin -hallway -ralph -##ick -graduating -controversial -distant -continental -spider -bite -##ho -recognize -intention -mixing -##ese -egyptian -bow -tourism -suppose -claiming -tiger -dominated -participants -vi -##ru -nurse -partially -tape -##rum -psychology -##rn -essential -touring -duo -voting -civilian -emotional -channels -##king -apparent -hebrew -1887 -tommy -carrier -intersection -beast -hudson -##gar -##zo -lab -nova -bench -discuss -costa -##ered -detailed -behalf -drivers -unfortunately -obtain -##lis -rocky -##dae -siege -friendship -honey -##rian -1861 -amy -hang -posted -governments -collins -respond -wildlife -preferred -operator -##po -laura -pregnant -videos -dennis -suspected -boots -instantly -weird -automatic -businessman -alleged -placing -throwing -ph -mood -1862 -perry -venue -jet -remainder -##lli -##ci -passion -biological -boyfriend -1863 -dirt -buffalo -ron -segment -fa -abuse -##era -genre -thrown -stroke -colored -stress -exercise -displayed -##gen -struggled -##tti -abroad -dramatic -wonderful -thereafter -madrid -component -widespread -##sed -tale -citizen -todd -monday -1886 -vancouver -overseas -forcing -crying -descent -##ris -discussed -substantial -ranks -regime -1870 -provinces -switch -drum -zane -ted -tribes -proof -lp -cream -researchers -volunteer -manor -silk -milan -donated -allies -venture -principle -delivery -enterprise -##ves -##ans -bars -traditionally -witch -reminded -copper -##uk -pete -inter -links -colin -grinned -elsewhere -competitive -frequent -##oy -scream -##hu -tension -texts -submarine -finnish -defending -defend -pat -detail -1884 -affiliated -stuart -themes -villa -periods -tool -belgian -ruling -crimes -answers -folded -licensed -resort -demolished -hans -lucy -1881 -lion -traded -photographs -writes -craig -##fa -trials -generated -beth -noble -debt -percentage -yorkshire -erected -ss -viewed -grades -confidence -ceased -islam -telephone -retail -##ible -chile -m² -roberts -sixteen -##ich -commented -hampshire -innocent -dual -pounds -checked -regulations -afghanistan -sung -rico -liberty -assets -bigger -options -angels -relegated -tribute -wells -attending -leaf -##yan -butler -romanian -forum -monthly -lisa -patterns -gmina -##tory -madison -hurricane -rev -##ians -bristol -##ula -elite -valuable -disaster -democracy -awareness -germans -freyja -##ins -loop -absolutely -paying -populations -maine -sole -prayer -spencer -releases -doorway -bull -##ani -lover -midnight -conclusion -##sson -thirteen -lily -mediterranean -##lt -nhl -proud -sample -##hill -drummer -guinea -##ova -murphy -climb -##ston -instant -attributed -horn -ain -railways -steven -##ao -autumn -ferry -opponent -root -traveling -secured -corridor -stretched -tales -sheet -trinity -cattle -helps -indicates -manhattan -murdered -fitted -1882 -gentle -grandmother -mines -shocked -vegas -produces -##light -caribbean -##ou -belong -continuous -desperate -drunk -historically -trio -waved -raf -dealing -nathan -bat -murmured -interrupted -residing -scientist -pioneer -harold -aaron -##net -delta -attempting -minority -mini -believes -chorus -tend -lots -eyed -indoor -load -shots -updated -jail -##llo -concerning -connecting -wealth -##ved -slaves -arrive -rangers -sufficient -rebuilt -##wick -cardinal -flood -muhammad -whenever -relation -runners -moral -repair -viewers -arriving -revenge -punk -assisted -bath -fairly -breathe -lists -innings -illustrated -whisper -nearest -voters -clinton -ties -ultimate -screamed -beijing -lions -andre -fictional -gathering -comfort -radar -suitable -dismissed -hms -ban -pine -wrist -atmosphere -voivodeship -bid -timber -##ned -##nan -giants -##ane -cameron -recovery -uss -identical -categories -switched -serbia -laughter -noah -ensemble -therapy -peoples -touching -##off -locally -pearl -platforms -everywhere -ballet -tables -lanka -herbert -outdoor -toured -derek -1883 -spaces -contested -swept -1878 -exclusive -slight -connections -##dra -winds -prisoner -collective -bangladesh -tube -publicly -wealthy -thai -##ys -isolated -select -##ric -insisted -pen -fortune -ticket -spotted -reportedly -animation -enforcement -tanks -110 -decides -wider -lowest -owen -##time -nod -hitting -##hn -gregory -furthermore -magazines -fighters -solutions -##ery -pointing -requested -peru -reed -chancellor -knights -mask -worker -eldest -flames -reduction -1860 -volunteers -##tis -reporting -##hl -wire -advisory -endemic -origins -settlers -pursue -knock -consumer -1876 -eu -compound -creatures -mansion -sentenced -ivan -deployed -guitars -frowned -involves -mechanism -kilometers -perspective -shops -maps -terminus -duncan -alien -fist -bridges -##pers -heroes -fed -derby -swallowed -##ros -patent -sara -illness -characterized -adventures -slide -hawaii -jurisdiction -##op -organised -##side -adelaide -walks -biology -se -##ties -rogers -swing -tightly -boundaries -##rie -prepare -implementation -stolen -##sha -certified -colombia -edwards -garage -##mm -recalled -##ball -rage -harm -nigeria -breast -##ren -furniture -pupils -settle -##lus -cuba -balls -client -alaska -21st -linear -thrust -celebration -latino -genetic -terror -##cia -##ening -lightning -fee -witness -lodge -establishing -skull -##ique -earning -hood -##ei -rebellion -wang -sporting -warned -missile -devoted -activist -porch -worship -fourteen -package -1871 -decorated -##shire -housed -##ock -chess -sailed -doctors -oscar -joan -treat -garcia -harbour -jeremy -##ire -traditions -dominant -jacques -##gon -##wan -relocated -1879 -amendment -sized -companion -simultaneously -volleyball -spun -acre -increases -stopping -loves -belongs -affect -drafted -tossed -scout -battles -1875 -filming -shoved -munich -tenure -vertical -romance -pc -##cher -argue -##ical -craft -ranging -www -opens -honest -tyler -yesterday -virtual -##let -muslims -reveal -snake -immigrants -radical -screaming -speakers -firing -saving -belonging -ease -lighting -prefecture -blame -farmer -hungry -grows -rubbed -beam -sur -subsidiary -##cha -armenian -sao -dropping -conventional -##fer -microsoft -reply -qualify -spots -1867 -sweat -festivals -##ken -immigration -physician -discover -exposure -sandy -explanation -isaac -implemented -##fish -hart -initiated -connect -stakes -presents -heights -householder -pleased -tourist -regardless -slip -closest -##ction -surely -sultan -brings -riley -preparation -aboard -slammed -baptist -experiment -ongoing -interstate -organic -playoffs -##ika -1877 -130 -##tar -hindu -error -tours -tier -plenty -arrangements -talks -trapped -excited -sank -ho -athens -1872 -denver -welfare -suburb -athletes -trick -diverse -belly -exclusively -yelled -1868 -##med -conversion -##ette -1874 -internationally -computers -conductor -abilities -sensitive -hello -dispute -measured -globe -rocket -prices -amsterdam -flights -tigers -inn -municipalities -emotion -references -3d -##mus -explains -airlines -manufactured -pm -archaeological -1873 -interpretation -devon -comment -##ites -settlements -kissing -absolute -improvement -suite -impressed -barcelona -sullivan -jefferson -towers -jesse -julie -##tin -##lu -grandson -hi -gauge -regard -rings -interviews -trace -raymond -thumb -departments -burns -serial -bulgarian -scores -demonstrated -##ix -1866 -kyle -alberta -underneath -romanized -##ward -relieved -acquisition -phrase -cliff -reveals -han -cuts -merger -custom -##dar -nee -gilbert -graduation -##nts -assessment -cafe -difficulty -demands -swung -democrat -jennifer -commons -1940s -grove -##yo -completing -focuses -sum -substitute -bearing -stretch -reception -##py -reflected -essentially -destination -pairs -##ched -survival -resource -##bach -promoting -doubles -messages -tear -##down -##fully -parade -florence -harvey -incumbent -partial -framework -900 -pedro -frozen -procedure -olivia -controls -##mic -shelter -personally -temperatures -##od -brisbane -tested -sits -marble -comprehensive -oxygen -leonard -##kov -inaugural -iranian -referring -quarters -attitude -##ivity -mainstream -lined -mars -dakota -norfolk -unsuccessful -##° -explosion -helicopter -congressional -##sing -inspector -bitch -seal -departed -divine -##ters -coaching -examination -punishment -manufacturer -sink -columns -unincorporated -signals -nevada -squeezed -dylan -dining -photos -martial -manuel -eighteen -elevator -brushed -plates -ministers -ivy -congregation -##len -slept -specialized -taxes -curve -restricted -negotiations -likes -statistical -arnold -inspiration -execution -bold -intermediate -significance -margin -ruler -wheels -gothic -intellectual -dependent -listened -eligible -buses -widow -syria -earn -cincinnati -collapsed -recipient -secrets -accessible -philippine -maritime -goddess -clerk -surrender -breaks -playoff -database -##ified -##lon -ideal -beetle -aspect -soap -regulation -strings -expand -anglo -shorter -crosses -retreat -tough -coins -wallace -directions -pressing -##oon -shipping -locomotives -comparison -topics -nephew -##mes -distinction -honors -travelled -sierra -ibn -##over -fortress -sa -recognised -carved -1869 -clients -##dan -intent -##mar -coaches -describing -bread -##ington -beaten -northwestern -##ona -merit -youtube -collapse -challenges -em -historians -objective -submitted -virus -attacking -drake -assume -##ere -diseases -marc -stem -leeds -##cus -##ab -farming -glasses -##lock -visits -nowhere -fellowship -relevant -carries -restaurants -experiments -101 -constantly -bases -targets -shah -tenth -opponents -verse -territorial -##ira -writings -corruption -##hs -instruction -inherited -reverse -emphasis -##vic -employee -arch -keeps -rabbi -watson -payment -uh -##ala -nancy -##tre -venice -fastest -sexy -banned -adrian -properly -ruth -touchdown -dollar -boards -metre -circles -edges -favour -comments -ok -travels -liberation -scattered -firmly -##ular -holland -permitted -diesel -kenya -den -originated -##ral -demons -resumed -dragged -rider -##rus -servant -blinked -extend -torn -##ias -##sey -input -meal -everybody -cylinder -kinds -camps -##fe -bullet -logic -##wn -croatian -evolved -healthy -fool -chocolate -wise -preserve -pradesh -##ess -respective -1850 -##ew -chicken -artificial -gross -corresponding -convicted -cage -caroline -dialogue -##dor -narrative -stranger -mario -br -christianity -failing -trent -commanding -buddhist -1848 -maurice -focusing -yale -bike -altitude -##ering -mouse -revised -##sley -veteran -##ig -pulls -theology -crashed -campaigns -legion -##ability -drag -excellence -customer -cancelled -intensity -excuse -##lar -liga -participating -contributing -printing -##burn -variable -##rk -curious -bin -legacy -renaissance -##my -symptoms -binding -vocalist -dancer -##nie -grammar -gospel -democrats -ya -enters -sc -diplomatic -hitler -##ser -clouds -mathematical -quit -defended -oriented -##heim -fundamental -hardware -impressive -equally -convince -confederate -guilt -chuck -sliding -##ware -magnetic -narrowed -petersburg -bulgaria -otto -phd -skill -##ama -reader -hopes -pitcher -reservoir -hearts -automatically -expecting -mysterious -bennett -extensively -imagined -seeds -monitor -fix -##ative -journalism -struggling -signature -ranch -encounter -photographer -observation -protests -##pin -influences -##hr -calendar -##all -cruz -croatia -locomotive -hughes -naturally -shakespeare -basement -hook -uncredited -faded -theories -approaches -dare -phillips -filling -fury -obama -##ain -efficient -arc -deliver -min -raid -breeding -inducted -leagues -efficiency -axis -montana -eagles -##ked -supplied -instructions -karen -picking -indicating -trap -anchor -practically -christians -tomb -vary -occasional -electronics -lords -readers -newcastle -faint -innovation -collect -situations -engagement -160 -claude -mixture -##feld -peer -tissue -logo -lean -##ration -°f -floors -##ven -architects -reducing -##our -##ments -rope -1859 -ottawa -##har -samples -banking -declaration -proteins -resignation -francois -saudi -advocate -exhibited -armor -twins -divorce -##ras -abraham -reviewed -jo -temporarily -matrix -physically -pulse -curled -##ena -difficulties -bengal -usage -##ban -annie -riders -certificate -##pi -holes -warsaw -distinctive -jessica -##mon -mutual -1857 -customs -circular -eugene -removal -loaded -mere -vulnerable -depicted -generations -dame -heir -enormous -lightly -climbing -pitched -lessons -pilots -nepal -ram -google -preparing -brad -louise -renowned -##₂ -liam -##ably -plaza -shaw -sophie -brilliant -bills -##bar -##nik -fucking -mainland -server -pleasant -seized -veterans -jerked -fail -beta -brush -radiation -stored -warmth -southeastern -nate -sin -raced -berkeley -joke -athlete -designation -trunk -##low -roland -qualification -archives -heels -artwork -receives -judicial -reserves -##bed -woke -installation -abu -floating -fake -lesser -excitement -interface -concentrated -addressed -characteristic -amanda -saxophone -monk -auto -##bus -releasing -egg -dies -interaction -defender -ce -outbreak -glory -loving -##bert -sequel -consciousness -http -awake -ski -enrolled -##ress -handling -rookie -brow -somebody -biography -warfare -amounts -contracts -presentation -fabric -dissolved -challenged -meter -psychological -lt -elevated -rally -accurate -##tha -hospitals -undergraduate -specialist -venezuela -exhibit -shed -nursing -protestant -fluid -structural -footage -jared -consistent -prey -##ska -succession -reflect -exile -lebanon -wiped -suspect -shanghai -resting -integration -preservation -marvel -variant -pirates -sheep -rounded -capita -sailing -colonies -manuscript -deemed -variations -clarke -functional -emerging -boxing -relaxed -curse -azerbaijan -heavyweight -nickname -editorial -rang -grid -tightened -earthquake -flashed -miguel -rushing -##ches -improvements -boxes -brooks -180 -consumption -molecular -felix -societies -repeatedly -variation -aids -civic -graphics -professionals -realm -autonomous -receiver -delayed -workshop -militia -chairs -trump -canyon -##point -harsh -extending -lovely -happiness -##jan -stake -eyebrows -embassy -wellington -hannah -##ella -sony -corners -bishops -swear -cloth -contents -xi -namely -commenced -1854 -stanford -nashville -courage -graphic -commitment -garrison -##bin -hamlet -clearing -rebels -attraction -literacy -cooking -ruins -temples -jenny -humanity -celebrate -hasn -freight -sixty -rebel -bastard -##art -newton -##ada -deer -##ges -##ching -smiles -delaware -singers -##ets -approaching -assists -flame -##ph -boulevard -barrel -planted -##ome -pursuit -##sia -consequences -posts -shallow -invitation -rode -depot -ernest -kane -rod -concepts -preston -topic -chambers -striking -blast -arrives -descendants -montgomery -ranges -worlds -##lay -##ari -span -chaos -praise -##ag -fewer -1855 -sanctuary -mud -fbi -##ions -programmes -maintaining -unity -harper -bore -handsome -closure -tournaments -thunder -nebraska -linda -facade -puts -satisfied -argentine -dale -cork -dome -panama -##yl -1858 -tasks -experts -##ates -feeding -equation -##las -##ida -##tu -engage -bryan -##ax -um -quartet -melody -disbanded -sheffield -blocked -gasped -delay -kisses -maggie -connects -##non -sts -poured -creator -publishers -##we -guided -ellis -extinct -hug -gaining -##ord -complicated -##bility -poll -clenched -investigate -##use -thereby -quantum -spine -cdp -humor -kills -administered -semifinals -##du -encountered -ignore -##bu -commentary -##maker -bother -roosevelt -140 -plains -halfway -flowing -cultures -crack -imprisoned -neighboring -airline -##ses -##view -##mate -##ec -gather -wolves -marathon -transformed -##ill -cruise -organisations -carol -punch -exhibitions -numbered -alarm -ratings -daddy -silently -##stein -queens -colours -impression -guidance -liu -tactical -##rat -marshal -della -arrow -##ings -rested -feared -tender -owns -bitter -advisor -escort -##ides -spare -farms -grants -##ene -dragons -encourage -colleagues -cameras -##und -sucked -pile -spirits -prague -statements -suspension -landmark -fence -torture -recreation -bags -permanently -survivors -pond -spy -predecessor -bombing -coup -##og -protecting -transformation -glow -##lands -##book -dug -priests -andrea -feat -barn -jumping -##chen -##ologist -##con -casualties -stern -auckland -pipe -serie -revealing -ba -##bel -trevor -mercy -spectrum -yang -consist -governing -collaborated -possessed -epic -comprises -blew -shane -##ack -lopez -honored -magical -sacrifice -judgment -perceived -hammer -mtv -baronet -tune -das -missionary -sheets -350 -neutral -oral -threatening -attractive -shade -aims -seminary -##master -estates -1856 -michel -wounds -refugees -manufacturers -##nic -mercury -syndrome -porter -##iya -##din -hamburg -identification -upstairs -purse -widened -pause -cared -breathed -affiliate -santiago -prevented -celtic -fisher -125 -recruited -byzantine -reconstruction -farther -##mp -diet -sake -au -spite -sensation -##ert -blank -separation -105 -##hon -vladimir -armies -anime -##lie -accommodate -orbit -cult -sofia -archive -##ify -##box -founders -sustained -disorder -honours -northeastern -mia -crops -violet -threats -blanket -fires -canton -followers -southwestern -prototype -voyage -assignment -altered -moderate -protocol -pistol -##eo -questioned -brass -lifting -1852 -math -authored -##ual -doug -dimensional -dynamic -##san -1851 -pronounced -grateful -quest -uncomfortable -boom -presidency -stevens -relating -politicians -chen -barrier -quinn -diana -mosque -tribal -cheese -palmer -portions -sometime -chester -treasure -wu -bend -download -millions -reforms -registration -##osa -consequently -monitoring -ate -preliminary -brandon -invented -ps -eaten -exterior -intervention -ports -documented -log -displays -lecture -sally -favourite -##itz -vermont -lo -invisible -isle -breed -##ator -journalists -relay -speaks -backward -explore -midfielder -actively -stefan -procedures -cannon -blond -kenneth -centered -servants -chains -libraries -malcolm -essex -henri -slavery -##hal -facts -fairy -coached -cassie -cats -washed -cop -##fi -announcement -item -2000s -vinyl -activated -marco -frontier -growled -curriculum -##das -loyal -accomplished -leslie -ritual -kenny -##00 -vii -napoleon -hollow -hybrid -jungle -stationed -friedrich -counted -##ulated -platinum -theatrical -seated -col -rubber -glen -1840 -diversity -healing -extends -id -provisions -administrator -columbus -##oe -tributary -te -assured -org -##uous -prestigious -examined -lectures -grammy -ronald -associations -bailey -allan -essays -flute -believing -consultant -proceedings -travelling -1853 -kit -kerala -yugoslavia -buddy -methodist -##ith -burial -centres -batman -##nda -discontinued -bo -dock -stockholm -lungs -severely -##nk -citing -manga -##ugh -steal -mumbai -iraqi -robot -celebrity -bride -broadcasts -abolished -pot -joel -overhead -franz -packed -reconnaissance -johann -acknowledged -introduce -handled -doctorate -developments -drinks -alley -palestine -##nis -##aki -proceeded -recover -bradley -grain -patch -afford -infection -nationalist -legendary -##ath -interchange -virtually -gen -gravity -exploration -amber -vital -wishes -powell -doctrine -elbow -screenplay -##bird -contribute -indonesian -pet -creates -##com -enzyme -kylie -discipline -drops -manila -hunger -##ien -layers -suffer -fever -bits -monica -keyboard -manages -##hood -searched -appeals -##bad -testament -grande -reid -##war -beliefs -congo -##ification -##dia -si -requiring -##via -casey -1849 -regret -streak -rape -depends -syrian -sprint -pound -tourists -upcoming -pub -##xi -tense -##els -practiced -echo -nationwide -guild -motorcycle -liz -##zar -chiefs -desired -elena -bye -precious -absorbed -relatives -booth -pianist -##mal -citizenship -exhausted -wilhelm -##ceae -##hed -noting -quarterback -urge -hectares -##gue -ace -holly -##tal -blonde -davies -parked -sustainable -stepping -twentieth -airfield -galaxy -nest -chip -##nell -tan -shaft -paulo -requirement -##zy -paradise -tobacco -trans -renewed -vietnamese -##cker -##ju -suggesting -catching -holmes -enjoying -md -trips -colt -holder -butterfly -nerve -reformed -cherry -bowling -trailer -carriage -goodbye -appreciate -toy -joshua -interactive -enabled -involve -##kan -collar -determination -bunch -facebook -recall -shorts -superintendent -episcopal -frustration -giovanni -nineteenth -laser -privately -array -circulation -##ovic -armstrong -deals -painful -permit -discrimination -##wi -aires -retiring -cottage -ni -##sta -horizon -ellen -jamaica -ripped -fernando -chapters -playstation -patron -lecturer -navigation -behaviour -genes -georgian -export -solomon -rivals -swift -seventeen -rodriguez -princeton -independently -sox -1847 -arguing -entity -casting -hank -criteria -oakland -geographic -milwaukee -reflection -expanding -conquest -dubbed -##tv -halt -brave -brunswick -doi -arched -curtis -divorced -predominantly -somerset -streams -ugly -zoo -horrible -curved -buenos -fierce -dictionary -vector -theological -unions -handful -stability -chan -punjab -segments -##lly -altar -ignoring -gesture -monsters -pastor -##stone -thighs -unexpected -operators -abruptly -coin -compiled -associates -improving -migration -pin -##ose -compact -collegiate -reserved -##urs -quarterfinals -roster -restore -assembled -hurry -oval -##cies -1846 -flags -martha -##del -victories -sharply -##rated -argues -deadly -neo -drawings -symbols -performer -##iel -griffin -restrictions -editing -andrews -java -journals -arabia -compositions -dee -pierce -removing -hindi -casino -runway -civilians -minds -nasa -hotels -##zation -refuge -rent -retain -potentially -conferences -suburban -conducting -##tto -##tions -##tle -descended -massacre -##cal -ammunition -terrain -fork -souls -counts -chelsea -durham -drives -cab -##bank -perth -realizing -palestinian -finn -simpson -##dal -betty -##ule -moreover -particles -cardinals -tent -evaluation -extraordinary -##oid -inscription -##works -wednesday -chloe -maintains -panels -ashley -trucks -##nation -cluster -sunlight -strikes -zhang -##wing -dialect -canon -##ap -tucked -##ws -collecting -##mas -##can -##sville -maker -quoted -evan -franco -aria -buying -cleaning -eva -closet -provision -apollo -clinic -rat -##ez -necessarily -ac -##gle -##ising -venues -flipped -cent -spreading -trustees -checking -authorized -##sco -disappointed -##ado -notion -duration -trumpet -hesitated -topped -brussels -rolls -theoretical -hint -define -aggressive -repeat -wash -peaceful -optical -width -allegedly -mcdonald -strict -copyright -##illa -investors -mar -jam -witnesses -sounding -miranda -michelle -privacy -hugo -harmony -##pp -valid -lynn -glared -nina -102 -headquartered -diving -boarding -gibson -##ncy -albanian -marsh -routine -dealt -enhanced -er -intelligent -substance -targeted -enlisted -discovers -spinning -observations -pissed -smoking -rebecca -capitol -visa -varied -costume -seemingly -indies -compensation -surgeon -thursday -arsenal -westminster -suburbs -rid -anglican -##ridge -knots -foods -alumni -lighter -fraser -whoever -portal -scandal -##ray -gavin -advised -instructor -flooding -terrorist -##ale -teenage -interim -senses -duck -teen -thesis -abby -eager -overcome -##ile -newport -glenn -rises -shame -##cc -prompted -priority -forgot -bomber -nicolas -protective -360 -cartoon -katherine -breeze -lonely -trusted -henderson -richardson -relax -banner -candy -palms -remarkable -##rio -legends -cricketer -essay -ordained -edmund -rifles -trigger -##uri -##away -sail -alert -1830 -audiences -penn -sussex -siblings -pursued -indianapolis -resist -rosa -consequence -succeed -avoided -1845 -##ulation -inland -##tie -##nna -counsel -profession -chronicle -hurried -##una -eyebrow -eventual -bleeding -innovative -cure -##dom -committees -accounting -con -scope -hardy -heather -tenor -gut -herald -codes -tore -scales -wagon -##oo -luxury -tin -prefer -fountain -triangle -bonds -darling -convoy -dried -traced -beings -troy -accidentally -slam -findings -smelled -joey -lawyers -outcome -steep -bosnia -configuration -shifting -toll -brook -performers -lobby -philosophical -construct -shrine -aggregate -boot -cox -phenomenon -savage -insane -solely -reynolds -lifestyle -##ima -nationally -holdings -consideration -enable -edgar -mo -mama -##tein -fights -relegation -chances -atomic -hub -conjunction -awkward -reactions -currency -finale -kumar -underwent -steering -elaborate -gifts -comprising -melissa -veins -reasonable -sunshine -chi -solve -trails -inhabited -elimination -ethics -huh -ana -molly -consent -apartments -layout -marines -##ces -hunters -bulk -##oma -hometown -##wall -##mont -cracked -reads -neighbouring -withdrawn -admission -wingspan -damned -anthology -lancashire -brands -batting -forgive -cuban -awful -##lyn -104 -dimensions -imagination -##ade -dante -##ship -tracking -desperately -goalkeeper -##yne -groaned -workshops -confident -burton -gerald -milton -circus -uncertain -slope -copenhagen -sophia -fog -philosopher -portraits -accent -cycling -varying -gripped -larvae -garrett -specified -scotia -mature -luther -kurt -rap -##kes -aerial -750 -ferdinand -heated -es -transported -##shan -safely -nonetheless -##orn -##gal -motors -demanding -##sburg -startled -##brook -ally -generate -caps -ghana -stained -demo -mentions -beds -ap -afterward -diary -##bling -utility -##iro -richards -1837 -conspiracy -conscious -shining -footsteps -observer -cyprus -urged -loyalty -developer -probability -olive -upgraded -gym -miracle -insects -graves -1844 -ourselves -hydrogen -amazon -katie -tickets -poets -##pm -planes -##pan -prevention -witnessed -dense -jin -randy -tang -warehouse -monroe -bang -archived -elderly -investigations -alec -granite -mineral -conflicts -controlling -aboriginal -carlo -##zu -mechanics -stan -stark -rhode -skirt -est -##berry -bombs -respected -##horn -imposed -limestone -deny -nominee -memphis -grabbing -disabled -##als -amusement -aa -frankfurt -corn -referendum -varies -slowed -disk -firms -unconscious -incredible -clue -sue -##zhou -twist -##cio -joins -idaho -chad -developers -computing -destroyer -103 -mortal -tucker -kingston -choices -yu -carson -1800 -os -whitney -geneva -pretend -dimension -staged -plateau -maya -##une -freestyle -##bc -rovers -hiv -##ids -tristan -classroom -prospect -##hus -honestly -diploma -lied -thermal -auxiliary -feast -unlikely -iata -##tel -morocco -pounding -treasury -lithuania -considerably -1841 -dish -1812 -geological -matching -stumbled -destroying -marched -brien -advances -cake -nicole -belle -settling -measuring -directing -##mie -tuesday -bassist -capabilities -stunned -fraud -torpedo -##list -##phone -anton -wisdom -surveillance -ruined -##ulate -lawsuit -healthcare -theorem -halls -trend -aka -horizontal -dozens -acquire -lasting -swim -hawk -gorgeous -fees -vicinity -decrease -adoption -tactics -##ography -pakistani -##ole -draws -##hall -willie -burke -heath -algorithm -integral -powder -elliott -brigadier -jackie -tate -varieties -darker -##cho -lately -cigarette -specimens -adds -##ree -##ensis -##inger -exploded -finalist -cia -murders -wilderness -arguments -nicknamed -acceptance -onwards -manufacture -robertson -jets -tampa -enterprises -blog -loudly -composers -nominations -1838 -ai -malta -inquiry -automobile -hosting -viii -rays -tilted -grief -museums -strategies -furious -euro -equality -cohen -poison -surrey -wireless -governed -ridiculous -moses -##esh -##room -vanished -##ito -barnes -attract -morrison -istanbul -##iness -absent -rotation -petition -janet -##logical -satisfaction -custody -deliberately -observatory -comedian -surfaces -pinyin -novelist -strictly -canterbury -oslo -monks -embrace -ibm -jealous -photograph -continent -dorothy -marina -doc -excess -holden -allegations -explaining -stack -avoiding -lance -storyline -majesty -poorly -spike -dos -bradford -raven -travis -classics -proven -voltage -pillow -fists -butt -1842 -interpreted -##car -1839 -gage -telegraph -lens -promising -expelled -casual -collector -zones -##min -silly -nintendo -##kh -##bra -downstairs -chef -suspicious -afl -flies -vacant -uganda -pregnancy -condemned -lutheran -estimates -cheap -decree -saxon -proximity -stripped -idiot -deposits -contrary -presenter -magnus -glacier -im -offense -edwin -##ori -upright -##long -bolt -##ois -toss -geographical -##izes -environments -delicate -marking -abstract -xavier -nails -windsor -plantation -occurring -equity -saskatchewan -fears -drifted -sequences -vegetation -revolt -##stic -1843 -sooner -fusion -opposing -nato -skating -1836 -secretly -ruin -lease -##oc -edit -##nne -flora -anxiety -ruby -##ological -##mia -tel -bout -taxi -emmy -frost -rainbow -compounds -foundations -rainfall -assassination -nightmare -dominican -##win -achievements -deserve -orlando -intact -armenia -##nte -calgary -valentine -106 -marion -proclaimed -theodore -bells -courtyard -thigh -gonzalez -console -troop -minimal -monte -everyday -##ence -##if -supporter -terrorism -buck -openly -presbyterian -activists -carpet -##iers -rubbing -uprising -##yi -cute -conceived -legally -##cht -millennium -cello -velocity -ji -rescued -cardiff -1835 -rex -concentrate -senators -beard -rendered -glowing -battalions -scouts -competitors -sculptor -catalogue -arctic -ion -raja -bicycle -wow -glancing -lawn -##woman -gentleman -lighthouse -publish -predicted -calculated -##val -variants -##gne -strain -##ui -winston -deceased -##nus -touchdowns -brady -caleb -sinking -echoed -crush -hon -blessed -protagonist -hayes -endangered -magnitude -editors -##tine -estimate -responsibilities -##mel -backup -laying -consumed -sealed -zurich -lovers -frustrated -##eau -ahmed -kicking -mit -treasurer -1832 -biblical -refuse -terrified -pump -agrees -genuine -imprisonment -refuses -plymouth -##hen -lou -##nen -tara -trembling -antarctic -ton -learns -##tas -crap -crucial -faction -atop -##borough -wrap -lancaster -odds -hopkins -erik -lyon -##eon -bros -##ode -snap -locality -tips -empress -crowned -cal -acclaimed -chuckled -##ory -clara -sends -mild -towel -##fl -##day -##а -wishing -assuming -interviewed -##bal -##die -interactions -eden -cups -helena -##lf -indie -beck -##fire -batteries -filipino -wizard -parted -##lam -traces -##born -rows -idol -albany -delegates -##ees -##sar -discussions -##ex -notre -instructed -belgrade -highways -suggestion -lauren -possess -orientation -alexandria -abdul -beats -salary -reunion -ludwig -alright -wagner -intimate -pockets -slovenia -hugged -brighton -merchants -cruel -stole -trek -slopes -repairs -enrollment -politically -underlying -promotional -counting -boeing -##bb -isabella -naming -##и -keen -bacteria -listing -separately -belfast -ussr -450 -lithuanian -anybody -ribs -sphere -martinez -cock -embarrassed -proposals -fragments -nationals -##fs -##wski -premises -fin -1500 -alpine -matched -freely -bounded -jace -sleeve -##af -gaming -pier -populated -evident -##like -frances -flooded -##dle -frightened -pour -trainer -framed -visitor -challenging -pig -wickets -##fold -infected -email -##pes -arose -##aw -reward -ecuador -oblast -vale -ch -shuttle -##usa -bach -rankings -forbidden -cornwall -accordance -salem -consumers -bruno -fantastic -toes -machinery -resolved -julius -remembering -propaganda -iceland -bombardment -tide -contacts -wives -##rah -concerto -macdonald -albania -implement -daisy -tapped -sudan -helmet -angela -mistress -##lic -crop -sunk -finest -##craft -hostile -##ute -##tsu -boxer -fr -paths -adjusted -habit -ballot -supervision -soprano -##zen -bullets -wicked -sunset -regiments -disappear -lamp -performs -app -##gia -##oa -rabbit -digging -incidents -entries -##cion -dishes -##oi -introducing -##ati -##fied -freshman -slot -jill -tackles -baroque -backs -##iest -lone -sponsor -destiny -altogether -convert -##aro -consensus -shapes -demonstration -basically -feminist -auction -artifacts -##bing -strongest -twitter -halifax -2019 -allmusic -mighty -smallest -precise -alexandra -viola -##los -##ille -manuscripts -##illo -dancers -ari -managers -monuments -blades -barracks -springfield -maiden -consolidated -electron -##end -berry -airing -wheat -nobel -inclusion -blair -payments -geography -bee -cc -eleanor -react -##hurst -afc -manitoba -##yu -su -lineup -fitness -recreational -investments -airborne -disappointment -##dis -edmonton -viewing -##row -renovation -##cast -infant -bankruptcy -roses -aftermath -pavilion -##yer -carpenter -withdrawal -ladder -##hy -discussing -popped -reliable -agreements -rochester -##abad -curves -bombers -220 -rao -reverend -decreased -choosing -107 -stiff -consulting -naples -crawford -tracy -ka -ribbon -cops -##lee -crushed -deciding -unified -teenager -accepting -flagship -explorer -poles -sanchez -inspection -revived -skilled -induced -exchanged -flee -locals -tragedy -swallow -loading -hanna -demonstrate -##ela -salvador -flown -contestants -civilization -##ines -wanna -rhodes -fletcher -hector -knocking -considers -##ough -nash -mechanisms -sensed -mentally -walt -unclear -##eus -renovated -madame -##cks -crews -governmental -##hin -undertaken -monkey -##ben -##ato -fatal -armored -copa -caves -governance -grasp -perception -certification -froze -damp -tugged -wyoming -##rg -##ero -newman -##lor -nerves -curiosity -graph -115 -##ami -withdraw -tunnels -dull -meredith -moss -exhibits -neighbors -communicate -accuracy -explored -raiders -republicans -secular -kat -superman -penny -criticised -##tch -freed -update -conviction -wade -ham -likewise -delegation -gotta -doll -promises -technological -myth -nationality -resolve -convent -##mark -sharon -dig -sip -coordinator -entrepreneur -fold -##dine -capability -councillor -synonym -blown -swan -cursed -1815 -jonas -haired -sofa -canvas -keeper -rivalry -##hart -rapper -speedway -swords -postal -maxwell -estonia -potter -recurring -##nn -##ave -errors -##oni -cognitive -1834 -##² -claws -nadu -roberto -bce -wrestler -ellie -##ations -infinite -ink -##tia -presumably -finite -staircase -108 -noel -patricia -nacional -##cation -chill -eternal -tu -preventing -prussia -fossil -limbs -##logist -ernst -frog -perez -rene -##ace -pizza -prussian -##ios -##vy -molecules -regulatory -answering -opinions -sworn -lengths -supposedly -hypothesis -upward -habitats -seating -ancestors -drank -yield -hd -synthesis -researcher -modest -##var -mothers -peered -voluntary -homeland -##the -acclaim -##igan -static -valve -luxembourg -alto -carroll -fe -receptor -norton -ambulance -##tian -johnston -catholics -depicting -jointly -elephant -gloria -mentor -badge -ahmad -distinguish -remarked -councils -precisely -allison -advancing -detection -crowded -##10 -cooperative -ankle -mercedes -dagger -surrendered -pollution -commit -subway -jeffrey -lesson -sculptures -provider -##fication -membrane -timothy -rectangular -fiscal -heating -teammate -basket -particle -anonymous -deployment -##ple -missiles -courthouse -proportion -shoe -sec -##ller -complaints -forbes -blacks -abandon -remind -sizes -overwhelming -autobiography -natalie -##awa -risks -contestant -countryside -babies -scorer -invaded -enclosed -proceed -hurling -disorders -##cu -reflecting -continuously -cruiser -graduates -freeway -investigated -ore -deserved -maid -blocking -phillip -jorge -shakes -dove -mann -variables -lacked -burden -accompanying -que -consistently -organizing -provisional -complained -endless -##rm -tubes -juice -georges -krishna -mick -labels -thriller -##uch -laps -arcade -sage -snail -##table -shannon -fi -laurence -seoul -vacation -presenting -hire -churchill -surprisingly -prohibited -savannah -technically -##oli -170 -##lessly -testimony -suited -speeds -toys -romans -mlb -flowering -measurement -talented -kay -settings -charleston -expectations -shattered -achieving -triumph -ceremonies -portsmouth -lanes -mandatory -loser -stretching -cologne -realizes -seventy -cornell -careers -webb -##ulating -americas -budapest -ava -suspicion -##ison -yo -conrad -##hai -sterling -jessie -rector -##az -1831 -transform -organize -loans -christine -volcanic -warrant -slender -summers -subfamily -newer -danced -dynamics -rhine -proceeds -heinrich -gastropod -commands -sings -facilitate -easter -ra -positioned -responses -expense -fruits -yanked -imported -25th -velvet -vic -primitive -tribune -baldwin -neighbourhood -donna -rip -hay -pr -##uro -1814 -espn -welcomed -##aria -qualifier -glare -highland -timing -##cted -shells -eased -geometry -louder -exciting -slovakia -##sion -##iz -##lot -savings -prairie -##ques -marching -rafael -tonnes -##lled -curtain -preceding -shy -heal -greene -worthy -##pot -detachment -bury -sherman -##eck -reinforced -seeks -bottles -contracted -duchess -outfit -walsh -##sc -mickey -##ase -geoffrey -archer -squeeze -dawson -eliminate -invention -##enberg -neal -##eth -stance -dealer -coral -maple -retire -polo -simplified -##ht -1833 -hid -watts -backwards -jules -##oke -genesis -mt -frames -rebounds -burma -woodland -moist -santos -whispers -drained -subspecies -##aa -streaming -ulster -burnt -correspondence -maternal -gerard -denis -stealing -##load -genius -duchy -##oria -inaugurated -momentum -suits -placement -sovereign -clause -thames -##hara -confederation -reservation -sketch -yankees -lets -rotten -charm -hal -verses -ultra -commercially -dot -salon -citation -adopt -winnipeg -mist -allocated -cairo -##boy -jenkins -interference -objectives -##wind -1820 -portfolio -armoured -sectors -##eh -initiatives -##world -integrity -exercises -robe -tap -ab -gazed -##tones -distracted -rulers -111 -favorable -jerome -tended -cart -factories -##eri -diplomat -valued -gravel -charitable -##try -calvin -exploring -chang -shepherd -terrace -pdf -pupil -##ural -reflects -ups -##rch -governors -shelf -depths -##nberg -trailed -crest -tackle -##nian -##ats -hatred -##kai -clare -makers -ethiopia -longtime -detected -embedded -lacking -slapped -rely -thomson -anticipation -iso -morton -successive -agnes -screenwriter -straightened -philippe -playwright -haunted -licence -iris -intentions -sutton -112 -logical -correctly -##weight -branded -licked -tipped -silva -ricky -narrator -requests -##ents -greeted -supernatural -cow -##wald -lung -refusing -employer -strait -gaelic -liner -##piece -zoe -sabha -##mba -driveway -harvest -prints -bates -reluctantly -threshold -algebra -ira -wherever -coupled -240 -assumption -picks -##air -designers -raids -gentlemen -##ean -roller -blowing -leipzig -locks -screw -dressing -strand -##lings -scar -dwarf -depicts -##nu -nods -##mine -differ -boris -##eur -yuan -flip -##gie -mob -invested -questioning -applying -##ture -shout -##sel -gameplay -blamed -illustrations -bothered -weakness -rehabilitation -##of -##zes -envelope -rumors -miners -leicester -subtle -kerry -##ico -ferguson -##fu -premiership -ne -##cat -bengali -prof -catches -remnants -dana -##rily -shouting -presidents -baltic -ought -ghosts -dances -sailors -shirley -fancy -dominic -##bie -madonna -##rick -bark -buttons -gymnasium -ashes -liver -toby -oath -providence -doyle -evangelical -nixon -cement -carnegie -embarked -hatch -surroundings -guarantee -needing -pirate -essence -##bee -filter -crane -hammond -projected -immune -percy -twelfth -##ult -regent -doctoral -damon -mikhail -##ichi -lu -critically -elect -realised -abortion -acute -screening -mythology -steadily -##fc -frown -nottingham -kirk -wa -minneapolis -##rra -module -algeria -mc -nautical -encounters -surprising -statues -availability -shirts -pie -alma -brows -munster -mack -soup -crater -tornado -sanskrit -cedar -explosive -bordered -dixon -planets -stamp -exam -happily -##bble -carriers -kidnapped -##vis -accommodation -emigrated -##met -knockout -correspondent -violation -profits -peaks -lang -specimen -agenda -ancestry -pottery -spelling -equations -obtaining -ki -linking -1825 -debris -asylum -##20 -buddhism -teddy -##ants -gazette -##nger -##sse -dental -eligibility -utc -fathers -averaged -zimbabwe -francesco -coloured -hissed -translator -lynch -mandate -humanities -mackenzie -uniforms -lin -##iana -##gio -asset -mhz -fitting -samantha -genera -wei -rim -beloved -shark -riot -entities -expressions -indo -carmen -slipping -owing -abbot -neighbor -sidney -##av -rats -recommendations -encouraging -squadrons -anticipated -commanders -conquered -##oto -donations -diagnosed -##mond -divide -##iva -guessed -decoration -vernon -auditorium -revelation -conversations -##kers -##power -herzegovina -dash -alike -protested -lateral -herman -accredited -mg -##gent -freeman -mel -fiji -crow -crimson -##rine -livestock -##pped -humanitarian -bored -oz -whip -##lene -##ali -legitimate -alter -grinning -spelled -anxious -oriental -wesley -##nin -##hole -carnival -controller -detect -##ssa -bowed -educator -kosovo -macedonia -##sin -occupy -mastering -stephanie -janeiro -para -unaware -nurses -noon -135 -cam -hopefully -ranger -combine -sociology -polar -rica -##eer -neill -##sman -holocaust -##ip -doubled -lust -1828 -109 -decent -cooling -unveiled -##card -1829 -nsw -homer -chapman -meyer -##gin -dive -mae -reagan -expertise -##gled -darwin -brooke -sided -prosecution -investigating -comprised -petroleum -genres -reluctant -differently -trilogy -johns -vegetables -corpse -highlighted -lounge -pension -unsuccessfully -elegant -aided -ivory -beatles -amelia -cain -dubai -sunny -immigrant -babe -click -##nder -underwater -pepper -combining -mumbled -atlas -horns -accessed -ballad -physicians -homeless -gestured -rpm -freak -louisville -corporations -patriots -prizes -rational -warn -modes -decorative -overnight -din -troubled -phantom -##ort -monarch -sheer -##dorf -generals -guidelines -organs -addresses -##zon -enhance -curling -parishes -cord -##kie -linux -caesar -deutsche -bavaria -##bia -coleman -cyclone -##eria -bacon -petty -##yama -##old -hampton -diagnosis -1824 -throws -complexity -rita -disputed -##₃ -pablo -##sch -marketed -trafficking -##ulus -examine -plague -formats -##oh -vault -faithful -##bourne -webster -##ox -highlights -##ient -##ann -phones -vacuum -sandwich -modeling -##gated -bolivia -clergy -qualities -isabel -##nas -##ars -wears -screams -reunited -annoyed -bra -##ancy -##rate -differential -transmitter -tattoo -container -poker -##och -excessive -resides -cowboys -##tum -augustus -trash -providers -statute -retreated -balcony -reversed -void -storey -preceded -masses -leap -laughs -neighborhoods -wards -schemes -falcon -santo -battlefield -pad -ronnie -thread -lesbian -venus -##dian -beg -sandstone -daylight -punched -gwen -analog -stroked -wwe -acceptable -measurements -dec -toxic -##kel -adequate -surgical -economist -parameters -varsity -##sberg -quantity -ella -##chy -##rton -countess -generating -precision -diamonds -expressway -ga -##ı -1821 -uruguay -talents -galleries -expenses -scanned -colleague -outlets -ryder -lucien -##ila -paramount -##bon -syracuse -dim -fangs -gown -sweep -##sie -toyota -missionaries -websites -##nsis -sentences -adviser -val -trademark -spells -##plane -patience -starter -slim -##borg -toe -incredibly -shoots -elliot -nobility -##wyn -cowboy -endorsed -gardner -tendency -persuaded -organisms -emissions -kazakhstan -amused -boring -chips -themed -##hand -llc -constantinople -chasing -systematic -guatemala -borrowed -erin -carey -##hard -highlands -struggles -1810 -##ifying -##ced -wong -exceptions -develops -enlarged -kindergarten -castro -##ern -##rina -leigh -zombie -juvenile -##most -consul -##nar -sailor -hyde -clarence -intensive -pinned -nasty -useless -jung -clayton -stuffed -exceptional -ix -apostolic -230 -transactions -##dge -exempt -swinging -cove -religions -##ash -shields -dairy -bypass -190 -pursuing -bug -joyce -bombay -chassis -southampton -chat -interact -redesignated -##pen -nascar -pray -salmon -rigid -regained -malaysian -grim -publicity -constituted -capturing -toilet -delegate -purely -tray -drift -loosely -striker -weakened -trinidad -mitch -itv -defines -transmitted -ming -scarlet -nodding -fitzgerald -fu -narrowly -sp -tooth -standings -virtue -##₁ -##wara -##cting -chateau -gloves -lid -##nel -hurting -conservatory -##pel -sinclair -reopened -sympathy -nigerian -strode -advocated -optional -chronic -discharge -##rc -suck -compatible -laurel -stella -shi -fails -wage -dodge -128 -informal -sorts -levi -buddha -villagers -##aka -chronicles -heavier -summoned -gateway -3000 -eleventh -jewelry -translations -accordingly -seas -##ency -fiber -pyramid -cubic -dragging -##ista -caring -##ops -android -contacted -lunar -##dt -kai -lisbon -patted -1826 -sacramento -theft -madagascar -subtropical -disputes -ta -holidays -piper -willow -mare -cane -itunes -newfoundland -benny -companions -dong -raj -observe -roar -charming -plaque -tibetan -fossils -enacted -manning -bubble -tina -tanzania -##eda -##hir -funk -swamp -deputies -cloak -ufc -scenario -par -scratch -metals -anthem -guru -engaging -specially -##boat -dialects -nineteen -cecil -duet -disability -messenger -unofficial -##lies -defunct -eds -moonlight -drainage -surname -puzzle -honda -switching -conservatives -mammals -knox -broadcaster -sidewalk -cope -##ried -benson -princes -peterson -##sal -bedford -sharks -eli -wreck -alberto -gasp -archaeology -lgbt -teaches -securities -madness -compromise -waving -coordination -davidson -visions -leased -possibilities -eighty -jun -fernandez -enthusiasm -assassin -sponsorship -reviewer -kingdoms -estonian -laboratories -##fy -##nal -applies -verb -celebrations -##zzo -rowing -lightweight -sadness -submit -mvp -balanced -dude -##vas -explicitly -metric -magnificent -mound -brett -mohammad -mistakes -irregular -##hing -##ass -sanders -betrayed -shipped -surge -##enburg -reporters -termed -georg -pity -verbal -bulls -abbreviated -enabling -appealed -##are -##atic -sicily -sting -heel -sweetheart -bart -spacecraft -brutal -monarchy -##tter -aberdeen -cameo -diane -##ub -survivor -clyde -##aries -complaint -##makers -clarinet -delicious -chilean -karnataka -coordinates -1818 -panties -##rst -pretending -ar -dramatically -kiev -bella -tends -distances -113 -catalog -launching -instances -telecommunications -portable -lindsay -vatican -##eim -angles -aliens -marker -stint -screens -bolton -##rne -judy -wool -benedict -plasma -europa -spark -imaging -filmmaker -swiftly -##een -contributor -##nor -opted -stamps -apologize -financing -butter -gideon -sophisticated -alignment -avery -chemicals -yearly -speculation -prominence -professionally -##ils -immortal -institutional -inception -wrists -identifying -tribunal -derives -gains -##wo -papal -preference -linguistic -vince -operative -brewery -##ont -unemployment -boyd -##ured -##outs -albeit -prophet -1813 -bi -##rr -##face -##rad -quarterly -asteroid -cleaned -radius -temper -##llen -telugu -jerk -viscount -menu -##ote -glimpse -##aya -yacht -hawaiian -baden -##rl -laptop -readily -##gu -monetary -offshore -scots -watches -##yang -##arian -upgrade -needle -xbox -lea -encyclopedia -flank -fingertips -##pus -delight -teachings -confirm -roth -beaches -midway -winters -##iah -teasing -daytime -beverly -gambling -bonnie -##backs -regulated -clement -hermann -tricks -knot -##shing -##uring -##vre -detached -ecological -owed -specialty -byron -inventor -bats -stays -screened -unesco -midland -trim -affection -##ander -##rry -jess -thoroughly -feedback -##uma -chennai -strained -heartbeat -wrapping -overtime -pleaded -##sworth -mon -leisure -oclc -##tate -##ele -feathers -angelo -thirds -nuts -surveys -clever -gill -commentator -##dos -darren -rides -gibraltar -##nc -##mu -dissolution -dedication -shin -meals -saddle -elvis -reds -chaired -taller -appreciation -functioning -niece -favored -advocacy -robbie -criminals -suffolk -yugoslav -passport -constable -congressman -hastings -vera -##rov -consecrated -sparks -ecclesiastical -confined -##ovich -muller -floyd -nora -1822 -paved -1827 -cumberland -ned -saga -spiral -##flow -appreciated -yi -collaborative -treating -similarities -feminine -finishes -##ib -jade -import -##nse -##hot -champagne -mice -securing -celebrities -helsinki -attributes -##gos -cousins -phases -ache -lucia -gandhi -submission -vicar -spear -shine -tasmania -biting -detention -constitute -tighter -seasonal -##gus -terrestrial -matthews -##oka -effectiveness -parody -philharmonic -##onic -1816 -strangers -encoded -consortium -guaranteed -regards -shifts -tortured -collision -supervisor -inform -broader -insight -theaters -armour -emeritus -blink -incorporates -mapping -##50 -##ein -handball -flexible -##nta -substantially -generous -thief -##own -carr -loses -1793 -prose -ucla -romeo -generic -metallic -realization -damages -mk -commissioners -zach -default -##ther -helicopters -lengthy -stems -spa -partnered -spectators -rogue -indication -penalties -teresa -1801 -sen -##tric -dalton -##wich -irving -photographic -##vey -dell -deaf -peters -excluded -unsure -##vable -patterson -crawled -##zio -resided -whipped -latvia -slower -ecole -pipes -employers -maharashtra -comparable -va -textile -pageant -##gel -alphabet -binary -irrigation -chartered -choked -antoine -offs -waking -supplement -##wen -quantities -demolition -regain -locate -urdu -folks -alt -114 -##mc -scary -andreas -whites -##ava -classrooms -mw -aesthetic -publishes -valleys -guides -cubs -johannes -bryant -conventions -affecting -##itt -drain -awesome -isolation -prosecutor -ambitious -apology -captive -downs -atmospheric -lorenzo -aisle -beef -foul -##onia -kidding -composite -disturbed -illusion -natives -##ffer -emi -rockets -riverside -wartime -painters -adolf -melted -##ail -uncertainty -simulation -hawks -progressed -meantime -builder -spray -breach -unhappy -regina -russians -##urg -determining -##tation -tram -1806 -##quin -aging -##12 -1823 -garion -rented -mister -diaz -terminated -clip -1817 -depend -nervously -disco -owe -defenders -shiva -notorious -disbelief -shiny -worcester -##gation -##yr -trailing -undertook -islander -belarus -limitations -watershed -fuller -overlooking -utilized -raphael -1819 -synthetic -breakdown -klein -##nate -moaned -memoir -lamb -practicing -##erly -cellular -arrows -exotic -##graphy -witches -117 -charted -rey -hut -hierarchy -subdivision -freshwater -giuseppe -aloud -reyes -qatar -marty -sideways -utterly -sexually -jude -prayers -mccarthy -softball -blend -damien -##gging -##metric -wholly -erupted -lebanese -negro -revenues -tasted -comparative -teamed -transaction -labeled -maori -sovereignty -parkway -trauma -gran -malay -121 -advancement -descendant -2020 -buzz -salvation -inventory -symbolic -##making -antarctica -mps -##gas -##bro -mohammed -myanmar -holt -submarines -tones -##lman -locker -patriarch -bangkok -emerson -remarks -predators -kin -afghan -confession -norwich -rental -emerge -advantages -##zel -rca -##hold -shortened -storms -aidan -##matic -autonomy -compliance -##quet -dudley -atp -##osis -1803 -motto -documentation -summary -professors -spectacular -christina -archdiocese -flashing -innocence -remake -##dell -psychic -reef -scare -employ -rs -sticks -meg -gus -leans -##ude -accompany -bergen -tomas -##iko -doom -wages -pools -##nch -##bes -breasts -scholarly -alison -outline -brittany -breakthrough -willis -realistic -##cut -##boro -competitor -##stan -pike -picnic -icon -designing -commercials -washing -villain -skiing -micro -costumes -auburn -halted -executives -##hat -logistics -cycles -vowel -applicable -barrett -exclaimed -eurovision -eternity -ramon -##umi -##lls -modifications -sweeping -disgust -##uck -torch -aviv -ensuring -rude -dusty -sonic -donovan -outskirts -cu -pathway -##band -##gun -##lines -disciplines -acids -cadet -paired -##40 -sketches -##sive -marriages -##⁺ -folding -peers -slovak -implies -admired -##beck -1880s -leopold -instinct -attained -weston -megan -horace -##ination -dorsal -ingredients -evolutionary -##its -complications -deity -lethal -brushing -levy -deserted -institutes -posthumously -delivering -telescope -coronation -motivated -rapids -luc -flicked -pays -volcano -tanner -weighed -##nica -crowds -frankie -gifted -addressing -granddaughter -winding -##rna -constantine -gomez -##front -landscapes -rudolf -anthropology -slate -werewolf -##lio -astronomy -circa -rouge -dreaming -sack -knelt -drowned -naomi -prolific -tracked -freezing -herb -##dium -agony -randall -twisting -wendy -deposit -touches -vein -wheeler -##bbled -##bor -batted -retaining -tire -presently -compare -specification -daemon -nigel -##grave -merry -recommendation -czechoslovakia -sandra -ng -roma -##sts -lambert -inheritance -sheikh -winchester -cries -examining -##yle -comeback -cuisine -nave -##iv -ko -retrieve -tomatoes -barker -polished -defining -irene -lantern -personalities -begging -tract -swore -1809 -175 -##gic -omaha -brotherhood -##rley -haiti -##ots -exeter -##ete -##zia -steele -dumb -pearson -210 -surveyed -elisabeth -trends -##ef -fritz -##rf -premium -bugs -fraction -calmly -viking -##birds -tug -inserted -unusually -##ield -confronted -distress -crashing -brent -turks -resign -##olo -cambodia -gabe -sauce -##kal -evelyn -116 -extant -clusters -quarry -teenagers -luna -##lers -##ister -affiliation -drill -##ashi -panthers -scenic -libya -anita -strengthen -inscriptions -##cated -lace -sued -judith -riots -##uted -mint -##eta -preparations -midst -dub -challenger -##vich -mock -cf -displaced -wicket -breaths -enables -schmidt -analyst -##lum -ag -highlight -automotive -axe -josef -newark -sufficiently -resembles -50th -##pal -flushed -mum -traits -##ante -commodore -incomplete -warming -titular -ceremonial -ethical -118 -celebrating -eighteenth -cao -lima -medalist -mobility -strips -snakes -##city -miniature -zagreb -barton -escapes -umbrella -automated -doubted -differs -cooled -georgetown -dresden -cooked -fade -wyatt -rna -jacobs -carlton -abundant -stereo -boost -madras -inning -##hia -spur -ip -malayalam -begged -osaka -groan -escaping -charging -dose -vista -##aj -bud -papa -communists -advocates -edged -tri -##cent -resemble -peaking -necklace -fried -montenegro -saxony -goose -glances -stuttgart -curator -recruit -grocery -sympathetic -##tting -##fort -127 -lotus -randolph -ancestor -##rand -succeeding -jupiter -1798 -macedonian -##heads -hiking -1808 -handing -fischer -##itive -garbage -node -##pies -prone -singular -papua -inclined -attractions -italia -pouring -motioned -grandma -garnered -jacksonville -corp -ego -ringing -aluminum -##hausen -ordering -##foot -drawer -traders -synagogue -##play -##kawa -resistant -wandering -fragile -fiona -teased -var -hardcore -soaked -jubilee -decisive -exposition -mercer -poster -valencia -hale -kuwait -1811 -##ises -##wr -##eed -tavern -gamma -122 -johan -##uer -airways -amino -gil -##ury -vocational -domains -torres -##sp -generator -folklore -outcomes -##keeper -canberra -shooter -fl -beams -confrontation -##lling -##gram -feb -aligned -forestry -pipeline -jax -motorway -conception -decay -##tos -coffin -##cott -stalin -1805 -escorted -minded -##nam -sitcom -purchasing -twilight -veronica -additions -passive -tensions -straw -123 -frequencies -1804 -refugee -cultivation -##iate -christie -clary -bulletin -crept -disposal -##rich -##zong -processor -crescent -##rol -bmw -emphasized -whale -nazis -aurora -##eng -dwelling -hauled -sponsors -toledo -mega -ideology -theatres -tessa -cerambycidae -saves -turtle -cone -suspects -kara -rusty -yelling -greeks -mozart -shades -cocked -participant -##tro -shire -spit -freeze -necessity -##cos -inmates -nielsen -councillors -loaned -uncommon -omar -peasants -botanical -offspring -daniels -formations -jokes -1794 -pioneers -sigma -licensing -##sus -wheelchair -polite -1807 -liquor -pratt -trustee -##uta -forewings -balloon -##zz -kilometre -camping -explicit -casually -shawn -foolish -teammates -nm -hassan -carrie -judged -satisfy -vanessa -knives -selective -cnn -flowed -##lice -eclipse -stressed -eliza -mathematician -cease -cultivated -##roy -commissions -browns -##ania -destroyers -sheridan -meadow -##rius -minerals -##cial -downstream -clash -gram -memoirs -ventures -baha -seymour -archie -midlands -edith -fare -flynn -invite -canceled -tiles -stabbed -boulder -incorporate -amended -camden -facial -mollusk -unreleased -descriptions -yoga -grabs -550 -raises -ramp -shiver -##rose -coined -pioneering -tunes -qing -warwick -tops -119 -melanie -giles -##rous -wandered -##inal -annexed -nov -30th -unnamed -##ished -organizational -airplane -normandy -stoke -whistle -blessing -violations -chased -holders -shotgun -##ctic -outlet -reactor -##vik -tires -tearing -shores -fortified -mascot -constituencies -nc -columnist -productive -tibet -##rta -lineage -hooked -oct -tapes -judging -cody -##gger -hansen -kashmir -triggered -##eva -solved -cliffs -##tree -resisted -anatomy -protesters -transparent -implied -##iga -injection -mattress -excluding -##mbo -defenses -helpless -devotion -##elli -growl -liberals -weber -phenomena -atoms -plug -##iff -mortality -apprentice -howe -convincing -aaa -swimmer -barber -leone -promptly -sodium -def -nowadays -arise -##oning -gloucester -corrected -dignity -norm -erie -##ders -elders -evacuated -sylvia -compression -##yar -hartford -pose -backpack -reasoning -accepts -24th -wipe -millimetres -marcel -##oda -dodgers -albion -1790 -overwhelmed -aerospace -oaks -1795 -showcase -acknowledge -recovering -nolan -ashe -hurts -geology -fashioned -disappearance -farewell -swollen -shrug -marquis -wimbledon -124 -rue -1792 -commemorate -reduces -experiencing -inevitable -calcutta -intel -##court -murderer -sticking -fisheries -imagery -bloom -280 -brake -##inus -gustav -hesitation -memorable -po -viral -beans -accidents -tunisia -antenna -spilled -consort -treatments -aye -perimeter -##gard -donation -hostage -migrated -banker -addiction -apex -lil -trout -##ously -conscience -##nova -rams -sands -genome -passionate -troubles -##lets -##set -amid -##ibility -##ret -higgins -exceed -vikings -##vie -payne -##zan -muscular -##ste -defendant -sucking -##wal -ibrahim -fuselage -claudia -vfl -europeans -snails -interval -##garh -preparatory -statewide -tasked -lacrosse -viktor -##lation -angola -##hra -flint -implications -employs -teens -patrons -stall -weekends -barriers -scrambled -nucleus -tehran -jenna -parsons -lifelong -robots -displacement -5000 -##bles -precipitation -##gt -knuckles -clutched -1802 -marrying -ecology -marx -accusations -declare -scars -kolkata -mat -meadows -bermuda -skeleton -finalists -vintage -crawl -coordinate -affects -subjected -orchestral -mistaken -##tc -mirrors -dipped -relied -260 -arches -candle -##nick -incorporating -wildly -fond -basilica -owl -fringe -rituals -whispering -stirred -feud -tertiary -slick -goat -honorable -whereby -skip -ricardo -stripes -parachute -adjoining -submerged -synthesizer -##gren -intend -positively -ninety -phi -beaver -partition -fellows -alexis -prohibition -carlisle -bizarre -fraternity -##bre -doubts -icy -cbc -aquatic -sneak -sonny -combines -airports -crude -supervised -spatial -merge -alfonso -##bic -corrupt -scan -undergo -##ams -disabilities -colombian -comparing -dolphins -perkins -##lish -reprinted -unanimous -bounced -hairs -underworld -midwest -semester -bucket -paperback -miniseries -coventry -demise -##leigh -demonstrations -sensor -rotating -yan -##hler -arrange -soils -##idge -hyderabad -labs -##dr -brakes -grandchildren -##nde -negotiated -rover -ferrari -continuation -directorate -augusta -stevenson -counterpart -gore -##rda -nursery -rican -ave -collectively -broadly -pastoral -repertoire -asserted -discovering -nordic -styled -fiba -cunningham -harley -middlesex -survives -tumor -tempo -zack -aiming -lok -urgent -##rade -##nto -devils -##ement -contractor -turin -##wl -##ool -bliss -repaired -simmons -moan -astronomical -cr -negotiate -lyric -1890s -lara -bred -clad -angus -pbs -##ience -engineered -posed -##lk -hernandez -possessions -elbows -psychiatric -strokes -confluence -electorate -lifts -campuses -lava -alps -##ep -##ution -##date -physicist -woody -##page -##ographic -##itis -juliet -reformation -sparhawk -320 -complement -suppressed -jewel -##½ -floated -##kas -continuity -sadly -##ische -inability -melting -scanning -paula -flour -judaism -safer -vague -##lm -solving -curb -##stown -financially -gable -bees -expired -miserable -cassidy -dominion -1789 -cupped -145 -robbery -facto -amos -warden -resume -tallest -marvin -ing -pounded -usd -declaring -gasoline -##aux -darkened -270 -650 -sophomore -##mere -erection -gossip -televised -risen -dial -##eu -pillars -##link -passages -profound -##tina -arabian -ashton -silicon -nail -##ead -##lated -##wer -##hardt -fleming -firearms -ducked -circuits -blows -waterloo -titans -##lina -atom -fireplace -cheshire -financed -activation -algorithms -##zzi -constituent -catcher -cherokee -partnerships -sexuality -platoon -tragic -vivian -guarded -whiskey -meditation -poetic -##late -##nga -##ake -porto -listeners -dominance -kendra -mona -chandler -factions -22nd -salisbury -attitudes -derivative -##ido -##haus -intake -paced -javier -illustrator -barrels -bias -cockpit -burnett -dreamed -ensuing -##anda -receptors -someday -hawkins -mattered -##lal -slavic -1799 -jesuit -cameroon -wasted -tai -wax -lowering -victorious -freaking -outright -hancock -librarian -sensing -bald -calcium -myers -tablet -announcing -barack -shipyard -pharmaceutical -##uan -greenwich -flush -medley -patches -wolfgang -pt -speeches -acquiring -exams -nikolai -##gg -hayden -kannada -##type -reilly -##pt -waitress -abdomen -devastated -capped -pseudonym -pharmacy -fulfill -paraguay -1796 -clicked -##trom -archipelago -syndicated -##hman -lumber -orgasm -rejection -clifford -lorraine -advent -mafia -rodney -brock -##ght -##used -##elia -cassette -chamberlain -despair -mongolia -sensors -developmental -upstream -##eg -##alis -spanning -165 -trombone -basque -seeded -interred -renewable -rhys -leapt -revision -molecule -##ages -chord -vicious -nord -shivered -23rd -arlington -debts -corpus -sunrise -bays -blackburn -centimetres -##uded -shuddered -gm -strangely -gripping -cartoons -isabelle -orbital -##ppa -seals -proving -##lton -refusal -strengthened -bust -assisting -baghdad -batsman -portrayal -mara -pushes -spears -og -##cock -reside -nathaniel -brennan -1776 -confirmation -caucus -##worthy -markings -yemen -nobles -ku -lazy -viewer -catalan -encompasses -sawyer -##fall -sparked -substances -patents -braves -arranger -evacuation -sergio -persuade -dover -tolerance -penguin -cum -jockey -insufficient -townships -occupying -declining -plural -processed -projection -puppet -flanders -introduces -liability -##yon -gymnastics -antwerp -taipei -hobart -candles -jeep -wes -observers -126 -chaplain -bundle -glorious -##hine -hazel -flung -sol -excavations -dumped -stares -sh -bangalore -triangular -icelandic -intervals -expressing -turbine -##vers -songwriting -crafts -##igo -jasmine -ditch -rite -##ways -entertaining -comply -sorrow -wrestlers -basel -emirates -marian -rivera -helpful -##some -caution -downward -networking -##atory -##tered -darted -genocide -emergence -replies -specializing -spokesman -convenient -unlocked -fading -augustine -concentrations -resemblance -elijah -investigator -andhra -##uda -promotes -bean -##rrell -fleeing -wan -simone -announcer -##ame -##bby -lydia -weaver -132 -residency -modification -##fest -stretches -##ast -alternatively -nat -lowe -lacks -##ented -pam -tile -concealed -inferior -abdullah -residences -tissues -vengeance -##ided -moisture -peculiar -groove -zip -bologna -jennings -ninja -oversaw -zombies -pumping -batch -livingston -emerald -installations -1797 -peel -nitrogen -rama -##fying -##star -schooling -strands -responding -werner -##ost -lime -casa -accurately -targeting -##rod -underway -##uru -hemisphere -lester -##yard -occupies -2d -griffith -angrily -reorganized -##owing -courtney -deposited -##dd -##30 -estadio -##ifies -dunn -exiled -##ying -checks -##combe -##о -##fly -successes -unexpectedly -blu -assessed -##flower -##ه -observing -sacked -spiders -kn -##tail -mu -nodes -prosperity -audrey -divisional -155 -broncos -tangled -adjust -feeds -erosion -paolo -surf -directory -snatched -humid -admiralty -screwed -gt -reddish -##nese -modules -trench -lamps -bind -leah -bucks -competes -##nz -##form -transcription -##uc -isles -violently -clutching -pga -cyclist -inflation -flats -ragged -unnecessary -##hian -stubborn -coordinated -harriet -baba -disqualified -330 -insect -wolfe -##fies -reinforcements -rocked -duel -winked -embraced -bricks -##raj -hiatus -defeats -pending -brightly -jealousy -##xton -##hm -##uki -lena -gdp -colorful -##dley -stein -kidney -##shu -underwear -wanderers -##haw -##icus -guardians -m³ -roared -habits -##wise -permits -gp -uranium -punished -disguise -bundesliga -elise -dundee -erotic -partisan -pi -collectors -float -individually -rendering -behavioral -bucharest -ser -hare -valerie -corporal -nutrition -proportional -##isa -immense -##kis -pavement -##zie -##eld -sutherland -crouched -1775 -##lp -suzuki -trades -endurance -operas -crosby -prayed -priory -rory -socially -##urn -gujarat -##pu -walton -cube -pasha -privilege -lennon -floods -thorne -waterfall -nipple -scouting -approve -##lov -minorities -voter -dwight -extensions -assure -ballroom -slap -dripping -privileges -rejoined -confessed -demonstrating -patriotic -yell -investor -##uth -pagan -slumped -squares -##cle -##kins -confront -bert -embarrassment -##aid -aston -urging -sweater -starr -yuri -brains -williamson -commuter -mortar -structured -selfish -exports -##jon -cds -##him -unfinished -##rre -mortgage -destinations -##nagar -canoe -solitary -buchanan -delays -magistrate -fk -##pling -motivation -##lier -##vier -recruiting -assess -##mouth -malik -antique -1791 -pius -rahman -reich -tub -zhou -smashed -airs -galway -xii -conditioning -honduras -discharged -dexter -##pf -lionel -129 -debates -lemon -tiffany -volunteered -dom -dioxide -procession -devi -sic -tremendous -advertisements -colts -transferring -verdict -hanover -decommissioned -utter -relate -pac -racism -##top -beacon -limp -similarity -terra -occurrence -ant -##how -becky -capt -updates -armament -richie -pal -##graph -halloween -mayo -##ssen -##bone -cara -serena -fcc -dolls -obligations -##dling -violated -lafayette -jakarta -exploitation -##ime -infamous -iconic -##lah -##park -kitty -moody -reginald -dread -spill -crystals -olivier -modeled -bluff -equilibrium -separating -notices -ordnance -extinction -onset -cosmic -attachment -sammy -expose -privy -anchored -##bil -abbott -admits -bending -baritone -emmanuel -policeman -vaughan -winged -climax -dresses -denny -polytechnic -mohamed -burmese -authentic -nikki -genetics -grandparents -homestead -gaza -postponed -metacritic -una -##sby -##bat -unstable -dissertation -##rial -##cian -curls -obscure -uncovered -bronx -praying -disappearing -##hoe -prehistoric -coke -turret -mutations -nonprofit -pits -monaco -##ي -##usion -prominently -dispatched -podium -##mir -uci -##uation -133 -fortifications -birthplace -kendall -##lby -##oll -preacher -rack -goodman -##rman -persistent -##ott -countless -jaime -recorder -lexington -persecution -jumps -renewal -wagons -##11 -crushing -##holder -decorations -##lake -abundance -wrath -laundry -£1 -garde -##rp -jeanne -beetles -peasant -##sl -splitting -caste -sergei -##rer -##ema -scripts -##ively -rub -satellites -##vor -inscribed -verlag -scrapped -gale -packages -chick -potato -slogan -kathleen -arabs -##culture -counterparts -reminiscent -choral -##tead -rand -retains -bushes -dane -accomplish -courtesy -closes -##oth -slaughter -hague -krakow -lawson -tailed -elias -ginger -##ttes -canopy -betrayal -rebuilding -turf -##hof -frowning -allegiance -brigades -kicks -rebuild -polls -alias -nationalism -td -rowan -audition -bowie -fortunately -recognizes -harp -dillon -horrified -##oro -renault -##tics -ropes -##α -presumed -rewarded -infrared -wiping -accelerated -illustration -##rid -presses -practitioners -badminton -##iard -detained -##tera -recognizing -relates -misery -##sies -##tly -reproduction -piercing -potatoes -thornton -esther -manners -hbo -##aan -ours -bullshit -ernie -perennial -sensitivity -illuminated -rupert -##jin -##iss -##ear -rfc -nassau -##dock -staggered -socialism -##haven -appointments -nonsense -prestige -sharma -haul -##tical -solidarity -gps -##ook -##rata -igor -pedestrian -##uit -baxter -tenants -wires -medication -unlimited -guiding -impacts -diabetes -##rama -sasha -pas -clive -extraction -131 -continually -constraints -##bilities -sonata -hunted -sixteenth -chu -planting -quote -mayer -pretended -abs -spat -##hua -ceramic -##cci -curtains -pigs -pitching -##dad -latvian -sore -dayton -##sted -##qi -patrols -slice -playground -##nted -shone -stool -apparatus -inadequate -mates -treason -##ija -desires -##liga -##croft -somalia -laurent -mir -leonardo -oracle -grape -obliged -chevrolet -thirteenth -stunning -enthusiastic -##ede -accounted -concludes -currents -basil -##kovic -drought -##rica -mai -##aire -shove -posting -##shed -pilgrimage -humorous -packing -fry -pencil -wines -smells -144 -marilyn -aching -newest -clung -bon -neighbours -sanctioned -##pie -mug -##stock -drowning -##mma -hydraulic -##vil -hiring -reminder -lilly -investigators -##ncies -sour -##eous -compulsory -packet -##rion -##graphic -##elle -cannes -##inate -depressed -##rit -heroic -importantly -theresa -##tled -conway -saturn -marginal -rae -##xia -corresponds -royce -pact -jasper -explosives -packaging -aluminium -##ttered -denotes -rhythmic -spans -assignments -hereditary -outlined -originating -sundays -lad -reissued -greeting -beatrice -##dic -pillar -marcos -plots -handbook -alcoholic -judiciary -avant -slides -extract -masculine -blur -##eum -##force -homage -trembled -owens -hymn -trey -omega -signaling -socks -accumulated -reacted -attic -theo -lining -angie -distraction -primera -talbot -##key -1200 -ti -creativity -billed -##hey -deacon -eduardo -identifies -proposition -dizzy -gunner -hogan -##yam -##pping -##hol -ja -##chan -jensen -reconstructed -##berger -clearance -darius -##nier -abe -harlem -plea -dei -circled -emotionally -notation -fascist -neville -exceeded -upwards -viable -ducks -##fo -workforce -racer -limiting -shri -##lson -possesses -1600 -kerr -moths -devastating -laden -disturbing -locking -##cture -gal -fearing -accreditation -flavor -aide -1870s -mountainous -##baum -melt -##ures -motel -texture -servers -soda -##mb -herd -##nium -erect -puzzled -hum -peggy -examinations -gould -testified -geoff -ren -devised -sacks -##law -denial -posters -grunted -cesar -tutor -ec -gerry -offerings -byrne -falcons -combinations -ct -incoming -pardon -rocking -26th -avengers -flared -mankind -seller -uttar -loch -nadia -stroking -exposing -##hd -fertile -ancestral -instituted -##has -noises -prophecy -taxation -eminent -vivid -pol -##bol -dart -indirect -multimedia -notebook -upside -displaying -adrenaline -referenced -geometric -##iving -progression -##ddy -blunt -announce -##far -implementing -##lav -aggression -liaison -cooler -cares -headache -plantations -gorge -dots -impulse -thickness -ashamed -averaging -kathy -obligation -precursor -137 -fowler -symmetry -thee -225 -hears -##rai -undergoing -ads -butcher -bowler -##lip -cigarettes -subscription -goodness -##ically -browne -##hos -##tech -kyoto -donor -##erty -damaging -friction -drifting -expeditions -hardened -prostitution -152 -fauna -blankets -claw -tossing -snarled -butterflies -recruits -investigative -coated -healed -138 -communal -hai -xiii -academics -boone -psychologist -restless -lahore -stephens -mba -brendan -foreigners -printer -##pc -ached -explode -27th -deed -scratched -dared -##pole -cardiac -1780 -okinawa -proto -commando -compelled -oddly -electrons -##base -replica -thanksgiving -##rist -sheila -deliberate -stafford -tidal -representations -hercules -ou -##path -##iated -kidnapping -lenses -##tling -deficit -samoa -mouths -consuming -computational -maze -granting -smirk -razor -fixture -ideals -inviting -aiden -nominal -##vs -issuing -julio -pitt -ramsey -docks -##oss -exhaust -##owed -bavarian -draped -anterior -mating -ethiopian -explores -noticing -##nton -discarded -convenience -hoffman -endowment -beasts -cartridge -mormon -paternal -probe -sleeves -interfere -lump -deadline -##rail -jenks -bulldogs -scrap -alternating -justified -reproductive -nam -seize -descending -secretariat -kirby -coupe -grouped -smash -panther -sedan -tapping -##18 -lola -cheer -germanic -unfortunate -##eter -unrelated -##fan -subordinate -##sdale -suzanne -advertisement -##ility -horsepower -##lda -cautiously -discourse -luigi -##mans -##fields -noun -prevalent -mao -schneider -everett -surround -governorate -kira -##avia -westward -##take -misty -rails -sustainability -134 -unused -##rating -packs -toast -unwilling -regulate -thy -suffrage -nile -awe -assam -definitions -travelers -affordable -##rb -conferred -sells -undefeated -beneficial -torso -basal -repeating -remixes -##pass -bahrain -cables -fang -##itated -excavated -numbering -statutory -##rey -deluxe -##lian -forested -ramirez -derbyshire -zeus -slamming -transfers -astronomer -banana -lottery -berg -histories -bamboo -##uchi -resurrection -posterior -bowls -vaguely -##thi -thou -preserving -tensed -offence -##inas -meyrick -callum -ridden -watt -langdon -tying -lowland -snorted -daring -truman -##hale -##girl -aura -overly -filing -weighing -goa -infections -philanthropist -saunders -eponymous -##owski -latitude -perspectives -reviewing -mets -commandant -radial -##kha -flashlight -reliability -koch -vowels -amazed -ada -elaine -supper -##rth -##encies -predator -debated -soviets -cola -##boards -##nah -compartment -crooked -arbitrary -fourteenth -##ctive -havana -majors -steelers -clips -profitable -ambush -exited -packers -##tile -nude -cracks -fungi -##е -limb -trousers -josie -shelby -tens -frederic -##ος -definite -smoothly -constellation -insult -baton -discs -lingering -##nco -conclusions -lent -staging -becker -grandpa -shaky -##tron -einstein -obstacles -sk -adverse -elle -economically -##moto -mccartney -thor -dismissal -motions -readings -nostrils -treatise -##pace -squeezing -evidently -prolonged -1783 -venezuelan -je -marguerite -beirut -takeover -shareholders -##vent -denise -digit -airplay -norse -##bbling -imaginary -pills -hubert -blaze -vacated -eliminating -##ello -vine -mansfield -##tty -retrospective -barrow -borne -clutch -bail -forensic -weaving -##nett -##witz -desktop -citadel -promotions -worrying -dorset -ieee -subdivided -##iating -manned -expeditionary -pickup -synod -chuckle -185 -barney -##rz -##ffin -functionality -karachi -litigation -meanings -uc -lick -turbo -anders -##ffed -execute -curl -oppose -ankles -typhoon -##د -##ache -##asia -linguistics -compassion -pressures -grazing -perfection -##iting -immunity -monopoly -muddy -backgrounds -136 -namibia -francesca -monitors -attracting -stunt -tuition -##ии -vegetable -##mates -##quent -mgm -jen -complexes -forts -##ond -cellar -bites -seventeenth -royals -flemish -failures -mast -charities -##cular -peruvian -capitals -macmillan -ipswich -outward -frigate -postgraduate -folds -employing -##ouse -concurrently -fiery -##tai -contingent -nightmares -monumental -nicaragua -##kowski -lizard -mal -fielding -gig -reject -##pad -harding -##ipe -coastline -##cin -##nos -beethoven -humphrey -innovations -##tam -##nge -norris -doris -solicitor -huang -obey -141 -##lc -niagara -##tton -shelves -aug -bourbon -curry -nightclub -specifications -hilton -##ndo -centennial -dispersed -worm -neglected -briggs -sm -font -kuala -uneasy -plc -##nstein -##bound -##aking -##burgh -awaiting -pronunciation -##bbed -##quest -eh -optimal -zhu -raped -greens -presided -brenda -worries -##life -venetian -marxist -turnout -##lius -refined -braced -sins -grasped -sunderland -nickel -speculated -lowell -cyrillic -communism -fundraising -resembling -colonists -mutant -freddie -usc -##mos -gratitude -##run -mural -##lous -chemist -wi -reminds -28th -steals -tess -pietro -##ingen -promoter -ri -microphone -honoured -rai -sant -##qui -feather -##nson -burlington -kurdish -terrorists -deborah -sickness -##wed -##eet -hazard -irritated -desperation -veil -clarity -##rik -jewels -xv -##gged -##ows -##cup -berkshire -unfair -mysteries -orchid -winced -exhaustion -renovations -stranded -obe -infinity -##nies -adapt -redevelopment -thanked -registry -olga -domingo -noir -tudor -ole -##atus -commenting -behaviors -##ais -crisp -pauline -probable -stirling -wigan -##bian -paralympics -panting -surpassed -##rew -luca -barred -pony -famed -##sters -cassandra -waiter -carolyn -exported -##orted -andres -destructive -deeds -jonah -castles -vacancy -suv -##glass -1788 -orchard -yep -famine -belarusian -sprang -##forth -skinny -##mis -administrators -rotterdam -zambia -zhao -boiler -discoveries -##ride -##physics -lucius -disappointing -outreach -spoon -##frame -qualifications -unanimously -enjoys -regency -##iidae -stade -realism -veterinary -rodgers -dump -alain -chestnut -castile -censorship -rumble -gibbs -##itor -communion -reggae -inactivated -logs -loads -##houses -homosexual -##iano -ale -informs -##cas -phrases -plaster -linebacker -ambrose -kaiser -fascinated -850 -limerick -recruitment -forge -mastered -##nding -leinster -rooted -threaten -##strom -borneo -##hes -suggestions -scholarships -propeller -documentaries -patronage -coats -constructing -invest -neurons -comet -entirety -shouts -identities -annoying -unchanged -wary -##antly -##ogy -neat -oversight -##kos -phillies -replay -constance -##kka -incarnation -humble -skies -minus -##acy -smithsonian -##chel -guerrilla -jar -cadets -##plate -surplus -audit -##aru -cracking -joanna -louisa -pacing -##lights -intentionally -##iri -diner -nwa -imprint -australians -tong -unprecedented -bunker -naive -specialists -ark -nichols -railing -leaked -pedal -##uka -shrub -longing -roofs -v8 -captains -neural -tuned -##ntal -##jet -emission -medina -frantic -codex -definitive -sid -abolition -intensified -stocks -enrique -sustain -genoa -oxide -##written -clues -cha -##gers -tributaries -fragment -venom -##rity -##ente -##sca -muffled -vain -sire -laos -##ingly -##hana -hastily -snapping -surfaced -sentiment -motive -##oft -contests -approximate -mesa -luckily -dinosaur -exchanges -propelled -accord -bourne -relieve -tow -masks -offended -##ues -cynthia -##mmer -rains -bartender -zinc -reviewers -lois -##sai -legged -arrogant -rafe -rosie -comprise -handicap -blockade -inlet -lagoon -copied -drilling -shelley -petals -##inian -mandarin -obsolete -##inated -onward -arguably -productivity -cindy -praising -seldom -busch -discusses -raleigh -shortage -ranged -stanton -encouragement -firstly -conceded -overs -temporal -##uke -cbe -##bos -woo -certainty -pumps -##pton -stalked -##uli -lizzie -periodic -thieves -weaker -##night -gases -shoving -chooses -wc -##chemical -prompting -weights -##kill -robust -flanked -sticky -hu -tuberculosis -##eb -##eal -christchurch -resembled -wallet -reese -inappropriate -pictured -distract -fixing -fiddle -giggled -burger -heirs -hairy -mechanic -torque -apache -obsessed -chiefly -cheng -logging -##tag -extracted -meaningful -numb -##vsky -gloucestershire -reminding -##bay -unite -##lit -breeds -diminished -clown -glove -1860s -##ن -##ug -archibald -focal -freelance -sliced -depiction -##yk -organism -switches -sights -stray -crawling -##ril -lever -leningrad -interpretations -loops -anytime -reel -alicia -delighted -##ech -inhaled -xiv -suitcase -bernie -vega -licenses -northampton -exclusion -induction -monasteries -racecourse -homosexuality -##right -##sfield -##rky -dimitri -michele -alternatives -ions -commentators -genuinely -objected -pork -hospitality -fencing -stephan -warships -peripheral -wit -drunken -wrinkled -quentin -spends -departing -chung -numerical -spokesperson -##zone -johannesburg -caliber -killers -##udge -assumes -neatly -demographic -abigail -bloc -##vel -mounting -##lain -bentley -slightest -xu -recipients -##jk -merlin -##writer -seniors -prisons -blinking -hindwings -flickered -kappa -##hel -80s -strengthening -appealing -brewing -gypsy -mali -lashes -hulk -unpleasant -harassment -bio -treaties -predict -instrumentation -pulp -troupe -boiling -mantle -##ffe -ins -##vn -dividing -handles -verbs -##onal -coconut -senegal -340 -thorough -gum -momentarily -##sto -cocaine -panicked -destined -##turing -teatro -denying -weary -captained -mans -##hawks -##code -wakefield -bollywood -thankfully -##16 -cyril -##wu -amendments -##bahn -consultation -stud -reflections -kindness -1787 -internally -##ovo -tex -mosaic -distribute -paddy -seeming -143 -##hic -piers -##15 -##mura -##verse -popularly -winger -kang -sentinel -mccoy -##anza -covenant -##bag -verge -fireworks -suppress -thrilled -dominate -##jar -swansea -##60 -142 -reconciliation -##ndi -stiffened -cue -dorian -##uf -damascus -amor -ida -foremost -##aga -porsche -unseen -dir -##had -##azi -stony -lexi -melodies -##nko -angular -integer -podcast -ants -inherent -jaws -justify -persona -##olved -josephine -##nr -##ressed -customary -flashes -gala -cyrus -glaring -backyard -ariel -physiology -greenland -html -stir -avon -atletico -finch -methodology -ked -##lent -mas -catholicism -townsend -branding -quincy -fits -containers -1777 -ashore -aragon -##19 -forearm -poisoning -##sd -adopting -conquer -grinding -amnesty -keller -finances -evaluate -forged -lankan -instincts -##uto -guam -bosnian -photographed -workplace -desirable -protector -##dog -allocation -intently -encourages -willy -##sten -bodyguard -electro -brighter -##ν -bihar -##chev -lasts -opener -amphibious -sal -verde -arte -##cope -captivity -vocabulary -yields -##tted -agreeing -desmond -pioneered -##chus -strap -campaigned -railroads -##ович -emblem -##dre -stormed -501 -##ulous -marijuana -northumberland -##gn -##nath -bowen -landmarks -beaumont -##qua -danube -##bler -attorneys -th -ge -flyers -critique -villains -cass -mutation -acc -##0s -colombo -mckay -motif -sampling -concluding -syndicate -##rell -neon -stables -ds -warnings -clint -mourning -wilkinson -##tated -merrill -leopard -evenings -exhaled -emil -sonia -ezra -discrete -stove -farrell -fifteenth -prescribed -superhero -##rier -worms -helm -wren -##duction -##hc -expo -##rator -hq -unfamiliar -antony -prevents -acceleration -fiercely -mari -painfully -calculations -cheaper -ign -clifton -irvine -davenport -mozambique -##np -pierced -##evich -wonders -##wig -##cate -##iling -crusade -ware -##uel -enzymes -reasonably -mls -##coe -mater -ambition -bunny -eliot -kernel -##fin -asphalt -headmaster -torah -aden -lush -pins -waived -##care -##yas -joao -substrate -enforce -##grad -##ules -alvarez -selections -epidemic -tempted -##bit -bremen -translates -ensured -waterfront -29th -forrest -manny -malone -kramer -reigning -cookies -simpler -absorption -205 -engraved -##ffy -evaluated -1778 -haze -146 -comforting -crossover -##abe -thorn -##rift -##imo -##pop -suppression -fatigue -cutter -##tr -201 -wurttemberg -##orf -enforced -hovering -proprietary -gb -samurai -syllable -ascent -lacey -tick -lars -tractor -merchandise -rep -bouncing -defendants -##yre -huntington -##ground -##oko -standardized -##hor -##hima -assassinated -nu -predecessors -rainy -liar -assurance -lyrical -##uga -secondly -flattened -ios -parameter -undercover -##mity -bordeaux -punish -ridges -markers -exodus -inactive -hesitate -debbie -nyc -pledge -savoy -nagar -offset -organist -##tium -hesse -marin -converting -##iver -diagram -propulsion -pu -validity -reverted -supportive -##dc -ministries -clans -responds -proclamation -##inae -##ø -##rea -ein -pleading -patriot -sf -birch -islanders -strauss -hates -##dh -brandenburg -concession -rd -##ob -1900s -killings -textbook -antiquity -cinematography -wharf -embarrassing -setup -creed -farmland -inequality -centred -signatures -fallon -370 -##ingham -##uts -ceylon -gazing -directive -laurie -##tern -globally -##uated -##dent -allah -excavation -threads -##cross -148 -frantically -icc -utilize -determines -respiratory -thoughtful -receptions -##dicate -merging -chandra -seine -147 -builders -builds -diagnostic -dev -visibility -goddamn -analyses -dhaka -cho -proves -chancel -concurrent -curiously -canadians -pumped -restoring -1850s -turtles -jaguar -sinister -spinal -traction -declan -vows -1784 -glowed -capitalism -swirling -install -universidad -##lder -##oat -soloist -##genic -##oor -coincidence -beginnings -nissan -dip -resorts -caucasus -combustion -infectious -##eno -pigeon -serpent -##itating -conclude -masked -salad -jew -##gr -surreal -toni -##wc -harmonica -151 -##gins -##etic -##coat -fishermen -intending -bravery -##wave -klaus -titan -wembley -taiwanese -ransom -40th -incorrect -hussein -eyelids -jp -cooke -dramas -utilities -##etta -##print -eisenhower -principally -granada -lana -##rak -openings -concord -##bl -bethany -connie -morality -sega -##mons -##nard -earnings -##kara -##cine -wii -communes -##rel -coma -composing -softened -severed -grapes -##17 -nguyen -analyzed -warlord -hubbard -heavenly -behave -slovenian -##hit -##ony -hailed -filmmakers -trance -caldwell -skye -unrest -coward -likelihood -##aging -bern -sci -taliban -honolulu -propose -##wang -1700 -browser -imagining -cobra -contributes -dukes -instinctively -conan -violinist -##ores -accessories -gradual -##amp -quotes -sioux -##dating -undertake -intercepted -sparkling -compressed -139 -fungus -tombs -haley -imposing -rests -degradation -lincolnshire -retailers -wetlands -tulsa -distributor -dungeon -nun -greenhouse -convey -atlantis -aft -exits -oman -dresser -lyons -##sti -joking -eddy -judgement -omitted -digits -##cts -##game -juniors -##rae -cents -stricken -une -##ngo -wizards -weir -breton -nan -technician -fibers -liking -royalty -##cca -154 -persia -terribly -magician -##rable -##unt -vance -cafeteria -booker -camille -warmer -##static -consume -cavern -gaps -compass -contemporaries -foyer -soothing -graveyard -maj -plunged -blush -##wear -cascade -demonstrates -ordinance -##nov -boyle -##lana -rockefeller -shaken -banjo -izzy -##ense -breathless -vines -##32 -##eman -alterations -chromosome -dwellings -feudal -mole -153 -catalonia -relics -tenant -mandated -##fm -fridge -hats -honesty -patented -raul -heap -cruisers -accusing -enlightenment -infants -wherein -chatham -contractors -zen -affinity -hc -osborne -piston -156 -traps -maturity -##rana -lagos -##zal -peering -##nay -attendant -dealers -protocols -subset -prospects -biographical -##cre -artery -##zers -insignia -nuns -endured -##eration -recommend -schwartz -serbs -berger -cromwell -crossroads -##ctor -enduring -clasped -grounded -##bine -marseille -twitched -abel -choke -https -catalyst -moldova -italians -##tist -disastrous -wee -##oured -##nti -wwf -nope -##piration -##asa -expresses -thumbs -167 -##nza -coca -1781 -cheating -##ption -skipped -sensory -heidelberg -spies -satan -dangers -semifinal -202 -bohemia -whitish -confusing -shipbuilding -relies -surgeons -landings -ravi -baku -moor -suffix -alejandro -##yana -litre -upheld -##unk -rajasthan -##rek -coaster -insists -posture -scenarios -etienne -favoured -appoint -transgender -elephants -poked -greenwood -defences -fulfilled -militant -somali -1758 -chalk -potent -##ucci -migrants -wink -assistants -nos -restriction -activism -niger -##ario -colon -shaun -##sat -daphne -##erated -swam -congregations -reprise -considerations -magnet -playable -xvi -##р -overthrow -tobias -knob -chavez -coding -##mers -propped -katrina -orient -newcomer -##suke -temperate -##pool -farmhouse -interrogation -##vd -committing -##vert -forthcoming -strawberry -joaquin -macau -ponds -shocking -siberia -##cellular -chant -contributors -##nant -##ologists -sped -absorb -hail -1782 -spared -##hore -barbados -karate -opus -originates -saul -##xie -evergreen -leaped -##rock -correlation -exaggerated -weekday -unification -bump -tracing -brig -afb -pathways -utilizing -##ners -mod -mb -disturbance -kneeling -##stad -##guchi -100th -pune -##thy -decreasing -168 -manipulation -miriam -academia -ecosystem -occupational -rbi -##lem -rift -##14 -rotary -stacked -incorporation -awakening -generators -guerrero -racist -##omy -cyber -derivatives -culminated -allie -annals -panzer -sainte -wikipedia -pops -zu -austro -##vate -algerian -politely -nicholson -mornings -educate -tastes -thrill -dartmouth -##gating -db -##jee -regan -differing -concentrating -choreography -divinity -##media -pledged -alexandre -routing -gregor -madeline -##idal -apocalypse -##hora -gunfire -culminating -elves -fined -liang -lam -programmed -tar -guessing -transparency -gabrielle -##gna -cancellation -flexibility -##lining -accession -shea -stronghold -nets -specializes -##rgan -abused -hasan -sgt -ling -exceeding -##₄ -admiration -supermarket -##ark -photographers -specialised -tilt -resonance -hmm -perfume -380 -sami -threatens -garland -botany -guarding -boiled -greet -puppy -russo -supplier -wilmington -vibrant -vijay -##bius -paralympic -grumbled -paige -faa -licking -margins -hurricanes -##gong -fest -grenade -ripping -##uz -counseling -weigh -##sian -needles -wiltshire -edison -costly -##not -fulton -tramway -redesigned -staffordshire -cache -gasping -watkins -sleepy -candidacy -##group -monkeys -timeline -throbbing -##bid -##sos -berth -uzbekistan -vanderbilt -bothering -overturned -ballots -gem -##iger -sunglasses -subscribers -hooker -compelling -ang -exceptionally -saloon -stab -##rdi -carla -terrifying -rom -##vision -coil -##oids -satisfying -vendors -31st -mackay -deities -overlooked -ambient -bahamas -felipe -olympia -whirled -botanist -advertised -tugging -##dden -disciples -morales -unionist -rites -foley -morse -motives -creepy -##₀ -soo -##sz -bargain -highness -frightening -turnpike -tory -reorganization -##cer -depict -biographer -##walk -unopposed -manifesto -##gles -institut -emile -accidental -kapoor -##dam -kilkenny -cortex -lively -##13 -romanesque -jain -shan -cannons -##ood -##ske -petrol -echoing -amalgamated -disappears -cautious -proposes -sanctions -trenton -##ر -flotilla -aus -contempt -tor -canary -cote -theirs -##hun -conceptual -deleted -fascinating -paso -blazing -elf -honourable -hutchinson -##eiro -##outh -##zin -surveyor -tee -amidst -wooded -reissue -intro -##ono -cobb -shelters -newsletter -hanson -brace -encoding -confiscated -dem -caravan -marino -scroll -melodic -cows -imam -##adi -##aneous -northward -searches -biodiversity -cora -310 -roaring -##bers -connell -theologian -halo -compose -pathetic -unmarried -dynamo -##oot -az -calculation -toulouse -deserves -humour -nr -forgiveness -tam -undergone -martyr -pamela -myths -whore -counselor -hicks -290 -heavens -battleship -electromagnetic -##bbs -stellar -establishments -presley -hopped -##chin -temptation -90s -wills -nas -##yuan -nhs -##nya -seminars -##yev -adaptations -gong -asher -lex -indicator -sikh -tobago -cites -goin -##yte -satirical -##gies -characterised -correspond -bubbles -lure -participates -##vid -eruption -skate -therapeutic -1785 -canals -wholesale -defaulted -sac -460 -petit -##zzled -virgil -leak -ravens -256 -portraying -##yx -ghetto -creators -dams -portray -vicente -##rington -fae -namesake -bounty -##arium -joachim -##ota -##iser -aforementioned -axle -snout -depended -dismantled -reuben -480 -##ibly -gallagher -##lau -##pd -earnest -##ieu -##iary -inflicted -objections -##llar -asa -gritted -##athy -jericho -##sea -##was -flick -underside -ceramics -undead -substituted -195 -eastward -undoubtedly -wheeled -chimney -##iche -guinness -cb -##ager -siding -##bell -traitor -baptiste -disguised -inauguration -149 -tipperary -choreographer -perched -warmed -stationary -eco -##ike -##ntes -bacterial -##aurus -flores -phosphate -##core -attacker -invaders -alvin -intersects -a1 -indirectly -immigrated -businessmen -cornelius -valves -narrated -pill -sober -ul -nationale -monastic -applicants -scenery -##jack -161 -motifs -constitutes -cpu -##osh -jurisdictions -sd -tuning -irritation -woven -##uddin -fertility -gao -##erie -antagonist -impatient -glacial -hides -boarded -denominations -interception -##jas -cookie -nicola -##tee -algebraic -marquess -bahn -parole -buyers -bait -turbines -paperwork -bestowed -natasha -renee -oceans -purchases -157 -vaccine -215 -##tock -fixtures -playhouse -integrate -jai -oswald -intellectuals -##cky -booked -nests -mortimer -##isi -obsession -sept -##gler -##sum -440 -scrutiny -simultaneous -squinted -##shin -collects -oven -shankar -penned -remarkably -##я -slips -luggage -spectral -1786 -collaborations -louie -consolidation -##ailed -##ivating -420 -hoover -blackpool -harness -ignition -vest -tails -belmont -mongol -skinner -##nae -visually -mage -derry -##tism -##unce -stevie -transitional -##rdy -redskins -drying -prep -prospective -##21 -annoyance -oversee -##loaded -fills -##books -##iki -announces -fda -scowled -respects -prasad -mystic -tucson -##vale -revue -springer -bankrupt -1772 -aristotle -salvatore -habsburg -##geny -dal -natal -nut -pod -chewing -darts -moroccan -walkover -rosario -lenin -punjabi -##ße -grossed -scattering -wired -invasive -hui -polynomial -corridors -wakes -gina -portrays -##cratic -arid -retreating -erich -irwin -sniper -##dha -linen -lindsey -maneuver -butch -shutting -socio -bounce -commemorative -postseason -jeremiah -pines -275 -mystical -beads -bp -abbas -furnace -bidding -consulted -assaulted -empirical -rubble -enclosure -sob -weakly -cancel -polly -yielded -##emann -curly -prediction -battered -70s -vhs -jacqueline -render -sails -barked -detailing -grayson -riga -sloane -raging -##yah -herbs -bravo -##athlon -alloy -giggle -imminent -suffers -assumptions -waltz -##itate -accomplishments -##ited -bathing -remixed -deception -prefix -##emia -deepest -##tier -##eis -balkan -frogs -##rong -slab -##pate -philosophers -peterborough -grains -imports -dickinson -rwanda -##atics -1774 -dirk -lan -tablets -##rove -clone -##rice -caretaker -hostilities -mclean -##gre -regimental -treasures -norms -impose -tsar -tango -diplomacy -variously -complain -192 -recognise -arrests -1779 -celestial -pulitzer -##dus -bing -libretto -##moor -adele -splash -##rite -expectation -lds -confronts -##izer -spontaneous -harmful -wedge -entrepreneurs -buyer -##ope -bilingual -translate -rugged -conner -circulated -uae -eaton -##gra -##zzle -lingered -lockheed -vishnu -reelection -alonso -##oom -joints -yankee -headline -cooperate -heinz -laureate -invading -##sford -echoes -scandinavian -##dham -hugging -vitamin -salute -micah -hind -trader -##sper -radioactive -##ndra -militants -poisoned -ratified -remark -campeonato -deprived -wander -prop -##dong -outlook -##tani -##rix -##eye -chiang -darcy -##oping -mandolin -spice -statesman -babylon -182 -walled -forgetting -afro -##cap -158 -giorgio -buffer -##polis -planetary -##gis -overlap -terminals -kinda -centenary -##bir -arising -manipulate -elm -ke -1770 -ak -##tad -chrysler -mapped -moose -pomeranian -quad -macarthur -assemblies -shoreline -recalls -stratford -##rted -noticeable -##evic -imp -##rita -##sque -accustomed -supplying -tents -disgusted -vogue -sipped -filters -khz -reno -selecting -luftwaffe -mcmahon -tyne -masterpiece -carriages -collided -dunes -exercised -flare -remembers -muzzle -##mobile -heck -##rson -burgess -lunged -middleton -boycott -bilateral -##sity -hazardous -lumpur -multiplayer -spotlight -jackets -goldman -liege -porcelain -rag -waterford -benz -attracts -hopeful -battling -ottomans -kensington -baked -hymns -cheyenne -lattice -levine -borrow -polymer -clashes -michaels -monitored -commitments -denounced -##25 -##von -cavity -##oney -hobby -akin -##holders -futures -intricate -cornish -patty -##oned -illegally -dolphin -##lag -barlow -yellowish -maddie -apologized -luton -plagued -##puram -nana -##rds -sway -fanny -łodz -##rino -psi -suspicions -hanged -##eding -initiate -charlton -##por -nak -competent -235 -analytical -annex -wardrobe -reservations -##rma -sect -162 -fairfax -hedge -piled -buckingham -uneven -bauer -simplicity -snyder -interpret -accountability -donors -moderately -byrd -continents -##cite -##max -disciple -hr -jamaican -ping -nominees -##uss -mongolian -diver -attackers -eagerly -ideological -pillows -miracles -apartheid -revolver -sulfur -clinics -moran -163 -##enko -ile -katy -rhetoric -##icated -chronology -recycling -##hrer -elongated -mughal -pascal -profiles -vibration -databases -domination -##fare -##rant -matthias -digest -rehearsal -polling -weiss -initiation -reeves -clinging -flourished -impress -ngo -##hoff -##ume -buckley -symposium -rhythms -weed -emphasize -transforming -##taking -##gence -##yman -accountant -analyze -flicker -foil -priesthood -voluntarily -decreases -##80 -##hya -slater -sv -charting -mcgill -##lde -moreno -##iu -besieged -zur -robes -##phic -admitting -api -deported -turmoil -peyton -earthquakes -##ares -nationalists -beau -clair -brethren -interrupt -welch -curated -galerie -requesting -164 -##ested -impending -steward -viper -##vina -complaining -beautifully -brandy -foam -nl -1660 -##cake -alessandro -punches -laced -explanations -##lim -attribute -clit -reggie -discomfort -##cards -smoothed -whales -##cene -adler -countered -duffy -disciplinary -widening -recipe -reliance -conducts -goats -gradient -preaching -##shaw -matilda -quasi -striped -meridian -cannabis -cordoba -certificates -##agh -##tering -graffiti -hangs -pilgrims -repeats -##ych -revive -urine -etat -##hawk -fueled -belts -fuzzy -susceptible -##hang -mauritius -salle -sincere -beers -hooks -##cki -arbitration -entrusted -advise -sniffed -seminar -junk -donnell -processors -principality -strapped -celia -mendoza -everton -fortunes -prejudice -starving -reassigned -steamer -##lund -tuck -evenly -foreman -##ffen -dans -375 -envisioned -slit -##xy -baseman -liberia -rosemary -##weed -electrified -periodically -potassium -stride -contexts -sperm -slade -mariners -influx -bianca -subcommittee -##rane -spilling -icao -estuary -##nock -delivers -iphone -##ulata -isa -mira -bohemian -dessert -##sbury -welcoming -proudly -slowing -##chs -musee -ascension -russ -##vian -waits -##psy -africans -exploit -##morphic -gov -eccentric -crab -peck -##ull -entrances -formidable -marketplace -groom -bolted -metabolism -patton -robbins -courier -payload -endure -##ifier -andes -refrigerator -##pr -ornate -##uca -ruthless -illegitimate -masonry -strasbourg -bikes -adobe -##³ -apples -quintet -willingly -niche -bakery -corpses -energetic -##cliffe -##sser -##ards -177 -centimeters -centro -fuscous -cretaceous -rancho -##yde -andrei -telecom -tottenham -oasis -ordination -vulnerability -presiding -corey -cp -penguins -sims -##pis -malawi -piss -##48 -correction -##cked -##ffle -##ryn -countdown -detectives -psychiatrist -psychedelic -dinosaurs -blouse -##get -choi -vowed -##oz -randomly -##pol -49ers -scrub -blanche -bruins -dusseldorf -##using -unwanted -##ums -212 -dominique -elevations -headlights -om -laguna -##oga -1750 -famously -ignorance -shrewsbury -##aine -ajax -breuning -che -confederacy -greco -overhaul -##screen -paz -skirts -disagreement -cruelty -jagged -phoebe -shifter -hovered -viruses -##wes -mandy -##lined -##gc -landlord -squirrel -dashed -##ι -ornamental -gag -wally -grange -literal -spurs -undisclosed -proceeding -yin -##text -billie -orphan -spanned -humidity -indy -weighted -presentations -explosions -lucian -##tary -vaughn -hindus -##anga -##hell -psycho -171 -daytona -protects -efficiently -rematch -sly -tandem -##oya -rebranded -impaired -hee -metropolis -peach -godfrey -diaspora -ethnicity -prosperous -gleaming -dar -grossing -playback -##rden -stripe -pistols -##tain -births -labelled -##cating -172 -rudy -alba -##onne -aquarium -hostility -##gb -##tase -shudder -sumatra -hardest -lakers -consonant -creeping -demos -homicide -capsule -zeke -liberties -expulsion -pueblo -##comb -trait -transporting -##ddin -##neck -##yna -depart -gregg -mold -ledge -hangar -oldham -playboy -termination -analysts -gmbh -romero -##itic -insist -cradle -filthy -brightness -slash -shootout -deposed -bordering -##truct -isis -microwave -tumbled -sheltered -cathy -werewolves -messy -andersen -convex -clapped -clinched -satire -wasting -edo -vc -rufus -##jak -mont -##etti -poznan -##keeping -restructuring -transverse -##rland -azerbaijani -slovene -gestures -roommate -choking -shear -##quist -vanguard -oblivious -##hiro -disagreed -baptism -##lich -coliseum -##aceae -salvage -societe -cory -locke -relocation -relying -versailles -ahl -swelling -##elo -cheerful -##word -##edes -gin -sarajevo -obstacle -diverted -##nac -messed -thoroughbred -fluttered -utrecht -chewed -acquaintance -assassins -dispatch -mirza -##wart -nike -salzburg -swell -yen -##gee -idle -ligue -samson -##nds -##igh -playful -spawned -##cise -tease -##case -burgundy -##bot -stirring -skeptical -interceptions -marathi -##dies -bedrooms -aroused -pinch -##lik -preferences -tattoos -buster -digitally -projecting -rust -##ital -kitten -priorities -addison -pseudo -##guard -dusk -icons -sermon -##psis -##iba -bt -##lift -##xt -ju -truce -rink -##dah -##wy -defects -psychiatry -offences -calculate -glucose -##iful -##rized -##unda -francaise -##hari -richest -warwickshire -carly -1763 -purity -redemption -lending -##cious -muse -bruises -cerebral -aero -carving -##name -preface -terminology -invade -monty -##int -anarchist -blurred -##iled -rossi -treats -guts -shu -foothills -ballads -undertaking -premise -cecilia -affiliates -blasted -conditional -wilder -minors -drone -rudolph -buffy -swallowing -horton -attested -##hop -rutherford -howell -primetime -livery -penal -##bis -minimize -hydro -wrecked -wrought -palazzo -##gling -cans -vernacular -friedman -nobleman -shale -walnut -danielle -##ection -##tley -sears -##kumar -chords -lend -flipping -streamed -por -dracula -gallons -sacrifices -gamble -orphanage -##iman -mckenzie -##gible -boxers -daly -##balls -##ان -208 -##ific -##rative -##iq -exploited -slated -##uity -circling -hillary -pinched -goldberg -provost -campaigning -lim -piles -ironically -jong -mohan -successors -usaf -##tem -##ught -autobiographical -haute -preserves -##ending -acquitted -comparisons -203 -hydroelectric -gangs -cypriot -torpedoes -rushes -chrome -derive -bumps -instability -fiat -pets -##mbe -silas -dye -reckless -settler -##itation -info -heats -##writing -176 -canonical -maltese -fins -mushroom -stacy -aspen -avid -##kur -##loading -vickers -gaston -hillside -statutes -wilde -gail -kung -sabine -comfortably -motorcycles -##rgo -169 -pneumonia -fetch -##sonic -axel -faintly -parallels -##oop -mclaren -spouse -compton -interdisciplinary -miner -##eni -181 -clamped -##chal -##llah -separates -versa -##mler -scarborough -labrador -##lity -##osing -rutgers -hurdles -como -166 -burt -divers -##100 -wichita -cade -coincided -##erson -bruised -mla -##pper -vineyard -##ili -##brush -notch -mentioning -jase -hearted -kits -doe -##acle -pomerania -##ady -ronan -seizure -pavel -problematic -##zaki -domenico -##ulin -catering -penelope -dependence -parental -emilio -ministerial -atkinson -##bolic -clarkson -chargers -colby -grill -peeked -arises -summon -##aged -fools -##grapher -faculties -qaeda -##vial -garner -refurbished -##hwa -geelong -disasters -nudged -bs -shareholder -lori -algae -reinstated -rot -##ades -##nous -invites -stainless -183 -inclusive -##itude -diocesan -til -##icz -denomination -##xa -benton -floral -registers -##ider -##erman -##kell -absurd -brunei -guangzhou -hitter -retaliation -##uled -##eve -blanc -nh -consistency -contamination -##eres -##rner -dire -palermo -broadcasters -diaries -inspire -vols -brewer -tightening -ky -mixtape -hormone -##tok -stokes -##color -##dly -##ssi -pg -##ometer -##lington -sanitation -##tility -intercontinental -apps -##adt -¹⁄₂ -cylinders -economies -favourable -unison -croix -gertrude -odyssey -vanity -dangling -##logists -upgrades -dice -middleweight -practitioner -##ight -206 -henrik -parlor -orion -angered -lac -python -blurted -##rri -sensual -intends -swings -angled -##phs -husky -attain -peerage -precinct -textiles -cheltenham -shuffled -dai -confess -tasting -bhutan -##riation -tyrone -segregation -abrupt -ruiz -##rish -smirked -blackwell -confidential -browning -amounted -##put -vase -scarce -fabulous -raided -staple -guyana -unemployed -glider -shay -##tow -carmine -troll -intervene -squash -superstar -##uce -cylindrical -len -roadway -researched -handy -##rium -##jana -meta -lao -declares -##rring -##tadt -##elin -##kova -willem -shrubs -napoleonic -realms -skater -qi -volkswagen -##ł -tad -hara -archaeologist -awkwardly -eerie -##kind -wiley -##heimer -##24 -titus -organizers -cfl -crusaders -lama -usb -vent -enraged -thankful -occupants -maximilian -##gaard -possessing -textbooks -##oran -collaborator -quaker -##ulo -avalanche -mono -silky -straits -isaiah -mustang -surged -resolutions -potomac -descend -cl -kilograms -plato -strains -saturdays -##olin -bernstein -##ype -holstein -ponytail -##watch -belize -conversely -heroine -perpetual -##ylus -charcoal -piedmont -glee -negotiating -backdrop -prologue -##jah -##mmy -pasadena -climbs -ramos -sunni -##holm -##tner -##tri -anand -deficiency -hertfordshire -stout -##avi -aperture -orioles -##irs -doncaster -intrigued -bombed -coating -otis -##mat -cocktail -##jit -##eto -amir -arousal -sar -##proof -##act -##ories -dixie -pots -##bow -whereabouts -159 -##fted -drains -bullying -cottages -scripture -coherent -fore -poe -appetite -##uration -sampled -##ators -##dp -derrick -rotor -jays -peacock -installment -##rro -advisors -##coming -rodeo -scotch -##mot -##db -##fen -##vant -ensued -rodrigo -dictatorship -martyrs -twenties -##н -towed -incidence -marta -rainforest -sai -scaled -##cles -oceanic -qualifiers -symphonic -mcbride -dislike -generalized -aubrey -colonization -##iation -##lion -##ssing -disliked -lublin -salesman -##ulates -spherical -whatsoever -sweating -avalon -contention -punt -severity -alderman -atari -##dina -##grant -##rop -scarf -seville -vertices -annexation -fairfield -fascination -inspiring -launches -palatinate -regretted -##rca -feral -##iom -elk -nap -olsen -reddy -yong -##leader -##iae -garment -transports -feng -gracie -outrage -viceroy -insides -##esis -breakup -grady -organizer -softer -grimaced -222 -murals -galicia -arranging -vectors -##rsten -bas -##sb -##cens -sloan -##eka -bitten -ara -fender -nausea -bumped -kris -banquet -comrades -detector -persisted -##llan -adjustment -endowed -cinemas -##shot -sellers -##uman -peek -epa -kindly -neglect -simpsons -talon -mausoleum -runaway -hangul -lookout -##cic -rewards -coughed -acquainted -chloride -##ald -quicker -accordion -neolithic -##qa -artemis -coefficient -lenny -pandora -tx -##xed -ecstasy -litter -segunda -chairperson -gemma -hiss -rumor -vow -nasal -antioch -compensate -patiently -transformers -##eded -judo -morrow -penis -posthumous -philips -bandits -husbands -denote -flaming -##any -##phones -langley -yorker -1760 -walters -##uo -##kle -gubernatorial -fatty -samsung -leroy -outlaw -##nine -unpublished -poole -jakob -##ᵢ -##ₙ -crete -distorted -superiority -##dhi -intercept -crust -mig -claus -crashes -positioning -188 -stallion -301 -frontal -armistice -##estinal -elton -aj -encompassing -camel -commemorated -malaria -woodward -calf -cigar -penetrate -##oso -willard -##rno -##uche -illustrate -amusing -convergence -noteworthy -##lma -##rva -journeys -realise -manfred -##sable -410 -##vocation -hearings -fiance -##posed -educators -provoked -adjusting -##cturing -modular -stockton -paterson -vlad -rejects -electors -selena -maureen -##tres -uber -##rce -swirled -##num -proportions -nanny -pawn -naturalist -parma -apostles -awoke -ethel -wen -##bey -monsoon -overview -##inating -mccain -rendition -risky -adorned -##ih -equestrian -germain -nj -conspicuous -confirming -##yoshi -shivering -##imeter -milestone -rumours -flinched -bounds -smacked -token -##bei -lectured -automobiles -##shore -impacted -##iable -nouns -nero -##leaf -ismail -prostitute -trams -##lace -bridget -sud -stimulus -impressions -reins -revolves -##oud -##gned -giro -honeymoon -##swell -criterion -##sms -##uil -libyan -prefers -##osition -211 -preview -sucks -accusation -bursts -metaphor -diffusion -tolerate -faye -betting -cinematographer -liturgical -specials -bitterly -humboldt -##ckle -flux -rattled -##itzer -archaeologists -odor -authorised -marshes -discretion -##ов -alarmed -archaic -inverse -##leton -explorers -##pine -drummond -tsunami -woodlands -##minate -##tland -booklet -insanity -owning -insert -crafted -calculus -##tore -receivers -##bt -stung -##eca -##nched -prevailing -travellers -eyeing -lila -graphs -##borne -178 -julien -##won -morale -adaptive -therapist -erica -cw -libertarian -bowman -pitches -vita -##ional -crook -##ads -##entation -caledonia -mutiny -##sible -1840s -automation -##ß -flock -##pia -ironic -pathology -##imus -remarried -##22 -joker -withstand -energies -##att -shropshire -hostages -madeleine -tentatively -conflicting -mateo -recipes -euros -ol -mercenaries -nico -##ndon -albuquerque -augmented -mythical -bel -freud -##child -cough -##lica -365 -freddy -lillian -genetically -nuremberg -calder -209 -bonn -outdoors -paste -suns -urgency -vin -restraint -tyson -##cera -##selle -barrage -bethlehem -kahn -##par -mounts -nippon -barony -happier -ryu -makeshift -sheldon -blushed -castillo -barking -listener -taped -bethel -fluent -headlines -pornography -rum -disclosure -sighing -mace -doubling -gunther -manly -##plex -rt -interventions -physiological -forwards -emerges -##tooth -##gny -compliment -rib -recession -visibly -barge -faults -connector -exquisite -prefect -##rlin -patio -##cured -elevators -brandt -italics -pena -173 -wasp -satin -ea -botswana -graceful -respectable -##jima -##rter -##oic -franciscan -generates -##dl -alfredo -disgusting -##olate -##iously -sherwood -warns -cod -promo -cheryl -sino -##ة -##escu -twitch -##zhi -brownish -thom -ortiz -##dron -densely -##beat -carmel -reinforce -##bana -187 -anastasia -downhill -vertex -contaminated -remembrance -harmonic -homework -##sol -fiancee -gears -olds -angelica -loft -ramsay -quiz -colliery -sevens -##cape -autism -##hil -walkway -##boats -ruben -abnormal -ounce -khmer -##bbe -zachary -bedside -morphology -punching -##olar -sparrow -convinces -##35 -hewitt -queer -remastered -rods -mabel -solemn -notified -lyricist -symmetric -##xide -174 -encore -passports -wildcats -##uni -baja -##pac -mildly -##ease -bleed -commodity -mounds -glossy -orchestras -##omo -damian -prelude -ambitions -##vet -awhile -remotely -##aud -asserts -imply -##iques -distinctly -modelling -remedy -##dded -windshield -dani -xiao -##endra -audible -powerplant -1300 -invalid -elemental -acquisitions -##hala -immaculate -libby -plata -smuggling -ventilation -denoted -minh -##morphism -430 -differed -dion -kelley -lore -mocking -sabbath -spikes -hygiene -drown -runoff -stylized -tally -liberated -aux -interpreter -righteous -aba -siren -reaper -pearce -millie -##cier -##yra -gaius -##iso -captures -##ttering -dorm -claudio -##sic -benches -knighted -blackness -##ored -discount -fumble -oxidation -routed -##ς -novak -perpendicular -spoiled -fracture -splits -##urt -pads -topology -##cats -axes -fortunate -offenders -protestants -esteem -221 -broadband -convened -frankly -hound -prototypes -isil -facilitated -keel -##sher -sahara -awaited -bubba -orb -prosecutors -186 -hem -520 -##xing -relaxing -remnant -romney -sorted -slalom -stefano -ulrich -##active -exemption -folder -pauses -foliage -hitchcock -epithet -204 -criticisms -##aca -ballistic -brody -hinduism -chaotic -youths -equals -##pala -pts -thicker -analogous -capitalist -improvised -overseeing -sinatra -ascended -beverage -##tl -straightforward -##kon -curran -##west -bois -325 -induce -surveying -emperors -sax -unpopular -##kk -cartoonist -fused -##mble -unto -##yuki -localities -##cko -##ln -darlington -slain -academie -lobbying -sediment -puzzles -##grass -defiance -dickens -manifest -tongues -alumnus -arbor -coincide -184 -appalachian -mustafa -examiner -cabaret -traumatic -yves -bracelet -draining -heroin -magnum -baths -odessa -consonants -mitsubishi -##gua -kellan -vaudeville -##fr -joked -null -straps -probation -##ław -ceded -interfaces -##pas -##zawa -blinding -viet -224 -rothschild -museo -640 -huddersfield -##vr -tactic -##storm -brackets -dazed -incorrectly -##vu -reg -glazed -fearful -manifold -benefited -irony -##sun -stumbling -##rte -willingness -balkans -mei -wraps -##aba -injected -##lea -gu -syed -harmless -##hammer -bray -takeoff -poppy -timor -cardboard -astronaut -purdue -weeping -southbound -cursing -stalls -diagonal -##neer -lamar -bryce -comte -weekdays -harrington -##uba -negatively -##see -lays -grouping -##cken -##henko -affirmed -halle -modernist -##lai -hodges -smelling -aristocratic -baptized -dismiss -justification -oilers -##now -coupling -qin -snack -healer -##qing -gardener -layla -battled -formulated -stephenson -gravitational -##gill -##jun -1768 -granny -coordinating -suites -##cd -##ioned -monarchs -##cote -##hips -sep -blended -apr -barrister -deposition -fia -mina -policemen -paranoid -##pressed -churchyard -covert -crumpled -creep -abandoning -tr -transmit -conceal -barr -understands -readiness -spire -##cology -##enia -##erry -610 -startling -unlock -vida -bowled -slots -##nat -##islav -spaced -trusting -admire -rig -##ink -slack -##70 -mv -207 -casualty -##wei -classmates -##odes -##rar -##rked -amherst -furnished -evolve -foundry -menace -mead -##lein -flu -wesleyan -##kled -monterey -webber -##vos -wil -##mith -##на -bartholomew -justices -restrained -##cke -amenities -191 -mediated -sewage -trenches -ml -mainz -##thus -1800s -##cula -##inski -caine -bonding -213 -converts -spheres -superseded -marianne -crypt -sweaty -ensign -historia -##br -spruce -##post -##ask -forks -thoughtfully -yukon -pamphlet -ames -##uter -karma -##yya -bryn -negotiation -sighs -incapable -##mbre -##ntial -actresses -taft -##mill -luce -prevailed -##amine -1773 -motionless -envoy -testify -investing -sculpted -instructors -provence -kali -cullen -horseback -##while -goodwin -##jos -gaa -norte -##ldon -modify -wavelength -abd -214 -skinned -sprinter -forecast -scheduling -marries -squared -tentative -##chman -boer -##isch -bolts -swap -fisherman -assyrian -impatiently -guthrie -martins -murdoch -194 -tanya -nicely -dolly -lacy -med -##45 -syn -decks -fashionable -millionaire -##ust -surfing -##ml -##ision -heaved -tammy -consulate -attendees -routinely -197 -fuse -saxophonist -backseat -malaya -##lord -scowl -tau -##ishly -193 -sighted -steaming -##rks -303 -911 -##holes -##hong -ching -##wife -bless -conserved -jurassic -stacey -unix -zion -chunk -rigorous -blaine -198 -peabody -slayer -dismay -brewers -nz -##jer -det -##glia -glover -postwar -int -penetration -sylvester -imitation -vertically -airlift -heiress -knoxville -viva -##uin -390 -macon -##rim -##fighter -##gonal -janice -##orescence -##wari -marius -belongings -leicestershire -196 -blanco -inverted -preseason -sanity -sobbing -##due -##elt -##dled -collingwood -regeneration -flickering -shortest -##mount -##osi -feminism -##lat -sherlock -cabinets -fumbled -northbound -precedent -snaps -##mme -researching -##akes -guillaume -insights -manipulated -vapor -neighbour -sap -gangster -frey -f1 -stalking -scarcely -callie -barnett -tendencies -audi -doomed -assessing -slung -panchayat -ambiguous -bartlett -##etto -distributing -violating -wolverhampton -##hetic -swami -histoire -##urus -liable -pounder -groin -hussain -larsen -popping -surprises -##atter -vie -curt -##station -mute -relocate -musicals -authorization -richter -##sef -immortality -tna -bombings -##press -deteriorated -yiddish -##acious -robbed -colchester -cs -pmid -ao -verified -balancing -apostle -swayed -recognizable -oxfordshire -retention -nottinghamshire -contender -judd -invitational -shrimp -uhf -##icient -cleaner -longitudinal -tanker -##mur -acronym -broker -koppen -sundance -suppliers -##gil -4000 -clipped -fuels -petite -##anne -landslide -helene -diversion -populous -landowners -auspices -melville -quantitative -##xes -ferries -nicky -##llus -doo -haunting -roche -carver -downed -unavailable -##pathy -approximation -hiroshima -##hue -garfield -valle -comparatively -keyboardist -traveler -##eit -congestion -calculating -subsidiaries -##bate -serb -modernization -fairies -deepened -ville -averages -##lore -inflammatory -tonga -##itch -co₂ -squads -##hea -gigantic -serum -enjoyment -retailer -verona -35th -cis -##phobic -magna -technicians -##vati -arithmetic -##sport -levin -##dation -amtrak -chow -sienna -##eyer -backstage -entrepreneurship -##otic -learnt -tao -##udy -worcestershire -formulation -baggage -hesitant -bali -sabotage -##kari -barren -enhancing -murmur -pl -freshly -putnam -syntax -aces -medicines -resentment -bandwidth -##sier -grins -chili -guido -##sei -framing -implying -gareth -lissa -genevieve -pertaining -admissions -geo -thorpe -proliferation -sato -bela -analyzing -parting -##gor -awakened -##isman -huddled -secrecy -##kling -hush -gentry -540 -dungeons -##ego -coasts -##utz -sacrificed -##chule -landowner -mutually -prevalence -programmer -adolescent -disrupted -seaside -gee -trusts -vamp -georgie -##nesian -##iol -schedules -sindh -##market -etched -hm -sparse -bey -beaux -scratching -gliding -unidentified -216 -collaborating -gems -jesuits -oro -accumulation -shaping -mbe -anal -##xin -231 -enthusiasts -newscast -##egan -janata -dewey -parkinson -179 -ankara -biennial -towering -dd -inconsistent -950 -##chet -thriving -terminate -cabins -furiously -eats -advocating -donkey -marley -muster -phyllis -leiden -##user -grassland -glittering -iucn -loneliness -217 -memorandum -armenians -##ddle -popularized -rhodesia -60s -lame -##illon -sans -bikini -header -orbits -##xx -##finger -##ulator -sharif -spines -biotechnology -strolled -naughty -yates -##wire -fremantle -milo -##mour -abducted -removes -##atin -humming -wonderland -##chrome -##ester -hume -pivotal -##rates -armand -grams -believers -elector -rte -apron -bis -scraped -##yria -endorsement -initials -##llation -eps -dotted -hints -buzzing -emigration -nearer -##tom -indicators -##ulu -coarse -neutron -protectorate -##uze -directional -exploits -pains -loire -1830s -proponents -guggenheim -rabbits -ritchie -305 -hectare -inputs -hutton -##raz -verify -##ako -boilers -longitude -##lev -skeletal -yer -emilia -citrus -compromised -##gau -pokemon -prescription -paragraph -eduard -cadillac -attire -categorized -kenyan -weddings -charley -##bourg -entertain -monmouth -##lles -nutrients -davey -mesh -incentive -practised -ecosystems -kemp -subdued -overheard -##rya -bodily -maxim -##nius -apprenticeship -ursula -##fight -lodged -rug -silesian -unconstitutional -patel -inspected -coyote -unbeaten -##hak -34th -disruption -convict -parcel -##cl -##nham -collier -implicated -mallory -##iac -##lab -susannah -winkler -##rber -shia -phelps -sediments -graphical -robotic -##sner -adulthood -mart -smoked -##isto -kathryn -clarified -##aran -divides -convictions -oppression -pausing -burying -##mt -federico -mathias -eileen -##tana -kite -hunched -##acies -189 -##atz -disadvantage -liza -kinetic -greedy -paradox -yokohama -dowager -trunks -ventured -##gement -gupta -vilnius -olaf -##thest -crimean -hopper -##ej -progressively -arturo -mouthed -arrondissement -##fusion -rubin -simulcast -oceania -##orum -##stra -##rred -busiest -intensely -navigator -cary -##vine -##hini -##bies -fife -rowe -rowland -posing -insurgents -shafts -lawsuits -activate -conor -inward -culturally -garlic -265 -##eering -eclectic -##hui -##kee -##nl -furrowed -vargas -meteorological -rendezvous -##aus -culinary -commencement -##dition -quota -##notes -mommy -salaries -overlapping -mule -##iology -##mology -sums -wentworth -##isk -##zione -mainline -subgroup -##illy -hack -plaintiff -verdi -bulb -differentiation -engagements -multinational -supplemented -bertrand -caller -regis -##naire -##sler -##arts -##imated -blossom -propagation -kilometer -viaduct -vineyards -##uate -beckett -optimization -golfer -songwriters -seminal -semitic -thud -volatile -evolving -ridley -##wley -trivial -distributions -scandinavia -jiang -##ject -wrestled -insistence -##dio -emphasizes -napkin -##ods -adjunct -rhyme -##ricted -##eti -hopeless -surrounds -tremble -32nd -smoky -##ntly -oils -medicinal -padded -steer -wilkes -219 -255 -concessions -hue -uniquely -blinded -landon -yahoo -##lane -hendrix -commemorating -dex -specify -chicks -##ggio -intercity -1400 -morley -##torm -highlighting -##oting -pang -oblique -stalled -##liner -flirting -newborn -1769 -bishopric -shaved -232 -currie -##ush -dharma -spartan -##ooped -favorites -smug -novella -sirens -abusive -creations -espana -##lage -paradigm -semiconductor -sheen -##rdo -##yen -##zak -nrl -renew -##pose -##tur -adjutant -marches -norma -##enity -ineffective -weimar -grunt -##gat -lordship -plotting -expenditure -infringement -lbs -refrain -av -mimi -mistakenly -postmaster -1771 -##bara -ras -motorsports -tito -199 -subjective -##zza -bully -stew -##kaya -prescott -1a -##raphic -##zam -bids -styling -paranormal -reeve -sneaking -exploding -katz -akbar -migrant -syllables -indefinitely -##ogical -destroys -replaces -applause -##phine -pest -##fide -218 -articulated -bertie -##thing -##cars -##ptic -courtroom -crowley -aesthetics -cummings -tehsil -hormones -titanic -dangerously -##ibe -stadion -jaenelle -auguste -ciudad -##chu -mysore -partisans -##sio -lucan -philipp -##aly -debating -henley -interiors -##rano -##tious -homecoming -beyonce -usher -henrietta -prepares -weeds -##oman -ely -plucked -##pire -##dable -luxurious -##aq -artifact -password -pasture -juno -maddy -minsk -##dder -##ologies -##rone -assessments -martian -royalist -1765 -examines -##mani -##rge -nino -223 -parry -scooped -relativity -##eli -##uting -##cao -congregational -noisy -traverse -##agawa -strikeouts -nickelodeon -obituary -transylvania -binds -depictions -polk -trolley -##yed -##lard -breeders -##under -dryly -hokkaido -1762 -strengths -stacks -bonaparte -connectivity -neared -prostitutes -stamped -anaheim -gutierrez -sinai -##zzling -bram -fresno -madhya -##86 -proton -##lena -##llum -##phon -reelected -wanda -##anus -##lb -ample -distinguishing -##yler -grasping -sermons -tomato -bland -stimulation -avenues -##eux -spreads -scarlett -fern -pentagon -assert -baird -chesapeake -ir -calmed -distortion -fatalities -##olis -correctional -pricing -##astic -##gina -prom -dammit -ying -collaborate -##chia -welterweight -33rd -pointer -substitution -bonded -umpire -communicating -multitude -paddle -##obe -federally -intimacy -##insky -betray -ssr -##lett -##lean -##lves -##therapy -airbus -##tery -functioned -ud -bearer -biomedical -netflix -##hire -##nca -condom -brink -ik -##nical -macy -##bet -flap -gma -experimented -jelly -lavender -##icles -##ulia -munro -##mian -##tial -rye -##rle -60th -gigs -hottest -rotated -predictions -fuji -bu -##erence -##omi -barangay -##fulness -##sas -clocks -##rwood -##liness -cereal -roe -wight -decker -uttered -babu -onion -xml -forcibly -##df -petra -sarcasm -hartley -peeled -storytelling -##42 -##xley -##ysis -##ffa -fibre -kiel -auditor -fig -harald -greenville -##berries -geographically -nell -quartz -##athic -cemeteries -##lr -crossings -nah -holloway -reptiles -chun -sichuan -snowy -660 -corrections -##ivo -zheng -ambassadors -blacksmith -fielded -fluids -hardcover -turnover -medications -melvin -academies -##erton -ro -roach -absorbing -spaniards -colton -##founded -outsider -espionage -kelsey -245 -edible -##ulf -dora -establishes -##sham -##tries -contracting -##tania -cinematic -costello -nesting -##uron -connolly -duff -##nology -mma -##mata -fergus -sexes -gi -optics -spectator -woodstock -banning -##hee -##fle -differentiate -outfielder -refinery -226 -312 -gerhard -horde -lair -drastically -##udi -landfall -##cheng -motorsport -odi -##achi -predominant -quay -skins -##ental -edna -harshly -complementary -murdering -##aves -wreckage -##90 -ono -outstretched -lennox -munitions -galen -reconcile -470 -scalp -bicycles -gillespie -questionable -rosenberg -guillermo -hostel -jarvis -kabul -volvo -opium -yd -##twined -abuses -decca -outpost -##cino -sensible -neutrality -##64 -ponce -anchorage -atkins -turrets -inadvertently -disagree -libre -vodka -reassuring -weighs -##yal -glide -jumper -ceilings -repertory -outs -stain -##bial -envy -##ucible -smashing -heightened -policing -hyun -mixes -lai -prima -##ples -celeste -##bina -lucrative -intervened -kc -manually -##rned -stature -staffed -bun -bastards -nairobi -priced -##auer -thatcher -##kia -tripped -comune -##ogan -##pled -brasil -incentives -emanuel -hereford -musica -##kim -benedictine -biennale -##lani -eureka -gardiner -rb -knocks -sha -##ael -##elled -##onate -efficacy -ventura -masonic -sanford -maize -leverage -##feit -capacities -santana -##aur -novelty -vanilla -##cter -##tour -benin -##oir -##rain -neptune -drafting -tallinn -##cable -humiliation -##boarding -schleswig -fabian -bernardo -liturgy -spectacle -sweeney -pont -routledge -##tment -cosmos -ut -hilt -sleek -universally -##eville -##gawa -typed -##dry -favors -allegheny -glaciers -##rly -recalling -aziz -##log -parasite -requiem -auf -##berto -##llin -illumination -##breaker -##issa -festivities -bows -govern -vibe -vp -333 -sprawled -larson -pilgrim -bwf -leaping -##rts -##ssel -alexei -greyhound -hoarse -##dler -##oration -seneca -##cule -gaping -##ulously -##pura -cinnamon -##gens -##rricular -craven -fantasies -houghton -engined -reigned -dictator -supervising -##oris -bogota -commentaries -unnatural -fingernails -spirituality -tighten -##tm -canadiens -protesting -intentional -cheers -sparta -##ytic -##iere -##zine -widen -belgarath -controllers -dodd -iaaf -navarre -##ication -defect -squire -steiner -whisky -##mins -560 -inevitably -tome -##gold -chew -##uid -##lid -elastic -##aby -streaked -alliances -jailed -regal -##ined -##phy -czechoslovak -narration -absently -##uld -bluegrass -guangdong -quran -criticizing -hose -hari -##liest -##owa -skier -streaks -deploy -##lom -raft -bose -dialed -huff -##eira -haifa -simplest -bursting -endings -ib -sultanate -##titled -franks -whitman -ensures -sven -##ggs -collaborators -forster -organising -ui -banished -napier -injustice -teller -layered -thump -##otti -roc -battleships -evidenced -fugitive -sadie -robotics -##roud -equatorial -geologist -##iza -yielding -##bron -##sr -internationale -mecca -##diment -sbs -skyline -toad -uploaded -reflective -undrafted -lal -leafs -bayern -##dai -lakshmi -shortlisted -##stick -##wicz -camouflage -donate -af -christi -lau -##acio -disclosed -nemesis -1761 -assemble -straining -northamptonshire -tal -##asi -bernardino -premature -heidi -42nd -coefficients -galactic -reproduce -buzzed -sensations -zionist -monsieur -myrtle -##eme -archery -strangled -musically -viewpoint -antiquities -bei -trailers -seahawks -cured -pee -preferring -tasmanian -lange -sul -##mail -##working -colder -overland -lucivar -massey -gatherings -haitian -##smith -disapproval -flaws -##cco -##enbach -1766 -npr -##icular -boroughs -creole -forums -techno -1755 -dent -abdominal -streetcar -##eson -##stream -procurement -gemini -predictable -##tya -acheron -christoph -feeder -fronts -vendor -bernhard -jammu -tumors -slang -##uber -goaltender -twists -curving -manson -vuelta -mer -peanut -confessions -pouch -unpredictable -allowance -theodor -vascular -##factory -bala -authenticity -metabolic -coughing -nanjing -##cea -pembroke -##bard -splendid -36th -ff -hourly -##ahu -elmer -handel -##ivate -awarding -thrusting -dl -experimentation -##hesion -##46 -caressed -entertained -steak -##rangle -biologist -orphans -baroness -oyster -stepfather -##dridge -mirage -reefs -speeding -##31 -barons -1764 -227 -inhabit -preached -repealed -##tral -honoring -boogie -captives -administer -johanna -##imate -gel -suspiciously -1767 -sobs -##dington -backbone -hayward -garry -##folding -##nesia -maxi -##oof -##ppe -ellison -galileo -##stand -crimea -frenzy -amour -bumper -matrices -natalia -baking -garth -palestinians -##grove -smack -conveyed -ensembles -gardening -##manship -##rup -##stituting -1640 -harvesting -topography -jing -shifters -dormitory -##carriage -##lston -ist -skulls -##stadt -dolores -jewellery -sarawak -##wai -##zier -fences -christy -confinement -tumbling -credibility -fir -stench -##bria -##plication -##nged -##sam -virtues -##belt -marjorie -pba -##eem -##made -celebrates -schooner -agitated -barley -fulfilling -anthropologist -##pro -restrict -novi -regulating -##nent -padres -##rani -##hesive -loyola -tabitha -milky -olson -proprietor -crambidae -guarantees -intercollegiate -ljubljana -hilda -##sko -ignorant -hooded -##lts -sardinia -##lidae -##vation -frontman -privileged -witchcraft -##gp -jammed -laude -poking -##than -bracket -amazement -yunnan -##erus -maharaja -linnaeus -264 -commissioning -milano -peacefully -##logies -akira -rani -regulator -##36 -grasses -##rance -luzon -crows -compiler -gretchen -seaman -edouard -tab -buccaneers -ellington -hamlets -whig -socialists -##anto -directorial -easton -mythological -##kr -##vary -rhineland -semantic -taut -dune -inventions -succeeds -##iter -replication -branched -##pired -jul -prosecuted -kangaroo -penetrated -##avian -middlesbrough -doses -bleak -madam -predatory -relentless -##vili -reluctance -##vir -hailey -crore -silvery -1759 -monstrous -swimmers -transmissions -hawthorn -informing -##eral -toilets -caracas -crouch -kb -##sett -295 -cartel -hadley -##aling -alexia -yvonne -##biology -cinderella -eton -superb -blizzard -stabbing -industrialist -maximus -##gm -##orus -groves -maud -clade -oversized -comedic -##bella -rosen -nomadic -fulham -montane -beverages -galaxies -redundant -swarm -##rot -##folia -##llis -buckinghamshire -fen -bearings -bahadur -##rom -gilles -phased -dynamite -faber -benoit -vip -##ount -##wd -booking -fractured -tailored -anya -spices -westwood -cairns -auditions -inflammation -steamed -##rocity -##acion -##urne -skyla -thereof -watford -torment -archdeacon -transforms -lulu -demeanor -fucked -serge -##sor -mckenna -minas -entertainer -##icide -caress -originate -residue -##sty -1740 -##ilised -##org -beech -##wana -subsidies -##ghton -emptied -gladstone -ru -firefighters -voodoo -##rcle -het -nightingale -tamara -edmond -ingredient -weaknesses -silhouette -285 -compatibility -withdrawing -hampson -##mona -anguish -giggling -##mber -bookstore -##jiang -southernmost -tilting -##vance -bai -economical -rf -briefcase -dreadful -hinted -projections -shattering -totaling -##rogate -analogue -indicted -periodical -fullback -##dman -haynes -##tenberg -##ffs -##ishment -1745 -thirst -stumble -penang -vigorous -##ddling -##kor -##lium -octave -##ove -##enstein -##inen -##ones -siberian -##uti -cbn -repeal -swaying -##vington -khalid -tanaka -unicorn -otago -plastered -lobe -riddle -##rella -perch -##ishing -croydon -filtered -graeme -tripoli -##ossa -crocodile -##chers -sufi -mined -##tung -inferno -lsu -##phi -swelled -utilizes -£2 -cale -periodicals -styx -hike -informally -coop -lund -##tidae -ala -hen -qui -transformations -disposed -sheath -chickens -##cade -fitzroy -sas -silesia -unacceptable -odisha -1650 -sabrina -pe -spokane -ratios -athena -massage -shen -dilemma -##drum -##riz -##hul -corona -doubtful -niall -##pha -##bino -fines -cite -acknowledging -bangor -ballard -bathurst -##resh -huron -mustered -alzheimer -garments -kinase -tyre -warship -##cp -flashback -pulmonary -braun -cheat -kamal -cyclists -constructions -grenades -ndp -traveller -excuses -stomped -signalling -trimmed -futsal -mosques -relevance -##wine -wta -##23 -##vah -##lter -hoc -##riding -optimistic -##´s -deco -sim -interacting -rejecting -moniker -waterways -##ieri -##oku -mayors -gdansk -outnumbered -pearls -##ended -##hampton -fairs -totals -dominating -262 -notions -stairway -compiling -pursed -commodities -grease -yeast -##jong -carthage -griffiths -residual -amc -contraction -laird -sapphire -##marine -##ivated -amalgamation -dissolve -inclination -lyle -packaged -altitudes -suez -canons -graded -lurched -narrowing -boasts -guise -wed -enrico -##ovsky -rower -scarred -bree -cub -iberian -protagonists -bargaining -proposing -trainers -voyages -vans -fishes -##aea -##ivist -##verance -encryption -artworks -kazan -sabre -cleopatra -hepburn -rotting -supremacy -mecklenburg -##brate -burrows -hazards -outgoing -flair -organizes -##ctions -scorpion -##usions -boo -234 -chevalier -dunedin -slapping -##34 -ineligible -pensions -##38 -##omic -manufactures -emails -bismarck -238 -weakening -blackish -ding -mcgee -quo -##rling -northernmost -xx -manpower -greed -sampson -clicking -##ange -##horpe -##inations -##roving -torre -##eptive -##moral -symbolism -38th -asshole -meritorious -outfits -splashed -biographies -sprung -astros -##tale -302 -737 -filly -raoul -nw -tokugawa -linden -clubhouse -##apa -tracts -romano -##pio -putin -tags -##note -chained -dickson -gunshot -moe -gunn -rashid -##tails -zipper -##bas -##nea -contrasted -##ply -##udes -plum -pharaoh -##pile -aw -comedies -ingrid -sandwiches -subdivisions -1100 -mariana -nokia -kamen -hz -delaney -veto -herring -##words -possessive -outlines -##roup -siemens -stairwell -rc -gallantry -messiah -palais -yells -233 -zeppelin -##dm -bolivar -##cede -smackdown -mckinley -##mora -##yt -muted -geologic -finely -unitary -avatar -hamas -maynard -rees -bog -contrasting -##rut -liv -chico -disposition -pixel -##erate -becca -dmitry -yeshiva -narratives -##lva -##ulton -mercenary -sharpe -tempered -navigate -stealth -amassed -keynes -##lini -untouched -##rrie -havoc -lithium -##fighting -abyss -graf -southward -wolverine -balloons -implements -ngos -transitions -##icum -ambushed -concacaf -dormant -economists -##dim -costing -csi -rana -universite -boulders -verity -##llon -collin -mellon -misses -cypress -fluorescent -lifeless -spence -##ulla -crewe -shepard -pak -revelations -##م -jolly -gibbons -paw -##dro -##quel -freeing -##test -shack -fries -palatine -##51 -##hiko -accompaniment -cruising -recycled -##aver -erwin -sorting -synthesizers -dyke -realities -sg -strides -enslaved -wetland -##ghan -competence -gunpowder -grassy -maroon -reactors -objection -##oms -carlson -gearbox -macintosh -radios -shelton -##sho -clergyman -prakash -254 -mongols -trophies -oricon -228 -stimuli -twenty20 -cantonese -cortes -mirrored -##saurus -bhp -cristina -melancholy -##lating -enjoyable -nuevo -##wny -downfall -schumacher -##ind -banging -lausanne -rumbled -paramilitary -reflex -ax -amplitude -migratory -##gall -##ups -midi -barnard -lastly -sherry -##hp -##nall -keystone -##kra -carleton -slippery -##53 -coloring -foe -socket -otter -##rgos -mats -##tose -consultants -bafta -bison -topping -##km -490 -primal -abandonment -transplant -atoll -hideous -mort -pained -reproduced -tae -howling -##turn -unlawful -billionaire -hotter -poised -lansing -##chang -dinamo -retro -messing -nfc -domesday -##mina -blitz -timed -##athing -##kley -ascending -gesturing -##izations -signaled -tis -chinatown -mermaid -savanna -jameson -##aint -catalina -##pet -##hers -cochrane -cy -chatting -##kus -alerted -computation -mused -noelle -majestic -mohawk -campo -octagonal -##sant -##hend -241 -aspiring -##mart -comprehend -iona -paralyzed -shimmering -swindon -rhone -##eley -reputed -configurations -pitchfork -agitation -francais -gillian -lipstick -##ilo -outsiders -pontifical -resisting -bitterness -sewer -rockies -##edd -##ucher -misleading -1756 -exiting -galloway -##nging -risked -##heart -246 -commemoration -schultz -##rka -integrating -##rsa -poses -shrieked -##weiler -guineas -gladys -jerking -owls -goldsmith -nightly -penetrating -##unced -lia -##33 -ignited -betsy -##aring -##thorpe -follower -vigorously -##rave -coded -kiran -knit -zoology -tbilisi -##28 -##bered -repository -govt -deciduous -dino -growling -##bba -enhancement -unleashed -chanting -pussy -biochemistry -##eric -kettle -repression -toxicity -nrhp -##arth -##kko -##bush -ernesto -commended -outspoken -242 -mca -parchment -sms -kristen -##aton -bisexual -raked -glamour -navajo -a2 -conditioned -showcased -##hma -spacious -youthful -##esa -usl -appliances -junta -brest -layne -conglomerate -enchanted -chao -loosened -picasso -circulating -inspect -montevideo -##centric -##kti -piazza -spurred -##aith -bari -freedoms -poultry -stamford -lieu -##ect -indigo -sarcastic -bahia -stump -attach -dvds -frankenstein -lille -approx -scriptures -pollen -##script -nmi -overseen -##ivism -tides -proponent -newmarket -inherit -milling -##erland -centralized -##rou -distributors -credentials -drawers -abbreviation -##lco -##xon -downing -uncomfortably -ripe -##oes -erase -franchises -##ever -populace -##bery -##khar -decomposition -pleas -##tet -daryl -sabah -##stle -##wide -fearless -genie -lesions -annette -##ogist -oboe -appendix -nair -dripped -petitioned -maclean -mosquito -parrot -rpg -hampered -1648 -operatic -reservoirs -##tham -irrelevant -jolt -summarized -##fp -medallion -##taff -##− -clawed -harlow -narrower -goddard -marcia -bodied -fremont -suarez -altering -tempest -mussolini -porn -##isms -sweetly -oversees -walkers -solitude -grimly -shrines -hk -ich -supervisors -hostess -dietrich -legitimacy -brushes -expressive -##yp -dissipated -##rse -localized -systemic -##nikov -gettysburg -##js -##uaries -dialogues -muttering -251 -housekeeper -sicilian -discouraged -##frey -beamed -kaladin -halftime -kidnap -##amo -##llet -1754 -synonymous -depleted -instituto -insulin -reprised -##opsis -clashed -##ctric -interrupting -radcliffe -insisting -medici -1715 -ejected -playfully -turbulent -##47 -starvation -##rini -shipment -rebellious -petersen -verification -merits -##rified -cakes -##charged -1757 -milford -shortages -spying -fidelity -##aker -emitted -storylines -harvested -seismic -##iform -cheung -kilda -theoretically -barbie -lynx -##rgy -##tius -goblin -mata -poisonous -##nburg -reactive -residues -obedience -##евич -conjecture -##rac -401 -hating -sixties -kicker -moaning -motown -##bha -emancipation -neoclassical -##hering -consoles -ebert -professorship -##tures -sustaining -assaults -obeyed -affluent -incurred -tornadoes -##eber -##zow -emphasizing -highlanders -cheated -helmets -##ctus -internship -terence -bony -executions -legislators -berries -peninsular -tinged -##aco -1689 -amplifier -corvette -ribbons -lavish -pennant -##lander -worthless -##chfield -##forms -mariano -pyrenees -expenditures -##icides -chesterfield -mandir -tailor -39th -sergey -nestled -willed -aristocracy -devotees -goodnight -raaf -rumored -weaponry -remy -appropriations -harcourt -burr -riaa -##lence -limitation -unnoticed -guo -soaking -swamps -##tica -collapsing -tatiana -descriptive -brigham -psalm -##chment -maddox -##lization -patti -caliph -##aja -akron -injuring -serra -##ganj -basins -##sari -astonished -launcher -##church -hilary -wilkins -sewing -##sf -stinging -##fia -##ncia -underwood -startup -##ition -compilations -vibrations -embankment -jurist -##nity -bard -juventus -groundwater -kern -palaces -helium -boca -cramped -marissa -soto -##worm -jae -princely -##ggy -faso -bazaar -warmly -##voking -229 -pairing -##lite -##grate -##nets -wien -freaked -ulysses -rebirth -##alia -##rent -mummy -guzman -jimenez -stilled -##nitz -trajectory -tha -woken -archival -professions -##pts -##pta -hilly -shadowy -shrink -##bolt -norwood -glued -migrate -stereotypes -devoid -##pheus -625 -evacuate -horrors -infancy -gotham -knowles -optic -downloaded -sachs -kingsley -parramatta -darryl -mor -##onale -shady -commence -confesses -kan -##meter -##placed -marlborough -roundabout -regents -frigates -io -##imating -gothenburg -revoked -carvings -clockwise -convertible -intruder -##sche -banged -##ogo -vicky -bourgeois -##mony -dupont -footing -##gum -pd -##real -buckle -yun -penthouse -sane -720 -serviced -stakeholders -neumann -bb -##eers -comb -##gam -catchment -pinning -rallies -typing -##elles -forefront -freiburg -sweetie -giacomo -widowed -goodwill -worshipped -aspirations -midday -##vat -fishery -##trick -bournemouth -turk -243 -hearth -ethanol -guadalajara -murmurs -sl -##uge -afforded -scripted -##hta -wah -##jn -coroner -translucent -252 -memorials -puck -progresses -clumsy -##race -315 -candace -recounted -##27 -##slin -##uve -filtering -##mac -howl -strata -heron -leveled -##ays -dubious -##oja -##т -##wheel -citations -exhibiting -##laya -##mics -##pods -turkic -##lberg -injunction -##ennial -##mit -antibodies -##44 -organise -##rigues -cardiovascular -cushion -inverness -##zquez -dia -cocoa -sibling -##tman -##roid -expanse -feasible -tunisian -algiers -##relli -rus -bloomberg -dso -westphalia -bro -tacoma -281 -downloads -##ours -konrad -duran -##hdi -continuum -jett -compares -legislator -secession -##nable -##gues -##zuka -translating -reacher -##gley -##ła -aleppo -##agi -tc -orchards -trapping -linguist -versatile -drumming -postage -calhoun -superiors -##mx -barefoot -leary -##cis -ignacio -alfa -kaplan -##rogen -bratislava -mori -##vot -disturb -haas -313 -cartridges -gilmore -radiated -salford -tunic -hades -##ulsive -archeological -delilah -magistrates -auditioned -brewster -charters -empowerment -blogs -cappella -dynasties -iroquois -whipping -##krishna -raceway -truths -myra -weaken -judah -mcgregor -##horse -mic -refueling -37th -burnley -bosses -markus -premio -query -##gga -dunbar -##economic -darkest -lyndon -sealing -commendation -reappeared -##mun -addicted -ezio -slaughtered -satisfactory -shuffle -##eves -##thic -##uj -fortification -warrington -##otto -resurrected -fargo -mane -##utable -##lei -##space -foreword -ox -##aris -##vern -abrams -hua -##mento -sakura -##alo -uv -sentimental -##skaya -midfield -##eses -sturdy -scrolls -macleod -##kyu -entropy -##lance -mitochondrial -cicero -excelled -thinner -convoys -perceive -##oslav -##urable -systematically -grind -burkina -287 -##tagram -ops -##aman -guantanamo -##cloth -##tite -forcefully -wavy -##jou -pointless -##linger -##tze -layton -portico -superficial -clerical -outlaws -##hism -burials -muir -##inn -creditors -hauling -rattle -##leg -calais -monde -archers -reclaimed -dwell -wexford -hellenic -falsely -remorse -##tek -dough -furnishings -##uttered -gabon -neurological -novice -##igraphy -contemplated -pulpit -nightstand -saratoga -##istan -documenting -pulsing -taluk -##firmed -busted -marital -##rien -disagreements -wasps -##yes -hodge -mcdonnell -mimic -fran -pendant -dhabi -musa -##nington -congratulations -argent -darrell -concussion -losers -regrets -thessaloniki -reversal -donaldson -hardwood -thence -achilles -ritter -##eran -demonic -jurgen -prophets -goethe -eki -classmate -buff -##cking -yank -irrational -##inging -perished -seductive -qur -sourced -##crat -##typic -mustard -ravine -barre -horizontally -characterization -phylogenetic -boise -##dit -##runner -##tower -brutally -intercourse -seduce -##bbing -fay -ferris -ogden -amar -nik -unarmed -##inator -evaluating -kyrgyzstan -sweetness -##lford -##oki -mccormick -meiji -notoriety -stimulate -disrupt -figuring -instructional -mcgrath -##zoo -groundbreaking -##lto -flinch -khorasan -agrarian -bengals -mixer -radiating -##sov -ingram -pitchers -nad -tariff -##cript -tata -##codes -##emi -##ungen -appellate -lehigh -##bled -##giri -brawl -duct -texans -##ciation -##ropolis -skipper -speculative -vomit -doctrines -stresses -253 -davy -graders -whitehead -jozef -timely -cumulative -haryana -paints -appropriately -boon -cactus -##ales -##pid -dow -legions -##pit -perceptions -1730 -picturesque -##yse -periphery -rune -wr -##aha -celtics -sentencing -whoa -##erin -confirms -variance -425 -moines -mathews -spade -rave -m1 -fronted -fx -blending -alleging -reared -##gl -237 -##paper -grassroots -eroded -##free -##physical -directs -ordeal -##sław -accelerate -hacker -rooftop -##inia -lev -buys -cebu -devote -##lce -specialising -##ulsion -choreographed -repetition -warehouses -##ryl -paisley -tuscany -analogy -sorcerer -hash -huts -shards -descends -exclude -nix -chaplin -gaga -ito -vane -##drich -causeway -misconduct -limo -orchestrated -glands -jana -##kot -u2 -##mple -##sons -branching -contrasts -scoop -longed -##virus -chattanooga -##75 -syrup -cornerstone -##tized -##mind -##iaceae -careless -precedence -frescoes -##uet -chilled -consult -modelled -snatch -peat -##thermal -caucasian -humane -relaxation -spins -temperance -##lbert -occupations -lambda -hybrids -moons -mp3 -##oese -247 -rolf -societal -yerevan -ness -##ssler -befriended -mechanized -nominate -trough -boasted -cues -seater -##hom -bends -##tangle -conductors -emptiness -##lmer -eurasian -adriatic -tian -##cie -anxiously -lark -propellers -chichester -jock -ev -2a -##holding -credible -recounts -tori -loyalist -abduction -##hoot -##redo -nepali -##mite -ventral -tempting -##ango -##crats -steered -##wice -javelin -dipping -laborers -prentice -looming -titanium -##ː -badges -emir -tensor -##ntation -egyptians -rash -denies -hawthorne -lombard -showers -wehrmacht -dietary -trojan -##reus -welles -executing -horseshoe -lifeboat -##lak -elsa -infirmary -nearing -roberta -boyer -mutter -trillion -joanne -##fine -##oked -sinks -vortex -uruguayan -clasp -sirius -##block -accelerator -prohibit -sunken -byu -chronological -diplomats -ochreous -510 -symmetrical -1644 -maia -##tology -salts -reigns -atrocities -##ия -hess -bared -issn -##vyn -cater -saturated -##cycle -##isse -sable -voyager -dyer -yusuf -##inge -fountains -wolff -##39 -##nni -engraving -rollins -atheist -ominous -##ault -herr -chariot -martina -strung -##fell -##farlane -horrific -sahib -gazes -saetan -erased -ptolemy -##olic -flushing -lauderdale -analytic -##ices -530 -navarro -beak -gorilla -herrera -broom -guadalupe -raiding -sykes -311 -bsc -deliveries -1720 -invasions -carmichael -tajikistan -thematic -ecumenical -sentiments -onstage -##rians -##brand -##sume -catastrophic -flanks -molten -##arns -waller -aimee -terminating -##icing -alternately -##oche -nehru -printers -outraged -##eving -empires -template -banners -repetitive -za -##oise -vegetarian -##tell -guiana -opt -cavendish -lucknow -synthesized -##hani -##mada -finalized -##ctable -fictitious -mayoral -unreliable -##enham -embracing -peppers -rbis -##chio -##neo -inhibition -slashed -togo -orderly -embroidered -safari -salty -236 -barron -benito -totaled -##dak -pubs -simulated -caden -devin -tolkien -momma -welding -sesame -##ept -gottingen -hardness -630 -shaman -temeraire -620 -adequately -pediatric -##kit -ck -assertion -radicals -composure -cadence -seafood -beaufort -lazarus -mani -warily -cunning -kurdistan -249 -cantata -##kir -ares -##41 -##clusive -nape -townland -geared -insulted -flutter -boating -violate -draper -dumping -malmo -##hh -##romatic -firearm -alta -bono -obscured -##clave -exceeds -panorama -unbelievable -##train -preschool -##essed -disconnected -installing -rescuing -secretaries -accessibility -##castle -##drive -##ifice -##film -bouts -slug -waterway -mindanao -##buro -##ratic -halves -##ل -calming -liter -maternity -adorable -bragg -electrification -mcc -##dote -roxy -schizophrenia -##body -munoz -kaye -whaling -239 -mil -tingling -tolerant -##ago -unconventional -volcanoes -##finder -deportivo -##llie -robson -kaufman -neuroscience -wai -deportation -masovian -scraping -converse -##bh -hacking -bulge -##oun -administratively -yao -580 -amp -mammoth -booster -claremont -hooper -nomenclature -pursuits -mclaughlin -melinda -##sul -catfish -barclay -substrates -taxa -zee -originals -kimberly -packets -padma -##ality -borrowing -ostensibly -solvent -##bri -##genesis -##mist -lukas -shreveport -veracruz -##ь -##lou -##wives -cheney -tt -anatolia -hobbs -##zyn -cyclic -radiant -alistair -greenish -siena -dat -independents -##bation -conform -pieter -hyper -applicant -bradshaw -spores -telangana -vinci -inexpensive -nuclei -322 -jang -nme -soho -spd -##ign -cradled -receptionist -pow -##43 -##rika -fascism -##ifer -experimenting -##ading -##iec -##region -345 -jocelyn -maris -stair -nocturnal -toro -constabulary -elgin -##kker -msc -##giving -##schen -##rase -doherty -doping -sarcastically -batter -maneuvers -##cano -##apple -##gai -##git -intrinsic -##nst -##stor -1753 -showtime -cafes -gasps -lviv -ushered -##thed -fours -restart -astonishment -transmitting -flyer -shrugs -##sau -intriguing -cones -dictated -mushrooms -medial -##kovsky -##elman -escorting -gaped -##26 -godfather -##door -##sell -djs -recaptured -timetable -vila -1710 -3a -aerodrome -mortals -scientology -##orne -angelina -mag -convection -unpaid -insertion -intermittent -lego -##nated -endeavor -kota -pereira -##lz -304 -bwv -glamorgan -insults -agatha -fey -##cend -fleetwood -mahogany -protruding -steamship -zeta -##arty -mcguire -suspense -##sphere -advising -urges -##wala -hurriedly -meteor -gilded -inline -arroyo -stalker -##oge -excitedly -revered -##cure -earle -introductory -##break -##ilde -mutants -puff -pulses -reinforcement -##haling -curses -lizards -stalk -correlated -##fixed -fallout -macquarie -##unas -bearded -denton -heaving -802 -##ocation -winery -assign -dortmund -##lkirk -everest -invariant -charismatic -susie -##elling -bled -lesley -telegram -sumner -bk -##ogen -##к -wilcox -needy -colbert -duval -##iferous -##mbled -allotted -attends -imperative -##hita -replacements -hawker -##inda -insurgency -##zee -##eke -casts -##yla -680 -ives -transitioned -##pack -##powering -authoritative -baylor -flex -cringed -plaintiffs -woodrow -##skie -drastic -ape -aroma -unfolded -commotion -nt -preoccupied -theta -routines -lasers -privatization -wand -domino -ek -clenching -nsa -strategically -showered -bile -handkerchief -pere -storing -christophe -insulting -316 -nakamura -romani -asiatic -magdalena -palma -cruises -stripping -405 -konstantin -soaring -##berman -colloquially -forerunner -havilland -incarcerated -parasites -sincerity -##utus -disks -plank -saigon -##ining -corbin -homo -ornaments -powerhouse -##tlement -chong -fastened -feasibility -idf -morphological -usable -##nish -##zuki -aqueduct -jaguars -keepers -##flies -aleksandr -faust -assigns -ewing -bacterium -hurled -tricky -hungarians -integers -wallis -321 -yamaha -##isha -hushed -oblivion -aviator -evangelist -friars -##eller -monograph -ode -##nary -airplanes -labourers -charms -##nee -1661 -hagen -tnt -rudder -fiesta -transcript -dorothea -ska -inhibitor -maccabi -retorted -raining -encompassed -clauses -menacing -1642 -lineman -##gist -vamps -##ape -##dick -gloom -##rera -dealings -easing -seekers -##nut -##pment -helens -unmanned -##anu -##isson -basics -##amy -##ckman -adjustments -1688 -brutality -horne -##zell -sui -##55 -##mable -aggregator -##thal -rhino -##drick -##vira -counters -zoom -##01 -##rting -mn -montenegrin -packard -##unciation -##♭ -##kki -reclaim -scholastic -thugs -pulsed -##icia -syriac -quan -saddam -banda -kobe -blaming -buddies -dissent -##lusion -##usia -corbett -jaya -delle -erratic -lexie -##hesis -435 -amiga -hermes -##pressing -##leen -chapels -gospels -jamal -##uating -compute -revolving -warp -##sso -##thes -armory -##eras -##gol -antrim -loki -##kow -##asian -##good -##zano -braid -handwriting -subdistrict -funky -pantheon -##iculate -concurrency -estimation -improper -juliana -##his -newcomers -johnstone -staten -communicated -##oco -##alle -sausage -stormy -##stered -##tters -superfamily -##grade -acidic -collateral -tabloid -##oped -##rza -bladder -austen -##ellant -mcgraw -##hay -hannibal -mein -aquino -lucifer -wo -badger -boar -cher -christensen -greenberg -interruption -##kken -jem -244 -mocked -bottoms -cambridgeshire -##lide -sprawling -##bbly -eastwood -ghent -synth -##buck -advisers -##bah -nominally -hapoel -qu -daggers -estranged -fabricated -towels -vinnie -wcw -misunderstanding -anglia -nothin -unmistakable -##dust -##lova -chilly -marquette -truss -##edge -##erine -reece -##lty -##chemist -##connected -272 -308 -41st -bash -raion -waterfalls -##ump -##main -labyrinth -queue -theorist -##istle -bharatiya -flexed -soundtracks -rooney -leftist -patrolling -wharton -plainly -alleviate -eastman -schuster -topographic -engages -immensely -unbearable -fairchild -1620 -dona -lurking -parisian -oliveira -ia -indictment -hahn -bangladeshi -##aster -vivo -##uming -##ential -antonia -expects -indoors -kildare -harlan -##logue -##ogenic -##sities -forgiven -##wat -childish -tavi -##mide -##orra -plausible -grimm -successively -scooted -##bola -##dget -##rith -spartans -emery -flatly -azure -epilogue -##wark -flourish -##iny -##tracted -##overs -##oshi -bestseller -distressed -receipt -spitting -hermit -topological -##cot -drilled -subunit -francs -##layer -eel -##fk -##itas -octopus -footprint -petitions -ufo -##say -##foil -interfering -leaking -palo -##metry -thistle -valiant -##pic -narayan -mcpherson -##fast -gonzales -##ym -##enne -dustin -novgorod -solos -##zman -doin -##raph -##patient -##meyer -soluble -ashland -cuffs -carole -pendleton -whistling -vassal -##river -deviation -revisited -constituents -rallied -rotate -loomed -##eil -##nting -amateurs -augsburg -auschwitz -crowns -skeletons -##cona -bonnet -257 -dummy -globalization -simeon -sleeper -mandal -differentiated -##crow -##mare -milne -bundled -exasperated -talmud -owes -segregated -##feng -##uary -dentist -piracy -props -##rang -devlin -##torium -malicious -paws -##laid -dependency -##ergy -##fers -##enna -258 -pistons -rourke -jed -grammatical -tres -maha -wig -512 -ghostly -jayne -##achal -##creen -##ilis -##lins -##rence -designate -##with -arrogance -cambodian -clones -showdown -throttle -twain -##ception -lobes -metz -nagoya -335 -braking -##furt -385 -roaming -##minster -amin -crippled -##37 -##llary -indifferent -hoffmann -idols -intimidating -1751 -261 -influenza -memo -onions -1748 -bandage -consciously -##landa -##rage -clandestine -observes -swiped -tangle -##ener -##jected -##trum -##bill -##lta -hugs -congresses -josiah -spirited -##dek -humanist -managerial -filmmaking -inmate -rhymes -debuting -grimsby -ur -##laze -duplicate -vigor -##tf -republished -bolshevik -refurbishment -antibiotics -martini -methane -newscasts -royale -horizons -levant -iain -visas -##ischen -paler -##around -manifestation -snuck -alf -chop -futile -pedestal -rehab -##kat -bmg -kerman -res -fairbanks -jarrett -abstraction -saharan -##zek -1746 -procedural -clearer -kincaid -sash -luciano -##ffey -crunch -helmut -##vara -revolutionaries -##tute -creamy -leach -##mmon -1747 -permitting -nes -plight -wendell -##lese -contra -ts -clancy -ipa -mach -staples -autopsy -disturbances -nueva -karin -pontiac -##uding -proxy -venerable -haunt -leto -bergman -expands -##helm -wal -##pipe -canning -celine -cords -obesity -##enary -intrusion -planner -##phate -reasoned -sequencing -307 -harrow -##chon -##dora -marred -mcintyre -repay -tarzan -darting -248 -harrisburg -margarita -repulsed -##hur -##lding -belinda -hamburger -novo -compliant -runways -bingham -registrar -skyscraper -ic -cuthbert -improvisation -livelihood -##corp -##elial -admiring -##dened -sporadic -believer -casablanca -popcorn -##29 -asha -shovel -##bek -##dice -coiled -tangible -##dez -casper -elsie -resin -tenderness -rectory -##ivision -avail -sonar -##mori -boutique -##dier -guerre -bathed -upbringing -vaulted -sandals -blessings -##naut -##utnant -1680 -306 -foxes -pia -corrosion -hesitantly -confederates -crystalline -footprints -shapiro -tirana -valentin -drones -45th -microscope -shipments -texted -inquisition -wry -guernsey -unauthorized -resigning -760 -ripple -schubert -stu -reassure -felony -##ardo -brittle -koreans -##havan -##ives -dun -implicit -tyres -##aldi -##lth -magnolia -##ehan -##puri -##poulos -aggressively -fei -gr -familiarity -##poo -indicative -##trust -fundamentally -jimmie -overrun -395 -anchors -moans -##opus -britannia -armagh -##ggle -purposely -seizing -##vao -bewildered -mundane -avoidance -cosmopolitan -geometridae -quartermaster -caf -415 -chatter -engulfed -gleam -purge -##icate -juliette -jurisprudence -guerra -revisions -##bn -casimir -brew -##jm -1749 -clapton -cloudy -conde -hermitage -278 -simulations -torches -vincenzo -matteo -##rill -hidalgo -booming -westbound -accomplishment -tentacles -unaffected -##sius -annabelle -flopped -sloping -##litz -dreamer -interceptor -vu -##loh -consecration -copying -messaging -breaker -climates -hospitalized -1752 -torino -afternoons -winfield -witnessing -##teacher -breakers -choirs -sawmill -coldly -##ege -sipping -haste -uninhabited -conical -bibliography -pamphlets -severn -edict -##oca -deux -illnesses -grips -##pl -rehearsals -sis -thinkers -tame -##keepers -1690 -acacia -reformer -##osed -##rys -shuffling -##iring -##shima -eastbound -ionic -rhea -flees -littered -##oum -rocker -vomiting -groaning -champ -overwhelmingly -civilizations -paces -sloop -adoptive -##tish -skaters -##vres -aiding -mango -##joy -nikola -shriek -##ignon -pharmaceuticals -##mg -tuna -calvert -gustavo -stocked -yearbook -##urai -##mana -computed -subsp -riff -hanoi -kelvin -hamid -moors -pastures -summons -jihad -nectar -##ctors -bayou -untitled -pleasing -vastly -republics -intellect -##η -##ulio -##tou -crumbling -stylistic -sb -##ی -consolation -frequented -h₂o -walden -widows -##iens -404 -##ignment -chunks -improves -288 -grit -recited -##dev -snarl -sociological -##arte -##gul -inquired -##held -bruise -clube -consultancy -homogeneous -hornets -multiplication -pasta -prick -savior -##grin -##kou -##phile -yoon -##gara -grimes -vanishing -cheering -reacting -bn -distillery -##quisite -##vity -coe -dockyard -massif -##jord -escorts -voss -##valent -byte -chopped -hawke -illusions -workings -floats -##koto -##vac -kv -annapolis -madden -##onus -alvaro -noctuidae -##cum -##scopic -avenge -steamboat -forte -illustrates -erika -##trip -570 -dew -nationalities -bran -manifested -thirsty -diversified -muscled -reborn -##standing -arson -##lessness -##dran -##logram -##boys -##kushima -##vious -willoughby -##phobia -286 -alsace -dashboard -yuki -##chai -granville -myspace -publicized -tricked -##gang -adjective -##ater -relic -reorganisation -enthusiastically -indications -saxe -##lassified -consolidate -iec -padua -helplessly -ramps -renaming -regulars -pedestrians -accents -convicts -inaccurate -lowers -mana -##pati -barrie -bjp -outta -someplace -berwick -flanking -invoked -marrow -sparsely -excerpts -clothed -rei -##ginal -wept -##straße -##vish -alexa -excel -##ptive -membranes -aquitaine -creeks -cutler -sheppard -implementations -ns -##dur -fragrance -budge -concordia -magnesium -marcelo -##antes -gladly -vibrating -##rral -##ggles -montrose -##omba -lew -seamus -1630 -cocky -##ament -##uen -bjorn -##rrick -fielder -fluttering -##lase -methyl -kimberley -mcdowell -reductions -barbed -##jic -##tonic -aeronautical -condensed -distracting -##promising -huffed -##cala -##sle -claudius -invincible -missy -pious -balthazar -ci -##lang -butte -combo -orson -##dication -myriad -1707 -silenced -##fed -##rh -coco -netball -yourselves -##oza -clarify -heller -peg -durban -etudes -offender -roast -blackmail -curvature -##woods -vile -309 -illicit -suriname -##linson -overture -1685 -bubbling -gymnast -tucking -##mming -##ouin -maldives -##bala -gurney -##dda -##eased -##oides -backside -pinto -jars -racehorse -tending -##rdial -baronetcy -wiener -duly -##rke -barbarian -cupping -flawed -##thesis -bertha -pleistocene -puddle -swearing -##nob -##tically -fleeting -prostate -amulet -educating -##mined -##iti -##tler -75th -jens -respondents -analytics -cavaliers -papacy -raju -##iente -##ulum -##tip -funnel -271 -disneyland -##lley -sociologist -##iam -2500 -faulkner -louvre -menon -##dson -276 -##ower -afterlife -mannheim -peptide -referees -comedians -meaningless -##anger -##laise -fabrics -hurley -renal -sleeps -##bour -##icle -breakout -kristin -roadside -animator -clover -disdain -unsafe -redesign -##urity -firth -barnsley -portage -reset -narrows -268 -commandos -expansive -speechless -tubular -##lux -essendon -eyelashes -smashwords -##yad -##bang -##claim -craved -sprinted -chet -somme -astor -wrocław -orton -266 -bane -##erving -##uing -mischief -##amps -##sund -scaling -terre -##xious -impairment -offenses -undermine -moi -soy -contiguous -arcadia -inuit -seam -##tops -macbeth -rebelled -##icative -##iot -590 -elaborated -frs -uniformed -##dberg -259 -powerless -priscilla -stimulated -980 -qc -arboretum -frustrating -trieste -bullock -##nified -enriched -glistening -intern -##adia -locus -nouvelle -ollie -ike -lash -starboard -ee -tapestry -headlined -hove -rigged -##vite -pollock -##yme -thrive -clustered -cas -roi -gleamed -olympiad -##lino -pressured -regimes -##hosis -##lick -ripley -##ophone -kickoff -gallon -rockwell -##arable -crusader -glue -revolutions -scrambling -1714 -grover -##jure -englishman -aztec -263 -contemplating -coven -ipad -preach -triumphant -tufts -##esian -rotational -##phus -328 -falkland -##brates -strewn -clarissa -rejoin -environmentally -glint -banded -drenched -moat -albanians -johor -rr -maestro -malley -nouveau -shaded -taxonomy -v6 -adhere -bunk -airfields -##ritan -1741 -encompass -remington -tran -##erative -amelie -mazda -friar -morals -passions -##zai -breadth -vis -##hae -argus -burnham -caressing -insider -rudd -##imov -##mini -##rso -italianate -murderous -textual -wainwright -armada -bam -weave -timer -##taken -##nh -fra -##crest -ardent -salazar -taps -tunis -##ntino -allegro -gland -philanthropic -##chester -implication -##optera -esq -judas -noticeably -wynn -##dara -inched -indexed -crises -villiers -bandit -royalties -patterned -cupboard -interspersed -accessory -isla -kendrick -entourage -stitches -##esthesia -headwaters -##ior -interlude -distraught -draught -1727 -##basket -biased -sy -transient -triad -subgenus -adapting -kidd -shortstop -##umatic -dimly -spiked -mcleod -reprint -nellie -pretoria -windmill -##cek -singled -##mps -273 -reunite -##orous -747 -bankers -outlying -##omp -##ports -##tream -apologies -cosmetics -patsy -##deh -##ocks -##yson -bender -nantes -serene -##nad -lucha -mmm -323 -##cius -##gli -cmll -coinage -nestor -juarez -##rook -smeared -sprayed -twitching -sterile -irina -embodied -juveniles -enveloped -miscellaneous -cancers -dq -gulped -luisa -crested -swat -donegal -ref -##anov -##acker -hearst -mercantile -##lika -doorbell -ua -vicki -##alla -##som -bilbao -psychologists -stryker -sw -horsemen -turkmenistan -wits -##national -anson -mathew -screenings -##umb -rihanna -##agne -##nessy -aisles -##iani -##osphere -hines -kenton -saskatoon -tasha -truncated -##champ -##itan -mildred -advises -fredrik -interpreting -inhibitors -##athi -spectroscopy -##hab -##kong -karim -panda -##oia -##nail -##vc -conqueror -kgb -leukemia -##dity -arrivals -cheered -pisa -phosphorus -shielded -##riated -mammal -unitarian -urgently -chopin -sanitary -##mission -spicy -drugged -hinges -##tort -tipping -trier -impoverished -westchester -##caster -267 -epoch -nonstop -##gman -##khov -aromatic -centrally -cerro -##tively -##vio -billions -modulation -sedimentary -283 -facilitating -outrageous -goldstein -##eak -##kt -ld -maitland -penultimate -pollard -##dance -fleets -spaceship -vertebrae -##nig -alcoholism -als -recital -##bham -##ference -##omics -m2 -##bm -trois -##tropical -##в -commemorates -##meric -marge -##raction -1643 -670 -cosmetic -ravaged -##ige -catastrophe -eng -##shida -albrecht -arterial -bellamy -decor -harmon -##rde -bulbs -synchronized -vito -easiest -shetland -shielding -wnba -##glers -##ssar -##riam -brianna -cumbria -##aceous -##rard -cores -thayer -##nsk -brood -hilltop -luminous -carts -keynote -larkin -logos -##cta -##ا -##mund -##quay -lilith -tinted -277 -wrestle -mobilization -##uses -sequential -siam -bloomfield -takahashi -274 -##ieving -presenters -ringo -blazed -witty -##oven -##ignant -devastation -haydn -harmed -newt -therese -##peed -gershwin -molina -rabbis -sudanese -001 -innate -restarted -##sack -##fus -slices -wb -##shah -enroll -hypothetical -hysterical -1743 -fabio -indefinite -warped -##hg -exchanging -525 -unsuitable -##sboro -gallo -1603 -bret -cobalt -homemade -##hunter -mx -operatives -##dhar -terraces -durable -latch -pens -whorls -##ctuated -##eaux -billing -ligament -succumbed -##gly -regulators -spawn -##brick -##stead -filmfare -rochelle -##nzo -1725 -circumstance -saber -supplements -##nsky -##tson -crowe -wellesley -carrot -##9th -##movable -primate -drury -sincerely -topical -##mad -##rao -callahan -kyiv -smarter -tits -undo -##yeh -announcements -anthologies -barrio -nebula -##islaus -##shaft -##tyn -bodyguards -2021 -assassinate -barns -emmett -scully -##mah -##yd -##eland -##tino -##itarian -demoted -gorman -lashed -prized -adventist -writ -##gui -alla -invertebrates -##ausen -1641 -amman -1742 -align -healy -redistribution -##gf -##rize -insulation -##drop -adherents -hezbollah -vitro -ferns -yanking -269 -php -registering -uppsala -cheerleading -confines -mischievous -tully -##ross -49th -docked -roam -stipulated -pumpkin -##bry -prompt -##ezer -blindly -shuddering -craftsmen -frail -scented -katharine -scramble -shaggy -sponge -helix -zaragoza -279 -##52 -43rd -backlash -fontaine -seizures -posse -cowan -nonfiction -telenovela -wwii -hammered -undone -##gpur -encircled -irs -##ivation -artefacts -oneself -searing -smallpox -##belle -##osaurus -shandong -breached -upland -blushing -rankin -infinitely -psyche -tolerated -docking -evicted -##col -unmarked -##lving -gnome -lettering -litres -musique -##oint -benevolent -##jal -blackened -##anna -mccall -racers -tingle -##ocene -##orestation -introductions -radically -292 -##hiff -##باد -1610 -1739 -munchen -plead -##nka -condo -scissors -##sight -##tens -apprehension -##cey -##yin -hallmark -watering -formulas -sequels -##llas -aggravated -bae -commencing -##building -enfield -prohibits -marne -vedic -civilized -euclidean -jagger -beforehand -blasts -dumont -##arney -##nem -740 -conversions -hierarchical -rios -simulator -##dya -##lellan -hedges -oleg -thrusts -shadowed -darby -maximize -1744 -gregorian -##nded -##routed -sham -unspecified -##hog -emory -factual -##smo -##tp -fooled -##rger -ortega -wellness -marlon -##oton -##urance -casket -keating -ley -enclave -##ayan -char -influencing -jia -##chenko -412 -ammonia -erebidae -incompatible -violins -cornered -##arat -grooves -astronauts -columbian -rampant -fabrication -kyushu -mahmud -vanish -##dern -mesopotamia -##lete -ict -##rgen -caspian -kenji -pitted -##vered -999 -grimace -roanoke -tchaikovsky -twinned -##analysis -##awan -xinjiang -arias -clemson -kazakh -sizable -1662 -##khand -##vard -plunge -tatum -vittorio -##nden -cholera -##dana -##oper -bracing -indifference -projectile -superliga -##chee -realises -upgrading -299 -porte -retribution -##vies -nk -stil -##resses -ama -bureaucracy -blackberry -bosch -testosterone -collapses -greer -##pathic -ioc -fifties -malls -##erved -bao -baskets -adolescents -siegfried -##osity -##tosis -mantra -detecting -existent -fledgling -##cchi -dissatisfied -gan -telecommunication -mingled -sobbed -6000 -controversies -outdated -taxis -##raus -fright -slams -##lham -##fect -##tten -detectors -fetal -tanned -##uw -fray -goth -olympian -skipping -mandates -scratches -sheng -unspoken -hyundai -tracey -hotspur -restrictive -##buch -americana -mundo -##bari -burroughs -diva -vulcan -##6th -distinctions -thumping -##ngen -mikey -sheds -fide -rescues -springsteen -vested -valuation -##ece -##ely -pinnacle -rake -sylvie -##edo -almond -quivering -##irus -alteration -faltered -##wad -51st -hydra -ticked -##kato -recommends -##dicated -antigua -arjun -stagecoach -wilfred -trickle -pronouns -##pon -aryan -nighttime -##anian -gall -pea -stitch -##hei -leung -milos -##dini -eritrea -nexus -starved -snowfall -kant -parasitic -cot -discus -hana -strikers -appleton -kitchens -##erina -##partisan -##itha -##vius -disclose -metis -##channel -1701 -tesla -##vera -fitch -1735 -blooded -##tila -decimal -##tang -##bai -cyclones -eun -bottled -peas -pensacola -basha -bolivian -crabs -boil -lanterns -partridge -roofed -1645 -necks -##phila -opined -patting -##kla -##lland -chuckles -volta -whereupon -##nche -devout -euroleague -suicidal -##dee -inherently -involuntary -knitting -nasser -##hide -puppets -colourful -courageous -southend -stills -miraculous -hodgson -richer -rochdale -ethernet -greta -uniting -prism -umm -##haya -##itical -##utation -deterioration -pointe -prowess -##ropriation -lids -scranton -billings -subcontinent -##koff -##scope -brute -kellogg -psalms -degraded -##vez -stanisław -##ructured -ferreira -pun -astonishing -gunnar -##yat -arya -prc -gottfried -##tight -excursion -##ographer -dina -##quil -##nare -huffington -illustrious -wilbur -gundam -verandah -##zard -naacp -##odle -constructive -fjord -kade -##naud -generosity -thrilling -baseline -cayman -frankish -plastics -accommodations -zoological -##fting -cedric -qb -motorized -##dome -##otted -squealed -tackled -canucks -budgets -situ -asthma -dail -gabled -grasslands -whimpered -writhing -judgments -##65 -minnie -pv -##carbon -bananas -grille -domes -monique -odin -maguire -markham -tierney -##estra -##chua -libel -poke -speedy -atrium -laval -notwithstanding -##edly -fai -kala -##sur -robb -##sma -listings -luz -supplementary -tianjin -##acing -enzo -jd -ric -scanner -croats -transcribed -##49 -arden -cv -##hair -##raphy -##lver -##uy -357 -seventies -staggering -alam -horticultural -hs -regression -timbers -blasting -##ounded -montagu -manipulating -##cit -catalytic -1550 -troopers -##meo -condemnation -fitzpatrick -##oire -##roved -inexperienced -1670 -castes -##lative -outing -314 -dubois -flicking -quarrel -ste -learners -1625 -iq -whistled -##class -282 -classify -tariffs -temperament -355 -folly -liszt -##yles -immersed -jordanian -ceasefire -apparel -extras -maru -fished -##bio -harta -stockport -assortment -craftsman -paralysis -transmitters -##cola -blindness -##wk -fatally -proficiency -solemnly -##orno -repairing -amore -groceries -ultraviolet -##chase -schoolhouse -##tua -resurgence -nailed -##otype -##× -ruse -saliva -diagrams -##tructing -albans -rann -thirties -1b -antennas -hilarious -cougars -paddington -stats -##eger -breakaway -ipod -reza -authorship -prohibiting -scoffed -##etz -##ttle -conscription -defected -trondheim -##fires -ivanov -keenan -##adan -##ciful -##fb -##slow -locating -##ials -##tford -cadiz -basalt -blankly -interned -rags -rattling -##tick -carpathian -reassured -sync -bum -guildford -iss -staunch -##onga -astronomers -sera -sofie -emergencies -susquehanna -##heard -duc -mastery -vh1 -williamsburg -bayer -buckled -craving -##khan -##rdes -bloomington -##write -alton -barbecue -##bians -justine -##hri -##ndt -delightful -smartphone -newtown -photon -retrieval -peugeot -hissing -##monium -##orough -flavors -lighted -relaunched -tainted -##games -##lysis -anarchy -microscopic -hopping -adept -evade -evie -##beau -inhibit -sinn -adjustable -hurst -intuition -wilton -cisco -44th -lawful -lowlands -stockings -thierry -##dalen -##hila -##nai -fates -prank -tb -maison -lobbied -provocative -1724 -4a -utopia -##qual -carbonate -gujarati -purcell -##rford -curtiss -##mei -overgrown -arenas -mediation -swallows -##rnik -respectful -turnbull -##hedron -##hope -alyssa -ozone -##ʻi -ami -gestapo -johansson -snooker -canteen -cuff -declines -empathy -stigma -##ags -##iner -##raine -taxpayers -gui -volga -##wright -##copic -lifespan -overcame -tattooed -enactment -giggles -##ador -##camp -barrington -bribe -obligatory -orbiting -peng -##enas -elusive -sucker -##vating -cong -hardship -empowered -anticipating -estrada -cryptic -greasy -detainees -planck -sudbury -plaid -dod -marriott -kayla -##ears -##vb -##zd -mortally -##hein -cognition -radha -319 -liechtenstein -meade -richly -argyle -harpsichord -liberalism -trumpets -lauded -tyrant -salsa -tiled -lear -promoters -reused -slicing -trident -##chuk -##gami -##lka -cantor -checkpoint -##points -gaul -leger -mammalian -##tov -##aar -##schaft -doha -frenchman -nirvana -##vino -delgado -headlining -##eron -##iography -jug -tko -1649 -naga -intersections -##jia -benfica -nawab -##suka -ashford -gulp -##deck -##vill -##rug -brentford -frazier -pleasures -dunne -potsdam -shenzhen -dentistry -##tec -flanagan -##dorff -##hear -chorale -dinah -prem -quezon -##rogated -relinquished -sutra -terri -##pani -flaps -##rissa -poly -##rnet -homme -aback -##eki -linger -womb -##kson -##lewood -doorstep -orthodoxy -threaded -westfield -##rval -dioceses -fridays -subsided -##gata -loyalists -##biotic -##ettes -letterman -lunatic -prelate -tenderly -invariably -souza -thug -winslow -##otide -furlongs -gogh -jeopardy -##runa -pegasus -##umble -humiliated -standalone -tagged -##roller -freshmen -klan -##bright -attaining -initiating -transatlantic -logged -viz -##uance -1723 -combatants -intervening -stephane -chieftain -despised -grazed -317 -cdc -galveston -godzilla -macro -simulate -##planes -parades -##esses -960 -##ductive -##unes -equator -overdose -##cans -##hosh -##lifting -joshi -epstein -sonora -treacherous -aquatics -manchu -responsive -##sation -supervisory -##christ -##llins -##ibar -##balance -##uso -kimball -karlsruhe -mab -##emy -ignores -phonetic -reuters -spaghetti -820 -almighty -danzig -rumbling -tombstone -designations -lured -outset -##felt -supermarkets -##wt -grupo -kei -kraft -susanna -##blood -comprehension -genealogy -##aghan -##verted -redding -##ythe -1722 -bowing -##pore -##roi -lest -sharpened -fulbright -valkyrie -sikhs -##unds -swans -bouquet -merritt -##tage -##venting -commuted -redhead -clerks -leasing -cesare -dea -hazy -##vances -fledged -greenfield -servicemen -##gical -armando -blackout -dt -sagged -downloadable -intra -potion -pods -##4th -##mism -xp -attendants -gambia -stale -##ntine -plump -asteroids -rediscovered -buds -flea -hive -##neas -1737 -classifications -debuts -##eles -olympus -scala -##eurs -##gno -##mute -hummed -sigismund -visuals -wiggled -await -pilasters -clench -sulfate -##ances -bellevue -enigma -trainee -snort -##sw -clouded -denim -##rank -##rder -churning -hartman -lodges -riches -sima -##missible -accountable -socrates -regulates -mueller -##cr -1702 -avoids -solids -himalayas -nutrient -pup -##jevic -squat -fades -nec -##lates -##pina -##rona -##ου -privateer -tequila -##gative -##mpton -apt -hornet -immortals -##dou -asturias -cleansing -dario -##rries -##anta -etymology -servicing -zhejiang -##venor -##nx -horned -erasmus -rayon -relocating -£10 -##bags -escalated -promenade -stubble -2010s -artisans -axial -liquids -mora -sho -yoo -##tsky -bundles -oldies -##nally -notification -bastion -##ths -sparkle -##lved -1728 -leash -pathogen -highs -##hmi -immature -880 -gonzaga -ignatius -mansions -monterrey -sweets -bryson -##loe -polled -regatta -brightest -pei -rosy -squid -hatfield -payroll -addict -meath -cornerback -heaviest -lodging -##mage -capcom -rippled -##sily -barnet -mayhem -ymca -snuggled -rousseau -##cute -blanchard -284 -fragmented -leighton -chromosomes -risking -##md -##strel -##utter -corinne -coyotes -cynical -hiroshi -yeomanry -##ractive -ebook -grading -mandela -plume -agustin -magdalene -##rkin -bea -femme -trafford -##coll -##lun -##tance -52nd -fourier -upton -##mental -camilla -gust -iihf -islamabad -longevity -##kala -feldman -netting -##rization -endeavour -foraging -mfa -orr -##open -greyish -contradiction -graz -##ruff -handicapped -marlene -tweed -oaxaca -spp -campos -miocene -pri -configured -cooks -pluto -cozy -pornographic -##entes -70th -fairness -glided -jonny -lynne -rounding -sired -##emon -##nist -remade -uncover -##mack -complied -lei -newsweek -##jured -##parts -##enting -##pg -293 -finer -guerrillas -athenian -deng -disused -stepmother -accuse -gingerly -seduction -521 -confronting -##walker -##going -gora -nostalgia -sabres -virginity -wrenched -##minated -syndication -wielding -eyre -##56 -##gnon -##igny -behaved -taxpayer -sweeps -##growth -childless -gallant -##ywood -amplified -geraldine -scrape -##ffi -babylonian -fresco -##rdan -##kney -##position -1718 -restricting -tack -fukuoka -osborn -selector -partnering -##dlow -318 -gnu -kia -tak -whitley -gables -##54 -##mania -mri -softness -immersion -##bots -##evsky -1713 -chilling -insignificant -pcs -##uis -elites -lina -purported -supplemental -teaming -##americana -##dding -##inton -proficient -rouen -##nage -##rret -niccolo -selects -##bread -fluffy -1621 -gruff -knotted -mukherjee -polgara -thrash -nicholls -secluded -smoothing -thru -corsica -loaf -whitaker -inquiries -##rrier -##kam -indochina -289 -marlins -myles -peking -##tea -extracts -pastry -superhuman -connacht -vogel -##ditional -##het -##udged -##lash -gloss -quarries -refit -teaser -##alic -##gaon -20s -materialized -sling -camped -pickering -tung -tracker -pursuant -##cide -cranes -soc -##cini -##typical -##viere -anhalt -overboard -workout -chores -fares -orphaned -stains -##logie -fenton -surpassing -joyah -triggers -##itte -grandmaster -##lass -##lists -clapping -fraudulent -ledger -nagasaki -##cor -##nosis -##tsa -eucalyptus -tun -##icio -##rney -##tara -dax -heroism -ina -wrexham -onboard -unsigned -##dates -moshe -galley -winnie -droplets -exiles -praises -watered -noodles -##aia -fein -adi -leland -multicultural -stink -bingo -comets -erskine -modernized -canned -constraint -domestically -chemotherapy -featherweight -stifled -##mum -darkly -irresistible -refreshing -hasty -isolate -##oys -kitchener -planners -##wehr -cages -yarn -implant -toulon -elects -childbirth -yue -##lind -##lone -cn -rightful -sportsman -junctions -remodeled -specifies -##rgh -291 -##oons -complimented -##urgent -lister -ot -##logic -bequeathed -cheekbones -fontana -gabby -##dial -amadeus -corrugated -maverick -resented -triangles -##hered -##usly -nazareth -tyrol -1675 -assent -poorer -sectional -aegean -##cous -296 -nylon -ghanaian -##egorical -##weig -cushions -forbid -fusiliers -obstruction -somerville -##scia -dime -earrings -elliptical -leyte -oder -polymers -timmy -atm -midtown -piloted -settles -continual -externally -mayfield -##uh -enrichment -henson -keane -persians -1733 -benji -braden -pep -324 -##efe -contenders -pepsi -valet -##isches -298 -##asse -##earing -goofy -stroll -##amen -authoritarian -occurrences -adversary -ahmedabad -tangent -toppled -dorchester -1672 -modernism -marxism -islamist -charlemagne -exponential -racks -unicode -brunette -mbc -pic -skirmish -##bund -##lad -##powered -##yst -hoisted -messina -shatter -##ctum -jedi -vantage -##music -##neil -clemens -mahmoud -corrupted -authentication -lowry -nils -##washed -omnibus -wounding -jillian -##itors -##opped -serialized -narcotics -handheld -##arm -##plicity -intersecting -stimulating -##onis -crate -fellowships -hemingway -casinos -climatic -fordham -copeland -drip -beatty -leaflets -robber -brothel -madeira -##hedral -sphinx -ultrasound -##vana -valor -forbade -leonid -villas -##aldo -duane -marquez -##cytes -disadvantaged -forearms -kawasaki -reacts -consular -lax -uncles -uphold -##hopper -concepcion -dorsey -lass -##izan -arching -passageway -1708 -researches -tia -internationals -##graphs -##opers -distinguishes -javanese -divert -##uven -plotted -##listic -##rwin -##erik -##tify -affirmative -signifies -validation -##bson -kari -felicity -georgina -zulu -##eros -##rained -##rath -overcoming -##dot -argyll -##rbin -1734 -chiba -ratification -windy -earls -parapet -##marks -hunan -pristine -astrid -punta -##gart -brodie -##kota -##oder -malaga -minerva -rouse -##phonic -bellowed -pagoda -portals -reclamation -##gur -##odies -##⁄₄ -parentheses -quoting -allergic -palette -showcases -benefactor -heartland -nonlinear -##tness -bladed -cheerfully -scans -##ety -##hone -1666 -girlfriends -pedersen -hiram -sous -##liche -##nator -1683 -##nery -##orio -##umen -bobo -primaries -smiley -##cb -unearthed -uniformly -fis -metadata -1635 -ind -##oted -recoil -##titles -##tura -##ια -406 -hilbert -jamestown -mcmillan -tulane -seychelles -##frid -antics -coli -fated -stucco -##grants -1654 -bulky -accolades -arrays -caledonian -carnage -optimism -puebla -##tative -##cave -enforcing -rotherham -seo -dunlop -aeronautics -chimed -incline -zoning -archduke -hellenistic -##oses -##sions -candi -thong -##ople -magnate -rustic -##rsk -projective -slant -##offs -danes -hollis -vocalists -##ammed -congenital -contend -gesellschaft -##ocating -##pressive -douglass -quieter -##cm -##kshi -howled -salim -spontaneously -townsville -buena -southport -##bold -kato -1638 -faerie -stiffly -##vus -##rled -297 -flawless -realising -taboo -##7th -bytes -straightening -356 -jena -##hid -##rmin -cartwright -berber -bertram -soloists -411 -noses -417 -coping -fission -hardin -inca -##cen -1717 -mobilized -vhf -##raf -biscuits -curate -##85 -##anial -331 -gaunt -neighbourhoods -1540 -##abas -blanca -bypassed -sockets -behold -coincidentally -##bane -nara -shave -splinter -terrific -##arion -##erian -commonplace -juris -redwood -waistband -boxed -caitlin -fingerprints -jennie -naturalized -##ired -balfour -craters -jody -bungalow -hugely -quilt -glitter -pigeons -undertaker -bulging -constrained -goo -##sil -##akh -assimilation -reworked -##person -persuasion -##pants -felicia -##cliff -##ulent -1732 -explodes -##dun -##inium -##zic -lyman -vulture -hog -overlook -begs -northwards -ow -spoil -##urer -fatima -favorably -accumulate -sargent -sorority -corresponded -dispersal -kochi -toned -##imi -##lita -internacional -newfound -##agger -##lynn -##rigue -booths -peanuts -##eborg -medicare -muriel -nur -##uram -crates -millennia -pajamas -worsened -##breakers -jimi -vanuatu -yawned -##udeau -carousel -##hony -hurdle -##ccus -##mounted -##pod -rv -##eche -airship -ambiguity -compulsion -recapture -##claiming -arthritis -##osomal -1667 -asserting -ngc -sniffing -dade -discontent -glendale -ported -##amina -defamation -rammed -##scent -fling -livingstone -##fleet -875 -##ppy -apocalyptic -comrade -lcd -##lowe -cessna -eine -persecuted -subsistence -demi -hoop -reliefs -710 -coptic -progressing -stemmed -perpetrators -1665 -priestess -##nio -dobson -ebony -rooster -itf -tortricidae -##bbon -##jian -cleanup -##jean -##øy -1721 -eighties -taxonomic -holiness -##hearted -##spar -antilles -showcasing -stabilized -##nb -gia -mascara -michelangelo -dawned -##uria -##vinsky -extinguished -fitz -grotesque -£100 -##fera -##loid -##mous -barges -neue -throbbed -cipher -johnnie -##a1 -##mpt -outburst -##swick -spearheaded -administrations -c1 -heartbreak -pixels -pleasantly -##enay -lombardy -plush -##nsed -bobbie -##hly -reapers -tremor -xiang -minogue -substantive -hitch -barak -##wyl -kwan -##encia -910 -obscene -elegance -indus -surfer -bribery -conserve -##hyllum -##masters -horatio -##fat -apes -rebound -psychotic -##pour -iteration -##mium -##vani -botanic -horribly -antiques -dispose -paxton -##hli -##wg -timeless -1704 -disregard -engraver -hounds -##bau -##version -looted -uno -facilitates -groans -masjid -rutland -antibody -disqualification -decatur -footballers -quake -slacks -48th -rein -scribe -stabilize -commits -exemplary -tho -##hort -##chison -pantry -traversed -##hiti -disrepair -identifiable -vibrated -baccalaureate -##nnis -csa -interviewing -##iensis -##raße -greaves -wealthiest -343 -classed -jogged -£5 -##58 -##atal -illuminating -knicks -respecting -##uno -scrubbed -##iji -##dles -kruger -moods -growls -raider -silvia -chefs -kam -vr -cree -percival -##terol -gunter -counterattack -defiant -henan -ze -##rasia -##riety -equivalence -submissions -##fra -##thor -bautista -mechanically -##heater -cornice -herbal -templar -##mering -outputs -ruining -ligand -renumbered -extravagant -mika -blockbuster -eta -insurrection -##ilia -darkening -ferocious -pianos -strife -kinship -##aer -melee -##anor -##iste -##may -##oue -decidedly -weep -##jad -##missive -##ppel -354 -puget -unease -##gnant -1629 -hammering -kassel -ob -wessex -##lga -bromwich -egan -paranoia -utilization -##atable -##idad -contradictory -provoke -##ols -##ouring -##tangled -knesset -##very -##lette -plumbing -##sden -##¹ -greensboro -occult -sniff -338 -zev -beaming -gamer -haggard -mahal -##olt -##pins -mendes -utmost -briefing -gunnery -##gut -##pher -##zh -##rok -1679 -khalifa -sonya -##boot -principals -urbana -wiring -##liffe -##minating -##rrado -dahl -nyu -skepticism -np -townspeople -ithaca -lobster -somethin -##fur -##arina -##−1 -freighter -zimmerman -biceps -contractual -##herton -amend -hurrying -subconscious -##anal -336 -meng -clermont -spawning -##eia -##lub -dignitaries -impetus -snacks -spotting -twigs -##bilis -##cz -##ouk -libertadores -nic -skylar -##aina -##firm -gustave -asean -##anum -dieter -legislatures -flirt -bromley -trolls -umar -##bbies -##tyle -blah -parc -bridgeport -crank -negligence -##nction -46th -constantin -molded -bandages -seriousness -00pm -siegel -carpets -compartments -upbeat -statehood -##dner -##edging -marko -730 -platt -##hane -paving -##iy -1738 -abbess -impatience -limousine -nbl -##talk -441 -lucille -mojo -nightfall -robbers -##nais -karel -brisk -calves -replicate -ascribed -telescopes -##olf -intimidated -##reen -ballast -specialization -##sit -aerodynamic -caliphate -rainer -visionary -##arded -epsilon -##aday -##onte -aggregation -auditory -boosted -reunification -kathmandu -loco -robyn -402 -acknowledges -appointing -humanoid -newell -redeveloped -restraints -##tained -barbarians -chopper -1609 -italiana -##lez -##lho -investigates -wrestlemania -##anies -##bib -690 -##falls -creaked -dragoons -gravely -minions -stupidity -volley -##harat -##week -musik -##eries -##uously -fungal -massimo -semantics -malvern -##ahl -##pee -discourage -embryo -imperialism -1910s -profoundly -##ddled -jiangsu -sparkled -stat -##holz -sweatshirt -tobin -##iction -sneered -##cheon -##oit -brit -causal -smyth -##neuve -diffuse -perrin -silvio -##ipes -##recht -detonated -iqbal -selma -##nism -##zumi -roasted -##riders -tay -##ados -##mament -##mut -##rud -840 -completes -nipples -cfa -flavour -hirsch -##laus -calderon -sneakers -moravian -##ksha -1622 -rq -294 -##imeters -bodo -##isance -##pre -##ronia -anatomical -excerpt -##lke -dh -kunst -##tablished -##scoe -biomass -panted -unharmed -gael -housemates -montpellier -##59 -coa -rodents -tonic -hickory -singleton -##taro -451 -1719 -aldo -breaststroke -dempsey -och -rocco -##cuit -merton -dissemination -midsummer -serials -##idi -haji -polynomials -##rdon -gs -enoch -prematurely -shutter -taunton -£3 -##grating -##inates -archangel -harassed -##asco -326 -archway -dazzling -##ecin -1736 -sumo -wat -##kovich -1086 -honneur -##ently -##nostic -##ttal -##idon -1605 -403 -1716 -blogger -rents -##gnan -hires -##ikh -##dant -howie -##rons -handler -retracted -shocks -1632 -arun -duluth -kepler -trumpeter -##lary -peeking -seasoned -trooper -##mara -laszlo -##iciencies -##rti -heterosexual -##inatory -##ssion -indira -jogging -##inga -##lism -beit -dissatisfaction -malice -##ately -nedra -peeling -##rgeon -47th -stadiums -475 -vertigo -##ains -iced -restroom -##plify -##tub -illustrating -pear -##chner -##sibility -inorganic -rappers -receipts -watery -##kura -lucinda -##oulos -reintroduced -##8th -##tched -gracefully -saxons -nutritional -wastewater -rained -favourites -bedrock -fisted -hallways -likeness -upscale -##lateral -1580 -blinds -prequel -##pps -##tama -deter -humiliating -restraining -tn -vents -1659 -laundering -recess -rosary -tractors -coulter -federer -##ifiers -##plin -persistence -##quitable -geschichte -pendulum -quakers -##beam -bassett -pictorial -buffet -koln -##sitor -drills -reciprocal -shooters -##57 -##cton -##tees -converge -pip -dmitri -donnelly -yamamoto -aqua -azores -demographics -hypnotic -spitfire -suspend -wryly -roderick -##rran -sebastien -##asurable -mavericks -##fles -##200 -himalayan -prodigy -##iance -transvaal -demonstrators -handcuffs -dodged -mcnamara -sublime -1726 -crazed -##efined -##till -ivo -pondered -reconciled -shrill -sava -##duk -bal -cad -heresy -jaipur -goran -##nished -341 -lux -shelly -whitehall -##hre -israelis -peacekeeping -##wled -1703 -demetrius -ousted -##arians -##zos -beale -anwar -backstroke -raged -shrinking -cremated -##yck -benign -towing -wadi -darmstadt -landfill -parana -soothe -colleen -sidewalks -mayfair -tumble -hepatitis -ferrer -superstructure -##gingly -##urse -##wee -anthropological -translators -##mies -closeness -hooves -##pw -mondays -##roll -##vita -landscaping -##urized -purification -sock -thorns -thwarted -jalan -tiberius -##taka -saline -##rito -confidently -khyber -sculptors -##ij -brahms -hammersmith -inspectors -battista -fivb -fragmentation -hackney -##uls -arresting -exercising -antoinette -bedfordshire -##zily -dyed -##hema -1656 -racetrack -variability -##tique -1655 -austrians -deteriorating -madman -theorists -aix -lehman -weathered -1731 -decreed -eruptions -1729 -flaw -quinlan -sorbonne -flutes -nunez -1711 -adored -downwards -fable -rasped -1712 -moritz -mouthful -renegade -shivers -stunts -dysfunction -restrain -translit -327 -pancakes -##avio -##cision -##tray -351 -vial -##lden -bain -##maid -##oxide -chihuahua -malacca -vimes -##rba -##rnier -1664 -donnie -plaques -##ually -337 -bangs -floppy -huntsville -loretta -nikolay -##otte -eater -handgun -ubiquitous -##hett -eras -zodiac -1634 -##omorphic -1820s -##zog -cochran -##bula -##lithic -warring -##rada -dalai -excused -blazers -mcconnell -reeling -bot -este -##abi -geese -hoax -taxon -##bla -guitarists -##icon -condemning -hunts -inversion -moffat -taekwondo -##lvis -1624 -stammered -##rest -##rzy -sousa -fundraiser -marylebone -navigable -uptown -cabbage -daniela -salman -shitty -whimper -##kian -##utive -programmers -protections -rm -##rmi -##rued -forceful -##enes -fuss -##tao -##wash -brat -oppressive -reykjavik -spartak -ticking -##inkles -##kiewicz -adolph -horst -maui -protege -straighten -cpc -landau -concourse -clements -resultant -##ando -imaginative -joo -reactivated -##rem -##ffled -##uising -consultative -##guide -flop -kaitlyn -mergers -parenting -somber -##vron -supervise -vidhan -##imum -courtship -exemplified -harmonies -medallist -refining -##rrow -##ка -amara -##hum -780 -goalscorer -sited -overshadowed -rohan -displeasure -secretive -multiplied -osman -##orth -engravings -padre -##kali -##veda -miniatures -mis -##yala -clap -pali -rook -##cana -1692 -57th -antennae -astro -oskar -1628 -bulldog -crotch -hackett -yucatan -##sure -amplifiers -brno -ferrara -migrating -##gree -thanking -turing -##eza -mccann -ting -andersson -onslaught -gaines -ganga -incense -standardization -##mation -sentai -scuba -stuffing -turquoise -waivers -alloys -##vitt -regaining -vaults -##clops -##gizing -digger -furry -memorabilia -probing -##iad -payton -rec -deutschland -filippo -opaque -seamen -zenith -afrikaans -##filtration -disciplined -inspirational -##merie -banco -confuse -grafton -tod -##dgets -championed -simi -anomaly -biplane -##ceptive -electrode -##para -1697 -cleavage -crossbow -swirl -informant -##lars -##osta -afi -bonfire -spec -##oux -lakeside -slump -##culus -##lais -##qvist -##rrigan -1016 -facades -borg -inwardly -cervical -xl -pointedly -050 -stabilization -##odon -chests -1699 -hacked -ctv -orthogonal -suzy -##lastic -gaulle -jacobite -rearview -##cam -##erted -ashby -##drik -##igate -##mise -##zbek -affectionately -canine -disperse -latham -##istles -##ivar -spielberg -##orin -##idium -ezekiel -cid -##sg -durga -middletown -##cina -customized -frontiers -harden -##etano -##zzy -1604 -bolsheviks -##66 -coloration -yoko -##bedo -briefs -slabs -debra -liquidation -plumage -##oin -blossoms -dementia -subsidy -1611 -proctor -relational -jerseys -parochial -ter -##ici -esa -peshawar -cavalier -loren -cpi -idiots -shamrock -1646 -dutton -malabar -mustache -##endez -##ocytes -referencing -terminates -marche -yarmouth -##sop -acton -mated -seton -subtly -baptised -beige -extremes -jolted -kristina -telecast -##actic -safeguard -waldo -##baldi -##bular -endeavors -sloppy -subterranean -##ensburg -##itung -delicately -pigment -tq -##scu -1626 -##ound -collisions -coveted -herds -##personal -##meister -##nberger -chopra -##ricting -abnormalities -defective -galician -lucie -##dilly -alligator -likened -##genase -burundi -clears -complexion -derelict -deafening -diablo -fingered -champaign -dogg -enlist -isotope -labeling -mrna -##erre -brilliance -marvelous -##ayo -1652 -crawley -ether -footed -dwellers -deserts -hamish -rubs -warlock -skimmed -##lizer -870 -buick -embark -heraldic -irregularities -##ajan -kiara -##kulam -##ieg -antigen -kowalski -##lge -oakley -visitation -##mbit -vt -##suit -1570 -murderers -##miento -##rites -chimneys -##sling -condemn -custer -exchequer -havre -##ghi -fluctuations -##rations -dfb -hendricks -vaccines -##tarian -nietzsche -biking -juicy -##duced -brooding -scrolling -selangor -##ragan -352 -annum -boomed -seminole -sugarcane -##dna -departmental -dismissing -innsbruck -arteries -ashok -batavia -daze -kun -overtook -##rga -##tlan -beheaded -gaddafi -holm -electronically -faulty -galilee -fractures -kobayashi -##lized -gunmen -magma -aramaic -mala -eastenders -inference -messengers -bf -##qu -407 -bathrooms -##vere -1658 -flashbacks -ideally -misunderstood -##jali -##weather -mendez -##grounds -505 -uncanny -##iii -1709 -friendships -##nbc -sacrament -accommodated -reiterated -logistical -pebbles -thumped -##escence -administering -decrees -drafts -##flight -##cased -##tula -futuristic -picket -intimidation -winthrop -##fahan -interfered -339 -afar -francoise -morally -uta -cochin -croft -dwarfs -##bruck -##dents -##nami -biker -##hner -##meral -nano -##isen -##ometric -##pres -##ан -brightened -meek -parcels -securely -gunners -##jhl -##zko -agile -hysteria -##lten -##rcus -bukit -champs -chevy -cuckoo -leith -sadler -theologians -welded -##section -1663 -jj -plurality -xander -##rooms -##formed -shredded -temps -intimately -pau -tormented -##lok -##stellar -1618 -charred -ems -essen -##mmel -alarms -spraying -ascot -blooms -twinkle -##abia -##apes -internment -obsidian -##chaft -snoop -##dav -##ooping -malibu -##tension -quiver -##itia -hays -mcintosh -travers -walsall -##ffie -1623 -beverley -schwarz -plunging -structurally -m3 -rosenthal -vikram -##tsk -770 -ghz -##onda -##tiv -chalmers -groningen -pew -reckon -unicef -##rvis -55th -##gni -1651 -sulawesi -avila -cai -metaphysical -screwing -turbulence -##mberg -augusto -samba -56th -baffled -momentary -toxin -##urian -##wani -aachen -condoms -dali -steppe -##3d -##app -##oed -##year -adolescence -dauphin -electrically -inaccessible -microscopy -nikita -##ega -atv -##cel -##enter -##oles -##oteric -##ы -accountants -punishments -wrongly -bribes -adventurous -clinch -flinders -southland -##hem -##kata -gough -##ciency -lads -soared -##ה -undergoes -deformation -outlawed -rubbish -##arus -##mussen -##nidae -##rzburg -arcs -##ingdon -##tituted -1695 -wheelbase -wheeling -bombardier -campground -zebra -##lices -##oj -##bain -lullaby -##ecure -donetsk -wylie -grenada -##arding -##ης -squinting -eireann -opposes -##andra -maximal -runes -##broken -##cuting -##iface -##ror -##rosis -additive -britney -adultery -triggering -##drome -detrimental -aarhus -containment -jc -swapped -vichy -##ioms -madly -##oric -##rag -brant -##ckey -##trix -1560 -1612 -broughton -rustling -##stems -##uder -asbestos -mentoring -##nivorous -finley -leaps -##isan -apical -pry -slits -substitutes -##dict -intuitive -fantasia -insistent -unreasonable -##igen -##vna -domed -hannover -margot -ponder -##zziness -impromptu -jian -lc -rampage -stemming -##eft -andrey -gerais -whichever -amnesia -appropriated -anzac -clicks -modifying -ultimatum -cambrian -maids -verve -yellowstone -##mbs -conservatoire -##scribe -adherence -dinners -spectra -imperfect -mysteriously -sidekick -tatar -tuba -##aks -##ifolia -distrust -##athan -##zle -c2 -ronin -zac -##pse -celaena -instrumentalist -scents -skopje -##mbling -comical -compensated -vidal -condor -intersect -jingle -wavelengths -##urrent -mcqueen -##izzly -carp -weasel -422 -kanye -militias -postdoctoral -eugen -gunslinger -##ɛ -faux -hospice -##for -appalled -derivation -dwarves -##elis -dilapidated -##folk -astoria -philology -##lwyn -##otho -##saka -inducing -philanthropy -##bf -##itative -geek -markedly -sql -##yce -bessie -indices -rn -##flict -495 -frowns -resolving -weightlifting -tugs -cleric -contentious -1653 -mania -rms -##miya -##reate -##ruck -##tucket -bien -eels -marek -##ayton -##cence -discreet -unofficially -##ife -leaks -##bber -1705 -332 -dung -compressor -hillsborough -pandit -shillings -distal -##skin -381 -##tat -##you -nosed -##nir -mangrove -undeveloped -##idia -textures -##inho -##500 -##rise -ae -irritating -nay -amazingly -bancroft -apologetic -compassionate -kata -symphonies -##lovic -airspace -##lch -930 -gifford -precautions -fulfillment -sevilla -vulgar -martinique -##urities -looting -piccolo -tidy -##dermott -quadrant -armchair -incomes -mathematicians -stampede -nilsson -##inking -##scan -foo -quarterfinal -##ostal -shang -shouldered -squirrels -##owe -344 -vinegar -##bner -##rchy -##systems -delaying -##trics -ars -dwyer -rhapsody -sponsoring -##gration -bipolar -cinder -starters -##olio -##urst -421 -signage -##nty -aground -figurative -mons -acquaintances -duets -erroneously -soyuz -elliptic -recreated -##cultural -##quette -##ssed -##tma -##zcz -moderator -scares -##itaire -##stones -##udence -juniper -sighting -##just -##nsen -britten -calabria -ry -bop -cramer -forsyth -stillness -##л -airmen -gathers -unfit -##umber -##upt -taunting -##rip -seeker -streamlined -##bution -holster -schumann -tread -vox -##gano -##onzo -strive -dil -reforming -covent -newbury -predicting -##orro -decorate -tre -##puted -andover -ie -asahi -dept -dunkirk -gills -##tori -buren -huskies -##stis -##stov -abstracts -bets -loosen -##opa -1682 -yearning -##glio -##sir -berman -effortlessly -enamel -napoli -persist -##peration -##uez -attache -elisa -b1 -invitations -##kic -accelerating -reindeer -boardwalk -clutches -nelly -polka -starbucks -##kei -adamant -huey -lough -unbroken -adventurer -embroidery -inspecting -stanza -##ducted -naia -taluka -##pone -##roids -chases -deprivation -florian -##jing -##ppet -earthly -##lib -##ssee -colossal -foreigner -vet -freaks -patrice -rosewood -triassic -upstate -##pkins -dominates -ata -chants -ks -vo -##400 -##bley -##raya -##rmed -555 -agra -infiltrate -##ailing -##ilation -##tzer -##uppe -##werk -binoculars -enthusiast -fujian -squeak -##avs -abolitionist -almeida -boredom -hampstead -marsden -rations -##ands -inflated -334 -bonuses -rosalie -patna -##rco -329 -detachments -penitentiary -54th -flourishing -woolf -##dion -##etched -papyrus -##lster -##nsor -##toy -bobbed -dismounted -endelle -inhuman -motorola -tbs -wince -wreath -##ticus -hideout -inspections -sanjay -disgrace -infused -pudding -stalks -##urbed -arsenic -leases -##hyl -##rrard -collarbone -##waite -##wil -dowry -##bant -##edance -genealogical -nitrate -salamanca -scandals -thyroid -necessitated -##! -##" -### -##$ -##% -##& -##' -##( -##) -##* -##+ -##, -##- -##. -##/ -##: -##; -##< -##= -##> -##? -##@ -##[ -##\ -##] -##^ -##_ -##` -##{ -##| -##} -##~ -##¡ -##¢ -##£ -##¤ -##¥ -##¦ -##§ -##¨ -##© -##ª -##« -##¬ -##® -##± -##´ -##µ -##¶ -##· -##º -##» -##¼ -##¾ -##¿ -##æ -##ð -##÷ -##þ -##đ -##ħ -##ŋ -##œ -##ƒ -##ɐ -##ɑ -##ɒ -##ɔ -##ɕ -##ə -##ɡ -##ɣ -##ɨ -##ɪ -##ɫ -##ɬ -##ɯ -##ɲ -##ɴ -##ɹ -##ɾ -##ʀ -##ʁ -##ʂ -##ʃ -##ʉ -##ʊ -##ʋ -##ʌ -##ʎ -##ʐ -##ʑ -##ʒ -##ʔ -##ʰ -##ʲ -##ʳ -##ʷ -##ʸ -##ʻ -##ʼ -##ʾ -##ʿ -##ˈ -##ˡ -##ˢ -##ˣ -##ˤ -##β -##γ -##δ -##ε -##ζ -##θ -##κ -##λ -##μ -##ξ -##ο -##π -##ρ -##σ -##τ -##υ -##φ -##χ -##ψ -##ω -##б -##г -##д -##ж -##з -##м -##п -##с -##у -##ф -##х -##ц -##ч -##ш -##щ -##ъ -##э -##ю -##ђ -##є -##і -##ј -##љ -##њ -##ћ -##ӏ -##ա -##բ -##գ -##դ -##ե -##թ -##ի -##լ -##կ -##հ -##մ -##յ -##ն -##ո -##պ -##ս -##վ -##տ -##ր -##ւ -##ք -##־ -##א -##ב -##ג -##ד -##ו -##ז -##ח -##ט -##י -##ך -##כ -##ל -##ם -##מ -##ן -##נ -##ס -##ע -##ף -##פ -##ץ -##צ -##ק -##ר -##ש -##ת -##، -##ء -##ب -##ت -##ث -##ج -##ح -##خ -##ذ -##ز -##س -##ش -##ص -##ض -##ط -##ظ -##ع -##غ -##ـ -##ف -##ق -##ك -##و -##ى -##ٹ -##پ -##چ -##ک -##گ -##ں -##ھ -##ہ -##ے -##अ -##आ -##उ -##ए -##क -##ख -##ग -##च -##ज -##ट -##ड -##ण -##त -##थ -##द -##ध -##न -##प -##ब -##भ -##म -##य -##र -##ल -##व -##श -##ष -##स -##ह -##ा -##ि -##ी -##ो -##। -##॥ -##ং -##অ -##আ -##ই -##উ -##এ -##ও -##ক -##খ -##গ -##চ -##ছ -##জ -##ট -##ড -##ণ -##ত -##থ -##দ -##ধ -##ন -##প -##ব -##ভ -##ম -##য -##র -##ল -##শ -##ষ -##স -##হ -##া -##ি -##ী -##ে -##க -##ச -##ட -##த -##ந -##ன -##ப -##ம -##ய -##ர -##ல -##ள -##வ -##ா -##ி -##ு -##ே -##ை -##ನ -##ರ -##ಾ -##ක -##ය -##ර -##ල -##ව -##ා -##ก -##ง -##ต -##ท -##น -##พ -##ม -##ย -##ร -##ล -##ว -##ส -##อ -##า -##เ -##་ -##། -##ག -##ང -##ད -##ན -##པ -##བ -##མ -##འ -##ར -##ལ -##ས -##မ -##ა -##ბ -##გ -##დ -##ე -##ვ -##თ -##ი -##კ -##ლ -##მ -##ნ -##ო -##რ -##ს -##ტ -##უ -##ᄀ -##ᄂ -##ᄃ -##ᄅ -##ᄆ -##ᄇ -##ᄉ -##ᄊ -##ᄋ -##ᄌ -##ᄎ -##ᄏ -##ᄐ -##ᄑ -##ᄒ -##ᅡ -##ᅢ -##ᅥ -##ᅦ -##ᅧ -##ᅩ -##ᅪ -##ᅭ -##ᅮ -##ᅯ -##ᅲ -##ᅳ -##ᅴ -##ᅵ -##ᆨ -##ᆫ -##ᆯ -##ᆷ -##ᆸ -##ᆼ -##ᴬ -##ᴮ -##ᴰ -##ᴵ -##ᴺ -##ᵀ -##ᵃ -##ᵇ -##ᵈ -##ᵉ -##ᵍ -##ᵏ -##ᵐ -##ᵒ -##ᵖ -##ᵗ -##ᵘ -##ᵣ -##ᵤ -##ᵥ -##ᶜ -##ᶠ -##‐ -##‑ -##‒ -##– -##— -##― -##‖ -##‘ -##’ -##‚ -##“ -##” -##„ -##† -##‡ -##• -##… -##‰ -##′ -##″ -##› -##‿ -##⁄ -##⁰ -##ⁱ -##⁴ -##⁵ -##⁶ -##⁷ -##⁸ -##⁹ -##⁻ -##ⁿ -##₅ -##₆ -##₇ -##₈ -##₉ -##₊ -##₍ -##₎ -##ₐ -##ₑ -##ₒ -##ₓ -##ₕ -##ₖ -##ₗ -##ₘ -##ₚ -##ₛ -##ₜ -##₤ -##₩ -##€ -##₱ -##₹ -##ℓ -##№ -##ℝ -##™ -##⅓ -##⅔ -##← -##↑ -##→ -##↓ -##↔ -##↦ -##⇄ -##⇌ -##⇒ -##∂ -##∅ -##∆ -##∇ -##∈ -##∗ -##∘ -##√ -##∞ -##∧ -##∨ -##∩ -##∪ -##≈ -##≡ -##≤ -##≥ -##⊂ -##⊆ -##⊕ -##⊗ -##⋅ -##─ -##│ -##■ -##▪ -##● -##★ -##☆ -##☉ -##♠ -##♣ -##♥ -##♦ -##♯ -##⟨ -##⟩ -##ⱼ -##⺩ -##⺼ -##⽥ -##、 -##。 -##〈 -##〉 -##《 -##》 -##「 -##」 -##『 -##』 -##〜 -##あ -##い -##う -##え -##お -##か -##き -##く -##け -##こ -##さ -##し -##す -##せ -##そ -##た -##ち -##っ -##つ -##て -##と -##な -##に -##ぬ -##ね -##の -##は -##ひ -##ふ -##へ -##ほ -##ま -##み -##む -##め -##も -##や -##ゆ -##よ -##ら -##り -##る -##れ -##ろ -##を -##ん -##ァ -##ア -##ィ -##イ -##ウ -##ェ -##エ -##オ -##カ -##キ -##ク -##ケ -##コ -##サ -##シ -##ス -##セ -##タ -##チ -##ッ -##ツ -##テ -##ト -##ナ -##ニ -##ノ -##ハ -##ヒ -##フ -##ヘ -##ホ -##マ -##ミ -##ム -##メ -##モ -##ャ -##ュ -##ョ -##ラ -##リ -##ル -##レ -##ロ -##ワ -##ン -##・ -##ー -##一 -##三 -##上 -##下 -##不 -##世 -##中 -##主 -##久 -##之 -##也 -##事 -##二 -##五 -##井 -##京 -##人 -##亻 -##仁 -##介 -##代 -##仮 -##伊 -##会 -##佐 -##侍 -##保 -##信 -##健 -##元 -##光 -##八 -##公 -##内 -##出 -##分 -##前 -##劉 -##力 -##加 -##勝 -##北 -##区 -##十 -##千 -##南 -##博 -##原 -##口 -##古 -##史 -##司 -##合 -##吉 -##同 -##名 -##和 -##囗 -##四 -##国 -##國 -##土 -##地 -##坂 -##城 -##堂 -##場 -##士 -##夏 -##外 -##大 -##天 -##太 -##夫 -##奈 -##女 -##子 -##学 -##宀 -##宇 -##安 -##宗 -##定 -##宣 -##宮 -##家 -##宿 -##寺 -##將 -##小 -##尚 -##山 -##岡 -##島 -##崎 -##川 -##州 -##巿 -##帝 -##平 -##年 -##幸 -##广 -##弘 -##張 -##彳 -##後 -##御 -##德 -##心 -##忄 -##志 -##忠 -##愛 -##成 -##我 -##戦 -##戸 -##手 -##扌 -##政 -##文 -##新 -##方 -##日 -##明 -##星 -##春 -##昭 -##智 -##曲 -##書 -##月 -##有 -##朝 -##木 -##本 -##李 -##村 -##東 -##松 -##林 -##森 -##楊 -##樹 -##橋 -##歌 -##止 -##正 -##武 -##比 -##氏 -##民 -##水 -##氵 -##氷 -##永 -##江 -##沢 -##河 -##治 -##法 -##海 -##清 -##漢 -##瀬 -##火 -##版 -##犬 -##王 -##生 -##田 -##男 -##疒 -##発 -##白 -##的 -##皇 -##目 -##相 -##省 -##真 -##石 -##示 -##社 -##神 -##福 -##禾 -##秀 -##秋 -##空 -##立 -##章 -##竹 -##糹 -##美 -##義 -##耳 -##良 -##艹 -##花 -##英 -##華 -##葉 -##藤 -##行 -##街 -##西 -##見 -##訁 -##語 -##谷 -##貝 -##貴 -##車 -##軍 -##辶 -##道 -##郎 -##郡 -##部 -##都 -##里 -##野 -##金 -##鈴 -##镇 -##長 -##門 -##間 -##阝 -##阿 -##陳 -##陽 -##雄 -##青 -##面 -##風 -##食 -##香 -##馬 -##高 -##龍 -##龸 -##fi -##fl -##! -##( -##) -##, -##- -##. -##/ -##: -##? -##~ From 837cd6f890a518ac5bd3355900a0d7f479fe5328 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 23:15:55 +0800 Subject: [PATCH 125/173] =?UTF-8?q?cache=5Fresults=20=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E7=9A=84=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/core/test_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/core/test_utils.py b/test/core/test_utils.py index b846a32d..6d70d97c 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -140,7 +140,7 @@ class TestCache(unittest.TestCase): try: start_time = time.time() embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', - cache_filepath='test/demo_overwrite.pkl') + _cache_fp='test/demo_overwrite.pkl') end_time = time.time() pre_time = end_time - start_time with open('test/demo_overwrite.pkl', 'rb') as f: @@ -150,7 +150,7 @@ class TestCache(unittest.TestCase): self.assertListEqual(embed[i].tolist(), _embed[i].tolist()) start_time = time.time() embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', - cache_filepath='test/demo_overwrite.pkl') + _cache_fp='test/demo_overwrite.pkl') end_time = time.time() read_time = end_time - start_time print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) @@ -162,7 +162,7 @@ class TestCache(unittest.TestCase): try: start_time = time.time() embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', - refresh=True) + _refresh=True) end_time = time.time() pre_time = end_time - start_time with open('test/demo1.pkl', 'rb') as f: @@ -172,7 +172,7 @@ class TestCache(unittest.TestCase): self.assertListEqual(embed[i].tolist(), _embed[i].tolist()) start_time = time.time() embed, vocab, d = process_data_1('test/data_for_tests/word2vec_test.txt', 'test/data_for_tests/cws_train', - refresh=True) + _refresh=True) end_time = time.time() read_time = end_time - start_time print("Read using {:.3f}, while prepare using:{:.3f}".format(read_time, pre_time)) From 51b493d7165cdb2045c2c8198217d23afb75d5eb Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 14 May 2019 23:21:21 +0800 Subject: [PATCH 126/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=20io=20=E7=9A=84?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E4=BB=B6;=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=BA=86=E4=B8=80=E4=BA=9B=E8=BF=87=E6=97=B6=E7=9A=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/io/config | 62 ---------------- test/io/test_config_saver.py | 112 ----------------------------- test/io/test_dataset_loader.py | 10 +-- test/io/test_embed_loader.py | 17 +++-- test/modules/test_other_modules.py | 46 ------------ 5 files changed, 13 insertions(+), 234 deletions(-) delete mode 100644 test/io/config delete mode 100644 test/io/test_config_saver.py diff --git a/test/io/config b/test/io/config deleted file mode 100644 index 5ff9eacf..00000000 --- a/test/io/config +++ /dev/null @@ -1,62 +0,0 @@ -[test] -x = 1 - -y = 2 - -z = 3 -#this is an example -input = [1,2,3] - -text = "this is text" - -doubles = 0.8 - -tt = 0.5 - -test = 105 - -str = "this is a str" - -double = 0.5 - - -[t] -x = "this is an test section" - - - -[test-case-2] -double = 0.5 - -doubles = 0.8 - -tt = 0.5 - -test = 105 - -str = "this is a str" - -[another-test] -doubles = 0.8 - -tt = 0.5 - -test = 105 - -str = "this is a str" - -double = 0.5 - - -[one-another-test] -doubles = 0.8 - -tt = 0.5 - -test = 105 - -str = "this is a str" - -double = 0.5 - - diff --git a/test/io/test_config_saver.py b/test/io/test_config_saver.py deleted file mode 100644 index e5341d63..00000000 --- a/test/io/test_config_saver.py +++ /dev/null @@ -1,112 +0,0 @@ -import os -import unittest - -# from fastNLP.io import ConfigSection, ConfigLoader, ConfigSaver - - -class TestConfigSaver(unittest.TestCase): - def test_case_1(self): - config_file_dir = "." - config_file_name = "config" - config_file_path = os.path.join(config_file_dir, config_file_name) - - tmp_config_file_path = os.path.join(config_file_dir, "tmp_config") - - with open(config_file_path, "r") as f: - lines = f.readlines() - - standard_section = ConfigSection() - t_section = ConfigSection() - ConfigLoader().load_config(config_file_path, {"test": standard_section, "t": t_section}) - - config_saver = ConfigSaver(config_file_path) - - section = ConfigSection() - section["doubles"] = 0.8 - section["tt"] = 0.5 - section["test"] = 105 - section["str"] = "this is a str" - - test_case_2_section = section - test_case_2_section["double"] = 0.5 - - for k in section.__dict__.keys(): - standard_section[k] = section[k] - - config_saver.save_config_file("test", section) - config_saver.save_config_file("another-test", section) - config_saver.save_config_file("one-another-test", section) - config_saver.save_config_file("test-case-2", section) - - test_section = ConfigSection() - at_section = ConfigSection() - another_test_section = ConfigSection() - one_another_test_section = ConfigSection() - a_test_case_2_section = ConfigSection() - - ConfigLoader().load_config(config_file_path, {"test": test_section, - "another-test": another_test_section, - "t": at_section, - "one-another-test": one_another_test_section, - "test-case-2": a_test_case_2_section}) - - assert test_section == standard_section - assert at_section == t_section - assert another_test_section == section - assert one_another_test_section == section - assert a_test_case_2_section == test_case_2_section - - config_saver.save_config_file("test", section) - - with open(config_file_path, "w") as f: - f.writelines(lines) - - with open(tmp_config_file_path, "w") as f: - f.write('[test]\n') - f.write('this is an fault example\n') - - tmp_config_saver = ConfigSaver(tmp_config_file_path) - try: - tmp_config_saver._read_section() - except Exception as e: - pass - os.remove(tmp_config_file_path) - - try: - tmp_config_saver = ConfigSaver("file-NOT-exist") - except Exception as e: - pass - - def test_case_2(self): - config = "[section_A]\n[section_B]\n" - - with open("./test.cfg", "w", encoding="utf-8") as f: - f.write(config) - saver = ConfigSaver("./test.cfg") - - section = ConfigSection() - section["doubles"] = 0.8 - section["tt"] = [1, 2, 3] - section["test"] = 105 - section["str"] = "this is a str" - - saver.save_config_file("section_A", section) - - os.system("rm ./test.cfg") - - def test_case_3(self): - config = "[section_A]\ndoubles = 0.9\ntt = [1, 2, 3]\n[section_B]\n" - - with open("./test.cfg", "w", encoding="utf-8") as f: - f.write(config) - saver = ConfigSaver("./test.cfg") - - section = ConfigSection() - section["doubles"] = 0.8 - section["tt"] = [1, 2, 3] - section["test"] = 105 - section["str"] = "this is a str" - - saver.save_config_file("section_A", section) - - os.system("rm ./test.cfg") diff --git a/test/io/test_dataset_loader.py b/test/io/test_dataset_loader.py index 3c9d7c07..12d352b1 100644 --- a/test/io/test_dataset_loader.py +++ b/test/io/test_dataset_loader.py @@ -9,22 +9,22 @@ class TestDatasetLoader(unittest.TestCase): """ Test the the loader of Conll2003 dataset """ - dataset_path = "../data_for_tests/conll_2003_example.txt" + dataset_path = "test/data_for_tests/conll_2003_example.txt" loader = Conll2003Loader() dataset_2003 = loader.load(dataset_path) def test_PeopleDailyCorpusLoader(self): - data_set = PeopleDailyCorpusLoader().load("../data_for_tests/people_daily_raw.txt") + data_set = PeopleDailyCorpusLoader().load("test/data_for_tests/people_daily_raw.txt") def test_CSVLoader(self): ds = CSVLoader(sep='\t', headers=['words', 'label']) \ - .load('../data_for_tests/tutorial_sample_dataset.csv') + .load('test/data_for_tests/tutorial_sample_dataset.csv') assert len(ds) > 0 def test_SNLILoader(self): - ds = SNLILoader().load('../data_for_tests/sample_snli.jsonl') + ds = SNLILoader().load('test/data_for_tests/sample_snli.jsonl') assert len(ds) == 3 def test_JsonLoader(self): - ds = JsonLoader().load('../data_for_tests/sample_snli.jsonl') + ds = JsonLoader().load('test/data_for_tests/sample_snli.jsonl') assert len(ds) == 3 diff --git a/test/io/test_embed_loader.py b/test/io/test_embed_loader.py index d43a00fe..ff8ecfcf 100644 --- a/test/io/test_embed_loader.py +++ b/test/io/test_embed_loader.py @@ -3,15 +3,13 @@ import numpy as np from fastNLP import Vocabulary from fastNLP.io import EmbedLoader -import os -from fastNLP.io.dataset_loader import SSTLoader -from fastNLP.core.const import Const as C + class TestEmbedLoader(unittest.TestCase): def test_load_with_vocab(self): vocab = Vocabulary() - glove = "../data_for_tests/glove.6B.50d_test.txt" - word2vec = "../data_for_tests/word2vec_test.txt" + glove = "test/data_for_tests/glove.6B.50d_test.txt" + word2vec = "test/data_for_tests/word2vec_test.txt" vocab.add_word('the') vocab.add_word('none') g_m = EmbedLoader.load_with_vocab(glove, vocab) @@ -19,11 +17,11 @@ class TestEmbedLoader(unittest.TestCase): w_m = EmbedLoader.load_with_vocab(word2vec, vocab, normalize=True) self.assertEqual(w_m.shape, (4, 50)) self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 4) - + def test_load_without_vocab(self): words = ['the', 'of', 'in', 'a', 'to', 'and'] - glove = "../data_for_tests/glove.6B.50d_test.txt" - word2vec = "../data_for_tests/word2vec_test.txt" + glove = "test/data_for_tests/glove.6B.50d_test.txt" + word2vec = "test/data_for_tests/word2vec_test.txt" g_m, vocab = EmbedLoader.load_without_vocab(glove) self.assertEqual(g_m.shape, (8, 50)) for word in words: @@ -39,9 +37,10 @@ class TestEmbedLoader(unittest.TestCase): self.assertAlmostEqual(np.linalg.norm(w_m, axis=1).sum(), 7) for word in words: self.assertIn(word, vocab) - + def test_read_all_glove(self): pass + # TODO # 这是可以运行的,但是总数少于行数,应该是由于glove有重复的word # path = '/where/to/read/full/glove' # init_embed, vocab = EmbedLoader.load_without_vocab(path, error='strict') diff --git a/test/modules/test_other_modules.py b/test/modules/test_other_modules.py index ef5020c1..c5462623 100644 --- a/test/modules/test_other_modules.py +++ b/test/modules/test_other_modules.py @@ -2,55 +2,9 @@ import unittest import torch -# from fastNLP.modules.other_modules import GroupNorm, LayerNormalization, BiLinear, BiAffine from fastNLP.modules.encoder.star_transformer import StarTransformer -class TestGroupNorm(unittest.TestCase): - def test_case_1(self): - gn = GroupNorm(num_features=1, num_groups=10, eps=1.5e-5) - x = torch.randn((20, 50, 10)) - y = gn(x) - - -class TestLayerNormalization(unittest.TestCase): - def test_case_1(self): - ln = LayerNormalization(layer_size=5, eps=2e-3) - x = torch.randn((20, 50, 5)) - y = ln(x) - - -class TestBiLinear(unittest.TestCase): - def test_case_1(self): - bl = BiLinear(n_left=5, n_right=5, n_out=10, bias=True) - x_left = torch.randn((7, 10, 20, 5)) - x_right = torch.randn((7, 10, 20, 5)) - y = bl(x_left, x_right) - print(bl) - bl2 = BiLinear(n_left=15, n_right=15, n_out=10, bias=True) - - -class TestBiAffine(unittest.TestCase): - def test_case_1(self): - batch_size = 16 - encoder_length = 21 - decoder_length = 32 - layer = BiAffine(10, 10, 25, biaffine=True) - decoder_input = torch.randn((batch_size, encoder_length, 10)) - encoder_input = torch.randn((batch_size, decoder_length, 10)) - y = layer(decoder_input, encoder_input) - self.assertEqual(tuple(y.shape), (batch_size, 25, encoder_length, decoder_length)) - - def test_case_2(self): - batch_size = 16 - encoder_length = 21 - decoder_length = 32 - layer = BiAffine(10, 10, 25, biaffine=False) - decoder_input = torch.randn((batch_size, encoder_length, 10)) - encoder_input = torch.randn((batch_size, decoder_length, 10)) - y = layer(decoder_input, encoder_input) - self.assertEqual(tuple(y.shape), (batch_size, 25, encoder_length, 1)) - class TestStarTransformer(unittest.TestCase): def test_1(self): model = StarTransformer(num_layers=6, hidden_size=100, num_head=8, head_dim=20, max_len=100) From 6ff33c4b8097561d5ad5dcba7fe47ae8ceaf6a14 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Tue, 14 May 2019 23:51:12 +0800 Subject: [PATCH 127/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/trainer.py | 2 ++ fastNLP/core/utils.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 2ad92f6b..a6293167 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -381,6 +381,8 @@ class Trainer(object): 5. None. 为None则不对模型进行任何处理,如果传入的model为torch.nn.DataParallel该值必须为None。 + 已知可能会出现的问题:Adagrad优化器可能无法正常使用这个参数,请手动管理模型位置。 + :param list(callbacks) callbacks: 用于在train过程中起调节作用的回调函数。比如early stop,negative sampling等可以 通过callback机制实现。 可使用的callback参见 :doc:`callback模块 ` :param int check_code_level: 模型检查等级. -1: 不进行检查; 0: 仅出现错误时停止; 1: 如果有field没有被使用, diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index f7539fd7..fa8f6feb 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -88,7 +88,7 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): def wrapper_(func): signature = inspect.signature(func) for key, _ in signature.parameters.items(): - if key in ('cache_filepath', 'refresh', 'verbose'): + if key in ('_cache_fp', '_refresh', '_verbose'): raise RuntimeError("The function decorated by cache_results cannot have keyword `{}`.".format(key)) def wrapper(*args, **kwargs): if '_cache_fp' in kwargs: From 38033ab8c3bdf8588b0928e8e495f3418ad8ab19 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 15 May 2019 00:27:22 +0800 Subject: [PATCH 128/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E9=80=9A=E8=BF=87test=5Futils.py=E7=9A=84=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/core/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/test_utils.py b/test/core/test_utils.py index 6d70d97c..91b5d00f 100644 --- a/test/core/test_utils.py +++ b/test/core/test_utils.py @@ -76,7 +76,7 @@ class TestMoveModelDeivce(unittest.TestCase): if torch.cuda.is_available(): device = [0] _model = _move_model_to_device(model, device) - assert isinstance(_model, nn.DataParallel) + assert not isinstance(_model, nn.DataParallel) device = [torch.device('cuda:0'), torch.device('cuda:0')] with self.assertRaises(Exception): _model = _move_model_to_device(model, device) From 846a1a515898fb37b8b1634f358e591afa3c7f75 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 15 May 2019 11:47:34 +0800 Subject: [PATCH 129/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8Epytorch1.1?= =?UTF-8?q?=E4=B8=AD=E7=9A=84padsequence=E7=9A=84=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E9=97=AE=E9=A2=98;=20=E4=BF=AE=E6=94=B9Trainer=E7=9A=84pbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 9 +++++---- fastNLP/core/trainer.py | 2 +- fastNLP/modules/encoder/variational_rnn.py | 7 ++++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index f337975a..9dce426b 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -130,7 +130,8 @@ class Callback(object): @property def pbar(self): - """如果在Callback中需要打印内容,请使用self.pbar.write(str)。否则可能出现命令行显示效果不太好的问题。""" + """如果在Callback中需要打印内容,请使用self.pbar.write(str)。否则可能出现命令行显示效果不太好的问题。在 + on_train_begin(), on_train_end(), on_exception()中请不要使用该属性,通过print输出即可。""" return self._trainer.pbar @property @@ -440,7 +441,7 @@ class LRScheduler(Callback): raise ValueError(f"Expect torch.optim.lr_scheduler for LRScheduler. Got {type(lr_scheduler)}.") def on_epoch_begin(self): - self.scheduler.step() + self.scheduler.step(self.epoch) class ControlC(Callback): @@ -526,7 +527,7 @@ class LRFinder(Callback): if torch.isnan(loss) or self.stop is True: self.stop = True return - loss_val = loss.detach().cpu().data + loss_val = loss.detach().mean().item() self.loss_history.append(loss_val) self.smooth_value.add_value(loss_val) if self.best_loss == 0. or self.smooth_value.smooth < self.best_loss: @@ -548,7 +549,7 @@ class LRFinder(Callback): self.find = False # reset model ModelLoader().load_pytorch(self.trainer.model, "tmp") - print("Model reset. \nFind best lr={}".format(self.best_lr)) + self.pbar.write("Model reset. \nFind best lr={}".format(self.best_lr)) class TensorboardCallback(Callback): diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index a6293167..9b56d834 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -558,7 +558,7 @@ class Trainer(object): start = time.time() with inner_tqdm(total=self.n_steps, postfix='loss:{0:<6.5f}', leave=False, dynamic_ncols=True) as pbar: - self.pbar = pbar if isinstance(pbar, tqdm) else None + self.pbar = pbar avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index 2657ebf4..5a2e99f3 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -43,7 +43,7 @@ class VarRnnCellWrapper(nn.Module): return torch.cat([hi, h0[:h0_size]], dim=0) return hi[:size] is_lstm = isinstance(hidden, tuple) - input, batch_sizes = input_x + input, batch_sizes = input_x.data, input_x.batch_sizes output = [] cell = self.cell if is_reversed: @@ -148,10 +148,11 @@ class VarRNNBase(nn.Module): seq_len = x.size(1) if self.batch_first else x.size(0) max_batch_size = x.size(0) if self.batch_first else x.size(1) seq_lens = torch.LongTensor([seq_len for _ in range(max_batch_size)]) - x, batch_sizes = pack_padded_sequence(x, seq_lens, batch_first=self.batch_first) + _tmp = pack_padded_sequence(x, seq_lens, batch_first=self.batch_first) + x, batch_sizes = _tmp.data, _tmp.batch_sizes else: max_batch_size = int(x.batch_sizes[0]) - x, batch_sizes = x + x, batch_sizes = x.data, x.batch_sizes if hx is None: hx = x.new_zeros(self.num_layers * self.num_directions, From 0a8f7c0e6947cac922b6494e4bc514bc0d66cffe Mon Sep 17 00:00:00 2001 From: yh_cc Date: Wed, 15 May 2019 21:42:37 +0800 Subject: [PATCH 130/173] =?UTF-8?q?1.=20=E4=BF=AE=E5=A4=8Dmetric=E4=B8=AD?= =?UTF-8?q?=E7=9A=84bug;=202.=E5=A2=9E=E5=8A=A0metric=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/metrics.py | 21 ++- test/core/test_metrics.py | 318 ++++++++++++++++++-------------------- 2 files changed, 162 insertions(+), 177 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 0354e7cc..7a96020b 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -342,8 +342,8 @@ class AccuracyMetric(MetricBase): def _bmes_tag_to_spans(tags, ignore_labels=None): """ - 给定一个tags的lis,比如['S', 'B-singer', 'M-singer', 'E-singer', 'S', 'S']。 - 返回[('', (0, 1)), ('singer', (1, 4)), ('', (4, 5)), ('', (5, 6))] (左闭右开区间) + 给定一个tags的lis,比如['S-song', 'B-singer', 'M-singer', 'E-singer', 'S-moive', 'S-actor']。 + 返回[('song', (0, 1)), ('singer', (1, 4)), ('moive', (4, 5)), ('actor', (5, 6))] (左闭右开区间) :param tags: List[str], :param ignore_labels: List[str], 在该list中的label将被忽略 @@ -527,8 +527,8 @@ class SpanFPreRecMetric(MetricBase): if pred.size() == target.size() and len(target.size()) == 2: pass elif len(pred.size()) == len(target.size()) + 1 and len(target.size()) == 2: - pred = pred.argmax(dim=-1) num_classes = pred.size(-1) + pred = pred.argmax(dim=-1) if (target >= num_classes).any(): raise ValueError("A gold label passed to SpanBasedF1Metric contains an " "id >= {}, the number of classes.".format(num_classes)) @@ -538,9 +538,11 @@ class SpanFPreRecMetric(MetricBase): f"{pred.size()[:-1]}, got {target.size()}.") batch_size = pred.size(0) + pred = pred.tolist() + target = target.tolist() for i in range(batch_size): - pred_tags = pred[i, :int(seq_len[i])].tolist() - gold_tags = target[i, :int(seq_len[i])].tolist() + pred_tags = pred[i][:int(seq_len[i])] + gold_tags = target[i][:int(seq_len[i])] pred_str_tags = [self.tag_vocab.to_word(tag) for tag in pred_tags] gold_str_tags = [self.tag_vocab.to_word(tag) for tag in gold_tags] @@ -592,15 +594,18 @@ class SpanFPreRecMetric(MetricBase): f, pre, rec = self._compute_f_pre_rec(sum(self._true_positives.values()), sum(self._false_negatives.values()), sum(self._false_positives.values())) - evaluate_result['f'] = round(f, 6) - evaluate_result['pre'] = round(pre, 6) - evaluate_result['rec'] = round(rec, 6) + evaluate_result['f'] = f + evaluate_result['pre'] = pre + evaluate_result['rec'] = rec if reset: self._true_positives = defaultdict(int) self._false_positives = defaultdict(int) self._false_negatives = defaultdict(int) + for key, value in evaluate_result.items(): + evaluate_result[key] = round(value, 6) + return evaluate_result def _compute_f_pre_rec(self, tp, fn, fp): diff --git a/test/core/test_metrics.py b/test/core/test_metrics.py index a5f7c0c3..f3b0178c 100644 --- a/test/core/test_metrics.py +++ b/test/core/test_metrics.py @@ -5,8 +5,39 @@ import torch from fastNLP import AccuracyMetric from fastNLP.core.metrics import _pred_topk, _accuracy_topk +from fastNLP.core.vocabulary import Vocabulary +from collections import Counter +from fastNLP.core.metrics import SpanFPreRecMetric +def _generate_tags(encoding_type, number_labels=4): + vocab = {} + for i in range(number_labels): + label = str(i) + for tag in encoding_type: + if tag == 'O': + if tag not in vocab: + vocab['O'] = len(vocab) + 1 + continue + vocab['{}-{}'.format(tag, label)] = len(vocab) + 1 # 其实表达的是这个的count + return vocab + + +def _convert_res_to_fastnlp_res(metric_result): + allen_result = {} + key_map = {'f1-measure-overall': "f", "recall-overall": "rec", "precision-overall": "pre"} + for key, value in metric_result.items(): + if key in key_map: + key = key_map[key] + else: + label = key.split('-')[-1] + if key.startswith('f1'): + key = 'f-{}'.format(label) + else: + key = '{}-{}'.format(key[:3], label) + allen_result[key] = round(value, 6) + return allen_result + class TestAccuracyMetric(unittest.TestCase): def test_AccuracyMetric1(self): # (1) only input, targets passed @@ -160,18 +191,7 @@ class SpanF1PreRecMetric(unittest.TestCase): ('6', (4, 5)), ('7', (6, 7))]) self.assertSetEqual(expect_bmes_res, set(_bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) - # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 - # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans - # from allennlp.data.dataset_readers.dataset_utils import bmes_tags_to_spans as allen_bmes_tags_to_spans - # for i in range(1000): - # strs = list(map(str, np.random.randint(100, size=1000))) - # bmes = list('bmes'.upper()) - # bmes_strs = [str_ + '-' + tag for tag, str_ in zip(strs, np.random.choice(bmes, size=len(strs)))] - # bio = list('bio'.upper()) - # bio_strs = [str_ + '-' + tag for tag, str_ in zip(strs, np.random.choice(bio, size=len(strs)))] - # self.assertSetEqual(set(allen_bmes_tags_to_spans(bmes_strs)),set(bmes_tag_to_spans(bmes_strs))) - # self.assertSetEqual(set(allen_bio_tags_to_spans(bio_strs)), set(bio_tag_to_spans(bio_strs))) - + def test_case2(self): # 测试不带label的 from fastNLP.core.metrics import _bmes_tag_to_spans @@ -185,170 +205,130 @@ class SpanF1PreRecMetric(unittest.TestCase): expect_bio_res.update([('', (7, 8)), ('', (6, 7)), ('', (4, 5)), ('', (0, 1)), ('', (1, 2))]) self.assertSetEqual(expect_bmes_res, set(_bmes_tag_to_spans(bmes_lst))) self.assertSetEqual(expect_bio_res, set(_bio_tag_to_spans(bio_lst))) - # 已与allennlp对应函数做过验证,但由于测试不能依赖allennlp,所以这里只是截取上面的例子做固定测试 - # from allennlp.data.dataset_readers.dataset_utils import bio_tags_to_spans as allen_bio_tags_to_spans - # from allennlp.data.dataset_readers.dataset_utils import bmes_tags_to_spans as allen_bmes_tags_to_spans - # for i in range(1000): - # bmes = list('bmes'.upper()) - # bmes_strs = np.random.choice(bmes, size=1000) - # bio = list('bio'.upper()) - # bio_strs = np.random.choice(bio, size=100) - # self.assertSetEqual(set(allen_bmes_tags_to_spans(bmes_strs)),set(bmes_tag_to_spans(bmes_strs))) - # self.assertSetEqual(set(allen_bio_tags_to_spans(bio_strs)), set(bio_tag_to_spans(bio_strs))) - - def tese_case3(self): - from fastNLP.core.vocabulary import Vocabulary - from collections import Counter - from fastNLP.core.metrics import SpanFPreRecMetric - # 与allennlp测试能否正确计算f metric - # - def generate_allen_tags(encoding_type, number_labels=4): - vocab = {} - for i in range(number_labels): - label = str(i) - for tag in encoding_type: - if tag == 'O': - if tag not in vocab: - vocab['O'] = len(vocab) + 1 - continue - vocab['{}-{}'.format(tag, label)] = len(vocab) + 1 # 其实表达的是这个的count - return vocab - + + def test_case3(self): number_labels = 4 # bio tag fastnlp_bio_vocab = Vocabulary(unknown=None, padding=None) - fastnlp_bio_vocab.word_count = Counter(generate_allen_tags('BIO', number_labels)) + fastnlp_bio_vocab.word_count = Counter(_generate_tags('BIO', number_labels)) fastnlp_bio_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bio_vocab, only_gross=False) - bio_sequence = torch.FloatTensor( - [[[-0.9543, -1.4357, -0.2365, 0.2438, 1.0312, -1.4302, 0.3011, - 0.0470, 0.0971], - [-0.6638, -0.7116, -1.9804, 0.2787, -0.2732, -0.9501, -1.4523, - 0.7987, -0.3970], - [0.2939, 0.8132, -0.0903, -2.8296, 0.2080, -0.9823, -0.1898, - 0.6880, 1.4348], - [-0.1886, 0.0067, -0.6862, -0.4635, 2.2776, 0.0710, -1.6793, - -1.6876, -0.8917], - [-0.7663, 0.6377, 0.8669, 0.1237, 1.7628, 0.0313, -1.0824, - 1.4217, 0.2622]], - - [[0.1529, 0.7474, -0.9037, 1.5287, 0.2771, 0.2223, 0.8136, - 1.3592, -0.8973], - [0.4515, -0.5235, 0.3265, -1.1947, 0.8308, 1.8754, -0.4887, - -0.4025, -0.3417], - [-0.7855, 0.1615, -0.1272, -1.9289, -0.5181, 1.9742, -0.9698, - 0.2861, -0.3966], - [-0.8291, -0.8823, -1.1496, 0.2164, 1.3390, -0.3964, -0.5275, - 0.0213, 1.4777], - [-1.1299, 0.0627, -0.1358, -1.5951, 0.4484, -0.6081, -1.9566, - 1.3024, 0.2001]]] - ) - bio_target = torch.LongTensor([[5., 0., 3., 3., 3.], - [5., 6., 8., 6., 0.]]) - fastnlp_bio_metric({'pred': bio_sequence, 'seq_lens': torch.LongTensor([5, 5])}, {'target': bio_target}) - expect_bio_res = {'pre-1': 0.24999999999999373, 'rec-1': 0.499999999999975, 'f-1': 0.33333333333327775, - 'pre-2': 0.0, 'rec-2': 0.0, 'f-2': 0.0, 'pre-3': 0.0, 'rec-3': 0.0, 'f-3': 0.0, 'pre-0': 0.0, - 'rec-0': 0.0, 'f-0': 0.0, 'pre': 0.12499999999999845, 'rec': 0.12499999999999845, - 'f': 0.12499999999994846} + bio_sequence = torch.FloatTensor([[[-0.4424, -0.4579, -0.7376, 1.8129, 0.1316, 1.6566, -1.2169, + -0.3782, 0.8240], + [-1.2348, -0.1876, -0.1462, -0.4834, -0.6692, -0.9735, 1.1563, + -0.3562, -1.4116], + [ 1.6550, -0.9555, 0.3782, -1.3160, -1.5835, -0.3443, -1.7858, + 2.0023, 0.7075], + [-0.3772, -0.5447, -1.5631, 1.1614, 1.4598, -1.2764, 0.5186, + 0.3832, -0.1540], + [-0.1011, 0.0600, 1.1090, -0.3545, 0.1284, 1.1484, -1.0120, + -1.3508, -0.9513], + [ 1.8948, 0.8627, -2.1359, 1.3740, -0.7499, 1.5019, 0.6919, + -0.0842, -0.4294]], + + [[-0.2802, 0.6941, -0.4788, -0.3845, 1.7752, 1.2950, -1.9490, + -1.4138, -0.8853], + [-1.3752, -0.5457, -0.5305, 0.4018, 0.2934, 0.7931, 2.3845, + -1.0726, 0.0364], + [ 0.3621, 0.2609, 0.1269, -0.5950, 0.7212, 0.5959, 1.6264, + -0.8836, -0.9320], + [ 0.2003, -1.0758, -1.1560, -0.6472, -1.7549, 0.1264, 0.6044, + -1.6857, 1.1571], + [ 1.4277, -0.4915, 0.4496, 2.2027, 0.0730, -3.1792, -0.5125, + -0.5837, 1.0184], + [ 1.9495, 1.7145, -0.2143, -0.1230, -0.2205, 0.8250, 0.4943, + -0.9025, 0.0864]]]) + bio_target = torch.LongTensor([[3, 6, 0, 8, 2, 4], + [4, 1, 7, 0, 4, 7]]) + fastnlp_bio_metric({'pred': bio_sequence, 'seq_len': torch.LongTensor([6, 6])}, {'target': bio_target}) + expect_bio_res = {'pre-1': 0.333333, 'rec-1': 0.333333, 'f-1': 0.333333, 'pre-2': 0.5, 'rec-2': 0.5, + 'f-2': 0.5, 'pre-0': 0.0, 'rec-0': 0.0, 'f-0': 0.0, 'pre-3': 0.0, 'rec-3': 0.0, + 'f-3': 0.0, 'pre': 0.222222, 'rec': 0.181818, 'f': 0.2} + self.assertDictEqual(expect_bio_res, fastnlp_bio_metric.get_metric()) - + + def test_case4(self): # bmes tag - bmes_sequence = torch.FloatTensor( - [[[0.6536, -0.7179, 0.6579, 1.2503, 0.4176, 0.6696, 0.2352, - -0.4085, 0.4084, -0.4185, 1.4172, -0.9162, -0.2679, 0.3332, - -0.3505, -0.6002], - [0.3238, -1.2378, -1.3304, -0.4903, 1.4518, -0.1868, -0.7641, - 1.6199, -0.8877, 0.1449, 0.8995, -0.5810, 0.1041, 0.1002, - 0.4439, 0.2514], - [-0.8362, 2.9526, 0.8008, 0.1193, 1.0488, 0.6670, 1.1696, - -1.1006, -0.8540, -0.1600, -0.9519, -0.2749, -0.4948, -1.4753, - 0.5802, -0.0516], - [-0.8383, -1.7292, -1.4079, -1.5023, 0.5383, 0.6653, 0.3121, - 4.1249, -0.4173, -0.2043, 1.7755, 1.1110, -1.7069, -0.0390, - -0.9242, -0.0333], - [0.9088, -0.4955, -0.5076, 0.3732, 0.0283, -0.0263, -1.0393, - 0.7734, 1.0968, 0.4132, -1.3647, -0.5762, 0.6678, 0.8809, - -0.3779, -0.3195]], - - [[-0.4638, -0.5939, -0.1052, -0.5573, 0.4600, -1.3484, 0.1753, - 0.0685, 0.3663, -0.6789, 0.0097, 1.0327, -0.0212, -0.9957, - -0.1103, 0.4417], - [-0.2903, 0.9205, -1.5758, -1.0421, 0.2921, -0.2142, -0.3049, - -0.0879, -0.4412, -1.3195, -0.0657, -0.2986, 0.7214, 0.0631, - -0.6386, 0.2797], - [0.6440, -0.3748, 1.2912, -0.0170, 0.7447, 1.4075, -0.4947, - 0.4123, -0.8447, -0.5502, 0.3520, -0.2832, 0.5019, -0.1522, - 1.1237, -1.5385], - [0.2839, -0.7649, 0.9067, -0.1163, -1.3789, 0.2571, -1.3977, - -0.3680, -0.8902, -0.6983, -1.1583, 1.2779, 0.2197, 0.1376, - -0.0591, -0.2461], - [-0.2977, -1.8564, -0.5347, 1.0011, -1.1260, 0.4252, -2.0097, - 2.6973, -0.8308, -1.4939, 0.9865, -0.3935, 0.2743, 0.1142, - -0.7344, -1.2046]]] - ) - bmes_target = torch.LongTensor([[9., 6., 1., 9., 15.], - [6., 15., 6., 15., 5.]]) - - fastnlp_bmes_vocab = Vocabulary(unknown=None, padding=None) - fastnlp_bmes_vocab.word_count = Counter(generate_allen_tags('BMES', number_labels)) - fastnlp_bmes_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bmes_vocab, only_gross=False, encoding_type='bmes') - fastnlp_bmes_metric({'pred': bmes_sequence, 'seq_lens': torch.LongTensor([20, 20])}, {'target': bmes_target}) - - expect_bmes_res = {'f-3': 0.6666666666665778, 'pre-3': 0.499999999999975, 'rec-3': 0.9999999999999001, - 'f-0': 0.0, 'pre-0': 0.0, 'rec-0': 0.0, 'f-1': 0.33333333333327775, - 'pre-1': 0.24999999999999373, 'rec-1': 0.499999999999975, 'f-2': 0.7499999999999314, - 'pre-2': 0.7499999999999812, 'rec-2': 0.7499999999999812, 'f': 0.49999999999994504, - 'pre': 0.499999999999995, 'rec': 0.499999999999995} - - self.assertDictEqual(fastnlp_bmes_metric.get_metric(), expect_bmes_res) - - # 已经和allennlp做过验证,但由于不能依赖allennlp,所以注释了以下代码 - # from allennlp.data.vocabulary import Vocabulary as allen_Vocabulary - # from allennlp.training.metrics import SpanBasedF1Measure - # allen_bio_vocab = allen_Vocabulary({"tags": generate_allen_tags('BIO', number_labels)}, - # non_padded_namespaces=['tags']) - # allen_bio_metric = SpanBasedF1Measure(allen_bio_vocab, 'tags') - # bio_sequence = torch.randn(size=(2, 20, 2 * number_labels + 1)) - # bio_target = torch.randint(2 * number_labels + 1, size=(2, 20)) - # allen_bio_metric(bio_sequence, bio_target, torch.ones(2, 20)) - # fastnlp_bio_vocab = Vocabulary(unknown=None, padding=None) - # fastnlp_bio_vocab.word_count = Counter(generate_allen_tags('BIO', number_labels)) - # fastnlp_bio_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bio_vocab, only_gross=False) - # - # def convert_allen_res_to_fastnlp_res(metric_result): - # allen_result = {} - # key_map = {'f1-measure-overall': "f", "recall-overall": "rec", "precision-overall": "pre"} - # for key, value in metric_result.items(): - # if key in key_map: - # key = key_map[key] - # else: - # label = key.split('-')[-1] - # if key.startswith('f1'): - # key = 'f-{}'.format(label) - # else: - # key = '{}-{}'.format(key[:3], label) - # allen_result[key] = value - # return allen_result - # - # # print(convert_allen_res_to_fastnlp_res(allen_bio_metric.get_metric())) - # # print(fastnlp_bio_metric.get_metric()) - # self.assertDictEqual(convert_allen_res_to_fastnlp_res(allen_bio_metric.get_metric()), - # fastnlp_bio_metric.get_metric()) - # - # allen_bmes_vocab = allen_Vocabulary({"tags": generate_allen_tags('BMES', number_labels)}) - # allen_bmes_metric = SpanBasedF1Measure(allen_bmes_vocab, 'tags', label_encoding='BMES') - # fastnlp_bmes_vocab = Vocabulary(unknown=None, padding=None) - # fastnlp_bmes_vocab.word_count = Counter(generate_allen_tags('BMES', number_labels)) - # fastnlp_bmes_metric = SpanFPreRecMetric(tag_vocab=fastnlp_bmes_vocab, only_gross=False, encoding_type='bmes') - # bmes_sequence = torch.randn(size=(2, 20, 4 * number_labels)) - # bmes_target = torch.randint(4 * number_labels, size=(2, 20)) - # allen_bmes_metric(bmes_sequence, bmes_target, torch.ones(2, 20)) - # fastnlp_bmes_metric({'pred': bmes_sequence, 'seq_lens': torch.LongTensor([20, 20])}, {'target': bmes_target}) - # - # # print(convert_allen_res_to_fastnlp_res(allen_bmes_metric.get_metric())) - # # print(fastnlp_bmes_metric.get_metric()) - # self.assertDictEqual(convert_allen_res_to_fastnlp_res(allen_bmes_metric.get_metric()), - # fastnlp_bmes_metric.get_metric()) + def _generate_samples(): + target = [] + seq_len = [] + vocab = Vocabulary(unknown=None, padding=None) + for i in range(3): + target_i = [] + seq_len_i = 0 + for j in range(1, 10): + word_len = np.random.randint(1, 5) + seq_len_i += word_len + if word_len==1: + target_i.append('S') + else: + target_i.append('B') + target_i.extend(['M']*(word_len-2)) + target_i.append('E') + vocab.add_word_lst(target_i) + target.append(target_i) + seq_len.append(seq_len_i) + target_ = np.zeros((3, max(seq_len))) + for i in range(3): + target_i = [vocab.to_index(t) for t in target[i]] + target_[i, :seq_len[i]] = target_i + return target_, target, seq_len, vocab + def get_eval(raw_target, pred, vocab, seq_len): + pred = pred.argmax(dim=-1).tolist() + tp = 0 + gold = 0 + seg = 0 + pred_target = [] + for i in range(len(seq_len)): + tags = [vocab.to_word(p) for p in pred[i][:seq_len[i]]] + spans = [] + prev_bmes_tag = None + for idx, tag in enumerate(tags): + if tag in ('B', 'S'): + spans.append([idx, idx]) + elif tag in ('M', 'E') and prev_bmes_tag in ('B', 'M'): + spans[-1][1] = idx + else: + spans.append([idx, idx]) + prev_bmes_tag = tag + tmp = [] + for span in spans: + if span[1]-span[0]>0: + tmp.extend(['B'] + ['M']*(span[1]-span[0]-1) + ['E']) + else: + tmp.append('S') + pred_target.append(tmp) + for i in range(len(seq_len)): + raw_pred = pred_target[i] + start = 0 + for j in range(seq_len[i]): + if raw_target[i][j] in ('E', 'S'): + flag = True + for k in range(start, j+1): + if raw_target[i][k]!=raw_pred[k]: + flag = False + break + if flag: + tp += 1 + start = j + 1 + gold += 1 + if raw_pred[j] in ('E', 'S'): + seg += 1 + + pre = round(tp/seg, 6) + rec = round(tp/gold, 6) + return {'f': round(2*pre*rec/(pre+rec), 6), 'pre': pre, 'rec':rec} + + target, raw_target, seq_len, vocab = _generate_samples() + pred = torch.randn(3, max(seq_len), 4) + expected_metric = get_eval(raw_target, pred, vocab, seq_len) + metric = SpanFPreRecMetric(vocab, encoding_type='bmes') + metric({'pred': pred, 'seq_len':torch.LongTensor(seq_len)}, {'target': torch.from_numpy(target)}) + # print(metric.get_metric(reset=False)) + # print(expected_metric) + metric_value = metric.get_metric() + for key, value in expected_metric.items(): + self.assertAlmostEqual(value, metric_value[key], places=5) class TestUsefulFunctions(unittest.TestCase): From 87e21a26a31b27a60c832fa8103ef3aaf619b059 Mon Sep 17 00:00:00 2001 From: yunfan Date: Thu, 16 May 2019 14:17:53 +0800 Subject: [PATCH 131/173] add model summary --- fastNLP/modules/utils.py | 47 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 047ebb78..23768b03 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -1,3 +1,5 @@ +from functools import reduce +from collections import OrderedDict import numpy as np import torch import torch.nn as nn @@ -71,7 +73,7 @@ def initial_parameter(net, initial_method=None): def get_embeddings(init_embed): """ 得到词嵌入 - + .. todo:: 补上文档 @@ -81,7 +83,8 @@ def get_embeddings(init_embed): :return nn.Embedding embeddings: """ if isinstance(init_embed, tuple): - res = nn.Embedding(num_embeddings=init_embed[0], embedding_dim=init_embed[1]) + res = nn.Embedding( + num_embeddings=init_embed[0], embedding_dim=init_embed[1]) elif isinstance(init_embed, nn.Embedding): res = init_embed elif isinstance(init_embed, torch.Tensor): @@ -90,5 +93,43 @@ def get_embeddings(init_embed): init_embed = torch.tensor(init_embed, dtype=torch.float32) res = nn.Embedding.from_pretrained(init_embed, freeze=False) else: - raise TypeError('invalid init_embed type: {}'.format((type(init_embed)))) + raise TypeError( + 'invalid init_embed type: {}'.format((type(init_embed)))) return res + + +def summary(model: nn.Module): + """ + 得到模型的总参数量 + + :params model: Pytorch 模型 + :return tuple: 包含总参数量,可训练参数量,不可训练参数量 + """ + train = [] + nontrain = [] + + def layer_summary(module: nn.Module): + def count_size(sizes): + return reduce(lambda x, y: x*y, sizes) + + for p in module.parameters(recurse=False): + if p.requires_grad: + train.append(count_size(p.shape)) + else: + nontrain.append(count_size(p.shape)) + for subm in module.children(): + layer_summary(subm) + + layer_summary(model) + total_train = sum(train) + total_nontrain = sum(nontrain) + total = total_train + total_nontrain + strings = [] + strings.append('Total params: {:,}'.format(total)) + strings.append('Trainable params: {:,}'.format(total_train)) + strings.append('Non-trainable params: {:,}'.format(total_nontrain)) + max_len = len(max(strings, key=len)) + bar = '-'*(max_len + 3) + strings = [bar] + strings + [bar] + print('\n'.join(strings)) + return total, total_train, total_nontrain From 6aaef9175c7708307d55858f47e296c975f283f5 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 16 May 2019 16:08:04 +0800 Subject: [PATCH 132/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=88=AB=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/models/biaffine_parser.py | 9 +++- fastNLP/models/cnn_text_classification.py | 2 + fastNLP/models/sequence_labeling.py | 4 ++ fastNLP/models/snli.py | 5 ++- fastNLP/models/star_transformer.py | 17 ++++++-- fastNLP/modules/aggregator/__init__.py | 2 +- fastNLP/modules/aggregator/pooling.py | 35 ++++++++++----- fastNLP/modules/decoder/CRF.py | 7 ++- fastNLP/modules/decoder/MLP.py | 5 ++- fastNLP/modules/decoder/utils.py | 5 ++- fastNLP/modules/dropout.py | 7 ++- fastNLP/modules/encoder/char_encoder.py | 2 + fastNLP/modules/encoder/conv_maxpool.py | 10 +++-- fastNLP/modules/encoder/embedding.py | 5 ++- fastNLP/modules/encoder/lstm.py | 5 ++- fastNLP/modules/encoder/star_transformer.py | 3 ++ fastNLP/modules/encoder/transformer.py | 6 ++- fastNLP/modules/encoder/variational_rnn.py | 47 ++++++--------------- fastNLP/modules/utils.py | 5 +-- 19 files changed, 112 insertions(+), 69 deletions(-) diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 3a5607f5..100bfb72 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -226,7 +226,10 @@ class LabelBilinear(nn.Module): return output class BiaffineParser(GraphParser): - """Biaffine Dependency Parser 实现. + """ + 别名::class:`fastNLP.models.BiaffineParser` :class:`fastNLP.models.baffine_parser.BiaffineParser` + + Biaffine Dependency Parser 实现. 论文参考 ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) `_ . @@ -456,6 +459,8 @@ class BiaffineParser(GraphParser): class ParserLoss(LossFunc): """ + 别名::class:`fastNLP.models.ParserLoss` :class:`fastNLP.models.baffine_parser.ParserLoss` + 计算parser的loss :param pred1: [batch_size, seq_len, seq_len] 边预测logits @@ -478,6 +483,8 @@ class ParserLoss(LossFunc): class ParserMetric(MetricBase): """ + 别名::class:`fastNLP.models.ParserMetric` :class:`fastNLP.models.baffine_parser.ParserMetric` + 评估parser的性能 :param pred1: 边预测logits diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index 5df4e62a..eb829601 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -10,6 +10,8 @@ from ..modules import encoder class CNNText(torch.nn.Module): """ + 别名::class:`fastNLP.models.CNNText` :class:`fastNLP.modules.aggregator.cnn_text_classification.CNNText` + 使用CNN进行文本分类的模型 'Yoon Kim. 2014. Convolution Neural Networks for Sentence Classification.' diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 98badd56..6cfbf28d 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -10,6 +10,8 @@ from torch import nn class SeqLabeling(BaseModel): """ + 别名::class:`fastNLP.models.SeqLabeling` :class:`fastNLP.modules.aggregator.sequence_labeling.SeqLabeling` + 一个基础的Sequence labeling的模型。 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 @@ -100,6 +102,8 @@ class SeqLabeling(BaseModel): class AdvSeqLabel(nn.Module): """ + 别名::class:`fastNLP.models.AdvSeqLabel` :class:`fastNLP.modules.aggregator.sequence_labeling.AdvSeqLabel` + 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index ac0a2e47..34b54302 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -14,8 +14,9 @@ my_inf = 10e12 class ESIM(BaseModel): """ + 别名::class:`fastNLP.models.ESIM` :class:`fastNLP.models.snli.ESIM` + ESIM模型的一个PyTorch实现。 - ESIM模型的论文: Enhanced LSTM for Natural Language Inference (arXiv: 1609.06038) :param int vocab_size: 词表大小 @@ -49,7 +50,7 @@ class ESIM(BaseModel): ) self.bi_attention = Aggregator.BiAttention() - self.mean_pooling = Aggregator.MeanPoolWithMask() + self.mean_pooling = Aggregator.AvgPoolWithMask() self.max_pooling = Aggregator.MaxPoolWithMask() self.inference_layer = nn.Linear(self.hidden_size * 4, self.hidden_size) diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index f7b9028e..cdd1f321 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -11,6 +11,8 @@ from torch import nn class StarTransEnc(nn.Module): """ + 别名::class:`fastNLP.models.StarTransEnc` :class:`fastNLP.models.start_transformer.StarTransEnc` + 带word embedding的Star-Transformer Encoder :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 @@ -93,7 +95,10 @@ class _NLICls(nn.Module): return h class STSeqLabel(nn.Module): - """用于序列标注的Star-Transformer模型 + """ + 别名::class:`fastNLP.models.STSeqLabel` :class:`fastNLP.models.start_transformer.STSeqLabel` + + 用于序列标注的Star-Transformer模型 :param vocab_size: 词嵌入的词典大小 :param emb_dim: 每个词嵌入的特征维度 @@ -153,7 +158,10 @@ class STSeqLabel(nn.Module): class STSeqCls(nn.Module): - """用于分类任务的Star-Transformer + """ + 别名::class:`fastNLP.models.STSeqCls` :class:`fastNLP.models.start_transformer.STSeqCls` + + 用于分类任务的Star-Transformer :param vocab_size: 词嵌入的词典大小 :param emb_dim: 每个词嵌入的特征维度 @@ -214,7 +222,10 @@ class STSeqCls(nn.Module): class STNLICls(nn.Module): - """用于自然语言推断(NLI)的Star-Transformer + """ + 别名::class:`fastNLP.models.STNLICls` :class:`fastNLP.models.start_transformer.STNLICls` + + 用于自然语言推断(NLI)的Star-Transformer :param vocab_size: 词嵌入的词典大小 :param emb_dim: 每个词嵌入的特征维度 diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 4a76cf5b..725ccd4b 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,7 +1,7 @@ from .pooling import MaxPool from .pooling import MaxPoolWithMask from .pooling import AvgPool -from .pooling import MeanPoolWithMask +from .pooling import AvgPoolWithMask from .attention import MultiHeadAttention, BiAttention __all__ = [ diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index 47050355..5d83ef68 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -5,6 +5,8 @@ import torch.nn as nn class MaxPool(nn.Module): """ + 别名::class:`fastNLP.modules.aggregator.MaxPool` :class:`fastNLP.modules.aggregator.pooling.MaxPool` + Max-pooling模块。 :param stride: 窗口移动大小,默认为kernel_size @@ -12,11 +14,9 @@ class MaxPool(nn.Module): :param dilation: 控制窗口内元素移动距离的大小 :param dimension: MaxPool的维度,支持1,2,3维。 :param kernel_size: max pooling的窗口大小,默认为tensor最后k维,其中k为dimension - :param return_indices: :param ceil_mode: """ - def __init__(self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, - return_indices=False, ceil_mode=False): + def __init__(self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, ceil_mode=False): super(MaxPool, self).__init__() assert (1 <= dimension) and (dimension <= 3) @@ -25,7 +25,6 @@ class MaxPool(nn.Module): self.padding = padding self.dilation = dilation self.kernel_size = kernel_size - self.return_indices = return_indices self.ceil_mode = ceil_mode def forward(self, x): @@ -33,27 +32,31 @@ class MaxPool(nn.Module): pooling = nn.MaxPool1d( stride=self.stride, padding=self.padding, dilation=self.dilation, kernel_size=self.kernel_size if self.kernel_size is not None else x.size(-1), - return_indices=self.return_indices, ceil_mode=self.ceil_mode + return_indices=False, ceil_mode=self.ceil_mode ) x = torch.transpose(x, 1, 2) # [N,L,C] -> [N,C,L] elif self.dimension == 2: pooling = nn.MaxPool2d( stride=self.stride, padding=self.padding, dilation=self.dilation, kernel_size=self.kernel_size if self.kernel_size is not None else (x.size(-2), x.size(-1)), - return_indices=self.return_indices, ceil_mode=self.ceil_mode + return_indices=False, ceil_mode=self.ceil_mode ) else: pooling = nn.MaxPool2d( stride=self.stride, padding=self.padding, dilation=self.dilation, kernel_size=self.kernel_size if self.kernel_size is not None else (x.size(-3), x.size(-2), x.size(-1)), - return_indices=self.return_indices, ceil_mode=self.ceil_mode + return_indices=False, ceil_mode=self.ceil_mode ) x = pooling(x) return x.squeeze(dim=-1) # [N,C,1] -> [N,C] class MaxPoolWithMask(nn.Module): - """带mask矩阵的1维max pooling""" + """ + 别名::class:`fastNLP.modules.aggregator.MaxPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.MaxPoolWithMask` + + 带mask矩阵的max pooling。在做max-pooling的时候不会考虑mask值为0的位置。 + """ def __init__(self): super(MaxPoolWithMask, self).__init__() @@ -89,7 +92,11 @@ class KMaxPool(nn.Module): class AvgPool(nn.Module): - """1-d average pooling module.""" + """ + 别名::class:`fastNLP.modules.aggregator.AvgPool` :class:`fastNLP.modules.aggregator.pooling.AvgPool` + + 给定形如[batch_size, max_len, hidden_size]的输入,在最后一维进行avg pooling. 输出为[batch_size, hidden_size] + """ def __init__(self, stride=None, padding=0): super(AvgPool, self).__init__() @@ -111,10 +118,16 @@ class AvgPool(nn.Module): return x.squeeze(dim=-1) -class MeanPoolWithMask(nn.Module): +class AvgPoolWithMask(nn.Module): + """ + 别名::class:`fastNLP.modules.aggregator.AvgPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.AvgPoolWithMask` + + 给定形如[batch_size, max_len, hidden_size]的输入,在最后一维进行avg pooling. 输出为[batch_size, hidden_size], pooling + 的时候只会考虑mask为1的位置 + """ def __init__(self): - super(MeanPoolWithMask, self).__init__() + super(AvgPoolWithMask, self).__init__() self.inf = 10e12 def forward(self, tensor, mask, dim=1): diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 2c9080b2..275f955c 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -6,6 +6,8 @@ from ..utils import initial_parameter def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): """ + 别名::class:`fastNLP.modules.decoder.allowed_transitions` :class:`fastNLP.modules.decoder.CRF.allowed_transitions` + 给定一个id到label的映射表,返回所有可以跳转的(from_tag_id, to_tag_id)列表。 :param dict id2target: key是label的indices,value是str类型的tag或tag-label。value可以是只有tag的, 比如"B", "M"; 也可以是 @@ -133,7 +135,10 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label class ConditionalRandomField(nn.Module): - """条件随机场。 + """ + 别名::class:`fastNLP.modules.decoder.ConditionalRandomField` :class:`fastNLP.modules.decoder.CRF.ConditionalRandomField` + + 条件随机场。 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 :param int num_tags: 标签的数量 diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/MLP.py index 35484932..71d899b0 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/MLP.py @@ -5,7 +5,10 @@ from ..utils import initial_parameter class MLP(nn.Module): - """Multilayer Perceptrons as a decoder + """ + 别名::class:`fastNLP.modules.MLP` :class:`fastNLP.modules.decoder.MLP.MLP` + + 多层感知器 :param list size_layer: 一个int的列表,用来定义MLP的层数,列表中的数字为每一层是hidden数目。MLP的层数为 len(size_layer) - 1 :param str or list activation: diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 95b25767..1e7a4258 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -3,7 +3,10 @@ import torch def viterbi_decode(logits, transitions, mask=None, unpad=False): - """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 + """ + 别名::class:`fastNLP.modules.decoder.viterbi_decode` :class:`fastNLP.modules.decoder.utils.viterbi_decode + + 给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 :param torch.FloatTensor logits: batch_size x max_len x num_tags,特征矩阵。 :param torch.FloatTensor transitions: n_tags x n_tags。[i, j]位置的值认为是从tag i到tag j的转换。 diff --git a/fastNLP/modules/dropout.py b/fastNLP/modules/dropout.py index 97745c00..34b426fd 100644 --- a/fastNLP/modules/dropout.py +++ b/fastNLP/modules/dropout.py @@ -2,8 +2,11 @@ import torch __all__ = [] class TimestepDropout(torch.nn.Dropout): - """This module accepts a ``[batch_size, num_timesteps, embedding_dim)]`` and use a single - dropout mask of shape ``(batch_size, embedding_dim)`` to apply on every time step. + """ + 别名::class:`fastNLP.modules.TimestepDropout` + + 接受的参数shape为``[batch_size, num_timesteps, embedding_dim)]`` 使用同一个mask(shape为``(batch_size, embedding_dim)``) + 在每个timestamp上做dropout。 """ def forward(self, x): diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index c3886c86..be04a6be 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -7,6 +7,8 @@ from ..utils import initial_parameter # from torch.nn.init import xavier_uniform class ConvolutionCharEncoder(nn.Module): """ + 别名::class:`fastNLP.modules.encoder.ConvolutionCharEncoder` :class:`fastNLP.modules.encoder.char_encoder.ConvolutionCharEncoder` + char级别的卷积编码器. :param int char_emb_size: char级别embedding的维度. Default: 50 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index d01eddea..6b4c39ed 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -9,10 +9,12 @@ from ..utils import initial_parameter class ConvMaxpool(nn.Module): - """集合了Convolution和Max-Pooling于一体的层。 - 给定一个batch_size x max_len x input_size的输入,返回batch_size x sum(output_channels) 大小的matrix。在内部,是先使用 - CNN给输入做卷积,然后经过activation激活层,在通过在长度(max_len)这一维进行max_pooling。最后得到每个sample的一个vector - 表示。 + """ + 别名::class:`fastNLP.modules.encoder.ConvMaxpool` :class:`fastNLP.modules.encoder.conv_maxpool.ConvMaxpool` + + 集合了Convolution和Max-Pooling于一体的层。给定一个batch_size x max_len x input_size的输入,返回batch_size x + sum(output_channels) 大小的matrix。在内部,是先使用CNN给输入做卷积,然后经过activation激活层,在通过在长度(max_len) + 这一维进行max_pooling。最后得到每个sample的一个向量表示。 :param int in_channels: 输入channel的大小,一般是embedding的维度; 或encoder的output维度 :param int,tuple(int) out_channels: 输出channel的数量。如果为list,则需要与kernel_sizes的数量保持一致 diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index 8cc53b0b..c402f318 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -2,7 +2,10 @@ import torch.nn as nn from ..utils import get_embeddings class Embedding(nn.Embedding): - """Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" + """ + 别名::class:`fastNLP.modules.Embedding` :class:`fastNLP.modules.encoder.embedding.Embedding` + + Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" def __init__(self, init_embed, padding_idx=None, dropout=0.0, sparse=False, max_norm=None, norm_type=2, scale_grad_by_freq=False): diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index b11a84eb..ada34c26 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -9,7 +9,10 @@ from ..utils import initial_parameter class LSTM(nn.Module): - """LSTM 模块, 轻量封装的Pytorch LSTM + """ + 别名::class:`fastNLP.modules.encoder.LSTM` :class:`fastNLP.modules.encoder.lstm.LSTM` + + LSTM 模块, 轻量封装的Pytorch LSTM :param input_size: 输入 `x` 的特征维度 :param hidden_size: 隐状态 `h` 的特征维度 diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 42662804..e721c16f 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -8,6 +8,9 @@ import numpy as NP class StarTransformer(nn.Module): """ + 别名::class:`fastNLP.modules.encoder.StarTransformer` :class:`fastNLP.modules.encoder.star_transformer.StarTransformer` + + Star-Transformer 的encoder部分。 输入3d的文本输入, 返回相同长度的文本编码 paper: https://arxiv.org/abs/1902.09113 diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index 60216c2b..9647f86e 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -5,7 +5,11 @@ from ..dropout import TimestepDropout class TransformerEncoder(nn.Module): - """transformer的encoder模块,不包含embedding层 + """ + 别名::class:`fastNLP.modules.encoder.TransformerEncoder` :class:`fastNLP.modules.encoder.transformer.TransformerEncoder` + + + transformer的encoder模块,不包含embedding层 :param int num_layers: transformer的层数 :param int model_size: 输入维度的大小。同时也是输出维度的大小。 diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index 5a2e99f3..b3858020 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -197,7 +197,10 @@ class VarRNNBase(nn.Module): return output, hidden class VarLSTM(VarRNNBase): - """Variational Dropout LSTM. + """ + 别名::class:`fastNLP.modules.encoder.VarLSTM` :class:`fastNLP.modules.encoder.variational_rnn.VarLSTM` + + Variational Dropout LSTM. :param input_size: 输入 `x` 的特征维度 :param hidden_size: 隐状态 `h` 的特征维度 @@ -218,7 +221,10 @@ class VarLSTM(VarRNNBase): class VarRNN(VarRNNBase): - """Variational Dropout RNN. + """ + 别名::class:`fastNLP.modules.encoder.VarRNN` :class:`fastNLP.modules.encoder.variational_rnn.VarRNN` + + Variational Dropout RNN. :param input_size: 输入 `x` 的特征维度 :param hidden_size: 隐状态 `h` 的特征维度 @@ -238,7 +244,10 @@ class VarRNN(VarRNNBase): return super(VarRNN, self).forward(x, hx) class VarGRU(VarRNNBase): - """Variational Dropout GRU. + """ + 别名::class:`fastNLP.modules.encoder.VarGRU` :class:`fastNLP.modules.encoder.variational_rnn.VarGRU` + + Variational Dropout GRU. :param input_size: 输入 `x` 的特征维度 :param hidden_size: 隐状态 `h` 的特征维度 @@ -257,35 +266,3 @@ class VarGRU(VarRNNBase): def forward(self, x, hx=None): return super(VarGRU, self).forward(x, hx) -# if __name__ == '__main__': -# x = torch.Tensor([[1,2,3], [4,5,0], [6,0,0]])[:,:,None] * 0.1 -# mask = (x != 0).float().view(3, -1) -# seq_lens = torch.LongTensor([3,2,1]) -# y = torch.Tensor([[0,1,1], [1,1,0], [0,0,0]]) -# # rev = _reverse_packed_sequence(pack) -# # # print(rev) -# lstm = VarLSTM(input_size=1, num_layers=2, hidden_size=2, -# batch_first=True, bidirectional=True, -# input_dropout=0.0, hidden_dropout=0.0,) -# # lstm = nn.LSTM(input_size=1, num_layers=2, hidden_size=2, -# # batch_first=True, bidirectional=True,) -# loss = nn.BCELoss() -# m = nn.Sigmoid() -# optim = torch.optim.SGD(lstm.parameters(), lr=1e-3) -# for i in range(2000): -# optim.zero_grad() -# pack = pack_padded_sequence(x, seq_lens, batch_first=True) -# out, hidden = lstm(pack) -# out, lens = pad_packed_sequence(out, batch_first=True) -# # print(lens) -# # print(out) -# # print(hidden[0]) -# # print(hidden[0].size()) -# # print(hidden[1]) -# out = out.sum(-1) -# out = m(out) * mask -# l = loss(out, y) -# l.backward() -# optim.step() -# if i % 50 == 0: -# print(out) diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 047ebb78..3dfe1969 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -70,10 +70,7 @@ def initial_parameter(net, initial_method=None): def get_embeddings(init_embed): """ - 得到词嵌入 - - .. todo:: - 补上文档 + 根据输入的init_embed生成nn.Embedding对象。 :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, From 885c640c16744f5584d3b0d16f5fe8dc4b79d398 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 16 May 2019 16:25:55 +0800 Subject: [PATCH 133/173] =?UTF-8?q?=E5=88=A0=E9=99=A4encoder=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=B5=8C=E5=A5=97=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/models/cnn_text_classification.py | 2 +- fastNLP/models/sequence_labeling.py | 4 ++-- fastNLP/modules/aggregator/pooling.py | 8 ++++---- fastNLP/modules/decoder/CRF.py | 4 ++-- fastNLP/modules/decoder/utils.py | 2 +- fastNLP/modules/encoder/char_encoder.py | 2 +- fastNLP/modules/encoder/conv_maxpool.py | 2 +- fastNLP/modules/encoder/lstm.py | 2 +- fastNLP/modules/encoder/star_transformer.py | 2 +- fastNLP/modules/encoder/transformer.py | 2 +- fastNLP/modules/encoder/variational_rnn.py | 6 +++--- 11 files changed, 18 insertions(+), 18 deletions(-) diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index eb829601..01b03b9f 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -10,7 +10,7 @@ from ..modules import encoder class CNNText(torch.nn.Module): """ - 别名::class:`fastNLP.models.CNNText` :class:`fastNLP.modules.aggregator.cnn_text_classification.CNNText` + 别名::class:`fastNLP.models.CNNText` :class:`fastNLP.models.cnn_text_classification.CNNText` 使用CNN进行文本分类的模型 'Yoon Kim. 2014. Convolution Neural Networks for Sentence Classification.' diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 6cfbf28d..39f4c3fe 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -10,7 +10,7 @@ from torch import nn class SeqLabeling(BaseModel): """ - 别名::class:`fastNLP.models.SeqLabeling` :class:`fastNLP.modules.aggregator.sequence_labeling.SeqLabeling` + 别名::class:`fastNLP.models.SeqLabeling` :class:`fastNLP.models.sequence_labeling.SeqLabeling` 一个基础的Sequence labeling的模型。 用于做sequence labeling的基础类。结构包含一层Embedding,一层LSTM(单向,一层),一层FC,以及一层CRF。 @@ -102,7 +102,7 @@ class SeqLabeling(BaseModel): class AdvSeqLabel(nn.Module): """ - 别名::class:`fastNLP.models.AdvSeqLabel` :class:`fastNLP.modules.aggregator.sequence_labeling.AdvSeqLabel` + 别名::class:`fastNLP.models.AdvSeqLabel` :class:`fastNLP.models.sequence_labeling.AdvSeqLabel` 更复杂的Sequence Labelling模型。结构为Embedding, LayerNorm, 双向LSTM(两层),FC,LayerNorm,DropOut,FC,CRF。 diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index 5d83ef68..be454d7b 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -5,7 +5,7 @@ import torch.nn as nn class MaxPool(nn.Module): """ - 别名::class:`fastNLP.modules.aggregator.MaxPool` :class:`fastNLP.modules.aggregator.pooling.MaxPool` + 别名::class:`fastNLP.modules.MaxPool` :class:`fastNLP.modules.aggregator.pooling.MaxPool` Max-pooling模块。 @@ -53,7 +53,7 @@ class MaxPool(nn.Module): class MaxPoolWithMask(nn.Module): """ - 别名::class:`fastNLP.modules.aggregator.MaxPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.MaxPoolWithMask` + 别名::class:`fastNLP.modules.MaxPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.MaxPoolWithMask` 带mask矩阵的max pooling。在做max-pooling的时候不会考虑mask值为0的位置。 """ @@ -93,7 +93,7 @@ class KMaxPool(nn.Module): class AvgPool(nn.Module): """ - 别名::class:`fastNLP.modules.aggregator.AvgPool` :class:`fastNLP.modules.aggregator.pooling.AvgPool` + 别名::class:`fastNLP.modules.AvgPool` :class:`fastNLP.modules.aggregator.pooling.AvgPool` 给定形如[batch_size, max_len, hidden_size]的输入,在最后一维进行avg pooling. 输出为[batch_size, hidden_size] """ @@ -120,7 +120,7 @@ class AvgPool(nn.Module): class AvgPoolWithMask(nn.Module): """ - 别名::class:`fastNLP.modules.aggregator.AvgPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.AvgPoolWithMask` + 别名::class:`fastNLP.modules.AvgPoolWithMask` :class:`fastNLP.modules.aggregator.pooling.AvgPoolWithMask` 给定形如[batch_size, max_len, hidden_size]的输入,在最后一维进行avg pooling. 输出为[batch_size, hidden_size], pooling 的时候只会考虑mask为1的位置 diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/CRF.py index 275f955c..84f374e6 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/CRF.py @@ -6,7 +6,7 @@ from ..utils import initial_parameter def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): """ - 别名::class:`fastNLP.modules.decoder.allowed_transitions` :class:`fastNLP.modules.decoder.CRF.allowed_transitions` + 别名::class:`fastNLP.modules.allowed_transitions` :class:`fastNLP.modules.decoder.CRF.allowed_transitions` 给定一个id到label的映射表,返回所有可以跳转的(from_tag_id, to_tag_id)列表。 @@ -136,7 +136,7 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label class ConditionalRandomField(nn.Module): """ - 别名::class:`fastNLP.modules.decoder.ConditionalRandomField` :class:`fastNLP.modules.decoder.CRF.ConditionalRandomField` + 别名::class:`fastNLP.modules.ConditionalRandomField` :class:`fastNLP.modules.decoder.CRF.ConditionalRandomField` 条件随机场。 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 1e7a4258..a749fa88 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -4,7 +4,7 @@ import torch def viterbi_decode(logits, transitions, mask=None, unpad=False): """ - 别名::class:`fastNLP.modules.decoder.viterbi_decode` :class:`fastNLP.modules.decoder.utils.viterbi_decode + 别名::class:`fastNLP.modules.viterbi_decode` :class:`fastNLP.modules.decoder.utils.viterbi_decode 给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index be04a6be..b5941547 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -7,7 +7,7 @@ from ..utils import initial_parameter # from torch.nn.init import xavier_uniform class ConvolutionCharEncoder(nn.Module): """ - 别名::class:`fastNLP.modules.encoder.ConvolutionCharEncoder` :class:`fastNLP.modules.encoder.char_encoder.ConvolutionCharEncoder` + 别名::class:`fastNLP.modules.ConvolutionCharEncoder` :class:`fastNLP.modules.encoder.char_encoder.ConvolutionCharEncoder` char级别的卷积编码器. :param int char_emb_size: char级别embedding的维度. Default: 50 diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index 6b4c39ed..5ecd376d 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -10,7 +10,7 @@ from ..utils import initial_parameter class ConvMaxpool(nn.Module): """ - 别名::class:`fastNLP.modules.encoder.ConvMaxpool` :class:`fastNLP.modules.encoder.conv_maxpool.ConvMaxpool` + 别名::class:`fastNLP.modules.ConvMaxpool` :class:`fastNLP.modules.encoder.conv_maxpool.ConvMaxpool` 集合了Convolution和Max-Pooling于一体的层。给定一个batch_size x max_len x input_size的输入,返回batch_size x sum(output_channels) 大小的matrix。在内部,是先使用CNN给输入做卷积,然后经过activation激活层,在通过在长度(max_len) diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index ada34c26..c853c142 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -10,7 +10,7 @@ from ..utils import initial_parameter class LSTM(nn.Module): """ - 别名::class:`fastNLP.modules.encoder.LSTM` :class:`fastNLP.modules.encoder.lstm.LSTM` + 别名::class:`fastNLP.modules.LSTM` :class:`fastNLP.modules.encoder.lstm.LSTM` LSTM 模块, 轻量封装的Pytorch LSTM diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index e721c16f..f0d8e38b 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -8,7 +8,7 @@ import numpy as NP class StarTransformer(nn.Module): """ - 别名::class:`fastNLP.modules.encoder.StarTransformer` :class:`fastNLP.modules.encoder.star_transformer.StarTransformer` + 别名::class:`fastNLP.modules.StarTransformer` :class:`fastNLP.modules.encoder.star_transformer.StarTransformer` Star-Transformer 的encoder部分。 输入3d的文本输入, 返回相同长度的文本编码 diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index 9647f86e..7dcae342 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -6,7 +6,7 @@ from ..dropout import TimestepDropout class TransformerEncoder(nn.Module): """ - 别名::class:`fastNLP.modules.encoder.TransformerEncoder` :class:`fastNLP.modules.encoder.transformer.TransformerEncoder` + 别名::class:`fastNLP.modules.TransformerEncoder` :class:`fastNLP.modules.encoder.transformer.TransformerEncoder` transformer的encoder模块,不包含embedding层 diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index e0dd9d90..b926ba9e 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -197,7 +197,7 @@ class VarRNNBase(nn.Module): class VarLSTM(VarRNNBase): """ - 别名::class:`fastNLP.modules.encoder.VarLSTM` :class:`fastNLP.modules.encoder.variational_rnn.VarLSTM` + 别名::class:`fastNLP.modules.VarLSTM` :class:`fastNLP.modules.encoder.variational_rnn.VarLSTM` Variational Dropout LSTM. @@ -221,7 +221,7 @@ class VarLSTM(VarRNNBase): class VarRNN(VarRNNBase): """ - 别名::class:`fastNLP.modules.encoder.VarRNN` :class:`fastNLP.modules.encoder.variational_rnn.VarRNN` + 别名::class:`fastNLP.modules.VarRNN` :class:`fastNLP.modules.encoder.variational_rnn.VarRNN` Variational Dropout RNN. @@ -244,7 +244,7 @@ class VarRNN(VarRNNBase): class VarGRU(VarRNNBase): """ - 别名::class:`fastNLP.modules.encoder.VarGRU` :class:`fastNLP.modules.encoder.variational_rnn.VarGRU` + 别名::class:`fastNLP.modules.VarGRU` :class:`fastNLP.modules.encoder.variational_rnn.VarGRU` Variational Dropout GRU. From 32fdb48754b87d7ecae02f3f5bf74af45775e151 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 16 May 2019 19:45:41 +0800 Subject: [PATCH 134/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20core=20?= =?UTF-8?q?=E9=83=A8=E5=88=86=20import=20=E7=9A=84=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=EF=BC=8C=5F=5Fall=5F=5F=20=E6=9A=B4=E9=9C=B2=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/batch.py | 17 +-- fastNLP/core/callback.py | 23 ++-- fastNLP/core/dataset.py | 8 +- fastNLP/core/field.py | 100 ++++++++++-------- fastNLP/core/instance.py | 4 +- fastNLP/core/losses.py | 17 ++- fastNLP/core/metrics.py | 208 +++++++++++++++++++------------------ fastNLP/core/optimizer.py | 20 ++-- fastNLP/core/predictor.py | 11 +- fastNLP/core/sampler.py | 12 ++- fastNLP/core/tester.py | 29 +++--- fastNLP/core/trainer.py | 13 ++- fastNLP/core/utils.py | 132 ++++++++++++----------- fastNLP/core/vocabulary.py | 5 + 14 files changed, 336 insertions(+), 263 deletions(-) diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 235a9a3a..90f0fc8c 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -2,15 +2,19 @@ batch 模块实现了 fastNLP 所需的 Batch 类。 """ -__all__ = ["Batch"] +import atexit import numpy as np import torch -import atexit - -from .sampler import RandomSampler, Sampler import torch.multiprocessing as mp + from queue import Empty, Full +from .sampler import RandomSampler + +__all__ = [ + "Batch" +] + _python_is_exit = False @@ -120,7 +124,7 @@ class Batch(object): :return list(int) indexes: 下标序列 """ return self.cur_batch_indices - + @staticmethod def _run_fetch(batch, q): try: @@ -145,7 +149,7 @@ class Batch(object): q.put(e) finally: q.join() - + @staticmethod def _run_batch_iter(batch): q = mp.JoinableQueue(maxsize=10) @@ -182,4 +186,3 @@ def _to_tensor(batch, dtype): except: pass return batch - diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 9dce426b..0a5ddc52 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -49,6 +49,18 @@ callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class: trainer.train() """ +import os +import torch + +try: + from tensorboardX import SummaryWriter + + tensorboardX_flag = True +except: + tensorboardX_flag = False + +from ..io.model_io import ModelSaver, ModelLoader + __all__ = [ "Callback", "GradientClipCallback", @@ -60,15 +72,6 @@ __all__ = [ "CallbackException", "EarlyStopError" ] -import os -import torch -from ..io.model_io import ModelSaver, ModelLoader - -try: - from tensorboardX import SummaryWriter - tensorboardX_flag = True -except: - tensorboardX_flag = False class Callback(object): @@ -587,7 +590,7 @@ class TensorboardCallback(Callback): self._summary_writer = SummaryWriter(path) else: self._summary_writer = None - + def on_batch_begin(self, batch_x, batch_y, indices): if "model" in self.options and self.graph_added is False: # tesorboardX 这里有大bug,暂时没法画模型图 diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index b506dfae..63f66019 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -272,9 +272,7 @@ """ -__all__ = ["DataSet"] import _pickle as pickle - import numpy as np import warnings @@ -283,6 +281,10 @@ from .field import FieldArray from .instance import Instance from .utils import _get_func_signature +__all__ = [ + "DataSet" +] + class DataSet(object): """ @@ -854,4 +856,4 @@ class DataSet(object): with open(path, 'rb') as f: d = pickle.load(f) assert isinstance(d, DataSet), "The object is not DataSet, but {}.".format(type(d)) - return d \ No newline at end of file + return d diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index a355c4d2..4029a4ca 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -3,11 +3,17 @@ field模块实现了 FieldArray 和若干 Padder。 FieldArray 是 :class:`~fas 原理部分请参考 :doc:`fastNLP.core.dataset` """ - - import numpy as np + from copy import deepcopy +__all__ = [ + "FieldArray", + "Padder", + "AutoPadder", + "EngChar2DPadder" +] + class FieldArray(object): """ @@ -24,6 +30,7 @@ class FieldArray(object): :param bool ignore_type: 是否忽略该field的type,一般如果这个field不需要转为torch.FloatTensor或torch.LongTensor, 就可以设置为True。具体意义请参考 :class:`~fastNLP.DataSet` 。 """ + def __init__(self, name, content, is_target=None, is_input=None, padder=None, ignore_type=False): self.name = name if isinstance(content, list): @@ -41,7 +48,7 @@ class FieldArray(object): raise TypeError("content in FieldArray can only be list or numpy.ndarray, got {}.".format(type(content))) if len(content) == 0: raise RuntimeError("Cannot initialize FieldArray with empty list.") - + self.content = content # 1维 或 2维 或 3维 list, 形状可能不对齐 self.content_dim = None # 表示content是多少维的list if padder is None: @@ -51,27 +58,27 @@ class FieldArray(object): padder = deepcopy(padder) self.set_padder(padder) self.ignore_type = ignore_type - + self.BASIC_TYPES = (int, float, str) # content中可接受的Python基本类型,这里没有np.array - + self.pytype = None self.dtype = None self._is_input = None self._is_target = None - + if is_input is not None or is_target is not None: self.is_input = is_input self.is_target = is_target - + def _set_dtype(self): if self.ignore_type is False: self.pytype = self._type_detection(self.content) self.dtype = self._map_to_np_type(self.pytype) - + @property def is_input(self): return self._is_input - + @is_input.setter def is_input(self, value): """ @@ -80,11 +87,11 @@ class FieldArray(object): if value is True: self._set_dtype() self._is_input = value - + @property def is_target(self): return self._is_target - + @is_target.setter def is_target(self, value): """ @@ -93,7 +100,7 @@ class FieldArray(object): if value is True: self._set_dtype() self._is_target = value - + def _type_detection(self, content): """ 当该field被设置为is_input或者is_target时被调用 @@ -101,9 +108,9 @@ class FieldArray(object): """ if len(content) == 0: raise RuntimeError("Empty list in Field {}.".format(self.name)) - + type_set = set([type(item) for item in content]) - + if list in type_set: if len(type_set) > 1: # list 跟 非list 混在一起 @@ -139,7 +146,7 @@ class FieldArray(object): self.name, self.BASIC_TYPES, content_type)) self.content_dim = 1 return self._basic_type_detection(type_set) - + def _basic_type_detection(self, type_set): """ :param type_set: a set of Python types @@ -158,7 +165,7 @@ class FieldArray(object): else: # str, int, float混在一起 raise RuntimeError("Mixed data types in Field {}: {}".format(self.name, list(type_set))) - + def _1d_list_check(self, val): """如果不是1D list就报错 """ @@ -168,7 +175,7 @@ class FieldArray(object): self._basic_type_detection(type_set) # otherwise: _basic_type_detection will raise error return True - + def _2d_list_check(self, val): """如果不是2D list 就报错 """ @@ -181,15 +188,15 @@ class FieldArray(object): inner_type_set.add(type(obj)) self._basic_type_detection(inner_type_set) return True - + @staticmethod def _map_to_np_type(basic_type): type_mapping = {int: np.int64, float: np.float64, str: np.str, np.ndarray: np.ndarray} return type_mapping[basic_type] - + def __repr__(self): return "FieldArray {}: {}".format(self.name, self.content.__repr__()) - + def append(self, val): """将val append到这个field的尾部。如果这个field已经被设置为input或者target,则在append之前会检查该类型是否与已有 的内容是匹配的。 @@ -208,7 +215,7 @@ class FieldArray(object): else: raise RuntimeError( "Unexpected data type {}. Should be list, np.array, or {}".format(type(val), self.BASIC_TYPES)) - + if self.is_input is True or self.is_target is True: if type(val) == list: if len(val) == 0: @@ -231,14 +238,14 @@ class FieldArray(object): raise RuntimeError( "Unexpected data type {}. Should be list, np.array, or {}".format(type(val), self.BASIC_TYPES)) self.content.append(val) - + def __getitem__(self, indices): return self.get(indices, pad=False) - + def __setitem__(self, idx, val): assert isinstance(idx, int) self.content[idx] = val - + def get(self, indices, pad=True): """ 根据给定的indices返回内容 @@ -251,13 +258,13 @@ class FieldArray(object): return self.content[indices] if self.is_input is False and self.is_target is False: raise RuntimeError("Please specify either is_input or is_target is True for {}".format(self.name)) - + contents = [self.content[i] for i in indices] if self.padder is None or pad is False: return np.array(contents) else: return self.padder(contents, field_name=self.name, field_ele_dtype=self.dtype) - + def set_padder(self, padder): """ 设置padder,在这个field进行pad的时候用这个padder进行pad,如果为None则不进行pad。 @@ -269,7 +276,7 @@ class FieldArray(object): self.padder = deepcopy(padder) else: self.padder = None - + def set_pad_val(self, pad_val): """ 修改padder的pad_val. @@ -279,8 +286,7 @@ class FieldArray(object): if self.padder is not None: self.padder.set_pad_val(pad_val) return self - - + def __len__(self): """ Returns the size of FieldArray. @@ -288,7 +294,7 @@ class FieldArray(object): :return int length: """ return len(self.content) - + def to(self, other): """ 将other的属性复制给本FieldArray(other必须为FieldArray类型). @@ -298,14 +304,15 @@ class FieldArray(object): :return: :class:`~fastNLP.FieldArray` """ assert isinstance(other, FieldArray), "Only support FieldArray type, not {}.".format(type(other)) - + self.is_input = other.is_input self.is_target = other.is_target self.padder = other.padder self.ignore_type = other.ignore_type - + return self + def _is_iterable(content): try: _ = (e for e in content) @@ -331,13 +338,13 @@ class Padder: :return: np.array([padded_element]) """ - + def __init__(self, pad_val=0, **kwargs): self.pad_val = pad_val - + def set_pad_val(self, pad_val): self.pad_val = pad_val - + def __call__(self, contents, field_name, field_ele_dtype): """ 传入的是List内容。假设有以下的DataSet。 @@ -396,13 +403,13 @@ class AutoPadder(Padder): 2.2 如果该field的内容为List, 那么会将Batch中的List pad为一样长。若该List下还有里层的List需要padding,请使用其它padder。 即如果Instance中field形如[1, 2, 3, ...],则可以pad;若为[[1,2], [3,4, ...]]则不能进行pad """ - + def __init__(self, pad_val=0): """ :param pad_val: int, padding的位置使用该index """ super().__init__(pad_val=pad_val) - + def _is_two_dimension(self, contents): """ 判断contents是不是只有两个维度。[[1,2], [3]]是两个维度. [[[1,2], [3, 4, 5]], [[4,5]]]有三个维度 @@ -416,7 +423,7 @@ class AutoPadder(Padder): return False return True return False - + def __call__(self, contents, field_name, field_ele_dtype): if not _is_iterable(contents[0]): @@ -458,6 +465,7 @@ class EngChar2DPadder(Padder): dataset.set_padder('chars', padder) # chars这个field的设置为了EnChar2DPadder """ + def __init__(self, pad_val=0, pad_length=0): """ :param pad_val: int, pad的位置使用该index @@ -465,9 +473,9 @@ class EngChar2DPadder(Padder): 都pad或截取到该长度. """ super().__init__(pad_val=pad_val) - + self.pad_length = pad_length - + def _exactly_three_dims(self, contents, field_name): """ 检查传入的contents是否刚好是3维,如果不是3维就报错。理论上,第一个维度是batch,第二个维度是word,第三个维度是character @@ -486,10 +494,10 @@ class EngChar2DPadder(Padder): value = value[0] except: raise ValueError("Field:{} only has two dimensions.".format(field_name)) - + if _is_iterable(value): raise ValueError("Field:{} has more than 3 dimension.".format(field_name)) - + def __call__(self, contents, field_name, field_ele_dtype): """ 期望输入类似于 @@ -516,12 +524,12 @@ class EngChar2DPadder(Padder): max_sent_length = max(len(word_lst) for word_lst in contents) batch_size = len(contents) dtype = type(contents[0][0][0]) - + padded_array = np.full((batch_size, max_sent_length, max_char_length), fill_value=self.pad_val, - dtype=dtype) + dtype=dtype) for b_idx, word_lst in enumerate(contents): for c_idx, char_lst in enumerate(word_lst): chars = char_lst[:max_char_length] padded_array[b_idx, c_idx, :len(chars)] = chars - - return padded_array \ No newline at end of file + + return padded_array diff --git a/fastNLP/core/instance.py b/fastNLP/core/instance.py index 2303c510..07ae6495 100644 --- a/fastNLP/core/instance.py +++ b/fastNLP/core/instance.py @@ -3,7 +3,9 @@ instance 模块实现了Instance 类在fastNLP中对应sample。一个sample可 便于理解的例子可以参考文档 :doc:`fastNLP.core.dataset` 中的表格 """ -__all__ = ["Instance"] +__all__ = [ + "Instance" +] class Instance(object): diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 7a5fdf9d..b98c5ac7 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -2,13 +2,12 @@ losses 模块定义了 fastNLP 中所需的各种损失函数,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ -__all__ = ["LossBase", "L1Loss", "LossFunc", "LossInForward", "BCELoss", "CrossEntropyLoss", "NLLLoss"] import inspect -from collections import defaultdict - import torch import torch.nn.functional as F +from collections import defaultdict + from .utils import _CheckError from .utils import _CheckRes from .utils import _build_args @@ -16,6 +15,18 @@ from .utils import _check_arg_dict_list from .utils import _check_function_or_method from .utils import _get_func_signature +__all__ = [ + "LossBase", + + "LossFunc", + "LossInForward", + + "CrossEntropyLoss", + "BCELoss", + "L1Loss", + "NLLLoss" +] + class LossBase(object): """ diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 7a96020b..df85a318 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -3,11 +3,11 @@ metrics 模块实现了 fastNLP 所需的各种常用衡量指标,一般做为 """ import inspect -from collections import defaultdict - import numpy as np import torch +from collections import defaultdict + from .utils import _CheckError from .utils import _CheckRes from .utils import _build_args @@ -16,6 +16,13 @@ from .utils import _get_func_signature from .utils import seq_len_to_mask from .vocabulary import Vocabulary +__all__ = [ + "MetricBase", + "AccuracyMetric", + "SpanFPreRecMetric", + "SQuADMetric" +] + class MetricBase(object): """ @@ -106,16 +113,17 @@ class MetricBase(object): self.get_metric将统计当前的评价指标并返回评价结果, 返回值需要是一个dict, key是指标名称,value是指标的值 """ + def __init__(self): self.param_map = {} # key is param in function, value is input param. self._checked = False - + def evaluate(self, *args, **kwargs): raise NotImplementedError - + def get_metric(self, reset=True): raise NotImplemented - + def _init_param_map(self, key_map=None, **kwargs): """检查key_map和其他参数map,并将这些映射关系添加到self.param_map @@ -148,7 +156,7 @@ class MetricBase(object): for value, key_set in value_counter.items(): if len(key_set) > 1: raise ValueError(f"Several parameters:{key_set} are provided with one output {value}.") - + # check consistence between signature and param_map func_spect = inspect.getfullargspec(self.evaluate) func_args = [arg for arg in func_spect.args if arg != 'self'] @@ -157,7 +165,7 @@ class MetricBase(object): raise NameError( f"Parameter `{func_param}` is not in {_get_func_signature(self.evaluate)}. Please check the " f"initialization parameters, or change its signature.") - + def _fast_param_map(self, pred_dict, target_dict): """Only used as inner function. When the pred_dict, target is unequivocal. Don't need users to pass key_map. such as pred_dict has one element, target_dict has one element @@ -172,7 +180,7 @@ class MetricBase(object): fast_param['target'] = list(target_dict.values())[0] return fast_param return fast_param - + def __call__(self, pred_dict, target_dict): """ 这个方法会调用self.evaluate 方法. @@ -187,12 +195,12 @@ class MetricBase(object): :param target_dict: DataSet.batch_y里的键-值对所组成的dict(即is_target=True的fields的内容) :return: """ - + fast_param = self._fast_param_map(pred_dict, target_dict) if fast_param: self.evaluate(**fast_param) return - + if not self._checked: if not callable(self.evaluate): raise TypeError(f"{self.__class__.__name__}.evaluate has to be callable, not {type(self.evaluate)}.") @@ -202,14 +210,14 @@ class MetricBase(object): for func_arg, input_arg in self.param_map.items(): if func_arg not in func_args: raise NameError(f"`{func_arg}` not in {_get_func_signature(self.evaluate)}.") - + # 2. only part of the param_map are passed, left are not for arg in func_args: if arg not in self.param_map: self.param_map[arg] = arg # This param does not need mapping. self._evaluate_args = func_args self._reverse_param_map = {input_arg: func_arg for func_arg, input_arg in self.param_map.items()} - + # need to wrap inputs in dict. mapped_pred_dict = {} mapped_target_dict = {} @@ -229,7 +237,7 @@ class MetricBase(object): not_duplicate_flag += 1 if not_duplicate_flag == 3: duplicated.append(input_arg) - + # missing if not self._checked: check_res = _check_arg_dict_list(self.evaluate, [mapped_pred_dict, mapped_target_dict]) @@ -240,23 +248,23 @@ class MetricBase(object): for idx, func_arg in enumerate(missing): # Don't delete `` in this information, nor add `` replaced_missing[idx] = f"{self.param_map[func_arg]}" + f"(assign to `{func_arg}` " \ - f"in `{self.__class__.__name__}`)" - + f"in `{self.__class__.__name__}`)" + check_res = _CheckRes(missing=replaced_missing, unused=check_res.unused, duplicated=duplicated, required=check_res.required, all_needed=check_res.all_needed, varargs=check_res.varargs) - + if check_res.missing or check_res.duplicated: raise _CheckError(check_res=check_res, func_signature=_get_func_signature(self.evaluate)) refined_args = _build_args(self.evaluate, **mapped_pred_dict, **mapped_target_dict) - + self.evaluate(**refined_args) self._checked = True - + return @@ -271,15 +279,16 @@ class AccuracyMetric(MetricBase): :param target: 参数映射表中 `target` 的映射关系,None表示映射关系为 `target` -> `target` :param seq_len: 参数映射表中 `seq_len` 的映射关系,None表示映射关系为 `seq_len` -> `seq_len` """ + def __init__(self, pred=None, target=None, seq_len=None): super().__init__() - + self._init_param_map(pred=pred, target=target, seq_len=seq_len) - + self.total = 0 self.acc_count = 0 - + def evaluate(self, pred, target, seq_len=None): """ evaluate函数将针对一个批次的预测结果做评价指标的累计 @@ -299,16 +308,16 @@ class AccuracyMetric(MetricBase): if not isinstance(target, torch.Tensor): raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") - + if seq_len is not None and not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_len)}.") - + if seq_len is not None: masks = seq_len_to_mask(seq_len=seq_len) else: masks = None - + if pred.size() == target.size(): pass elif len(pred.size()) == len(target.size()) + 1: @@ -317,7 +326,7 @@ class AccuracyMetric(MetricBase): raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") - + target = target.to(pred) if masks is not None: self.acc_count += torch.sum(torch.eq(pred, target).masked_fill(masks.eq(0), 0)).item() @@ -325,7 +334,7 @@ class AccuracyMetric(MetricBase): else: self.acc_count += torch.sum(torch.eq(pred, target)).item() self.total += np.prod(list(pred.size())) - + def get_metric(self, reset=True): """ get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果. @@ -350,7 +359,7 @@ def _bmes_tag_to_spans(tags, ignore_labels=None): :return: List[Tuple[str, List[int, int]]]. [(label,[start, end])] """ ignore_labels = set(ignore_labels) if ignore_labels else set() - + spans = [] prev_bmes_tag = None for idx, tag in enumerate(tags): @@ -358,14 +367,14 @@ def _bmes_tag_to_spans(tags, ignore_labels=None): bmes_tag, label = tag[:1], tag[2:] if bmes_tag in ('b', 's'): spans.append((label, [idx, idx])) - elif bmes_tag in ('m', 'e') and prev_bmes_tag in ('b', 'm') and label==spans[-1][0]: + elif bmes_tag in ('m', 'e') and prev_bmes_tag in ('b', 'm') and label == spans[-1][0]: spans[-1][1][1] = idx else: spans.append((label, [idx, idx])) prev_bmes_tag = bmes_tag - return [(span[0], (span[1][0], span[1][1]+1)) - for span in spans - if span[0] not in ignore_labels + return [(span[0], (span[1][0], span[1][1] + 1)) + for span in spans + if span[0] not in ignore_labels ] @@ -379,7 +388,7 @@ def _bmeso_tag_to_spans(tags, ignore_labels=None): :return: List[Tuple[str, List[int, int]]]. [(label,[start, end])] """ ignore_labels = set(ignore_labels) if ignore_labels else set() - + spans = [] prev_bmes_tag = None for idx, tag in enumerate(tags): @@ -387,16 +396,16 @@ def _bmeso_tag_to_spans(tags, ignore_labels=None): bmes_tag, label = tag[:1], tag[2:] if bmes_tag in ('b', 's'): spans.append((label, [idx, idx])) - elif bmes_tag in ('m', 'e') and prev_bmes_tag in ('b', 'm') and label==spans[-1][0]: + elif bmes_tag in ('m', 'e') and prev_bmes_tag in ('b', 'm') and label == spans[-1][0]: spans[-1][1][1] = idx elif bmes_tag == 'o': pass else: spans.append((label, [idx, idx])) prev_bmes_tag = bmes_tag - return [(span[0], (span[1][0], span[1][1]+1)) - for span in spans - if span[0] not in ignore_labels + return [(span[0], (span[1][0], span[1][1] + 1)) + for span in spans + if span[0] not in ignore_labels ] @@ -410,7 +419,7 @@ def _bio_tag_to_spans(tags, ignore_labels=None): :return: List[Tuple[str, List[int, int]]]. [(label,[start, end])] """ ignore_labels = set(ignore_labels) if ignore_labels else set() - + spans = [] prev_bio_tag = None for idx, tag in enumerate(tags): @@ -418,14 +427,14 @@ def _bio_tag_to_spans(tags, ignore_labels=None): bio_tag, label = tag[:1], tag[2:] if bio_tag == 'b': spans.append((label, [idx, idx])) - elif bio_tag == 'i' and prev_bio_tag in ('b', 'i') and label==spans[-1][0]: + elif bio_tag == 'i' and prev_bio_tag in ('b', 'i') and label == spans[-1][0]: spans[-1][1][1] = idx - elif bio_tag == 'o': # o tag does not count + elif bio_tag == 'o': # o tag does not count pass else: spans.append((label, [idx, idx])) prev_bio_tag = bio_tag - return [(span[0], (span[1][0], span[1][1]+1)) for span in spans if span[0] not in ignore_labels] + return [(span[0], (span[1][0], span[1][1] + 1)) for span in spans if span[0] not in ignore_labels] class SpanFPreRecMetric(MetricBase): @@ -470,16 +479,17 @@ class SpanFPreRecMetric(MetricBase): :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 """ + def __init__(self, tag_vocab, pred=None, target=None, seq_len=None, encoding_type='bio', ignore_labels=None, - only_gross=True, f_type='micro', beta=1): + only_gross=True, f_type='micro', beta=1): encoding_type = encoding_type.lower() - + if not isinstance(tag_vocab, Vocabulary): raise TypeError("tag_vocab can only be fastNLP.Vocabulary, not {}.".format(type(tag_vocab))) if f_type not in ('micro', 'macro'): raise ValueError("f_type only supports `micro` or `macro`', got {}.".format(f_type)) - + self.encoding_type = encoding_type if self.encoding_type == 'bmes': self.tag_to_span_func = _bmes_tag_to_spans @@ -489,22 +499,22 @@ class SpanFPreRecMetric(MetricBase): self.tag_to_span_func = _bmeso_tag_to_spans else: raise ValueError("Only support 'bio', 'bmes', 'bmeso' type.") - + self.ignore_labels = ignore_labels self.f_type = f_type self.beta = beta - self.beta_square = self.beta**2 + self.beta_square = self.beta ** 2 self.only_gross = only_gross - + super().__init__() self._init_param_map(pred=pred, target=target, seq_len=seq_len) - + self.tag_vocab = tag_vocab - + self._true_positives = defaultdict(int) self._false_positives = defaultdict(int) self._false_negatives = defaultdict(int) - + def evaluate(self, pred, target, seq_len): """evaluate函数将针对一个批次的预测结果做评价指标的累计 @@ -519,11 +529,11 @@ class SpanFPreRecMetric(MetricBase): if not isinstance(target, torch.Tensor): raise TypeError(f"`target` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(target)}.") - + if not isinstance(seq_len, torch.Tensor): raise TypeError(f"`seq_lens` in {_get_func_signature(self.evaluate)} must be torch.Tensor," f"got {type(seq_len)}.") - + if pred.size() == target.size() and len(target.size()) == 2: pass elif len(pred.size()) == len(target.size()) + 1 and len(target.size()) == 2: @@ -536,20 +546,20 @@ class SpanFPreRecMetric(MetricBase): raise RuntimeError(f"In {_get_func_signature(self.evaluate)}, when pred have " f"size:{pred.size()}, target should have size: {pred.size()} or " f"{pred.size()[:-1]}, got {target.size()}.") - + batch_size = pred.size(0) pred = pred.tolist() target = target.tolist() for i in range(batch_size): pred_tags = pred[i][:int(seq_len[i])] gold_tags = target[i][:int(seq_len[i])] - + pred_str_tags = [self.tag_vocab.to_word(tag) for tag in pred_tags] gold_str_tags = [self.tag_vocab.to_word(tag) for tag in gold_tags] - + pred_spans = self.tag_to_span_func(pred_str_tags, ignore_labels=self.ignore_labels) gold_spans = self.tag_to_span_func(gold_str_tags, ignore_labels=self.ignore_labels) - + for span in pred_spans: if span in gold_spans: self._true_positives[span[0]] += 1 @@ -558,7 +568,7 @@ class SpanFPreRecMetric(MetricBase): self._false_positives[span[0]] += 1 for span in gold_spans: self._false_negatives[span[0]] += 1 - + def get_metric(self, reset=True): """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" evaluate_result = {} @@ -577,19 +587,19 @@ class SpanFPreRecMetric(MetricBase): f_sum += f pre_sum += pre rec_sum + rec - if not self.only_gross and tag!='': # tag!=''防止无tag的情况 + if not self.only_gross and tag != '': # tag!=''防止无tag的情况 f_key = 'f-{}'.format(tag) pre_key = 'pre-{}'.format(tag) rec_key = 'rec-{}'.format(tag) evaluate_result[f_key] = f evaluate_result[pre_key] = pre evaluate_result[rec_key] = rec - + if self.f_type == 'macro': - evaluate_result['f'] = f_sum/len(tags) - evaluate_result['pre'] = pre_sum/len(tags) - evaluate_result['rec'] = rec_sum/len(tags) - + evaluate_result['f'] = f_sum / len(tags) + evaluate_result['pre'] = pre_sum / len(tags) + evaluate_result['rec'] = rec_sum / len(tags) + if self.f_type == 'micro': f, pre, rec = self._compute_f_pre_rec(sum(self._true_positives.values()), sum(self._false_negatives.values()), @@ -597,17 +607,17 @@ class SpanFPreRecMetric(MetricBase): evaluate_result['f'] = f evaluate_result['pre'] = pre evaluate_result['rec'] = rec - + if reset: self._true_positives = defaultdict(int) self._false_positives = defaultdict(int) self._false_negatives = defaultdict(int) - + for key, value in evaluate_result.items(): evaluate_result[key] = round(value, 6) - + return evaluate_result - + def _compute_f_pre_rec(self, tp, fn, fp): """ @@ -619,11 +629,10 @@ class SpanFPreRecMetric(MetricBase): pre = tp / (fp + tp + 1e-13) rec = tp / (fn + tp + 1e-13) f = (1 + self.beta_square) * pre * rec / (self.beta_square * pre + rec + 1e-13) - + return f, pre, rec - def _prepare_metrics(metrics): """ @@ -705,33 +714,33 @@ class SQuADMetric(MetricBase): :param bool print_predict_stat: True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 """ - + def __init__(self, pred1=None, pred2=None, target1=None, target2=None, beta=1, right_open=True, print_predict_stat=False): super(SQuADMetric, self).__init__() - + self._init_param_map(pred1=pred1, pred2=pred2, target1=target1, target2=target2) - + self.print_predict_stat = print_predict_stat - + self.no_ans_correct = 0 self.no_ans_wrong = 0 - + self.has_ans_correct = 0 self.has_ans_wrong = 0 - + self.has_ans_f = 0. - + self.no2no = 0 self.no2yes = 0 self.yes2no = 0 self.yes2yes = 0 - + self.f_beta = beta - + self.right_open = right_open - + def evaluate(self, pred1, pred2, target1, target2): """evaluate函数将针对一个批次的预测结果做评价指标的累计 @@ -745,7 +754,7 @@ class SQuADMetric(MetricBase): pred_end = pred2 target_start = target1 target_end = target2 - + if len(pred_start.size()) == 2: start_inference = pred_start.max(dim=-1)[1].cpu().tolist() else: @@ -754,12 +763,12 @@ class SQuADMetric(MetricBase): end_inference = pred_end.max(dim=-1)[1].cpu().tolist() else: end_inference = pred_end.cpu().tolist() - + start, end = [], [] max_len = pred_start.size(1) t_start = target_start.cpu().tolist() t_end = target_end.cpu().tolist() - + for s, e in zip(start_inference, end_inference): start.append(min(s, e)) end.append(max(s, e)) @@ -779,7 +788,7 @@ class SQuADMetric(MetricBase): self.yes2no += 1 else: self.yes2yes += 1 - + if s == ts and e == te: self.has_ans_correct += 1 else: @@ -787,29 +796,29 @@ class SQuADMetric(MetricBase): a = [0] * s + [1] * (e - s) + [0] * (max_len - e) b = [0] * ts + [1] * (te - ts) + [0] * (max_len - te) a, b = torch.tensor(a), torch.tensor(b) - + TP = int(torch.sum(a * b)) pre = TP / int(torch.sum(a)) if int(torch.sum(a)) > 0 else 0 rec = TP / int(torch.sum(b)) if int(torch.sum(b)) > 0 else 0 - + if pre + rec > 0: - f = (1 + (self.f_beta**2)) * pre * rec / ((self.f_beta**2) * pre + rec) + f = (1 + (self.f_beta ** 2)) * pre * rec / ((self.f_beta ** 2) * pre + rec) else: f = 0 self.has_ans_f += f - + def get_metric(self, reset=True): """get_metric函数将根据evaluate函数累计的评价指标统计量来计算最终的评价结果.""" evaluate_result = {} - + if self.no_ans_correct + self.no_ans_wrong + self.has_ans_correct + self.no_ans_wrong <= 0: return evaluate_result - + evaluate_result['EM'] = 0 evaluate_result[f'f_{self.f_beta}'] = 0 - + flag = 0 - + if self.no_ans_correct + self.no_ans_wrong > 0: evaluate_result[f'noAns-f_{self.f_beta}'] = \ round(100 * self.no_ans_correct / (self.no_ans_correct + self.no_ans_wrong), 3) @@ -818,7 +827,7 @@ class SQuADMetric(MetricBase): evaluate_result[f'f_{self.f_beta}'] += evaluate_result[f'noAns-f_{self.f_beta}'] evaluate_result['EM'] += evaluate_result['noAns-EM'] flag += 1 - + if self.has_ans_correct + self.has_ans_wrong > 0: evaluate_result[f'hasAns-f_{self.f_beta}'] = \ round(100 * self.has_ans_f / (self.has_ans_correct + self.has_ans_wrong), 3) @@ -827,32 +836,31 @@ class SQuADMetric(MetricBase): evaluate_result[f'f_{self.f_beta}'] += evaluate_result[f'hasAns-f_{self.f_beta}'] evaluate_result['EM'] += evaluate_result['hasAns-EM'] flag += 1 - + if self.print_predict_stat: evaluate_result['no2no'] = self.no2no evaluate_result['no2yes'] = self.no2yes evaluate_result['yes2no'] = self.yes2no evaluate_result['yes2yes'] = self.yes2yes - + if flag <= 0: return evaluate_result - + evaluate_result[f'f_{self.f_beta}'] = round(evaluate_result[f'f_{self.f_beta}'] / flag, 3) evaluate_result['EM'] = round(evaluate_result['EM'] / flag, 3) - + if reset: self.no_ans_correct = 0 self.no_ans_wrong = 0 - + self.has_ans_correct = 0 self.has_ans_wrong = 0 - + self.has_ans_f = 0. - + self.no2no = 0 self.no2yes = 0 self.yes2no = 0 self.yes2yes = 0 - + return evaluate_result - diff --git a/fastNLP/core/optimizer.py b/fastNLP/core/optimizer.py index ea4905eb..28f618f9 100644 --- a/fastNLP/core/optimizer.py +++ b/fastNLP/core/optimizer.py @@ -4,6 +4,12 @@ optimizer 模块定义了 fastNLP 中所需的各种优化器,一般做为 :cl """ import torch +__all__ = [ + "Optimizer", + "SGD", + "Adam" +] + class Optimizer(object): """ @@ -12,15 +18,16 @@ class Optimizer(object): :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. :param kwargs: additional parameters. """ + def __init__(self, model_params, **kwargs): if model_params is not None and not hasattr(model_params, "__next__"): raise RuntimeError("model parameters should be a generator, rather than {}.".format(type(model_params))) self.model_params = model_params self.settings = kwargs - + def construct_from_pytorch(self, model_params): raise NotImplementedError - + def _get_require_grads_param(self, params): """ 将params中不需要gradient的删除 @@ -29,6 +36,7 @@ class Optimizer(object): """ return [param for param in params if param.requires_grad] + class SGD(Optimizer): """ 别名::class:`fastNLP.SGD` :class:`fastNLP.core.optimizer.SGD` @@ -37,12 +45,12 @@ class SGD(Optimizer): :param float momentum: momentum. Default: 0 :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. """ - + def __init__(self, lr=0.001, momentum=0, model_params=None): if not isinstance(lr, float): raise TypeError("learning rate has to be float.") super(SGD, self).__init__(model_params, lr=lr, momentum=momentum) - + def construct_from_pytorch(self, model_params): if self.model_params is None: # careful! generator cannot be assigned. @@ -59,13 +67,13 @@ class Adam(Optimizer): :param float weight_decay: :param model_params: a generator. E.g. ``model.parameters()`` for PyTorch models. """ - + def __init__(self, lr=0.001, weight_decay=0, betas=(0.9, 0.999), eps=1e-8, amsgrad=False, model_params=None): if not isinstance(lr, float): raise TypeError("learning rate has to be float.") super(Adam, self).__init__(model_params, lr=lr, betas=betas, eps=eps, amsgrad=amsgrad, weight_decay=weight_decay) - + def construct_from_pytorch(self, model_params): if self.model_params is None: # careful! generator cannot be assigned. diff --git a/fastNLP/core/predictor.py b/fastNLP/core/predictor.py index 34784b7c..a9ef7924 100644 --- a/fastNLP/core/predictor.py +++ b/fastNLP/core/predictor.py @@ -1,7 +1,11 @@ -from collections import defaultdict - +""" + ..todo:: + 检查这个类是否需要 +""" import torch +from collections import defaultdict + from . import Batch from . import DataSet from . import SequentialSampler @@ -9,7 +13,8 @@ from .utils import _build_args class Predictor(object): - """An interface for predicting outputs based on trained models. + """ + An interface for predicting outputs based on trained models. It does not care about evaluations of the model, which is different from Tester. This is a high-level model wrapper to be called by FastNLP. diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index e270dac1..0900e733 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -1,12 +1,16 @@ """ sampler 子类实现了 fastNLP 所需的各种采样器。 - - """ -__all__ = ["Sampler", "BucketSampler", "SequentialSampler", "RandomSampler"] +import numpy as np + from itertools import chain -import numpy as np +__all__ = [ + "Sampler", + "BucketSampler", + "SequentialSampler", + "RandomSampler" +] class Sampler(object): diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 7b6fdda5..47aef46e 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -33,9 +33,8 @@ Tester在验证进行之前会调用model.eval()提示当前进入了evaluation """ import warnings - import torch -from torch import nn +import torch.nn as nn from .batch import Batch from .dataset import DataSet @@ -49,6 +48,10 @@ from .utils import _get_func_signature from .utils import _get_model_device from .utils import _move_model_to_device +__all__ = [ + "Tester" +] + class Tester(object): """ @@ -77,29 +80,29 @@ class Tester(object): 如果模型是通过predict()进行预测的话,那么将不能使用多卡(DataParallel)进行验证,只会使用第一张卡上的模型。 :param int verbose: 如果为0不输出任何信息; 如果为1,打印出验证结果。 """ - + def __init__(self, data, model, metrics, batch_size=16, device=None, verbose=1): super(Tester, self).__init__() - + if not isinstance(data, DataSet): raise TypeError(f"The type of data must be `fastNLP.DataSet`, got `{type(data)}`.") if not isinstance(model, nn.Module): raise TypeError(f"The type of model must be `torch.nn.Module`, got `{type(model)}`.") - + self.metrics = _prepare_metrics(metrics) - + self.data = data self._model = _move_model_to_device(model, device=device) self.batch_size = batch_size self.verbose = verbose - + # 如果是DataParallel将没有办法使用predict方法 if isinstance(self._model, nn.DataParallel): if hasattr(self._model.module, 'predict') and not hasattr(self._model, 'predict'): warnings.warn("Cannot use DataParallel to test your model, because your model offer predict() function," " while DataParallel has no predict() function.") self._model = self._model.module - + # check predict if hasattr(self._model, 'predict'): self._predict_func = self._model.predict @@ -109,7 +112,7 @@ class Tester(object): f"for evaluation, not `{type(self._predict_func)}`.") else: self._predict_func = self._model.forward - + def test(self): """开始进行验证,并返回验证结果。 @@ -144,12 +147,12 @@ class Tester(object): _check_loss_evaluate(prev_func_signature=prev_func_signature, func_signature=e.func_signature, check_res=e.check_res, pred_dict=pred_dict, target_dict=batch_y, dataset=self.data, check_level=0) - + if self.verbose >= 1: print("[tester] \n{}".format(self._format_eval_results(eval_results))) self._mode(network, is_test=False) return eval_results - + def _mode(self, model, is_test=False): """Train mode or Test mode. This is for PyTorch currently. @@ -161,13 +164,13 @@ class Tester(object): model.eval() else: model.train() - + def _data_forward(self, func, x): """A forward pass of the model. """ x = _build_args(func, **x) y = func(**x) return y - + def _format_eval_results(self, results): """Override this method to support more print formats. diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 9b56d834..87d57f12 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -297,13 +297,12 @@ Example2.3 """ import os -import time -from datetime import datetime -from datetime import timedelta - import numpy as np +import time import torch -from torch import nn +import torch.nn as nn + +from datetime import datetime, timedelta try: from tqdm.auto import tqdm @@ -315,6 +314,7 @@ from .callback import CallbackManager, CallbackException from .dataset import DataSet from .losses import _prepare_losser from .metrics import _prepare_metrics +from .optimizer import Optimizer from .sampler import Sampler from .sampler import RandomSampler from .sampler import SequentialSampler @@ -326,7 +326,6 @@ from .utils import _check_loss_evaluate from .utils import _move_dict_value_to_device from .utils import _get_func_signature from .utils import _get_model_device -from .optimizer import Optimizer from .utils import _move_model_to_device @@ -464,7 +463,7 @@ class Trainer(object): len(self.train_data) % self.batch_size != 0)) * self.n_epochs self.model = _move_model_to_device(self.model, device=device) - + if isinstance(optimizer, torch.optim.Optimizer): self.optimizer = optimizer elif isinstance(optimizer, Optimizer): diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 2c386bbe..a7ad3326 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -1,20 +1,25 @@ """ utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户可以使用的是 :func:`cache_results` 修饰器。 """ -__all__ = ["cache_results", "seq_len_to_mask"] import _pickle import inspect +import numpy as np import os +import torch +import torch.nn as nn import warnings + from collections import Counter from collections import namedtuple -import numpy as np -import torch -from torch import nn +__all__ = [ + "cache_results", + "seq_len_to_mask" +] _CheckRes = namedtuple('_CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', - 'varargs']) + 'varargs']) + def _prepare_cache_filepath(filepath): """ @@ -83,11 +88,13 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): :param int _verbose: 是否打印cache的信息。 :return: """ + def wrapper_(func): signature = inspect.signature(func) for key, _ in signature.parameters.items(): if key in ('_cache_fp', '_refresh', '_verbose'): raise RuntimeError("The function decorated by cache_results cannot have keyword `{}`.".format(key)) + def wrapper(*args, **kwargs): if '_cache_fp' in kwargs: cache_filepath = kwargs.pop('_cache_fp') @@ -95,7 +102,7 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): else: cache_filepath = _cache_fp if '_refresh' in kwargs: - refresh = kwargs.pop('_refresh') + refresh = kwargs.pop('_refresh') assert isinstance(refresh, bool), "_refresh can only be bool." else: refresh = _refresh @@ -105,16 +112,16 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): else: verbose = _verbose refresh_flag = True - + if cache_filepath is not None and refresh is False: # load data if os.path.exists(cache_filepath): with open(cache_filepath, 'rb') as f: results = _pickle.load(f) - if verbose==1: + if verbose == 1: print("Read cache from {}.".format(cache_filepath)) refresh_flag = False - + if refresh_flag: results = func(*args, **kwargs) if cache_filepath is not None: @@ -124,11 +131,14 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): with open(cache_filepath, 'wb') as f: _pickle.dump(results, f) print("Save cache to {}.".format(cache_filepath)) - + return results + return wrapper + return wrapper_ + # def save_pickle(obj, pickle_path, file_name): # """Save an object into a pickle file. # @@ -196,7 +206,7 @@ def _move_model_to_device(model, device): """ if isinstance(model, torch.nn.parallel.DistributedDataParallel): raise RuntimeError("model of `torch.nn.parallel.DistributedDataParallel` is not supported right now.") - + if device is None: if isinstance(model, torch.nn.DataParallel): model.cuda() @@ -205,34 +215,35 @@ def _move_model_to_device(model, device): if not torch.cuda.is_available() and ( device != 'cpu' or (isinstance(device, torch.device) and device.type != 'cpu')): raise ValueError("There is no usable gpu. set `device` as `cpu` or `None`.") - + if isinstance(model, torch.nn.DataParallel): raise RuntimeError("When model is `torch.nn.DataParallel`, the device has to be `None`.") - + if isinstance(device, int): - assert device>-1, "device can only be non-negative integer" - assert torch.cuda.device_count()>device, "Only has {} gpus, cannot use device {}.".format(torch.cuda.device_count(), - device) + assert device > -1, "device can only be non-negative integer" + assert torch.cuda.device_count() > device, "Only has {} gpus, cannot use device {}.".format( + torch.cuda.device_count(), + device) device = torch.device('cuda:{}'.format(device)) elif isinstance(device, str): device = torch.device(device) if device.type == 'cuda' and device.index is not None: - assert device.index-1, "Only non-negative device id allowed." - if len(device)>1: + assert d > -1, "Only non-negative device id allowed." + if len(device) > 1: output_device = device[0] model = nn.DataParallel(model, device_ids=device, output_device=output_device) device = torch.device(device[0]) @@ -250,9 +261,9 @@ def _get_model_device(model): :return: torch.device,None 如果返回值为None,说明这个模型没有任何参数。 """ assert isinstance(model, nn.Module) - + parameters = list(model.parameters()) - if len(parameters)==0: + if len(parameters) == 0: return None else: return parameters[0].device @@ -407,7 +418,7 @@ def _move_dict_value_to_device(*args, device: torch.device, non_blocking=False): if not isinstance(device, torch.device): raise TypeError(f"device must be `torch.device`, got `{type(device)}`") - + for arg in args: if isinstance(arg, dict): for key, value in arg.items(): @@ -422,10 +433,10 @@ class _CheckError(Exception): _CheckError. Used in losses.LossBase, metrics.MetricBase. """ - + def __init__(self, check_res: _CheckRes, func_signature: str): errs = [f'Problems occurred when calling `{func_signature}`'] - + if check_res.varargs: errs.append(f"\tvarargs: {check_res.varargs}(Does not support pass positional arguments, please delete it)") if check_res.missing: @@ -434,9 +445,9 @@ class _CheckError(Exception): errs.append(f"\tduplicated param: {check_res.duplicated}") if check_res.unused: errs.append(f"\tunused param: {check_res.unused}") - + Exception.__init__(self, '\n'.join(errs)) - + self.check_res = check_res self.func_signature = func_signature @@ -456,7 +467,7 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re # if check_res.varargs: # errs.append(f"\tvarargs: *{check_res.varargs}") # suggestions.append(f"Does not support pass positional arguments, please delete *{check_res.varargs}.") - + if check_res.unused: for _unused in check_res.unused: if _unused in target_dict: @@ -466,8 +477,8 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re if _unused_field: unuseds.append(f"\tunused field: {_unused_field}") if _unused_param: - unuseds.append(f"\tunused param: {_unused_param}") # output from predict or forward - + unuseds.append(f"\tunused param: {_unused_param}") # output from predict or forward + module_name = func_signature.split('.')[0] if check_res.missing: errs.append(f"\tmissing param: {check_res.missing}") @@ -488,14 +499,14 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re mapped_missing.append(_miss) else: unmapped_missing.append(_miss) - + for _miss in mapped_missing + unmapped_missing: if _miss in dataset: suggestions.append(f"Set `{_miss}` as target.") else: _tmp = '' if check_res.unused: - _tmp = f"Check key assignment for `{input_func_map.get(_miss, _miss)}` when initialize {module_name}." + _tmp = f"Check key assignment for `{input_func_map.get(_miss,_miss)}` when initialize {module_name}." if _tmp: _tmp += f' Or provide `{_miss}` in DataSet or output of {prev_func_signature}.' else: @@ -513,25 +524,25 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re # else: # _tmp = f'Provide `{_miss}` in output of {prev_func_signature} or DataSet.' # suggestions.append(_tmp) - + if check_res.duplicated: errs.append(f"\tduplicated param: {check_res.duplicated}.") suggestions.append(f"Delete {check_res.duplicated} in the output of " f"{prev_func_signature} or do not set {check_res.duplicated} as targets. ") - - if len(errs)>0: + + if len(errs) > 0: errs.extend(unuseds) elif check_level == STRICT_CHECK_LEVEL: errs.extend(unuseds) - + if len(errs) > 0: errs.insert(0, f'Problems occurred when calling {func_signature}') sugg_str = "" if len(suggestions) > 1: for idx, sugg in enumerate(suggestions): - if idx>0: + if idx > 0: sugg_str += '\t\t\t' - sugg_str += f'({idx+1}). {sugg}\n' + sugg_str += f'({idx + 1}). {sugg}\n' sugg_str = sugg_str[:-1] else: sugg_str += suggestions[0] @@ -546,14 +557,15 @@ def _check_loss_evaluate(prev_func_signature: str, func_signature: str, check_re _unused_warn = f'{check_res.unused} is not used by {module_name}.' warnings.warn(message=_unused_warn) + def _check_forward_error(forward_func, batch_x, dataset, check_level): check_res = _check_arg_dict_list(forward_func, batch_x) func_signature = _get_func_signature(forward_func) - + errs = [] suggestions = [] _unused = [] - + # if check_res.varargs: # errs.append(f"\tvarargs: {check_res.varargs}") # suggestions.append(f"Does not support pass positional arguments, please delete *{check_res.varargs}.") @@ -574,20 +586,20 @@ def _check_forward_error(forward_func, batch_x, dataset, check_level): # _tmp += f"Or you might find it in `unused field:`, you can use DataSet.rename_field() to " \ # f"rename the field in `unused field:`." suggestions.append(_tmp) - + if check_res.unused: _unused = [f"\tunused field: {check_res.unused}"] - if len(errs)>0: + if len(errs) > 0: errs.extend(_unused) elif check_level == STRICT_CHECK_LEVEL: errs.extend(_unused) - + if len(errs) > 0: errs.insert(0, f'Problems occurred when calling {func_signature}') sugg_str = "" if len(suggestions) > 1: for idx, sugg in enumerate(suggestions): - sugg_str += f'({idx+1}). {sugg}' + sugg_str += f'({idx + 1}). {sugg}' else: sugg_str += suggestions[0] err_str = '\n' + '\n'.join(errs) + '\n\tSuggestion: ' + sugg_str @@ -622,8 +634,8 @@ def seq_len_to_mask(seq_len): assert len(np.shape(seq_len)) == 1, f"seq_len can only have one dimension, got {len(np.shape(seq_len))}." max_len = int(seq_len.max()) broad_cast_seq_len = np.tile(np.arange(max_len), (len(seq_len), 1)) - mask = broad_cast_seq_len Date: Thu, 16 May 2019 20:32:10 +0800 Subject: [PATCH 135/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20models=20?= =?UTF-8?q?=E9=83=A8=E5=88=86=20import=20=E7=9A=84=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=EF=BC=8C=5F=5Fall=5F=5F=20=E6=9A=B4=E9=9C=B2=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/fastNLP.models.base_model.rst | 7 - docs/source/fastNLP.models.bert.rst | 7 - .../source/fastNLP.models.enas_controller.rst | 7 - docs/source/fastNLP.models.enas_model.rst | 7 - docs/source/fastNLP.models.enas_trainer.rst | 7 - docs/source/fastNLP.models.enas_utils.rst | 7 - docs/source/fastNLP.models.rst | 6 - fastNLP/core/batch.py | 4 +- fastNLP/core/callback.py | 1 + fastNLP/core/dataset.py | 3 +- fastNLP/core/field.py | 4 +- fastNLP/core/losses.py | 4 +- fastNLP/core/metrics.py | 4 +- fastNLP/core/predictor.py | 4 +- fastNLP/core/sampler.py | 4 +- fastNLP/core/tester.py | 1 + fastNLP/core/trainer.py | 6 +- fastNLP/core/utils.py | 9 +- fastNLP/io/__init__.py | 9 +- fastNLP/io/base_loader.py | 19 ++- fastNLP/io/config_io.py | 64 ++++--- fastNLP/io/dataset_loader.py | 11 +- fastNLP/io/embed_loader.py | 56 +++--- fastNLP/io/model_io.py | 15 +- fastNLP/models/__init__.py | 20 ++- fastNLP/models/base_model.py | 10 +- fastNLP/models/biaffine_parser.py | 159 ++++++++++-------- fastNLP/models/cnn_text_classification.py | 17 +- fastNLP/models/enas_controller.py | 1 + fastNLP/models/enas_model.py | 139 +++++++-------- fastNLP/models/enas_trainer.py | 141 ++++++++-------- fastNLP/models/enas_utils.py | 2 - fastNLP/models/sequence_labeling.py | 10 +- fastNLP/models/snli.py | 61 +++---- fastNLP/models/star_transformer.py | 67 +++++--- 35 files changed, 465 insertions(+), 428 deletions(-) delete mode 100644 docs/source/fastNLP.models.base_model.rst delete mode 100644 docs/source/fastNLP.models.bert.rst delete mode 100644 docs/source/fastNLP.models.enas_controller.rst delete mode 100644 docs/source/fastNLP.models.enas_model.rst delete mode 100644 docs/source/fastNLP.models.enas_trainer.rst delete mode 100644 docs/source/fastNLP.models.enas_utils.rst diff --git a/docs/source/fastNLP.models.base_model.rst b/docs/source/fastNLP.models.base_model.rst deleted file mode 100644 index e1d4d64f..00000000 --- a/docs/source/fastNLP.models.base_model.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.base\_model -========================== - -.. automodule:: fastNLP.models.base_model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.bert.rst b/docs/source/fastNLP.models.bert.rst deleted file mode 100644 index bba323df..00000000 --- a/docs/source/fastNLP.models.bert.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.bert -=================== - -.. automodule:: fastNLP.models.bert - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_controller.rst b/docs/source/fastNLP.models.enas_controller.rst deleted file mode 100644 index 28655bd7..00000000 --- a/docs/source/fastNLP.models.enas_controller.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.enas\_controller -=============================== - -.. automodule:: fastNLP.models.enas_controller - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_model.rst b/docs/source/fastNLP.models.enas_model.rst deleted file mode 100644 index 35fbe495..00000000 --- a/docs/source/fastNLP.models.enas_model.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.enas\_model -========================== - -.. automodule:: fastNLP.models.enas_model - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_trainer.rst b/docs/source/fastNLP.models.enas_trainer.rst deleted file mode 100644 index 7e0ef462..00000000 --- a/docs/source/fastNLP.models.enas_trainer.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.enas\_trainer -============================ - -.. automodule:: fastNLP.models.enas_trainer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.enas_utils.rst b/docs/source/fastNLP.models.enas_utils.rst deleted file mode 100644 index 0a049706..00000000 --- a/docs/source/fastNLP.models.enas_utils.rst +++ /dev/null @@ -1,7 +0,0 @@ -fastNLP.models.enas\_utils -========================== - -.. automodule:: fastNLP.models.enas_utils - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/fastNLP.models.rst b/docs/source/fastNLP.models.rst index 57592bf4..5858ebcd 100644 --- a/docs/source/fastNLP.models.rst +++ b/docs/source/fastNLP.models.rst @@ -12,14 +12,8 @@ fastNLP.models .. toctree:: :titlesonly: - fastNLP.models.base_model - fastNLP.models.bert fastNLP.models.biaffine_parser fastNLP.models.cnn_text_classification - fastNLP.models.enas_controller - fastNLP.models.enas_model - fastNLP.models.enas_trainer - fastNLP.models.enas_utils fastNLP.models.sequence_labeling fastNLP.models.snli fastNLP.models.star_transformer diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index 90f0fc8c..b031d051 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -3,12 +3,12 @@ batch 模块实现了 fastNLP 所需的 Batch 类。 """ import atexit +from queue import Empty, Full + import numpy as np import torch import torch.multiprocessing as mp -from queue import Empty, Full - from .sampler import RandomSampler __all__ = [ diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 0a5ddc52..51495f23 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -50,6 +50,7 @@ callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class: """ import os + import torch try: diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 63f66019..f20dd1f8 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -273,9 +273,10 @@ """ import _pickle as pickle -import numpy as np import warnings +import numpy as np + from .field import AutoPadder from .field import FieldArray from .instance import Instance diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index 4029a4ca..14e2538d 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -3,10 +3,10 @@ field模块实现了 FieldArray 和若干 Padder。 FieldArray 是 :class:`~fas 原理部分请参考 :doc:`fastNLP.core.dataset` """ -import numpy as np - from copy import deepcopy +import numpy as np + __all__ = [ "FieldArray", "Padder", diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index b98c5ac7..797b557d 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -3,11 +3,11 @@ losses 模块定义了 fastNLP 中所需的各种损失函数,一般做为 :cl """ import inspect +from collections import defaultdict + import torch import torch.nn.functional as F -from collections import defaultdict - from .utils import _CheckError from .utils import _CheckRes from .utils import _build_args diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index df85a318..5ea2a5f1 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -3,11 +3,11 @@ metrics 模块实现了 fastNLP 所需的各种常用衡量指标,一般做为 """ import inspect +from collections import defaultdict + import numpy as np import torch -from collections import defaultdict - from .utils import _CheckError from .utils import _CheckRes from .utils import _build_args diff --git a/fastNLP/core/predictor.py b/fastNLP/core/predictor.py index a9ef7924..4f37e105 100644 --- a/fastNLP/core/predictor.py +++ b/fastNLP/core/predictor.py @@ -2,10 +2,10 @@ ..todo:: 检查这个类是否需要 """ -import torch - from collections import defaultdict +import torch + from . import Batch from . import DataSet from . import SequentialSampler diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index 0900e733..c8577722 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -1,10 +1,10 @@ """ sampler 子类实现了 fastNLP 所需的各种采样器。 """ -import numpy as np - from itertools import chain +import numpy as np + __all__ = [ "Sampler", "BucketSampler", diff --git a/fastNLP/core/tester.py b/fastNLP/core/tester.py index 47aef46e..883e0d01 100644 --- a/fastNLP/core/tester.py +++ b/fastNLP/core/tester.py @@ -33,6 +33,7 @@ Tester在验证进行之前会调用model.eval()提示当前进入了evaluation """ import warnings + import torch import torch.nn as nn diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 87d57f12..7efa5d28 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -297,13 +297,13 @@ Example2.3 """ import os -import numpy as np import time +from datetime import datetime, timedelta + +import numpy as np import torch import torch.nn as nn -from datetime import datetime, timedelta - try: from tqdm.auto import tqdm except: diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index a7ad3326..6e2f99ff 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -3,14 +3,13 @@ utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户 """ import _pickle import inspect -import numpy as np import os -import torch -import torch.nn as nn import warnings +from collections import Counter, namedtuple -from collections import Counter -from collections import namedtuple +import numpy as np +import torch +import torch.nn as nn __all__ = [ "cache_results", diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index 3baf878c..6ce7ebc3 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -9,6 +9,11 @@ 这些类的使用方法如下: """ +from .embed_loader import EmbedLoader +from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ + PeopleDailyCorpusLoader, Conll2003Loader +from .model_io import ModelLoader, ModelSaver + __all__ = [ 'EmbedLoader', @@ -24,7 +29,3 @@ __all__ = [ 'ModelLoader', 'ModelSaver', ] -from .embed_loader import EmbedLoader -from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ - PeopleDailyCorpusLoader, Conll2003Loader -from .model_io import ModelLoader as ModelLoader, ModelSaver as ModelSaver \ No newline at end of file diff --git a/fastNLP/io/base_loader.py b/fastNLP/io/base_loader.py index 051de281..33f59fe5 100644 --- a/fastNLP/io/base_loader.py +++ b/fastNLP/io/base_loader.py @@ -1,15 +1,20 @@ import _pickle as pickle import os +__all__ = [ + "BaseLoader" +] + class BaseLoader(object): """ 各个 Loader 的基类,提供了 API 的参考。 """ + def __init__(self): super(BaseLoader, self).__init__() - + @staticmethod def load_lines(data_path): """ @@ -20,7 +25,7 @@ class BaseLoader(object): with open(data_path, "r", encoding="utf=8") as f: text = f.readlines() return [line.strip() for line in text] - + @classmethod def load(cls, data_path): """ @@ -31,7 +36,7 @@ class BaseLoader(object): with open(data_path, "r", encoding="utf-8") as f: text = f.readlines() return [[word for word in sent.strip()] for sent in text] - + @classmethod def load_with_cache(cls, data_path, cache_path): """缓存版的load @@ -48,16 +53,18 @@ class BaseLoader(object): class DataLoaderRegister: _readers = {} - + @classmethod def set_reader(cls, reader_cls, read_fn_name): # def wrapper(reader_cls): if read_fn_name in cls._readers: - raise KeyError('duplicate reader: {} and {} for read_func: {}'.format(cls._readers[read_fn_name], reader_cls, read_fn_name)) + raise KeyError( + 'duplicate reader: {} and {} for read_func: {}'.format(cls._readers[read_fn_name], reader_cls, + read_fn_name)) if hasattr(reader_cls, 'load'): cls._readers[read_fn_name] = reader_cls().load return reader_cls - + @classmethod def get_reader(cls, read_fn_name): if read_fn_name in cls._readers: diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index 8fa30dd4..e67511ee 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -1,14 +1,20 @@ """ - 用于读入和处理和保存 config 文件 + .. todo:: + 这个模块中的类可能被抛弃? """ -__all__ = ["ConfigLoader","ConfigSection","ConfigSaver"] import configparser import json import os from .base_loader import BaseLoader +__all__ = [ + "ConfigLoader", + "ConfigSection", + "ConfigSaver" +] + class ConfigLoader(BaseLoader): """ @@ -19,15 +25,16 @@ class ConfigLoader(BaseLoader): :param str data_path: 配置文件的路径 """ + def __init__(self, data_path=None): super(ConfigLoader, self).__init__() if data_path is not None: self.config = self.parse(super(ConfigLoader, self).load(data_path)) - + @staticmethod def parse(string): raise NotImplementedError - + @staticmethod def load_config(file_path, sections): """ @@ -81,10 +88,10 @@ class ConfigSection(object): ConfigSection是一个存储了一个section中所有键值对的数据结构,推荐使用此类的实例来配合 :meth:`ConfigLoader.load_config` 使用 """ - + def __init__(self): super(ConfigSection, self).__init__() - + def __getitem__(self, key): """ :param key: str, the name of the attribute @@ -97,7 +104,7 @@ class ConfigSection(object): if key in self.__dict__.keys(): return getattr(self, key) raise AttributeError("do NOT have attribute %s" % key) - + def __setitem__(self, key, value): """ :param key: str, the name of the attribute @@ -112,14 +119,14 @@ class ConfigSection(object): raise AttributeError("attr %s except %s but got %s" % (key, str(type(getattr(self, key))), str(type(value)))) setattr(self, key, value) - + def __contains__(self, item): """ :param item: The key of item. :return: True if the key in self.__dict__.keys() else False. """ return item in self.__dict__.keys() - + def __eq__(self, other): """Overwrite the == operator @@ -131,15 +138,15 @@ class ConfigSection(object): return False if getattr(self, k) != getattr(self, k): return False - + for k in other.__dict__.keys(): if k not in self.__dict__.keys(): return False if getattr(self, k) != getattr(self, k): return False - + return True - + def __ne__(self, other): """Overwrite the != operator @@ -147,7 +154,7 @@ class ConfigSection(object): :return: """ return not self.__eq__(other) - + @property def data(self): return self.__dict__ @@ -162,11 +169,12 @@ class ConfigSaver(object): :param str file_path: 配置文件的路径 """ + def __init__(self, file_path): self.file_path = file_path if not os.path.exists(self.file_path): raise FileNotFoundError("file {} NOT found!".__format__(self.file_path)) - + def _get_section(self, sect_name): """ This is the function to get the section with the section name. @@ -177,7 +185,7 @@ class ConfigSaver(object): sect = ConfigSection() ConfigLoader().load_config(self.file_path, {sect_name: sect}) return sect - + def _read_section(self): """ This is the function to read sections from the config file. @@ -187,16 +195,16 @@ class ConfigSaver(object): sect_key_list: A list of names in sect_list. """ sect_name = None - + sect_list = {} sect_key_list = [] - + single_section = {} single_section_key = [] - + with open(self.file_path, 'r') as f: lines = f.readlines() - + for line in lines: if line.startswith('[') and line.endswith(']\n'): if sect_name is None: @@ -208,29 +216,29 @@ class ConfigSaver(object): sect_key_list.append(sect_name) sect_name = line[1: -2] continue - + if line.startswith('#'): single_section[line] = '#' single_section_key.append(line) continue - + if line.startswith('\n'): single_section_key.append('\n') continue - + if '=' not in line: raise RuntimeError("can NOT load config file {}".__format__(self.file_path)) - + key = line.split('=', maxsplit=1)[0].strip() value = line.split('=', maxsplit=1)[1].strip() + '\n' single_section[key] = value single_section_key.append(key) - + if sect_name is not None: sect_list[sect_name] = single_section, single_section_key sect_key_list.append(sect_name) return sect_list, sect_key_list - + def _write_section(self, sect_list, sect_key_list): """ This is the function to write config file with section list and name list. @@ -252,7 +260,7 @@ class ConfigSaver(object): continue f.write(key + ' = ' + single_section[key]) f.write('\n') - + def save_config_file(self, section_name, section): """ 这个方法可以用来修改并保存配置文件中单独的一个 section @@ -284,11 +292,11 @@ class ConfigSaver(object): break if not change_file: return - + sect_list, sect_key_list = self._read_section() if section_name not in sect_key_list: raise AttributeError() - + sect, sect_key = sect_list[section_name] for k in section.__dict__.keys(): if k not in sect_key: diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 3cd475a5..a4b233ad 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -10,6 +10,12 @@ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的 # ... do stuff """ +from nltk.tree import Tree + +from ..core.dataset import DataSet +from ..core.instance import Instance +from .file_reader import _read_csv, _read_json, _read_conll + __all__ = [ 'DataSetLoader', 'CSVLoader', @@ -20,11 +26,6 @@ __all__ = [ 'PeopleDailyCorpusLoader', 'Conll2003Loader', ] -from nltk.tree import Tree - -from ..core.dataset import DataSet -from ..core.instance import Instance -from .file_reader import _read_csv, _read_json, _read_conll def _download_from_url(url, path): diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 9f3a73dd..7a845366 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,11 +1,15 @@ import os +import warnings import numpy as np from ..core.vocabulary import Vocabulary from .base_loader import BaseLoader -import warnings +__all__ = [ + "EmbedLoader" +] + class EmbedLoader(BaseLoader): """ @@ -13,10 +17,10 @@ class EmbedLoader(BaseLoader): 用于读取预训练的embedding, 读取结果可直接载入为模型参数。 """ - + def __init__(self): super(EmbedLoader, self).__init__() - + @staticmethod def load_with_vocab(embed_filepath, vocab, dtype=np.float32, normalize=True, error='ignore'): """ @@ -40,11 +44,11 @@ class EmbedLoader(BaseLoader): line = f.readline().strip() parts = line.split() start_idx = 0 - if len(parts)==2: + if len(parts) == 2: dim = int(parts[1]) start_idx += 1 else: - dim = len(parts)-1 + dim = len(parts) - 1 f.seek(0) matrix = np.random.randn(len(vocab), dim).astype(dtype) for idx, line in enumerate(f, start_idx): @@ -63,21 +67,21 @@ class EmbedLoader(BaseLoader): total_hits = sum(hit_flags) print("Found {} out of {} words in the pre-training embedding.".format(total_hits, len(vocab))) found_vectors = matrix[hit_flags] - if len(found_vectors)!=0: + if len(found_vectors) != 0: mean = np.mean(found_vectors, axis=0, keepdims=True) std = np.std(found_vectors, axis=0, keepdims=True) unfound_vec_num = len(vocab) - total_hits - r_vecs = np.random.randn(unfound_vec_num, dim).astype(dtype)*std + mean - matrix[hit_flags==False] = r_vecs - + r_vecs = np.random.randn(unfound_vec_num, dim).astype(dtype) * std + mean + matrix[hit_flags == False] = r_vecs + if normalize: matrix /= np.linalg.norm(matrix, axis=1, keepdims=True) - + return matrix - + @staticmethod def load_without_vocab(embed_filepath, dtype=np.float32, padding='', unknown='', normalize=True, - error='ignore'): + error='ignore'): """ 从embed_filepath中读取预训练的word vector。根据预训练的词表读取embedding并生成一个对应的Vocabulary。 @@ -96,35 +100,35 @@ class EmbedLoader(BaseLoader): vec_dict = {} found_unknown = False found_pad = False - + with open(embed_filepath, 'r', encoding='utf-8') as f: line = f.readline() start = 1 dim = -1 - if len(line.strip().split())!=2: + if len(line.strip().split()) != 2: f.seek(0) start = 0 for idx, line in enumerate(f, start=start): try: parts = line.strip().split() word = parts[0] - if dim==-1: - dim = len(parts)-1 + if dim == -1: + dim = len(parts) - 1 vec = np.fromstring(' '.join(parts[1:]), sep=' ', dtype=dtype, count=dim) vec_dict[word] = vec vocab.add_word(word) - if unknown is not None and unknown==word: + if unknown is not None and unknown == word: found_unknown = True - if found_pad is not None and padding==word: + if found_pad is not None and padding == word: found_pad = True except Exception as e: - if error=='ignore': + if error == 'ignore': warnings.warn("Error occurred at the {} line.".format(idx)) pass else: print("Error occurred at the {} line.".format(idx)) raise e - if dim==-1: + if dim == -1: raise RuntimeError("{} is an empty file.".format(embed_filepath)) matrix = np.random.randn(len(vocab), dim).astype(dtype) if (unknown is not None and not found_unknown) or (padding is not None and not found_pad): @@ -133,19 +137,19 @@ class EmbedLoader(BaseLoader): start_idx += 1 if unknown is not None: start_idx += 1 - + mean = np.mean(matrix[start_idx:], axis=0, keepdims=True) std = np.std(matrix[start_idx:], axis=0, keepdims=True) if (unknown is not None and not found_unknown): - matrix[start_idx-1] = np.random.randn(1, dim).astype(dtype)*std + mean + matrix[start_idx - 1] = np.random.randn(1, dim).astype(dtype) * std + mean if (padding is not None and not found_pad): - matrix[0] = np.random.randn(1, dim).astype(dtype)*std + mean - + matrix[0] = np.random.randn(1, dim).astype(dtype) * std + mean + for key, vec in vec_dict.items(): index = vocab.to_index(key) matrix[index] = vec - + if normalize: matrix /= np.linalg.norm(matrix, axis=1, keepdims=True) - + return matrix, vocab diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index 48e53ab3..36393cd4 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -5,6 +5,11 @@ import torch from .base_loader import BaseLoader +__all__ = [ + "ModelLoader", + "ModelSaver" +] + class ModelLoader(BaseLoader): """ @@ -12,10 +17,10 @@ class ModelLoader(BaseLoader): 用于读取模型 """ - + def __init__(self): super(ModelLoader, self).__init__() - + @staticmethod def load_pytorch(empty_model, model_path): """ @@ -25,7 +30,7 @@ class ModelLoader(BaseLoader): :param str model_path: 模型保存的路径 """ empty_model.load_state_dict(torch.load(model_path)) - + @staticmethod def load_pytorch_model(model_path): """ @@ -48,14 +53,14 @@ class ModelSaver(object): saver.save_pytorch(model) """ - + def __init__(self, save_path): """ :param save_path: 模型保存的路径 """ self.save_path = save_path - + def save_pytorch(self, model, param_only=True): """ 把 PyTorch 模型存入 ".pkl" 文件 diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index 66af3a46..f9ade153 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -7,7 +7,6 @@ fastNLP 在 :mod:`~fastNLP.models` 模块中内置了如 :class:`~fastNLP.models """ -__all__ = ["CNNText", "SeqLabeling", "ESIM", "STSeqLabel", "AdvSeqLabel", "STNLICls", "STSeqCls"] from .base_model import BaseModel from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequenceClassification, \ BertForTokenClassification @@ -15,4 +14,21 @@ from .biaffine_parser import BiaffineParser, GraphParser from .cnn_text_classification import CNNText from .sequence_labeling import SeqLabeling, AdvSeqLabel from .snli import ESIM -from .star_transformer import STSeqCls, STNLICls, STSeqLabel +from .star_transformer import StarTransEnc, STSeqCls, STNLICls, STSeqLabel + +__all__ = [ + "CNNText", + + "SeqLabeling", + "AdvSeqLabel", + + "ESIM", + + "StarTransEnc", + "STSeqLabel", + "STNLICls", + "STSeqCls", + + "BiaffineParser", + "GraphParser" +] diff --git a/fastNLP/models/base_model.py b/fastNLP/models/base_model.py index 39ac99a0..d27f1d21 100644 --- a/fastNLP/models/base_model.py +++ b/fastNLP/models/base_model.py @@ -6,13 +6,13 @@ from ..modules.decoder.MLP import MLP class BaseModel(torch.nn.Module): """Base PyTorch model for all models. """ - + def __init__(self): super(BaseModel, self).__init__() - + def fit(self, train_data, dev_data=None, **train_args): pass - + def predict(self, *args, **kwargs): raise NotImplementedError @@ -21,9 +21,9 @@ class NaiveClassifier(BaseModel): def __init__(self, in_feature_dim, out_feature_dim): super(NaiveClassifier, self).__init__() self.mlp = MLP([in_feature_dim, in_feature_dim, out_feature_dim]) - + def forward(self, x): return {"predict": torch.sigmoid(self.mlp(x))} - + def predict(self, x): return {"predict": torch.sigmoid(self.mlp(x)) > 0.5} diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 100bfb72..7f16202d 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -1,11 +1,12 @@ -"""Biaffine Dependency Parser 的 Pytorch 实现. """ -from collections import defaultdict - +Biaffine Dependency Parser 的 Pytorch 实现. +""" import numpy as np import torch -from torch import nn -from torch.nn import functional as F +import torch.nn as nn +import torch.nn.functional as F + +from collections import defaultdict from ..core.const import Const as C from ..core.losses import LossFunc @@ -18,6 +19,12 @@ from ..modules.utils import get_embeddings from .base_model import BaseModel from ..core.utils import seq_len_to_mask +__all__ = [ + "BiaffineParser", + "GraphParser" +] + + def _mst(scores): """ with some modification to support parser output for MST decoding @@ -44,7 +51,7 @@ def _mst(scores): scores[roots, new_heads] / root_scores)] heads[roots] = new_heads heads[new_root] = 0 - + edges = defaultdict(set) vertices = set((0,)) for dep, head in enumerate(heads[tokens]): @@ -73,7 +80,7 @@ def _mst(scores): heads[changed_cycle] = new_head edges[new_head].add(changed_cycle) edges[old_head].remove(changed_cycle) - + return heads @@ -88,7 +95,7 @@ def _find_cycle(vertices, edges): _lowlinks = {} _onstack = defaultdict(lambda: False) _SCCs = [] - + def _strongconnect(v): nonlocal _index _indices[v] = _index @@ -96,28 +103,28 @@ def _find_cycle(vertices, edges): _index += 1 _stack.append(v) _onstack[v] = True - + for w in edges[v]: if w not in _indices: _strongconnect(w) _lowlinks[v] = min(_lowlinks[v], _lowlinks[w]) elif _onstack[w]: _lowlinks[v] = min(_lowlinks[v], _indices[w]) - + if _lowlinks[v] == _indices[v]: SCC = set() while True: w = _stack.pop() _onstack[w] = False SCC.add(w) - if not(w != v): + if not (w != v): break _SCCs.append(SCC) - + for v in vertices: if v not in _indices: _strongconnect(v) - + return [SCC for SCC in _SCCs if len(SCC) > 1] @@ -125,9 +132,10 @@ class GraphParser(BaseModel): """ 基于图的parser base class, 支持贪婪解码和最大生成树解码 """ + def __init__(self): super(GraphParser, self).__init__() - + @staticmethod def greedy_decoder(arc_matrix, mask=None): """ @@ -146,7 +154,7 @@ class GraphParser(BaseModel): if mask is not None: heads *= mask.long() return heads - + @staticmethod def mst_decoder(arc_matrix, mask=None): """ @@ -176,6 +184,7 @@ class ArcBiaffine(nn.Module): :param hidden_size: 输入的特征维度 :param bias: 是否使用bias. Default: ``True`` """ + def __init__(self, hidden_size, bias=True): super(ArcBiaffine, self).__init__() self.U = nn.Parameter(torch.Tensor(hidden_size, hidden_size), requires_grad=True) @@ -185,7 +194,7 @@ class ArcBiaffine(nn.Module): else: self.register_parameter("bias", None) initial_parameter(self) - + def forward(self, head, dep): """ @@ -209,11 +218,12 @@ class LabelBilinear(nn.Module): :param num_label: 边类别的个数 :param bias: 是否使用bias. Default: ``True`` """ + def __init__(self, in1_features, in2_features, num_label, bias=True): super(LabelBilinear, self).__init__() self.bilinear = nn.Bilinear(in1_features, in2_features, num_label, bias=bias) self.lin = nn.Linear(in1_features + in2_features, num_label, bias=False) - + def forward(self, x1, x2): """ @@ -225,13 +235,13 @@ class LabelBilinear(nn.Module): output += self.lin(torch.cat([x1, x2], dim=2)) return output + class BiaffineParser(GraphParser): """ 别名::class:`fastNLP.models.BiaffineParser` :class:`fastNLP.models.baffine_parser.BiaffineParser` Biaffine Dependency Parser 实现. - 论文参考 ` Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) - `_ . + 论文参考 `Deep Biaffine Attention for Neural Dependency Parsing (Dozat and Manning, 2016) `_ . :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, @@ -248,18 +258,19 @@ class BiaffineParser(GraphParser): :param use_greedy_infer: 是否在inference时使用贪心算法. 若 ``False`` , 使用更加精确但相对缓慢的MST算法. Default: ``False`` """ + def __init__(self, - init_embed, - pos_vocab_size, - pos_emb_dim, - num_label, - rnn_layers=1, - rnn_hidden_size=200, - arc_mlp_size=100, - label_mlp_size=100, - dropout=0.3, - encoder='lstm', - use_greedy_infer=False): + init_embed, + pos_vocab_size, + pos_emb_dim, + num_label, + rnn_layers=1, + rnn_hidden_size=200, + arc_mlp_size=100, + label_mlp_size=100, + dropout=0.3, + encoder='lstm', + use_greedy_infer=False): super(BiaffineParser, self).__init__() rnn_out_size = 2 * rnn_hidden_size word_hid_dim = pos_hid_dim = rnn_hidden_size @@ -295,20 +306,20 @@ class BiaffineParser(GraphParser): if (d_k * n_head) != rnn_out_size: raise ValueError('unsupported rnn_out_size: {} for transformer'.format(rnn_out_size)) self.position_emb = nn.Embedding(num_embeddings=self.max_len, - embedding_dim=rnn_out_size,) + embedding_dim=rnn_out_size, ) self.encoder = TransformerEncoder(num_layers=rnn_layers, model_size=rnn_out_size, inner_size=1024, key_size=d_k, value_size=d_v, num_head=n_head, - dropout=dropout,) + dropout=dropout, ) else: raise ValueError('unsupported encoder type: {}'.format(encoder)) - + self.mlp = nn.Sequential(nn.Linear(rnn_out_size, arc_mlp_size * 2 + label_mlp_size * 2), - nn.ELU(), - TimestepDropout(p=dropout),) + nn.ELU(), + TimestepDropout(p=dropout), ) self.arc_mlp_size = arc_mlp_size self.label_mlp_size = label_mlp_size self.arc_predictor = ArcBiaffine(arc_mlp_size, bias=True) @@ -316,7 +327,7 @@ class BiaffineParser(GraphParser): self.use_greedy_infer = use_greedy_infer self.reset_parameters() self.dropout = dropout - + def reset_parameters(self): for m in self.modules(): if isinstance(m, nn.Embedding): @@ -327,7 +338,7 @@ class BiaffineParser(GraphParser): else: for p in m.parameters(): nn.init.normal_(p, 0, 0.1) - + def forward(self, words1, words2, seq_len, target1=None): """模型forward阶段 @@ -337,50 +348,52 @@ class BiaffineParser(GraphParser): :param target1: [batch_size, seq_len] 输入真实标注的heads, 仅在训练阶段有效, 用于训练label分类器. 若为 ``None`` , 使用预测的heads输入到label分类器 Default: ``None`` - :return dict: parsing结果:: + :return dict: parsing + 结果:: + + pred1: [batch_size, seq_len, seq_len] 边预测logits + pred2: [batch_size, seq_len, num_label] label预测logits + pred3: [batch_size, seq_len] heads的预测结果, 在 ``target1=None`` 时预测 - pred1: [batch_size, seq_len, seq_len] 边预测logits - pred2: [batch_size, seq_len, num_label] label预测logits - pred3: [batch_size, seq_len] heads的预测结果, 在 ``target1=None`` 时预测 """ # prepare embeddings batch_size, length = words1.shape # print('forward {} {}'.format(batch_size, seq_len)) - + # get sequence mask mask = seq_len_to_mask(seq_len).long() - - word = self.word_embedding(words1) # [N,L] -> [N,L,C_0] - pos = self.pos_embedding(words2) # [N,L] -> [N,L,C_1] - + + word = self.word_embedding(words1) # [N,L] -> [N,L,C_0] + pos = self.pos_embedding(words2) # [N,L] -> [N,L,C_1] + word, pos = self.word_fc(word), self.pos_fc(pos) word, pos = self.word_norm(word), self.pos_norm(pos) - x = torch.cat([word, pos], dim=2) # -> [N,L,C] - + x = torch.cat([word, pos], dim=2) # -> [N,L,C] + # encoder, extract features if self.encoder_name.endswith('lstm'): sort_lens, sort_idx = torch.sort(seq_len, dim=0, descending=True) x = x[sort_idx] x = nn.utils.rnn.pack_padded_sequence(x, sort_lens, batch_first=True) - feat, _ = self.encoder(x) # -> [N,L,C] + feat, _ = self.encoder(x) # -> [N,L,C] feat, _ = nn.utils.rnn.pad_packed_sequence(feat, batch_first=True) _, unsort_idx = torch.sort(sort_idx, dim=0, descending=False) feat = feat[unsort_idx] else: - seq_range = torch.arange(length, dtype=torch.long, device=x.device)[None,:] + seq_range = torch.arange(length, dtype=torch.long, device=x.device)[None, :] x = x + self.position_emb(seq_range) feat = self.encoder(x, mask.float()) - + # for arc biaffine # mlp, reduce dim feat = self.mlp(feat) arc_sz, label_sz = self.arc_mlp_size, self.label_mlp_size - arc_dep, arc_head = feat[:,:,:arc_sz], feat[:,:,arc_sz:2*arc_sz] - label_dep, label_head = feat[:,:,2*arc_sz:2*arc_sz+label_sz], feat[:,:,2*arc_sz+label_sz:] - + arc_dep, arc_head = feat[:, :, :arc_sz], feat[:, :, arc_sz:2 * arc_sz] + label_dep, label_head = feat[:, :, 2 * arc_sz:2 * arc_sz + label_sz], feat[:, :, 2 * arc_sz + label_sz:] + # biaffine arc classifier - arc_pred = self.arc_predictor(arc_head, arc_dep) # [N, L, L] - + arc_pred = self.arc_predictor(arc_head, arc_dep) # [N, L, L] + # use gold or predicted arc to predict label if target1 is None or not self.training: # use greedy decoding in training @@ -390,22 +403,22 @@ class BiaffineParser(GraphParser): heads = self.mst_decoder(arc_pred, mask) head_pred = heads else: - assert self.training # must be training mode + assert self.training # must be training mode if target1 is None: heads = self.greedy_decoder(arc_pred, mask) head_pred = heads else: head_pred = None heads = target1 - + batch_range = torch.arange(start=0, end=batch_size, dtype=torch.long, device=words1.device).unsqueeze(1) label_head = label_head[batch_range, heads].contiguous() - label_pred = self.label_predictor(label_head, label_dep) # [N, L, num_label] + label_pred = self.label_predictor(label_head, label_dep) # [N, L, num_label] res_dict = {C.OUTPUTS(0): arc_pred, C.OUTPUTS(1): label_pred} if head_pred is not None: res_dict[C.OUTPUTS(2)] = head_pred return res_dict - + @staticmethod def loss(pred1, pred2, target1, target2, seq_len): """ @@ -418,7 +431,7 @@ class BiaffineParser(GraphParser): :param seq_len: [batch_size, seq_len] 真实目标的长度 :return loss: scalar """ - + batch_size, length, _ = pred1.shape mask = seq_len_to_mask(seq_len) flip_mask = (mask == 0) @@ -430,24 +443,26 @@ class BiaffineParser(GraphParser): child_index = torch.arange(length, device=arc_logits.device, dtype=torch.long).unsqueeze(0) arc_loss = arc_logits[batch_index, child_index, target1] label_loss = label_logits[batch_index, child_index, target2] - + byte_mask = flip_mask.byte() arc_loss.masked_fill_(byte_mask, 0) label_loss.masked_fill_(byte_mask, 0) arc_nll = -arc_loss.mean() label_nll = -label_loss.mean() return arc_nll + label_nll - + def predict(self, words1, words2, seq_len): """模型预测API :param words1: [batch_size, seq_len] 输入word序列 :param words2: [batch_size, seq_len] 输入pos序列 :param seq_len: [batch_size, seq_len] 输入序列长度 - :return dict: parsing结果:: + :return dict: parsing + 结果:: + + pred1: [batch_size, seq_len] heads的预测结果 + pred2: [batch_size, seq_len, num_label] label预测logits - pred1: [batch_size, seq_len] heads的预测结果 - pred2: [batch_size, seq_len, num_label] label预测logits """ res = self(words1, words2, seq_len) output = {} @@ -470,6 +485,7 @@ class ParserLoss(LossFunc): :param seq_len: [batch_size, seq_len] 真实目标的长度 :return loss: scalar """ + def __init__(self, pred1=None, pred2=None, target1=None, target2=None, seq_len=None): @@ -497,9 +513,10 @@ class ParserMetric(MetricBase): UAS: 不带label时, 边预测的准确率 LAS: 同时预测边和label的准确率 """ + def __init__(self, pred1=None, pred2=None, target1=None, target2=None, seq_len=None): - + super().__init__() self._init_param_map(pred1=pred1, pred2=pred2, target1=target1, target2=target2, @@ -507,13 +524,13 @@ class ParserMetric(MetricBase): self.num_arc = 0 self.num_label = 0 self.num_sample = 0 - + def get_metric(self, reset=True): - res = {'UAS': self.num_arc*1.0 / self.num_sample, 'LAS': self.num_label*1.0 / self.num_sample} + res = {'UAS': self.num_arc * 1.0 / self.num_sample, 'LAS': self.num_label * 1.0 / self.num_sample} if reset: self.num_sample = self.num_label = self.num_arc = 0 return res - + def evaluate(self, pred1, pred2, target1, target2, seq_len=None): """Evaluate the performance of prediction. """ @@ -522,7 +539,7 @@ class ParserMetric(MetricBase): else: seq_mask = seq_len_to_mask(seq_len.long()).long() # mask out tag - seq_mask[:,0] = 0 + seq_mask[:, 0] = 0 head_pred_correct = (pred1 == target1).long() * seq_mask label_pred_correct = (pred2 == target2).long() * head_pred_correct self.num_arc += head_pred_correct.sum().item() diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index 01b03b9f..a9ccc568 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -1,12 +1,13 @@ -# python: 3.6 -# encoding: utf-8 - import torch import torch.nn as nn -from ..core.const import Const as C +from ..core.const import Const as C from ..modules import encoder +__all__ = [ + "CNNText" +] + class CNNText(torch.nn.Module): """ @@ -23,7 +24,7 @@ class CNNText(torch.nn.Module): :param int padding: 对句子前后的pad的大小, 用0填充。 :param float dropout: Dropout的大小 """ - + def __init__(self, init_embed, num_classes, kernel_nums=(3, 4, 5), @@ -31,7 +32,7 @@ class CNNText(torch.nn.Module): padding=0, dropout=0.5): super(CNNText, self).__init__() - + # no support for pre-trained embedding currently self.embed = encoder.Embedding(init_embed) self.conv_pool = encoder.ConvMaxpool( @@ -41,7 +42,7 @@ class CNNText(torch.nn.Module): padding=padding) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(sum(kernel_nums), num_classes) - + def forward(self, words, seq_len=None): """ @@ -54,7 +55,7 @@ class CNNText(torch.nn.Module): x = self.dropout(x) x = self.fc(x) # [N,C] -> [N, N_class] return {C.OUTPUT: x} - + def predict(self, words, seq_len=None): """ :param torch.LongTensor words: [batch_size, seq_len],句子中word的index diff --git a/fastNLP/models/enas_controller.py b/fastNLP/models/enas_controller.py index 16b970e6..e83c6b51 100644 --- a/fastNLP/models/enas_controller.py +++ b/fastNLP/models/enas_controller.py @@ -5,6 +5,7 @@ import os import torch import torch.nn.functional as F + from . import enas_utils as utils from .enas_utils import Node diff --git a/fastNLP/models/enas_model.py b/fastNLP/models/enas_model.py index 5c667927..b6b683c0 100644 --- a/fastNLP/models/enas_model.py +++ b/fastNLP/models/enas_model.py @@ -1,17 +1,19 @@ -# Code Modified from https://github.com/carpedm20/ENAS-pytorch - -"""Module containing the shared RNN model.""" -import numpy as np +""" +Module containing the shared RNN model. +Code Modified from https://github.com/carpedm20/ENAS-pytorch +""" import collections +import numpy as np import torch -from torch import nn +import torch.nn as nn import torch.nn.functional as F from torch.autograd import Variable from . import enas_utils as utils from .base_model import BaseModel + def _get_dropped_weights(w_raw, dropout_p, is_training): """Drops out weights to implement DropConnect. @@ -35,12 +37,13 @@ def _get_dropped_weights(w_raw, dropout_p, is_training): The above TODO is the reason for the hacky check for `torch.nn.Parameter`. """ dropped_w = F.dropout(w_raw, p=dropout_p, training=is_training) - + if isinstance(dropped_w, torch.nn.Parameter): dropped_w = dropped_w.clone() - + return dropped_w + class EmbeddingDropout(torch.nn.Embedding): """Class for dropping out embeddings by zero'ing out parameters in the embedding matrix. @@ -53,6 +56,7 @@ class EmbeddingDropout(torch.nn.Embedding): See 'A Theoretically Grounded Application of Dropout in Recurrent Neural Networks', (Gal and Ghahramani, 2016). """ + def __init__(self, num_embeddings, embedding_dim, @@ -83,14 +87,14 @@ class EmbeddingDropout(torch.nn.Embedding): assert (dropout >= 0.0) and (dropout < 1.0), ('Dropout must be >= 0.0 ' 'and < 1.0') self.scale = scale - + def forward(self, inputs): # pylint:disable=arguments-differ """Embeds `inputs` with the dropped out embedding weight matrix.""" if self.training: dropout = self.dropout else: dropout = 0 - + if dropout: mask = self.weight.data.new(self.weight.size(0), 1) mask.bernoulli_(1 - dropout) @@ -101,7 +105,7 @@ class EmbeddingDropout(torch.nn.Embedding): masked_weight = self.weight if self.scale and self.scale != 1: masked_weight = masked_weight * self.scale - + return F.embedding(inputs, masked_weight, max_norm=self.max_norm, @@ -114,7 +118,7 @@ class LockedDropout(nn.Module): # code from https://github.com/salesforce/awd-lstm-lm/blob/master/locked_dropout.py def __init__(self): super().__init__() - + def forward(self, x, dropout=0.5): if not self.training or not dropout: return x @@ -126,11 +130,12 @@ class LockedDropout(nn.Module): class ENASModel(BaseModel): """Shared RNN model.""" + def __init__(self, embed_num, num_classes, num_blocks=4, cuda=False, shared_hid=1000, shared_embed=1000): super(ENASModel, self).__init__() - + self.use_cuda = cuda - + self.shared_hid = shared_hid self.num_blocks = num_blocks self.decoder = nn.Linear(self.shared_hid, num_classes) @@ -139,16 +144,16 @@ class ENASModel(BaseModel): dropout=0.1) self.lockdrop = LockedDropout() self.dag = None - + # Tie weights # self.decoder.weight = self.encoder.weight - + # Since W^{x, c} and W^{h, c} are always summed, there # is no point duplicating their bias offset parameter. Likewise for # W^{x, h} and W^{h, h}. self.w_xc = nn.Linear(shared_embed, self.shared_hid) self.w_xh = nn.Linear(shared_embed, self.shared_hid) - + # The raw weights are stored here because the hidden-to-hidden weights # are weight dropped on the forward pass. self.w_hc_raw = torch.nn.Parameter( @@ -157,10 +162,10 @@ class ENASModel(BaseModel): torch.Tensor(self.shared_hid, self.shared_hid)) self.w_hc = None self.w_hh = None - + self.w_h = collections.defaultdict(dict) self.w_c = collections.defaultdict(dict) - + for idx in range(self.num_blocks): for jdx in range(idx + 1, self.num_blocks): self.w_h[idx][jdx] = nn.Linear(self.shared_hid, @@ -169,48 +174,47 @@ class ENASModel(BaseModel): self.w_c[idx][jdx] = nn.Linear(self.shared_hid, self.shared_hid, bias=False) - + self._w_h = nn.ModuleList([self.w_h[idx][jdx] for idx in self.w_h for jdx in self.w_h[idx]]) self._w_c = nn.ModuleList([self.w_c[idx][jdx] for idx in self.w_c for jdx in self.w_c[idx]]) - + self.batch_norm = None # if args.mode == 'train': # self.batch_norm = nn.BatchNorm1d(self.shared_hid) # else: # self.batch_norm = None - + self.reset_parameters() self.static_init_hidden = utils.keydefaultdict(self.init_hidden) - + def setDAG(self, dag): if self.dag is None: self.dag = dag - + def forward(self, word_seq, hidden=None): inputs = torch.transpose(word_seq, 0, 1) - + time_steps = inputs.size(0) batch_size = inputs.size(1) - - + self.w_hh = _get_dropped_weights(self.w_hh_raw, 0.5, self.training) self.w_hc = _get_dropped_weights(self.w_hc_raw, 0.5, self.training) - + # hidden = self.static_init_hidden[batch_size] if hidden is None else hidden hidden = self.static_init_hidden[batch_size] - + embed = self.encoder(inputs) - + embed = self.lockdrop(embed, 0.65 if self.training else 0) - + # The norm of hidden states are clipped here because # otherwise ENAS is especially prone to exploding activations on the # forward pass. This could probably be fixed in a more elegant way, but @@ -226,7 +230,7 @@ class ENASModel(BaseModel): for step in range(time_steps): x_t = embed[step] logit, hidden = self.cell(x_t, hidden, self.dag) - + hidden_norms = hidden.norm(dim=-1) max_norm = 25.0 if hidden_norms.data.max() > max_norm: @@ -237,60 +241,60 @@ class ENASModel(BaseModel): # because the PyTorch slicing and slice assignment is too # flaky. hidden_norms = hidden_norms.data.cpu().numpy() - + clipped_num += 1 if hidden_norms.max() > max_clipped_norm: max_clipped_norm = hidden_norms.max() - + clip_select = hidden_norms > max_norm clip_norms = hidden_norms[clip_select] - + mask = np.ones(hidden.size()) - normalizer = max_norm/clip_norms + normalizer = max_norm / clip_norms normalizer = normalizer[:, np.newaxis] - + mask[clip_select] = normalizer - + if self.use_cuda: hidden *= torch.autograd.Variable( torch.FloatTensor(mask).cuda(), requires_grad=False) else: hidden *= torch.autograd.Variable( - torch.FloatTensor(mask), requires_grad=False) + torch.FloatTensor(mask), requires_grad=False) logits.append(logit) h1tohT.append(hidden) - + h1tohT = torch.stack(h1tohT) output = torch.stack(logits) raw_output = output - + output = self.lockdrop(output, 0.4 if self.training else 0) - - #Pooling + + # Pooling output = torch.mean(output, 0) - + decoded = self.decoder(output) - + extra_out = {'dropped': decoded, 'hiddens': h1tohT, 'raw': raw_output} return {'pred': decoded, 'hidden': hidden, 'extra_out': extra_out} - + def cell(self, x, h_prev, dag): """Computes a single pass through the discovered RNN cell.""" c = {} h = {} f = {} - + f[0] = self.get_f(dag[-1][0].name) c[0] = torch.sigmoid(self.w_xc(x) + F.linear(h_prev, self.w_hc, None)) - h[0] = (c[0]*f[0](self.w_xh(x) + F.linear(h_prev, self.w_hh, None)) + - (1 - c[0])*h_prev) - + h[0] = (c[0] * f[0](self.w_xh(x) + F.linear(h_prev, self.w_hh, None)) + + (1 - c[0]) * h_prev) + leaf_node_ids = [] q = collections.deque() q.append(0) - + # Computes connections from the parent nodes `node_id` # to their child nodes `next_id` recursively, skipping leaf nodes. A # leaf node is a node whose id == `self.num_blocks`. @@ -306,10 +310,10 @@ class ENASModel(BaseModel): while True: if len(q) == 0: break - + node_id = q.popleft() nodes = dag[node_id] - + for next_node in nodes: next_id = next_node.id if next_id == self.num_blocks: @@ -317,38 +321,38 @@ class ENASModel(BaseModel): assert len(nodes) == 1, ('parent of leaf node should have ' 'only one child') continue - + w_h = self.w_h[node_id][next_id] w_c = self.w_c[node_id][next_id] - + f[next_id] = self.get_f(next_node.name) c[next_id] = torch.sigmoid(w_c(h[node_id])) - h[next_id] = (c[next_id]*f[next_id](w_h(h[node_id])) + - (1 - c[next_id])*h[node_id]) - + h[next_id] = (c[next_id] * f[next_id](w_h(h[node_id])) + + (1 - c[next_id]) * h[node_id]) + q.append(next_id) - + # Instead of averaging loose ends, perhaps there should # be a set of separate unshared weights for each "loose" connection # between each node in a cell and the output. # # As it stands, all weights W^h_{ij} are doing double duty by # connecting both from i to j, as well as from i to the output. - + # average all the loose ends leaf_nodes = [h[node_id] for node_id in leaf_node_ids] output = torch.mean(torch.stack(leaf_nodes, 2), -1) - + # stabilizing the Updates of omega if self.batch_norm is not None: output = self.batch_norm(output) - + return output, h[self.num_blocks - 1] - + def init_hidden(self, batch_size): zeros = torch.zeros(batch_size, self.shared_hid) return utils.get_variable(zeros, self.use_cuda, requires_grad=False) - + def get_f(self, name): name = name.lower() if name == 'relu': @@ -360,22 +364,21 @@ class ENASModel(BaseModel): elif name == 'sigmoid': f = torch.sigmoid return f - - + @property def num_parameters(self): def size(p): return np.prod(p.size()) + return sum([size(param) for param in self.parameters()]) - - + def reset_parameters(self): init_range = 0.025 # init_range = 0.025 if self.args.mode == 'train' else 0.04 for param in self.parameters(): param.data.uniform_(-init_range, init_range) self.decoder.bias.data.fill_(0) - + def predict(self, word_seq): """ diff --git a/fastNLP/models/enas_trainer.py b/fastNLP/models/enas_trainer.py index 9cd7d8d0..ef596b03 100644 --- a/fastNLP/models/enas_trainer.py +++ b/fastNLP/models/enas_trainer.py @@ -1,12 +1,12 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch - -import time -from datetime import datetime -from datetime import timedelta - +import math import numpy as np +import time import torch -import math + +from datetime import datetime, timedelta + +from torch.optim import Adam try: from tqdm.auto import tqdm @@ -21,8 +21,6 @@ from ..core.utils import _move_dict_value_to_device from . import enas_utils as utils from ..core.utils import _build_args -from torch.optim import Adam - def _get_no_grad_ctx_mgr(): """Returns a the `torch.no_grad` context manager for PyTorch version >= @@ -33,6 +31,7 @@ def _get_no_grad_ctx_mgr(): class ENASTrainer(Trainer): """A class to wrap training code.""" + def __init__(self, train_data, model, controller, **kwargs): """Constructor for training algorithm. :param DataSet train_data: the training data @@ -45,19 +44,19 @@ class ENASTrainer(Trainer): self.controller_step = 0 self.shared_step = 0 self.max_length = 35 - + self.shared = model self.controller = controller - + self.shared_optim = Adam( self.shared.parameters(), lr=20.0, weight_decay=1e-7) - + self.controller_optim = Adam( self.controller.parameters(), lr=3.5e-4) - + def train(self, load_best_model=True): """ :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 @@ -82,21 +81,22 @@ class ENASTrainer(Trainer): self.model = self.model.cuda() self._model_device = self.model.parameters().__next__().device self._mode(self.model, is_test=False) - + self.start_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S')) start_time = time.time() print("training epochs started " + self.start_time, flush=True) - + try: self.callback_manager.on_train_begin() self._train() self.callback_manager.on_train_end() except (CallbackException, KeyboardInterrupt) as e: self.callback_manager.on_exception(e) - + if self.dev_data is not None: - print("\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + - self.tester._format_eval_results(self.best_dev_perf),) + print( + "\nIn Epoch:{}/Step:{}, got best dev performance:".format(self.best_dev_epoch, self.best_dev_step) + + self.tester._format_eval_results(self.best_dev_perf), ) results['best_eval'] = self.best_dev_perf results['best_epoch'] = self.best_dev_epoch results['best_step'] = self.best_dev_step @@ -110,9 +110,9 @@ class ENASTrainer(Trainer): finally: pass results['seconds'] = round(time.time() - start_time, 2) - + return results - + def _train(self): if not self.use_tqdm: from fastNLP.core.utils import _pseudo_tqdm as inner_tqdm @@ -126,21 +126,21 @@ class ENASTrainer(Trainer): avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, prefetch=self.prefetch) - for epoch in range(1, self.n_epochs+1): + for epoch in range(1, self.n_epochs + 1): pbar.set_description_str(desc="Epoch {}/{}".format(epoch, self.n_epochs)) last_stage = (epoch > self.n_epochs + 1 - self.final_epochs) if epoch == self.n_epochs + 1 - self.final_epochs: print('Entering the final stage. (Only train the selected structure)') # early stopping self.callback_manager.on_epoch_begin() - + # 1. Training the shared parameters omega of the child models self.train_shared(pbar) - + # 2. Training the controller parameters theta if not last_stage: self.train_controller() - + if ((self.validate_every > 0 and self.step % self.validate_every == 0) or (self.validate_every < 0 and self.step % len(data_iterator) == 0)) \ and self.dev_data is not None: @@ -149,16 +149,15 @@ class ENASTrainer(Trainer): eval_res = self._do_validation(epoch=epoch, step=self.step) eval_str = "Evaluation at Epoch {}/{}. Step:{}/{}. ".format(epoch, self.n_epochs, self.step, total_steps) + \ - self.tester._format_eval_results(eval_res) + self.tester._format_eval_results(eval_res) pbar.write(eval_str) - + # lr decay; early stopping self.callback_manager.on_epoch_end() # =============== epochs end =================== # pbar.close() # ============ tqdm end ============== # - - + def get_loss(self, inputs, targets, hidden, dags): """Computes the loss for the same batch for M models. @@ -167,7 +166,7 @@ class ENASTrainer(Trainer): """ if not isinstance(dags, list): dags = [dags] - + loss = 0 for dag in dags: self.shared.setDAG(dag) @@ -175,14 +174,14 @@ class ENASTrainer(Trainer): inputs['hidden'] = hidden result = self.shared(**inputs) output, hidden, extra_out = result['pred'], result['hidden'], result['extra_out'] - + self.callback_manager.on_loss_begin(targets, result) sample_loss = self._compute_loss(result, targets) loss += sample_loss - + assert len(dags) == 1, 'there are multiple `hidden` for multple `dags`' return loss, hidden, extra_out - + def train_shared(self, pbar=None, max_step=None, dag=None): """Train the language model for 400 steps of minibatches of 64 examples. @@ -200,9 +199,9 @@ class ENASTrainer(Trainer): model = self.shared model.train() self.controller.eval() - + hidden = self.shared.init_hidden(self.batch_size) - + abs_max_grad = 0 abs_max_hidden_norm = 0 step = 0 @@ -211,15 +210,15 @@ class ENASTrainer(Trainer): train_idx = 0 avg_loss = 0 data_iterator = Batch(self.train_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, - prefetch=self.prefetch) - + prefetch=self.prefetch) + for batch_x, batch_y in data_iterator: _move_dict_value_to_device(batch_x, batch_y, device=self._model_device) indices = data_iterator.get_batch_indices() # negative sampling; replace unknown; re-weight batch_y self.callback_manager.on_batch_begin(batch_x, batch_y, indices) # prediction = self._data_forward(self.model, batch_x) - + dags = self.controller.sample(1) inputs, targets = batch_x, batch_y # self.callback_manager.on_loss_begin(batch_y, prediction) @@ -228,18 +227,18 @@ class ENASTrainer(Trainer): hidden, dags) hidden.detach_() - + avg_loss += loss.item() - + # Is loss NaN or inf? requires_grad = False self.callback_manager.on_backward_begin(loss) self._grad_backward(loss) self.callback_manager.on_backward_end() - + self._update() self.callback_manager.on_step_end() - - if (self.step+1) % self.print_every == 0: + + if (self.step + 1) % self.print_every == 0: if self.use_tqdm: print_output = "loss:{0:<6.5f}".format(avg_loss / self.print_every) pbar.update(self.print_every) @@ -255,30 +254,29 @@ class ENASTrainer(Trainer): self.shared_step += 1 self.callback_manager.on_batch_end() # ================= mini-batch end ==================== # - - + def get_reward(self, dag, entropies, hidden, valid_idx=0): """Computes the perplexity of a single sampled model on a minibatch of validation data. """ if not isinstance(entropies, np.ndarray): entropies = entropies.data.cpu().numpy() - + data_iterator = Batch(self.dev_data, batch_size=self.batch_size, sampler=self.sampler, as_numpy=False, - prefetch=self.prefetch) - + prefetch=self.prefetch) + for inputs, targets in data_iterator: valid_loss, hidden, _ = self.get_loss(inputs, targets, hidden, dag) valid_loss = utils.to_item(valid_loss.data) - + valid_ppl = math.exp(valid_loss) - + R = 80 / valid_ppl - + rewards = R + 1e-4 * entropies - + return rewards, hidden - + def train_controller(self): """Fixes the shared parameters and updates the controller parameters. @@ -296,13 +294,13 @@ class ENASTrainer(Trainer): # Why can't we call shared.eval() here? Leads to loss # being uniformly zero for the controller. # self.shared.eval() - + avg_reward_base = None baseline = None adv_history = [] entropy_history = [] reward_history = [] - + hidden = self.shared.init_hidden(self.batch_size) total_loss = 0 valid_idx = 0 @@ -310,7 +308,7 @@ class ENASTrainer(Trainer): # sample models dags, log_probs, entropies = self.controller.sample( with_details=True) - + # calculate reward np_entropies = entropies.data.cpu().numpy() # No gradients should be backpropagated to the @@ -320,40 +318,39 @@ class ENASTrainer(Trainer): np_entropies, hidden, valid_idx) - - + reward_history.extend(rewards) entropy_history.extend(np_entropies) - + # moving average baseline if baseline is None: baseline = rewards else: decay = 0.95 baseline = decay * baseline + (1 - decay) * rewards - + adv = rewards - baseline adv_history.extend(adv) - + # policy loss - loss = -log_probs*utils.get_variable(adv, - 'cuda' in self.device, - requires_grad=False) - + loss = -log_probs * utils.get_variable(adv, + 'cuda' in self.device, + requires_grad=False) + loss = loss.sum() # or loss.mean() - + # update self.controller_optim.zero_grad() loss.backward() - + self.controller_optim.step() - + total_loss += utils.to_item(loss.data) - + if ((step % 50) == 0) and (step > 0): reward_history, adv_history, entropy_history = [], [], [] total_loss = 0 - + self.controller_step += 1 # prev_valid_idx = valid_idx # valid_idx = ((valid_idx + self.max_length) % @@ -362,16 +359,16 @@ class ENASTrainer(Trainer): # # validation data, we reset the hidden states. # if prev_valid_idx > valid_idx: # hidden = self.shared.init_hidden(self.batch_size) - + def derive(self, sample_num=10, valid_idx=0): """We are always deriving based on the very first batch of validation data? This seems wrong... """ hidden = self.shared.init_hidden(self.batch_size) - + dags, _, entropies = self.controller.sample(sample_num, with_details=True) - + max_R = 0 best_dag = None for dag in dags: @@ -379,5 +376,5 @@ class ENASTrainer(Trainer): if R.max() > max_R: max_R = R.max() best_dag = dag - + self.model.setDAG(best_dag) diff --git a/fastNLP/models/enas_utils.py b/fastNLP/models/enas_utils.py index aafcb3a7..68c170ed 100644 --- a/fastNLP/models/enas_utils.py +++ b/fastNLP/models/enas_utils.py @@ -1,12 +1,10 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch from __future__ import print_function - from collections import defaultdict import collections import numpy as np - import torch from torch.autograd import Variable diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 39f4c3fe..17f02298 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -1,11 +1,19 @@ +""" + 本模块实现了两种序列标注模型 +""" import torch +import torch.nn as nn from .base_model import BaseModel from ..modules import decoder, encoder from ..modules.decoder.CRF import allowed_transitions from ..core.utils import seq_len_to_mask from ..core.const import Const as C -from torch import nn + +__all__ = [ + "SeqLabeling", + "AdvSeqLabel" +] class SeqLabeling(BaseModel): diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 34b54302..606bcc42 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -8,6 +8,9 @@ from ..modules import encoder as Encoder from ..modules import aggregator as Aggregator from ..core.utils import seq_len_to_mask +__all__ = [ + "ESIM" +] my_inf = 10e12 @@ -26,7 +29,7 @@ class ESIM(BaseModel): :param int num_classes: 标签数目,默认为3 :param numpy.array init_embedding: 初始词嵌入矩阵,形状为(vocab_size, embed_dim),默认为None,即随机初始化词嵌入矩阵 """ - + def __init__(self, vocab_size, embed_dim, hidden_size, dropout=0.0, num_classes=3, init_embedding=None): super(ESIM, self).__init__() @@ -35,35 +38,36 @@ class ESIM(BaseModel): self.hidden_size = hidden_size self.dropout = dropout self.n_labels = num_classes - + self.drop = nn.Dropout(self.dropout) - + self.embedding = Encoder.Embedding( (self.vocab_size, self.embed_dim), dropout=self.dropout, ) - + self.embedding_layer = nn.Linear(self.embed_dim, self.hidden_size) - + self.encoder = Encoder.LSTM( input_size=self.embed_dim, hidden_size=self.hidden_size, num_layers=1, bias=True, batch_first=True, bidirectional=True ) - + self.bi_attention = Aggregator.BiAttention() self.mean_pooling = Aggregator.AvgPoolWithMask() self.max_pooling = Aggregator.MaxPoolWithMask() - + self.inference_layer = nn.Linear(self.hidden_size * 4, self.hidden_size) - + self.decoder = Encoder.LSTM( input_size=self.hidden_size, hidden_size=self.hidden_size, num_layers=1, bias=True, batch_first=True, bidirectional=True ) - + self.output = Decoder.MLP([4 * self.hidden_size, self.hidden_size, self.n_labels], 'tanh', dropout=self.dropout) - + def forward(self, words1, words2, seq_len1=None, seq_len2=None, target=None): """ Forward function + :param torch.Tensor words1: [batch size(B), premise seq len(PL)] premise的token表示 :param torch.Tensor words2: [B, hypothesis seq len(HL)] hypothesis的token表示 :param torch.LongTensor seq_len1: [B] premise的长度 @@ -71,10 +75,10 @@ class ESIM(BaseModel): :param torch.LongTensor target: [B] 真实目标值 :return: dict prediction: [B, n_labels(N)] 预测结果 """ - + premise0 = self.embedding_layer(self.embedding(words1)) hypothesis0 = self.embedding_layer(self.embedding(words2)) - + if seq_len1 is not None: seq_len1 = seq_len_to_mask(seq_len1) else: @@ -85,55 +89,55 @@ class ESIM(BaseModel): else: seq_len2 = torch.ones(hypothesis0.size(0), hypothesis0.size(1)) seq_len2 = (seq_len2.long()).to(device=hypothesis0.device) - + _BP, _PSL, _HP = premise0.size() _BH, _HSL, _HH = hypothesis0.size() _BPL, _PLL = seq_len1.size() _HPL, _HLL = seq_len2.size() - + assert _BP == _BH and _BPL == _HPL and _BP == _BPL assert _HP == _HH assert _PSL == _PLL and _HSL == _HLL - + B, PL, H = premise0.size() B, HL, H = hypothesis0.size() - + a0 = self.encoder(self.drop(premise0)) # a0: [B, PL, H * 2] b0 = self.encoder(self.drop(hypothesis0)) # b0: [B, HL, H * 2] - + a = torch.mean(a0.view(B, PL, -1, H), dim=2) # a: [B, PL, H] b = torch.mean(b0.view(B, HL, -1, H), dim=2) # b: [B, HL, H] - + ai, bi = self.bi_attention(a, b, seq_len1, seq_len2) - + ma = torch.cat((a, ai, a - ai, a * ai), dim=2) # ma: [B, PL, 4 * H] mb = torch.cat((b, bi, b - bi, b * bi), dim=2) # mb: [B, HL, 4 * H] - + f_ma = self.inference_layer(ma) f_mb = self.inference_layer(mb) - + vat = self.decoder(self.drop(f_ma)) vbt = self.decoder(self.drop(f_mb)) - + va = torch.mean(vat.view(B, PL, -1, H), dim=2) # va: [B, PL, H] vb = torch.mean(vbt.view(B, HL, -1, H), dim=2) # vb: [B, HL, H] - + va_ave = self.mean_pooling(va, seq_len1, dim=1) # va_ave: [B, H] va_max, va_arg_max = self.max_pooling(va, seq_len1, dim=1) # va_max: [B, H] vb_ave = self.mean_pooling(vb, seq_len2, dim=1) # vb_ave: [B, H] vb_max, vb_arg_max = self.max_pooling(vb, seq_len2, dim=1) # vb_max: [B, H] - + v = torch.cat((va_ave, va_max, vb_ave, vb_max), dim=1) # v: [B, 4 * H] - + prediction = torch.tanh(self.output(v)) # prediction: [B, N] - + if target is not None: func = nn.CrossEntropyLoss() loss = func(prediction, target) return {Const.OUTPUT: prediction, Const.LOSS: loss} - + return {Const.OUTPUT: prediction} - + def predict(self, words1, words2, seq_len1=None, seq_len2=None, target=None): """ Predict function @@ -146,4 +150,3 @@ class ESIM(BaseModel): """ prediction = self.forward(words1, words2, seq_len1, seq_len2)[Const.OUTPUT] return {Const.OUTPUT: torch.argmax(prediction, dim=-1)} - diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index cdd1f321..2e55f7e4 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,17 +1,25 @@ -"""Star-Transformer 的 一个 Pytorch 实现. """ +Star-Transformer 的 Pytorch 实现。 +""" +import torch +from torch import nn + from ..modules.encoder.star_transformer import StarTransformer from ..core.utils import seq_len_to_mask from ..modules.utils import get_embeddings from ..core.const import Const -import torch -from torch import nn +__all__ = [ + "StarTransEnc", + "STNLICls", + "STSeqCls", + "STSeqLabel", +] class StarTransEnc(nn.Module): """ - 别名::class:`fastNLP.models.StarTransEnc` :class:`fastNLP.models.start_transformer.StarTransEnc` + 别名::class:`fastNLP.models.StarTransEnc` :class:`fastNLP.models.star_transformer.StarTransEnc` 带word embedding的Star-Transformer Encoder @@ -28,6 +36,7 @@ class StarTransEnc(nn.Module): :param emb_dropout: 词嵌入的dropout概率. :param dropout: 模型除词嵌入外的dropout概率. """ + def __init__(self, init_embed, hidden_size, num_layers, @@ -47,7 +56,7 @@ class StarTransEnc(nn.Module): head_dim=head_dim, dropout=dropout, max_len=max_len) - + def forward(self, x, mask): """ :param FloatTensor data: [batch, length, hidden] 输入的序列 @@ -72,7 +81,7 @@ class _Cls(nn.Module): nn.Dropout(dropout), nn.Linear(hid_dim, num_cls), ) - + def forward(self, x): h = self.fc(x) return h @@ -83,20 +92,21 @@ class _NLICls(nn.Module): super(_NLICls, self).__init__() self.fc = nn.Sequential( nn.Dropout(dropout), - nn.Linear(in_dim*4, hid_dim), #4 + nn.Linear(in_dim * 4, hid_dim), # 4 nn.LeakyReLU(), nn.Dropout(dropout), nn.Linear(hid_dim, num_cls), ) - + def forward(self, x1, x2): - x = torch.cat([x1, x2, torch.abs(x1-x2), x1*x2], 1) + x = torch.cat([x1, x2, torch.abs(x1 - x2), x1 * x2], 1) h = self.fc(x) return h + class STSeqLabel(nn.Module): """ - 别名::class:`fastNLP.models.STSeqLabel` :class:`fastNLP.models.start_transformer.STSeqLabel` + 别名::class:`fastNLP.models.STSeqLabel` :class:`fastNLP.models.star_transformer.STSeqLabel` 用于序列标注的Star-Transformer模型 @@ -112,6 +122,7 @@ class STSeqLabel(nn.Module): :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, @@ -120,7 +131,7 @@ class STSeqLabel(nn.Module): max_len=512, cls_hidden_size=600, emb_dropout=0.1, - dropout=0.1,): + dropout=0.1, ): super(STSeqLabel, self).__init__() self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, @@ -131,7 +142,7 @@ class STSeqLabel(nn.Module): emb_dropout=emb_dropout, dropout=dropout) self.cls = _Cls(hidden_size, num_cls, cls_hidden_size) - + def forward(self, words, seq_len): """ @@ -142,9 +153,9 @@ class STSeqLabel(nn.Module): mask = seq_len_to_mask(seq_len) nodes, _ = self.enc(words, mask) output = self.cls(nodes) - output = output.transpose(1,2) # make hidden to be dim 1 - return {Const.OUTPUT: output} # [bsz, n_cls, seq_len] - + output = output.transpose(1, 2) # make hidden to be dim 1 + return {Const.OUTPUT: output} # [bsz, n_cls, seq_len] + def predict(self, words, seq_len): """ @@ -159,7 +170,7 @@ class STSeqLabel(nn.Module): class STSeqCls(nn.Module): """ - 别名::class:`fastNLP.models.STSeqCls` :class:`fastNLP.models.start_transformer.STSeqCls` + 别名::class:`fastNLP.models.STSeqCls` :class:`fastNLP.models.star_transformer.STSeqCls` 用于分类任务的Star-Transformer @@ -175,7 +186,7 @@ class STSeqCls(nn.Module): :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ - + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, @@ -184,7 +195,7 @@ class STSeqCls(nn.Module): max_len=512, cls_hidden_size=600, emb_dropout=0.1, - dropout=0.1,): + dropout=0.1, ): super(STSeqCls, self).__init__() self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, @@ -195,7 +206,7 @@ class STSeqCls(nn.Module): emb_dropout=emb_dropout, dropout=dropout) self.cls = _Cls(hidden_size, num_cls, cls_hidden_size) - + def forward(self, words, seq_len): """ @@ -206,9 +217,9 @@ class STSeqCls(nn.Module): mask = seq_len_to_mask(seq_len) nodes, relay = self.enc(words, mask) y = 0.5 * (relay + nodes.max(1)[0]) - output = self.cls(y) # [bsz, n_cls] + output = self.cls(y) # [bsz, n_cls] return {Const.OUTPUT: output} - + def predict(self, words, seq_len): """ @@ -223,7 +234,7 @@ class STSeqCls(nn.Module): class STNLICls(nn.Module): """ - 别名::class:`fastNLP.models.STNLICls` :class:`fastNLP.models.start_transformer.STNLICls` + 别名::class:`fastNLP.models.STNLICls` :class:`fastNLP.models.star_transformer.STNLICls` 用于自然语言推断(NLI)的Star-Transformer @@ -239,7 +250,7 @@ class STNLICls(nn.Module): :param emb_dropout: 词嵌入的dropout概率. Default: 0.1 :param dropout: 模型除词嵌入外的dropout概率. Default: 0.1 """ - + def __init__(self, init_embed, num_cls, hidden_size=300, num_layers=4, @@ -248,7 +259,7 @@ class STNLICls(nn.Module): max_len=512, cls_hidden_size=600, emb_dropout=0.1, - dropout=0.1,): + dropout=0.1, ): super(STNLICls, self).__init__() self.enc = StarTransEnc(init_embed=init_embed, hidden_size=hidden_size, @@ -259,7 +270,7 @@ class STNLICls(nn.Module): emb_dropout=emb_dropout, dropout=dropout) self.cls = _NLICls(hidden_size, num_cls, cls_hidden_size) - + def forward(self, words1, words2, seq_len1, seq_len2): """ @@ -271,14 +282,16 @@ class STNLICls(nn.Module): """ mask1 = seq_len_to_mask(seq_len1) mask2 = seq_len_to_mask(seq_len2) + def enc(seq, mask): nodes, relay = self.enc(seq, mask) return 0.5 * (relay + nodes.max(1)[0]) + y1 = enc(words1, mask1) y2 = enc(words2, mask2) - output = self.cls(y1, y2) # [bsz, n_cls] + output = self.cls(y1, y2) # [bsz, n_cls] return {Const.OUTPUT: output} - + def predict(self, words1, words2, seq_len1, seq_len2): """ From fb143ff2add1a7ddd6c31fbc4eb3a68d95d423cb Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 16 May 2019 21:37:19 +0800 Subject: [PATCH 136/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...odules.decoder.CRF.rst => fastNLP.modules.decoder.crf.rst} | 2 +- ...odules.decoder.MLP.rst => fastNLP.modules.decoder.mlp.rst} | 2 +- docs/source/fastNLP.modules.decoder.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename docs/source/{fastNLP.modules.decoder.CRF.rst => fastNLP.modules.decoder.crf.rst} (72%) rename docs/source/{fastNLP.modules.decoder.MLP.rst => fastNLP.modules.decoder.mlp.rst} (72%) diff --git a/docs/source/fastNLP.modules.decoder.CRF.rst b/docs/source/fastNLP.modules.decoder.crf.rst similarity index 72% rename from docs/source/fastNLP.modules.decoder.CRF.rst rename to docs/source/fastNLP.modules.decoder.crf.rst index fc643fef..6d5b0d5b 100644 --- a/docs/source/fastNLP.modules.decoder.CRF.rst +++ b/docs/source/fastNLP.modules.decoder.crf.rst @@ -1,7 +1,7 @@ fastNLP.modules.decoder.CRF =========================== -.. automodule:: fastNLP.modules.decoder.CRF +.. automodule:: fastNLP.modules.decoder.crf :members: :undoc-members: :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.MLP.rst b/docs/source/fastNLP.modules.decoder.mlp.rst similarity index 72% rename from docs/source/fastNLP.modules.decoder.MLP.rst rename to docs/source/fastNLP.modules.decoder.mlp.rst index feb5c228..7d661ebf 100644 --- a/docs/source/fastNLP.modules.decoder.MLP.rst +++ b/docs/source/fastNLP.modules.decoder.mlp.rst @@ -1,7 +1,7 @@ fastNLP.modules.decoder.MLP =========================== -.. automodule:: fastNLP.modules.decoder.MLP +.. automodule:: fastNLP.modules.decoder.mlp :members: :undoc-members: :show-inheritance: diff --git a/docs/source/fastNLP.modules.decoder.rst b/docs/source/fastNLP.modules.decoder.rst index 1c28740b..e42a9f39 100644 --- a/docs/source/fastNLP.modules.decoder.rst +++ b/docs/source/fastNLP.modules.decoder.rst @@ -12,7 +12,7 @@ fastNLP.modules.decoder .. toctree:: :titlesonly: - fastNLP.modules.decoder.CRF - fastNLP.modules.decoder.MLP + fastNLP.modules.decoder.crf + fastNLP.modules.decoder.mlp fastNLP.modules.decoder.utils From ff1d695aa40a5beebea50825c4b5e4d09391ec29 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 16 May 2019 21:45:17 +0800 Subject: [PATCH 137/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20modules?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E7=9A=84=5F=5Fall=5F=5F=20=E5=92=8C=20imp?= =?UTF-8?q?ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/models/base_model.py | 2 +- fastNLP/models/sequence_labeling.py | 8 +- fastNLP/modules/__init__.py | 18 ++-- fastNLP/modules/aggregator/__init__.py | 4 +- fastNLP/modules/aggregator/attention.py | 62 +++++++----- fastNLP/modules/decoder/__init__.py | 6 +- fastNLP/modules/decoder/{CRF.py => crf.py} | 99 ++++++++++--------- fastNLP/modules/decoder/{MLP.py => mlp.py} | 42 +++----- fastNLP/modules/decoder/utils.py | 24 +++-- fastNLP/modules/encoder/__init__.py | 26 ++++- fastNLP/modules/encoder/char_encoder.py | 32 ++++-- fastNLP/modules/encoder/conv_maxpool.py | 29 +++--- fastNLP/modules/encoder/embedding.py | 19 ++-- fastNLP/modules/encoder/lstm.py | 10 +- fastNLP/modules/encoder/star_transformer.py | 71 +++++++------ fastNLP/modules/encoder/transformer.py | 17 ++-- fastNLP/modules/encoder/variational_rnn.py | 66 ++++++++----- .../models/cws_model.py | 6 +- .../models/cws_transformer.py | 4 +- .../main.py | 2 +- test/modules/decoder/test_CRF.py | 10 +- 21 files changed, 321 insertions(+), 236 deletions(-) rename fastNLP/modules/decoder/{CRF.py => crf.py} (87%) rename fastNLP/modules/decoder/{MLP.py => mlp.py} (77%) diff --git a/fastNLP/models/base_model.py b/fastNLP/models/base_model.py index d27f1d21..2646d580 100644 --- a/fastNLP/models/base_model.py +++ b/fastNLP/models/base_model.py @@ -1,6 +1,6 @@ import torch -from ..modules.decoder.MLP import MLP +from ..modules.decoder.mlp import MLP class BaseModel(torch.nn.Module): diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 17f02298..503c79ba 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -6,7 +6,7 @@ import torch.nn as nn from .base_model import BaseModel from ..modules import decoder, encoder -from ..modules.decoder.CRF import allowed_transitions +from ..modules.decoder.crf import allowed_transitions from ..core.utils import seq_len_to_mask from ..core.const import Const as C @@ -35,7 +35,7 @@ class SeqLabeling(BaseModel): self.Embedding = encoder.embedding.Embedding(init_embed) self.Rnn = encoder.lstm.LSTM(self.Embedding.embedding_dim, hidden_size) self.Linear = nn.Linear(hidden_size, num_classes) - self.Crf = decoder.CRF.ConditionalRandomField(num_classes) + self.Crf = decoder.crf.ConditionalRandomField(num_classes) self.mask = None def forward(self, words, seq_len, target): @@ -141,9 +141,9 @@ class AdvSeqLabel(nn.Module): self.Linear2 = nn.Linear(hidden_size * 2 // 3, num_classes) if id2words is None: - self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False) + self.Crf = decoder.crf.ConditionalRandomField(num_classes, include_start_end_trans=False) else: - self.Crf = decoder.CRF.ConditionalRandomField(num_classes, include_start_end_trans=False, + self.Crf = decoder.crf.ConditionalRandomField(num_classes, include_start_end_trans=False, allowed_transitions=allowed_transitions(id2words, encoding_type=encoding_type)) diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index 53d44f47..cd54c8db 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -32,19 +32,25 @@ from .encoder import * from .utils import get_embeddings __all__ = [ - "LSTM", - "Embedding", + # "BertModel", + "ConvolutionCharEncoder", + "LSTMCharEncoder", "ConvMaxpool", - "BertModel", + "Embedding", + "LSTM", + "StarTransformer", + "TransformerEncoder", + "VarRNN", + "VarLSTM", + "VarGRU", "MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention", - "BiAttention", - + "MLP", "ConditionalRandomField", "viterbi_decode", "allowed_transitions", -] \ No newline at end of file +] diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 725ccd4b..117dad83 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -3,12 +3,12 @@ from .pooling import MaxPoolWithMask from .pooling import AvgPool from .pooling import AvgPoolWithMask -from .attention import MultiHeadAttention, BiAttention +from .attention import MultiHeadAttention + __all__ = [ "MaxPool", "MaxPoolWithMask", "AvgPool", "MultiHeadAttention", - "BiAttention" ] diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index cea9c405..a1a7fda8 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -1,4 +1,3 @@ -__all__ =["MultiHeadAttention"] import math import torch @@ -9,12 +8,17 @@ from ..dropout import TimestepDropout from ..utils import initial_parameter +__all__ = [ + "MultiHeadAttention" +] + class DotAttention(nn.Module): """ .. todo:: 补上文档 """ + def __init__(self, key_size, value_size, dropout=0): super(DotAttention, self).__init__() self.key_size = key_size @@ -22,7 +26,7 @@ class DotAttention(nn.Module): self.scale = math.sqrt(key_size) self.drop = nn.Dropout(dropout) self.softmax = nn.Softmax(dim=2) - + def forward(self, Q, K, V, mask_out=None): """ @@ -41,6 +45,8 @@ class DotAttention(nn.Module): class MultiHeadAttention(nn.Module): """ + 别名::class:`fastNLP.modules.MultiHeadAttention` :class:`fastNLP.modules.aggregator.attention.MultiHeadAttention` + :param input_size: int, 输入维度的大小。同时也是输出维度的大小。 :param key_size: int, 每个head的维度大小。 @@ -48,13 +54,14 @@ class MultiHeadAttention(nn.Module): :param num_head: int,head的数量。 :param dropout: float。 """ + def __init__(self, input_size, key_size, value_size, num_head, dropout=0.1): super(MultiHeadAttention, self).__init__() self.input_size = input_size self.key_size = key_size self.value_size = value_size self.num_head = num_head - + in_size = key_size * num_head self.q_in = nn.Linear(input_size, in_size) self.k_in = nn.Linear(input_size, in_size) @@ -64,14 +71,14 @@ class MultiHeadAttention(nn.Module): self.out = nn.Linear(value_size * num_head, input_size) self.drop = TimestepDropout(dropout) self.reset_parameters() - + def reset_parameters(self): sqrt = math.sqrt nn.init.normal_(self.q_in.weight, mean=0, std=sqrt(2.0 / (self.input_size + self.key_size))) nn.init.normal_(self.k_in.weight, mean=0, std=sqrt(2.0 / (self.input_size + self.key_size))) nn.init.normal_(self.v_in.weight, mean=0, std=sqrt(2.0 / (self.input_size + self.value_size))) nn.init.xavier_normal_(self.out.weight) - + def forward(self, Q, K, V, atte_mask_out=None): """ @@ -87,7 +94,7 @@ class MultiHeadAttention(nn.Module): q = self.q_in(Q).view(batch, sq, n_head, d_k) k = self.k_in(K).view(batch, sk, n_head, d_k) v = self.v_in(V).view(batch, sk, n_head, d_v) - + # transpose q, k and v to do batch attention q = q.permute(2, 0, 1, 3).contiguous().view(-1, sq, d_k) k = k.permute(2, 0, 1, 3).contiguous().view(-1, sk, d_k) @@ -95,7 +102,7 @@ class MultiHeadAttention(nn.Module): if atte_mask_out is not None: atte_mask_out = atte_mask_out.repeat(n_head, 1, 1) atte = self.attention(q, k, v, atte_mask_out).view(n_head, batch, sq, d_v) - + # concat all heads, do output linear atte = atte.permute(1, 2, 0, 3).contiguous().view(batch, sq, -1) output = self.drop(self.out(atte)) @@ -104,6 +111,10 @@ class MultiHeadAttention(nn.Module): class BiAttention(nn.Module): r"""Bi Attention module + + .. todo:: + 这个模块的负责人来继续完善一下 + Calculate Bi Attention matrix `e` .. math:: @@ -115,11 +126,11 @@ class BiAttention(nn.Module): \end{array} """ - + def __init__(self): super(BiAttention, self).__init__() self.inf = 10e12 - + def forward(self, in_x1, in_x2, x1_len, x2_len): """ :param torch.Tensor in_x1: [batch_size, x1_seq_len, hidden_size] 第一句的特征表示 @@ -130,36 +141,36 @@ class BiAttention(nn.Module): torch.Tensor out_x2: [batch_size, x2_seq_len, hidden_size] 第一句attend到的特征表示 """ - + assert in_x1.size()[0] == in_x2.size()[0] assert in_x1.size()[2] == in_x2.size()[2] # The batch size and hidden size must be equal. assert in_x1.size()[1] == x1_len.size()[1] and in_x2.size()[1] == x2_len.size()[1] # The seq len in in_x and x_len must be equal. assert in_x1.size()[0] == x1_len.size()[0] and x1_len.size()[0] == x2_len.size()[0] - + batch_size = in_x1.size()[0] x1_max_len = in_x1.size()[1] x2_max_len = in_x2.size()[1] - + in_x2_t = torch.transpose(in_x2, 1, 2) # [batch_size, hidden_size, x2_seq_len] - + attention_matrix = torch.bmm(in_x1, in_x2_t) # [batch_size, x1_seq_len, x2_seq_len] - + a_mask = x1_len.le(0.5).float() * -self.inf # [batch_size, x1_seq_len] a_mask = a_mask.view(batch_size, x1_max_len, -1) a_mask = a_mask.expand(-1, -1, x2_max_len) # [batch_size, x1_seq_len, x2_seq_len] b_mask = x2_len.le(0.5).float() * -self.inf b_mask = b_mask.view(batch_size, -1, x2_max_len) b_mask = b_mask.expand(-1, x1_max_len, -1) # [batch_size, x1_seq_len, x2_seq_len] - + attention_a = F.softmax(attention_matrix + a_mask, dim=2) # [batch_size, x1_seq_len, x2_seq_len] attention_b = F.softmax(attention_matrix + b_mask, dim=1) # [batch_size, x1_seq_len, x2_seq_len] - + out_x1 = torch.bmm(attention_a, in_x2) # [batch_size, x1_seq_len, hidden_size] attention_b_t = torch.transpose(attention_b, 1, 2) out_x2 = torch.bmm(attention_b_t, in_x1) # [batch_size, x2_seq_len, hidden_size] - + return out_x1, out_x2 @@ -173,10 +184,10 @@ class SelfAttention(nn.Module): :param float drop: dropout概率,默认值为0.5 :param str initial_method: 初始化参数方法 """ - - def __init__(self, input_size, attention_unit=300, attention_hops=10, drop=0.5, initial_method=None,): + + def __init__(self, input_size, attention_unit=300, attention_hops=10, drop=0.5, initial_method=None, ): super(SelfAttention, self).__init__() - + self.attention_hops = attention_hops self.ws1 = nn.Linear(input_size, attention_unit, bias=False) self.ws2 = nn.Linear(attention_unit, attention_hops, bias=False) @@ -185,7 +196,7 @@ class SelfAttention(nn.Module): self.drop = nn.Dropout(drop) self.tanh = nn.Tanh() initial_parameter(self, initial_method) - + def _penalization(self, attention): """ compute the penalization term for attention module @@ -199,7 +210,7 @@ class SelfAttention(nn.Module): mat = torch.bmm(attention, attention_t) - self.I[:attention.size(0)] ret = (torch.sum(torch.sum((mat ** 2), 2), 1).squeeze() + 1e-10) ** 0.5 return torch.sum(ret) / size[0] - + def forward(self, input, input_origin): """ :param torch.Tensor input: [baz, senLen, h_dim] 要做attention的矩阵 @@ -209,15 +220,14 @@ class SelfAttention(nn.Module): """ input = input.contiguous() size = input.size() # [bsz, len, nhid] - + input_origin = input_origin.expand(self.attention_hops, -1, -1) # [hops,baz, len] input_origin = input_origin.transpose(0, 1).contiguous() # [baz, hops,len] - + y1 = self.tanh(self.ws1(self.drop(input))) # [baz,len,dim] -->[bsz,len, attention-unit] attention = self.ws2(y1).transpose(1, 2).contiguous() # [bsz,len, attention-unit]--> [bsz, len, hop]--> [baz,hop,len] - + attention = attention + (-999999 * (input_origin == 0).float()) # remove the weight on padding token. attention = F.softmax(attention, 2) # [baz ,hop, len] return torch.bmm(attention, input), self._penalization(attention) # output1 --> [baz ,hop ,nhid] - diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index 516b687a..5df48c43 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,7 +1,7 @@ -from .CRF import ConditionalRandomField -from .MLP import MLP +from .crf import ConditionalRandomField +from .mlp import MLP from .utils import viterbi_decode -from .CRF import allowed_transitions +from .crf import allowed_transitions __all__ = [ "MLP", diff --git a/fastNLP/modules/decoder/CRF.py b/fastNLP/modules/decoder/crf.py similarity index 87% rename from fastNLP/modules/decoder/CRF.py rename to fastNLP/modules/decoder/crf.py index 84f374e6..130ed40e 100644 --- a/fastNLP/modules/decoder/CRF.py +++ b/fastNLP/modules/decoder/crf.py @@ -3,10 +3,15 @@ from torch import nn from ..utils import initial_parameter +__all__ = [ + "ConditionalRandomField", + "allowed_transitions" +] + def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): """ - 别名::class:`fastNLP.modules.allowed_transitions` :class:`fastNLP.modules.decoder.CRF.allowed_transitions` + 别名::class:`fastNLP.modules.allowed_transitions` :class:`fastNLP.modules.decoder.crf.allowed_transitions` 给定一个id到label的映射表,返回所有可以跳转的(from_tag_id, to_tag_id)列表。 @@ -15,8 +20,7 @@ def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): :param str encoding_type: 支持"bio", "bmes", "bmeso"。 :param bool include_start_end: 是否包含开始与结尾的转换。比如在bio中,b/o可以在开头,但是i不能在开头; 为True,返回的结果中会包含(start_idx, b_idx), (start_idx, o_idx), 但是不包含(start_idx, i_idx); - start_idx=len(id2label), end_idx=len(id2label)+1。 - 为False, 返回的结果中不含与开始结尾相关的内容 + start_idx=len(id2label), end_idx=len(id2label)+1。为False, 返回的结果中不含与开始结尾相关的内容 :return: List[Tuple(int, int)]], 内部的Tuple是可以进行跳转的(from_tag_id, to_tag_id)。 """ num_tags = len(id2target) @@ -27,6 +31,7 @@ def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): id_label_lst = list(id2target.items()) if include_start_end: id_label_lst += [(start_idx, 'start'), (end_idx, 'end')] + def split_tag_label(from_label): from_label = from_label.lower() if from_label in ['start', 'end']: @@ -36,7 +41,7 @@ def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): from_tag = from_label[:1] from_label = from_label[2:] return from_tag, from_label - + for from_id, from_label in id_label_lst: if from_label in ['', '']: continue @@ -60,7 +65,7 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label :param str to_label: 比如"PER", "LOC"等label :return: bool,能否跃迁 """ - if to_tag=='start' or from_tag=='end': + if to_tag == 'start' or from_tag == 'end': return False encoding_type = encoding_type.lower() if encoding_type == 'bio': @@ -83,12 +88,12 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label if from_tag == 'start': return to_tag in ('b', 'o') elif from_tag in ['b', 'i']: - return any([to_tag in ['end', 'b', 'o'], to_tag=='i' and from_label==to_label]) + return any([to_tag in ['end', 'b', 'o'], to_tag == 'i' and from_label == to_label]) elif from_tag == 'o': return to_tag in ['end', 'b', 'o'] else: raise ValueError("Unexpect tag {}. Expect only 'B', 'I', 'O'.".format(from_tag)) - + elif encoding_type == 'bmes': """ 第一行是to_tag, 第一列是from_tag,y任意条件下可转,-只有在label相同时可转,n不可转 @@ -111,9 +116,9 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label if from_tag == 'start': return to_tag in ['b', 's'] elif from_tag == 'b': - return to_tag in ['m', 'e'] and from_label==to_label + return to_tag in ['m', 'e'] and from_label == to_label elif from_tag == 'm': - return to_tag in ['m', 'e'] and from_label==to_label + return to_tag in ['m', 'e'] and from_label == to_label elif from_tag in ['e', 's']: return to_tag in ['b', 's', 'end'] else: @@ -122,21 +127,21 @@ def _is_transition_allowed(encoding_type, from_tag, from_label, to_tag, to_label if from_tag == 'start': return to_tag in ['b', 's', 'o'] elif from_tag == 'b': - return to_tag in ['m', 'e'] and from_label==to_label + return to_tag in ['m', 'e'] and from_label == to_label elif from_tag == 'm': - return to_tag in ['m', 'e'] and from_label==to_label + return to_tag in ['m', 'e'] and from_label == to_label elif from_tag in ['e', 's', 'o']: return to_tag in ['b', 's', 'end', 'o'] else: raise ValueError("Unexpect tag type {}. Expect only 'B', 'M', 'E', 'S', 'O'.".format(from_tag)) - + else: raise ValueError("Only support BIO, BMES, BMESO encoding type, got {}.".format(encoding_type)) class ConditionalRandomField(nn.Module): """ - 别名::class:`fastNLP.modules.ConditionalRandomField` :class:`fastNLP.modules.decoder.CRF.ConditionalRandomField` + 别名::class:`fastNLP.modules.ConditionalRandomField` :class:`fastNLP.modules.decoder.crf.ConditionalRandomField` 条件随机场。 提供forward()以及viterbi_decode()两个方法,分别用于训练与inference。 @@ -148,30 +153,31 @@ class ConditionalRandomField(nn.Module): allowed_transitions()函数得到;如果为None,则所有跃迁均为合法 :param str initial_method: 初始化方法。见initial_parameter """ + def __init__(self, num_tags, include_start_end_trans=False, allowed_transitions=None, initial_method=None): super(ConditionalRandomField, self).__init__() - + self.include_start_end_trans = include_start_end_trans self.num_tags = num_tags - + # the meaning of entry in this matrix is (from_tag_id, to_tag_id) score self.trans_m = nn.Parameter(torch.randn(num_tags, num_tags)) if self.include_start_end_trans: self.start_scores = nn.Parameter(torch.randn(num_tags)) self.end_scores = nn.Parameter(torch.randn(num_tags)) - + if allowed_transitions is None: constrain = torch.zeros(num_tags + 2, num_tags + 2) else: - constrain = torch.full((num_tags+2, num_tags+2), fill_value=-10000.0, dtype=torch.float) + constrain = torch.full((num_tags + 2, num_tags + 2), fill_value=-10000.0, dtype=torch.float) for from_tag_id, to_tag_id in allowed_transitions: constrain[from_tag_id, to_tag_id] = 0 self._constrain = nn.Parameter(constrain, requires_grad=False) - + initial_parameter(self, initial_method) - + def _normalizer_likelihood(self, logits, mask): """Computes the (batch_size,) denominator term for the log-likelihood, which is the sum of the likelihoods across all possible state sequences. @@ -184,21 +190,21 @@ class ConditionalRandomField(nn.Module): alpha = logits[0] if self.include_start_end_trans: alpha = alpha + self.start_scores.view(1, -1) - + flip_mask = mask.eq(0) - + for i in range(1, seq_len): emit_score = logits[i].view(batch_size, 1, n_tags) trans_score = self.trans_m.view(1, n_tags, n_tags) tmp = alpha.view(batch_size, n_tags, 1) + emit_score + trans_score alpha = torch.logsumexp(tmp, 1).masked_fill(flip_mask[i].view(batch_size, 1), 0) + \ alpha.masked_fill(mask[i].byte().view(batch_size, 1), 0) - + if self.include_start_end_trans: alpha = alpha + self.end_scores.view(1, -1) - + return torch.logsumexp(alpha, 1) - + def _gold_score(self, logits, tags, mask): """ Compute the score for the gold path. @@ -210,15 +216,15 @@ class ConditionalRandomField(nn.Module): seq_len, batch_size, _ = logits.size() batch_idx = torch.arange(batch_size, dtype=torch.long, device=logits.device) seq_idx = torch.arange(seq_len, dtype=torch.long, device=logits.device) - + # trans_socre [L-1, B] mask = mask.byte() flip_mask = mask.eq(0) - trans_score = self.trans_m[tags[:seq_len-1], tags[1:]].masked_fill(flip_mask[1:, :], 0) + trans_score = self.trans_m[tags[:seq_len - 1], tags[1:]].masked_fill(flip_mask[1:, :], 0) # emit_score [L, B] - emit_score = logits[seq_idx.view(-1,1), batch_idx.view(1,-1), tags].masked_fill(flip_mask, 0) + emit_score = logits[seq_idx.view(-1, 1), batch_idx.view(1, -1), tags].masked_fill(flip_mask, 0) # score [L-1, B] - score = trans_score + emit_score[:seq_len-1, :] + score = trans_score + emit_score[:seq_len - 1, :] score = score.sum(0) + emit_score[-1].masked_fill(flip_mask[-1], 0) if self.include_start_end_trans: st_scores = self.start_scores.view(1, -1).repeat(batch_size, 1)[batch_idx, tags[0]] @@ -227,24 +233,24 @@ class ConditionalRandomField(nn.Module): score = score + st_scores + ed_scores # return [B,] return score - + def forward(self, feats, tags, mask): """ 用于计算CRF的前向loss,返回值为一个batch_size的FloatTensor,可能需要mean()求得loss。 - :param torch.FloatTensor feats:batch_size x max_len x num_tags,特征矩阵。 + :param torch.FloatTensor feats: batch_size x max_len x num_tags,特征矩阵。 :param torch.LongTensor tags: batch_size x max_len,标签矩阵。 :param torch.ByteTensor mask: batch_size x max_len,为0的位置认为是padding。 - :return:torch.FloatTensor, (batch_size,) + :return: torch.FloatTensor, (batch_size,) """ feats = feats.transpose(0, 1) tags = tags.transpose(0, 1).long() mask = mask.transpose(0, 1).float() all_path_score = self._normalizer_likelihood(feats, mask) gold_path_score = self._gold_score(feats, tags, mask) - + return all_path_score - gold_path_score - + def viterbi_decode(self, logits, mask, unpad=False): """给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 @@ -259,9 +265,9 @@ class ConditionalRandomField(nn.Module): """ batch_size, seq_len, n_tags = logits.size() - logits = logits.transpose(0, 1).data # L, B, H - mask = mask.transpose(0, 1).data.byte() # L, B - + logits = logits.transpose(0, 1).data # L, B, H + mask = mask.transpose(0, 1).data.byte() # L, B + # dp vpath = logits.new_zeros((seq_len, batch_size, n_tags), dtype=torch.long) vscore = logits[0] @@ -269,8 +275,8 @@ class ConditionalRandomField(nn.Module): transitions[:n_tags, :n_tags] += self.trans_m.data if self.include_start_end_trans: transitions[n_tags, :n_tags] += self.start_scores.data - transitions[:n_tags, n_tags+1] += self.end_scores.data - + transitions[:n_tags, n_tags + 1] += self.end_scores.data + vscore += transitions[n_tags, :n_tags] trans_score = transitions[:n_tags, :n_tags].view(1, n_tags, n_tags).data for i in range(1, seq_len): @@ -280,30 +286,29 @@ class ConditionalRandomField(nn.Module): best_score, best_dst = score.max(1) vpath[i] = best_dst vscore = best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ - vscore.masked_fill(mask[i].view(batch_size, 1), 0) - + vscore.masked_fill(mask[i].view(batch_size, 1), 0) + if self.include_start_end_trans: - vscore += transitions[:n_tags, n_tags+1].view(1, -1) - + vscore += transitions[:n_tags, n_tags + 1].view(1, -1) + # backtrace 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 - + idxes = (lens.view(1, -1) - seq_idx.view(-1, 1)) % seq_len + 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): last_tags = vpath[idxes[i], batch_idx, last_tags] - ans[idxes[i+1], batch_idx] = last_tags + ans[idxes[i + 1], batch_idx] = last_tags ans = ans.transpose(0, 1) if unpad: paths = [] for idx, seq_len in enumerate(lens): - paths.append(ans[idx, :seq_len+1].tolist()) + paths.append(ans[idx, :seq_len + 1].tolist()) else: paths = ans return paths, ans_score - diff --git a/fastNLP/modules/decoder/MLP.py b/fastNLP/modules/decoder/mlp.py similarity index 77% rename from fastNLP/modules/decoder/MLP.py rename to fastNLP/modules/decoder/mlp.py index 71d899b0..27019432 100644 --- a/fastNLP/modules/decoder/MLP.py +++ b/fastNLP/modules/decoder/mlp.py @@ -3,20 +3,23 @@ import torch.nn as nn from ..utils import initial_parameter +__all__ = [ + "MLP" +] + class MLP(nn.Module): """ - 别名::class:`fastNLP.modules.MLP` :class:`fastNLP.modules.decoder.MLP.MLP` + 别名::class:`fastNLP.modules.MLP` :class:`fastNLP.modules.decoder.mlp.MLP` 多层感知器 - :param list size_layer: 一个int的列表,用来定义MLP的层数,列表中的数字为每一层是hidden数目。MLP的层数为 len(size_layer) - 1 - :param str or list activation: - 一个字符串或者函数或者字符串跟函数的列表,用来定义每一个隐层的激活函数,字符串包括relu,tanh和sigmoid,默认值为relu - :param str or function output_activation : 字符串或者函数,用来定义输出层的激活函数,默认值为None,表示输出层没有激活函数 + :param List[int] size_layer: 一个int的列表,用来定义MLP的层数,列表中的数字为每一层是hidden数目。MLP的层数为 len(size_layer) - 1 + :param Union[str,func,List[str]] activation: 一个字符串或者函数的列表,用来定义每一个隐层的激活函数,字符串包括relu,tanh和sigmoid,默认值为relu + :param Union[str,func] output_activation: 字符串或者函数,用来定义输出层的激活函数,默认值为None,表示输出层没有激活函数 :param str initial_method: 参数初始化方式 :param float dropout: dropout概率,默认值为0 - + .. note:: 隐藏层的激活函数通过activation定义。一个str/function或者一个str/function的list可以被传入activation。 如果只传入了一个str/function,那么所有隐藏层的激活函数都由这个str/function定义; @@ -35,10 +38,8 @@ class MLP(nn.Module): >>> y = net(x) >>> print(x) >>> print(y) - >>> - """ - + def __init__(self, size_layer, activation='relu', output_activation=None, initial_method=None, dropout=0.0): super(MLP, self).__init__() self.hiddens = nn.ModuleList() @@ -46,12 +47,12 @@ class MLP(nn.Module): self.output_activation = output_activation for i in range(1, len(size_layer)): if i + 1 == len(size_layer): - self.output = nn.Linear(size_layer[i-1], size_layer[i]) + self.output = nn.Linear(size_layer[i - 1], size_layer[i]) else: - self.hiddens.append(nn.Linear(size_layer[i-1], size_layer[i])) - + self.hiddens.append(nn.Linear(size_layer[i - 1], size_layer[i])) + self.dropout = nn.Dropout(p=dropout) - + actives = { 'relu': nn.ReLU(), 'tanh': nn.Tanh(), @@ -80,7 +81,7 @@ class MLP(nn.Module): else: raise ValueError("should set activation correctly: {}".format(activation)) initial_parameter(self, initial_method) - + def forward(self, x): """ :param torch.Tensor x: MLP接受的输入 @@ -93,16 +94,3 @@ class MLP(nn.Module): x = self.output_activation(x) x = self.dropout(x) return x - - -if __name__ == '__main__': - net1 = MLP([5, 10, 5]) - net2 = MLP([5, 10, 5], 'tanh') - net3 = MLP([5, 6, 7, 8, 5], 'tanh') - net4 = MLP([5, 6, 7, 8, 5], 'relu', output_activation='tanh') - net5 = MLP([5, 6, 7, 8, 5], ['tanh', 'relu', 'tanh'], 'tanh') - for net in [net1, net2, net3, net4, net5]: - x = torch.randn(5, 5) - y = net(x) - print(x) - print(y) diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index a749fa88..434873c7 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -1,10 +1,13 @@ -__all__ = ["viterbi_decode"] import torch +__all__ = [ + "viterbi_decode" +] + def viterbi_decode(logits, transitions, mask=None, unpad=False): - """ - 别名::class:`fastNLP.modules.viterbi_decode` :class:`fastNLP.modules.decoder.utils.viterbi_decode + r""" + 别名::class:`fastNLP.modules.viterbi_decode` :class:`fastNLP.modules.decoder.utils.viterbi_decode` 给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 @@ -20,18 +23,19 @@ def viterbi_decode(logits, transitions, mask=None, unpad=False): """ 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." + assert n_tags == transitions.size(0) and n_tags == transitions.size( + 1), "The shapes of transitions and feats are not " \ + "compatible." 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 = logits.new_ones((seq_len, batch_size), dtype=torch.uint8) - + # dp 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) @@ -41,14 +45,14 @@ def viterbi_decode(logits, transitions, mask=None, unpad=False): vpath[i] = best_dst vscore = best_score.masked_fill(mask[i].eq(0).view(batch_size, 1), 0) + \ vscore.masked_fill(mask[i].view(batch_size, 1), 0) - + # backtrace 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 = logits.new_empty((seq_len, batch_size), dtype=torch.long) ans_score, last_tags = vscore.max(1) ans[idxes[0], batch_idx] = last_tags @@ -62,4 +66,4 @@ def viterbi_decode(logits, transitions, mask=None, unpad=False): paths.append(ans[idx, :seq_len + 1].tolist()) else: paths = ans - return paths, ans_score \ No newline at end of file + return paths, ans_score diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index 67f69850..3d65867a 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -1,11 +1,29 @@ +from .bert import BertModel +from .char_encoder import ConvolutionCharEncoder, LSTMCharEncoder from .conv_maxpool import ConvMaxpool from .embedding import Embedding from .lstm import LSTM -from .bert import BertModel +from .star_transformer import StarTransformer +from .transformer import TransformerEncoder +from .variational_rnn import VarRNN, VarLSTM, VarGRU __all__ = [ - "LSTM", - "Embedding", + # "BertModel", + + "ConvolutionCharEncoder", + "LSTMCharEncoder", + "ConvMaxpool", - "BertModel" + + "Embedding", + + "LSTM", + + "StarTransformer", + + "TransformerEncoder", + + "VarRNN", + "VarLSTM", + "VarGRU" ] diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index b5941547..8aefd284 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -1,8 +1,13 @@ import torch -from torch import nn +import torch.nn as nn from ..utils import initial_parameter +__all__ = [ + "ConvolutionCharEncoder", + "LSTMCharEncoder" +] + # from torch.nn.init import xavier_uniform class ConvolutionCharEncoder(nn.Module): @@ -10,20 +15,22 @@ class ConvolutionCharEncoder(nn.Module): 别名::class:`fastNLP.modules.ConvolutionCharEncoder` :class:`fastNLP.modules.encoder.char_encoder.ConvolutionCharEncoder` char级别的卷积编码器. + :param int char_emb_size: char级别embedding的维度. Default: 50 - 例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. + :例: 有26个字符, 每一个的embedding是一个50维的向量, 所以输入的向量维度为50. :param tuple feature_maps: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的filter. :param tuple kernels: 一个由int组成的tuple. tuple的长度是char级别卷积操作的数目, 第`i`个int表示第`i`个卷积操作的卷积核. :param initial_method: 初始化参数的方式, 默认为`xavier normal` """ + def __init__(self, char_emb_size=50, feature_maps=(40, 30, 30), kernels=(3, 4, 5), initial_method=None): super(ConvolutionCharEncoder, self).__init__() self.convs = nn.ModuleList([ nn.Conv2d(1, feature_maps[i], kernel_size=(char_emb_size, kernels[i]), bias=True, padding=(0, 4)) for i in range(len(kernels))]) - + initial_parameter(self, initial_method) - + def forward(self, x): """ :param torch.Tensor x: ``[batch_size * sent_length, word_length, char_emb_size]`` 输入字符的embedding @@ -34,7 +41,7 @@ class ConvolutionCharEncoder(nn.Module): x = x.transpose(2, 3) # [batch_size*sent_length, channel, height, width] return self._convolute(x).unsqueeze(2) - + def _convolute(self, x): feats = [] for conv in self.convs: @@ -50,7 +57,14 @@ class ConvolutionCharEncoder(nn.Module): class LSTMCharEncoder(nn.Module): - """char级别基于LSTM的encoder.""" + """ + 别名::class:`fastNLP.modules.LSTMCharEncoder` :class:`fastNLP.modules.encoder.char_encoder.LSTMCharEncoder` + + char级别基于LSTM的encoder. + + + """ + def __init__(self, char_emb_size=50, hidden_size=None, initial_method=None): """ :param int char_emb_size: char级别embedding的维度. Default: 50 @@ -60,14 +74,14 @@ class LSTMCharEncoder(nn.Module): """ super(LSTMCharEncoder, self).__init__() self.hidden_size = char_emb_size if hidden_size is None else hidden_size - + self.lstm = nn.LSTM(input_size=char_emb_size, hidden_size=self.hidden_size, num_layers=1, bias=True, batch_first=True) initial_parameter(self, initial_method) - + def forward(self, x): """ :param torch.Tensor x: ``[ n_batch*n_word, word_length, char_emb_size]`` 输入字符的embedding @@ -78,6 +92,6 @@ class LSTMCharEncoder(nn.Module): h0 = nn.init.orthogonal_(h0) c0 = torch.empty(1, batch_size, self.hidden_size) c0 = nn.init.orthogonal_(c0) - + _, hidden = self.lstm(x, (h0, c0)) return hidden[0].squeeze().unsqueeze(2) diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index 5ecd376d..5e714e88 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -1,12 +1,13 @@ -# python: 3.6 -# encoding: utf-8 - import torch import torch.nn as nn import torch.nn.functional as F from ..utils import initial_parameter +__all__ = [ + "ConvMaxpool" +] + class ConvMaxpool(nn.Module): """ @@ -27,22 +28,24 @@ class ConvMaxpool(nn.Module): :param str activation: Convolution后的结果将通过该activation后再经过max-pooling。支持relu, sigmoid, tanh :param str initial_method: str。 """ + def __init__(self, in_channels, out_channels, kernel_sizes, stride=1, padding=0, dilation=1, groups=1, bias=True, activation="relu", initial_method=None): super(ConvMaxpool, self).__init__() - + # convolution if isinstance(kernel_sizes, (list, tuple, int)): if isinstance(kernel_sizes, int) and isinstance(out_channels, int): out_channels = [out_channels] kernel_sizes = [kernel_sizes] elif isinstance(kernel_sizes, (tuple, list)) and isinstance(out_channels, (tuple, list)): - assert len(out_channels)==len(kernel_sizes), "The number of out_channels should be equal to the number" \ - " of kernel_sizes." + assert len(out_channels) == len( + kernel_sizes), "The number of out_channels should be equal to the number" \ + " of kernel_sizes." else: raise ValueError("The type of out_channels and kernel_sizes should be the same.") - + self.convs = nn.ModuleList([nn.Conv1d( in_channels=in_channels, out_channels=oc, @@ -53,11 +56,11 @@ class ConvMaxpool(nn.Module): groups=groups, bias=bias) for oc, ks in zip(out_channels, kernel_sizes)]) - + else: raise Exception( 'Incorrect kernel sizes: should be list, tuple or int') - + # activation function if activation == 'relu': self.activation = F.relu @@ -68,9 +71,9 @@ class ConvMaxpool(nn.Module): else: raise Exception( "Undefined activation function: choose from: relu, tanh, sigmoid") - + initial_parameter(self, initial_method) - + def forward(self, x, mask=None): """ @@ -83,9 +86,9 @@ class ConvMaxpool(nn.Module): # convolution xs = [self.activation(conv(x)) for conv in self.convs] # [[N,C,L], ...] if mask is not None: - mask = mask.unsqueeze(1) # B x 1 x L + mask = mask.unsqueeze(1) # B x 1 x L xs = [x.masked_fill_(mask, float('-inf')) for x in xs] # max-pooling xs = [F.max_pool1d(input=i, kernel_size=i.size(2)).squeeze(2) for i in xs] # [[N, C], ...] - return torch.cat(xs, dim=-1) # [N, C] \ No newline at end of file + return torch.cat(xs, dim=-1) # [N, C] diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index c402f318..9fa89e7f 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -1,14 +1,19 @@ import torch.nn as nn from ..utils import get_embeddings +__all__ = [ + "Embedding" +] + + class Embedding(nn.Embedding): """ 别名::class:`fastNLP.modules.Embedding` :class:`fastNLP.modules.encoder.embedding.Embedding` Embedding组件. 可以通过self.num_embeddings获取词表大小; self.embedding_dim获取embedding的维度""" - + def __init__(self, init_embed, padding_idx=None, dropout=0.0, sparse=False, max_norm=None, norm_type=2, - scale_grad_by_freq=False): + scale_grad_by_freq=False): """ :param tuple(int,int),torch.FloatTensor,nn.Embedding,numpy.ndarray init_embed: Embedding的大小(传入tuple(int, int), @@ -22,14 +27,14 @@ class Embedding(nn.Embedding): """ embed = get_embeddings(init_embed) num_embeddings, embedding_dim = embed.weight.size() - + super().__init__(num_embeddings, embedding_dim, padding_idx=padding_idx, - max_norm=max_norm, norm_type=norm_type, scale_grad_by_freq=scale_grad_by_freq, - sparse=sparse, _weight=embed.weight.data) + max_norm=max_norm, norm_type=norm_type, scale_grad_by_freq=scale_grad_by_freq, + sparse=sparse, _weight=embed.weight.data) del embed - + self.dropout = nn.Dropout(dropout) - + def forward(self, x): """ :param torch.LongTensor x: [batch, seq_len] diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index c853c142..bc9cb155 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -1,4 +1,5 @@ -"""轻量封装的 Pytorch LSTM 模块. +""" +轻量封装的 Pytorch LSTM 模块. 可在 forward 时传入序列的长度, 自动对padding做合适的处理. """ import torch @@ -7,6 +8,10 @@ import torch.nn.utils.rnn as rnn from ..utils import initial_parameter +__all__ = [ + "LSTM" +] + class LSTM(nn.Module): """ @@ -23,6 +28,7 @@ class LSTM(nn.Module): :(batch, seq, feature). Default: ``False`` :param bias: 如果为 ``False``, 模型将不会使用bias. Default: ``True`` """ + def __init__(self, input_size, hidden_size=100, num_layers=1, dropout=0.0, batch_first=True, bidirectional=False, bias=True, initial_method=None): super(LSTM, self).__init__() @@ -30,7 +36,7 @@ class LSTM(nn.Module): self.lstm = nn.LSTM(input_size, hidden_size, num_layers, bias=bias, batch_first=batch_first, dropout=dropout, bidirectional=bidirectional) initial_parameter(self, initial_method) - + def forward(self, x, seq_len=None, h0=None, c0=None): """ diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index f0d8e38b..677af48a 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -1,9 +1,14 @@ -"""Star-Transformer 的encoder部分的 Pytorch 实现 """ +Star-Transformer 的encoder部分的 Pytorch 实现 +""" +import numpy as NP import torch from torch import nn from torch.nn import functional as F -import numpy as NP + +__all__ = [ + "StarTransformer" +] class StarTransformer(nn.Module): @@ -24,10 +29,11 @@ class StarTransformer(nn.Module): 模型会为输入序列加上position embedding。 若为`None`,忽略加上position embedding的步骤. Default: `None` """ + def __init__(self, hidden_size, num_layers, num_head, head_dim, dropout=0.1, max_len=None): super(StarTransformer, self).__init__() self.iters = num_layers - + self.norm = nn.ModuleList([nn.LayerNorm(hidden_size) for _ in range(self.iters)]) self.ring_att = nn.ModuleList( [_MSA1(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) @@ -35,12 +41,12 @@ class StarTransformer(nn.Module): self.star_att = nn.ModuleList( [_MSA2(hidden_size, nhead=num_head, head_dim=head_dim, dropout=dropout) for _ in range(self.iters)]) - + if max_len is not None: self.pos_emb = self.pos_emb = nn.Embedding(max_len, hidden_size) else: self.pos_emb = None - + def forward(self, data, mask): """ :param FloatTensor data: [batch, length, hidden] 输入的序列 @@ -50,20 +56,21 @@ class StarTransformer(nn.Module): [batch, hidden] 全局 relay 节点, 详见论文 """ + def norm_func(f, x): # B, H, L, 1 return f(x.permute(0, 2, 3, 1)).permute(0, 3, 1, 2) - + B, L, H = data.size() - mask = (mask == 0) # flip the mask for masked_fill_ + mask = (mask == 0) # flip the mask for masked_fill_ smask = torch.cat([torch.zeros(B, 1, ).byte().to(mask), mask], 1) - - embs = data.permute(0, 2, 1)[:,:,:,None] # B H L 1 + + embs = data.permute(0, 2, 1)[:, :, :, None] # B H L 1 if self.pos_emb: - P = self.pos_emb(torch.arange(L, dtype=torch.long, device=embs.device)\ - .view(1, L)).permute(0, 2, 1).contiguous()[:, :, :, None] # 1 H L 1 + P = self.pos_emb(torch.arange(L, dtype=torch.long, device=embs.device) \ + .view(1, L)).permute(0, 2, 1).contiguous()[:, :, :, None] # 1 H L 1 embs = embs + P - + nodes = embs relay = embs.mean(2, keepdim=True) ex_mask = mask[:, None, :, None].expand(B, H, L, 1) @@ -72,11 +79,11 @@ class StarTransformer(nn.Module): ax = torch.cat([r_embs, relay.expand(B, H, 1, L)], 2) nodes = nodes + F.leaky_relu(self.ring_att[i](norm_func(self.norm[i], nodes), ax=ax)) relay = F.leaky_relu(self.star_att[i](relay, torch.cat([relay, nodes], 2), smask)) - + nodes = nodes.masked_fill_(ex_mask, 0) - + nodes = nodes.view(B, H, L).permute(0, 2, 1) - + return nodes, relay.view(B, H) @@ -89,37 +96,37 @@ class _MSA1(nn.Module): self.WK = nn.Conv2d(nhid, nhead * head_dim, 1) self.WV = nn.Conv2d(nhid, nhead * head_dim, 1) self.WO = nn.Conv2d(nhead * head_dim, nhid, 1) - + self.drop = nn.Dropout(dropout) - + # print('NUM_HEAD', nhead, 'DIM_HEAD', head_dim) self.nhid, self.nhead, self.head_dim, self.unfold_size = nhid, nhead, head_dim, 3 - + def forward(self, x, ax=None): # x: B, H, L, 1, ax : B, H, X, L append features nhid, nhead, head_dim, unfold_size = self.nhid, self.nhead, self.head_dim, self.unfold_size B, H, L, _ = x.shape - + q, k, v = self.WQ(x), self.WK(x), self.WV(x) # x: (B,H,L,1) - + if ax is not None: aL = ax.shape[2] ak = self.WK(ax).view(B, nhead, head_dim, aL, L) av = self.WV(ax).view(B, nhead, head_dim, aL, L) q = q.view(B, nhead, head_dim, 1, L) - k = F.unfold(k.view(B, nhead * head_dim, L, 1), (unfold_size, 1), padding=(unfold_size // 2, 0))\ - .view(B, nhead, head_dim, unfold_size, L) - v = F.unfold(v.view(B, nhead * head_dim, L, 1), (unfold_size, 1), padding=(unfold_size // 2, 0))\ - .view(B, nhead, head_dim, unfold_size, L) + k = F.unfold(k.view(B, nhead * head_dim, L, 1), (unfold_size, 1), padding=(unfold_size // 2, 0)) \ + .view(B, nhead, head_dim, unfold_size, L) + v = F.unfold(v.view(B, nhead * head_dim, L, 1), (unfold_size, 1), padding=(unfold_size // 2, 0)) \ + .view(B, nhead, head_dim, unfold_size, L) if ax is not None: k = torch.cat([k, ak], 3) v = torch.cat([v, av], 3) - + alphas = self.drop(F.softmax((q * k).sum(2, keepdim=True) / NP.sqrt(head_dim), 3)) # B N L 1 U att = (alphas * v).sum(3).view(B, nhead * head_dim, L, 1) - + ret = self.WO(att) - + return ret @@ -131,19 +138,19 @@ class _MSA2(nn.Module): self.WK = nn.Conv2d(nhid, nhead * head_dim, 1) self.WV = nn.Conv2d(nhid, nhead * head_dim, 1) self.WO = nn.Conv2d(nhead * head_dim, nhid, 1) - + self.drop = nn.Dropout(dropout) - + # print('NUM_HEAD', nhead, 'DIM_HEAD', head_dim) self.nhid, self.nhead, self.head_dim, self.unfold_size = nhid, nhead, head_dim, 3 - + def forward(self, x, y, mask=None): # x: B, H, 1, 1, 1 y: B H L 1 nhid, nhead, head_dim, unfold_size = self.nhid, self.nhead, self.head_dim, self.unfold_size B, H, L, _ = y.shape - + q, k, v = self.WQ(x), self.WK(y), self.WV(y) - + q = q.view(B, nhead, 1, head_dim) # B, H, 1, 1 -> B, N, 1, h k = k.view(B, nhead, head_dim, L) # B, H, L, 1 -> B, N, h, L v = v.view(B, nhead, head_dim, L).permute(0, 1, 3, 2) # B, H, L, 1 -> B, N, L, h diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index 7dcae342..2532d90a 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -3,6 +3,10 @@ from torch import nn from ..aggregator.attention import MultiHeadAttention from ..dropout import TimestepDropout +__all__ = [ + "TransformerEncoder" +] + class TransformerEncoder(nn.Module): """ @@ -19,6 +23,7 @@ class TransformerEncoder(nn.Module): :param int num_head: head的数量。 :param float dropout: dropout概率. Default: 0.1 """ + class SubLayer(nn.Module): def __init__(self, model_size, inner_size, key_size, value_size, num_head, dropout=0.1): super(TransformerEncoder.SubLayer, self).__init__() @@ -27,9 +32,9 @@ class TransformerEncoder(nn.Module): self.ffn = nn.Sequential(nn.Linear(model_size, inner_size), nn.ReLU(), nn.Linear(inner_size, model_size), - TimestepDropout(dropout),) + TimestepDropout(dropout), ) self.norm2 = nn.LayerNorm(model_size) - + def forward(self, input, seq_mask=None, atte_mask_out=None): """ @@ -44,11 +49,11 @@ class TransformerEncoder(nn.Module): output = self.norm2(output + norm_atte) output *= seq_mask return output - + def __init__(self, num_layers, **kargs): super(TransformerEncoder, self).__init__() self.layers = nn.ModuleList([self.SubLayer(**kargs) for _ in range(num_layers)]) - + def forward(self, x, seq_mask=None): """ :param x: [batch, seq_len, model_size] 输入序列 @@ -60,8 +65,8 @@ class TransformerEncoder(nn.Module): if seq_mask is None: atte_mask_out = None else: - atte_mask_out = (seq_mask < 1)[:,None,:] - seq_mask = seq_mask[:,:,None] + atte_mask_out = (seq_mask < 1)[:, None, :] + seq_mask = seq_mask[:, :, None] for layer in self.layers: output = layer(output, seq_mask, atte_mask_out) return output diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index b926ba9e..60cdf9c5 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -1,9 +1,9 @@ -"""Variational RNN 的 Pytorch 实现 +""" +Variational RNN 的 Pytorch 实现 """ import torch import torch.nn as nn from torch.nn.utils.rnn import PackedSequence, pack_padded_sequence, pad_packed_sequence -from ..utils import initial_parameter try: from torch import flip @@ -14,18 +14,27 @@ except ImportError: indices[dim] = torch.arange(x.size(dim) - 1, -1, -1, dtype=torch.long, device=x.device) return x[tuple(indices)] +from ..utils import initial_parameter + +__all__ = [ + "VarRNN", + "VarLSTM", + "VarGRU" +] + class VarRnnCellWrapper(nn.Module): - """Wrapper for normal RNN Cells, make it support variational dropout """ - + Wrapper for normal RNN Cells, make it support variational dropout + """ + def __init__(self, cell, hidden_size, input_p, hidden_p): super(VarRnnCellWrapper, self).__init__() self.cell = cell self.hidden_size = hidden_size self.input_p = input_p self.hidden_p = hidden_p - + def forward(self, input_x, hidden, mask_x, mask_h, is_reversed=False): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] @@ -37,11 +46,13 @@ class VarRnnCellWrapper(nn.Module): hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ + def get_hi(hi, h0, size): h0_size = size - hi.size(0) if h0_size > 0: return torch.cat([hi, h0[:h0_size]], dim=0) return hi[:size] + is_lstm = isinstance(hidden, tuple) input, batch_sizes = input_x.data, input_x.batch_sizes output = [] @@ -52,7 +63,7 @@ class VarRnnCellWrapper(nn.Module): else: batch_iter = batch_sizes idx = 0 - + if is_lstm: hn = (hidden[0].clone(), hidden[1].clone()) else: @@ -60,10 +71,10 @@ class VarRnnCellWrapper(nn.Module): hi = hidden for size in batch_iter: if is_reversed: - input_i = input[idx-size: idx] * mask_x[:size] + input_i = input[idx - size: idx] * mask_x[:size] idx -= size else: - input_i = input[idx: idx+size] * mask_x[:size] + input_i = input[idx: idx + size] * mask_x[:size] idx += size mask_hi = mask_h[:size] if is_lstm: @@ -78,7 +89,7 @@ class VarRnnCellWrapper(nn.Module): hi = cell(input_i, hi) hn[:size] = hi output.append(hi) - + if is_reversed: output = list(reversed(output)) output = torch.cat(output, dim=0) @@ -86,7 +97,9 @@ class VarRnnCellWrapper(nn.Module): class VarRNNBase(nn.Module): - """Variational Dropout RNN 实现. + """ + Variational Dropout RNN 实现. + 论文参考: `A Theoretically Grounded Application of Dropout in Recurrent Neural Networks (Yarin Gal and Zoubin Ghahramani, 2016) https://arxiv.org/abs/1512.05287`. @@ -102,7 +115,7 @@ class VarRNNBase(nn.Module): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, mode, Cell, input_size, hidden_size, num_layers=1, bias=True, batch_first=False, input_dropout=0, hidden_dropout=0, bidirectional=False): @@ -125,7 +138,7 @@ class VarRNNBase(nn.Module): self._all_cells.append(VarRnnCellWrapper(cell, self.hidden_size, input_dropout, hidden_dropout)) initial_parameter(self) self.is_lstm = (self.mode == "LSTM") - + def _forward_one(self, n_layer, n_direction, input, hx, mask_x, mask_h): is_lstm = self.is_lstm idx = self.num_directions * n_layer + n_direction @@ -133,7 +146,7 @@ class VarRNNBase(nn.Module): hi = (hx[0][idx], hx[1][idx]) if is_lstm else hx[idx] output_x, hidden_x = cell(input, hi, mask_x, mask_h, is_reversed=(n_direction == 1)) return output_x, hidden_x - + def forward(self, x, hx=None): """ @@ -152,19 +165,19 @@ class VarRNNBase(nn.Module): else: max_batch_size = int(input.batch_sizes[0]) input, batch_sizes = input.data, input.batch_sizes - + if hx is None: hx = x.new_zeros(self.num_layers * self.num_directions, max_batch_size, self.hidden_size, requires_grad=True) if is_lstm: hx = (hx, hx.new_zeros(hx.size(), requires_grad=True)) - + mask_x = x.new_ones((max_batch_size, self.input_size)) mask_out = x.new_ones((max_batch_size, self.hidden_size * self.num_directions)) mask_h_ones = x.new_ones((max_batch_size, self.hidden_size)) nn.functional.dropout(mask_x, p=self.input_dropout, training=self.training, inplace=True) nn.functional.dropout(mask_out, p=self.hidden_dropout, training=self.training, inplace=True) - + hidden = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) if is_lstm: cellstate = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) @@ -183,18 +196,19 @@ class VarRNNBase(nn.Module): else: hidden[idx] = hidden_x x = torch.cat(output_list, dim=-1) - + if is_lstm: hidden = (hidden, cellstate) - + if is_packed: output = PackedSequence(x, batch_sizes) else: x = PackedSequence(x, batch_sizes) output, _ = pad_packed_sequence(x, batch_first=self.batch_first) - + return output, hidden + class VarLSTM(VarRNNBase): """ 别名::class:`fastNLP.modules.VarLSTM` :class:`fastNLP.modules.encoder.variational_rnn.VarLSTM` @@ -211,10 +225,10 @@ class VarLSTM(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarLSTM, self).__init__(mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarLSTM, self).forward(x, hx) @@ -235,13 +249,14 @@ class VarRNN(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarRNN, self).__init__(mode="RNN", Cell=nn.RNNCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarRNN, self).forward(x, hx) + class VarGRU(VarRNNBase): """ 别名::class:`fastNLP.modules.VarGRU` :class:`fastNLP.modules.encoder.variational_rnn.VarGRU` @@ -258,10 +273,9 @@ class VarGRU(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarGRU, self).__init__(mode="GRU", Cell=nn.GRUCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarGRU, self).forward(x, hx) - diff --git a/reproduction/Chinese_word_segmentation/models/cws_model.py b/reproduction/Chinese_word_segmentation/models/cws_model.py index 13632207..b41ad87d 100644 --- a/reproduction/Chinese_word_segmentation/models/cws_model.py +++ b/reproduction/Chinese_word_segmentation/models/cws_model.py @@ -3,7 +3,7 @@ import torch from torch import nn from fastNLP.models.base_model import BaseModel -from fastNLP.modules.decoder.MLP import MLP +from fastNLP.modules.decoder.mlp import MLP from reproduction.Chinese_word_segmentation.utils import seq_lens_to_mask @@ -120,8 +120,8 @@ class CWSBiLSTMSegApp(BaseModel): return {'pred_tags': pred_tags} -from fastNLP.modules.decoder.CRF import ConditionalRandomField -from fastNLP.modules.decoder.CRF import allowed_transitions +from fastNLP.modules.decoder.crf import ConditionalRandomField +from fastNLP.modules.decoder.crf import allowed_transitions class CWSBiLSTMCRF(BaseModel): def __init__(self, vocab_num, embed_dim=100, bigram_vocab_num=None, bigram_embed_dim=100, num_bigram_per_char=None, diff --git a/reproduction/Chinese_word_segmentation/models/cws_transformer.py b/reproduction/Chinese_word_segmentation/models/cws_transformer.py index f6c2dab6..e8ae5ecc 100644 --- a/reproduction/Chinese_word_segmentation/models/cws_transformer.py +++ b/reproduction/Chinese_word_segmentation/models/cws_transformer.py @@ -10,8 +10,8 @@ from torch import nn import torch # from fastNLP.modules.encoder.transformer import TransformerEncoder from reproduction.Chinese_word_segmentation.models.transformer import TransformerEncoder -from fastNLP.modules.decoder.CRF import ConditionalRandomField,seq_len_to_byte_mask -from fastNLP.modules.decoder.CRF import allowed_transitions +from fastNLP.modules.decoder.crf import ConditionalRandomField,seq_len_to_byte_mask +from fastNLP.modules.decoder.crf import allowed_transitions class TransformerCWS(nn.Module): def __init__(self, vocab_num, embed_dim=100, bigram_vocab_num=None, bigram_embed_dim=100, num_bigram_per_char=None, diff --git a/reproduction/LSTM+self_attention_sentiment_analysis/main.py b/reproduction/LSTM+self_attention_sentiment_analysis/main.py index 4ca5388f..871dc476 100644 --- a/reproduction/LSTM+self_attention_sentiment_analysis/main.py +++ b/reproduction/LSTM+self_attention_sentiment_analysis/main.py @@ -7,7 +7,7 @@ from fastNLP.io.config_io import ConfigSection from fastNLP.io.dataset_loader import DummyClassificationReader as Dataset_loader from fastNLP.models.base_model import BaseModel from fastNLP.modules.aggregator.self_attention import SelfAttention -from fastNLP.modules.decoder.MLP import MLP +from fastNLP.modules.decoder.mlp import MLP from fastNLP.modules.encoder.embedding import Embedding as Embedding from fastNLP.modules.encoder.lstm import LSTM diff --git a/test/modules/decoder/test_CRF.py b/test/modules/decoder/test_CRF.py index 5fb49253..5dec7d47 100644 --- a/test/modules/decoder/test_CRF.py +++ b/test/modules/decoder/test_CRF.py @@ -5,7 +5,7 @@ import unittest class TestCRF(unittest.TestCase): def test_case1(self): # 检查allowed_transitions()能否正确使用 - from fastNLP.modules.decoder.CRF import allowed_transitions + from fastNLP.modules.decoder.crf import allowed_transitions id2label = {0: 'B', 1: 'I', 2:'O'} expected_res = {(0, 0), (0, 1), (0, 2), (0, 4), (1, 0), (1, 1), (1, 2), (1, 4), (2, 0), (2, 2), @@ -43,7 +43,7 @@ class TestCRF(unittest.TestCase): # 测试CRF能否避免解码出非法跃迁, 使用allennlp做了验证。 pass # import torch - # from fastNLP.modules.decoder.CRF import seq_len_to_byte_mask + # from fastNLP.modules.decoder.crf import seq_len_to_byte_mask # # labels = ['O'] # for label in ['X', 'Y']: @@ -63,7 +63,7 @@ class TestCRF(unittest.TestCase): # mask = seq_len_to_byte_mask(seq_lens) # allen_res = allen_CRF.viterbi_tags(logits, mask) # - # from fastNLP.modules.decoder.CRF import ConditionalRandomField, allowed_transitions + # from fastNLP.modules.decoder.crf import ConditionalRandomField, allowed_transitions # fast_CRF = ConditionalRandomField(num_tags=num_tags, allowed_transitions=allowed_transitions(id2label)) # fast_CRF.trans_m = trans_m # fast_res = fast_CRF.viterbi_decode(logits, mask, get_score=True, unpad=True) @@ -91,7 +91,7 @@ class TestCRF(unittest.TestCase): # mask = seq_len_to_byte_mask(seq_lens) # allen_res = allen_CRF.viterbi_tags(logits, mask) # - # from fastNLP.modules.decoder.CRF import ConditionalRandomField, allowed_transitions + # from fastNLP.modules.decoder.crf import ConditionalRandomField, allowed_transitions # fast_CRF = ConditionalRandomField(num_tags=num_tags, allowed_transitions=allowed_transitions(id2label, # encoding_type='BMES')) # fast_CRF.trans_m = trans_m @@ -104,7 +104,7 @@ class TestCRF(unittest.TestCase): def test_case3(self): # 测试crf的loss不会出现负数 import torch - from fastNLP.modules.decoder.CRF import ConditionalRandomField + from fastNLP.modules.decoder.crf import ConditionalRandomField from fastNLP.core.utils import seq_len_to_mask from torch import optim from torch import nn From 4ac4cda049a85f6cc73b854ac79f2bb549cfee97 Mon Sep 17 00:00:00 2001 From: yunfan Date: Fri, 17 May 2019 13:22:42 +0800 Subject: [PATCH 138/173] fix var runn --- fastNLP/modules/encoder/variational_rnn.py | 92 +++++++++++++--------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index 60cdf9c5..753741de 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -11,7 +11,8 @@ except ImportError: def flip(x, dims): indices = [slice(None)] * x.dim() for dim in dims: - indices[dim] = torch.arange(x.size(dim) - 1, -1, -1, dtype=torch.long, device=x.device) + indices[dim] = torch.arange( + x.size(dim) - 1, -1, -1, dtype=torch.long, device=x.device) return x[tuple(indices)] from ..utils import initial_parameter @@ -27,14 +28,14 @@ class VarRnnCellWrapper(nn.Module): """ Wrapper for normal RNN Cells, make it support variational dropout """ - + def __init__(self, cell, hidden_size, input_p, hidden_p): super(VarRnnCellWrapper, self).__init__() self.cell = cell self.hidden_size = hidden_size self.input_p = input_p self.hidden_p = hidden_p - + def forward(self, input_x, hidden, mask_x, mask_h, is_reversed=False): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] @@ -46,13 +47,13 @@ class VarRnnCellWrapper(nn.Module): hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ - + def get_hi(hi, h0, size): h0_size = size - hi.size(0) if h0_size > 0: return torch.cat([hi, h0[:h0_size]], dim=0) return hi[:size] - + is_lstm = isinstance(hidden, tuple) input, batch_sizes = input_x.data, input_x.batch_sizes output = [] @@ -63,7 +64,7 @@ class VarRnnCellWrapper(nn.Module): else: batch_iter = batch_sizes idx = 0 - + if is_lstm: hn = (hidden[0].clone(), hidden[1].clone()) else: @@ -79,7 +80,8 @@ class VarRnnCellWrapper(nn.Module): mask_hi = mask_h[:size] if is_lstm: hx, cx = hi - hi = (get_hi(hx, hidden[0], size) * mask_hi, get_hi(cx, hidden[1], size)) + hi = (get_hi(hx, hidden[0], size) * + mask_hi, get_hi(cx, hidden[1], size)) hi = cell(input_i, hi) hn[0][:size] = hi[0] hn[1][:size] = hi[1] @@ -89,7 +91,7 @@ class VarRnnCellWrapper(nn.Module): hi = cell(input_i, hi) hn[:size] = hi output.append(hi) - + if is_reversed: output = list(reversed(output)) output = torch.cat(output, dim=0) @@ -99,7 +101,7 @@ class VarRnnCellWrapper(nn.Module): class VarRNNBase(nn.Module): """ Variational Dropout RNN 实现. - + 论文参考: `A Theoretically Grounded Application of Dropout in Recurrent Neural Networks (Yarin Gal and Zoubin Ghahramani, 2016) https://arxiv.org/abs/1512.05287`. @@ -115,7 +117,7 @@ class VarRNNBase(nn.Module): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, mode, Cell, input_size, hidden_size, num_layers=1, bias=True, batch_first=False, input_dropout=0, hidden_dropout=0, bidirectional=False): @@ -135,18 +137,20 @@ class VarRNNBase(nn.Module): for direction in range(self.num_directions): input_size = self.input_size if layer == 0 else self.hidden_size * self.num_directions cell = Cell(input_size, self.hidden_size, bias) - self._all_cells.append(VarRnnCellWrapper(cell, self.hidden_size, input_dropout, hidden_dropout)) + self._all_cells.append(VarRnnCellWrapper( + cell, self.hidden_size, input_dropout, hidden_dropout)) initial_parameter(self) self.is_lstm = (self.mode == "LSTM") - + def _forward_one(self, n_layer, n_direction, input, hx, mask_x, mask_h): is_lstm = self.is_lstm idx = self.num_directions * n_layer + n_direction cell = self._all_cells[idx] hi = (hx[0][idx], hx[1][idx]) if is_lstm else hx[idx] - output_x, hidden_x = cell(input, hi, mask_x, mask_h, is_reversed=(n_direction == 1)) + output_x, hidden_x = cell( + input, hi, mask_x, mask_h, is_reversed=(n_direction == 1)) return output_x, hidden_x - + def forward(self, x, hx=None): """ @@ -160,31 +164,38 @@ class VarRNNBase(nn.Module): if not is_packed: seq_len = x.size(1) if self.batch_first else x.size(0) max_batch_size = x.size(0) if self.batch_first else x.size(1) - seq_lens = torch.LongTensor([seq_len for _ in range(max_batch_size)]) - input = pack_padded_sequence(input, seq_lens, batch_first=self.batch_first) + seq_lens = torch.LongTensor( + [seq_len for _ in range(max_batch_size)]) + x = pack_padded_sequence(x, seq_lens, batch_first=self.batch_first) else: - max_batch_size = int(input.batch_sizes[0]) - input, batch_sizes = input.data, input.batch_sizes - + max_batch_size = int(x.batch_sizes[0]) + x, batch_sizes = x.data, x.batch_sizes + if hx is None: hx = x.new_zeros(self.num_layers * self.num_directions, max_batch_size, self.hidden_size, requires_grad=True) if is_lstm: hx = (hx, hx.new_zeros(hx.size(), requires_grad=True)) - + mask_x = x.new_ones((max_batch_size, self.input_size)) - mask_out = x.new_ones((max_batch_size, self.hidden_size * self.num_directions)) + mask_out = x.new_ones( + (max_batch_size, self.hidden_size * self.num_directions)) mask_h_ones = x.new_ones((max_batch_size, self.hidden_size)) - nn.functional.dropout(mask_x, p=self.input_dropout, training=self.training, inplace=True) - nn.functional.dropout(mask_out, p=self.hidden_dropout, training=self.training, inplace=True) - - hidden = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) + nn.functional.dropout(mask_x, p=self.input_dropout, + training=self.training, inplace=True) + nn.functional.dropout(mask_out, p=self.hidden_dropout, + training=self.training, inplace=True) + + hidden = x.new_zeros( + (self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) if is_lstm: - cellstate = x.new_zeros((self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) + cellstate = x.new_zeros( + (self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) for layer in range(self.num_layers): output_list = [] input_seq = PackedSequence(x, batch_sizes) - mask_h = nn.functional.dropout(mask_h_ones, p=self.hidden_dropout, training=self.training, inplace=False) + mask_h = nn.functional.dropout( + mask_h_ones, p=self.hidden_dropout, training=self.training, inplace=False) for direction in range(self.num_directions): output_x, hidden_x = self._forward_one(layer, direction, input_seq, hx, mask_x if layer == 0 else mask_out, mask_h) @@ -196,16 +207,16 @@ class VarRNNBase(nn.Module): else: hidden[idx] = hidden_x x = torch.cat(output_list, dim=-1) - + if is_lstm: hidden = (hidden, cellstate) - + if is_packed: output = PackedSequence(x, batch_sizes) else: x = PackedSequence(x, batch_sizes) output, _ = pad_packed_sequence(x, batch_first=self.batch_first) - + return output, hidden @@ -225,10 +236,11 @@ class VarLSTM(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` """ - + def __init__(self, *args, **kwargs): - super(VarLSTM, self).__init__(mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) - + super(VarLSTM, self).__init__( + mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) + def forward(self, x, hx=None): return super(VarLSTM, self).forward(x, hx) @@ -249,10 +261,11 @@ class VarRNN(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, *args, **kwargs): - super(VarRNN, self).__init__(mode="RNN", Cell=nn.RNNCell, *args, **kwargs) - + super(VarRNN, self).__init__( + mode="RNN", Cell=nn.RNNCell, *args, **kwargs) + def forward(self, x, hx=None): return super(VarRNN, self).forward(x, hx) @@ -273,9 +286,10 @@ class VarGRU(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` """ - + def __init__(self, *args, **kwargs): - super(VarGRU, self).__init__(mode="GRU", Cell=nn.GRUCell, *args, **kwargs) - + super(VarGRU, self).__init__( + mode="GRU", Cell=nn.GRUCell, *args, **kwargs) + def forward(self, x, hx=None): return super(VarGRU, self).forward(x, hx) From aabbdb4df5464cde1cc37e98772260c8a25997cc Mon Sep 17 00:00:00 2001 From: yh Date: Fri, 17 May 2019 14:01:40 +0800 Subject: [PATCH 139/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0vocab=E7=9A=84clear?= =?UTF-8?q?=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/vocabulary.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 03759194..3d9598a3 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -322,7 +322,18 @@ class Vocabulary(object): :return str word: the word """ return self.idx2word[idx] - + + def clear(self): + """ + 删除Vocabulary中的词表数据。相当于重新初始化一下。 + + :return: + """ + self.word_count.clear() + self.word2idx = None + self.idx2word = None + self.rebuild = True + def __getstate__(self): """Use to prepare data for pickle. From bdec6187a2a1e2c3d0b7a5095a79f7d4bb9fbbdf Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sat, 18 May 2019 16:40:54 +0800 Subject: [PATCH 140/173] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E7=AC=A6=E5=90=88=20?= =?UTF-8?q?PEP8=20=E7=9A=84=E5=BE=AE=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/__init__.py | 4 +- fastNLP/core/batch.py | 8 +-- fastNLP/core/callback.py | 24 ++++----- fastNLP/core/dataset.py | 8 +-- fastNLP/core/field.py | 8 +-- fastNLP/core/losses.py | 24 ++++----- fastNLP/core/metrics.py | 14 +++--- fastNLP/core/optimizer.py | 4 +- fastNLP/core/sampler.py | 8 +-- fastNLP/core/trainer.py | 3 ++ fastNLP/core/utils.py | 9 ++-- fastNLP/core/vocabulary.py | 12 ++--- fastNLP/io/__init__.py | 10 ++-- fastNLP/io/base_loader.py | 6 +-- fastNLP/io/config_io.py | 12 ++--- fastNLP/io/dataset_loader.py | 12 ++--- fastNLP/io/embed_loader.py | 8 +-- fastNLP/io/model_io.py | 8 +-- fastNLP/models/__init__.py | 18 +++---- fastNLP/models/biaffine_parser.py | 10 ++-- fastNLP/models/cnn_text_classification.py | 8 +-- fastNLP/models/enas_utils.py | 1 - fastNLP/models/sequence_labeling.py | 10 ++-- fastNLP/models/snli.py | 8 +-- fastNLP/models/star_transformer.py | 14 +++--- fastNLP/modules/__init__.py | 18 +++---- fastNLP/modules/aggregator/__init__.py | 14 +++--- fastNLP/modules/aggregator/attention.py | 8 +-- fastNLP/modules/aggregator/pooling.py | 9 +++- fastNLP/modules/decoder/__init__.py | 10 ++-- fastNLP/modules/decoder/crf.py | 10 ++-- fastNLP/modules/decoder/mlp.py | 8 +-- fastNLP/modules/decoder/utils.py | 3 +- fastNLP/modules/dropout.py | 6 ++- fastNLP/modules/encoder/__init__.py | 17 +++---- fastNLP/modules/encoder/char_encoder.py | 9 ++-- fastNLP/modules/encoder/conv_maxpool.py | 7 ++- fastNLP/modules/encoder/embedding.py | 5 +- fastNLP/modules/encoder/lstm.py | 8 +-- fastNLP/modules/encoder/star_transformer.py | 8 +-- fastNLP/modules/encoder/transformer.py | 7 ++- fastNLP/modules/encoder/variational_rnn.py | 54 ++++++++++----------- fastNLP/modules/utils.py | 2 +- 43 files changed, 229 insertions(+), 225 deletions(-) diff --git a/fastNLP/__init__.py b/fastNLP/__init__.py index 5dd5fd54..c67e5919 100644 --- a/fastNLP/__init__.py +++ b/fastNLP/__init__.py @@ -52,8 +52,8 @@ __all__ = [ "cache_results" ] +__version__ = '0.4.0' + from .core import * from . import models from . import modules - -__version__ = '0.4.0' diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index b031d051..c1289adf 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -2,6 +2,10 @@ batch 模块实现了 fastNLP 所需的 Batch 类。 """ +__all__ = [ + "Batch" +] + import atexit from queue import Empty, Full @@ -11,10 +15,6 @@ import torch.multiprocessing as mp from .sampler import RandomSampler -__all__ = [ - "Batch" -] - _python_is_exit = False diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 51495f23..6825ea6e 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -49,6 +49,18 @@ callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class: trainer.train() """ +__all__ = [ + "Callback", + "GradientClipCallback", + "EarlyStopCallback", + "TensorboardCallback", + "LRScheduler", + "ControlC", + + "CallbackException", + "EarlyStopError" +] + import os import torch @@ -62,18 +74,6 @@ except: from ..io.model_io import ModelSaver, ModelLoader -__all__ = [ - "Callback", - "GradientClipCallback", - "EarlyStopCallback", - "TensorboardCallback", - "LRScheduler", - "ControlC", - - "CallbackException", - "EarlyStopError" -] - class Callback(object): """ diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index f20dd1f8..2da9f6d9 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -272,6 +272,10 @@ """ +__all__ = [ + "DataSet" +] + import _pickle as pickle import warnings @@ -282,10 +286,6 @@ from .field import FieldArray from .instance import Instance from .utils import _get_func_signature -__all__ = [ - "DataSet" -] - class DataSet(object): """ diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index 14e2538d..21ead327 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -3,10 +3,6 @@ field模块实现了 FieldArray 和若干 Padder。 FieldArray 是 :class:`~fas 原理部分请参考 :doc:`fastNLP.core.dataset` """ -from copy import deepcopy - -import numpy as np - __all__ = [ "FieldArray", "Padder", @@ -14,6 +10,10 @@ __all__ = [ "EngChar2DPadder" ] +from copy import deepcopy + +import numpy as np + class FieldArray(object): """ diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 797b557d..ddc2c49f 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -2,6 +2,18 @@ losses 模块定义了 fastNLP 中所需的各种损失函数,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ +__all__ = [ + "LossBase", + + "LossFunc", + "LossInForward", + + "CrossEntropyLoss", + "BCELoss", + "L1Loss", + "NLLLoss" +] + import inspect from collections import defaultdict @@ -15,18 +27,6 @@ from .utils import _check_arg_dict_list from .utils import _check_function_or_method from .utils import _get_func_signature -__all__ = [ - "LossBase", - - "LossFunc", - "LossInForward", - - "CrossEntropyLoss", - "BCELoss", - "L1Loss", - "NLLLoss" -] - class LossBase(object): """ diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index 5ea2a5f1..f633a80f 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -2,6 +2,13 @@ metrics 模块实现了 fastNLP 所需的各种常用衡量指标,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ +__all__ = [ + "MetricBase", + "AccuracyMetric", + "SpanFPreRecMetric", + "SQuADMetric" +] + import inspect from collections import defaultdict @@ -16,13 +23,6 @@ from .utils import _get_func_signature from .utils import seq_len_to_mask from .vocabulary import Vocabulary -__all__ = [ - "MetricBase", - "AccuracyMetric", - "SpanFPreRecMetric", - "SQuADMetric" -] - class MetricBase(object): """ diff --git a/fastNLP/core/optimizer.py b/fastNLP/core/optimizer.py index 28f618f9..ef619042 100644 --- a/fastNLP/core/optimizer.py +++ b/fastNLP/core/optimizer.py @@ -2,14 +2,14 @@ optimizer 模块定义了 fastNLP 中所需的各种优化器,一般做为 :class:`~fastNLP.Trainer` 的参数使用。 """ -import torch - __all__ = [ "Optimizer", "SGD", "Adam" ] +import torch + class Optimizer(object): """ diff --git a/fastNLP/core/sampler.py b/fastNLP/core/sampler.py index c8577722..c5784f59 100644 --- a/fastNLP/core/sampler.py +++ b/fastNLP/core/sampler.py @@ -1,10 +1,6 @@ """ sampler 子类实现了 fastNLP 所需的各种采样器。 """ -from itertools import chain - -import numpy as np - __all__ = [ "Sampler", "BucketSampler", @@ -12,6 +8,10 @@ __all__ = [ "RandomSampler" ] +from itertools import chain + +import numpy as np + class Sampler(object): """ diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 7efa5d28..702cb6e7 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -295,6 +295,9 @@ Example2.3 fastNLP已经自带了很多callback函数供使用,可以参考 :doc:`fastNLP.core.callback` 。 """ +__all__ = [ + "Trainer" +] import os import time diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 6e2f99ff..14ac409f 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -1,6 +1,11 @@ """ utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户可以使用的是 :func:`cache_results` 修饰器。 """ +__all__ = [ + "cache_results", + "seq_len_to_mask" +] + import _pickle import inspect import os @@ -11,10 +16,6 @@ import numpy as np import torch import torch.nn as nn -__all__ = [ - "cache_results", - "seq_len_to_mask" -] _CheckRes = namedtuple('_CheckRes', ['missing', 'unused', 'duplicated', 'required', 'all_needed', 'varargs']) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 3d9598a3..43f590fd 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -1,12 +1,12 @@ +__all__ = [ + "Vocabulary" +] + from functools import wraps from collections import Counter from .dataset import DataSet -__all__ = [ - "Vocabulary" -] - def _check_build_vocab(func): """A decorator to make sure the indexing is built before used. @@ -322,7 +322,7 @@ class Vocabulary(object): :return str word: the word """ return self.idx2word[idx] - + def clear(self): """ 删除Vocabulary中的词表数据。相当于重新初始化一下。 @@ -333,7 +333,7 @@ class Vocabulary(object): self.word2idx = None self.idx2word = None self.rebuild = True - + def __getstate__(self): """Use to prepare data for pickle. diff --git a/fastNLP/io/__init__.py b/fastNLP/io/__init__.py index 6ce7ebc3..c8d6a441 100644 --- a/fastNLP/io/__init__.py +++ b/fastNLP/io/__init__.py @@ -9,11 +9,6 @@ 这些类的使用方法如下: """ -from .embed_loader import EmbedLoader -from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ - PeopleDailyCorpusLoader, Conll2003Loader -from .model_io import ModelLoader, ModelSaver - __all__ = [ 'EmbedLoader', @@ -29,3 +24,8 @@ __all__ = [ 'ModelLoader', 'ModelSaver', ] + +from .embed_loader import EmbedLoader +from .dataset_loader import DataSetLoader, CSVLoader, JsonLoader, ConllLoader, SNLILoader, SSTLoader, \ + PeopleDailyCorpusLoader, Conll2003Loader +from .model_io import ModelLoader, ModelSaver diff --git a/fastNLP/io/base_loader.py b/fastNLP/io/base_loader.py index 33f59fe5..4ab1e2d0 100644 --- a/fastNLP/io/base_loader.py +++ b/fastNLP/io/base_loader.py @@ -1,10 +1,10 @@ -import _pickle as pickle -import os - __all__ = [ "BaseLoader" ] +import _pickle as pickle +import os + class BaseLoader(object): """ diff --git a/fastNLP/io/config_io.py b/fastNLP/io/config_io.py index e67511ee..4acdbb96 100644 --- a/fastNLP/io/config_io.py +++ b/fastNLP/io/config_io.py @@ -3,18 +3,18 @@ .. todo:: 这个模块中的类可能被抛弃? """ -import configparser -import json -import os - -from .base_loader import BaseLoader - __all__ = [ "ConfigLoader", "ConfigSection", "ConfigSaver" ] +import configparser +import json +import os + +from .base_loader import BaseLoader + class ConfigLoader(BaseLoader): """ diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index a4b233ad..b820af44 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -10,12 +10,6 @@ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的 # ... do stuff """ -from nltk.tree import Tree - -from ..core.dataset import DataSet -from ..core.instance import Instance -from .file_reader import _read_csv, _read_json, _read_conll - __all__ = [ 'DataSetLoader', 'CSVLoader', @@ -27,6 +21,12 @@ __all__ = [ 'Conll2003Loader', ] +from nltk.tree import Tree + +from ..core.dataset import DataSet +from ..core.instance import Instance +from .file_reader import _read_csv, _read_json, _read_conll + def _download_from_url(url, path): try: diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index 7a845366..fb024e73 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,3 +1,7 @@ +__all__ = [ + "EmbedLoader" +] + import os import warnings @@ -6,10 +10,6 @@ import numpy as np from ..core.vocabulary import Vocabulary from .base_loader import BaseLoader -__all__ = [ - "EmbedLoader" -] - class EmbedLoader(BaseLoader): """ diff --git a/fastNLP/io/model_io.py b/fastNLP/io/model_io.py index 36393cd4..ffaa4ef5 100644 --- a/fastNLP/io/model_io.py +++ b/fastNLP/io/model_io.py @@ -1,15 +1,15 @@ """ 用于载入和保存模型 """ -import torch - -from .base_loader import BaseLoader - __all__ = [ "ModelLoader", "ModelSaver" ] +import torch + +from .base_loader import BaseLoader + class ModelLoader(BaseLoader): """ diff --git a/fastNLP/models/__init__.py b/fastNLP/models/__init__.py index f9ade153..14314049 100644 --- a/fastNLP/models/__init__.py +++ b/fastNLP/models/__init__.py @@ -7,15 +7,6 @@ fastNLP 在 :mod:`~fastNLP.models` 模块中内置了如 :class:`~fastNLP.models """ -from .base_model import BaseModel -from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequenceClassification, \ - BertForTokenClassification -from .biaffine_parser import BiaffineParser, GraphParser -from .cnn_text_classification import CNNText -from .sequence_labeling import SeqLabeling, AdvSeqLabel -from .snli import ESIM -from .star_transformer import StarTransEnc, STSeqCls, STNLICls, STSeqLabel - __all__ = [ "CNNText", @@ -32,3 +23,12 @@ __all__ = [ "BiaffineParser", "GraphParser" ] + +from .base_model import BaseModel +from .bert import BertForMultipleChoice, BertForQuestionAnswering, BertForSequenceClassification, \ + BertForTokenClassification +from .biaffine_parser import BiaffineParser, GraphParser +from .cnn_text_classification import CNNText +from .sequence_labeling import SeqLabeling, AdvSeqLabel +from .snli import ESIM +from .star_transformer import StarTransEnc, STSeqCls, STNLICls, STSeqLabel diff --git a/fastNLP/models/biaffine_parser.py b/fastNLP/models/biaffine_parser.py index 7f16202d..8533e7af 100644 --- a/fastNLP/models/biaffine_parser.py +++ b/fastNLP/models/biaffine_parser.py @@ -1,6 +1,11 @@ """ Biaffine Dependency Parser 的 Pytorch 实现. """ +__all__ = [ + "BiaffineParser", + "GraphParser" +] + import numpy as np import torch import torch.nn as nn @@ -19,11 +24,6 @@ from ..modules.utils import get_embeddings from .base_model import BaseModel from ..core.utils import seq_len_to_mask -__all__ = [ - "BiaffineParser", - "GraphParser" -] - def _mst(scores): """ diff --git a/fastNLP/models/cnn_text_classification.py b/fastNLP/models/cnn_text_classification.py index a9ccc568..3a71a80a 100644 --- a/fastNLP/models/cnn_text_classification.py +++ b/fastNLP/models/cnn_text_classification.py @@ -1,13 +1,13 @@ +__all__ = [ + "CNNText" +] + import torch import torch.nn as nn from ..core.const import Const as C from ..modules import encoder -__all__ = [ - "CNNText" -] - class CNNText(torch.nn.Module): """ diff --git a/fastNLP/models/enas_utils.py b/fastNLP/models/enas_utils.py index 68c170ed..4e402a9a 100644 --- a/fastNLP/models/enas_utils.py +++ b/fastNLP/models/enas_utils.py @@ -1,6 +1,5 @@ # Code Modified from https://github.com/carpedm20/ENAS-pytorch -from __future__ import print_function from collections import defaultdict import collections diff --git a/fastNLP/models/sequence_labeling.py b/fastNLP/models/sequence_labeling.py index 503c79ba..8e6a5db1 100644 --- a/fastNLP/models/sequence_labeling.py +++ b/fastNLP/models/sequence_labeling.py @@ -1,6 +1,11 @@ """ 本模块实现了两种序列标注模型 """ +__all__ = [ + "SeqLabeling", + "AdvSeqLabel" +] + import torch import torch.nn as nn @@ -10,11 +15,6 @@ from ..modules.decoder.crf import allowed_transitions from ..core.utils import seq_len_to_mask from ..core.const import Const as C -__all__ = [ - "SeqLabeling", - "AdvSeqLabel" -] - class SeqLabeling(BaseModel): """ diff --git a/fastNLP/models/snli.py b/fastNLP/models/snli.py index 606bcc42..395a9bbf 100644 --- a/fastNLP/models/snli.py +++ b/fastNLP/models/snli.py @@ -1,3 +1,7 @@ +__all__ = [ + "ESIM" +] + import torch import torch.nn as nn @@ -8,10 +12,6 @@ from ..modules import encoder as Encoder from ..modules import aggregator as Aggregator from ..core.utils import seq_len_to_mask -__all__ = [ - "ESIM" -] - my_inf = 10e12 diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index 2e55f7e4..c67e5938 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -1,6 +1,13 @@ """ Star-Transformer 的 Pytorch 实现。 """ +__all__ = [ + "StarTransEnc", + "STNLICls", + "STSeqCls", + "STSeqLabel", +] + import torch from torch import nn @@ -9,13 +16,6 @@ from ..core.utils import seq_len_to_mask from ..modules.utils import get_embeddings from ..core.const import Const -__all__ = [ - "StarTransEnc", - "STNLICls", - "STSeqCls", - "STSeqLabel", -] - class StarTransEnc(nn.Module): """ diff --git a/fastNLP/modules/__init__.py b/fastNLP/modules/__init__.py index cd54c8db..194fda4e 100644 --- a/fastNLP/modules/__init__.py +++ b/fastNLP/modules/__init__.py @@ -22,15 +22,6 @@ +-----------------------+-----------------------+-----------------------+ """ -from . import aggregator -from . import decoder -from . import encoder -from .aggregator import * -from .decoder import * -from .dropout import TimestepDropout -from .encoder import * -from .utils import get_embeddings - __all__ = [ # "BertModel", "ConvolutionCharEncoder", @@ -54,3 +45,12 @@ __all__ = [ "viterbi_decode", "allowed_transitions", ] + +from . import aggregator +from . import decoder +from . import encoder +from .aggregator import * +from .decoder import * +from .dropout import TimestepDropout +from .encoder import * +from .utils import get_embeddings diff --git a/fastNLP/modules/aggregator/__init__.py b/fastNLP/modules/aggregator/__init__.py index 117dad83..a82138e7 100644 --- a/fastNLP/modules/aggregator/__init__.py +++ b/fastNLP/modules/aggregator/__init__.py @@ -1,10 +1,3 @@ -from .pooling import MaxPool -from .pooling import MaxPoolWithMask -from .pooling import AvgPool -from .pooling import AvgPoolWithMask - -from .attention import MultiHeadAttention - __all__ = [ "MaxPool", "MaxPoolWithMask", @@ -12,3 +5,10 @@ __all__ = [ "MultiHeadAttention", ] + +from .pooling import MaxPool +from .pooling import MaxPoolWithMask +from .pooling import AvgPool +from .pooling import AvgPoolWithMask + +from .attention import MultiHeadAttention diff --git a/fastNLP/modules/aggregator/attention.py b/fastNLP/modules/aggregator/attention.py index a1a7fda8..4101b033 100644 --- a/fastNLP/modules/aggregator/attention.py +++ b/fastNLP/modules/aggregator/attention.py @@ -1,3 +1,7 @@ +__all__ = [ + "MultiHeadAttention" +] + import math import torch @@ -8,10 +12,6 @@ from ..dropout import TimestepDropout from ..utils import initial_parameter -__all__ = [ - "MultiHeadAttention" -] - class DotAttention(nn.Module): """ diff --git a/fastNLP/modules/aggregator/pooling.py b/fastNLP/modules/aggregator/pooling.py index be454d7b..51438aae 100644 --- a/fastNLP/modules/aggregator/pooling.py +++ b/fastNLP/modules/aggregator/pooling.py @@ -1,4 +1,8 @@ -__all__ = ["MaxPool", "MaxPoolWithMask", "AvgPool"] +__all__ = [ + "MaxPool", + "MaxPoolWithMask", + "AvgPool" +] import torch import torch.nn as nn @@ -16,6 +20,7 @@ class MaxPool(nn.Module): :param kernel_size: max pooling的窗口大小,默认为tensor最后k维,其中k为dimension :param ceil_mode: """ + def __init__(self, stride=None, padding=0, dilation=1, dimension=1, kernel_size=None, ceil_mode=False): super(MaxPool, self).__init__() @@ -125,7 +130,7 @@ class AvgPoolWithMask(nn.Module): 给定形如[batch_size, max_len, hidden_size]的输入,在最后一维进行avg pooling. 输出为[batch_size, hidden_size], pooling 的时候只会考虑mask为1的位置 """ - + def __init__(self): super(AvgPoolWithMask, self).__init__() self.inf = 10e12 diff --git a/fastNLP/modules/decoder/__init__.py b/fastNLP/modules/decoder/__init__.py index 5df48c43..664618b2 100644 --- a/fastNLP/modules/decoder/__init__.py +++ b/fastNLP/modules/decoder/__init__.py @@ -1,11 +1,11 @@ -from .crf import ConditionalRandomField -from .mlp import MLP -from .utils import viterbi_decode -from .crf import allowed_transitions - __all__ = [ "MLP", "ConditionalRandomField", "viterbi_decode", "allowed_transitions" ] + +from .crf import ConditionalRandomField +from .mlp import MLP +from .utils import viterbi_decode +from .crf import allowed_transitions diff --git a/fastNLP/modules/decoder/crf.py b/fastNLP/modules/decoder/crf.py index 130ed40e..beb2b9be 100644 --- a/fastNLP/modules/decoder/crf.py +++ b/fastNLP/modules/decoder/crf.py @@ -1,13 +1,13 @@ -import torch -from torch import nn - -from ..utils import initial_parameter - __all__ = [ "ConditionalRandomField", "allowed_transitions" ] +import torch +from torch import nn + +from ..utils import initial_parameter + def allowed_transitions(id2target, encoding_type='bio', include_start_end=True): """ diff --git a/fastNLP/modules/decoder/mlp.py b/fastNLP/modules/decoder/mlp.py index 27019432..c1579224 100644 --- a/fastNLP/modules/decoder/mlp.py +++ b/fastNLP/modules/decoder/mlp.py @@ -1,12 +1,12 @@ +__all__ = [ + "MLP" +] + import torch import torch.nn as nn from ..utils import initial_parameter -__all__ = [ - "MLP" -] - class MLP(nn.Module): """ diff --git a/fastNLP/modules/decoder/utils.py b/fastNLP/modules/decoder/utils.py index 434873c7..249f3ff6 100644 --- a/fastNLP/modules/decoder/utils.py +++ b/fastNLP/modules/decoder/utils.py @@ -1,8 +1,7 @@ -import torch - __all__ = [ "viterbi_decode" ] +import torch def viterbi_decode(logits, transitions, mask=None, unpad=False): diff --git a/fastNLP/modules/dropout.py b/fastNLP/modules/dropout.py index 34b426fd..1363165c 100644 --- a/fastNLP/modules/dropout.py +++ b/fastNLP/modules/dropout.py @@ -1,6 +1,8 @@ -import torch __all__ = [] +import torch + + class TimestepDropout(torch.nn.Dropout): """ 别名::class:`fastNLP.modules.TimestepDropout` @@ -8,7 +10,7 @@ class TimestepDropout(torch.nn.Dropout): 接受的参数shape为``[batch_size, num_timesteps, embedding_dim)]`` 使用同一个mask(shape为``(batch_size, embedding_dim)``) 在每个timestamp上做dropout。 """ - + def forward(self, x): dropout_mask = x.new_ones(x.shape[0], x.shape[-1]) torch.nn.functional.dropout(dropout_mask, self.p, self.training, inplace=True) diff --git a/fastNLP/modules/encoder/__init__.py b/fastNLP/modules/encoder/__init__.py index 3d65867a..bdc4cbf3 100644 --- a/fastNLP/modules/encoder/__init__.py +++ b/fastNLP/modules/encoder/__init__.py @@ -1,12 +1,3 @@ -from .bert import BertModel -from .char_encoder import ConvolutionCharEncoder, LSTMCharEncoder -from .conv_maxpool import ConvMaxpool -from .embedding import Embedding -from .lstm import LSTM -from .star_transformer import StarTransformer -from .transformer import TransformerEncoder -from .variational_rnn import VarRNN, VarLSTM, VarGRU - __all__ = [ # "BertModel", @@ -27,3 +18,11 @@ __all__ = [ "VarLSTM", "VarGRU" ] +from .bert import BertModel +from .char_encoder import ConvolutionCharEncoder, LSTMCharEncoder +from .conv_maxpool import ConvMaxpool +from .embedding import Embedding +from .lstm import LSTM +from .star_transformer import StarTransformer +from .transformer import TransformerEncoder +from .variational_rnn import VarRNN, VarLSTM, VarGRU diff --git a/fastNLP/modules/encoder/char_encoder.py b/fastNLP/modules/encoder/char_encoder.py index 8aefd284..481ad7ad 100644 --- a/fastNLP/modules/encoder/char_encoder.py +++ b/fastNLP/modules/encoder/char_encoder.py @@ -1,12 +1,11 @@ -import torch -import torch.nn as nn - -from ..utils import initial_parameter - __all__ = [ "ConvolutionCharEncoder", "LSTMCharEncoder" ] +import torch +import torch.nn as nn + +from ..utils import initial_parameter # from torch.nn.init import xavier_uniform diff --git a/fastNLP/modules/encoder/conv_maxpool.py b/fastNLP/modules/encoder/conv_maxpool.py index 5e714e88..ae6bea04 100644 --- a/fastNLP/modules/encoder/conv_maxpool.py +++ b/fastNLP/modules/encoder/conv_maxpool.py @@ -1,13 +1,12 @@ +__all__ = [ + "ConvMaxpool" +] import torch import torch.nn as nn import torch.nn.functional as F from ..utils import initial_parameter -__all__ = [ - "ConvMaxpool" -] - class ConvMaxpool(nn.Module): """ diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index 9fa89e7f..f3c1f475 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -1,9 +1,8 @@ -import torch.nn as nn -from ..utils import get_embeddings - __all__ = [ "Embedding" ] +import torch.nn as nn +from ..utils import get_embeddings class Embedding(nn.Embedding): diff --git a/fastNLP/modules/encoder/lstm.py b/fastNLP/modules/encoder/lstm.py index bc9cb155..b4f960e7 100644 --- a/fastNLP/modules/encoder/lstm.py +++ b/fastNLP/modules/encoder/lstm.py @@ -2,16 +2,16 @@ 轻量封装的 Pytorch LSTM 模块. 可在 forward 时传入序列的长度, 自动对padding做合适的处理. """ +__all__ = [ + "LSTM" +] + import torch import torch.nn as nn import torch.nn.utils.rnn as rnn from ..utils import initial_parameter -__all__ = [ - "LSTM" -] - class LSTM(nn.Module): """ diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 677af48a..5a7f3d67 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -1,15 +1,15 @@ """ Star-Transformer 的encoder部分的 Pytorch 实现 """ +__all__ = [ + "StarTransformer" +] + import numpy as NP import torch from torch import nn from torch.nn import functional as F -__all__ = [ - "StarTransformer" -] - class StarTransformer(nn.Module): """ diff --git a/fastNLP/modules/encoder/transformer.py b/fastNLP/modules/encoder/transformer.py index 2532d90a..698ff95c 100644 --- a/fastNLP/modules/encoder/transformer.py +++ b/fastNLP/modules/encoder/transformer.py @@ -1,12 +1,11 @@ +__all__ = [ + "TransformerEncoder" +] from torch import nn from ..aggregator.attention import MultiHeadAttention from ..dropout import TimestepDropout -__all__ = [ - "TransformerEncoder" -] - class TransformerEncoder(nn.Module): """ diff --git a/fastNLP/modules/encoder/variational_rnn.py b/fastNLP/modules/encoder/variational_rnn.py index 753741de..29b728e5 100644 --- a/fastNLP/modules/encoder/variational_rnn.py +++ b/fastNLP/modules/encoder/variational_rnn.py @@ -1,6 +1,12 @@ """ Variational RNN 的 Pytorch 实现 """ +__all__ = [ + "VarRNN", + "VarLSTM", + "VarGRU" +] + import torch import torch.nn as nn from torch.nn.utils.rnn import PackedSequence, pack_padded_sequence, pad_packed_sequence @@ -17,25 +23,19 @@ except ImportError: from ..utils import initial_parameter -__all__ = [ - "VarRNN", - "VarLSTM", - "VarGRU" -] - class VarRnnCellWrapper(nn.Module): """ Wrapper for normal RNN Cells, make it support variational dropout """ - + def __init__(self, cell, hidden_size, input_p, hidden_p): super(VarRnnCellWrapper, self).__init__() self.cell = cell self.hidden_size = hidden_size self.input_p = input_p self.hidden_p = hidden_p - + def forward(self, input_x, hidden, mask_x, mask_h, is_reversed=False): """ :param PackedSequence input_x: [seq_len, batch_size, input_size] @@ -47,13 +47,13 @@ class VarRnnCellWrapper(nn.Module): hidden: for LSTM, tuple of (h_n, c_n), [batch_size, hidden_size] for other RNN, h_n, [batch_size, hidden_size] """ - + def get_hi(hi, h0, size): h0_size = size - hi.size(0) if h0_size > 0: return torch.cat([hi, h0[:h0_size]], dim=0) return hi[:size] - + is_lstm = isinstance(hidden, tuple) input, batch_sizes = input_x.data, input_x.batch_sizes output = [] @@ -64,7 +64,7 @@ class VarRnnCellWrapper(nn.Module): else: batch_iter = batch_sizes idx = 0 - + if is_lstm: hn = (hidden[0].clone(), hidden[1].clone()) else: @@ -91,7 +91,7 @@ class VarRnnCellWrapper(nn.Module): hi = cell(input_i, hi) hn[:size] = hi output.append(hi) - + if is_reversed: output = list(reversed(output)) output = torch.cat(output, dim=0) @@ -117,7 +117,7 @@ class VarRNNBase(nn.Module): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, mode, Cell, input_size, hidden_size, num_layers=1, bias=True, batch_first=False, input_dropout=0, hidden_dropout=0, bidirectional=False): @@ -141,7 +141,7 @@ class VarRNNBase(nn.Module): cell, self.hidden_size, input_dropout, hidden_dropout)) initial_parameter(self) self.is_lstm = (self.mode == "LSTM") - + def _forward_one(self, n_layer, n_direction, input, hx, mask_x, mask_h): is_lstm = self.is_lstm idx = self.num_directions * n_layer + n_direction @@ -150,7 +150,7 @@ class VarRNNBase(nn.Module): output_x, hidden_x = cell( input, hi, mask_x, mask_h, is_reversed=(n_direction == 1)) return output_x, hidden_x - + def forward(self, x, hx=None): """ @@ -170,13 +170,13 @@ class VarRNNBase(nn.Module): else: max_batch_size = int(x.batch_sizes[0]) x, batch_sizes = x.data, x.batch_sizes - + if hx is None: hx = x.new_zeros(self.num_layers * self.num_directions, max_batch_size, self.hidden_size, requires_grad=True) if is_lstm: hx = (hx, hx.new_zeros(hx.size(), requires_grad=True)) - + mask_x = x.new_ones((max_batch_size, self.input_size)) mask_out = x.new_ones( (max_batch_size, self.hidden_size * self.num_directions)) @@ -185,7 +185,7 @@ class VarRNNBase(nn.Module): training=self.training, inplace=True) nn.functional.dropout(mask_out, p=self.hidden_dropout, training=self.training, inplace=True) - + hidden = x.new_zeros( (self.num_layers * self.num_directions, max_batch_size, self.hidden_size)) if is_lstm: @@ -207,16 +207,16 @@ class VarRNNBase(nn.Module): else: hidden[idx] = hidden_x x = torch.cat(output_list, dim=-1) - + if is_lstm: hidden = (hidden, cellstate) - + if is_packed: output = PackedSequence(x, batch_sizes) else: x = PackedSequence(x, batch_sizes) output, _ = pad_packed_sequence(x, batch_first=self.batch_first) - + return output, hidden @@ -236,11 +236,11 @@ class VarLSTM(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的LSTM. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarLSTM, self).__init__( mode="LSTM", Cell=nn.LSTMCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarLSTM, self).forward(x, hx) @@ -261,11 +261,11 @@ class VarRNN(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的RNN. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarRNN, self).__init__( mode="RNN", Cell=nn.RNNCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarRNN, self).forward(x, hx) @@ -286,10 +286,10 @@ class VarGRU(VarRNNBase): :param hidden_dropout: 对每个隐状态的dropout概率. Default: 0 :param bidirectional: 若为 ``True``, 使用双向的GRU. Default: ``False`` """ - + def __init__(self, *args, **kwargs): super(VarGRU, self).__init__( mode="GRU", Cell=nn.GRUCell, *args, **kwargs) - + def forward(self, x, hx=None): return super(VarGRU, self).forward(x, hx) diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index 0aba7e62..c9a1f682 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -1,5 +1,5 @@ from functools import reduce -from collections import OrderedDict + import numpy as np import torch import torch.nn as nn From a6f60d8fead5b8d76721287f9dd53bed0e1dc3e3 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Sun, 19 May 2019 19:03:43 +0800 Subject: [PATCH 141/173] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86cache=5Fresu?= =?UTF-8?q?lts=E7=9A=84=E4=BE=8B=E5=AD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/utils.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 14ac409f..518c8213 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -45,26 +45,28 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): import time import numpy as np from fastNLP import cache_results - + @cache_results('cache.pkl') def process_data(): # 一些比较耗时的工作,比如读取数据,预处理数据等,这里用time.sleep()代替耗时 time.sleep(1) - return np.random.randint(5, size=(10, 20)) - + return np.random.randint(10, size=(5,)) + start_time = time.time() - process_data() + print("res =",process_data()) print(time.time() - start_time) - + start_time = time.time() - process_data() + print("res =",process_data()) print(time.time() - start_time) - - # 输出内容如下 - # Save cache to cache.pkl. - # 1.0015439987182617 - # Read cache from cache.pkl. - # 0.00013065338134765625 + + # 输出内容如下,可以看到两次结果相同,且第二次几乎没有花费时间 + # Save cache to cache.pkl. + # res = [5 4 9 1 8] + # 1.0042750835418701 + # Read cache from cache.pkl. + # res = [5 4 9 1 8] + # 0.0040721893310546875 可以看到第二次运行的时候,只用了0.0001s左右,是由于第二次运行将直接从cache.pkl这个文件读取数据,而不会经过再次预处理 From 6d0b1ea716ec70b0f17096ac77ed23a87a64cfdf Mon Sep 17 00:00:00 2001 From: yh_cc Date: Mon, 20 May 2019 23:10:52 +0800 Subject: [PATCH 142/173] =?UTF-8?q?1.=20=E5=AF=B9callback=E4=B8=ADindices?= =?UTF-8?q?=E6=BD=9C=E5=9C=A8None=E4=BD=9C=E5=87=BA=E6=8F=90=E7=A4=BA;2.Da?= =?UTF-8?q?taSet=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87List=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 2 +- fastNLP/core/dataset.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 9dce426b..7f5624d8 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -168,7 +168,7 @@ class Callback(object): :param dict batch_x: DataSet中被设置为input的field的batch。 :param dict batch_y: DataSet中被设置为target的field的batch。 :param list(int) indices: 这次采样使用到的indices,可以通过DataSet[indices]获取出这个batch采出的Instance,在一些 - 情况下可以帮助定位是哪个Sample导致了错误。 + 情况下可以帮助定位是哪个Sample导致了错误。仅在Trainer的prefetch为False时可用。 :return: """ pass diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index b506dfae..38623caa 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -365,6 +365,15 @@ class DataSet(object): if idx not in self: raise KeyError("No such field called {} in DataSet.".format(idx)) return self.field_arrays[idx] + elif isinstance(idx, list): + dataset = DataSet() + for i in idx: + assert isinstance(i, int), "Only int index allowed." + instance = self[i] + dataset.append(instance) + for field_name, field in self.field_arrays.items(): + dataset.field_arrays[field_name].to(field) + return dataset else: raise KeyError("Unrecognized type {} for idx in __getitem__ method".format(type(idx))) From 117f556a10c81c7a5b16e4826976145d61d700e3 Mon Sep 17 00:00:00 2001 From: yh Date: Tue, 21 May 2019 18:03:50 +0800 Subject: [PATCH 143/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=AAtypo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/dataset_loader.py | 16 ++++++++-------- legacy/api/api.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index b820af44..8273d2f8 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -214,27 +214,27 @@ class ConllLoader(DataSetLoader): 11:N Predicate Arguments N Coreference - :param headers: 每一列数据的名称,需为List or Tuple of str。``header`` 与 ``indexs`` 一一对应 - :param indexs: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` + :param headers: 每一列数据的名称,需为List or Tuple of str。``header`` 与 ``indexes`` 一一对应 + :param indexes: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ - def __init__(self, headers, indexs=None, dropna=False): + def __init__(self, headers, indexes=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): raise TypeError('invalid headers: {}, should be list of strings'.format(headers)) self.headers = headers self.dropna = dropna - if indexs is None: - self.indexs = list(range(len(self.headers))) + if indexes is None: + self.indexes = list(range(len(self.headers))) else: - if len(indexs) != len(headers): + if len(indexes) != len(headers): raise ValueError - self.indexs = indexs + self.indexes = indexes def load(self, path): ds = DataSet() - for idx, data in _read_conll(path, indexes=self.indexs, dropna=self.dropna): + for idx, data in _read_conll(path, indexes=self.indexes, dropna=self.dropna): ins = {h: data[i] for i, h in enumerate(self.headers)} ds.append(Instance(**ins)) return ds diff --git a/legacy/api/api.py b/legacy/api/api.py index 0b5d3cd3..d5d1df6b 100644 --- a/legacy/api/api.py +++ b/legacy/api/api.py @@ -101,7 +101,7 @@ class ConllxDataLoader(ConllLoader): indexs = [ 1, 3, 6, 7, ] - super(ConllxDataLoader, self).__init__(headers=headers, indexs=indexs) + super(ConllxDataLoader, self).__init__(headers=headers, indexes=indexs) class API: From 001c835e0ba4080d03de51f692db9ced41ce8e08 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Tue, 21 May 2019 18:41:35 +0800 Subject: [PATCH 144/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 108 +-- tutorials/README.md | 11 +- tutorials/fastNLP_padding_tutorial.ipynb | 370 --------- tutorials/fastnlp_10min_tutorial.ipynb | 751 ------------------ tutorials/fastnlp_test_tutorial.ipynb | 97 --- .../{tutorial_one.ipynb => tutorial_1.ipynb} | 0 tutorials/tutorial_for_developer.md | 283 ------- 7 files changed, 64 insertions(+), 1556 deletions(-) delete mode 100644 tutorials/fastNLP_padding_tutorial.ipynb delete mode 100644 tutorials/fastnlp_10min_tutorial.ipynb delete mode 100644 tutorials/fastnlp_test_tutorial.ipynb rename tutorials/{tutorial_one.ipynb => tutorial_1.ipynb} (100%) delete mode 100644 tutorials/tutorial_for_developer.md diff --git a/README.md b/README.md index bb62fc38..f4a8f2a6 100644 --- a/README.md +++ b/README.md @@ -6,94 +6,108 @@ ![Hex.pm](https://img.shields.io/hexpm/l/plug.svg) [![Documentation Status](https://readthedocs.org/projects/fastnlp/badge/?version=latest)](http://fastnlp.readthedocs.io/?badge=latest) -FastNLP is a modular Natural Language Processing system based on PyTorch, built for fast development of NLP models. +fastNLP 是一款轻量级的 NLP 处理套件。你既可以使用它快速地完成一个命名实体识别(NER)、中文分词或文本分类任务; 也可以使用他构建许多复杂的网络模型,进行科研。它具有如下的特性: + +- 统一的Tabular式数据容器,让数据预处理过程简洁明了。内置多种数据集的DataSet Loader,省去预处理代码。 +- 各种方便的NLP工具,例如预处理embedding加载; 中间数据cache等; +- 详尽的中文文档以供查阅; +- 提供诸多高级模块,例如Variational LSTM, Transformer, CRF等; +- 封装CNNText,Biaffine等模型可供直接使用; +- 便捷且具有扩展性的训练器; 提供多种内置callback函数,方便实验记录、异常捕获等。 + + +## 安装指南 + +fastNLP 依赖如下包: + ++ numpy ++ torch>=0.4.0 ++ tqdm ++ nltk + +其中torch的安装可能与操作系统及 CUDA 的版本相关,请参见 PyTorch 官网 。 +在依赖包安装完成的情况,您可以在命令行执行如下指令完成安装 + +```shell +pip install fastNLP +``` + + +## 内置组件 + +大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三种模块组成。 + + +![](./docs/source/figures/text_classification.png) + +fastNLP 在 modules 模块中内置了三种模块的诸多组件,可以帮助用户快速搭建自己所需的网络。 三种模块的功能和常见组件如下: -A deep learning NLP model is the composition of three types of modules:
- - - + + + - + - + - +
module type functionality example 类型 功能 例子
encoder encode the input into some abstract representation 将输入编码为具有具 有表示能力的向量 embedding, RNN, CNN, transformer
aggregator aggregate and reduce information 从多个向量中聚合信息 self-attention, max-pooling
decoder decode the representation into the output 将具有某种表示意义的 向量解码为需要的输出 形式 MLP, CRF
-For example: - -![](docs/source/figures/text_classification.png) - -## Requirements - -- Python>=3.6 -- numpy>=1.14.2 -- torch>=0.4.0 -- tensorboardX -- tqdm>=4.28.1 +## 完整模型 +fastNLP 为不同的 NLP 任务实现了许多完整的模型,它们都经过了训练和测试。 -## Resources +你可以在以下两个地方查看相关信息 +- [介绍](reproduction/) +- [源码](fastNLP/models/) -- [Tutorials](https://github.com/fastnlp/fastNLP/tree/master/tutorials) -- [Documentation](https://fastnlp.readthedocs.io/en/latest/) -- [Source Code](https://github.com/fastnlp/fastNLP) +## 项目结构 +![](./docs/source/figures/workflow.png) -## Installation -Run the following commands to install fastNLP package. -```shell -pip install fastNLP -``` - -## Models -fastNLP implements different models for variant NLP tasks. -Each model has been trained and tested carefully. - -Check out models' performance, usage and source code here. -- [Documentation](reproduction/) -- [Source Code](fastNLP/models/) - -## Project Structure +fastNLP的大致工作流程如上图所示,而项目结构如下: - - - - - + - + - + - + - +
fastNLP an open-source NLP library
fastNLP.api APIs for end-to-end prediction 开源的自然语言处理库
fastNLP.core data representation & train/test procedure 实现了核心功能,包括数据处理组件、训练器、测速器等
fastNLP.models a collection of NLP models 实现了一些完整的神经网络模型
fastNLP.modules a collection of PyTorch sub-models/components/wheels 实现了用于搭建神经网络模型的诸多组件
fastNLP.io readers & savers 实现了读写功能,包括数据读入,模型读写等
+## 参考资源 + +- [教程](https://github.com/fastnlp/fastNLP/tree/master/tutorials) +- [文档](https://fastnlp.readthedocs.io/en/latest/) +- [源码](https://github.com/fastnlp/fastNLP) + + *In memory of @FengZiYjun. May his soul rest in peace. We will miss you very very much!* \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md index 1de342e6..83df2bb9 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -1,12 +1,7 @@ # fastNLP 教程 ### 上手教程 Quick Start -- 一分钟上手:`fastnlp_1min_tutorial.ipynb` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/fastnlp_1min_tutorial.ipynb) -- 十分钟上手:`fastnlp_10min_tutorial.ipynb` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/fastnlp_10min_tutorial.ipynb) +`quickstart.ipynb` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/quickstart.ipynb) -### 进阶教程 Advanced Tutorial -- `fastnlp_advanced_tutorial/advance_tutorial.ipynb` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/fastnlp_advanced_tutorial/advance_tutorial.ipynb) - - -### 开发者指南 Developer Guide -- `tutorial_for_developer.md` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/tutorial_for_developer.md) +### 详细教程 Tutorial 1 +十分钟上手:`tutorial_1.ipynb` [Click Here](https://github.com/fastnlp/fastNLP/tree/master/tutorials/tutorial_1.ipynb) diff --git a/tutorials/fastNLP_padding_tutorial.ipynb b/tutorials/fastNLP_padding_tutorial.ipynb deleted file mode 100644 index 7dc50206..00000000 --- a/tutorials/fastNLP_padding_tutorial.ipynb +++ /dev/null @@ -1,370 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/yh/miniconda2/envs/python3/lib/python3.6/site-packages/tqdm/autonotebook/__init__.py:14: TqdmExperimentalWarning: Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)\n", - " \" (e.g. in jupyter console)\", TqdmExperimentalWarning)\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet({'raw_sent': this is a bad idea . type=str,\n", - "'label': 0 type=int,\n", - "'word_str_lst': ['this', 'is', 'a', 'bad', 'idea', '.'] type=list,\n", - "'words': [4, 2, 5, 6, 7, 3] type=list},\n", - "{'raw_sent': it is great . type=str,\n", - "'label': 1 type=int,\n", - "'word_str_lst': ['it', 'is', 'great', '.'] type=list,\n", - "'words': [8, 2, 9, 3] type=list})" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 假设有以下的DataSet, 这里只是为了举例所以只选择了两个sample\n", - "import sys\n", - "import os\n", - "sys.path.append('/Users/yh/Desktop/fastNLP/fastNLP')\n", - "\n", - "from fastNLP import DataSet\n", - "from fastNLP import Instance\n", - "from fastNLP import Vocabulary\n", - "\n", - "dataset = DataSet()\n", - "dataset.append(Instance(raw_sent='This is a bad idea .', label=0))\n", - "dataset.append(Instance(raw_sent='It is great .', label=1))\n", - "\n", - "# 按照fastNLP_10min_tutorial.ipynb的步骤,对数据进行一些处理。这里为了演示padding操作,把field的名称做了一些改变\n", - "dataset.apply(lambda x:x['raw_sent'].lower(), new_field_name='raw_sent')\n", - "dataset.apply(lambda x:x['raw_sent'].split(), new_field_name='word_str_lst')\n", - "\n", - "# 建立Vocabulary\n", - "word_vocab = Vocabulary()\n", - "dataset.apply(lambda x:word_vocab.update(x['word_str_lst']))\n", - "dataset.apply(lambda x:[word_vocab.to_index(word) for word in x['word_str_lst']], new_field_name='words')\n", - "\n", - "# 检查以下是否得到我们想要的结果了\n", - "dataset[:2]" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'word_str_lst': array([list(['this', 'is', 'a', 'bad', 'idea', '.']),\n", - " list(['it', 'is', 'great', '.'])], dtype=object), 'words': tensor([[4, 2, 5, 6, 7, 3],\n", - " [8, 2, 9, 3, 0, 0]])}\n", - "batch_y has: {'label': tensor([0, 1])}\n" - ] - }, - { - "data": { - "text/plain": [ - "'\"\\n结果中\\n Batch会对元素类型(元素即最内层的数据,raw_sent为str,word_str_lst为str,words为int, label为int)为int或者float的数据进行默认\\n padding,而非int或float的则不进行padding。但若每个Instance中该field为二维数据,也不进行padding。因为二维数据的padding涉及到\\n 两个维度的padding,不容易自动判断padding的形式。\\n'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 将field设置为input或者target\n", - "dataset.set_input('word_str_lst')\n", - "dataset.set_input('words')\n", - "dataset.set_target('label')\n", - "\n", - "# 使用Batch取出batch数据\n", - "from fastNLP.core.batch import Batch\n", - "from fastNLP.core.sampler import RandomSampler\n", - "\n", - "batch_iterator = Batch(dataset=dataset, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - "\"\"\"\"\n", - "结果中\n", - " Batch会对元素类型(元素即最内层的数据,raw_sent为str,word_str_lst为str,words为int, label为int)为int或者float的数据进行默认\n", - " padding,而非int或float的则不进行padding。但若每个Instance中该field为二维数据,也不进行padding。因为二维数据的padding涉及到\n", - " 两个维度的padding,不容易自动判断padding的形式。\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'word_str_lst': array([list(['it', 'is', 'great', '.']),\n", - " list(['this', 'is', 'a', 'bad', 'idea', '.'])], dtype=object), 'words': tensor([[ 8, 2, 9, 3, -100, -100],\n", - " [ 4, 2, 5, 6, 7, 3]])}\n", - "batch_y has: {'label': tensor([1, 0])}\n" - ] - } - ], - "source": [ - "# 所有的pad_val都默认为0,如果需要修改某一个field的默认pad值,可以通过DataSet.set_pad_val(field_name, pad_val)进行修改\n", - "# 若需要将word的padding修改为-100\n", - "dataset.set_pad_val('words', pad_val=-100)\n", - "batch_iterator = Batch(dataset=dataset, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - "# pad的值修改为-100了" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataSet({'raw_sent': this is a bad idea . type=str,\n", - "'label': 0 type=int,\n", - "'word_str_lst': ['this', 'is', 'a', 'bad', 'idea', '.'] type=list,\n", - "'words': [4, 2, 5, 6, 7, 3] type=list,\n", - "'char_str_lst': [['t', 'h', 'i', 's'], ['i', 's'], ['a'], ['b', 'a', 'd'], ['i', 'd', 'e', 'a'], ['.']] type=list,\n", - "'chars': [[4, 9, 2, 5], [2, 5], [3], [10, 3, 6], [2, 6, 7, 3], [8]] type=list},\n", - "{'raw_sent': it is great . type=str,\n", - "'label': 1 type=int,\n", - "'word_str_lst': ['it', 'is', 'great', '.'] type=list,\n", - "'words': [8, 2, 9, 3] type=list,\n", - "'char_str_lst': [['i', 't'], ['i', 's'], ['g', 'r', 'e', 'a', 't'], ['.']] type=list,\n", - "'chars': [[2, 4], [2, 5], [11, 12, 7, 3, 4], [8]] type=list})" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 若需要使用二维padding或指定padding方式,可以通过设置该field的padder实现,下面以英文的character padding为例。在某些场景下,可能想要\n", - "# 使用英文word的character作为特征,character的padding为二维padding,fastNLP默认只会进行一维padding。\n", - "\n", - "dataset.apply(lambda x: [[c for c in word] for word in x['word_str_lst']], new_field_name='char_str_lst')\n", - "char_vocab = Vocabulary()\n", - "dataset.apply(lambda x:[char_vocab.update(chars) for chars in x['char_str_lst']])\n", - "dataset.apply(lambda x:[[char_vocab.to_index(c) for c in chars] for chars in x['char_str_lst']],new_field_name='chars')\n", - "dataset[:2]" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'word_str_lst': array([list(['this', 'is', 'a', 'bad', 'idea', '.']),\n", - " list(['it', 'is', 'great', '.'])], dtype=object), 'words': tensor([[ 4, 2, 5, 6, 7, 3],\n", - " [ 8, 2, 9, 3, -100, -100]]), 'chars': array([list([[4, 9, 2, 5], [2, 5], [3], [10, 3, 6], [2, 6, 7, 3], [8]]),\n", - " list([[2, 4], [2, 5], [11, 12, 7, 3, 4], [8]])], dtype=object)}\n", - "batch_y has: {'label': tensor([0, 1])}\n" - ] - }, - { - "data": { - "text/plain": [ - "'\\n 其它field与之前的是相同的。chars因为存在两个维度需要padding,不能自动决定padding方式,所以直接输出了原始形式。\\n'" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 如果不针对二维的character指定padding方法\n", - "dataset.set_input('chars')\n", - "batch_iterator = Batch(dataset=dataset, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - " \n", - "\"\"\"\n", - " 其它field与之前的是相同的。chars因为存在两个维度需要padding,不能自动决定padding方式,所以直接输出了原始形式。\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'word_str_lst': array([list(['this', 'is', 'a', 'bad', 'idea', '.']),\n", - " list(['it', 'is', 'great', '.'])], dtype=object), 'words': tensor([[ 4, 2, 5, 6, 7, 3],\n", - " [ 8, 2, 9, 3, -100, -100]]), 'chars': tensor([[[ 4, 9, 2, 5],\n", - " [ 2, 5, 0, 0],\n", - " [ 3, 0, 0, 0],\n", - " [10, 3, 6, 0],\n", - " [ 2, 6, 7, 3],\n", - " [ 8, 0, 0, 0]],\n", - "\n", - " [[ 2, 4, 0, 0],\n", - " [ 2, 5, 0, 0],\n", - " [11, 12, 7, 3],\n", - " [ 8, 0, 0, 0],\n", - " [ 0, 0, 0, 0],\n", - " [ 0, 0, 0, 0]]])}\n", - "batch_y has: {'label': tensor([0, 1])}\n" - ] - }, - { - "data": { - "text/plain": [ - "'\\n chars被正确padding了\\n'" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 若要使用二维padding,需要手动设置padding方式\n", - "from fastNLP.core.fieldarray import EngChar2DPadder\n", - "dataset.set_padder('chars', EngChar2DPadder())\n", - "batch_iterator = Batch(dataset=dataset, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - " \n", - "\"\"\"\n", - " chars被正确padding了\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'raw_sent': ['this is a bad idea .', 'it is great . '], 'word_str_lst': array([list(['this', 'is', 'a', 'bad', 'idea', '.']),\n", - " list(['it', 'is', 'great', '.'])], dtype=object), 'words': tensor([[ 4, 2, 5, 6, 7, 3],\n", - " [ 8, 2, 9, 3, -100, -100]]), 'chars': tensor([[[ 4, 9, 2, 5],\n", - " [ 2, 5, 0, 0],\n", - " [ 3, 0, 0, 0],\n", - " [10, 3, 6, 0],\n", - " [ 2, 6, 7, 3],\n", - " [ 8, 0, 0, 0]],\n", - "\n", - " [[ 2, 4, 0, 0],\n", - " [ 2, 5, 0, 0],\n", - " [11, 12, 7, 3],\n", - " [ 8, 0, 0, 0],\n", - " [ 0, 0, 0, 0],\n", - " [ 0, 0, 0, 0]]])}\n", - "batch_y has: {'label': tensor([0, 1])}\n" - ] - }, - { - "data": { - "text/plain": [ - "'\\n raw_sent正确输出,对应内容也进行了pad。\\n'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# 如果AutoPad与EngChar2DPadder不能满足需要,可以自己实现Padder对象。这里举一个例子,比如需要把raw_sentence pad到一样长\n", - "from fastNLP.core.fieldarray import PadderBase\n", - "\n", - "class PadStr(PadderBase):\n", - " def __init__(self, pad_val=' '):\n", - " super().__init__(pad_val=pad_val) #让父类管理pad_val的值,这样可以通过DataSet.set_pad_val()修改到该值\n", - " \n", - " def __call__(self, contents, field_name, field_ele_dtype):\n", - " \"\"\"\n", - " 如果以上面的例子举例,在raw_sent这个field进行pad时,传入的\n", - " contents:\n", - " [\n", - " 'This is a bad idea .',\n", - " 'It is great .'\n", - " ]\n", - " field_name: 'raw_sent',当前field的名称,主要用于帮助debug。\n", - " field_ele_dtype: np.str. 这个参数基本都用不上,是该field中内部元素的类型\n", - " \"\"\"\n", - " max_len = max([len(str_) for str_ in contents])\n", - " pad_strs = []\n", - " for content in contents:\n", - " pad_strs.append(content + (max_len-len(content))*self.pad_val)\n", - " return pad_strs\n", - "\n", - "dataset.set_input('raw_sent')\n", - "dataset.set_padder('raw_sent', PadStr())\n", - "batch_iterator = Batch(dataset=dataset, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - "\n", - "\"\"\"\n", - " raw_sent正确输出,对应内容也进行了pad。\n", - "\"\"\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/fastnlp_10min_tutorial.ipynb b/tutorials/fastnlp_10min_tutorial.ipynb deleted file mode 100644 index 526fd49f..00000000 --- a/tutorials/fastnlp_10min_tutorial.ipynb +++ /dev/null @@ -1,751 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "fastNLP10 分钟上手教程\n", - "-------\n", - "\n", - "fastNLP提供方便的数据预处理,训练和测试模型的功能" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "如果您还没有通过pip安装fastNLP,可以执行下面的操作加载当前模块" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "import sys\n", - "sys.path.append(\"../\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "DataSet & Instance\n", - "------\n", - "\n", - "fastNLP用DataSet和Instance保存和处理数据。每个DataSet表示一个数据集,每个Instance表示一个数据样本。一个DataSet存有多个Instance,每个Instance可以自定义存哪些内容。\n", - "\n", - "有一些read_*方法,可以轻松从文件读取数据,存成DataSet。" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "77\n" - ] - } - ], - "source": [ - "from fastNLP import DataSet\n", - "from fastNLP import Instance\n", - "\n", - "# 从csv读取数据到DataSet\n", - "dataset = DataSet.read_csv('sample_data/tutorial_sample_dataset.csv', headers=('raw_sentence', 'label'), sep='\\t')\n", - "print(len(dataset))" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': A series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=str}\n", - "{'raw_sentence': The plot is romantic comedy boilerplate from start to finish . type=str,\n", - "'label': 2 type=str}\n" - ] - } - ], - "source": [ - "# 使用数字索引[k],获取第k个样本\n", - "print(dataset[0])\n", - "\n", - "# 索引也可以是负数\n", - "print(dataset[-3])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Instance\n", - "Instance表示一个样本,由一个或多个field(域,属性,特征)组成,每个field有名字和值。\n", - "\n", - "在初始化Instance时即可定义它包含的域,使用 \"field_name=field_value\"的写法。" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'raw_sentence': fake data type=str,\n", - "'label': 0 type=str}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# DataSet.append(Instance)加入新数据\n", - "dataset.append(Instance(raw_sentence='fake data', label='0'))\n", - "dataset[-1]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## DataSet.apply方法\n", - "数据预处理利器" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=str}\n" - ] - } - ], - "source": [ - "# 将所有数字转为小写\n", - "dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='raw_sentence')\n", - "print(dataset[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=int}\n" - ] - } - ], - "source": [ - "# label转int\n", - "dataset.apply(lambda x: int(x['label']), new_field_name='label')\n", - "print(dataset[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=int,\n", - "'words': ['a', 'series', 'of', 'escapades', 'demonstrating', 'the', 'adage', 'that', 'what', 'is', 'good', 'for', 'the', 'goose', 'is', 'also', 'good', 'for', 'the', 'gander', ',', 'some', 'of', 'which', 'occasionally', 'amuses', 'but', 'none', 'of', 'which', 'amounts', 'to', 'much', 'of', 'a', 'story', '.'] type=list}\n" - ] - } - ], - "source": [ - "# 使用空格分割句子\n", - "def split_sent(ins):\n", - " return ins['raw_sentence'].split()\n", - "dataset.apply(split_sent, new_field_name='words')\n", - "print(dataset[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': a series of escapades demonstrating the adage that what is good for the goose is also good for the gander , some of which occasionally amuses but none of which amounts to much of a story . type=str,\n", - "'label': 1 type=int,\n", - "'words': ['a', 'series', 'of', 'escapades', 'demonstrating', 'the', 'adage', 'that', 'what', 'is', 'good', 'for', 'the', 'goose', 'is', 'also', 'good', 'for', 'the', 'gander', ',', 'some', 'of', 'which', 'occasionally', 'amuses', 'but', 'none', 'of', 'which', 'amounts', 'to', 'much', 'of', 'a', 'story', '.'] type=list,\n", - "'seq_len': 37 type=int}\n" - ] - } - ], - "source": [ - "# 增加长度信息\n", - "dataset.apply(lambda x: len(x['words']), new_field_name='seq_len')\n", - "print(dataset[0])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## DataSet.drop\n", - "筛选数据" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "77\n" - ] - } - ], - "source": [ - "# 删除低于某个长度的词语\n", - "dataset.drop(lambda x: x['seq_len'] <= 3)\n", - "print(len(dataset))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 配置DataSet\n", - "1. 哪些域是特征,哪些域是标签\n", - "2. 切分训练集/验证集" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# 设置DataSet中,哪些field要转为tensor\n", - "\n", - "# set target,loss或evaluate中的golden,计算loss,模型评估时使用\n", - "dataset.set_target(\"label\")\n", - "# set input,模型forward时使用\n", - "dataset.set_input(\"words\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "54\n", - "23\n" - ] - } - ], - "source": [ - "# 分出测试集、训练集\n", - "\n", - "test_data, train_data = dataset.split(0.3)\n", - "print(len(test_data))\n", - "print(len(train_data))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Vocabulary\n", - "------\n", - "\n", - "fastNLP中的Vocabulary轻松构建词表,将词转成数字" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'raw_sentence': the performances are an absolute joy . type=str,\n", - "'label': 4 type=int,\n", - "'words': [3, 1, 1, 26, 1, 1, 2] type=list,\n", - "'seq_len': 7 type=int}\n" - ] - } - ], - "source": [ - "from fastNLP import Vocabulary\n", - "\n", - "# 构建词表, Vocabulary.add(word)\n", - "vocab = Vocabulary(min_freq=2)\n", - "train_data.apply(lambda x: [vocab.add(word) for word in x['words']])\n", - "vocab.build_vocab()\n", - "\n", - "# index句子, Vocabulary.to_index(word)\n", - "train_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words')\n", - "test_data.apply(lambda x: [vocab.to_index(word) for word in x['words']], new_field_name='words')\n", - "\n", - "\n", - "print(test_data[0])" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "batch_x has: {'words': tensor([[ 15, 72, 15, 73, 74, 7, 3, 75, 6, 3, 16, 16,\n", - " 76, 2],\n", - " [ 15, 72, 15, 73, 74, 7, 3, 75, 6, 3, 16, 16,\n", - " 76, 2]])}\n", - "batch_y has: {'label': tensor([ 1, 1])}\n" - ] - } - ], - "source": [ - "# 如果你们需要做强化学习或者GAN之类的项目,你们也可以使用这些数据预处理的工具\n", - "from fastNLP.core.batch import Batch\n", - "from fastNLP.core.sampler import RandomSampler\n", - "\n", - "batch_iterator = Batch(dataset=train_data, batch_size=2, sampler=RandomSampler())\n", - "for batch_x, batch_y in batch_iterator:\n", - " print(\"batch_x has: \", batch_x)\n", - " print(\"batch_y has: \", batch_y)\n", - " break" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Model\n", - "定义一个PyTorch模型" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "CNNText(\n", - " (embed): Embedding(\n", - " 77, 50\n", - " (dropout): Dropout(p=0.0)\n", - " )\n", - " (conv_pool): ConvMaxpool(\n", - " (convs): ModuleList(\n", - " (0): Conv1d(50, 3, kernel_size=(3,), stride=(1,), padding=(2,))\n", - " (1): Conv1d(50, 4, kernel_size=(4,), stride=(1,), padding=(2,))\n", - " (2): Conv1d(50, 5, kernel_size=(5,), stride=(1,), padding=(2,))\n", - " )\n", - " )\n", - " (dropout): Dropout(p=0.1)\n", - " (fc): Linear(\n", - " (linear): Linear(in_features=12, out_features=5, bias=True)\n", - " )\n", - ")" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from fastNLP.models import CNNText\n", - "model = CNNText((len(vocab), 50), num_classes=5, padding=2, dropout=0.1)\n", - "model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "这是上述模型的forward方法。如果你不知道什么是forward方法,请参考我们的PyTorch教程。\n", - "\n", - "注意两点:\n", - "1. forward参数名字叫**word_seq**,请记住。\n", - "2. forward的返回值是一个**dict**,其中有个key的名字叫**output**。\n", - "\n", - "```Python\n", - " def forward(self, word_seq):\n", - " \"\"\"\n", - "\n", - " :param word_seq: torch.LongTensor, [batch_size, seq_len]\n", - " :return output: dict of torch.LongTensor, [batch_size, num_classes]\n", - " \"\"\"\n", - " x = self.embed(word_seq) # [N,L] -> [N,L,C]\n", - " x = self.conv_pool(x) # [N,L,C] -> [N,C]\n", - " x = self.dropout(x)\n", - " x = self.fc(x) # [N,C] -> [N, N_class]\n", - " return {'output': x}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "这是上述模型的predict方法,是用来直接输出该任务的预测结果,与forward目的不同。\n", - "\n", - "注意两点:\n", - "1. predict参数名也叫**word_seq**。\n", - "2. predict的返回值是也一个**dict**,其中有个key的名字叫**predict**。\n", - "\n", - "```\n", - " def predict(self, word_seq):\n", - " \"\"\"\n", - "\n", - " :param word_seq: torch.LongTensor, [batch_size, seq_len]\n", - " :return predict: dict of torch.LongTensor, [batch_size, seq_len]\n", - " \"\"\"\n", - " output = self(word_seq)\n", - " _, predict = output['output'].max(dim=1)\n", - " return {'predict': predict}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Trainer & Tester\n", - "------\n", - "\n", - "使用fastNLP的Trainer训练模型" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "from fastNLP import Trainer\n", - "from copy import deepcopy\n", - "from fastNLP.core.losses import CrossEntropyLoss\n", - "from fastNLP.core.metrics import AccuracyMetric\n", - "\n", - "\n", - "# 更改DataSet中对应field的名称,与模型的forward的参数名一致\n", - "# 因为forward的参数叫word_seq, 所以要把原本叫words的field改名为word_seq\n", - "# 这里的演示是让你了解这种**命名规则**\n", - "train_data.rename_field('words', 'word_seq')\n", - "test_data.rename_field('words', 'word_seq')\n", - "\n", - "# 顺便把label换名为label_seq\n", - "train_data.rename_field('label', 'label_seq')\n", - "test_data.rename_field('label', 'label_seq')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### loss\n", - "训练模型需要提供一个损失函数\n", - "\n", - "下面提供了一个在分类问题中常用的交叉熵损失。注意它的**初始化参数**。\n", - "\n", - "pred参数对应的是模型的forward返回的dict的一个key的名字,这里是\"output\"。\n", - "\n", - "target参数对应的是dataset作为标签的field的名字,这里是\"label_seq\"。" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "loss = CrossEntropyLoss(pred=\"output\", target=\"label_seq\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Metric\n", - "定义评价指标\n", - "\n", - "这里使用准确率。参数的“命名规则”跟上面类似。\n", - "\n", - "pred参数对应的是模型的predict方法返回的dict的一个key的名字,这里是\"predict\"。\n", - "\n", - "target参数对应的是dataset作为标签的field的名字,这里是\"label_seq\"。" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [], - "source": [ - "metric = AccuracyMetric(pred=\"predict\", target=\"label_seq\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "input fields after batch(if batch size is 2):\n", - "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 11]) \n", - "target fields after batch(if batch size is 2):\n", - "\tlabel_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", - "\n" - ] - }, - { - "ename": "NameError", - "evalue": "\nProblems occurred when calling CNNText.forward(self, words, seq_len=None)\n\tmissing param: ['words']\n\tunused field: ['word_seq']\n\tSuggestion: You need to provide ['words'] in DataSet and set it as input. ", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0msave_path\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[0mbatch_size\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m32\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 9\u001b[0;31m n_epochs=5)\n\u001b[0m\u001b[1;32m 10\u001b[0m \u001b[0moverfit_trainer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m__init__\u001b[0;34m(self, train_data, model, optimizer, loss, batch_size, sampler, update_every, n_epochs, print_every, dev_data, metrics, metric_key, validate_every, save_path, prefetch, use_tqdm, device, callbacks, check_code_level)\u001b[0m\n\u001b[1;32m 447\u001b[0m _check_code(dataset=train_data, model=model, losser=losser, metrics=metrics, dev_data=dev_data,\n\u001b[1;32m 448\u001b[0m \u001b[0mmetric_key\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmetric_key\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcheck_level\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mcheck_code_level\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 449\u001b[0;31m batch_size=min(batch_size, DEFAULT_CHECK_BATCH_SIZE))\n\u001b[0m\u001b[1;32m 450\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 451\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtrain_data\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtrain_data\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/trainer.py\u001b[0m in \u001b[0;36m_check_code\u001b[0;34m(dataset, model, losser, metrics, batch_size, dev_data, metric_key, check_level)\u001b[0m\n\u001b[1;32m 808\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0minfo_str\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 809\u001b[0m _check_forward_error(forward_func=model.forward, dataset=dataset,\n\u001b[0;32m--> 810\u001b[0;31m batch_x=batch_x, check_level=check_level)\n\u001b[0m\u001b[1;32m 811\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 812\u001b[0m \u001b[0mrefined_batch_x\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_build_args\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmodel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mforward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mbatch_x\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/Users/fdujyn/anaconda3/lib/python3.6/site-packages/fastNLP/core/utils.py\u001b[0m in \u001b[0;36m_check_forward_error\u001b[0;34m(forward_func, batch_x, dataset, check_level)\u001b[0m\n\u001b[1;32m 594\u001b[0m \u001b[0msugg_str\u001b[0m \u001b[0;34m+=\u001b[0m \u001b[0msuggestions\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 595\u001b[0m \u001b[0merr_str\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m'\\n'\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m'\\n'\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merrs\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m'\\n\\tSuggestion: '\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msugg_str\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 596\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mNameError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0merr_str\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 597\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0m_unused\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 598\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcheck_level\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mWARNING_CHECK_LEVEL\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mNameError\u001b[0m: \nProblems occurred when calling CNNText.forward(self, words, seq_len=None)\n\tmissing param: ['words']\n\tunused field: ['word_seq']\n\tSuggestion: You need to provide ['words'] in DataSet and set it as input. " - ] - } - ], - "source": [ - "# 实例化Trainer,传入模型和数据,进行训练\n", - "# 先在test_data拟合(确保模型的实现是正确的)\n", - "copy_model = deepcopy(model)\n", - "overfit_trainer = Trainer(model=copy_model, train_data=test_data, dev_data=test_data,\n", - " loss=loss,\n", - " metrics=metric,\n", - " save_path=None,\n", - " batch_size=32,\n", - " n_epochs=5)\n", - "overfit_trainer.train()" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "input fields after batch(if batch size is 2):\n", - "\tword_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 20]) \n", - "target fields after batch(if batch size is 2):\n", - "\tlabel_seq: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) \n", - "\n", - "training epochs started 2019-01-12 17-09-05\n" - ] - }, - { - "data": { - "text/plain": [ - "HBox(children=(IntProgress(value=0, layout=Layout(flex='2'), max=5), HTML(value='')), layout=Layout(display='i…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Evaluation at Epoch 1/5. Step:1/5. AccuracyMetric: acc=0.37037\n", - "Evaluation at Epoch 2/5. Step:2/5. AccuracyMetric: acc=0.37037\n", - "Evaluation at Epoch 3/5. Step:3/5. AccuracyMetric: acc=0.462963\n", - "Evaluation at Epoch 4/5. Step:4/5. AccuracyMetric: acc=0.425926\n", - "Evaluation at Epoch 5/5. Step:5/5. AccuracyMetric: acc=0.481481\n", - "\n", - "In Epoch:5/Step:5, got best dev performance:AccuracyMetric: acc=0.481481\n", - "Reloaded the best model.\n", - "Train finished!\n" - ] - } - ], - "source": [ - "# 用train_data训练,在test_data验证\n", - "trainer = Trainer(model=model, train_data=train_data, dev_data=test_data,\n", - " loss=CrossEntropyLoss(pred=\"output\", target=\"label_seq\"),\n", - " metrics=AccuracyMetric(pred=\"predict\", target=\"label_seq\"),\n", - " save_path=None,\n", - " batch_size=32,\n", - " n_epochs=5)\n", - "trainer.train()\n", - "print('Train finished!')" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[tester] \n", - "AccuracyMetric: acc=0.481481\n", - "{'AccuracyMetric': {'acc': 0.481481}}\n" - ] - } - ], - "source": [ - "# 调用Tester在test_data上评价效果\n", - "from fastNLP import Tester\n", - "\n", - "tester = Tester(data=test_data, model=model, metrics=AccuracyMetric(pred=\"predict\", target=\"label_seq\"),\n", - " batch_size=4)\n", - "acc = tester.test()\n", - "print(acc)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# In summary\n", - "\n", - "## fastNLP Trainer的伪代码逻辑\n", - "### 1. 准备DataSet,假设DataSet中共有如下的fields\n", - " ['raw_sentence', 'word_seq1', 'word_seq2', 'raw_label','label']\n", - " 通过\n", - " DataSet.set_input('word_seq1', word_seq2', flag=True)将'word_seq1', 'word_seq2'设置为input\n", - " 通过\n", - " DataSet.set_target('label', flag=True)将'label'设置为target\n", - "### 2. 初始化模型\n", - " class Model(nn.Module):\n", - " def __init__(self):\n", - " xxx\n", - " def forward(self, word_seq1, word_seq2):\n", - " # (1) 这里使用的形参名必须和DataSet中的input field的名称对应。因为我们是通过形参名, 进行赋值的\n", - " # (2) input field的数量可以多于这里的形参数量。但是不能少于。\n", - " xxxx\n", - " # 输出必须是一个dict\n", - "### 3. Trainer的训练过程\n", - " (1) 从DataSet中按照batch_size取出一个batch,调用Model.forward\n", - " (2) 将 Model.forward的结果 与 标记为target的field 传入Losser当中。\n", - " 由于每个人写的Model.forward的output的dict可能key并不一样,比如有人是{'pred':xxx}, {'output': xxx}; \n", - " 另外每个人将target可能也会设置为不同的名称, 比如有人是label, 有人设置为target;\n", - " 为了解决以上的问题,我们的loss提供映射机制\n", - " 比如CrossEntropyLosser的需要的输入是(prediction, target)。但是forward的output是{'output': xxx}; 'label'是target\n", - " 那么初始化losser的时候写为CrossEntropyLosser(prediction='output', target='label')即可\n", - " (3) 对于Metric是同理的\n", - " Metric计算也是从 forward的结果中取值 与 设置target的field中取值。 也是可以通过映射找到对应的值 \n", - " \n", - " \n", - "\n", - "## 一些问题.\n", - "### 1. DataSet中为什么需要设置input和target\n", - " 只有被设置为input或者target的数据才会在train的过程中被取出来\n", - " (1.1) 我们只会在设置为input的field中寻找传递给Model.forward的参数。\n", - " (1.2) 我们在传递值给losser或者metric的时候会使用来自: \n", - " (a)Model.forward的output\n", - " (b)被设置为target的field\n", - " \n", - "\n", - "### 2. 我们是通过forwad中的形参名将DataSet中的field赋值给对应的参数\n", - " (1.1) 构建模型过程中,\n", - " 例如:\n", - " DataSet中x,seq_lens是input,那么forward就应该是\n", - " def forward(self, x, seq_lens):\n", - " pass\n", - " 我们是通过形参名称进行匹配的field的\n", - " \n", - "\n", - "\n", - "### 1. 加载数据到DataSet\n", - "### 2. 使用apply操作对DataSet进行预处理\n", - " (2.1) 处理过程中将某些field设置为input,某些field设置为target\n", - "### 3. 构建模型\n", - " (3.1) 构建模型过程中,需要注意forward函数的形参名需要和DataSet中设置为input的field名称是一致的。\n", - " 例如:\n", - " DataSet中x,seq_lens是input,那么forward就应该是\n", - " def forward(self, x, seq_lens):\n", - " pass\n", - " 我们是通过形参名称进行匹配的field的\n", - " (3.2) 模型的forward的output需要是dict类型的。\n", - " 建议将输出设置为{\"pred\": xx}.\n", - " \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/fastnlp_test_tutorial.ipynb b/tutorials/fastnlp_test_tutorial.ipynb deleted file mode 100644 index fb87606e..00000000 --- a/tutorials/fastnlp_test_tutorial.ipynb +++ /dev/null @@ -1,97 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## fastNLP测试说明\n", - "### 测试环境\n", - "fastNLP使用pytest对代码进行单元测试,测试代码在test文件夹下,测试所需数据在test/data_for_tests文件夹下\n", - "测试的步骤主要分为准备数据,执行测试,比对结果,清除环境四步\n", - "测试代码以test_xxx.py命名,以DataSet的测试代码为例,测试代码文件名为test_dataset.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import unittest # 单元测试需要用到unittest\n", - "\n", - "from fastNLP.core.dataset import DataSet\n", - "from fastNLP.core.fieldarray import FieldArray\n", - "from fastNLP.core.instance import Instance\n", - "# 在这个单元测试文件中,需要测试DataSet、FieldArray、以及Instance" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class TestDataSet(unittest.TestCase): # 类名字以Test打头,继承unittest.TestCase\n", - "\n", - " def test_init_v1(self): # 测试样例1, 函数名称以test_打头\n", - " # 该测试样例测试的是DataSet的初始化\n", - " ins = Instance(x=[1, 2, 3, 4], y=[5, 6]) # 准备数据\n", - " ds = DataSet([ins] * 40) # 执行测试(调用DataSet的初始化函数)\n", - " self.assertTrue(\"x\" in ds.field_arrays and \"y\" in ds.field_arrays) # 比对结果:'x'跟'y'都是ds的field\n", - " self.assertEqual(ds.field_arrays[\"x\"].content, [[1, 2, 3, 4], ] * 40) # 比对结果: field 'x'的内容正确\n", - " self.assertEqual(ds.field_arrays[\"y\"].content, [[5, 6], ] * 40) # 比对结果: field 'y'的内容正确\n", - " \n", - " def test_init_v2(self): # 测试样例2,该样例测试DataSet的另一种初始化方式\n", - " ds = DataSet({\"x\": [[1, 2, 3, 4]] * 40, \"y\": [[5, 6]] * 40})\n", - " self.assertTrue(\"x\" in ds.field_arrays and \"y\" in ds.field_arrays)\n", - " self.assertEqual(ds.field_arrays[\"x\"].content, [[1, 2, 3, 4], ] * 40)\n", - " self.assertEqual(ds.field_arrays[\"y\"].content, [[5, 6], ] * 40)\n", - " \n", - " def test_init_assert(self): # 测试样例3,该样例测试不规范初始化DataSet时是否会报正确错误\n", - " with self.assertRaises(AssertionError):\n", - " _ = DataSet({\"x\": [[1, 2, 3, 4]] * 40, \"y\": [[5, 6]] * 100})\n", - " with self.assertRaises(AssertionError):\n", - " _ = DataSet([[1, 2, 3, 4]] * 10)\n", - " with self.assertRaises(ValueError):\n", - " _ = DataSet(0.00001)\n", - " \n", - " def test_contains(self): # 测试样例4,该样例测试DataSet的contains函数,是功能测试\n", - " ds = DataSet({\"x\": [[1, 2, 3, 4]] * 40, \"y\": [[5, 6]] * 40})\n", - " self.assertTrue(\"x\" in ds)\n", - " self.assertTrue(\"y\" in ds)\n", - " self.assertFalse(\"z\" in ds)\n", - " \n", - " # 更多测试样例见test/core/test_dataset.py" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/tutorials/tutorial_one.ipynb b/tutorials/tutorial_1.ipynb similarity index 100% rename from tutorials/tutorial_one.ipynb rename to tutorials/tutorial_1.ipynb diff --git a/tutorials/tutorial_for_developer.md b/tutorials/tutorial_for_developer.md deleted file mode 100644 index b2ec0b98..00000000 --- a/tutorials/tutorial_for_developer.md +++ /dev/null @@ -1,283 +0,0 @@ -# fastNLP开发者指南 -#### 本教程涉及以下类: -- DataSet -- Sampler -- Batch -- Model -- Loss -- Metric -- Trainer -- Tester - -#### DataSet: 用于承载数据。 -1. DataSet里面每个元素只能是以下的三类`np.float64`, `np.int64`, `np.str`。如果传入的数据是`int`则被转换为`np.int64`, `float`被转为`np.float64`。 -2. DataSet可以将field设置为input或者target。其中被设置为input的field会被传递给Model.forward, 这个过程中我们是通过键匹配完成传递的。举例来说,假设DataSet中有'x1', 'x2', 'x3'被设置为了input,而 - - 函数是Model.forward(self, x1, x3), 那么DataSet中'x1', 'x3'会被传递给forward函数。多余的'x2'会被忽略 - - 函数是Model.forward(self, x1, x4), 这里多需要了一个'x4', 但是DataSet的input field中没有这个field,会报错。 - - 函数是Model.forward(self, x1, **kwargs), 会把'x1', 'x2', 'x3'都传入。但如果是Model.forward(self, x4, **kwargs)就会发生报错,因为没有'x4'。 -3. 对于设置为target的field的名称,我们建议取名为'target'(如果只有一个需要predict的值),但是不强制。后面会讲为什么target可以不强制。 -DataSet应该是不需要单独再开发的,如果有不能满足的场景,请在开发群提出或者github提交issue。 - -#### Sampler: 给定一个DataSet,返回一个序号的list,Batch按照这个list输出数据。 -Sampler需要继承fastNLP.core.sampler.BaseSampler -```python -class BaseSampler(object): - """The base class of all samplers. - - Sub-classes must implement the __call__ method. - __call__ takes a DataSet object and returns a list of int - the sampling indices. - """ -def __call__(self, *args, **kwargs): - raise NotImplementedError - -# 子类需要复写__call__方法。这个函数只能有一个必选参数, 且必须是DataSet类别, 否则Trainer没法调 -class SonSampler(BaseSampler): - def __init__(self, xxx): - # 可以实现init也不可以不实现。 - pass - def __call__(self, data_set): - pass -``` - -#### Batch: 将DataSet中设置为input和target的field取出来构成batch_x, batch_y -并且根据情况(主要根据数据类型能不能转为Tensor)将数据转换为pytorch的Tensor。batch中sample的取出顺序是由Sampler决定的。 -Sampler是传入一个DataSet,返回一个与DataSet等长的序号list,Batch一次会取出batch_size个sample(最后一个batch可能数量不足batch_size个)。 -举例: -1. SequentialSampler是顺序采样 - - 假设传入的DataSet长度是100, SequentialSampler返回的序号list就是[0, 1, ...,98, 99]. batch_size如果被设置为4,那么第一个batch所获取的instance就是[0, 1, 2, 3]这四个instance. 第二个batch所获取instace就是[4, 5, 6, 7], ...直到采完所有的sample。 -2. RandomSampler是随机采样 - - 假设传入的DataSet长度是100, RandomSampler返回的序号list可能是[0, 99, 20, 5, 3, 1, ...]. 依次按照batch_size的大小取出sample。 - -Batch应该不需要继承与开发,如果你有特殊需求请在开发群里提出。 - -#### Model:用户自定的Model -必须是nn.Module的子类 -1. 必须实现forward方法,并且forward方法不能出现*arg这种参数. 例如 - ```python - def forward(self, word_seq, *args): #这是不允许的. - # ... - pass - ``` - 返回值必须是dict的 - ```python - def forward(self, word_seq, seq_lens): - xxx = "xxx" - return {'pred': xxx} #return的值必须是dict的。里面的预测的key推荐使用pred,但是不做强制限制。输出元素数目不限。 - ``` -2. 如果实现了predict方法,在做evaluation的时候将调用predict方法而不是forward。如果没有predict方法,则在evaluation时调用forward方法。predict方法也不能使用*args这种参数形式,同时结果也必须返回一个dict,同样推荐key为'pred'。 - -#### Loss: 根据model.forward()返回的prediction(是一个dict)和batch_y计算相应的loss -1. 先介绍"键映射"。 如在DataSet, Model一节所看见的那样,fastNLP并不限制Model.forward()的返回值,也不限制DataSet中target field的key。计算的loss的时候,怎么才能知道从哪里取值呢? -这里以CrossEntropyLoss为例,一般情况下, 计算CrossEntropy需要prediction和target两个值。而在CrossEntropyLoss初始化时可以传入两个参数(pred=None, target=None), 这两个参数接受的类型是str,假设(pred='output', target='label'),那么CrossEntropyLoss会使用'output'这个key在forward的output与batch_y中寻找值;'label'也是在forward的output与batch_y中寻找值。注意这里pred或target的来源并不一定非要来自于model.forward与batch_y,也可以只来自于forward的结果。 -2. 如何创建一个自己的loss - - 使用fastNLP.LossInForward, 在model.forward()的结果中包含一个为loss的key。 - - trainer中使用loss(假设loss=CrossEntropyLoss())的时候其实是 - los = loss(prediction, batch_y),即直接调用的是`loss.__call__()`方法,但是CrossEntropyLoss里面并没有自己实现`__call__`方法,这是因为`__call__`在LossBase中实现了。所有的loss必须继承fastNLP.core.loss.LossBase, 下面先说一下LossBase的几个方法,见下一节。 -3. 尽量不要复写`__call__()`, `_init_param_map()`方法。 - -```python -class LossBase(): - def __init__(self): - self.param_map = {} # 一般情况下也不需要自己创建。调用_init_param_map()更好 - self._checked = False # 这个参数可以忽略 - - def _init_param_map(self, key_map=None, **kwargs): - # 这个函数是用于注册Loss的“键映射”,有两种传值方法, - # 第一种是通过key_map传入dict,取值是用value到forward和batch_y取 - # key_map = {'pred': 'output', 'target': 'label'} - # 第二种是自己写 - # _init_param_map(pred='output', target='label') - # 为什么会提供这么一个方法?通过调用这个方法会自动注册param_map,并会做一些检查,防止出现传入的key其实并不是get_loss - # 的一个参数。注意传入这个方法的参数必须都是需要做键映射的内容,其它loss参数不要传入。如果传入(pred=None, target=None) - # 则__call__()会到pred_dict与target_dict去寻找key为'pred'和'target'的值。 - # 但这个参数不是必须要调用的。 - - def __call__(self, pred_dict, target_dict, check=False): # check=False忽略这个参数,之后应该会被删除的 - # 这个函数主要会做一些check的工作,比如pred_dict与target_dict中是否包含了计算loss所必须的key等。检查通过,则调用get_loss - # 方法。 - fast_param = self._fast_param_map(predict_dict, target_dict): - if fast_param: - return self.get_loss(**fast_param) - # 如果没有fast_param则通过匹配参数然后调用get_loss完成 - xxxx - return loss # 返回为Tensor的loss - def _fast_param_map(self, pred_dict, target_dict): - # 这是一种快速计算loss的机制,因为在很多情况下其实都不需要通过"键映射",比如计算loss时,pred_dict只有一个元素, - # target_dict也只有一个元素,那么无歧义地就可以把预测值与实际值用于计算loss, 基类判断了这种情况(可能还有其它无歧义的情况)。 - # 即_fast_param_map成功的话,就不需要使用键映射,这样即使在没有传递或者传递错误"键映射"的情况也可以直接计算loss。 - # 返回值是一个dict, 如果匹配成功,应该返回类似{'pred':value, 'target': value}的结果;如果dict为空则说明匹配失败, - # __call__方法会继续执行。 - - def get_loss(self, *args, **kwargs): - # 这个是一定需要实现的,计算loss的地方。 - # (1) get_loss中一定不能包含*arg这种参数形式。 - # (2) 如果包含**kwargs这种参数,这会将pred_dict与target_dict中所有参数传入。但是建议不要用这个参数 - raise NotImplementedError - -# 下面使用L1Loss举例 -class L1Loss(LossBase): # 继承LossBase - # 初始化需要映射的值,这里需要映射的值'pred', 'target'必须与get_loss需要参数名是对应的 - def __init__(self, pred=None, target=None): - super(L1Loss, self).__init__() - # 这里传入_init_param_map以使得pred和target被正确注册,但这一步不是必须的, 建议调用。传入_init_param_map的是用于 - # “键映射"的键值对。假设初始化__init__(pred=None, target=None, threshold=0.1)中threshold是用于控制loss计算的,则 - # 不要将threshold传入_init_param_map. - self._init_param_map(pred=pred, target=target) - - def get_loss(self, pred, target): - # 这里'pred', 'target'必须和初始化的映射是一致的。 - return F.l1_loss(input=pred, target=target) #直接返回一个loss即可 -``` - -### Metric: 根据Model.forward()或者Model.predict()的结果计算metric -metric的设计和loss的设计类似。都是传入pred_dict与target_dict进行计算。但是metric的pred_dict来源可能是Model.forward的返回值, 也可能是Model.predict(如果Model具有predict方法则会调用predict方法)的返回值,下面统一用pred_dict代替。 -1. 这里的"键映射"与loss的"键映射"是类似的。举例来说,若Metric(pred='output', target='label'),则使用'output'到pred_dict和target_dict中寻找pred, 用'label'寻找target。 -2. 如何创建一个自己的Metric方法 -Metric与loss的计算不同在于,Metric的计算有两个步骤。 - - **每个batch的输出**都会调用Metric的``__call__(pred_dict, target_dict)``方法,而``__call__``方法会调用evaluate()(需要实现)方法。 - - 在所有batch传入之后,调用Metric的get_metric()方法得到最终的metric值。 - - 所以Metric在调用evaluate方法时,根据拿到的数据: pred_dict与batch_y, 改变自己的状态(比如累加正确的次数,总的sample数等)。在调用get_metric()的时候给出一个最终计算结果。 - 所有的Metric必须继承自fastNLP.core.metrics.MetricBase. 例子见下一个cell -3. 尽量不要复写``__call__()``,``_init_param_map()``方法。 - -```python -class MetricBase: - def __init__(self): - self.param_map = {} # 一般情况下也不需要自己创建。调用_init_param_map()更好 - self._checked = False # 这个参数可以忽略 - - def _init_param_map(self, key_map=None, **kwargs): - # 这个函数是用于注册Metric的“键映射”,有两种传值方法, - # 第一种是通过key_map传入dict,取值是用value到forward和batch_y取 - # key_map = {'pred': 'output', 'target': 'label'} - # 第二种是自己写(建议使用改种方式) - # _init_param_map(pred='output', target='label') - # 为什么会提供这么一个方法?通过调用这个方法会自动注册param_map,并会做一些检查,防止出现传入的key其实并不是evaluate() - # 的一个参数。注意传入这个方法的参数必须都是需要做键映射的内容,其它evaluate参数不要传入。如果传入(pred=None, target=None) - # 则__call__()会到pred_dict与target_dict去寻找key为'pred'和'target'的值。 - # 但这个参数不是必须要调用的。 - pass - - def __call__(self, pred_dict, target_dict, check=False): # check=False忽略这个参数,之后应该会被删除的 - # 这个函数主要会做一些check的工作,比如pred_dict与target_dict中是否包含了计算evaluate所必须的key等。检查通过,则调用 - # evaluate方法。 - fast_param = self._fast_param_map(predict_dict, target_dict): - if fast_param: - return self.evaluate(**fast_param) - # 如果没有fast_param则通过匹配参数然后调用get_loss完成 - # xxxx - - def _fast_param_map(self, pred_dict, target_dict): - # 这是一种快速计算loss的机制,因为在很多情况下其实都不需要通过"键映射",比如evaluate时,pred_dict只有一个元素, - # target_dict也只有一个元素,那么无歧义地就可以把预测值与实际值用于计算metric, 基类判断了这种情况(可能还有其它无歧义的 - # 情况)。即_fast_param_map成功的话,就不需要使用键映射,这样即使在没有传递或者传递错误"键映射"的情况也可以直接计算metric。 - # 返回值是一个dict, 如果匹配成功,应该返回类似{'pred':value, 'target': value}的结果;如果dict为空则说明匹配失败, - # __call__方法会继续尝试匹配。 - pass - - def evaluate(self, *args, **kwargs): - # 这个是一定需要实现的,累加metric状态 - # (1) evaluate()中一定不能包含*arg这种参数形式。 - # (2) 如果包含**kwargs这种参数,这会将pred_dict与target_dict中所有参数传入。但是建议不要用这个参数 - raise NotImplementedError - - def get_metric(self, reset=True): - # 这是一定需要实现的,获取最终的metric。返回值必须是一个dict。会在所有batch传入之后调用 - raise NotImplementedError - -# 下面使用AccuracyMetric举例 -class AccuracyMetric(MetricBase): # MetricBase - # 初始化需要映射的值,这里需要映射的值'pred', 'target'必须与evaluate()需要参数名是对应的 - def __init__(self, pred=None, target=None): - super(AccuracyMetric, self).__init__() - # 这里传入_init_param_map以使得pred和target被正确注册,但这一步不是必须的, 建议调用。传入_init_param_map的是用于 - # “键映射"的键值对。假设初始化__init__(pred=None, target=None, threshold=0.1)中threshold是用于控制loss计算的,则 - # 不要将threshold传入_init_param_map. - self._init_param_map(pred=pred, target=target) - - self.total = 0 # 用于累加一共有多少sample - self.corr = 0 # 用于累加一共有多少正确的sample - - def evaluate(self, pred, target): - # 对pred和target做一些基本的判断或者预处理等 - if pred.size()==target.size() and len(pred.size())=1: #如果pred已经做了argmax - pass - elif len(pred.size())==2 and len(target.size())==1: # pred还没有进行argmax - pred = pred.argmax(dim=1) - else: - raise ValueError("The shape of pred and target should be ((B, n_classes), (B, )) or (" - "(B,),(B,)).") - assert pred.size(0)==target.size(0), "Mismatch batch size." - # 进行相应的累加 - self.total += pred.size(0) - self.corr += torch.sum(torch.eq(pred, target).float()).item() - - def get_metric(self, reset=True): - # reset用于指示是否清空累加信息。默认为True - # 这个函数需要返回dict,可以包含多个metric。 - metric = {} - metric['acc'] = self.corr/self.total - if reset: - self.total = 0 - self.corr = 0 - return metric -``` - -#### Tester: 用于做evaluation,应该不需要更改 -重要的初始化参数有data, model, metric;比较重要的function是test()。 - -test中的运行过程 -``` -predict_func = 如果有model.predict则为model.predict, 否则是model.forward -for batch_x, batch_y in batch: -# (1) 同步数据与model -# (2) 根据predict_func的参数从batch_x中取出数据传入到predict_func中,得到结果pred_dict -# (3) 调用metric(pred_dict, batch_y -# (4) 当所有batch都运行完毕,会调用metric的get_metric方法,并且以返回的值作为evaluation的结果 -metric.get_metric() -``` - -#### Trainer: 对训练过程的封装。 -里面比较重要的function是train() -train()中的运行过程 -``` -(1) 创建batch - batch = Batch(dataset, batch_size, sampler=sampler) - for batch_x, batch_y in batch: - # ... - batch_x,batch_y都是dict。batch_x是DataSet中被设置为input的field;batch_y是DataSet中被设置为target的field。 - 两个dict中的key就是DataSet中的key,value会根据情况做好padding的tensor。 -(2)会将batch_x, batch_y中tensor移动到model所在的device -(3)根据model.forward的参数列表, 从batch_x中取出需要传递给forward的数据。 -(4)获取model.forward的输出结果pred_dict,并与batch_y一起传递给loss函数, 求得loss -(5)对loss进行反向梯度并更新参数 -(6) 如果有验证集,则需要做验证 - tester = Tester(model, dev_data,metric) - eval_results = tester.test() -(7) 如果eval_results是当前的最佳结果,则保存模型。 -``` - -#### 其他 -Trainer中还提供了"预跑"的功能。该功能通过check_code_level管理,如果check_code_level为-1,则不进行"预跑"。 - -check_code_level=0,1,2代表不同的提醒级别。 -目前不同提醒级别对应的是对DataSet中设置为input或target但又没有使用的field的提醒级别。 -0是忽略(默认);1是会warning发生了未使用field的情况;2是出现了unused会直接报错并退出运行 - -"预跑"的主要目的有两个: -- 防止train完了之后进行evaluation的时候出现错误。之前的train就白费了 -- 由于存在"键映射",直接运行导致的报错可能不太容易debug,通过"预跑"过程的报错会有一些debug提示 - -"预跑"会进行以下的操作: -- 使用很小的batch_size, 检查batch_x中是否包含Model.forward所需要的参数。只会运行两个循环。 -- 将Model.foward的输出pred_dict与batch_y输入到loss中, 并尝试backward. 不会更新参数,而且grad会被清零 - 如果传入了dev_data,还将进行metric的测试 -- 创建Tester,并传入少量数据,检测是否可以正常运行 - -"预跑"操作是在Trainer初始化的时候执行的。 - -正常情况下,应该不需要改动"预跑"的代码。但如果你遇到bug或者有什么好的建议,欢迎在开发群或者github提交issue。 - - From 0448e450068a973006c68bed3569cfb12d9e389e Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 21 May 2019 21:25:23 +0800 Subject: [PATCH 145/173] update documents on bert --- fastNLP/modules/encoder/bert.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/fastNLP/modules/encoder/bert.py b/fastNLP/modules/encoder/bert.py index e87f6f5d..db060a60 100644 --- a/fastNLP/modules/encoder/bert.py +++ b/fastNLP/modules/encoder/bert.py @@ -224,9 +224,9 @@ class BertPooler(nn.Module): class BertModel(nn.Module): - """Bidirectional Embedding Representations from Transformers. + """BERT(Bidirectional Embedding Representations from Transformers). - If you want to use pre-trained weights, please download from the following sources provided by pytorch-pretrained-BERT. + 如果你想使用预训练好的权重矩阵,请在以下网址下载. sources:: 'bert-base-uncased': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-uncased.tar.gz", @@ -238,13 +238,28 @@ class BertModel(nn.Module): 'bert-base-chinese': "https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese.tar.gz", - Construct a BERT model with pre-trained weights:: + 用预训练权重矩阵来建立BERT模型:: model = BertModel.from_pretrained("path/to/weights/directory") + 用随机初始化权重矩阵来建立BERT模型:: + + model = BertModel() + + :param int vocab_size: 词表大小 + :param int hidden_size: 隐层大小 + :param int num_hidden_layers: 隐藏层数 + :param int num_attention_heads: 多头注意力头数 + :param int intermediate_size: FFN隐藏层大小 + :param str hidden_act: FFN隐藏层激活函数 + :param float hidden_dropout_prob: FFN隐藏层dropout + :param float attention_probs_dropout_prob: Attention层的dropout + :param int max_position_embeddings: 最大的序列长度 + :param int type_vocab_size: 最大segment数量 + :param int initializer_range: 初始化权重范围 """ - def __init__(self, vocab_size, + def __init__(self, vocab_size=30522, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, From d362bc147d7c4c36c57ece8523691584fc80efa8 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 21 May 2019 21:33:31 +0800 Subject: [PATCH 146/173] update documents on encoder/bert --- fastNLP/modules/encoder/bert.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fastNLP/modules/encoder/bert.py b/fastNLP/modules/encoder/bert.py index db060a60..e123fda6 100644 --- a/fastNLP/modules/encoder/bert.py +++ b/fastNLP/modules/encoder/bert.py @@ -246,17 +246,17 @@ class BertModel(nn.Module): model = BertModel() - :param int vocab_size: 词表大小 - :param int hidden_size: 隐层大小 - :param int num_hidden_layers: 隐藏层数 - :param int num_attention_heads: 多头注意力头数 - :param int intermediate_size: FFN隐藏层大小 - :param str hidden_act: FFN隐藏层激活函数 - :param float hidden_dropout_prob: FFN隐藏层dropout - :param float attention_probs_dropout_prob: Attention层的dropout - :param int max_position_embeddings: 最大的序列长度 - :param int type_vocab_size: 最大segment数量 - :param int initializer_range: 初始化权重范围 + :param int vocab_size: 词表大小,默认值为30522,为BERT English uncase版本的词表大小 + :param int hidden_size: 隐层大小,默认值为768,为BERT base的版本 + :param int num_hidden_layers: 隐藏层数,默认值为12,为BERT base的版本 + :param int num_attention_heads: 多头注意力头数,默认值为12,为BERT base的版本 + :param int intermediate_size: FFN隐藏层大小,默认值是3072,为BERT base的版本 + :param str hidden_act: FFN隐藏层激活函数,默认值为``gelu`` + :param float hidden_dropout_prob: FFN隐藏层dropout,默认值为0.1 + :param float attention_probs_dropout_prob: Attention层的dropout,默认值为0.1 + :param int max_position_embeddings: 最大的序列长度,默认值为512, + :param int type_vocab_size: 最大segment数量,默认值为2 + :param int initializer_range: 初始化权重范围,默认值为0.02 """ def __init__(self, vocab_size=30522, From 6ad85a823b086a1c9229957875759c715a41203c Mon Sep 17 00:00:00 2001 From: yh_cc Date: Tue, 21 May 2019 22:46:50 +0800 Subject: [PATCH 147/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AAfit?= =?UTF-8?q?log=20callback=EF=BC=8C=E5=AE=9E=E7=8E=B0=E4=B8=8Efitlog?= =?UTF-8?q?=E5=AE=9E=E9=AA=8C=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 94 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 2 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index ed1b0697..cd97b64b 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -29,7 +29,7 @@ callback模块实现了 fastNLP 中的许多 callback 类,用于增强 :class: callback.on_valid_end() # 可以进行在其它数据集上进行验证 callback.on_epoch_end() # epoch结束调用 callback.on_train_end() # 训练结束 - callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里 + callback.on_exception() # 这是一个特殊的步骤,在训练过程中遭遇exception会跳转到这里。 如下面的例子所示,我们可以使用内置的 callback 类,或者继承 :class:`~fastNLP.core.callback.Callback` 定义自己的 callback 类:: @@ -64,7 +64,7 @@ __all__ = [ import os import torch - +from copy import deepcopy try: from tensorboardX import SummaryWriter @@ -73,7 +73,13 @@ except: tensorboardX_flag = False from ..io.model_io import ModelSaver, ModelLoader +from .dataset import DataSet +from .tester import Tester +try: + import fitlog +except: + pass class Callback(object): """ @@ -425,6 +431,90 @@ class EarlyStopCallback(Callback): else: raise exception # 抛出陌生Error +class FitlogCallback(Callback): + """ + 别名: :class:`fastNLP.FitlogCallback` :class:`fastNLP.core.callback.FitlogCallback` + + 该callback将loss和progress自动写入到fitlog中; 如果Trainer有dev的数据,将自动把dev的结果写入到log中; 同时还支持传入 + 一个(或多个)test数据集进行测试(只有在trainer具有dev时才能使用),每次在dev上evaluate之后会在这些数据集上验证一下。 + 并将验证结果写入到fitlog中。这些数据集的结果是根据dev上最好的结果报道的,即如果dev在第3个epoch取得了最佳,则 + fitlog中记录的关于这些数据集的结果就是来自第三个epoch的结果。 + + :param DataSet,dict(DataSet) data: 传入DataSet对象,会使用多个Trainer中的metric对数据进行验证。如果需要传入多个 + DataSet请通过dict的方式传入,dict的key将作为对应dataset的name传递给fitlog。若tester不为None时,data需要通过 + dict的方式传入。如果仅传入DataSet, 则被命名为test + :param Tester tester: Tester对象,将在on_valid_end时调用。tester中的会被命名为test + :param int verbose: 是否在终端打印内容,0不打印 + :param bool log_exception: fitlog是否记录发生的exception信息 + """ + + def __init__(self, data=None, tester=None, verbose=0, log_exception=False): + super().__init__() + self.datasets = {} + self.testers = {} + self._log_exception = log_exception + if tester is not None: + assert isinstance(tester, Tester), "Only fastNLP.Tester allowed." + assert isinstance(data, dict) or data is None, "If tester is not None, only dict[DataSet] allowed for data." + if data is not None: + assert 'test' not in data, "Cannot use `test` as DataSet key, when tester is passed." + setattr(tester, 'verbose', 0) + self.testers['test'] = tester + + if isinstance(data, dict): + for key, value in data.items(): + assert isinstance(value, DataSet), f"Only DataSet object is allowed, not {type(value)}." + for key, value in data.items(): + self.datasets[key] = value + elif isinstance(data, DataSet): + self.datasets['test'] = data + else: + raise TypeError("data receives dict[DataSet] or DataSet object.") + + self.verbose = verbose + + def on_train_begin(self): + if (len(self.datasets)>0 or len(self.testers)>0 ) and self.trainer.dev_data is None: + raise RuntimeError("Trainer has no dev data, you cannot pass extra data to do evaluation.") + + if len(self.datasets)>0: + for key, data in self.datasets.items(): + tester = Tester(data=data, model=self.model, batch_size=self.batch_size, metrics=self.trainer.metrics, + verbose=0) + self.testers[key] = tester + fitlog.add_progress(total_steps=self.n_steps) + + def on_backward_begin(self, loss): + fitlog.add_loss(loss.item(), name='loss', step=self.step, epoch=self.epoch) + + def on_valid_end(self, eval_result, metric_key, optimizer, better_result): + if better_result: + eval_result = deepcopy(eval_result) + eval_result['step'] = self.step + eval_result['epoch'] = self.epoch + fitlog.add_best_metric(eval_result) + fitlog.add_metric(eval_result, step=self.step, epoch=self.epoch) + if len(self.testers)>0: + for key, tester in self.testers.items(): + try: + eval_result = tester.test() + if self.verbose!=0: + self.pbar.write("Evaluation on DataSet {}:".format(key)) + self.pbar.write(tester._format_eval_results(eval_result)) + fitlog.add_metric(eval_result, name=key, step=self.step, epoch=self.epoch) + if better_result: + fitlog.add_best_metric(eval_result, name=key) + except Exception: + self.pbar.write("Exception happens when evaluate on DataSet named `{}`.".format(key)) + + def on_train_end(self): + fitlog.finish() + + def on_exception(self, exception): + fitlog.finish(status=1) + if self._log_exception: + fitlog.add_other(str(exception), name='except_info') + class LRScheduler(Callback): """ From b84e1df7d84805f94e4e28107184b424adf23fc9 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Tue, 21 May 2019 23:27:06 +0800 Subject: [PATCH 148/173] typo --- fastNLP/core/callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index cd97b64b..7fad2d0b 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -443,7 +443,7 @@ class FitlogCallback(Callback): :param DataSet,dict(DataSet) data: 传入DataSet对象,会使用多个Trainer中的metric对数据进行验证。如果需要传入多个 DataSet请通过dict的方式传入,dict的key将作为对应dataset的name传递给fitlog。若tester不为None时,data需要通过 dict的方式传入。如果仅传入DataSet, 则被命名为test - :param Tester tester: Tester对象,将在on_valid_end时调用。tester中的会被命名为test + :param Tester tester: Tester对象,将在on_valid_end时调用。tester中的DataSet会被称为为`test` :param int verbose: 是否在终端打印内容,0不打印 :param bool log_exception: fitlog是否记录发生的exception信息 """ From 6862a8f16979565d2aa915aec0a0974cdae2350d Mon Sep 17 00:00:00 2001 From: yunfan Date: Wed, 22 May 2019 11:19:14 +0800 Subject: [PATCH 149/173] - update dataset_loader --- fastNLP/io/dataset_loader.py | 140 +++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 57 deletions(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 8273d2f8..19600ef7 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -26,6 +26,8 @@ from nltk.tree import Tree from ..core.dataset import DataSet from ..core.instance import Instance from .file_reader import _read_csv, _read_json, _read_conll +from typing import Union +import os def _download_from_url(url, path): @@ -34,7 +36,7 @@ def _download_from_url(url, path): except: from ..core.utils import _pseudo_tqdm as tqdm import requests - + """Download file""" r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) chunk_size = 16 * 1024 @@ -49,12 +51,15 @@ def _download_from_url(url, path): def _uncompress(src, dst): - import zipfile, gzip, tarfile, os - + import zipfile + import gzip + import tarfile + import os + def unzip(src, dst): with zipfile.ZipFile(src, 'r') as f: f.extractall(dst) - + def ungz(src, dst): with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: length = 16 * 1024 # 16KB @@ -62,11 +67,11 @@ def _uncompress(src, dst): while buf: uf.write(buf) buf = f.read(length) - + def untar(src, dst): with tarfile.open(src, 'r:gz') as f: f.extractall(dst) - + fn, ext = os.path.splitext(src) _, ext_2 = os.path.splitext(fn) if ext == '.zip': @@ -79,27 +84,52 @@ def _uncompress(src, dst): raise ValueError('unsupported file {}'.format(src)) +class DataInfo(): + def __init__(self, vocabs: dict = None, embeddings: dict = None, datasets: dict = None): + self.vocabs = vocabs or {} + self.embeddings = embeddings or {} + self.datasets = datasets or {} + + class DataSetLoader: """ 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` 所有 DataSetLoader 的 API 接口,你可以继承它实现自己的 DataSetLoader """ - - def load(self, path): + def _download(self, url: str, path: str, uncompress=True) -> str: + """从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 + 返回数据的路径。 + """ + pdir = os.path.dirname(path) + os.makedirs(pdir, exist_ok=True) + _download_from_url(url, path) + if uncompress: + dst = os.path.join(pdir, 'data') + _uncompress(path, dst) + return dst + return path + + def load(self, paths: Union[str, dict]) -> Union[DataSet, dict]: + """从指定一个或多个 ``paths`` 的文件中读取数据,返回DataSet + + :param str or dict paths: 文件路径 + :return: 一个存储 :class:`~fastNLP.DataSet` 的字典 + """ + if isinstance(paths, str): + return self._load(paths) + return {name: self._load(path) for name, path in paths.items()} + + def _load(self, path: str) -> DataSet: """从指定 ``path`` 的文件中读取数据,返回DataSet :param str path: 文件路径 :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ raise NotImplementedError - - def convert(self, data): - """ - 用Python数据对象创建DataSet,各个子类需要自行实现这个方法 - :param data: Python 内置的数据结构 - :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 + def process(self, paths: Union[str, dict], **options) -> Union[DataInfo, dict]: + """读取并处理数据,返回处理结果 """ raise NotImplementedError @@ -110,21 +140,13 @@ class PeopleDailyCorpusLoader(DataSetLoader): 读取人民日报数据集 """ - - def __init__(self): + + def __init__(self, pos=True, ner=True): super(PeopleDailyCorpusLoader, self).__init__() - self.pos = True - self.ner = True - - def load(self, data_path, pos=True, ner=True): - """ + self.pos = pos + self.ner = ner - :param str data_path: 数据路径 - :param bool pos: 是否使用词性标签 - :param bool ner: 是否使用命名实体标签 - :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 - """ - self.pos, self.ner = pos, ner + def _load(self, data_path): with open(data_path, "r", encoding="utf-8") as f: sents = f.readlines() examples = [] @@ -168,10 +190,10 @@ class PeopleDailyCorpusLoader(DataSetLoader): example.append(sent_ner) examples.append(example) return self.convert(examples) - + def convert(self, data): """ - + :param data: python 内置对象 :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ @@ -179,7 +201,8 @@ class PeopleDailyCorpusLoader(DataSetLoader): for item in data: sent_words = item[0] if self.pos is True and self.ner is True: - instance = Instance(words=sent_words, pos_tags=item[1], ner=item[2]) + instance = Instance( + words=sent_words, pos_tags=item[1], ner=item[2]) elif self.pos is True: instance = Instance(words=sent_words, pos_tags=item[1]) elif self.ner is True: @@ -218,11 +241,12 @@ class ConllLoader(DataSetLoader): :param indexes: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ - + def __init__(self, headers, indexes=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): - raise TypeError('invalid headers: {}, should be list of strings'.format(headers)) + raise TypeError( + 'invalid headers: {}, should be list of strings'.format(headers)) self.headers = headers self.dropna = dropna if indexes is None: @@ -231,8 +255,8 @@ class ConllLoader(DataSetLoader): if len(indexes) != len(headers): raise ValueError self.indexes = indexes - - def load(self, path): + + def _load(self, path): ds = DataSet() for idx, data in _read_conll(path, indexes=self.indexes, dropna=self.dropna): ins = {h: data[i] for i, h in enumerate(self.headers)} @@ -245,11 +269,11 @@ class Conll2003Loader(ConllLoader): 别名::class:`fastNLP.io.Conll2003Loader` :class:`fastNLP.io.dataset_loader.Conll2003Loader` 读取Conll2003数据 - + 关于数据集的更多信息,参考: https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ - + def __init__(self): headers = [ 'tokens', 'pos', 'chunks', 'ner', @@ -290,7 +314,7 @@ def _cut_long_sentence(sent, max_sample_length=200): class SSTLoader(DataSetLoader): """ 别名::class:`fastNLP.io.SSTLoader` :class:`fastNLP.io.dataset_loader.SSTLoader` - + 读取SST数据集, DataSet包含fields:: words: list(str) 需要分类的文本 @@ -301,18 +325,18 @@ class SSTLoader(DataSetLoader): :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` """ - + def __init__(self, subtree=False, fine_grained=False): self.subtree = subtree - + tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', '3': 'positive', '4': 'very positive'} if not fine_grained: tag_v['0'] = tag_v['1'] tag_v['4'] = tag_v['3'] self.tag_v = tag_v - - def load(self, path): + + def _load(self, path): """ :param str path: 存储数据的路径 @@ -328,7 +352,7 @@ class SSTLoader(DataSetLoader): for words, tag in datas: ds.append(Instance(words=words, target=tag)) return ds - + @staticmethod def _get_one(data, subtree): tree = Tree.fromstring(data) @@ -350,7 +374,7 @@ class JsonLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() self.dropna = dropna @@ -361,8 +385,8 @@ class JsonLoader(DataSetLoader): for k, v in fields.items(): self.fields[k] = k if v is None else v self.fields_list = list(self.fields.keys()) - - def load(self, path): + + def _load(self, path): ds = DataSet() for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): if self.fields: @@ -385,7 +409,7 @@ class SNLILoader(JsonLoader): 数据来源: https://nlp.stanford.edu/projects/snli/snli_1.0.zip """ - + def __init__(self): fields = { 'sentence1_parse': 'words1', @@ -393,16 +417,18 @@ class SNLILoader(JsonLoader): 'gold_label': 'target', } super(SNLILoader, self).__init__(fields=fields) - - def load(self, path): - ds = super(SNLILoader, self).load(path) - + + def _load(self, path): + ds = super(SNLILoader, self)._load(path) + def parse_tree(x): t = Tree.fromstring(x) return t.leaves() - - ds.apply(lambda ins: parse_tree(ins['words1']), new_field_name='words1') - ds.apply(lambda ins: parse_tree(ins['words2']), new_field_name='words2') + + ds.apply(lambda ins: parse_tree( + ins['words1']), new_field_name='words1') + ds.apply(lambda ins: parse_tree( + ins['words2']), new_field_name='words2') ds.drop(lambda x: x['target'] == '-') return ds @@ -419,13 +445,13 @@ class CSVLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, headers=None, sep=",", dropna=False): self.headers = headers self.sep = sep self.dropna = dropna - - def load(self, path): + + def _load(self, path): ds = DataSet() for idx, data in _read_csv(path, headers=self.headers, sep=self.sep, dropna=self.dropna): @@ -439,7 +465,7 @@ def _add_seg_tag(data): :param data: list of ([word], [pos], [heads], [head_tags]) :return: list of ([word], [pos]) """ - + _processed = [] for word_list, pos_list, _, _ in data: new_sample = [] From 9dba869a3632ee13b1a545719aef2412ae9e9e11 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 14:09:17 +0800 Subject: [PATCH 150/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=88=B0=20?= =?UTF-8?q?fitlog=20=E6=96=87=E6=A1=A3=E7=9A=84=E9=93=BE=E6=8E=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/index.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 554b1afc..219e32f9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -55,8 +55,6 @@ fastNLP 在 :mod:`~fastNLP.models` 模块中内置了如 :class:`~fastNLP.models 安装指南 快速入门 详细指南 - 科研向导 - API 文档 ------------- @@ -65,9 +63,15 @@ API 文档 .. toctree:: :titlesonly: + :maxdepth: 2 fastNLP +fitlog +------ + +用户可以 `点此 `_ 查看fitlog的文档。 +fitlog 是由我们团队开发,用于帮助用户记录日志并管理代码的工具 索引与搜索 ================== From 6a8f50e73e81a5175eb5bd9d3d24ba615ce7d901 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 15:06:10 +0800 Subject: [PATCH 151/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=20DataSet?= =?UTF-8?q?=20Loader=20=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/io/dataset_loader.py | 123 +++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 40 deletions(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 19600ef7..32cca88f 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -1,6 +1,6 @@ """ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的数据, 并返回 `DataSet` , -得到的 :class:`~fastNLP.DataSet` 对象可以直接传入 :class:`~fastNLP.Trainer`, :class:`~fastNLP.Tester`, 用于模型的训练和测试。 +得到的 :class:`~fastNLP.DataSet` 对象可以直接传入 :class:`~fastNLP.Trainer` 和 :class:`~fastNLP.Tester`, 用于模型的训练和测试。 以SNLI数据集为例:: loader = SNLILoader() @@ -9,8 +9,11 @@ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的 test_ds = loader.load('path/to/test') # ... do stuff + +为 fastNLP 提供 DataSetLoader 的开发者请参考 :class:`~fastNLP.io.DataSetLoader` 的介绍。 """ __all__ = [ + 'DataInfo', 'DataSetLoader', 'CSVLoader', 'JsonLoader', @@ -26,7 +29,7 @@ from nltk.tree import Tree from ..core.dataset import DataSet from ..core.instance import Instance from .file_reader import _read_csv, _read_json, _read_conll -from typing import Union +from typing import Union, Dict import os @@ -36,7 +39,7 @@ def _download_from_url(url, path): except: from ..core.utils import _pseudo_tqdm as tqdm import requests - + """Download file""" r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) chunk_size = 16 * 1024 @@ -55,11 +58,11 @@ def _uncompress(src, dst): import gzip import tarfile import os - + def unzip(src, dst): with zipfile.ZipFile(src, 'r') as f: f.extractall(dst) - + def ungz(src, dst): with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: length = 16 * 1024 # 16KB @@ -67,11 +70,11 @@ def _uncompress(src, dst): while buf: uf.write(buf) buf = f.read(length) - + def untar(src, dst): with tarfile.open(src, 'r:gz') as f: f.extractall(dst) - + fn, ext = os.path.splitext(src) _, ext_2 = os.path.splitext(fn) if ext == '.zip': @@ -84,7 +87,15 @@ def _uncompress(src, dst): raise ValueError('unsupported file {}'.format(src)) -class DataInfo(): +class DataInfo: + """ + 经过处理的数据信息,包括一系列数据集(比如:分开的训练集、验证集和测试集)及它们所用的词表和词嵌入。 + + :param vocabs: 从名称(字符串)到 :class:`~fastNLP.Vocabulary` 类型的dict + :param embeddings: 从名称(字符串)到一系列 embedding 的dict,参考 :class:`~fastNLP.io.EmbedLoader` + :param datasets: 从名称(字符串)到 :class:`~fastNLP.DataSet` 类型的dict + """ + def __init__(self, vocabs: dict = None, embeddings: dict = None, datasets: dict = None): self.vocabs = vocabs or {} self.embeddings = embeddings or {} @@ -95,11 +106,27 @@ class DataSetLoader: """ 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` - 所有 DataSetLoader 的 API 接口,你可以继承它实现自己的 DataSetLoader + 定义了各种 DataSetLoader 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 + + 开发者至少应该编写如下内容: + + - _load 函数:从一个数据文件中读取数据到一个 :class:`~fastNLP.DataSet` + - load 函数(可以使用基类的方法):从一个或多个数据文件中读取数据到一个或多个 :class:`~fastNLP.DataSet` + - process 函数:一个或多个从数据文件中读取数据,并处理成可以训练的一个或多个 :class:`~fastNLP.DataSet` + + **process 函数中可以 调用load 函数或 _load 函数** + """ + def _download(self, url: str, path: str, uncompress=True) -> str: - """从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 - 返回数据的路径。 + """ + + 从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 + + :param url: 下载的网站 + :param path: 下载到的目录 + :param uncompress: 是否自动解压缩 + :return: 数据的存放路径 """ pdir = os.path.dirname(path) os.makedirs(pdir, exist_ok=True) @@ -109,27 +136,43 @@ class DataSetLoader: _uncompress(path, dst) return dst return path + + def load(self, paths: Union[str, Dict[str, str]]) -> Union[DataSet, Dict[str, DataSet]]: + """ + 从指定一个或多个路径中的文件中读取数据,返回一个或多个数据集 :class:`~fastNLP.DataSet` 。 + 如果处理多个路径,传入的 dict 中的 key 与返回的 dict 中的 key 保存一致。 - def load(self, paths: Union[str, dict]) -> Union[DataSet, dict]: - """从指定一个或多个 ``paths`` 的文件中读取数据,返回DataSet - - :param str or dict paths: 文件路径 - :return: 一个存储 :class:`~fastNLP.DataSet` 的字典 + :param Union[str, Dict[str, str]] paths: 文件路径 + :return: :class:`~fastNLP.DataSet` 类的对象或存储多个 :class:`~fastNLP.DataSet` 的字典 """ if isinstance(paths, str): return self._load(paths) return {name: self._load(path) for name, path in paths.items()} - + def _load(self, path: str) -> DataSet: - """从指定 ``path`` 的文件中读取数据,返回DataSet + """从指定路径的文件中读取数据,返回 :class:`~fastNLP.DataSet` 类型的对象 :param str path: 文件路径 :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ raise NotImplementedError - - def process(self, paths: Union[str, dict], **options) -> Union[DataInfo, dict]: - """读取并处理数据,返回处理结果 + + def process(self, paths: Union[str, Dict[str, str]], **options) -> DataInfo: + """ + 对于特定的任务和数据集,读取并处理数据,返回处理DataInfo类对象或字典。 + + 从指定一个或多个路径中的文件中读取数据,DataInfo对象中可以包含一个或多个数据集 。 + 如果处理多个路径,传入的 dict 的 key 与返回DataInfo中的 dict 中的 key 保存一致。 + + 返回的 :class:`DataInfo` 对象有如下属性: + + - vocabs: 由从数据集中获取的词表组成的字典,每个词表 + - embeddings: (可选) 数据集对应的词嵌入 + - datasets: 一个dict,包含一系列 :class:`~fastNLP.DataSet` 类型的对象。其中 field 的命名参考 :mod:`~fastNLP.core.const` + + :param paths: 原始数据读取的路径 + :param options: 根据不同的任务和数据集,设计自己的参数 + :return: 返回一个 DataInfo """ raise NotImplementedError @@ -140,12 +183,12 @@ class PeopleDailyCorpusLoader(DataSetLoader): 读取人民日报数据集 """ - + def __init__(self, pos=True, ner=True): super(PeopleDailyCorpusLoader, self).__init__() self.pos = pos self.ner = ner - + def _load(self, data_path): with open(data_path, "r", encoding="utf-8") as f: sents = f.readlines() @@ -190,7 +233,7 @@ class PeopleDailyCorpusLoader(DataSetLoader): example.append(sent_ner) examples.append(example) return self.convert(examples) - + def convert(self, data): """ @@ -241,7 +284,7 @@ class ConllLoader(DataSetLoader): :param indexes: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ - + def __init__(self, headers, indexes=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): @@ -255,7 +298,7 @@ class ConllLoader(DataSetLoader): if len(indexes) != len(headers): raise ValueError self.indexes = indexes - + def _load(self, path): ds = DataSet() for idx, data in _read_conll(path, indexes=self.indexes, dropna=self.dropna): @@ -273,7 +316,7 @@ class Conll2003Loader(ConllLoader): 关于数据集的更多信息,参考: https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ - + def __init__(self): headers = [ 'tokens', 'pos', 'chunks', 'ner', @@ -325,17 +368,17 @@ class SSTLoader(DataSetLoader): :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` """ - + def __init__(self, subtree=False, fine_grained=False): self.subtree = subtree - + tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', '3': 'positive', '4': 'very positive'} if not fine_grained: tag_v['0'] = tag_v['1'] tag_v['4'] = tag_v['3'] self.tag_v = tag_v - + def _load(self, path): """ @@ -352,7 +395,7 @@ class SSTLoader(DataSetLoader): for words, tag in datas: ds.append(Instance(words=words, target=tag)) return ds - + @staticmethod def _get_one(data, subtree): tree = Tree.fromstring(data) @@ -374,7 +417,7 @@ class JsonLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() self.dropna = dropna @@ -385,7 +428,7 @@ class JsonLoader(DataSetLoader): for k, v in fields.items(): self.fields[k] = k if v is None else v self.fields_list = list(self.fields.keys()) - + def _load(self, path): ds = DataSet() for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): @@ -409,7 +452,7 @@ class SNLILoader(JsonLoader): 数据来源: https://nlp.stanford.edu/projects/snli/snli_1.0.zip """ - + def __init__(self): fields = { 'sentence1_parse': 'words1', @@ -417,14 +460,14 @@ class SNLILoader(JsonLoader): 'gold_label': 'target', } super(SNLILoader, self).__init__(fields=fields) - + def _load(self, path): ds = super(SNLILoader, self)._load(path) - + def parse_tree(x): t = Tree.fromstring(x) return t.leaves() - + ds.apply(lambda ins: parse_tree( ins['words1']), new_field_name='words1') ds.apply(lambda ins: parse_tree( @@ -445,12 +488,12 @@ class CSVLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, headers=None, sep=",", dropna=False): self.headers = headers self.sep = sep self.dropna = dropna - + def _load(self, path): ds = DataSet() for idx, data in _read_csv(path, headers=self.headers, @@ -465,7 +508,7 @@ def _add_seg_tag(data): :param data: list of ([word], [pos], [heads], [head_tags]) :return: list of ([word], [pos]) """ - + _processed = [] for word_list, pos_list, _, _ in data: new_sample = [] From 040bd2ab0774842c759b7424f355ae3a23648a64 Mon Sep 17 00:00:00 2001 From: yunfan Date: Wed, 22 May 2019 18:06:52 +0800 Subject: [PATCH 152/173] - add star-transformer reproduction --- fastNLP/models/star_transformer.py | 19 +- fastNLP/modules/encoder/star_transformer.py | 2 +- reproduction/Star_transformer/datasets.py | 157 ++++++++++++++ reproduction/Star_transformer/modules.py | 56 +++++ reproduction/Star_transformer/run.sh | 5 + reproduction/Star_transformer/train.py | 214 ++++++++++++++++++++ reproduction/Star_transformer/util.py | 112 ++++++++++ 7 files changed, 555 insertions(+), 10 deletions(-) create mode 100644 reproduction/Star_transformer/datasets.py create mode 100644 reproduction/Star_transformer/modules.py create mode 100644 reproduction/Star_transformer/run.sh create mode 100644 reproduction/Star_transformer/train.py create mode 100644 reproduction/Star_transformer/util.py diff --git a/fastNLP/models/star_transformer.py b/fastNLP/models/star_transformer.py index c67e5938..4c944a54 100644 --- a/fastNLP/models/star_transformer.py +++ b/fastNLP/models/star_transformer.py @@ -26,13 +26,11 @@ class StarTransEnc(nn.Module): :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, 此时就以传入的对象作为embedding - :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. :param num_layers: 模型层数. :param num_head: 模型中multi-head的head个数. :param head_dim: 模型中multi-head中每个head特征维度. :param max_len: 模型能接受的最大输入长度. - :param cls_hidden_size: 分类器隐层维度. :param emb_dropout: 词嵌入的dropout概率. :param dropout: 模型除词嵌入外的dropout概率. """ @@ -59,7 +57,7 @@ class StarTransEnc(nn.Module): def forward(self, x, mask): """ - :param FloatTensor data: [batch, length, hidden] 输入的序列 + :param FloatTensor x: [batch, length, hidden] 输入的序列 :param ByteTensor mask: [batch, length] 输入序列的padding mask, 在没有内容(padding 部分) 为 0, 否则为 1 :return: [batch, length, hidden] 编码后的输出序列 @@ -110,8 +108,9 @@ class STSeqLabel(nn.Module): 用于序列标注的Star-Transformer模型 - :param vocab_size: 词嵌入的词典大小 - :param emb_dim: 每个词嵌入的特征维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. Default: 300 :param num_layers: 模型层数. Default: 4 @@ -174,8 +173,9 @@ class STSeqCls(nn.Module): 用于分类任务的Star-Transformer - :param vocab_size: 词嵌入的词典大小 - :param emb_dim: 每个词嵌入的特征维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. Default: 300 :param num_layers: 模型层数. Default: 4 @@ -238,8 +238,9 @@ class STNLICls(nn.Module): 用于自然语言推断(NLI)的Star-Transformer - :param vocab_size: 词嵌入的词典大小 - :param emb_dim: 每个词嵌入的特征维度 + :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 + embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, + 此时就以传入的对象作为embedding :param num_cls: 输出类别个数 :param hidden_size: 模型中特征维度. Default: 300 :param num_layers: 模型层数. Default: 4 diff --git a/fastNLP/modules/encoder/star_transformer.py b/fastNLP/modules/encoder/star_transformer.py index 5a7f3d67..1eec7c13 100644 --- a/fastNLP/modules/encoder/star_transformer.py +++ b/fastNLP/modules/encoder/star_transformer.py @@ -43,7 +43,7 @@ class StarTransformer(nn.Module): for _ in range(self.iters)]) if max_len is not None: - self.pos_emb = self.pos_emb = nn.Embedding(max_len, hidden_size) + self.pos_emb = nn.Embedding(max_len, hidden_size) else: self.pos_emb = None diff --git a/reproduction/Star_transformer/datasets.py b/reproduction/Star_transformer/datasets.py new file mode 100644 index 00000000..a9257fd4 --- /dev/null +++ b/reproduction/Star_transformer/datasets.py @@ -0,0 +1,157 @@ +import torch +import json +import os +from fastNLP import Vocabulary +from fastNLP.io.dataset_loader import ConllLoader, SSTLoader, SNLILoader +from fastNLP.core import Const as C +import numpy as np + +MAX_LEN = 128 + +def update_v(vocab, data, field): + data.apply(lambda x: vocab.add_word_lst(x[field]), new_field_name=None) + + +def to_index(vocab, data, field, name): + def func(x): + try: + return [vocab.to_index(w) for w in x[field]] + except ValueError: + return [vocab.padding_idx for _ in x[field]] + data.apply(func, new_field_name=name) + + +def load_seqtag(path, files, indexs): + word_h, tag_h = 'words', 'tags' + loader = ConllLoader(headers=[word_h, tag_h], indexes=indexs) + ds_list = [] + for fn in files: + ds_list.append(loader.load(os.path.join(path, fn))) + word_v = Vocabulary(min_freq=2) + tag_v = Vocabulary(unknown=None) + update_v(word_v, ds_list[0], word_h) + update_v(tag_v, ds_list[0], tag_h) + + def process_data(ds): + to_index(word_v, ds, word_h, C.INPUT) + to_index(tag_v, ds, tag_h, C.TARGET) + ds.apply(lambda x: x[C.INPUT][:MAX_LEN], new_field_name=C.INPUT) + ds.apply(lambda x: x[C.TARGET][:MAX_LEN], new_field_name=C.TARGET) + ds.apply(lambda x: len(x[word_h]), new_field_name=C.INPUT_LEN) + ds.set_input(C.INPUT, C.INPUT_LEN) + ds.set_target(C.TARGET, C.INPUT_LEN) + for i in range(len(ds_list)): + process_data(ds_list[i]) + return ds_list, word_v, tag_v + + +def load_sst(path, files): + loaders = [SSTLoader(subtree=sub, fine_grained=True) + for sub in [True, False, False]] + ds_list = [loader.load(os.path.join(path, fn)) + for fn, loader in zip(files, loaders)] + word_v = Vocabulary(min_freq=2) + tag_v = Vocabulary(unknown=None, padding=None) + for ds in ds_list: + ds.apply(lambda x: [w.lower() + for w in x['words']], new_field_name='words') + ds_list[0].drop(lambda x: len(x['words']) < 3) + update_v(word_v, ds_list[0], 'words') + ds_list[0].apply(lambda x: tag_v.add_word( + x['target']), new_field_name=None) + + def process_data(ds): + to_index(word_v, ds, 'words', C.INPUT) + ds.apply(lambda x: tag_v.to_index(x['target']), new_field_name=C.TARGET) + ds.apply(lambda x: x[C.INPUT][:MAX_LEN], new_field_name=C.INPUT) + ds.apply(lambda x: len(x['words']), new_field_name=C.INPUT_LEN) + ds.set_input(C.INPUT, C.INPUT_LEN) + ds.set_target(C.TARGET) + for i in range(len(ds_list)): + process_data(ds_list[i]) + return ds_list, word_v, tag_v + + +def load_snli(path, files): + loader = SNLILoader() + ds_list = [loader.load(os.path.join(path, f)) for f in files] + word_v = Vocabulary(min_freq=2) + tag_v = Vocabulary(unknown=None, padding=None) + for ds in ds_list: + ds.apply(lambda x: [w.lower() + for w in x['words1']], new_field_name='words1') + ds.apply(lambda x: [w.lower() + for w in x['words2']], new_field_name='words2') + update_v(word_v, ds_list[0], 'words1') + update_v(word_v, ds_list[0], 'words2') + ds_list[0].apply(lambda x: tag_v.add_word( + x['target']), new_field_name=None) + + def process_data(ds): + to_index(word_v, ds, 'words1', C.INPUTS(0)) + to_index(word_v, ds, 'words2', C.INPUTS(1)) + ds.apply(lambda x: tag_v.to_index(x['target']), new_field_name=C.TARGET) + ds.apply(lambda x: x[C.INPUTS(0)][:MAX_LEN], new_field_name=C.INPUTS(0)) + ds.apply(lambda x: x[C.INPUTS(1)][:MAX_LEN], new_field_name=C.INPUTS(1)) + ds.apply(lambda x: len(x[C.INPUTS(0)]), new_field_name=C.INPUT_LENS(0)) + ds.apply(lambda x: len(x[C.INPUTS(1)]), new_field_name=C.INPUT_LENS(1)) + ds.set_input(C.INPUTS(0), C.INPUTS(1), C.INPUT_LENS(0), C.INPUT_LENS(1)) + ds.set_target(C.TARGET) + for i in range(len(ds_list)): + process_data(ds_list[i]) + return ds_list, word_v, tag_v + + +class EmbedLoader: + @staticmethod + def parse_glove_line(line): + line = line.split() + if len(line) <= 2: + raise RuntimeError( + "something goes wrong in parsing glove embedding") + return line[0], line[1:] + + @staticmethod + def str_list_2_vec(line): + return torch.Tensor(list(map(float, line))) + + @staticmethod + def fast_load_embedding(emb_dim, emb_file, vocab): + """Fast load the pre-trained embedding and combine with the given dictionary. + This loading method uses line-by-line operation. + + :param int emb_dim: the dimension of the embedding. Should be the same as pre-trained embedding. + :param str emb_file: the pre-trained embedding file path. + :param Vocabulary vocab: a mapping from word to index, can be provided by user or built from pre-trained embedding + :return embedding_matrix: numpy.ndarray + + """ + if vocab is None: + raise RuntimeError("You must provide a vocabulary.") + embedding_matrix = np.zeros( + shape=(len(vocab), emb_dim), dtype=np.float32) + hit_flags = np.zeros(shape=(len(vocab),), dtype=int) + with open(emb_file, "r", encoding="utf-8") as f: + startline = f.readline() + if len(startline.split()) > 2: + f.seek(0) + for line in f: + word, vector = EmbedLoader.parse_glove_line(line) + try: + if word in vocab: + vector = EmbedLoader.str_list_2_vec(vector) + if emb_dim != vector.size(0): + continue + embedding_matrix[vocab[word]] = vector + hit_flags[vocab[word]] = 1 + except Exception: + continue + + if np.sum(hit_flags) < len(vocab): + # some words from vocab are missing in pre-trained embedding + # we normally sample each dimension + vocab_embed = embedding_matrix[np.where(hit_flags)] + sampled_vectors = np.random.normal(vocab_embed.mean(axis=0), vocab_embed.std(axis=0), + size=(len(vocab) - np.sum(hit_flags), emb_dim)) + embedding_matrix[np.where(1 - hit_flags)] = sampled_vectors + return embedding_matrix diff --git a/reproduction/Star_transformer/modules.py b/reproduction/Star_transformer/modules.py new file mode 100644 index 00000000..61a61d25 --- /dev/null +++ b/reproduction/Star_transformer/modules.py @@ -0,0 +1,56 @@ +import torch +import torch.nn as nn +import torch.nn.functional as F +import numpy as np +from fastNLP.core.losses import LossBase + + +reduce_func = { + 'none': lambda x, mask: x*mask, + 'sum': lambda x, mask: (x*mask).sum(), + 'mean': lambda x, mask: (x*mask).sum() / mask.sum(), +} + + +class LabelSmoothCrossEntropy(nn.Module): + def __init__(self, smoothing=0.1, ignore_index=-100, reduction='mean'): + global reduce_func + super().__init__() + if smoothing < 0 or smoothing > 1: + raise ValueError('invalid smoothing value: {}'.format(smoothing)) + self.smoothing = smoothing + self.ignore_index = ignore_index + if reduction not in reduce_func: + raise ValueError('invalid reduce type: {}'.format(reduction)) + self.reduce_func = reduce_func[reduction] + + def forward(self, input, target): + input = F.log_softmax(input, dim=1) # [N, C, ...] + smooth_val = self.smoothing / input.size(1) # [N, C, ...] + target_logit = input.new_full(input.size(), fill_value=smooth_val) + target_logit.scatter_(1, target[:, None], 1 - self.smoothing) + result = -(target_logit * input).sum(1) # [N, ...] + mask = (target != self.ignore_index).float() + return self.reduce_func(result, mask) + + +class SmoothCE(LossBase): + def __init__(self, pred=None, target=None, **kwargs): + super().__init__() + self.loss_fn = LabelSmoothCrossEntropy(**kwargs) + self._init_param_map(pred=pred, target=target) + + def get_loss(self, pred, target): + return self.loss_fn(pred, target) + + +if __name__ == '__main__': + loss_fn = nn.CrossEntropyLoss(ignore_index=0) + sm_loss_fn = LabelSmoothCrossEntropy(smoothing=0, ignore_index=0) + predict = torch.tensor([[0, 0.2, 0.7, 0.1, 0], + [0, 0.9, 0.2, 0.1, 0], + [1, 0.2, 0.7, 0.1, 0]]) + target = torch.tensor([2, 1, 0]) + loss = loss_fn(predict, target) + sm_loss = sm_loss_fn(predict, target) + print(loss, sm_loss) diff --git a/reproduction/Star_transformer/run.sh b/reproduction/Star_transformer/run.sh new file mode 100644 index 00000000..0972c662 --- /dev/null +++ b/reproduction/Star_transformer/run.sh @@ -0,0 +1,5 @@ +#python -u train.py --task pos --ds conll --mode train --gpu 1 --lr 3e-4 --w_decay 2e-5 --lr_decay .95 --drop 0.3 --ep 25 --bsz 64 > conll_pos102.log 2>&1 & +#python -u train.py --task pos --ds ctb --mode train --gpu 1 --lr 3e-4 --w_decay 2e-5 --lr_decay .95 --drop 0.3 --ep 25 --bsz 64 > ctb_pos101.log 2>&1 & +#python -u train.py --task cls --ds sst --mode train --gpu 2 --lr 1e-4 --w_decay 1e-5 --lr_decay 0.9 --drop 0.5 --ep 50 --bsz 128 > sst_cls201.log & +#python -u train.py --task nli --ds snli --mode train --gpu 1 --lr 1e-4 --w_decay 1e-5 --lr_decay 0.9 --drop 0.4 --ep 120 --bsz 128 > snli_nli201.log & +python -u train.py --task ner --ds conll --mode train --gpu 0 --lr 1e-4 --w_decay 1e-5 --lr_decay 0.9 --drop 0.4 --ep 120 --bsz 64 > conll_ner201.log & diff --git a/reproduction/Star_transformer/train.py b/reproduction/Star_transformer/train.py new file mode 100644 index 00000000..dee85c38 --- /dev/null +++ b/reproduction/Star_transformer/train.py @@ -0,0 +1,214 @@ +from util import get_argparser, set_gpu, set_rng_seeds, add_model_args +from datasets import load_seqtag, load_sst, load_snli, EmbedLoader, MAX_LEN +import torch.nn as nn +import torch +import numpy as np +import fastNLP as FN +from fastNLP.models.star_transformer import STSeqLabel, STSeqCls, STNLICls +from fastNLP.core.const import Const as C +import sys +sys.path.append('/remote-home/yfshao/workdir/dev_fastnlp/') + + +g_model_select = { + 'pos': STSeqLabel, + 'ner': STSeqLabel, + 'cls': STSeqCls, + 'nli': STNLICls, +} + +g_emb_file_path = {'en': '/remote-home/yfshao/workdir/datasets/word_vector/glove.840B.300d.txt', + 'zh': '/remote-home/yfshao/workdir/datasets/word_vector/cc.zh.300.vec'} + +g_args = None +g_model_cfg = None + + +def get_ptb_pos(): + pos_dir = '/remote-home/yfshao/workdir/datasets/pos' + pos_files = ['train.pos', 'dev.pos', 'test.pos', ] + return load_seqtag(pos_dir, pos_files, [0, 1]) + + +def get_ctb_pos(): + ctb_dir = '/remote-home/yfshao/workdir/datasets/ctb9_hy' + files = ['train.conllx', 'dev.conllx', 'test.conllx'] + return load_seqtag(ctb_dir, files, [1, 4]) + + +def get_conll2012_pos(): + path = '/remote-home/yfshao/workdir/datasets/ontonotes/pos' + files = ['ontonotes-conll.train', + 'ontonotes-conll.dev', + 'ontonotes-conll.conll-2012-test'] + return load_seqtag(path, files, [0, 1]) + + +def get_conll2012_ner(): + path = '/remote-home/yfshao/workdir/datasets/ontonotes/ner' + files = ['bieso-ontonotes-conll-ner.train', + 'bieso-ontonotes-conll-ner.dev', + 'bieso-ontonotes-conll-ner.conll-2012-test'] + return load_seqtag(path, files, [0, 1]) + + +def get_sst(): + path = '/remote-home/yfshao/workdir/datasets/SST' + files = ['train.txt', 'dev.txt', 'test.txt'] + return load_sst(path, files) + + +def get_snli(): + path = '/remote-home/yfshao/workdir/datasets/nli-data/snli_1.0' + files = ['snli_1.0_train.jsonl', + 'snli_1.0_dev.jsonl', 'snli_1.0_test.jsonl'] + return load_snli(path, files) + + +g_datasets = { + 'ptb-pos': get_ptb_pos, + 'ctb-pos': get_ctb_pos, + 'conll-pos': get_conll2012_pos, + 'conll-ner': get_conll2012_ner, + 'sst-cls': get_sst, + 'snli-nli': get_snli, +} + + +def load_pretrain_emb(word_v, lang='en'): + print('loading pre-train embeddings') + emb = EmbedLoader.fast_load_embedding(300, g_emb_file_path[lang], word_v) + emb /= np.linalg.norm(emb, axis=1, keepdims=True) + emb = torch.tensor(emb, dtype=torch.float32) + print('embedding mean: {:.6}, std: {:.6}'.format(emb.mean(), emb.std())) + emb[word_v.padding_idx].fill_(0) + return emb + + +class MyCallback(FN.core.callback.Callback): + def on_train_begin(self): + super(MyCallback, self).on_train_begin() + self.init_lrs = [pg['lr'] for pg in self.optimizer.param_groups] + + def on_backward_end(self): + nn.utils.clip_grad.clip_grad_norm_(self.model.parameters(), 5.0) + + def on_step_end(self): + warm_steps = 6000 + # learning rate warm-up & decay + if self.step <= warm_steps: + for lr, pg in zip(self.init_lrs, self.optimizer.param_groups): + pg['lr'] = lr * (self.step / float(warm_steps)) + + elif self.step % 3000 == 0: + for pg in self.optimizer.param_groups: + cur_lr = pg['lr'] + pg['lr'] = max(1e-5, cur_lr*g_args.lr_decay) + + + +def train(): + seed = set_rng_seeds(1234) + print('RNG SEED {}'.format(seed)) + print('loading data') + ds_list, word_v, tag_v = g_datasets['{}-{}'.format( + g_args.ds, g_args.task)]() + print(ds_list[0][:2]) + embed = load_pretrain_emb(word_v, lang='zh' if g_args.ds == 'ctb' else 'en') + g_model_cfg['num_cls'] = len(tag_v) + print(g_model_cfg) + g_model_cfg['init_embed'] = embed + model = g_model_select[g_args.task.lower()](**g_model_cfg) + + def init_model(model): + for p in model.parameters(): + if p.size(0) != len(word_v): + nn.init.normal_(p, 0.0, 0.05) + init_model(model) + train_data = ds_list[0] + dev_data = ds_list[2] + test_data = ds_list[1] + print(tag_v.word2idx) + + if g_args.task in ['pos', 'ner']: + padding_idx = tag_v.padding_idx + else: + padding_idx = -100 + print('padding_idx ', padding_idx) + loss = FN.CrossEntropyLoss(padding_idx=padding_idx) + metrics = { + 'pos': (None, FN.AccuracyMetric()), + 'ner': ('f', FN.core.metrics.SpanFPreRecMetric( + tag_vocab=tag_v, encoding_type='bmeso', ignore_labels=[''], )), + 'cls': (None, FN.AccuracyMetric()), + 'nli': (None, FN.AccuracyMetric()), + } + metric_key, metric = metrics[g_args.task] + device = 'cuda' if torch.cuda.is_available() else 'cpu' + ex_param = [x for x in model.parameters( + ) if x.requires_grad and x.size(0) != len(word_v)] + optim_cfg = [{'params': model.enc.embedding.parameters(), 'lr': g_args.lr*0.1}, + {'params': ex_param, 'lr': g_args.lr, 'weight_decay': g_args.w_decay}, ] + trainer = FN.Trainer(model=model, train_data=train_data, dev_data=dev_data, + loss=loss, metrics=metric, metric_key=metric_key, + optimizer=torch.optim.Adam(optim_cfg), + n_epochs=g_args.ep, batch_size=g_args.bsz, print_every=10, validate_every=3000, + device=device, + use_tqdm=False, prefetch=False, + save_path=g_args.log, + callbacks=[MyCallback()]) + + trainer.train() + tester = FN.Tester(data=test_data, model=model, metrics=metric, + batch_size=128, device=device) + tester.test() + + +def test(): + pass + + +def infer(): + pass + + +run_select = { + 'train': train, + 'test': test, + 'infer': infer, +} + + +def main(): + global g_args, g_model_cfg + import signal + + def signal_handler(signal, frame): + raise KeyboardInterrupt + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + parser = get_argparser() + parser.add_argument('--task', choices=['pos', 'ner', 'cls', 'nli']) + parser.add_argument('--mode', choices=['train', 'test', 'infer']) + parser.add_argument('--ds', type=str) + add_model_args(parser) + g_args = parser.parse_args() + print(g_args.__dict__) + set_gpu(g_args.gpu) + g_model_cfg = { + 'init_embed': (None, 300), + 'num_cls': None, + 'hidden_size': g_args.hidden, + 'num_layers': 4, + 'num_head': g_args.nhead, + 'head_dim': g_args.hdim, + 'max_len': MAX_LEN, + 'cls_hidden_size': 600, + 'emb_dropout': 0.3, + 'dropout': g_args.drop, + } + run_select[g_args.mode.lower()]() + + +if __name__ == '__main__': + main() diff --git a/reproduction/Star_transformer/util.py b/reproduction/Star_transformer/util.py new file mode 100644 index 00000000..ecd1e18d --- /dev/null +++ b/reproduction/Star_transformer/util.py @@ -0,0 +1,112 @@ +import fastNLP as FN +import argparse +import os +import random +import numpy +import torch + + +def get_argparser(): + parser = argparse.ArgumentParser() + parser.add_argument('--lr', type=float, required=True) + parser.add_argument('--w_decay', type=float, required=True) + parser.add_argument('--lr_decay', type=float, required=True) + parser.add_argument('--bsz', type=int, required=True) + parser.add_argument('--ep', type=int, required=True) + parser.add_argument('--drop', type=float, required=True) + parser.add_argument('--gpu', type=str, required=True) + parser.add_argument('--log', type=str, default=None) + return parser + + +def add_model_args(parser): + parser.add_argument('--nhead', type=int, default=6) + parser.add_argument('--hdim', type=int, default=50) + parser.add_argument('--hidden', type=int, default=300) + return parser + + +def set_gpu(gpu_str): + os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" + os.environ['CUDA_VISIBLE_DEVICES'] = gpu_str + + +def set_rng_seeds(seed=None): + if seed is None: + seed = numpy.random.randint(0, 65536) + random.seed(seed) + numpy.random.seed(seed) + torch.random.manual_seed(seed) + torch.cuda.manual_seed_all(seed) + # print('RNG_SEED {}'.format(seed)) + return seed + + +class TensorboardCallback(FN.Callback): + """ + 接受以下一个或多个字符串作为参数: + - "model" + - "loss" + - "metric" + """ + + def __init__(self, *options): + super(TensorboardCallback, self).__init__() + args = {"model", "loss", "metric"} + for opt in options: + if opt not in args: + raise ValueError( + "Unrecognized argument {}. Expect one of {}".format(opt, args)) + self.options = options + self._summary_writer = None + self.graph_added = False + + def on_train_begin(self): + save_dir = self.trainer.save_path + if save_dir is None: + path = os.path.join( + "./", 'tensorboard_logs_{}'.format(self.trainer.start_time)) + else: + path = os.path.join( + save_dir, 'tensorboard_logs_{}'.format(self.trainer.start_time)) + self._summary_writer = SummaryWriter(path) + + def on_batch_begin(self, batch_x, batch_y, indices): + if "model" in self.options and self.graph_added is False: + # tesorboardX 这里有大bug,暂时没法画模型图 + # from fastNLP.core.utils import _build_args + # inputs = _build_args(self.trainer.model, **batch_x) + # args = tuple([value for value in inputs.values()]) + # args = args[0] if len(args) == 1 else args + # self._summary_writer.add_graph(self.trainer.model, torch.zeros(32, 2)) + self.graph_added = True + + def on_backward_begin(self, loss): + if "loss" in self.options: + self._summary_writer.add_scalar( + "loss", loss.item(), global_step=self.trainer.step) + + if "model" in self.options: + for name, param in self.trainer.model.named_parameters(): + if param.requires_grad: + self._summary_writer.add_scalar( + name + "_mean", param.mean(), global_step=self.trainer.step) + # self._summary_writer.add_scalar(name + "_std", param.std(), global_step=self.trainer.step) + self._summary_writer.add_scalar(name + "_grad_mean", param.grad.mean(), + global_step=self.trainer.step) + + def on_valid_end(self, eval_result, metric_key): + if "metric" in self.options: + for name, metric in eval_result.items(): + for metric_key, metric_val in metric.items(): + self._summary_writer.add_scalar("valid_{}_{}".format(name, metric_key), metric_val, + global_step=self.trainer.step) + + def on_train_end(self): + self._summary_writer.close() + del self._summary_writer + + def on_exception(self, exception): + if hasattr(self, "_summary_writer"): + self._summary_writer.close() + del self._summary_writer From 96437f9e266d01febfa54cde541594efa160c4fb Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 19:30:04 +0800 Subject: [PATCH 153/173] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E6=96=87=E6=A1=A3=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/batch.py | 7 ++----- fastNLP/core/dataset.py | 8 ++------ fastNLP/core/field.py | 4 +--- fastNLP/core/instance.py | 4 +--- fastNLP/core/losses.py | 6 +++--- fastNLP/core/utils.py | 8 ++------ fastNLP/core/vocabulary.py | 20 +++++--------------- fastNLP/io/dataset_loader.py | 2 +- 8 files changed, 17 insertions(+), 42 deletions(-) diff --git a/fastNLP/core/batch.py b/fastNLP/core/batch.py index c1289adf..109d4fe9 100644 --- a/fastNLP/core/batch.py +++ b/fastNLP/core/batch.py @@ -30,11 +30,8 @@ class Batch(object): """ 别名::class:`fastNLP.Batch` :class:`fastNLP.core.batch.Batch` - Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出. - 组成 `x` 和 `y` - - - Example:: + Batch 用于从 `DataSet` 中按一定的顺序, 依次按 ``batch_size`` 的大小将数据取出, + 组成 `x` 和 `y`:: batch = Batch(data_set, batch_size=16, sampler=SequentialSampler()) num_batch = len(batch) diff --git a/fastNLP/core/dataset.py b/fastNLP/core/dataset.py index 2f3e35ca..9f24adf2 100644 --- a/fastNLP/core/dataset.py +++ b/fastNLP/core/dataset.py @@ -571,9 +571,7 @@ class DataSet(object): def set_input(self, *field_names, flag=True): """ - 将field_names的field设置为input - - Example:: + 将field_names的field设置为input:: dataset.set_input('words', 'seq_len') # 将words和seq_len这两个field的input属性设置为True dataset.set_input('words', flag=False) # 将words这个field的input属性设置为False @@ -605,9 +603,7 @@ class DataSet(object): def set_padder(self, field_name, padder): """ - 为field_name设置padder - - Example:: + 为field_name设置padder:: from fastNLP import EngChar2DPadder padder = EngChar2DPadder() diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index 21ead327..fca1cee1 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -448,9 +448,7 @@ class EngChar2DPadder(Padder): 但这个Padder只能处理index为int的情况。 padded过后的batch内容,形状为(batch_size, max_sentence_length, max_word_length). max_sentence_length为这个batch中最大句 - 子长度;max_word_length为这个batch中最长的word的长度 - - Example:: + 子长度;max_word_length为这个batch中最长的word的长度:: from fastNLP import DataSet from fastNLP import EngChar2DPadder diff --git a/fastNLP/core/instance.py b/fastNLP/core/instance.py index 07ae6495..5408522e 100644 --- a/fastNLP/core/instance.py +++ b/fastNLP/core/instance.py @@ -13,9 +13,7 @@ class Instance(object): 别名::class:`fastNLP.Instance` :class:`fastNLP.core.instance.Instance` Instance是fastNLP中对应一个sample的类。每个sample在fastNLP中是一个Instance对象。 - Instance一般与 :class:`~fastNLP.DataSet` 一起使用, Instance的初始化如下面的Example所示 - - Example:: + Instance一般与 :class:`~fastNLP.DataSet` 一起使用, Instance的初始化如下面的Example所示:: >>>from fastNLP import Instance >>>ins = Instance(field_1=[1, 1, 1], field_2=[2, 2, 2]) diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index ddc2c49f..35c14770 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -190,10 +190,10 @@ class LossFunc(LossBase): 找到相对应的参数名为value的参数,并传入func中作为参数名为key的参数 :param kwargs: 除了参数映射表以外可以用key word args的方式设置参数映射关系 - Example:: + 使用方法:: - >>> func = torch.nn.CrossEntropyLoss() - >>> loss_func = LossFunc(func, input="pred", target="label") + func = torch.nn.CrossEntropyLoss() + loss_func = LossFunc(func, input="pred", target="label") # 这表示构建了一个损失函数类,由func计算损失函数,其中将从模型返回值或者DataSet的target=True的field # 当中找到一个参数名为`pred`的参数传入func一个参数名为`input`的参数;找到一个参数名为`label`的参数 # 传入func作为一个名为`target`的参数 diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 518c8213..79af296b 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -68,18 +68,14 @@ def cache_results(_cache_fp, _refresh=False, _verbose=1): # res = [5 4 9 1 8] # 0.0040721893310546875 - 可以看到第二次运行的时候,只用了0.0001s左右,是由于第二次运行将直接从cache.pkl这个文件读取数据,而不会经过再次预处理 - - Example:: + 可以看到第二次运行的时候,只用了0.0001s左右,是由于第二次运行将直接从cache.pkl这个文件读取数据,而不会经过再次预处理:: # 还是以上面的例子为例,如果需要重新生成另一个cache,比如另一个数据集的内容,通过如下的方式调用即可 process_data(_cache_fp='cache2.pkl') # 完全不影响之前的‘cache.pkl' 上面的_cache_fp是cache_results会识别的参数,它将从'cache2.pkl'这里缓存/读取数据,即这里的'cache2.pkl'覆盖默认的 'cache.pkl'。如果在你的函数前面加上了@cache_results()则你的函数会增加三个参数[_cache_fp, _refresh, _verbose]。 - 上面的例子即为使用_cache_fp的情况,这三个参数不会传入到你的函数中,当然你写的函数参数名也不可能包含这三个名称。 - - Example:: + 上面的例子即为使用_cache_fp的情况,这三个参数不会传入到你的函数中,当然你写的函数参数名也不可能包含这三个名称:: process_data(_cache_fp='cache2.pkl', _refresh=True) # 这里强制重新生成一份对预处理的cache。 # _verbose是用于控制输出信息的,如果为0,则不输出任何内容;如果为1,则会提醒当前步骤是读取的cache还是生成了新的cache diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index 43f590fd..cbde9cba 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -44,9 +44,7 @@ class Vocabulary(object): """ 别名::class:`fastNLP.Vocabulary` :class:`fastNLP.core.vocabulary.Vocabulary` - 用于构建, 存储和使用 `str` 到 `int` 的一一映射 - - Example:: + 用于构建, 存储和使用 `str` 到 `int` 的一一映射:: vocab = Vocabulary() word_list = "this is a word list".split() @@ -159,9 +157,7 @@ class Vocabulary(object): def has_word(self, w): """ - 检查词是否被记录 - - Example:: + 检查词是否被记录:: has_abc = vocab.has_word('abc') # equals to @@ -189,9 +185,7 @@ class Vocabulary(object): @_check_build_vocab def index_dataset(self, *datasets, field_name, new_field_name=None): """ - 将DataSet中对应field的词转为数字. - - Example:: + 将DataSet中对应field的词转为数字,Example:: # remember to use `field_name` vocab.index_dataset(train_data, dev_data, test_data, field_name='words') @@ -234,9 +228,7 @@ class Vocabulary(object): def from_dataset(self, *datasets, field_name): """ - 使用dataset的对应field中词构建词典 - - Example:: + 使用dataset的对应field中词构建词典:: # remember to use `field_name` vocab.from_dataset(train_data1, train_data2, field_name='words') @@ -280,9 +272,7 @@ class Vocabulary(object): def to_index(self, w): """ 将词转为数字. 若词不再词典中被记录, 将视为 unknown, 若 ``unknown=None`` , 将抛出 - ``ValueError`` - - Example:: + ``ValueError``:: index = vocab.to_index('abc') # equals to diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 32cca88f..0abaa42b 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -106,7 +106,7 @@ class DataSetLoader: """ 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` - 定义了各种 DataSetLoader 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 + 定义了各种 DataSetLoader (针对特定数据上的特定任务) 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 开发者至少应该编写如下内容: From 86536e0ed0486ab69ebcb8e7c2fff147bae37902 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 20:49:47 +0800 Subject: [PATCH 154/173] =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .readthedocs.yml | 11 +++++++++++ README.md | 12 +++++++----- fastNLP/core/losses.py | 2 +- readthedocs.yml | 6 ------ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 .readthedocs.yml delete mode 100644 readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..d9dd3f92 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,11 @@ +version: 2 + +sphinx: + configuration: docs/conf.py + +build: + image: latest + +python: + version: 3.6 + setup_py_install: true \ No newline at end of file diff --git a/README.md b/README.md index f4a8f2a6..77758cfd 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,13 @@ pip install fastNLP ``` +## 参考资源 + +- [文档](https://fastnlp.readthedocs.io/zh/latest/) +- [源码](https://github.com/fastnlp/fastNLP) + + + ## 内置组件 大部分用于的 NLP 任务神经网络都可以看做由编码(encoder)、聚合(aggregator)、解码(decoder)三种模块组成。 @@ -102,11 +109,6 @@ fastNLP的大致工作流程如上图所示,而项目结构如下: -## 参考资源 - -- [教程](https://github.com/fastnlp/fastNLP/tree/master/tutorials) -- [文档](https://fastnlp.readthedocs.io/en/latest/) -- [源码](https://github.com/fastnlp/fastNLP) diff --git a/fastNLP/core/losses.py b/fastNLP/core/losses.py index 35c14770..9dc02f3d 100644 --- a/fastNLP/core/losses.py +++ b/fastNLP/core/losses.py @@ -227,7 +227,7 @@ class CrossEntropyLoss(LossBase): Example:: - >>> loss = CrossEntropyLoss(pred='pred', target='label', padding_idx=0) + loss = CrossEntropyLoss(pred='pred', target='label', padding_idx=0) """ diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index 9b172987..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,6 +0,0 @@ -build: - image: latest - -python: - version: 3.6 - setup_py_install: true \ No newline at end of file From 1cf64b2aab132e57898f8b66367ef227920abbb6 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 20:56:52 +0800 Subject: [PATCH 155/173] =?UTF-8?q?=E5=86=8D=E5=B0=9D=E8=AF=95=E4=B8=80?= =?UTF-8?q?=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .readthedocs.yml | 11 ----------- docs/readthedocs.yml | 15 +++++++++++++++ readthedocs.yml | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) delete mode 100644 .readthedocs.yml create mode 100644 docs/readthedocs.yml create mode 100644 readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index d9dd3f92..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: 2 - -sphinx: - configuration: docs/conf.py - -build: - image: latest - -python: - version: 3.6 - setup_py_install: true \ No newline at end of file diff --git a/docs/readthedocs.yml b/docs/readthedocs.yml new file mode 100644 index 00000000..6b191eb8 --- /dev/null +++ b/docs/readthedocs.yml @@ -0,0 +1,15 @@ +version: 2 + +sphinx: + configuration: source/conf.py + +build: + image: latest + +python: + version: 3.6 + setup_py_install: true + + +formats: + - htmlzip \ No newline at end of file diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 00000000..e9189222 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,15 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +build: + image: latest + +python: + version: 3.6 + setup_py_install: true + + +formats: + - htmlzip \ No newline at end of file From f1accd55b6896d4c2f4b61d424b5b574b87af278 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 21:02:21 +0800 Subject: [PATCH 156/173] =?UTF-8?q?=E5=86=8D=E5=B0=9D=E8=AF=95=E4=B8=80?= =?UTF-8?q?=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/readthedocs.yml | 4 +++- readthedocs.yml | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/readthedocs.yml b/docs/readthedocs.yml index 6b191eb8..6efe756a 100644 --- a/docs/readthedocs.yml +++ b/docs/readthedocs.yml @@ -8,7 +8,9 @@ build: python: version: 3.6 - setup_py_install: true + install: + - method: setuptools + path: .. formats: diff --git a/readthedocs.yml b/readthedocs.yml index e9189222..e6d5bafd 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -8,8 +8,9 @@ build: python: version: 3.6 - setup_py_install: true - + install: + - method: setuptools + path: . formats: - htmlzip \ No newline at end of file From 282649a66728fb1ee0ec9f1e21f8a4a942fe4478 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 21:05:43 +0800 Subject: [PATCH 157/173] =?UTF-8?q?=E5=BA=94=E8=AF=A5=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/readthedocs.yml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/readthedocs.yml diff --git a/docs/readthedocs.yml b/docs/readthedocs.yml deleted file mode 100644 index 6efe756a..00000000 --- a/docs/readthedocs.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 - -sphinx: - configuration: source/conf.py - -build: - image: latest - -python: - version: 3.6 - install: - - method: setuptools - path: .. - - -formats: - - htmlzip \ No newline at end of file From 83aae881006385df422c07b025b11edfff9f1440 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Wed, 22 May 2019 21:21:51 +0800 Subject: [PATCH 158/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E5=9B=BE?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77758cfd..1e040f4b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/fastnlp/fastNLP.svg?branch=master)](https://travis-ci.org/fastnlp/fastNLP) [![codecov](https://codecov.io/gh/fastnlp/fastNLP/branch/master/graph/badge.svg)](https://codecov.io/gh/fastnlp/fastNLP) -[![PyPI version](https://badge.fury.io/py/fastNLP.svg)](https://badge.fury.io/py/fastNLP) +[![Pypi](https://img.shields.io/pypi/v/fastNLP.svg)](https://pypi.org/project/fastNLP) ![Hex.pm](https://img.shields.io/hexpm/l/plug.svg) [![Documentation Status](https://readthedocs.org/projects/fastnlp/badge/?version=latest)](http://fastnlp.readthedocs.io/?badge=latest) @@ -112,4 +112,4 @@ fastNLP的大致工作流程如上图所示,而项目结构如下: -*In memory of @FengZiYjun. May his soul rest in peace. We will miss you very very much!* \ No newline at end of file +*In memory of @FengZiYjun. May his soul rest in peace. We will miss you very very much!* From 3b3f550cc5a691d7cd81beea26b3ceeb90475b20 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Thu, 23 May 2019 14:56:05 +0800 Subject: [PATCH 159/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/modules/encoder/embedding.py | 7 +++++++ fastNLP/modules/utils.py | 6 +++--- setup.py | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/fastNLP/modules/encoder/embedding.py b/fastNLP/modules/encoder/embedding.py index f3c1f475..c2dfab65 100644 --- a/fastNLP/modules/encoder/embedding.py +++ b/fastNLP/modules/encoder/embedding.py @@ -41,3 +41,10 @@ class Embedding(nn.Embedding): """ x = super().forward(x) return self.dropout(x) + + def size(self): + """ + Embedding的大小 + :return: torch.Size() + """ + return self.weight.size() diff --git a/fastNLP/modules/utils.py b/fastNLP/modules/utils.py index c9a1f682..741429bb 100644 --- a/fastNLP/modules/utils.py +++ b/fastNLP/modules/utils.py @@ -74,9 +74,9 @@ def get_embeddings(init_embed): """ 根据输入的init_embed生成nn.Embedding对象。 - :param init_embed: 单词词典, 可以是 tuple, 包括(num_embedings, embedding_dim), 即 - embedding的大小和每个词的维度. 也可以传入 nn.Embedding 对象, - 此时就以传入的对象作为embedding + :param init_embed: 可以是 tuple:(num_embedings, embedding_dim), 即embedding的大小和每个词的维度;也可以传入 + nn.Embedding 对象, 此时就以传入的对象作为embedding; 传入np.ndarray也行,将使用传入的ndarray作为作为Embedding初始 + 化; 传入orch.Tensor, 将使用传入的值作为Embedding初始化。 :return nn.Embedding embeddings: """ if isinstance(init_embed, tuple): diff --git a/setup.py b/setup.py index b7834d8d..49646761 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,8 @@ setup( version='0.4.0', description='fastNLP: Deep Learning Toolkit for NLP, developed by Fudan FastNLP Team', long_description=readme, - license=license, + long_description_content_type='text/markdown', + license='Apache License', author='FudanNLP', python_requires='>=3.6', packages=find_packages(), From 34a638175f17b87d238a33a2979f46676575e81a Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 23 May 2019 21:24:56 +0800 Subject: [PATCH 160/173] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E7=BB=93?= =?UTF-8?q?=E5=90=88=20fitlog=20=E7=9A=84=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/figures/fitlogChart.png | Bin 0 -> 271505 bytes docs/source/figures/fitlogTable.png | Bin 0 -> 168457 bytes docs/source/index.rst | 1 + docs/source/user/with_fitlog.rst | 119 +++++++++++++++++++++++++++- fastNLP/core/callback.py | 40 +++++----- 5 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 docs/source/figures/fitlogChart.png create mode 100644 docs/source/figures/fitlogTable.png diff --git a/docs/source/figures/fitlogChart.png b/docs/source/figures/fitlogChart.png new file mode 100644 index 0000000000000000000000000000000000000000..57ae16830641570bd7ebacb364c7ca7a951050be GIT binary patch literal 271505 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

K|K8tHt^Bjdt1Zfx1%3#oOJ+U7s603h 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

Ehqb@$*9R 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 z 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 快速入门 详细指南 + 科研指南 API 文档 ------------- diff --git a/docs/source/user/with_fitlog.rst b/docs/source/user/with_fitlog.rst index 97c3ea71..14cf7d91 100644 --- a/docs/source/user/with_fitlog.rst +++ b/docs/source/user/with_fitlog.rst @@ -2,4 +2,121 @@ 科研向导 ================= -本文介绍使用 fastNLP 和 fitlog 进行科学研究的方法 \ No newline at end of file +本文介绍使用 fastNLP 和 fitlog 结合进行科研的方法。 + +首先,我们需要安装 `fitlog `_ 。你需要确认你的电脑中没有其它名为为 `fitlog` 的命令。 + +我们从命令行中进入到一个文件夹,现在我们要在文件夹中创建我们的 fastNLP 项目。你可以在命令行输入 `fitlog init test1` , +然后你会看到如下提示:: + + Initialized empty Git repository in /Users/fdujyn/workspaces/test1/.git/ + Auto commit by fitlog + Initialized empty Git repository in /Users/fdujyn/workspaces/test1/.git/ + Fitlog project test1 is initialized. + +这表明你已经创建成功了项目文件夹,并且在项目文件夹中已经初始化了 Git。如果你不想初始化 Git, +可以参考文档 `命令行工具 `_ + +现在我们进入你创建的项目文件夹 test1 中,可以看到有一个名为 logs 的文件夹,后面我们将会在里面存放你的实验记录。 +同时也有一个名为 main.py 的文件,是我们推荐你使用的入口文件。文件的内容如下:: + + import fitlog + + fitlog.commit(__file__) # auto commit your codes + fitlog.add_hyper_in_file (__file__) # record your hyperparameters + + """ + Your training code here, you may use these functions to log your result: + fitlog.add_hyper() + fitlog.add_loss() + fitlog.add_metric() + fitlog.add_best_metric() + ...... + """ + + fitlog.finish() # finish the logging + +我们推荐你保留除注释外的四行代码,它们有助于你的实验, +他们的具体用处参见文档 `用户 API `_ + +我们假定你要进行前两个教程中的实验,并已经把数据复制到了项目根目录下的 tutorial_sample_dataset.csv 文件中。 +现在我们编写如下的训练代码,使用 :class:`~fastNLP.core.callback.FitlogCallback` 进行实验记录保存:: + + import fitlog + from fastNLP import Vocabulary, Trainer, CrossEntropyLoss, AccuracyMetric + from fastNLP.io import CSVLoader + from fastNLP.models import CNNText + from fastNLP.core.callback import FitlogCallback + + fitlog.commit(__file__) # auto commit your codes + fitlog.add_hyper_in_file (__file__) # record your hyperparameters + + ############hyper + word_embed = 50 + dropout = 0.1 + ############hyper + + loader = CSVLoader(headers=('raw_sentence', 'label'), sep='\t') + dataset = loader.load("tutorial_sample_dataset.csv") + + dataset.apply(lambda x: x['raw_sentence'].lower(), new_field_name='sentence') + dataset.apply(lambda x: x['sentence'].split(), new_field_name='words', is_input=True) + dataset.apply(lambda x: int(x['label']), new_field_name='target', is_target=True) + vocab = Vocabulary(min_freq=2).from_dataset(dataset, field_name='words') + vocab.index_dataset(dataset, field_name='words',new_field_name='words') + + model = CNNText((len(vocab),word_embed), num_classes=5, padding=2, dropout=dropout) + + train_dev_data, test_data = dataset.split(0.1) + train_data, dev_data = train_dev_data.split(0.1) + + trainer = Trainer(model=model, train_data=train_data, dev_data=dev_data, + loss=CrossEntropyLoss(), metrics=AccuracyMetric(), + callbacks=[FitlogCallback(test_data)]) + trainer.train() + + fitlog.finish() # finish the logging + +用命令行在项目目录下执行 `python main.py` 之后,输出结果如下:: + + Auto commit by fitlog + input fields after batch(if batch size is 2): + words: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2, 11]) + target fields after batch(if batch size is 2): + target: (1)type:torch.Tensor (2)dtype:torch.int64, (3)shape:torch.Size([2]) + + training epochs started 2019-05-23-21-11-51 + Evaluation at Epoch 1/10. Step:2/20. AccuracyMetric: acc=0.285714 + + Evaluation at Epoch 2/10. Step:4/20. AccuracyMetric: acc=0.285714 + + Evaluation at Epoch 3/10. Step:6/20. AccuracyMetric: acc=0.285714 + + Evaluation at Epoch 4/10. Step:8/20. AccuracyMetric: acc=0.428571 + + Evaluation at Epoch 5/10. Step:10/20. AccuracyMetric: acc=0.571429 + + Evaluation at Epoch 6/10. Step:12/20. AccuracyMetric: acc=0.571429 + + Evaluation at Epoch 7/10. Step:14/20. AccuracyMetric: acc=0.285714 + + Evaluation at Epoch 8/10. Step:16/20. AccuracyMetric: acc=0.142857 + + Evaluation at Epoch 9/10. Step:18/20. AccuracyMetric: acc=0.285714 + + Evaluation at Epoch 10/10. Step:20/20. AccuracyMetric: acc=0.571429 + + + In Epoch:5/Step:10, got best dev performance:AccuracyMetric: acc=0.571429 + Reloaded the best model. + +现在,我们在项目目录下输入 `fitlog log logs` ,命令行会启动一个网页,默认 url 为 ``0.0.0.0:5000`` 。 +我们在浏览器中打开网页,可以看到如下的统计表格: + +.. image:: ../figures/fitlogTable.png + +如果我们点击action中的最后一个键钮,可以看到详细的 loss 图: + +.. image:: ../figures/fitlogChart.png + +更多的教程还在编写中,敬请期待~ \ No newline at end of file diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 7fad2d0b..07654cc8 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -54,6 +54,7 @@ __all__ = [ "GradientClipCallback", "EarlyStopCallback", "TensorboardCallback", + "FitlogCallback", "LRScheduler", "ControlC", @@ -65,6 +66,7 @@ import os import torch from copy import deepcopy + try: from tensorboardX import SummaryWriter @@ -81,6 +83,7 @@ try: except: pass + class Callback(object): """ 别名::class:`fastNLP.Callback` :class:`fastNLP.core.callback.Callback` @@ -431,14 +434,13 @@ class EarlyStopCallback(Callback): else: raise exception # 抛出陌生Error + class FitlogCallback(Callback): """ - 别名: :class:`fastNLP.FitlogCallback` :class:`fastNLP.core.callback.FitlogCallback` - 该callback将loss和progress自动写入到fitlog中; 如果Trainer有dev的数据,将自动把dev的结果写入到log中; 同时还支持传入 - 一个(或多个)test数据集进行测试(只有在trainer具有dev时才能使用),每次在dev上evaluate之后会在这些数据集上验证一下。 - 并将验证结果写入到fitlog中。这些数据集的结果是根据dev上最好的结果报道的,即如果dev在第3个epoch取得了最佳,则 - fitlog中记录的关于这些数据集的结果就是来自第三个epoch的结果。 + 一个(或多个)test数据集进行测试(只有在trainer具有dev时才能使用),每次在dev上evaluate之后会在这些数据集上验证一下。 + 并将验证结果写入到fitlog中。这些数据集的结果是根据dev上最好的结果报道的,即如果dev在第3个epoch取得了最佳,则 + fitlog中记录的关于这些数据集的结果就是来自第三个epoch的结果。 :param DataSet,dict(DataSet) data: 传入DataSet对象,会使用多个Trainer中的metric对数据进行验证。如果需要传入多个 DataSet请通过dict的方式传入,dict的key将作为对应dataset的name传递给fitlog。若tester不为None时,data需要通过 @@ -447,7 +449,9 @@ class FitlogCallback(Callback): :param int verbose: 是否在终端打印内容,0不打印 :param bool log_exception: fitlog是否记录发生的exception信息 """ - + # 还没有被导出到 fastNLP 层 + # 别名: :class:`fastNLP.FitlogCallback` :class:`fastNLP.core.callback.FitlogCallback` + def __init__(self, data=None, tester=None, verbose=0, log_exception=False): super().__init__() self.datasets = {} @@ -460,7 +464,7 @@ class FitlogCallback(Callback): assert 'test' not in data, "Cannot use `test` as DataSet key, when tester is passed." setattr(tester, 'verbose', 0) self.testers['test'] = tester - + if isinstance(data, dict): for key, value in data.items(): assert isinstance(value, DataSet), f"Only DataSet object is allowed, not {type(value)}." @@ -470,23 +474,23 @@ class FitlogCallback(Callback): self.datasets['test'] = data else: raise TypeError("data receives dict[DataSet] or DataSet object.") - + self.verbose = verbose - + def on_train_begin(self): - if (len(self.datasets)>0 or len(self.testers)>0 ) and self.trainer.dev_data is None: + if (len(self.datasets) > 0 or len(self.testers) > 0) and self.trainer.dev_data is None: raise RuntimeError("Trainer has no dev data, you cannot pass extra data to do evaluation.") - - if len(self.datasets)>0: + + if len(self.datasets) > 0: for key, data in self.datasets.items(): tester = Tester(data=data, model=self.model, batch_size=self.batch_size, metrics=self.trainer.metrics, verbose=0) self.testers[key] = tester fitlog.add_progress(total_steps=self.n_steps) - + def on_backward_begin(self, loss): fitlog.add_loss(loss.item(), name='loss', step=self.step, epoch=self.epoch) - + def on_valid_end(self, eval_result, metric_key, optimizer, better_result): if better_result: eval_result = deepcopy(eval_result) @@ -494,11 +498,11 @@ class FitlogCallback(Callback): eval_result['epoch'] = self.epoch fitlog.add_best_metric(eval_result) fitlog.add_metric(eval_result, step=self.step, epoch=self.epoch) - if len(self.testers)>0: + if len(self.testers) > 0: for key, tester in self.testers.items(): try: eval_result = tester.test() - if self.verbose!=0: + if self.verbose != 0: self.pbar.write("Evaluation on DataSet {}:".format(key)) self.pbar.write(tester._format_eval_results(eval_result)) fitlog.add_metric(eval_result, name=key, step=self.step, epoch=self.epoch) @@ -506,10 +510,10 @@ class FitlogCallback(Callback): fitlog.add_best_metric(eval_result, name=key) except Exception: self.pbar.write("Exception happens when evaluate on DataSet named `{}`.".format(key)) - + def on_train_end(self): fitlog.finish() - + def on_exception(self, exception): fitlog.finish(status=1) if self._log_exception: From 186b9367f0fb15cc0ee81c5061c8acf00ecb296f Mon Sep 17 00:00:00 2001 From: ChenXin Date: Thu, 23 May 2019 21:31:06 +0800 Subject: [PATCH 161/173] =?UTF-8?q?=E6=94=B9=E4=BA=86=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E9=94=99=E5=88=AB=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/user/with_fitlog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/user/with_fitlog.rst b/docs/source/user/with_fitlog.rst index 14cf7d91..51445775 100644 --- a/docs/source/user/with_fitlog.rst +++ b/docs/source/user/with_fitlog.rst @@ -2,9 +2,9 @@ 科研向导 ================= -本文介绍使用 fastNLP 和 fitlog 结合进行科研的方法。 +本文介绍结合使用 fastNLP 和 fitlog 进行科研的方法。 -首先,我们需要安装 `fitlog `_ 。你需要确认你的电脑中没有其它名为为 `fitlog` 的命令。 +首先,我们需要安装 `fitlog `_ 。你需要确认你的电脑中没有其它名为 `fitlog` 的命令。 我们从命令行中进入到一个文件夹,现在我们要在文件夹中创建我们的 fastNLP 项目。你可以在命令行输入 `fitlog init test1` , 然后你会看到如下提示:: @@ -18,7 +18,7 @@ 可以参考文档 `命令行工具 `_ 现在我们进入你创建的项目文件夹 test1 中,可以看到有一个名为 logs 的文件夹,后面我们将会在里面存放你的实验记录。 -同时也有一个名为 main.py 的文件,是我们推荐你使用的入口文件。文件的内容如下:: +同时也有一个名为 main.py 的文件,这是我们推荐你使用的训练入口文件。文件的内容如下:: import fitlog From e1133f600e70f5e6537e67fae1e071164232e614 Mon Sep 17 00:00:00 2001 From: ChenXin Date: Fri, 24 May 2019 01:12:21 +0800 Subject: [PATCH 162/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=20README.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- docs/source/figures/workflow.png | Bin 0 -> 336236 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/source/figures/workflow.png diff --git a/README.md b/README.md index 1e040f4b..9d949482 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,6 @@ fastNLP的大致工作流程如上图所示,而项目结构如下: - +
*In memory of @FengZiYjun. May his soul rest in peace. We will miss you very very much!* diff --git a/docs/source/figures/workflow.png b/docs/source/figures/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..d2f22df84050a0b74e73ca729bb9231ea049b3d6 GIT binary patch literal 336236 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%0AVFfebQkty{&fodb_g; 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%0DeW>cs!yO|P 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

*sjS)olf50lR5GL}b`)_E4&u>v7-N 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�xwr>SxwuJeh=7GpVtU?bm+9 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 e*#DcO7VFg*iD}!PLB_ej$K=E*!=mGu(0>Dt Date: Fri, 24 May 2019 01:21:29 +0800 Subject: [PATCH 163/173] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E6=96=87=E6=A1=A3=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 9 +++++---- fastNLP/core/trainer.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 07654cc8..e617cf2a 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -370,16 +370,17 @@ class GradientClipCallback(Callback): 每次backward前,将parameter的gradient clip到某个范围。 - :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。如果为None则默认对Trainer - 的model中所有参数进行clip + :param None,torch.Tensor,List[torch.Tensor] parameters: 一般通过model.parameters()获得。 + 如果为None则默认对Trainer的model中所有参数进行clip :param float clip_value: 将gradient 限制到[-clip_value, clip_value]。clip_value应该为正数 :param str clip_type: 支持'norm', 'value' 两种:: 1 'norm', 将gradient的norm rescale到[-clip_value, clip_value] - 2 'value', 将gradient限制在[-clip_value, clip_value], 小于-clip_value的gradient被赋值为-clip_value; - 大于clip_value的gradient被赋值为clip_value. + 2 'value', 将gradient限制在[-clip_value, clip_value], + 小于-clip_value的gradient被赋值为-clip_value; + 大于clip_value的gradient被赋值为clip_value. """ diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 702cb6e7..2523a957 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -498,14 +498,15 @@ class Trainer(object): """ 使用该函数使Trainer开始训练。 - :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 - 最好的模型参数。 + :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效, + 如果True, trainer将在返回之前重新加载dev表现最好的模型参数。 :return dict: 返回一个字典类型的数据, 内含以下内容:: seconds: float, 表示训练时长 以下三个内容只有在提供了dev_data的情况下会有。 - best_eval: Dict of Dict, 表示evaluation的结果。第一层的key为Metric的名称,第二层的key为具体的Metric + best_eval: Dict of Dict, 表示evaluation的结果。第一层的key为Metric的名称, + 第二层的key为具体的Metric best_epoch: int,在第几个epoch取得的最佳值 best_step: int, 在第几个step(batch)更新取得的最佳值 From 1a3dcd3dde6b14eaba350f63e9543e7d3f70825c Mon Sep 17 00:00:00 2001 From: yh_cc Date: Fri, 24 May 2019 18:28:36 +0800 Subject: [PATCH 164/173] =?UTF-8?q?1.Trainer=E8=AE=AD=E7=BB=83train?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=80=E4=B8=AAon=5Fexception=E5=88=A4?= =?UTF-8?q?=E6=96=AD=EF=BC=8C=E6=98=AF=E5=90=A6=E6=8A=9B=E5=87=BA=E5=BC=82?= =?UTF-8?q?=E5=B8=B8;=202.Fitlog=E9=BB=98=E8=AE=A4=E4=B8=8D=E8=AE=B0?= =?UTF-8?q?=E5=BD=95loss=EF=BC=8C=E5=9B=A0=E4=B8=BAloss=E5=AE=9E=E5=9C=A8?= =?UTF-8?q?=E6=AF=94=E8=BE=83=E5=8D=A0=E7=A1=AC=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/callback.py | 19 ++++++++++++++----- fastNLP/core/trainer.py | 8 ++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/fastNLP/core/callback.py b/fastNLP/core/callback.py index 7fad2d0b..c65f2a56 100644 --- a/fastNLP/core/callback.py +++ b/fastNLP/core/callback.py @@ -435,7 +435,7 @@ class FitlogCallback(Callback): """ 别名: :class:`fastNLP.FitlogCallback` :class:`fastNLP.core.callback.FitlogCallback` - 该callback将loss和progress自动写入到fitlog中; 如果Trainer有dev的数据,将自动把dev的结果写入到log中; 同时还支持传入 + 该callback可将loss和progress写入到fitlog中; 如果Trainer有dev的数据,将自动把dev的结果写入到log中; 同时还支持传入 一个(或多个)test数据集进行测试(只有在trainer具有dev时才能使用),每次在dev上evaluate之后会在这些数据集上验证一下。 并将验证结果写入到fitlog中。这些数据集的结果是根据dev上最好的结果报道的,即如果dev在第3个epoch取得了最佳,则 fitlog中记录的关于这些数据集的结果就是来自第三个epoch的结果。 @@ -444,15 +444,18 @@ class FitlogCallback(Callback): DataSet请通过dict的方式传入,dict的key将作为对应dataset的name传递给fitlog。若tester不为None时,data需要通过 dict的方式传入。如果仅传入DataSet, 则被命名为test :param Tester tester: Tester对象,将在on_valid_end时调用。tester中的DataSet会被称为为`test` - :param int verbose: 是否在终端打印内容,0不打印 + :param int log_loss_every: 多少个step记录一次loss(记录的是这几个batch的loss平均值),如果数据集较大建议将该值设置得 + 大一些,不然会导致log文件巨大。默认为0, 即不要记录loss。 + :param int verbose: 是否在终端打印evaluation的结果,0不打印。 :param bool log_exception: fitlog是否记录发生的exception信息 """ - def __init__(self, data=None, tester=None, verbose=0, log_exception=False): + def __init__(self, data=None, tester=None, log_loss_every=0, verbose=0, log_exception=False): super().__init__() self.datasets = {} self.testers = {} self._log_exception = log_exception + assert isinstance(log_loss_every, int) and log_loss_every>=0 if tester is not None: assert isinstance(tester, Tester), "Only fastNLP.Tester allowed." assert isinstance(data, dict) or data is None, "If tester is not None, only dict[DataSet] allowed for data." @@ -472,6 +475,8 @@ class FitlogCallback(Callback): raise TypeError("data receives dict[DataSet] or DataSet object.") self.verbose = verbose + self._log_loss_every = log_loss_every + self._avg_loss = 0 def on_train_begin(self): if (len(self.datasets)>0 or len(self.testers)>0 ) and self.trainer.dev_data is None: @@ -485,7 +490,11 @@ class FitlogCallback(Callback): fitlog.add_progress(total_steps=self.n_steps) def on_backward_begin(self, loss): - fitlog.add_loss(loss.item(), name='loss', step=self.step, epoch=self.epoch) + if self._log_loss_every>0: + self._avg_loss += loss.item() + if self.step%self._log_loss_every==0: + fitlog.add_loss(self._avg_loss/self._log_loss_every, name='loss', step=self.step, epoch=self.epoch) + self._avg_loss = 0 def on_valid_end(self, eval_result, metric_key, optimizer, better_result): if better_result: @@ -513,7 +522,7 @@ class FitlogCallback(Callback): def on_exception(self, exception): fitlog.finish(status=1) if self._log_exception: - fitlog.add_other(str(exception), name='except_info') + fitlog.add_other(repr(exception), name='except_info') class LRScheduler(Callback): diff --git a/fastNLP/core/trainer.py b/fastNLP/core/trainer.py index 702cb6e7..3b1a8bf5 100644 --- a/fastNLP/core/trainer.py +++ b/fastNLP/core/trainer.py @@ -494,12 +494,14 @@ class Trainer(object): self.callback_manager = CallbackManager(env={"trainer": self}, callbacks=callbacks) - def train(self, load_best_model=True): + def train(self, load_best_model=True, on_exception='ignore'): """ 使用该函数使Trainer开始训练。 :param bool load_best_model: 该参数只有在初始化提供了dev_data的情况下有效,如果True, trainer将在返回之前重新加载dev表现 最好的模型参数。 + :param str on_exception: 在训练过程遭遇exception,并被 :py:class:Callback 的on_exception()处理后,是否继续抛出异常。 + 支持'ignore'与'raise': 'ignore'将捕获异常,写在Trainer.train()后面的代码将继续运行; 'raise'将异常抛出。 :return dict: 返回一个字典类型的数据, 内含以下内容:: @@ -527,8 +529,10 @@ class Trainer(object): self.callback_manager.on_train_begin() self._train() self.callback_manager.on_train_end() - except (CallbackException, KeyboardInterrupt) as e: + except (CallbackException, KeyboardInterrupt, Exception) as e: self.callback_manager.on_exception(e) + if on_exception=='raise': + raise e if self.dev_data is not None and hasattr(self, 'best_dev_perf'): print( From e2819d0713ea3451afe060d963a52281f1e85546 Mon Sep 17 00:00:00 2001 From: yh_cc Date: Sat, 25 May 2019 21:01:24 +0800 Subject: [PATCH 165/173] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E4=BA=8EEng2DPadder=E7=9A=84max=20char=20length=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastNLP/core/field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastNLP/core/field.py b/fastNLP/core/field.py index fca1cee1..9ef8d963 100644 --- a/fastNLP/core/field.py +++ b/fastNLP/core/field.py @@ -516,7 +516,7 @@ class EngChar2DPadder(Padder): )) self._exactly_three_dims(contents, field_name) if self.pad_length < 1: - max_char_length = max(max([[len(char_lst) for char_lst in word_lst] for word_lst in contents])) + max_char_length = max([max(len(char_lst) for char_lst in word_lst) for word_lst in contents]) else: max_char_length = self.pad_length max_sent_length = max(len(word_lst) for word_lst in contents) From f5a005358cac16135b2fafd04780bd23e5a54a0d Mon Sep 17 00:00:00 2001 From: yunfan Date: Tue, 28 May 2019 16:15:44 +0800 Subject: [PATCH 166/173] - add process for SSTLoader --- fastNLP/core/utils.py | 29 ++++++- fastNLP/io/dataset_loader.py | 142 +++++++++++++++++++++++++---------- 2 files changed, 132 insertions(+), 39 deletions(-) diff --git a/fastNLP/core/utils.py b/fastNLP/core/utils.py index 79af296b..fa6d90a2 100644 --- a/fastNLP/core/utils.py +++ b/fastNLP/core/utils.py @@ -3,7 +3,8 @@ utils模块实现了 fastNLP 内部和外部所需的很多工具。其中用户 """ __all__ = [ "cache_results", - "seq_len_to_mask" + "seq_len_to_mask", + "Example", ] import _pickle @@ -21,6 +22,32 @@ _CheckRes = namedtuple('_CheckRes', ['missing', 'unused', 'duplicated', 'require 'varargs']) +class Example(dict): + """a dict can treat keys as attributes""" + def __getattr__(self, item): + try: + return self.__getitem__(item) + except KeyError: + raise AttributeError(item) + + def __setattr__(self, key, value): + if key.startswith('__') and key.endswith('__'): + raise AttributeError(key) + self.__setitem__(key, value) + + def __delattr__(self, item): + try: + self.pop(item) + except KeyError: + raise AttributeError(item) + + def __getstate__(self): + return self + + def __setstate__(self, state): + self.update(state) + + def _prepare_cache_filepath(filepath): """ 检查filepath是否可以作为合理的cache文件. 如果可以的话,会自动创造路径 diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 0abaa42b..40604deb 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -29,8 +29,12 @@ from nltk.tree import Tree from ..core.dataset import DataSet from ..core.instance import Instance from .file_reader import _read_csv, _read_json, _read_conll -from typing import Union, Dict +from typing import Union, Dict, Iterable import os +from ..core.utils import Example +from ..core import Vocabulary +from ..io import EmbedLoader +import numpy as np def _download_from_url(url, path): @@ -39,7 +43,7 @@ def _download_from_url(url, path): except: from ..core.utils import _pseudo_tqdm as tqdm import requests - + """Download file""" r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) chunk_size = 16 * 1024 @@ -58,11 +62,11 @@ def _uncompress(src, dst): import gzip import tarfile import os - + def unzip(src, dst): with zipfile.ZipFile(src, 'r') as f: f.extractall(dst) - + def ungz(src, dst): with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: length = 16 * 1024 # 16KB @@ -70,11 +74,11 @@ def _uncompress(src, dst): while buf: uf.write(buf) buf = f.read(length) - + def untar(src, dst): with tarfile.open(src, 'r:gz') as f: f.extractall(dst) - + fn, ext = os.path.splitext(src) _, ext_2 = os.path.splitext(fn) if ext == '.zip': @@ -87,6 +91,34 @@ def _uncompress(src, dst): raise ValueError('unsupported file {}'.format(src)) +class VocabularyOption(Example): + def __init__(self, + max_size=None, + min_freq=None, + padding='', + unknown=''): + super().__init__( + max_size=max_size, + min_freq=min_freq, + padding=padding, + unknown=unknown + ) + + +class EmbeddingOption(Example): + def __init__(self, + embed_filepath=None, + dtype=np.float32, + normalize=True, + error='ignore'): + super().__init__( + embed_filepath=embed_filepath, + dtype=dtype, + normalize=normalize, + error=error + ) + + class DataInfo: """ 经过处理的数据信息,包括一系列数据集(比如:分开的训练集、验证集和测试集)及它们所用的词表和词嵌入。 @@ -95,7 +127,7 @@ class DataInfo: :param embeddings: 从名称(字符串)到一系列 embedding 的dict,参考 :class:`~fastNLP.io.EmbedLoader` :param datasets: 从名称(字符串)到 :class:`~fastNLP.DataSet` 类型的dict """ - + def __init__(self, vocabs: dict = None, embeddings: dict = None, datasets: dict = None): self.vocabs = vocabs or {} self.embeddings = embeddings or {} @@ -106,21 +138,21 @@ class DataSetLoader: """ 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` - 定义了各种 DataSetLoader (针对特定数据上的特定任务) 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 - + 定义了各种 DataSetLoader 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 + 开发者至少应该编写如下内容: - + - _load 函数:从一个数据文件中读取数据到一个 :class:`~fastNLP.DataSet` - load 函数(可以使用基类的方法):从一个或多个数据文件中读取数据到一个或多个 :class:`~fastNLP.DataSet` - process 函数:一个或多个从数据文件中读取数据,并处理成可以训练的一个或多个 :class:`~fastNLP.DataSet` - + **process 函数中可以 调用load 函数或 _load 函数** - + """ - + def _download(self, url: str, path: str, uncompress=True) -> str: """ - + 从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 :param url: 下载的网站 @@ -136,7 +168,7 @@ class DataSetLoader: _uncompress(path, dst) return dst return path - + def load(self, paths: Union[str, Dict[str, str]]) -> Union[DataSet, Dict[str, DataSet]]: """ 从指定一个或多个路径中的文件中读取数据,返回一个或多个数据集 :class:`~fastNLP.DataSet` 。 @@ -148,7 +180,7 @@ class DataSetLoader: if isinstance(paths, str): return self._load(paths) return {name: self._load(path) for name, path in paths.items()} - + def _load(self, path: str) -> DataSet: """从指定路径的文件中读取数据,返回 :class:`~fastNLP.DataSet` 类型的对象 @@ -156,16 +188,16 @@ class DataSetLoader: :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 """ raise NotImplementedError - + def process(self, paths: Union[str, Dict[str, str]], **options) -> DataInfo: """ 对于特定的任务和数据集,读取并处理数据,返回处理DataInfo类对象或字典。 - + 从指定一个或多个路径中的文件中读取数据,DataInfo对象中可以包含一个或多个数据集 。 如果处理多个路径,传入的 dict 的 key 与返回DataInfo中的 dict 中的 key 保存一致。 返回的 :class:`DataInfo` 对象有如下属性: - + - vocabs: 由从数据集中获取的词表组成的字典,每个词表 - embeddings: (可选) 数据集对应的词嵌入 - datasets: 一个dict,包含一系列 :class:`~fastNLP.DataSet` 类型的对象。其中 field 的命名参考 :mod:`~fastNLP.core.const` @@ -183,12 +215,12 @@ class PeopleDailyCorpusLoader(DataSetLoader): 读取人民日报数据集 """ - + def __init__(self, pos=True, ner=True): super(PeopleDailyCorpusLoader, self).__init__() self.pos = pos self.ner = ner - + def _load(self, data_path): with open(data_path, "r", encoding="utf-8") as f: sents = f.readlines() @@ -233,7 +265,7 @@ class PeopleDailyCorpusLoader(DataSetLoader): example.append(sent_ner) examples.append(example) return self.convert(examples) - + def convert(self, data): """ @@ -284,7 +316,7 @@ class ConllLoader(DataSetLoader): :param indexes: 需要保留的数据列下标,从0开始。若为 ``None`` ,则所有列都保留。Default: ``None`` :param dropna: 是否忽略非法数据,若 ``False`` ,遇到非法数据时抛出 ``ValueError`` 。Default: ``False`` """ - + def __init__(self, headers, indexes=None, dropna=False): super(ConllLoader, self).__init__() if not isinstance(headers, (list, tuple)): @@ -298,7 +330,7 @@ class ConllLoader(DataSetLoader): if len(indexes) != len(headers): raise ValueError self.indexes = indexes - + def _load(self, path): ds = DataSet() for idx, data in _read_conll(path, indexes=self.indexes, dropna=self.dropna): @@ -316,7 +348,7 @@ class Conll2003Loader(ConllLoader): 关于数据集的更多信息,参考: https://sites.google.com/site/ermasoftware/getting-started/ne-tagging-conll2003-data """ - + def __init__(self): headers = [ 'tokens', 'pos', 'chunks', 'ner', @@ -368,17 +400,17 @@ class SSTLoader(DataSetLoader): :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` """ - + def __init__(self, subtree=False, fine_grained=False): self.subtree = subtree - + tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', '3': 'positive', '4': 'very positive'} if not fine_grained: tag_v['0'] = tag_v['1'] tag_v['4'] = tag_v['3'] self.tag_v = tag_v - + def _load(self, path): """ @@ -395,7 +427,7 @@ class SSTLoader(DataSetLoader): for words, tag in datas: ds.append(Instance(words=words, target=tag)) return ds - + @staticmethod def _get_one(data, subtree): tree = Tree.fromstring(data) @@ -403,6 +435,40 @@ class SSTLoader(DataSetLoader): return [(t.leaves(), t.label()) for t in tree.subtrees()] return [(tree.leaves(), tree.label())] + def process(self, + paths, + train_ds: Iterable[str] = None, + src_vocab_op: VocabularyOption = None, + tgt_vocab_op: VocabularyOption = None, + embed_op: EmbeddingOption = None): + input_name, target_name = 'words', 'target' + src_vocab = Vocabulary() if src_vocab_op is None else Vocabulary(**src_vocab_op) + tgt_vocab = Vocabulary() if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) + + info = DataInfo(datasets=self.load(paths)) + _train_ds = [info.datasets[name] + for name in train_ds] if train_ds else info.datasets.values() + + src_vocab.from_dataset(_train_ds, field_name=input_name) + tgt_vocab.from_dataset(_train_ds, field_name=target_name) + src_vocab.index_dataset( + info.datasets.values(), + field_name=input_name, new_field_name=input_name) + tgt_vocab.index_dataset( + info.datasets.values(), + field_name=target_name, new_field_name=target_name) + info.vocabs = { + input_name: src_vocab, + target_name: tgt_vocab + } + + if embed_op is not None: + embed_op.vocab = src_vocab + init_emb = EmbedLoader.load_with_vocab(**embed_op) + info.embeddings[input_name] = init_emb + + return info + class JsonLoader(DataSetLoader): """ @@ -417,7 +483,7 @@ class JsonLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, fields=None, dropna=False): super(JsonLoader, self).__init__() self.dropna = dropna @@ -428,7 +494,7 @@ class JsonLoader(DataSetLoader): for k, v in fields.items(): self.fields[k] = k if v is None else v self.fields_list = list(self.fields.keys()) - + def _load(self, path): ds = DataSet() for idx, d in _read_json(path, fields=self.fields_list, dropna=self.dropna): @@ -452,7 +518,7 @@ class SNLILoader(JsonLoader): 数据来源: https://nlp.stanford.edu/projects/snli/snli_1.0.zip """ - + def __init__(self): fields = { 'sentence1_parse': 'words1', @@ -460,14 +526,14 @@ class SNLILoader(JsonLoader): 'gold_label': 'target', } super(SNLILoader, self).__init__(fields=fields) - + def _load(self, path): ds = super(SNLILoader, self)._load(path) - + def parse_tree(x): t = Tree.fromstring(x) return t.leaves() - + ds.apply(lambda ins: parse_tree( ins['words1']), new_field_name='words1') ds.apply(lambda ins: parse_tree( @@ -488,12 +554,12 @@ class CSVLoader(DataSetLoader): :param bool dropna: 是否忽略非法数据,若 ``True`` 则忽略,若 ``False`` ,在遇到非法数据时,抛出 ``ValueError`` . Default: ``False`` """ - + def __init__(self, headers=None, sep=",", dropna=False): self.headers = headers self.sep = sep self.dropna = dropna - + def _load(self, path): ds = DataSet() for idx, data in _read_csv(path, headers=self.headers, @@ -508,7 +574,7 @@ def _add_seg_tag(data): :param data: list of ([word], [pos], [heads], [head_tags]) :return: list of ([word], [pos]) """ - + _processed = [] for word_list, pos_list, _, _ in data: new_sample = [] From e206cae45c8499d9c8f3c5793a725d8592625ba9 Mon Sep 17 00:00:00 2001 From: yunfan Date: Tue, 28 May 2019 17:02:07 +0800 Subject: [PATCH 167/173] - update dataloader --- fastNLP/io/dataset_loader.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 40604deb..8c697e4a 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -22,6 +22,8 @@ __all__ = [ 'SSTLoader', 'PeopleDailyCorpusLoader', 'Conll2003Loader', + 'VocabularyOption', + 'EmbeddingOption', ] from nltk.tree import Tree @@ -32,8 +34,8 @@ from .file_reader import _read_csv, _read_json, _read_conll from typing import Union, Dict, Iterable import os from ..core.utils import Example -from ..core import Vocabulary -from ..io import EmbedLoader +from ..core.vocabulary import Vocabulary +from ..io.embed_loader import EmbedLoader import numpy as np @@ -448,14 +450,13 @@ class SSTLoader(DataSetLoader): info = DataInfo(datasets=self.load(paths)) _train_ds = [info.datasets[name] for name in train_ds] if train_ds else info.datasets.values() - - src_vocab.from_dataset(_train_ds, field_name=input_name) - tgt_vocab.from_dataset(_train_ds, field_name=target_name) + src_vocab.from_dataset(*_train_ds, field_name=input_name) + tgt_vocab.from_dataset(*_train_ds, field_name=target_name) src_vocab.index_dataset( - info.datasets.values(), + *info.datasets.values(), field_name=input_name, new_field_name=input_name) tgt_vocab.index_dataset( - info.datasets.values(), + *info.datasets.values(), field_name=target_name, new_field_name=target_name) info.vocabs = { input_name: src_vocab, From 016f02be3b3db52d08790f8e053fcea99858e85d Mon Sep 17 00:00:00 2001 From: xuyige Date: Wed, 29 May 2019 14:46:48 +0800 Subject: [PATCH 168/173] fix bugs in model/bert.py and add testing codes --- fastNLP/models/bert.py | 71 ++++++++++++++++++++++++++----- test/models/test_bert.py | 60 ++++++++++++++++++++++---- test/modules/encoder/test_bert.py | 21 +++++++++ 3 files changed, 133 insertions(+), 19 deletions(-) create mode 100644 test/modules/encoder/test_bert.py diff --git a/fastNLP/models/bert.py b/fastNLP/models/bert.py index 960132ad..02227c0d 100644 --- a/fastNLP/models/bert.py +++ b/fastNLP/models/bert.py @@ -10,6 +10,35 @@ from ..core.const import Const from ..modules.encoder import BertModel +class BertConfig: + + def __init__( + self, + vocab_size=30522, + hidden_size=768, + num_hidden_layers=12, + num_attention_heads=12, + intermediate_size=3072, + hidden_act="gelu", + hidden_dropout_prob=0.1, + attention_probs_dropout_prob=0.1, + max_position_embeddings=512, + type_vocab_size=2, + initializer_range=0.02 + ): + self.vocab_size = vocab_size + self.hidden_size = hidden_size + self.num_hidden_layers = num_hidden_layers + self.num_attention_heads = num_attention_heads + self.intermediate = intermediate_size + self.hidden_act = hidden_act + self.hidden_dropout_prob = hidden_dropout_prob + self.attention_probs_dropout_prob = attention_probs_dropout_prob + self.max_position_embeddings = max_position_embeddings + self.type_vocab_size = type_vocab_size + self.initializer_range = initializer_range + + class BertForSequenceClassification(BaseModel): """BERT model for classification. This module is composed of the BERT model with a linear layer on top of @@ -44,14 +73,19 @@ class BertForSequenceClassification(BaseModel): config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) num_labels = 2 - model = BertForSequenceClassification(config, num_labels) + model = BertForSequenceClassification(num_labels, config) logits = model(input_ids, token_type_ids, input_mask) ``` """ - def __init__(self, config, num_labels, bert_dir): + def __init__(self, num_labels, config=None, bert_dir=None): super(BertForSequenceClassification, self).__init__() self.num_labels = num_labels - self.bert = BertModel.from_pretrained(bert_dir) + if bert_dir is not None: + self.bert = BertModel.from_pretrained(bert_dir) + else: + if config is None: + config = BertConfig() + self.bert = BertModel(**config.__dict__) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, num_labels) @@ -106,14 +140,19 @@ class BertForMultipleChoice(BaseModel): config = BertConfig(vocab_size_or_config_json_file=32000, hidden_size=768, num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) num_choices = 2 - model = BertForMultipleChoice(config, num_choices, bert_dir) + model = BertForMultipleChoice(num_choices, config, bert_dir) logits = model(input_ids, token_type_ids, input_mask) ``` """ - def __init__(self, config, num_choices, bert_dir): + def __init__(self, num_choices, config=None, bert_dir=None): super(BertForMultipleChoice, self).__init__() self.num_choices = num_choices - self.bert = BertModel.from_pretrained(bert_dir) + if bert_dir is not None: + self.bert = BertModel.from_pretrained(bert_dir) + else: + if config is None: + config = BertConfig() + self.bert = BertModel(**config.__dict__) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, 1) @@ -174,14 +213,19 @@ class BertForTokenClassification(BaseModel): num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) num_labels = 2 bert_dir = 'your-bert-file-dir' - model = BertForTokenClassification(config, num_labels, bert_dir) + model = BertForTokenClassification(num_labels, config, bert_dir) logits = model(input_ids, token_type_ids, input_mask) ``` """ - def __init__(self, config, num_labels, bert_dir): + def __init__(self, num_labels, config=None, bert_dir=None): super(BertForTokenClassification, self).__init__() self.num_labels = num_labels - self.bert = BertModel.from_pretrained(bert_dir) + if bert_dir is not None: + self.bert = BertModel.from_pretrained(bert_dir) + else: + if config is None: + config = BertConfig() + self.bert = BertModel(**config.__dict__) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, num_labels) @@ -252,9 +296,14 @@ class BertForQuestionAnswering(BaseModel): start_logits, end_logits = model(input_ids, token_type_ids, input_mask) ``` """ - def __init__(self, config, bert_dir): + def __init__(self, config=None, bert_dir=None): super(BertForQuestionAnswering, self).__init__() - self.bert = BertModel.from_pretrained(bert_dir) + if bert_dir is not None: + self.bert = BertModel.from_pretrained(bert_dir) + else: + if config is None: + config = BertConfig() + self.bert = BertModel(**config.__dict__) # TODO check with Google if it's normal there is no dropout on the token classifier of SQuAD in the TF version # self.dropout = nn.Dropout(config.hidden_dropout_prob) self.qa_outputs = nn.Linear(config.hidden_size, 2) diff --git a/test/models/test_bert.py b/test/models/test_bert.py index b2899a89..7177f31b 100644 --- a/test/models/test_bert.py +++ b/test/models/test_bert.py @@ -2,20 +2,64 @@ import unittest import torch -from fastNLP.models.bert import BertModel +from fastNLP.models.bert import * class TestBert(unittest.TestCase): def test_bert_1(self): - # model = BertModel.from_pretrained("/home/zyfeng/data/bert-base-chinese") - model = BertModel(vocab_size=32000, hidden_size=768, - num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + from fastNLP.core.const import Const + + model = BertForSequenceClassification(2) + + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + + pred = model(input_ids, token_type_ids, input_mask) + self.assertTrue(isinstance(pred, dict)) + self.assertTrue(Const.OUTPUT in pred) + self.assertEqual(tuple(pred[Const.OUTPUT].shape), (2, 2)) + + def test_bert_2(self): + from fastNLP.core.const import Const + + model = BertForMultipleChoice(2) + + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + + pred = model(input_ids, token_type_ids, input_mask) + self.assertTrue(isinstance(pred, dict)) + self.assertTrue(Const.OUTPUT in pred) + self.assertEqual(tuple(pred[Const.OUTPUT].shape), (1, 2)) + + def test_bert_3(self): + from fastNLP.core.const import Const + + model = BertForTokenClassification(7) + + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + + pred = model(input_ids, token_type_ids, input_mask) + self.assertTrue(isinstance(pred, dict)) + self.assertTrue(Const.OUTPUT in pred) + self.assertEqual(tuple(pred[Const.OUTPUT].shape), (2, 3, 7)) + + def test_bert_4(self): + from fastNLP.core.const import Const + + model = BertForQuestionAnswering() input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) - all_encoder_layers, pooled_output = model(input_ids, token_type_ids, input_mask) - for layer in all_encoder_layers: - self.assertEqual(tuple(layer.shape), (2, 3, 768)) - self.assertEqual(tuple(pooled_output.shape), (2, 768)) + pred = model(input_ids, token_type_ids, input_mask) + self.assertTrue(isinstance(pred, dict)) + self.assertTrue(Const.OUTPUTS(0) in pred) + self.assertTrue(Const.OUTPUTS(1) in pred) + self.assertEqual(tuple(pred[Const.OUTPUTS(0)].shape), (2, 3)) + self.assertEqual(tuple(pred[Const.OUTPUTS(1)].shape), (2, 3)) diff --git a/test/modules/encoder/test_bert.py b/test/modules/encoder/test_bert.py new file mode 100644 index 00000000..78bcf633 --- /dev/null +++ b/test/modules/encoder/test_bert.py @@ -0,0 +1,21 @@ + +import unittest + +import torch + +from fastNLP.models.bert import BertModel + + +class TestBert(unittest.TestCase): + def test_bert_1(self): + model = BertModel(vocab_size=32000, hidden_size=768, + num_hidden_layers=12, num_attention_heads=12, intermediate_size=3072) + + input_ids = torch.LongTensor([[31, 51, 99], [15, 5, 0]]) + input_mask = torch.LongTensor([[1, 1, 1], [1, 1, 0]]) + token_type_ids = torch.LongTensor([[0, 0, 1], [0, 1, 0]]) + + all_encoder_layers, pooled_output = model(input_ids, token_type_ids, input_mask) + for layer in all_encoder_layers: + self.assertEqual(tuple(layer.shape), (2, 3, 768)) + self.assertEqual(tuple(pooled_output.shape), (2, 768)) \ No newline at end of file From 9a5cc3801c48e9836cee74333ab135855a34e8c7 Mon Sep 17 00:00:00 2001 From: yunfan Date: Thu, 30 May 2019 14:27:12 +0800 Subject: [PATCH 169/173] - update sst data loader - add Option --- fastNLP/core/vocabulary.py | 19 ++++++++-- fastNLP/io/dataset_loader.py | 68 ++++++++++++++---------------------- fastNLP/io/embed_loader.py | 17 ++++++++- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/fastNLP/core/vocabulary.py b/fastNLP/core/vocabulary.py index cbde9cba..0cf45049 100644 --- a/fastNLP/core/vocabulary.py +++ b/fastNLP/core/vocabulary.py @@ -1,11 +1,26 @@ __all__ = [ - "Vocabulary" + "Vocabulary", + "VocabularyOption", ] from functools import wraps from collections import Counter - from .dataset import DataSet +from .utils import Example + + +class VocabularyOption(Example): + def __init__(self, + max_size=None, + min_freq=None, + padding='', + unknown=''): + super().__init__( + max_size=max_size, + min_freq=min_freq, + padding=padding, + unknown=unknown + ) def _check_build_vocab(func): diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 8c697e4a..9ad5dff8 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -22,8 +22,6 @@ __all__ = [ 'SSTLoader', 'PeopleDailyCorpusLoader', 'Conll2003Loader', - 'VocabularyOption', - 'EmbeddingOption', ] from nltk.tree import Tree @@ -37,6 +35,8 @@ from ..core.utils import Example from ..core.vocabulary import Vocabulary from ..io.embed_loader import EmbedLoader import numpy as np +from ..core.vocabulary import VocabularyOption +from .embed_loader import EmbeddingOption def _download_from_url(url, path): @@ -56,7 +56,6 @@ def _download_from_url(url, path): if chunk: file.write(chunk) t.update(len(chunk)) - return def _uncompress(src, dst): @@ -93,34 +92,6 @@ def _uncompress(src, dst): raise ValueError('unsupported file {}'.format(src)) -class VocabularyOption(Example): - def __init__(self, - max_size=None, - min_freq=None, - padding='', - unknown=''): - super().__init__( - max_size=max_size, - min_freq=min_freq, - padding=padding, - unknown=unknown - ) - - -class EmbeddingOption(Example): - def __init__(self, - embed_filepath=None, - dtype=np.float32, - normalize=True, - error='ignore'): - super().__init__( - embed_filepath=embed_filepath, - dtype=dtype, - normalize=normalize, - error=error - ) - - class DataInfo: """ 经过处理的数据信息,包括一系列数据集(比如:分开的训练集、验证集和测试集)及它们所用的词表和词嵌入。 @@ -151,26 +122,41 @@ class DataSetLoader: **process 函数中可以 调用load 函数或 _load 函数** """ + URL = '' + DATA_DIR = '' - def _download(self, url: str, path: str, uncompress=True) -> str: + ROOT_DIR = '.fastnlp/datasets/' + UNCOMPRESS = True + + def _download(self, url: str, pdir: str, uncompress=True) -> str: """ 从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 :param url: 下载的网站 - :param path: 下载到的目录 + :param pdir: 下载到的目录 :param uncompress: 是否自动解压缩 :return: 数据的存放路径 """ - pdir = os.path.dirname(path) - os.makedirs(pdir, exist_ok=True) - _download_from_url(url, path) + fn = os.path.basename(url) + path = os.path.join(pdir, fn) + """check data exists""" + if not os.path.exists(path): + os.makedirs(pdir, exist_ok=True) + _download_from_url(url, path) if uncompress: dst = os.path.join(pdir, 'data') - _uncompress(path, dst) + if not os.path.exists(dst): + _uncompress(path, dst) return dst return path + def download(self): + return self._download( + self.URL, + os.path.join(self.ROOT_DIR, self.DATA_DIR), + uncompress=self.UNCOMPRESS) + def load(self, paths: Union[str, Dict[str, str]]) -> Union[DataSet, Dict[str, DataSet]]: """ 从指定一个或多个路径中的文件中读取数据,返回一个或多个数据集 :class:`~fastNLP.DataSet` 。 @@ -442,7 +428,7 @@ class SSTLoader(DataSetLoader): train_ds: Iterable[str] = None, src_vocab_op: VocabularyOption = None, tgt_vocab_op: VocabularyOption = None, - embed_op: EmbeddingOption = None): + src_embed_op: EmbeddingOption = None): input_name, target_name = 'words', 'target' src_vocab = Vocabulary() if src_vocab_op is None else Vocabulary(**src_vocab_op) tgt_vocab = Vocabulary() if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) @@ -463,9 +449,9 @@ class SSTLoader(DataSetLoader): target_name: tgt_vocab } - if embed_op is not None: - embed_op.vocab = src_vocab - init_emb = EmbedLoader.load_with_vocab(**embed_op) + if src_embed_op is not None: + src_embed_op.vocab = src_vocab + init_emb = EmbedLoader.load_with_vocab(**src_embed_op) info.embeddings[input_name] = init_emb return info diff --git a/fastNLP/io/embed_loader.py b/fastNLP/io/embed_loader.py index fb024e73..93861258 100644 --- a/fastNLP/io/embed_loader.py +++ b/fastNLP/io/embed_loader.py @@ -1,5 +1,6 @@ __all__ = [ - "EmbedLoader" + "EmbedLoader", + "EmbeddingOption", ] import os @@ -9,8 +10,22 @@ import numpy as np from ..core.vocabulary import Vocabulary from .base_loader import BaseLoader +from ..core.utils import Example +class EmbeddingOption(Example): + def __init__(self, + embed_filepath=None, + dtype=np.float32, + normalize=True, + error='ignore'): + super().__init__( + embed_filepath=embed_filepath, + dtype=dtype, + normalize=normalize, + error=error + ) + class EmbedLoader(BaseLoader): """ 别名::class:`fastNLP.io.EmbedLoader` :class:`fastNLP.io.embed_loader.EmbedLoader` From 7fea175f0a5ff48452821e45ad40a4ca515356fb Mon Sep 17 00:00:00 2001 From: yunfan Date: Thu, 30 May 2019 14:46:22 +0800 Subject: [PATCH 170/173] move sst loader to new folder --- fastNLP/io/base_loader.py | 187 ++++++++++++++++++++++--- fastNLP/io/data_loader/sst.py | 91 ++++++++++++ fastNLP/io/dataset_loader.py | 257 +--------------------------------- 3 files changed, 258 insertions(+), 277 deletions(-) create mode 100644 fastNLP/io/data_loader/sst.py diff --git a/fastNLP/io/base_loader.py b/fastNLP/io/base_loader.py index 4ab1e2d0..adfa8ca1 100644 --- a/fastNLP/io/base_loader.py +++ b/fastNLP/io/base_loader.py @@ -1,10 +1,14 @@ __all__ = [ - "BaseLoader" + "BaseLoader", + 'DataInfo', + 'DataSetLoader', ] import _pickle as pickle import os - +from typing import Union, Dict +import os +from ..core.dataset import DataSet class BaseLoader(object): """ @@ -51,24 +55,161 @@ class BaseLoader(object): return obj -class DataLoaderRegister: - _readers = {} - - @classmethod - def set_reader(cls, reader_cls, read_fn_name): - # def wrapper(reader_cls): - if read_fn_name in cls._readers: - raise KeyError( - 'duplicate reader: {} and {} for read_func: {}'.format(cls._readers[read_fn_name], reader_cls, - read_fn_name)) - if hasattr(reader_cls, 'load'): - cls._readers[read_fn_name] = reader_cls().load - return reader_cls - - @classmethod - def get_reader(cls, read_fn_name): - if read_fn_name in cls._readers: - return cls._readers[read_fn_name] - raise AttributeError('no read function: {}'.format(read_fn_name)) - - # TODO 这个类使用在何处? + + +def _download_from_url(url, path): + try: + from tqdm.auto import tqdm + except: + from ..core.utils import _pseudo_tqdm as tqdm + import requests + + """Download file""" + r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) + chunk_size = 16 * 1024 + total_size = int(r.headers.get('Content-length', 0)) + with open(path, "wb") as file, \ + tqdm(total=total_size, unit='B', unit_scale=1, desc=path.split('/')[-1]) as t: + for chunk in r.iter_content(chunk_size): + if chunk: + file.write(chunk) + t.update(len(chunk)) + + +def _uncompress(src, dst): + import zipfile + import gzip + import tarfile + import os + + def unzip(src, dst): + with zipfile.ZipFile(src, 'r') as f: + f.extractall(dst) + + def ungz(src, dst): + with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: + length = 16 * 1024 # 16KB + buf = f.read(length) + while buf: + uf.write(buf) + buf = f.read(length) + + def untar(src, dst): + with tarfile.open(src, 'r:gz') as f: + f.extractall(dst) + + fn, ext = os.path.splitext(src) + _, ext_2 = os.path.splitext(fn) + if ext == '.zip': + unzip(src, dst) + elif ext == '.gz' and ext_2 != '.tar': + ungz(src, dst) + elif (ext == '.gz' and ext_2 == '.tar') or ext_2 == '.tgz': + untar(src, dst) + else: + raise ValueError('unsupported file {}'.format(src)) + + +class DataInfo: + """ + 经过处理的数据信息,包括一系列数据集(比如:分开的训练集、验证集和测试集)及它们所用的词表和词嵌入。 + + :param vocabs: 从名称(字符串)到 :class:`~fastNLP.Vocabulary` 类型的dict + :param embeddings: 从名称(字符串)到一系列 embedding 的dict,参考 :class:`~fastNLP.io.EmbedLoader` + :param datasets: 从名称(字符串)到 :class:`~fastNLP.DataSet` 类型的dict + """ + + def __init__(self, vocabs: dict = None, embeddings: dict = None, datasets: dict = None): + self.vocabs = vocabs or {} + self.embeddings = embeddings or {} + self.datasets = datasets or {} + + +class DataSetLoader: + """ + 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` + + 定义了各种 DataSetLoader 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 + + 开发者至少应该编写如下内容: + + - _load 函数:从一个数据文件中读取数据到一个 :class:`~fastNLP.DataSet` + - load 函数(可以使用基类的方法):从一个或多个数据文件中读取数据到一个或多个 :class:`~fastNLP.DataSet` + - process 函数:一个或多个从数据文件中读取数据,并处理成可以训练的一个或多个 :class:`~fastNLP.DataSet` + + **process 函数中可以 调用load 函数或 _load 函数** + + """ + URL = '' + DATA_DIR = '' + + ROOT_DIR = '.fastnlp/datasets/' + UNCOMPRESS = True + + def _download(self, url: str, pdir: str, uncompress=True) -> str: + """ + + 从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 + + :param url: 下载的网站 + :param pdir: 下载到的目录 + :param uncompress: 是否自动解压缩 + :return: 数据的存放路径 + """ + fn = os.path.basename(url) + path = os.path.join(pdir, fn) + """check data exists""" + if not os.path.exists(path): + os.makedirs(pdir, exist_ok=True) + _download_from_url(url, path) + if uncompress: + dst = os.path.join(pdir, 'data') + if not os.path.exists(dst): + _uncompress(path, dst) + return dst + return path + + def download(self): + return self._download( + self.URL, + os.path.join(self.ROOT_DIR, self.DATA_DIR), + uncompress=self.UNCOMPRESS) + + def load(self, paths: Union[str, Dict[str, str]]) -> Union[DataSet, Dict[str, DataSet]]: + """ + 从指定一个或多个路径中的文件中读取数据,返回一个或多个数据集 :class:`~fastNLP.DataSet` 。 + 如果处理多个路径,传入的 dict 中的 key 与返回的 dict 中的 key 保存一致。 + + :param Union[str, Dict[str, str]] paths: 文件路径 + :return: :class:`~fastNLP.DataSet` 类的对象或存储多个 :class:`~fastNLP.DataSet` 的字典 + """ + if isinstance(paths, str): + return self._load(paths) + return {name: self._load(path) for name, path in paths.items()} + + def _load(self, path: str) -> DataSet: + """从指定路径的文件中读取数据,返回 :class:`~fastNLP.DataSet` 类型的对象 + + :param str path: 文件路径 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 + """ + raise NotImplementedError + + def process(self, paths: Union[str, Dict[str, str]], **options) -> DataInfo: + """ + 对于特定的任务和数据集,读取并处理数据,返回处理DataInfo类对象或字典。 + + 从指定一个或多个路径中的文件中读取数据,DataInfo对象中可以包含一个或多个数据集 。 + 如果处理多个路径,传入的 dict 的 key 与返回DataInfo中的 dict 中的 key 保存一致。 + + 返回的 :class:`DataInfo` 对象有如下属性: + + - vocabs: 由从数据集中获取的词表组成的字典,每个词表 + - embeddings: (可选) 数据集对应的词嵌入 + - datasets: 一个dict,包含一系列 :class:`~fastNLP.DataSet` 类型的对象。其中 field 的命名参考 :mod:`~fastNLP.core.const` + + :param paths: 原始数据读取的路径 + :param options: 根据不同的任务和数据集,设计自己的参数 + :return: 返回一个 DataInfo + """ + raise NotImplementedError diff --git a/fastNLP/io/data_loader/sst.py b/fastNLP/io/data_loader/sst.py new file mode 100644 index 00000000..1410f122 --- /dev/null +++ b/fastNLP/io/data_loader/sst.py @@ -0,0 +1,91 @@ +from typing import Iterable +from nltk import Tree +from ..base_loader import DataInfo, DataSetLoader +from ...core.vocabulary import VocabularyOption, Vocabulary +from ...core.dataset import DataSet +from ...core.instance import Instance +from ..embed_loader import EmbeddingOption, EmbedLoader + + +class SSTLoader(DataSetLoader): + """ + 别名::class:`fastNLP.io.SSTLoader` :class:`fastNLP.io.dataset_loader.SSTLoader` + + 读取SST数据集, DataSet包含fields:: + + words: list(str) 需要分类的文本 + target: str 文本的标签 + + 数据来源: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip + + :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` + :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` + """ + + def __init__(self, subtree=False, fine_grained=False): + self.subtree = subtree + + tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', + '3': 'positive', '4': 'very positive'} + if not fine_grained: + tag_v['0'] = tag_v['1'] + tag_v['4'] = tag_v['3'] + self.tag_v = tag_v + + def _load(self, path): + """ + + :param str path: 存储数据的路径 + :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 + """ + datalist = [] + with open(path, 'r', encoding='utf-8') as f: + datas = [] + for l in f: + datas.extend([(s, self.tag_v[t]) + for s, t in self._get_one(l, self.subtree)]) + ds = DataSet() + for words, tag in datas: + ds.append(Instance(words=words, target=tag)) + return ds + + @staticmethod + def _get_one(data, subtree): + tree = Tree.fromstring(data) + if subtree: + return [(t.leaves(), t.label()) for t in tree.subtrees()] + return [(tree.leaves(), tree.label())] + + def process(self, + paths, + train_ds: Iterable[str] = None, + src_vocab_op: VocabularyOption = None, + tgt_vocab_op: VocabularyOption = None, + src_embed_op: EmbeddingOption = None): + input_name, target_name = 'words', 'target' + src_vocab = Vocabulary() if src_vocab_op is None else Vocabulary(**src_vocab_op) + tgt_vocab = Vocabulary() if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) + + info = DataInfo(datasets=self.load(paths)) + _train_ds = [info.datasets[name] + for name in train_ds] if train_ds else info.datasets.values() + src_vocab.from_dataset(*_train_ds, field_name=input_name) + tgt_vocab.from_dataset(*_train_ds, field_name=target_name) + src_vocab.index_dataset( + *info.datasets.values(), + field_name=input_name, new_field_name=input_name) + tgt_vocab.index_dataset( + *info.datasets.values(), + field_name=target_name, new_field_name=target_name) + info.vocabs = { + input_name: src_vocab, + target_name: tgt_vocab + } + + if src_embed_op is not None: + src_embed_op.vocab = src_vocab + init_emb = EmbedLoader.load_with_vocab(**src_embed_op) + info.embeddings[input_name] = init_emb + + return info + diff --git a/fastNLP/io/dataset_loader.py b/fastNLP/io/dataset_loader.py index 9ad5dff8..d175d3b9 100644 --- a/fastNLP/io/dataset_loader.py +++ b/fastNLP/io/dataset_loader.py @@ -13,8 +13,6 @@ dataset_loader模块实现了许多 DataSetLoader, 用于读取不同格式的 为 fastNLP 提供 DataSetLoader 的开发者请参考 :class:`~fastNLP.io.DataSetLoader` 的介绍。 """ __all__ = [ - 'DataInfo', - 'DataSetLoader', 'CSVLoader', 'JsonLoader', 'ConllLoader', @@ -24,178 +22,12 @@ __all__ = [ 'Conll2003Loader', ] -from nltk.tree import Tree - +from nltk import Tree from ..core.dataset import DataSet from ..core.instance import Instance from .file_reader import _read_csv, _read_json, _read_conll -from typing import Union, Dict, Iterable -import os -from ..core.utils import Example -from ..core.vocabulary import Vocabulary -from ..io.embed_loader import EmbedLoader -import numpy as np -from ..core.vocabulary import VocabularyOption -from .embed_loader import EmbeddingOption - - -def _download_from_url(url, path): - try: - from tqdm.auto import tqdm - except: - from ..core.utils import _pseudo_tqdm as tqdm - import requests - - """Download file""" - r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0'}, stream=True) - chunk_size = 16 * 1024 - total_size = int(r.headers.get('Content-length', 0)) - with open(path, "wb") as file, \ - tqdm(total=total_size, unit='B', unit_scale=1, desc=path.split('/')[-1]) as t: - for chunk in r.iter_content(chunk_size): - if chunk: - file.write(chunk) - t.update(len(chunk)) - - -def _uncompress(src, dst): - import zipfile - import gzip - import tarfile - import os - - def unzip(src, dst): - with zipfile.ZipFile(src, 'r') as f: - f.extractall(dst) - - def ungz(src, dst): - with gzip.open(src, 'rb') as f, open(dst, 'wb') as uf: - length = 16 * 1024 # 16KB - buf = f.read(length) - while buf: - uf.write(buf) - buf = f.read(length) - - def untar(src, dst): - with tarfile.open(src, 'r:gz') as f: - f.extractall(dst) - - fn, ext = os.path.splitext(src) - _, ext_2 = os.path.splitext(fn) - if ext == '.zip': - unzip(src, dst) - elif ext == '.gz' and ext_2 != '.tar': - ungz(src, dst) - elif (ext == '.gz' and ext_2 == '.tar') or ext_2 == '.tgz': - untar(src, dst) - else: - raise ValueError('unsupported file {}'.format(src)) - - -class DataInfo: - """ - 经过处理的数据信息,包括一系列数据集(比如:分开的训练集、验证集和测试集)及它们所用的词表和词嵌入。 - - :param vocabs: 从名称(字符串)到 :class:`~fastNLP.Vocabulary` 类型的dict - :param embeddings: 从名称(字符串)到一系列 embedding 的dict,参考 :class:`~fastNLP.io.EmbedLoader` - :param datasets: 从名称(字符串)到 :class:`~fastNLP.DataSet` 类型的dict - """ - - def __init__(self, vocabs: dict = None, embeddings: dict = None, datasets: dict = None): - self.vocabs = vocabs or {} - self.embeddings = embeddings or {} - self.datasets = datasets or {} - - -class DataSetLoader: - """ - 别名::class:`fastNLP.io.DataSetLoader` :class:`fastNLP.io.dataset_loader.DataSetLoader` - - 定义了各种 DataSetLoader 所需的API 接口,开发者应该继承它实现各种的 DataSetLoader。 - - 开发者至少应该编写如下内容: - - - _load 函数:从一个数据文件中读取数据到一个 :class:`~fastNLP.DataSet` - - load 函数(可以使用基类的方法):从一个或多个数据文件中读取数据到一个或多个 :class:`~fastNLP.DataSet` - - process 函数:一个或多个从数据文件中读取数据,并处理成可以训练的一个或多个 :class:`~fastNLP.DataSet` - - **process 函数中可以 调用load 函数或 _load 函数** - - """ - URL = '' - DATA_DIR = '' - - ROOT_DIR = '.fastnlp/datasets/' - UNCOMPRESS = True - - def _download(self, url: str, pdir: str, uncompress=True) -> str: - """ - - 从 ``url`` 下载数据到 ``path``, 如果 ``uncompress`` 为 ``True`` ,自动解压。 - - :param url: 下载的网站 - :param pdir: 下载到的目录 - :param uncompress: 是否自动解压缩 - :return: 数据的存放路径 - """ - fn = os.path.basename(url) - path = os.path.join(pdir, fn) - """check data exists""" - if not os.path.exists(path): - os.makedirs(pdir, exist_ok=True) - _download_from_url(url, path) - if uncompress: - dst = os.path.join(pdir, 'data') - if not os.path.exists(dst): - _uncompress(path, dst) - return dst - return path - - def download(self): - return self._download( - self.URL, - os.path.join(self.ROOT_DIR, self.DATA_DIR), - uncompress=self.UNCOMPRESS) - - def load(self, paths: Union[str, Dict[str, str]]) -> Union[DataSet, Dict[str, DataSet]]: - """ - 从指定一个或多个路径中的文件中读取数据,返回一个或多个数据集 :class:`~fastNLP.DataSet` 。 - 如果处理多个路径,传入的 dict 中的 key 与返回的 dict 中的 key 保存一致。 - - :param Union[str, Dict[str, str]] paths: 文件路径 - :return: :class:`~fastNLP.DataSet` 类的对象或存储多个 :class:`~fastNLP.DataSet` 的字典 - """ - if isinstance(paths, str): - return self._load(paths) - return {name: self._load(path) for name, path in paths.items()} - - def _load(self, path: str) -> DataSet: - """从指定路径的文件中读取数据,返回 :class:`~fastNLP.DataSet` 类型的对象 - - :param str path: 文件路径 - :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 - """ - raise NotImplementedError - - def process(self, paths: Union[str, Dict[str, str]], **options) -> DataInfo: - """ - 对于特定的任务和数据集,读取并处理数据,返回处理DataInfo类对象或字典。 - - 从指定一个或多个路径中的文件中读取数据,DataInfo对象中可以包含一个或多个数据集 。 - 如果处理多个路径,传入的 dict 的 key 与返回DataInfo中的 dict 中的 key 保存一致。 - - 返回的 :class:`DataInfo` 对象有如下属性: - - - vocabs: 由从数据集中获取的词表组成的字典,每个词表 - - embeddings: (可选) 数据集对应的词嵌入 - - datasets: 一个dict,包含一系列 :class:`~fastNLP.DataSet` 类型的对象。其中 field 的命名参考 :mod:`~fastNLP.core.const` - - :param paths: 原始数据读取的路径 - :param options: 根据不同的任务和数据集,设计自己的参数 - :return: 返回一个 DataInfo - """ - raise NotImplementedError - +from .base_loader import DataSetLoader +from .data_loader.sst import SSTLoader class PeopleDailyCorpusLoader(DataSetLoader): """ @@ -374,89 +206,6 @@ def _cut_long_sentence(sent, max_sample_length=200): return cutted_sentence -class SSTLoader(DataSetLoader): - """ - 别名::class:`fastNLP.io.SSTLoader` :class:`fastNLP.io.dataset_loader.SSTLoader` - - 读取SST数据集, DataSet包含fields:: - - words: list(str) 需要分类的文本 - target: str 文本的标签 - - 数据来源: https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip - - :param subtree: 是否将数据展开为子树,扩充数据量. Default: ``False`` - :param fine_grained: 是否使用SST-5标准,若 ``False`` , 使用SST-2。Default: ``False`` - """ - - def __init__(self, subtree=False, fine_grained=False): - self.subtree = subtree - - tag_v = {'0': 'very negative', '1': 'negative', '2': 'neutral', - '3': 'positive', '4': 'very positive'} - if not fine_grained: - tag_v['0'] = tag_v['1'] - tag_v['4'] = tag_v['3'] - self.tag_v = tag_v - - def _load(self, path): - """ - - :param str path: 存储数据的路径 - :return: 一个 :class:`~fastNLP.DataSet` 类型的对象 - """ - datalist = [] - with open(path, 'r', encoding='utf-8') as f: - datas = [] - for l in f: - datas.extend([(s, self.tag_v[t]) - for s, t in self._get_one(l, self.subtree)]) - ds = DataSet() - for words, tag in datas: - ds.append(Instance(words=words, target=tag)) - return ds - - @staticmethod - def _get_one(data, subtree): - tree = Tree.fromstring(data) - if subtree: - return [(t.leaves(), t.label()) for t in tree.subtrees()] - return [(tree.leaves(), tree.label())] - - def process(self, - paths, - train_ds: Iterable[str] = None, - src_vocab_op: VocabularyOption = None, - tgt_vocab_op: VocabularyOption = None, - src_embed_op: EmbeddingOption = None): - input_name, target_name = 'words', 'target' - src_vocab = Vocabulary() if src_vocab_op is None else Vocabulary(**src_vocab_op) - tgt_vocab = Vocabulary() if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) - - info = DataInfo(datasets=self.load(paths)) - _train_ds = [info.datasets[name] - for name in train_ds] if train_ds else info.datasets.values() - src_vocab.from_dataset(*_train_ds, field_name=input_name) - tgt_vocab.from_dataset(*_train_ds, field_name=target_name) - src_vocab.index_dataset( - *info.datasets.values(), - field_name=input_name, new_field_name=input_name) - tgt_vocab.index_dataset( - *info.datasets.values(), - field_name=target_name, new_field_name=target_name) - info.vocabs = { - input_name: src_vocab, - target_name: tgt_vocab - } - - if src_embed_op is not None: - src_embed_op.vocab = src_vocab - init_emb = EmbedLoader.load_with_vocab(**src_embed_op) - info.embeddings[input_name] = init_emb - - return info - - class JsonLoader(DataSetLoader): """ 别名::class:`fastNLP.io.JsonLoader` :class:`fastNLP.io.dataset_loader.JsonLoader` From dca1d56602d705a6f86d0eafdb12ad293f1f44a7 Mon Sep 17 00:00:00 2001 From: yunfan Date: Thu, 30 May 2019 14:59:16 +0800 Subject: [PATCH 171/173] update sst loader --- fastNLP/io/data_loader/sst.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastNLP/io/data_loader/sst.py b/fastNLP/io/data_loader/sst.py index 1410f122..73b01959 100644 --- a/fastNLP/io/data_loader/sst.py +++ b/fastNLP/io/data_loader/sst.py @@ -64,7 +64,8 @@ class SSTLoader(DataSetLoader): src_embed_op: EmbeddingOption = None): input_name, target_name = 'words', 'target' src_vocab = Vocabulary() if src_vocab_op is None else Vocabulary(**src_vocab_op) - tgt_vocab = Vocabulary() if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) + tgt_vocab = Vocabulary(unknown=None, padding=None) \ + if tgt_vocab_op is None else Vocabulary(**tgt_vocab_op) info = DataInfo(datasets=self.load(paths)) _train_ds = [info.datasets[name] From 429ff5784610c3541cfbbfc52f6a2edcbaf2a982 Mon Sep 17 00:00:00 2001 From: xuyige Date: Tue, 4 Jun 2019 15:15:33 +0800 Subject: [PATCH 172/173] Update metrics.py update documents in metrics.py --- fastNLP/core/metrics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fastNLP/core/metrics.py b/fastNLP/core/metrics.py index f633a80f..f994bd31 100644 --- a/fastNLP/core/metrics.py +++ b/fastNLP/core/metrics.py @@ -476,8 +476,8 @@ class SpanFPreRecMetric(MetricBase): label的f1, pre, rec :param str f_type: 'micro'或'macro'. 'micro':通过先计算总体的TP,FN和FP的数量,再计算f, precision, recall; 'macro': 分布计算每个类别的f, precision, recall,然后做平均(各类别f的权重相同) - :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 - 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 + :param float beta: f_beta分数,:math:`f_beta = \frac{(1 + {beta}^{2})*(pre*rec)}{({beta}^{2}*pre + rec)}`. + 常用为beta=0.5, 1, 2. 若为0.5则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 """ def __init__(self, tag_vocab, pred=None, target=None, seq_len=None, encoding_type='bio', ignore_labels=None, @@ -708,8 +708,8 @@ class SQuADMetric(MetricBase): :param pred2: 参数映射表中`pred2`的映射关系,None表示映射关系为`pred2`->`pred2` :param target1: 参数映射表中`target1`的映射关系,None表示映射关系为`target1`->`target1` :param target2: 参数映射表中`target2`的映射关系,None表示映射关系为`target2`->`target2` - :param float beta: f_beta分数,f_beta = (1 + beta^2)*(pre*rec)/(beta^2*pre + rec). 常用为beta=0.5, 1, 2. 若为0.5 - 则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 + :param float beta: f_beta分数,:math:`f_beta = \frac{(1 + {beta}^{2})*(pre*rec)}{({beta}^{2}*pre + rec)}`. + 常用为beta=0.5, 1, 2. 若为0.5则精确率的权重高于召回率;若为1,则两者平等;若为2,则召回率权重高于精确率。 :param bool right_open: right_open为true表示start跟end指针指向一个左闭右开区间,为false表示指向一个左闭右闭区间。 :param bool print_predict_stat: True则输出预测答案是否为空与正确答案是否为空的统计信息, False则不输出 From c54ebace2c2eb999f1a18ca5237ab59f5b278925 Mon Sep 17 00:00:00 2001 From: yunfan Date: Tue, 4 Jun 2019 17:06:19 +0800 Subject: [PATCH 173/173] update sst loader --- fastNLP/io/data_loader/sst.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fastNLP/io/data_loader/sst.py b/fastNLP/io/data_loader/sst.py index 73b01959..1e1b8bef 100644 --- a/fastNLP/io/data_loader/sst.py +++ b/fastNLP/io/data_loader/sst.py @@ -8,6 +8,9 @@ from ..embed_loader import EmbeddingOption, EmbedLoader class SSTLoader(DataSetLoader): + URL = 'https://nlp.stanford.edu/sentiment/trainDevTestTrees_PTB.zip' + DATA_DIR = 'sst/' + """ 别名::class:`fastNLP.io.SSTLoader` :class:`fastNLP.io.dataset_loader.SSTLoader`

6M9JW6{$|G23c16-kT`NqgTP9 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%0A!9bJTQr(*jpbfdzbha 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