@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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: | |||
@@ -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): | |||
@@ -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): | |||
@@ -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'), | |||
) |
@@ -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 | |||
@@ -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) | |||
@@ -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]: | |||