fastNLP

搬运自github高星项目fastNLP,复旦的工作,本文主要是自己使用中会参考的一些代码,传送门
https://fastnlp.readthedocs.io/zh/latest/index.html

DataSet

DataSet是fastNLP用于封装数据的类,一般训练集、验证集和测试集会被加载为三个单独的DataSet对象

DataSet中的数据组织形式类似一个表格,列即为多个field,FieldArray;行为多个instance,Instance

DataSet创建和删除
# 使用字典方式创建
from fastNLP import DataSet
data = {'raw_words':["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)
print(dataset)
+------------------------------+------------------------------------------------+---------+
|           raw_words          |                     words                      | seq_len |
+------------------------------+------------------------------------------------+---------+
| This is the first instance . | ['this', 'is', 'the', 'first', 'instance', ... |    6    |
|      Second instance .       |          ['Second', 'instance', '.']           |    3    |
|       Third instance .       |           ['Third', 'instance', '.']           |    3    |
+------------------------------+------------------------------------------------+---------+
# 使用append方法加入Instance
instance = Instance(raw_words="This is the fourth instance",
                    words=['this', 'is', 'the', 'fourth', 'instance', '.'],
                    seq_len=6)
dataset.append(instance)
# 可以继续append更多内容,但是append的instance应该和前面的instance拥有完全相同的field

# 使用Instance数组方式构建
dataset = DataSet([
    Instance(raw_words="This is the first instance",
        words=['this', 'is', 'the', 'first', 'instance', '.'],
        seq_len=6),
    Instance(raw_words="Second instance .",
        words=['Second', 'instance', '.'],
        seq_len=3)
    ])

# 使用for循环遍历DataSet
for instance in dataset:
    # type()=Instance
    # do something
# 使用key-value形式读取Instance中的内容
print(dataset[0]['words'])
# 删除数据
# 在dataset中删除满足条件的instance
dropped_dataset = dataset.drop(lambda ins:ins['a']<0, inplace=False)
#  删除第3个instance
dataset.delete_instance(2)
#  删除名为'a'的field
dataset.delete_field('a')
DataSet水平上的简单数据预处理

因为 fastNLP 中的数据是按列存储的,所以大部分的数据预处理操作是以列field为操作对象的

#  检查是否存在名为'a'的field
dataset.has_field('a')  # 或 ('a' in dataset)
#  将名为'a'的field改名为'b'
dataset.rename_field('a', 'b')
#  DataSet的长度
len(dataset)

# 使用apply()或apply_field()函数进行数据预处理操作,是对
# field中每个instance执行的操作
dataset.apply(lambda ins: ins['raw_words'].split(), new_field_name='words')
# 或使用DataSet.apply_field()
dataset.apply_field(lambda sent:sent.split(), field_name='raw_words', new_field_name='words')
# 除了匿名函数,也可以定义函数传递进去
def get_words(instance):
    sentence = instance['raw_words']
    words = sentence.split()
    return words
dataset.apply(get_words, new_field_name='words')
英文任务中常用的field名

raw_words: 表示的是原始的str; 存在多个raw_words的情况,例如matching任务,它们会被定义为raw_words0, raw_words1

words: 表示的是已经tokenize后的词语。例如[“This”, “is”, “a”, “demo”, “sentence”], 但由于str并不能直接被神经网络所使用,所以words中的内容往往被转换为int,如[3, 10, 4, 2, 7, …]等。多列words的情况,会被命名为words0, words1

target: 表示目标值。分类场景下,只有一个值;序列标注场景下是一个序列

seq_len: 一般用于表示words列的长度

Vocabulary

可直接一个一个建立词表

还可以通过直接从DataSet中的某一列建立词表以及将该列转换为index

from fastNLP import Vocabulary
from fastNLP import DataSet

dataset = DataSet({'chars': [
                                ['今', '天', '天', '气', '很', '好', '。'],
                                ['被', '这', '部', '电', '影', '浪', '费', '了', '两', '个', '小', '时', '。']
                            ],
                    'target': ['neutral', 'negative']
})

# 将词建立索引
vocab = Vocabulary()
vocab.from_dataset(dataset, field_name='chars')
vocab.index_dataset(dataset, field_name='chars')

# 将target转为数字
target_vocab = Vocabulary(padding=None, unknown=None)
target_vocab.from_dataset(dataset, field_name='target')
target_vocab.index_dataset(dataset, field_name='target')
print(dataset)

±-----------------------------------±-------+
| chars | target |
±-----------------------------------±-------+
| [4, 2, 2, 5, 6, 7, 3] | 0 |
| [8, 9, 10, 11, 12, 13, 14,…| 1 |
±-----------------------------------±-------+

注意要将验证集或者测试集在建立词表时放入no_create_entry_dataset这个参数中

#  将验证集或者测试集在建立词表是放入no_create_entry_dataset这个参数中
vocab.from_dataset(tr_data, field_name='chars', no_create_entry_dataset=[dev_data])

下例进行说明

from fastNLP.embeddings import StaticEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word('train')
vocab.add_word('only_in_train')  # 仅在train出现,但肯定在预训练词表中不存在
vocab.add_word('test', no_create_entry=True)  # 该词只在dev或test中出现
vocab.add_word('only_in_test', no_create_entry=True)  # 这个词在预训练的词表中找不到

embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d')
print(embed(torch.LongTensor([vocab.to_index('train')])))
print(embed(torch.LongTensor([vocab.to_index('only_in_train')])))
print(embed(torch.LongTensor([vocab.to_index('test')])))
print(embed(torch.LongTensor([vocab.to_index('only_in_test')])))
print(embed(torch.LongTensor([vocab.unknown_idx])))    #unk

no_create_entry=False

当存在于预训练词向量中时,替换为预训练词向量;当不存在时进行随机初始化,然后进行fine-tune training

no_create_entry=True

当存在于预训练词向量中时,替换为预训练词向量;当不存在时表示为unk,fastNLP用零向量进行初始化

Embedding

使用这些词嵌入方式的时候都需要做一些加载上的处理,比如预训练的word2vec, fasttext以及glove都有着超过几十万个词语的表示,但一般任务大概 只会用到其中的几万个词,如果直接加载所有的词汇,会导致内存占用变大以及训练速度变慢,需要从预训练文件中抽取本次实验的用到的词汇;而对于英文的 elmocharacter embedding, 需要将word拆分成character才能使用;Bert的使用更是涉及到了Byte pair encoding(BPE)相关的内容。为了方便大家的使用,fastNLP通过 Vocabulary统一了不同embedding的使用

StaticEmbedding
from fastNLP.embeddings import StaticEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d')

words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])  # 将文本转为index
print(embed(words).size())  # StaticEmbedding的使用和pytorch的nn.Embedding是类似的

若要加载本地词向量,只要传入本地的embedding文件路径即可

随机初始化embedding
embed = StaticEmbedding(vocab, model_dir_or_name=None, embedding_dim=30)
ELMo Embedding
from fastNLP.embeddings import ElmoEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

embed = ElmoEmbedding(vocab, model_dir_or_name='en-small', requires_grad=False)
words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])
print(embed(words).size())
# torch.Size([1, 5, 256])


# 也可以输出多层的ELMo结果,fastNLP将在不同层的结果在最后一维上拼接
# 下面的代码需要在上面的代码执行结束之后执行
embed = ElmoEmbedding(vocab, model_dir_or_name='en-small', requires_grad=False, layers='1,2')
print(embed(words).size())
# torch.Size([1, 5, 512])

# 不同层之间使用可学习的权重可以使得ELMo的效果更好,在fastNLP中可以通过以下的初始化 实现3层输出的结果通过可学习的权重进行加法融合
embed = ElmoEmbedding(vocab, model_dir_or_name='en-small', requires_grad=True, layers='mix')
print(embed(words).size())  # 三层输出按照权重element-wise的加起来
# torch.Size([1, 5, 256])
Bert Embedding
from fastNLP.embeddings import BertEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

embed = BertEmbedding(vocab, model_dir_or_name='en-base-cased')
words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])
print(embed(words).size())
# torch.Size([1, 5, 768])

# 可以通过申明使用指定层数的output也可以使用多层的output
# 使用后面两层的输出
embed = BertEmbedding(vocab, model_dir_or_name='en-base-cased', layers='10,11')
print(embed(words).size())  # 结果将是在最后一维做拼接
# torch.Size([1, 5, 1536])

在Bert中还存在两个特殊的字符**[CLS][SEP]**,默认情况下这两个字符是自动加入并且在计算结束之后会自动删除

但是有些分类的情况,必须需要使用**[CLS]的表示,这种情况可以通过在初始化时申明一下需要保留[CLS]**的表示

embed = BertEmbedding(vocab, model_dir_or_name='en-base-cased', layers='-1', include_cls_sep=True)
print(embed(words).size())  # 结果将在序列维度上增加2
# 取出句子的cls表示
cls_reps = embed(words)[:, 0]  # shape: [batch_size, 768]
# torch.Size([1, 7, 768])

在英文Bert模型中,一个英文单词可能会被切分为多个subword,例如"fairness"会被拆分为 ["fair", "##ness"] ,这样一个word对应的将有两个输出, BertEmbedding 会使用pooling方法将一个word的subword的表示合并成一个vector,通过pool_method可以控制 该pooling方法,支持的有"first"(即使用fair的表示作为fairness的表示), “last”(使用##ness的表示作为fairness的表示), “max”(对fair和 ##ness在每一维上做max),“avg”(对fair和##ness每一维做average)

embed = BertEmbedding(vocab, model_dir_or_name='en-base-cased', layers='-1', pool_method='max')
print(embed(words).size())
# torch.Size([1, 5, 768])

Bert在针对具有两句话的任务时(如matching,Q&A任务),句子之间通过**[SEP]拼接起来,前一句话的token embedding为0, 后一句话的token embedding为1。BertEmbedding能够自动识别句子中间的[SEP]**来正确设置对应的token_type_id的

vocab.add_word_lst("this is a demo . [SEP] another sentence .".split())
embed = BertEmbedding(vocab, model_dir_or_name='en-base-cased', layers='-1', pool_method='max')
words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo . [SEP] another sentence .".split()]])
print(embed(words).size())

在多个**[SEP]**的情况下,将会使token_type_id不断0,1循环。比如"first sentence [SEP] second sentence [SEP] third sentence", 它们的 token_type_id将是[0, 0, 0, 1, 1, 1, 0, 0]。但请注意[SEP]一定要大写的,不能是[sep],否则无法识别

character-level embedding

fastNLP还提供了两种Character Embedding: CNNCharEmbeddingLSTMCharEmbedding

一般在使用character embedding时,需要在预处理的时候将word拆分成character,这 会使得预处理过程变得非常繁琐。在fastNLP中,使用character embedding也只需要传入 Vocabulary即可,而且该 Vocabulary与其它Embedding使用的Vocabulary是一致的

from fastNLP.embeddings import CNNCharEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

# character的embedding维度大小为50,返回的embedding结果维度大小为64。
embed = CNNCharEmbedding(vocab, embed_size=64, char_emb_size=50)
words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])
print(embed(words).size())
# torch.Size([1, 5, 64])
from fastNLP.embeddings import LSTMCharEmbeddding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

# character的embedding维度大小为50,返回的embedding结果维度大小为64。
embed = LSTMCharEmbeddding(vocab, embed_size=64, char_emb_size=50)
words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])
print(embed(words).size())
# torch.Size([1, 5, 64])
叠加使用多个embedding
from fastNLP.embeddings import StaticEmbedding, StackEmbedding, CNNCharEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

# 能够拼接起来的Embedding 必须使用同样的vocab
word_embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d')
char_embed = CNNCharEmbedding(vocab, embed_size=64, char_emb_size=50)
embed = StackEmbedding([word_embed, char_embed])

words = torch.LongTensor([[vocab.to_index(word) for word in "this is a demo .".split()]])
print(embed(words).size())  # 输出embedding的维度为50+64=114
# torch.Size([1, 5, 114])
获取Embedding的dimension
from fastNLP.embeddings import *

vocab = Vocabulary()
vocab.add_word_lst("this is a demo .".split())

static_embed = StaticEmbedding(vocab, model_dir_or_name='en-glove-6b-50d')
print(static_embed.embedding_dim)  # 50
char_embed = CNNCharEmbedding(vocab, embed_size=30)
print(char_embed.embedding_dim)    # 30
elmo_embed_1 = ElmoEmbedding(vocab, model_dir_or_name='en-small', layers='2')
print(elmo_embed_1.embedding_dim)  # 256
elmo_embed_2 = ElmoEmbedding(vocab, model_dir_or_name='en-small', layers='1,2')
print(elmo_embed_2.embedding_dim)  # 512
bert_embed_1 = BertEmbedding(vocab, layers='-1', model_dir_or_name='en-base-cased')
print(bert_embed_1.embedding_dim)  # 768
bert_embed_2 = BertEmbedding(vocab, layers='2,-1', model_dir_or_name='en-base-cased')
print(bert_embed_2.embedding_dim)  # 1536
stack_embed = StackEmbedding([static_embed, char_embed])
print(stack_embed.embedding_dim)  # 80

使用requires_grad设置Embedding的权重是否更新

fastNLP中所有的Embedding都支持传入word_dropoutdropout参数,word_dropout指示的是以多大概率将输入的word置为unk的index,这样既可以使得unk得到训练,也可以有一定的regularize效果; dropout参数是在获取到word的表示之后,以多大概率将一些维度的表示置为0。

如果使用 StackEmbedding且需要用到word_dropout,建议将word_dropout设置在 StackEmbedding

同时使用cnn character embedding和word embedding 会使得NER的效果有比较大的提升

fastNLP支持将 CNNCharEmbeddingStaticEmbedding拼成一个 StackEmbedding。如果通过这种方式使用,需要在预处理文本时,不要将词汇小写化(因为Character Embedding需要利用词语中的大小写信息)且不要将出现频次低于某个阈值的word设置为unk(因为 Character embedding需要利用字形信息);但 StaticEmbedding 使用的某些预训练词嵌入的词汇表中只有小写的词语, 且某些低频词并未在预训练中出现需要被剔除。即(1) character embedding需要保留大小写,而预训练词向量不需要保留大小写。(2) character embedding需要保留所有的字形, 而static embedding需要设置一个最低阈值以学到更好的表示。

  1. 解决大小写的问题
from fastNLP.embeddings import StaticEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary().add_word_lst("The the a A".split())
#  下面用随机的StaticEmbedding演示,但与使用预训练时效果是一致的
embed = StaticEmbedding(vocab, model_name_or_dir=None, embedding_dim=5, lower=True)
print(embed(torch.LongTensor([vocab.to_index('The')])))
print(embed(torch.LongTensor([vocab.to_index('the')])))

可以看到"The"与"the"的vector是一致的。他们实际上也是引用的同一个vector。通过将lower设置为True,可以在 StaticEmbedding实现类似具备相同小写结果的词语引用同一个vector。

  1. 解决min_freq的问题
from fastNLP.embeddings import StaticEmbedding
from fastNLP import Vocabulary

vocab = Vocabulary().add_word_lst("the the the a".split())
#  下面用随机的StaticEmbedding演示,但与使用预训练时效果是一致的
embed = StaticEmbedding(vocab, model_name_or_dir=None, embedding_dim=5, min_freq=2)
print(embed(torch.LongTensor([vocab.to_index('the')])))
print(embed(torch.LongTensor([vocab.to_index('a')])))
print(embed(torch.LongTensor([vocab.unknown_idx])))
# tensor([[ 0.0454,  0.3375,  0.6758, -0.2026, -0.4715]], grad_fn=<EmbeddingBackward>)
# tensor([[-0.7602,  0.0149,  0.2733,  0.3974,  0.7371]], grad_fn=<EmbeddingBackward>)
# tensor([[-0.7602,  0.0149,  0.2733,  0.3974,  0.7371]], grad_fn=<EmbeddingBackward>)

可以看到a的vector表示与unknown是一样的,这是由于a的频次低于了2,所以被指向了unknown的表示

同时考虑大小写和词频

embed = StaticEmbedding(vocab, model_name_or_dir=None, embedding_dim=5, min_freq=2, lower=True)

加载和处理数据集

Loader
  1. download() 自动将该数据集下载到缓存地址,默认缓存地址为~/.fastNLP/datasets/
  2. _load() 从一个数据文件中读取数据,返回一个dataSet
  3. load() 从文件或者文件夹中读取数据为DataSet,并将它们组装成DataBundle
  • None, 将尝试读取自动缓存的数据,仅支持提供了自动下载数据的Loader
  • 文件夹路径, 默认将尝试在该文件夹下匹配文件名中含有 train , test , dev 的文件
  • dict, 例如{‘train’:"/path/to/tr.conll", ‘dev’:"/to/validate.conll", “test”:"/to/te.conll"}
from fastNLP.io import CWSLoader

loader = CWSLoader(dataset_name='pku')
data_bundle = loader.load()
print(data_bundle)

取出DataSet

tr_data = data_bundle.get_dataset('train')
print(tr_data[:2])
Pipe

通过Loader 可以将文本数据读入,但并不能直接被神经网络使用,还需要进行一定的预处理

LoaderPipe一般具备一一对应的关系,一般Pipe 处理包含以下的几个过程

  1. 将raw_words或 raw_chars进行tokenize以切分成不同的词或字
  2. 再建立词或字的Vocabulary, 并将词或字转换为index
  3. 将target 列建立Vocabulary并将target列转为index
from fastNLP.io import CWSPipe

data_bundle = CWSPipe().process(data_bundle)
print(data_bundle)

vocab = data_bundle.get_vocab('target')
print(vocab)
不同格式类型的Loader

CSVLoader读取CSV类型的数据集文件

from fastNLP.io.loader import CSVLoader
data_set_loader = CSVLoader(
    headers=('raw_words', 'target'), sep='\t'
)
# 表示将CSV文件中每一行的第一项将填入'raw_words' field,第二项填入'target' field。
# 其中项之间由'\t'分隔

data_set = data_set_loader._load('path/to/your/file')

Ex:

But it does not leave you with much .   1
You could hate it for the same reason . 1
The performances are an absolute joy .  4

Print:

raw_wordstarget
But it does not leave you with much .1
You could hate it for the same reason .1
The performances are an absolute joy .4

JsonLoader读取Json类型的数据集文件, 数据必须按行存储,每行是一个包含各类属性的Json对象

from fastNLP.io.loader import JsonLoader
oader = JsonLoader(
    fields={'sentence1': 'raw_words1', 'sentence2': 'raw_words2', 'gold_label': 'target'}
)
# 表示将Json对象中'sentence1'、'sentence2'和'gold_label'对应的值赋给'raw_words1'、'raw_words2'、'target'这三个fields

data_set = loader._load('path/to/your/file')

动手实现一个文本分类器

使用Trainer和Tester进行快速训练和测试

1 读入SST2数据集

from fastNLP.io import SST2Pipe

pipe = SST2Pipe()
databundle = pipe.process_from_file()
vocab = databundle.get_vocab('words')
print(databundle)
print(databundle.get_dataset('train')[0])
print(databundle.get_vocab('words'))

2 由于SST2数据集的测试集并不带有标签数值,故我们分割出一部分训练集作为测试集

train_data = databundle.get_dataset('train')
train_data, test_data = train_data.split(0.015)
dev_data = databundle.get_dataset('dev')
print(len(train_data),len(dev_data),len(test_data))

3 将数据集中不同field设置为input/target

train_data.print_field_meta()
+-------------+-----------+--------+-------+---------+
| field_names | raw_words | target | words | seq_len |
+-------------+-----------+--------+-------+---------+
|   is_input  |   False   | False  |  True |   True  |
|  is_target  |   False   |  True  | False |  False  |
| ignore_type |           | False  | False |  False  |
|  pad_value  |           |   0    |   0   |    0    |
+-------------+-----------+--------+-------+---------+

其中is_input和is_target分别表示是否为input和target

ignore_type为true时指使用DataSetIter取出batch数据时fastNLP不会自动进行padding,pad_value为padding时使用的值,这两者仅当field设定为Input/target时才有存在的意义

is_input为true的field在DataSetIter迭代取出的batch_x中,is_target为true的field在batch_y中

4 使用内置的文本分类模型CNNText对模型进行定义

from fastNLP.models import CNNText

#词嵌入的维度
EMBED_DIM = 100

#使用CNNText的时候第一个参数输入一个tuple,作为模型定义embedding的参数
#还可以传入 kernel_nums, kernel_sizes, padding, dropout的自定义值
model_cnn = CNNText((len(vocab),EMBED_DIM), num_classes=2, dropout=0.1)

5 将准确率作为评价指标

  • pred 参数对应的是模型的 forward 方法返回的 dict 中的一个 key 的名字
  • target 参数对应的是DataSet中作为标签的field的名字
from fastNLP import AccuracyMetric
from fastNLP import Const

# metrics=AccuracyMetric() 在本例中与下面这行代码等价
metrics=AccuracyMetric(pred=Const.OUTPUT, target=Const.TARGET)

6 损失函数

预定义的四种loss

  • CrossEntropyLoss: 返回交叉熵损失
  • BCELoss: 返回二分类的交叉熵
  • L1Loss: 返回L1 损失
  • NLLLoss: 返回负对数似然损失
from fastNLP import CrossEntropyLoss

# loss = CrossEntropyLoss() 在本例中与下面这行代码等价
loss = CrossEntropyLoss(pred=Const.OUTPUT, target=Const.TARGET)

自定义损失函数

# 这表示构建了一个损失函数类,由func计算损失函数,其中将从模型返回值或者DataSet的target=True的field
# 当中找到一个参数名为`pred`的参数传入func一个参数名为`input`的参数;找到一个参数名为`label`的参数
# 传入func作为一个名为`target`的参数
#下面自己构建了一个交叉熵函数,和之后直接使用fastNLP中的交叉熵函数是一个效果
import torch
from fastNLP import LossFunc
func = torch.nn.functional.cross_entropy
loss_func = LossFunc(func, input=Const.OUTPUT, target=Const.TARGET)

7 优化器

定义模型运行的时候使用的优化器, 可以直接使用torch.optim.Optimizer中的优化器

import torch.optim as optim

#使用 torch.optim 定义优化器
optimizer=optim.RMSprop(model_cnn.parameters(), lr=0.01, alpha=0.99, eps=1e-08, weight_decay=0, momentum=0, centered=False)

8 训练

from fastNLP import Trainer

#训练的轮数和batch size
N_EPOCHS = 10
BATCH_SIZE = 16

#如果在定义trainer的时候没有传入optimizer参数,模型默认的优化器为torch.optim.Adam且learning rate为lr=4e-3
#这里只使用了loss作为损失函数输入,感兴趣可以尝试其他损失函数(如之前自定义的loss_func)作为输入
trainer = Trainer(model=model_cnn, train_data=train_data, dev_data=dev_data, loss=loss, metrics=metrics,
optimizer=optimizer,n_epochs=N_EPOCHS, batch_size=BATCH_SIZE)
trainer.train()

使用DataSetIter进行batch训练

DataSetIter用于定义一个batch并实现batch的多种功能

  • dataset: DataSet数据集
  • batch_size
  • sampler: 默认为RandomSampler
  • as_numpy: True→numpy.array False→torch.Tensor
  • prefetch: True使用多进程预先取出下一个batch

sampler

  • BucketSampler: 随机地取出长度相似的元素
  • SequentialSampler: 顺序取出元素的采样器
  • RandomSampler: 随机化取元素的采样器

Padder

​ 在fastNLP里,pad是与field绑定的,即不同的field可以使用不同的pad方式, 比如在英文任务中word需要的pad和 character的pad方式往往是不同的;默认情况下使用AutoPadder,常用的还有EngChar2DPadder

自动padding

from fastNLP import BucketSampler
from fastNLP import DataSetIter

tmp_data = dev_data[:10]
# 定义一个Batch,传入DataSet,规定batch_size和去batch的规则。
# 顺序(Sequential),随机(Random),相似长度组成一个batch(Bucket)
sampler = BucketSampler(batch_size=2, seq_len_field_name='seq_len')
batch = DataSetIter(batch_size=2, dataset=tmp_data, sampler=sampler)
for batch_x, batch_y in batch:
    print("batch_x: ",batch_x)
    print("batch_y: ", batch_y)

可以看到那些设定为input的field都出现在batch_x中,而设定为target的field则出现在batch_y中,同时对于同一个batch_x中的两个数据,长度偏短的那个会自动padding到和长度偏长的句子长度一致,默认的padding为0

可以改变默认的pad值

tmp_data.set_pad_val('words',-1)
batch = DataSetIter(batch_size=2, dataset=tmp_data, sampler=sampler)
for batch_x, batch_y in batch:
    print("batch_x: ",batch_x)
    print("batch_y: ", batch_y)

也可以自己构造Padder类个性化padding

最后使用DataSetIter进行训练

from fastNLP import BucketSampler
from fastNLP import DataSetIter
from fastNLP.models import CNNText
from fastNLP import Tester
import torch
import time

embed_dim = 100
model = CNNText((len(vocab),embed_dim), num_classes=2, dropout=0.1)

def train(epoch, data, devdata):
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    lossfunc = torch.nn.CrossEntropyLoss()
    batch_size = 32

    # 定义一个Batch,传入DataSet,规定batch_size和去batch的规则。
    # 顺序(Sequential),随机(Random),相似长度组成一个batch(Bucket)
    train_sampler = BucketSampler(batch_size=batch_size, seq_len_field_name='seq_len')
    train_batch = DataSetIter(batch_size=batch_size, dataset=data, sampler=train_sampler)

    start_time = time.time()
    print("-"*5+"start training"+"-"*5)
    for i in range(epoch):
        loss_list = []
        for batch_x, batch_y in train_batch:
            optimizer.zero_grad()
            output = model(batch_x['words'])
            loss = lossfunc(output['pred'], batch_y['target'])
            loss.backward()
            optimizer.step()
            loss_list.append(loss.item())

        #这里verbose如果为0,在调用Tester对象的test()函数时不输出任何信息,返回评估信息; 如果为1,打印出验证结果,返回评估信息
        #在调用过Tester对象的test()函数后,调用其_format_eval_results(res)函数,结构化输出验证结果
        tester_tmp = Tester(devdata, model, metrics=AccuracyMetric(), verbose=0)
        res=tester_tmp.test()

        print('Epoch {:d} Avg Loss: {:.2f}'.format(i, sum(loss_list) / len(loss_list)),end=" ")
        print(tester_tmp._format_eval_results(res),end=" ")
        print('{:d}ms'.format(round((time.time()-start_time)*1000)))
        loss_list.clear()

train(10, train_data, dev_data)
#使用tester进行快速测试
tester = Tester(test_data, model, metrics=AccuracyMetric())
tester.test()

9 测试

from fastNLP import Tester

tester = Tester(test_data, model_cnn, metrics=AccuracyMetric())
tester.test()
Evaluate data in 0.19 seconds!
[tester]
AccuracyMetric: acc=0.889109

使用Metric快速评测你的模型

SpanFPreRecMetric 也是一种非常见的评价指标, 例如在序列标注问题中,常以span的方式计算 F-measure, precision, recall

名称介绍
MetricBase自定义metrics需继承的基类
AccuracyMetric简单的正确率metric
SpanFPreRecMetric同时计算 F-measure, precision, recall 值的 metric
ExtractiveQAMetric用于抽取式QA任务 的metric

定义自己的metrics

在定义自己的metrics类时需要继承fastNLP的MetricBase,并覆盖写入evaluateget_metric方法

evaluate传入一个批次的数据,将针对一个批次的预测结果做评价指标的累计

get_metric当所有数据处理完毕时调用,根据evaluate累计的评价指标计算最终评价结果

1 如果模型forward计算得到的字典中,包含’pred’:output

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

2 dataset中label这个field是需要预测的值,并且该field被设置为is_target=True

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中

MetricBase将会在输入的字典pred_dicttarget_dict中进行检查

pred_dict是模型当中forward()函数或者predict()函数的返回值

target_dict是DataSet当中设置is_target为True的field

使用Modules和Models快速搭建自定义模型

modelsmodules用于构建fastNLP所需的神经网络模型,它可以和torch.nn中的模型一起使用

CNNText使用 CNN 进行文本分类的模型
SeqLabeling简单的序列标注模型
AdvSeqLabel更大网络结构的序列标注模型
ESIMESIM 模型的实现
StarTransEnc带 word-embedding的Star-Transformer模 型
STSeqLabel用于序列标注的 Star-Transformer 模型
STNLICls用于自然语言推断 (NLI) 的 Star-Transformer 模型
STSeqCls用于分类任务的 Star-Transformer 模型
BiaffineParserBiaffine 依存句法分析网络的实现
BiLSTMCRF使用BiLSTM与CRF进行序列标注
使用nn.torch编写模型

但与 pyTorch 中编写模型的常见方法不同, 用于 fastNLP 的模型中 forward 函数需要返回一个字典,字典中至少需要包含pred这个字段

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}
使用modules编写模型

fastNLP统一把batch_size放在第一维

from fastNLP.modules import Embedding, LSTM, MLP

class Model(nn.Module):
    def __init__(self, vocab_size, embedding_dim, output_dim, hidden_dim=64, num_layers=2, dropout=0.5):
        super().__init__()

        self.embedding = Embedding((vocab_size, embedding_dim))
        self.lstm = LSTM(embedding_dim, hidden_dim, num_layers=num_layers, bidirectional=True)
        self.mlp = MLP([hidden_dim*2,output_dim], dropout=dropout)

    def forward(self, words):
        embedded = self.embedding(words)
        _,(hidden,_) = self.lstm(embedded)
        pred = self.mlp(torch.cat((hidden[-1],hidden[-2]),dim=1))
        return {"pred":pred}

自带module

名称介绍
ConvolutionCharEncoderchar级别的卷积 encoder
LSTMCharEncoderchar级别基于LSTM的 encoder
ConvMaxpool结合了Convolution和Max-Pooling于一体的模块
LSTMLSTM模块轻量封装了PyTorch的LSTM
StarTransformerStar-Transformer 的encoder部分
TransformerEncoderTransformer的encoder模块,不包含embedding层
VarRNNVariational Dropout RNN 模块
VarLSTMVariational Dropout LSTM 模块
VarGRUVariational Dropout GRU 模块
MaxPoolMax-pooling模块
MaxPoolWithMask带mask矩阵的max pooling。在做 max-pooling的时候不会考虑mask值为0的位置。
AvgPoolAverage-pooling模块
AvgPoolWithMask带mask矩阵的average pooling。在做 average-pooling的时候不会考虑mask值为0的位置。
MultiHeadAttentionMultiHead Attention 模块
MLP简单的多层感知器模块
ConditionalRandomField条件随机场模块
viterbi_decode给定一个特征矩阵以及转移分数矩阵,计算出最佳的路径以及对应的分数 (与 ConditionalRandomField 配合使用)
allowed_transitions给定一个id到label的映射表,返回所有可以跳转的列表(与 ConditionalRandomField 配合使用)
TimestepDropout简单包装过的Dropout 组件
使用Bert做序列标注
from fastNLP.io import WeiboNERPipe
data_bundle = WeiboNERPipe().process_from_file()
data_bundle.rename_field('chars', 'words')

from fastNLP.embeddings import BertEmbedding
embed = BertEmbedding(vocab=data_bundle.get_vocab('words'), model_dir_or_name='cn')
model = BiLSTMCRF(embed=embed, num_classes=len(data_bundle.get_vocab('target')), num_layers=1, hidden_size=200, dropout=0.5,
              target_vocab=data_bundle.get_vocab('target'))

from fastNLP import SpanFPreRecMetric
from torch import Adam
from fastNLP import LossInForward
metric = SpanFPreRecMetric(tag_vocab=data_bundle.get_vocab('target'))
optimizer = Adam(model.parameters(), lr=2e-5)
loss = LossInForward()

from fastNLP import Trainer
import torch
device= 0 if torch.cuda.is_available() else 'cpu'
trainer = Trainer(data_bundle.get_dataset('train'), model, loss=loss, optimizer=optimizer, batch_size=12,
                    dev_data=data_bundle.get_dataset('dev'), metrics=metric, device=device)
trainer.train()

from fastNLP import Tester
tester = Tester(data_bundle.get_dataset('test'), model, metrics=metric)
tester.test()

使用Callback自定义训练过程

利用Callback可以在Trainer训练时加入自定义的操作,比如梯度裁剪,学习率调节,测试模型的性能等。

from fastNLP import (Callback, EarlyStopCallback,
                     Trainer, CrossEntropyLoss, AccuracyMetric)
from fastNLP.models import CNNText
import torch.cuda

# prepare data
def get_data():
    from fastNLP.io import ChnSentiCorpPipe as pipe
    data = pipe().process_from_file()
    print(data)
    data.rename_field('chars', 'words')
    train_data = data.get_dataset('train')
    dev_data = data.get_dataset('dev')
    test_data = data.get_dataset('test')
    vocab = data.get_vocab('words')
    tgt_vocab = data.get_vocab('target')
    return train_data, dev_data, test_data, vocab, tgt_vocab

# prepare model
train_data, dev_data, _, vocab, tgt_vocab = get_data()
device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
model = CNNText((len(vocab),50), num_classes=len(tgt_vocab))

# define callback
callbacks=[EarlyStopCallback(5)]

# pass callbacks to Trainer
def train_with_callback(cb_list):
    trainer = Trainer(
        device=device,
        n_epochs=3,
        model=model,
        train_data=train_data,
        dev_data=dev_data,
        loss=CrossEntropyLoss(),
        metrics=AccuracyMetric(),
        callbacks=cb_list,
        check_code_level=-1
    )
    trainer.train()

train_with_callback(callbacks)

其他的Callback

from fastNLP import EarlyStopCallback, GradientClipCallback, EvaluateCallback
callbacks = [
    EarlyStopCallback(5),
    GradientClipCallback(clip_value=5, clip_type='value'),
    EvaluateCallback(dev_data)
]

train_with_callback(callbacks)

自定义Callback函数

  • 继承Callback
  • 所有以 on_ 开头的类方法会在 Trainer 的训练中在特定阶段调用; on_train_begin() 会在训练开始时被调用,on_epoch_end() 会在每个 epoch 结束时调用
  • 访问 Trainer中的对应信息,如 optimizer, epoch, n_epochs,分别对应训练时的优化器, 当前 epoch 数,和总 epoch 数。 具体可访问的属性,参见 Callback
from fastNLP import Callback
from fastNLP import logger

class MyCallBack(Callback):
    """Print average loss in each epoch"""
    def __init__(self):
        super().__init__()
        self.total_loss = 0
        self.start_step = 0

    def on_backward_begin(self, loss):
        self.total_loss += loss.item()

    def on_epoch_end(self):
        n_steps = self.step - self.start_step
        avg_loss = self.total_loss / n_steps
        logger.info('Avg loss at epoch %d, %.6f', self.epoch, avg_loss)
        self.start_step = self.step

callbacks = [MyCallBack()]
train_with_callback(callbacks)

使用Bert进行各项NLP任务

文本情感分类
from fastNLP.io import WeiboSenti100kPipe

data_bundle =WeiboSenti100kPipe().process_from_file()
data_bundle.rename_field('chars', 'words')

# 载入BertEmbedding
from fastNLP.embeddings import BertEmbedding

embed = BertEmbedding(data_bundle.get_vocab('words'), model_dir_or_name='cn-wwm', include_cls_sep=True)

# 载入模型
from fastNLP.models import BertForSequenceClassification

model = BertForSequenceClassification(embed, len(data_bundle.get_vocab('target')))

# 训练模型
from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric, Adam

trainer = Trainer(data_bundle.get_dataset('train'), model,
                  optimizer=Adam(model_params=model.parameters(), lr=2e-5),
                  loss=CrossEntropyLoss(), device=0,
                  batch_size=8, dev_data=data_bundle.get_dataset('dev'),
                  metrics=AccuracyMetric(), n_epochs=2, print_every=1)
trainer.train()

# 测试结果
from fastNLP import Tester

tester = Tester(data_bundle.get_dataset('test'), model, batch_size=128, metrics=AccuracyMetric())
tester.test()
命名实体识别

一般序列标注的任务都使用conll格式,conll格式是指一行中通过制表符分隔不同的内容,使用空行分隔两句话

文本匹配

文本匹配任务是指给定两句话判断他们的关系。比如,给定两句话判断前一句是否和后一句具有因果关系或是否是矛盾关系;或者给定两句话判断两句话是否 具有相同的意思

data_bundle = CNXNLIBertPipe().process_from_file(paths)
data_bundle.rename_field('chars', 'words')
print(data_bundle)

# 载入BertEmbedding
from fastNLP.embeddings import BertEmbedding

embed = BertEmbedding(data_bundle.get_vocab('words'), model_dir_or_name='cn-wwm', include_cls_sep=True)

# 载入模型
from fastNLP.models import BertForSentenceMatching

model = BertForSentenceMatching(embed, len(data_bundle.get_vocab('target')))

# 训练模型
from fastNLP import Trainer, CrossEntropyLoss, AccuracyMetric, Adam
from fastNLP.core.optimizer import AdamW
from fastNLP.core.callback import WarmupCallback

callbacks = [WarmupCallback(warmup=0.1, schedule='linear'), ]

trainer = Trainer(data_bundle.get_dataset('train'), model,
                  optimizer=AdamW(params=model.parameters(), lr=4e-5),
                  loss=CrossEntropyLoss(), device=0,
                  batch_size=8, dev_data=data_bundle.get_dataset('dev'),
                  metrics=AccuracyMetric(), n_epochs=5, print_every=1,
                  update_every=8, callbacks=callbacks)
trainer.train()

from fastNLP import Tester
tester = Tester(data_bundle.get_dataset('test'), model, batch_size=8, metrics=AccuracyMetric())
tester.test()
中文问答

问答任务是给定一段内容,以及一个问题,需要从这段内容中找到答案

"context": "锣鼓经是大陆传统器乐及戏曲里面常用的打击乐记谱方法,以中文字的声音模拟敲击乐的声音,纪录打击乐的各种不同的演奏方法。常
用的节奏型称为「锣鼓点」。而锣鼓是戏曲节奏的支柱,除了加强演员身段动作的节奏感,也作为音乐的引子和尾声,提示音乐的板式和速度,以及
作为唱腔和念白的伴奏,令诗句的韵律更加抑扬顿锉,段落分明。锣鼓的运用有约定俗成的程式,依照角色行当的身份、性格、情绪以及环境,配合
相应的锣鼓点。锣鼓亦可以模仿大自然的音响效果,如雷电、波浪等等。戏曲锣鼓所运用的敲击乐器主要分为鼓、锣、钹和板四类型:鼓类包括有单
皮鼓(板鼓)、大鼓、大堂鼓(唐鼓)、小堂鼓、怀鼓、花盆鼓等;锣类有大锣、小锣(手锣)、钲锣、筛锣、马锣、镗锣、云锣;钹类有铙钹、大
钹、小钹、水钹、齐钹、镲钹、铰子、碰钟等;打拍子用的檀板、木鱼、梆子等。因为京剧的锣鼓通常由四位乐师负责,又称为四大件,领奏的师
傅称为:「鼓佬」,其职责有如西方乐队的指挥,负责控制速度以及利用各种手势提示乐师演奏不同的锣鼓点。粤剧吸收了部份京剧的锣鼓,但以木鱼
和沙的代替了京剧的板和鼓,作为打拍子的主要乐器。以下是京剧、昆剧和粤剧锣鼓中乐器对应的口诀用字:",
"question": "锣鼓经是什么?",
"answers": [
    {
      "text": "大陆传统器乐及戏曲里面常用的打击乐记谱方法",
      "answer_start": 4
    },
    {
      "text": "大陆传统器乐及戏曲里面常用的打击乐记谱方法",
      "answer_start": 4
    },
    {
      "text": "大陆传统器乐及戏曲里面常用的打击乐记谱方法",
      "answer_start": 4
    }
]
from fastNLP.embeddings import BertEmbedding
from fastNLP.models import BertForQuestionAnswering
from fastNLP.core.losses import CMRC2018Loss
from fastNLP.core.metrics import CMRC2018Metric
from fastNLP.io.pipe.qa import CMRC2018BertPipe
from fastNLP import Trainer, BucketSampler
from fastNLP import WarmupCallback, GradientClipCallback
from fastNLP.core.optimizer import AdamW


data_bundle = CMRC2018BertPipe().process_from_file()
data_bundle.rename_field('chars', 'words')

print(data_bundle)

embed = BertEmbedding(data_bundle.get_vocab('words'), model_dir_or_name='cn', requires_grad=True, include_cls_sep=False, auto_truncate=True,
                      dropout=0.5, word_dropout=0.01)
model = BertForQuestionAnswering(embed)
loss = CMRC2018Loss()
metric = CMRC2018Metric()

wm_callback = WarmupCallback(schedule='linear')
gc_callback = GradientClipCallback(clip_value=1, clip_type='norm')
callbacks = [wm_callback, gc_callback]

optimizer = AdamW(model.parameters(), lr=5e-5)

trainer = Trainer(data_bundle.get_dataset('train'), model, loss=loss, optimizer=optimizer,
                  sampler=BucketSampler(seq_len_field_name='context_len'),
                  dev_data=data_bundle.get_dataset('dev'), metrics=metric,
                  callbacks=callbacks, device=0, batch_size=6, num_workers=2, n_epochs=2, print_every=1,
                  test_use_tqdm=False, update_every=10)
trainer.train(load_best_model=False)
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐