本文解读代码https://github.com/dmesquita/easy-deep-learning-with-AllenNLP,用不到三百行的代码解决一个文本分类的任务,实现了NLP的整个流程。
一、前期准备:
配环境:参见我的上一篇博客https://blog.csdn.net/Findingxu/article/details/90722995
下代码:在Ubuntu上 git clone 一下代码。
二、AlleNLP介绍:
从图中我们可以知道在AllenNLP中构建一个模型的组成和数据流动,
分为四个部分:dataset_reader, model,iterator,training。数据用dataset_reader读取和处理,用迭代器iterator 定义的参数,迭代输入到model 中,model提供了整个网络结构和参数,然后用training定义的参数进行训练。
AllenNLP的厉害之处就是能通过一个配置文件就把上面四部分的参数都写好,然后调用就行。还有封装很多功能,可以提供字典,各种网络结构等。
三、解读代码:
(一) 先来看最重要的json文件:
{
"dataset_reader": {
"type": "20newsgroups"
},
"train_data_path": "train",
"test_data_path": "test",
"evaluate_on_test": true,
"model": {
"type": "20newsgroups_classifier",
"model_text_field_embedder": {
"tokens": {
"type": "embedding",
"pretrained_file": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/glove/glove.6B.100d.txt.gz",
"embedding_dim": 100,
"trainable": false
}
},
"internal_text_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"classifier_feedforward": {
"input_dim": 200,
"num_layers": 2,
"hidden_dims": [200, 100],
"activations": ["relu", "linear"],
"dropout": [0.2, 0.0]
}
},
"iterator": {
"type": "bucket",
"sorting_keys": [["text", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 30,
"patience": 3,
"cuda_device": 0,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adagrad"
}
}
}
包括以下几个部分:
1. 数据输入:datareader
告诉AllenNLP输入的数据集和如何读取它,DatasetReader从某个位置读取数据并构造Dataset。除文件路径之外的读取数据所需的所有参数都应递给DatasetReader的构造器。
2.模型:model
设置'model'键值来指定模型,在'model'键值中还有三个参数:'model_text_field_embedder','internal_text_encoder'和'classifier_feedforward'
分别对应着embedding 层,encoder 层,feedforward层的参数。
3.数据迭代器:iterator
分批分离训练数据。 AllenNLP提供了一个名为BucketIterator的迭代器,通过对每批最大输入长度填充批量,使计算(填充)更高效。
4.训练器:trainer
提供训练的参数,patience 当模型的性能经过多少个epoch没有增加时,停止训练。
(二)然后,编写AllenNLP python 类:
1 .Dataset_readers 文件夹下的:fetch_newsgroups.py
为了引用JSON文件中的DatasetReader,需要注册它:用以下两行
@DatasetReader.register("20newsgroups")
class NewsgroupsDatasetReader(DatasetReader):
用到两个方法:
(1)read()
@overrides
def _read(self, file_path):
instances = []
if file_path == "train":
logger.info("Reading instances from: %s", file_path)
categories = ["comp.graphics","sci.space","rec.sport.baseball"]
newsgroups_data = fetch_20newsgroups(subset='train',categories=categories)
elif file_path == "test":
logger.info("Reading instances from: %s", file_path)
categories = ["comp.graphics","sci.space","rec.sport.baseball"]
newsgroups_data = fetch_20newsgroups(subset='test',categories=categories)
else:
raise ConfigurationError("Path string not specified in read method")
for i,text in enumerate(newsgroups_data.data):
if file_path == "validate":
if i == 400:
break
text = newsgroups_data.data[i]
target = newsgroups_data.target[i]
yield self.text_to_instance(text, target) # 提取需要的字段,利用text_to_instance将这些字段转换成instance类型的数据
read()从scikit-learn获取数据。通过AllenNLP,你可以设置数据文件的路径(例如JSON文件的路径),但在我们的例子中,我们只需像Python模块一样导入数据。 我们将读取数据集中的每个文本和每个标签,并用text_to_instance()包装它。
(2)text_to_instance()
@overrides
def text_to_instance(self, text: str, target: str = None) -> Instance: # type: ignore
# pylint: disable=arguments-differ
tokenized_text = self._tokenizer.tokenize(text) # 把文本变成你想要的格式:单词字母等
text_field = TextField(tokenized_text, self._token_indexers) # TextField表示转化成了序号之后(tokenized)的文本数据
fields = {'text': text_field}
if target is not None:
fields['label'] = LabelField(int(target),skip_indexing=True) # LabelField表示类别标签
return Instance(fields)
此方法“进行任何符号化或必要的处理,来把文本输入转为Instance”(AllenNLP Documentation),将来自20个新闻组的文本和标签包装到TextField和LabelField中。
Tokenizer是把你的文本转换成单词啦,字母啦,比特对啦等等常见的你想要的形式。TokenIndexer是给这些形式编个号,并且把最终的文本转换成序号表示的形式。举个例子来说,如果你的token是单词,那么我们强大的TokenIndexer可以自动的为你生成单词编号,字母编号,pos_tags的编号。
2 .models 文件夹下的:newsgroups_classifier.py 编写模型类
首先初始化:
def __init__(self, vocab: Vocabulary,
model_text_field_embedder: TextFieldEmbedder, # 这三个都是allennlp.modules的内容
internal_text_encoder: Seq2VecEncoder,
classifier_feedforward: FeedForward,
initializer: InitializerApplicator = InitializerApplicator(),
regularizer: Optional[RegularizerApplicator] = None) -> None:
(1)vocab: Vocabulary
来自allennlp.data。这个数据字典其实是个复合字典,包括所有TextField的字典,以及LabelField自己单独的字典。
(2)model_text_field_embedder: TextFieldEmbedder
来自allennlp.modules,提供了结构,只要把参数填进去就可以。
# 在from_params里面填参数:L126
embedder_params = params.pop("model_text_field_embedder")
model_text_field_embedder =TextFieldEmbedder.from_params(embedder_params, vocab=vocab)
(3)internal_text_encoder: Seq2VecEncoder
Seq2VecEncoder可以用来做encoder,可以有很多种,CNN,RNN等各种模型都有用...,这里传进的参数是双向LSTM
(4)classifier_feedforward: FeedForward
(5)initializer: InitializerApplicator = InitializerApplicator()
来自allennlp.nn ,InitializerApplicator包含着所有参数的基本初始化方法。如果你想自定义初始化,就需要时候用RegularizerApplicator
然后,重写一下forward函数就可以了(同pytorch)
@overrides #pytorch 在这里构建网络,在init里面定义网络参数结构
def forward(self, # type: ignore
text: Dict[str, torch.LongTensor],
label: torch.LongTensor = None) -> Dict[str, torch.Tensor]:
embedded_text = self.model_text_field_embedder(text)
text_mask = util.get_text_field_mask(text) # 获得掩码来表示符号序列的哪些元素仅用于填充
encoded_text = self.internal_text_encoder(embedded_text, text_mask)
logits = self.classifier_feedforward(encoded_text)
output_dict = {'logits': logits}
if label is not None:
loss = self.loss(logits, label.squeeze(-1))
for metric in self.metrics.values():
metric(logits, label.squeeze(-1))
output_dict["loss"] = loss
return output_dict
decode() :接收forward的输出,并对其进行任何必要的推理或解码,得到你要的结果形式。
@overrides
def decode(self, output_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
class_probabilities = F.softmax(output_dict['logits'])
output_dict['class_probabilities'] = class_probabilities
predictions = class_probabilities.cpu().data.numpy()
argmax_indices = numpy.argmax(predictions, axis=-1)
labels = [self.vocab.get_token_from_index(x, namespace="labels")
for x in argmax_indices]
output_dict['label'] = labels
return output_dict
最后是AllenNLP为所有的注册的类都实现了一个from_params的方法,这个方法能够非常好根据json配置文件中提供的信息,对应的调用构造函数,为我们构造DatasetReader以及model实例。
@classmethod
def from_params(cls, vocab: Vocabulary, params: Params) -> 'Fetch20NewsgroupsClassifier': # ->返回值注释
embedder_params = params.pop("model_text_field_embedder")
model_text_field_embedder = TextFieldEmbedder.from_params(embedder_params, vocab=vocab)
internal_text_encoder = Seq2VecEncoder.from_params(params.pop("internal_text_encoder"))
classifier_feedforward = FeedForward.from_params(params.pop("classifier_feedforward"))
initializer = InitializerApplicator.from_params(params.pop('initializer', []))
regularizer = RegularizerApplicator.from_params(params.pop('regularizer', []))
return cls(vocab=vocab, #cls 类函数 ,cls 和self的区别就是 self要实例化之后才能用,cls不需要
model_text_field_embedder=model_text_field_embedder,
internal_text_encoder=internal_text_encoder,
classifier_feedforward=classifier_feedforward,
initializer=initializer,
regularizer=regularizer)
四、运行代码和解决bug
命令行运行 python run.py train experiments/newsgroups_with_cuda.json -s tmp/result(保存结果的文件)
但是 报错了:
然后发现,给的代码中少写了一个参数:
需要加上, lazy=False super().__init__(lazy=lazy)
def __init__(self,
tokenizer: Tokenizer = None,
token_indexers: Dict[str, TokenIndexer] = None, lazy=False) -> None:
super().__init__(lazy=lazy)
这个参数的含义可以看这个官方文档的解释。
好了,代码运行成功了。
要了解代码执行的过程,可以到你保存结果的路径下,找到log信息文件查看。
这个路径包含很多,有词汇表,有可视化summary等。
总的来说,AllenNLP还是很简易便捷的。
参考: