
本文基于AllenNLP中文教程<AllenNLP 使用教程>,僅作為個(gè)人學(xué)習(xí)記錄,作者也是一位騷話博主,和原文對(duì)照發(fā)現(xiàn)不少騷話蛤蛤蛤
有一篇帖子總結(jié)了一下學(xué)習(xí)處理NLP問題中間的坑。NLP數(shù)據(jù)預(yù)處理要比CV的麻煩很多。
- 去除停用詞,建立詞典,加載各種預(yù)訓(xùn)練詞向量,Sentence -> Word ID -> Word Embedding的過程(Tobias Lee:文本預(yù)處理方法小記),其中不僅需要學(xué)習(xí)pytorch,可能還要學(xué)習(xí)spacy,NLTK,numpy,pandas,tensorboardX等常用python包。
- 用到RNN時(shí),還要經(jīng)過pad,pack,pad的過程,像這樣的很多函數(shù)在使用時(shí)需要有數(shù)學(xué)基礎(chǔ)加上簡單的實(shí)踐,感覺對(duì)一個(gè)新人來說,高維數(shù)據(jù)的流動(dòng)有點(diǎn)抽象,不容易理解。
- 數(shù)據(jù)集的讀取,tensorboardX的使用。。。。各種東西要學(xué)習(xí)。在運(yùn)行別人的代碼后打印出信息,不僅看著上檔次,而且可以看到很多實(shí)用的信息。。。
AllenNLP是在pytorch基礎(chǔ)上的封裝,它的目標(biāo)是處理NLP任務(wù),可以減少很多額外的學(xué)習(xí)。
- 分詞,幫你用spacy,NLTK,或者簡單的按空格分詞處理。
- 數(shù)據(jù)集的讀取,它內(nèi)置了很多數(shù)據(jù)集的讀取,你可以在通過學(xué)習(xí)它的讀取方式,在它的基礎(chǔ)上對(duì)自己需要的數(shù)據(jù)集進(jìn)行讀取。 、
- 在Sentence -> Word ID -> Word Embedding的過程中,Glove,ELMo,BERT等常用的都可以直接使用,需要word,char粒度的都可以。
- log打印輸出,在內(nèi)置的輸出項(xiàng)之外,你可以很方便地加入想要輸出的信息。模型的各個(gè)組件中的參數(shù)都可以存在一個(gè)json/jsonnet文件中,修改參數(shù)進(jìn)行實(shí)驗(yàn)很方便。
3. 完整實(shí)例,預(yù)測論文發(fā)表場合
第一部分 數(shù)據(jù)集和模型
第四步 構(gòu)建Model
完成模型構(gòu)建之后,就可以進(jìn)行測試了,在這里先定義測試文件。
注意源文件中,測試文件被單獨(dú)放在一個(gè)文件夾中,有之前DatasetReader以及模型、預(yù)測的測試文件??戳艘幌麓_實(shí)原始數(shù)據(jù)和我下載的不一樣,在這里以教程的為準(zhǔn)。
from allennlp.common.testing import ModelTestCase
class AcademicPaperClassifierTest(ModelTestCase):
def setUp(self):
super(AcademicPaperClassifierTest,self).setUp()
self.set_up_model(
'tests/fixtures/academic_paper_classifier.json',
'tests/fixtures/s2_papers.jsonl'
)
def test_model_can_train_save_and_load(self):
self.ensure_model_can_train_save_and_load(self.param_file)
這個(gè)測試文件使用了allennlp.common.testing.ModelTestCase類,測試的是模型能夠訓(xùn)練、保存、恢復(fù)、預(yù)測。為了能夠很好的使用這些測試,我們還需要定義一個(gè)測試配置文件;構(gòu)造一個(gè)更小的輸入文件。
- tests/fixtures/academic_paper_classifier.json
- tests/fixtures/s2_papers.jsonl
這個(gè)方法不錯(cuò),之前測試都是直接加載數(shù)據(jù)運(yùn)行。
接下來就是模型了,注意這兩個(gè)輸入都轉(zhuǎn)換成序號(hào)。那么我們下一步自然就是需要把序號(hào)轉(zhuǎn)換成對(duì)應(yīng)的embeddings。
- inputs:title 和 abstract
- output:label
模型的結(jié)構(gòu)是由AllenNLP封裝好的。
模型構(gòu)造函數(shù)
from typing import Dict, Optional
import numpy
from overrides import overrides
import torch
import torch.nn.functional as F
from allennlp.common.checks import ConfigurationError
from allennlp.data import Vocabulary
from allennlp.modules import FeedForward, Seq2VecEncoder, TextFieldEmbedder
from allennlp.models.model import Model
from allennlp.nn import InitializerApplicator, RegularizerApplicator
from allennlp.nn import util
from allennlp.training.metrics import CategoricalAccuracy
@Model.register("paper_classifier")
class AcademicPaperClassifier(Model):
'''
這個(gè)``Model``為學(xué)術(shù)論文執(zhí)行文本分類。我們假設(shè)我們有一個(gè)標(biāo)題和一個(gè)摘要,我們預(yù)測一些輸出標(biāo)簽。
基本模型結(jié)構(gòu):我們將嵌入標(biāo)題和摘要,并使用單獨(dú)的Seq2VecEncoders對(duì)它們進(jìn)行編碼,獲得表示每個(gè)內(nèi)容的單個(gè)向量。然后我們將這兩個(gè)向量連接起來,并通過前饋網(wǎng)絡(luò)傳遞結(jié)果,我們將使用它作為每個(gè)標(biāo)簽的分?jǐn)?shù)。
'''
def __init__(self, vocab: Vocabulary,
text_field_embedder: TextFieldEmbedder,
title_encoder: Seq2VecEncoder,
abstract_encoder: Seq2VecEncoder,
calssifier_feedforward: FeedForward,
initializer: InitializerApplicator = InitializerApplicator(),
regularizer: Optional[RegularizerApplicator] = None
) -> None:
super(AcademicPaperClassifier,self).__init__(vocab,regularizer)
self.text_field_embeeder=text_field_embedder
self.abstract_encoder=abstract_encoder
self.title_encoder=title_encoder
self.abstract_encoder=abstract_encoder
self.calssifier_feedforward=calssifier_feedforward
self.metrics = {
"accuracy": CategoricalAccuracy(),
"accuracy3": CategoricalAccuracy(top_k=3)
}
self.loss = torch.nn.CrossEntropyLoss()
initializer(self)
模型前饋神經(jīng)網(wǎng)絡(luò)
類似DatasetReader注冊(cè)模型,方便配置文件的查找。注意,這里出現(xiàn)了一個(gè)奇怪的參數(shù)Vocabulary,這個(gè)參數(shù)顧名思義就是我們的數(shù)據(jù)字典,但是我們?cè)谀睦飿?gòu)造的呢?答案是不用構(gòu)造!寫model的時(shí)候順手寫上去就行啦,這個(gè)是Allennlp幫助我們寫好的。
同時(shí)這個(gè)數(shù)據(jù)字典其實(shí)是個(gè)復(fù)合字典,包括所有TextField的字典,以及LabelField自己單獨(dú)的字典。然后需要介紹的參數(shù)就是TextFieldEmbedder為所有的TextField類共同建立了一個(gè)embeddings。
利用這個(gè)embeddings以及我們輸入的序號(hào),我們就能夠獲得一個(gè)向量組成的序列。下一步就是對(duì)這個(gè)序列進(jìn)行變化。在這里我們使用的是Seq2VecEncoder。這個(gè)Encoder可以有很多的變化,在這里我們使用的是最最簡單的一種,就是bag of embeddings,直接求平均。當(dāng)然啦,我們也可以使用什么CNN啦,RNN,Transformer模型。
前饋神經(jīng)網(wǎng)絡(luò)呢也是一個(gè)預(yù)先定義好的Module,我們可以修改這個(gè)網(wǎng)絡(luò)的深度寬度激活函數(shù)。InitializerApplicator包含著所有參數(shù)的基本初始化方法。如果你想自定義初始化,就需要時(shí)候用RegularizerApplicator
def forward(self,
title: Dict[str, torch.LongTensor],
abstract:Dict[str,torch.LongTensor],
label:torch.LongTensor=None
)-> Dict[str, torch.Tensor]:
embedded_title=self.text_field_embeeder(title)
title_mask = util.get_text_field_mask(title)
encoded_title = self.title_encoder(embedded_title, title_mask)
embedded_abstract = self.text_field_embedder(abstract)
abstract_mask = util.get_text_field_mask(abstract)
encoded_abstract = self.abstract_encoder(embedded_abstract, abstract_mask)
logits = self.classifier_feedforward(torch.cat([encoded_title, encoded_abstract], dim=-1))
class_probabilities = F.softmax(logits)
output_dict = {"class_probabilities": class_probabilities}
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
我們首先注意到的應(yīng)該是這個(gè)函數(shù)的參數(shù)。在這里,參數(shù)的名字一定要和DatasetReader中定義的名字保持一致。AllenNLP在這里將會(huì)自動(dòng)的利用你的DatasetReader并且把數(shù)據(jù)組織成batches的形式。注意,forward函數(shù)接收的參數(shù)正是一個(gè)batch的數(shù)據(jù)。
注意,把labels也傳遞給forward函數(shù)用于計(jì)算損失函數(shù)。在訓(xùn)練的時(shí)候,我們的模型會(huì)主動(dòng)的去尋找這個(gè)loss,然后自動(dòng)的反向傳播回去,然后更改參數(shù)。同時(shí)我們也應(yīng)該注意到,這個(gè)參數(shù)是可以為空的,這主要是為了應(yīng)對(duì)prediction的情況。這個(gè)將會(huì)在后面章節(jié)中進(jìn)行介紹。
輸入的類型。label是一個(gè)[batch_size,1]大小的tensor。title和abstract兩個(gè)是TextField類型的,這些TextField轉(zhuǎn)換為字典類型的。這個(gè)新的字典呢可能包括了單詞id,字母array或者pos標(biāo)簽ID什么的。embedder直接一股腦的扔進(jìn)去就能夠幫你完成轉(zhuǎn)換過程。這就意味著我們TextFieldEmbedder必須和TextField完全對(duì)應(yīng)。對(duì)接的過程又是在配置文件中完成的。
模型的decode和metric
現(xiàn)在我們已經(jīng)理解了模型的基本輸入,來看看它的基本邏輯。
- 找到title和abstract的embeddings,然后對(duì)這些向量進(jìn)行操作。注意我們需要利用一個(gè)叫masks的變量來標(biāo)識(shí)哪些元素僅僅是用來標(biāo)識(shí)邊界的,而不需要模型考慮。
- 我們對(duì)這些向量進(jìn)行了一通操作之后,生成了一個(gè)向量。將這個(gè)向量輸入一個(gè)前饋神經(jīng)網(wǎng)絡(luò)中就可以得到logits(預(yù)測為各個(gè)類的概率),有了這個(gè)概率我們就可以得到最終預(yù)測的結(jié)果。
- 如果是訓(xùn)練過程的話,我們還需要計(jì)算損失和評(píng)價(jià)標(biāo)準(zhǔn)。
decode函數(shù)包括兩個(gè)功能
- 接收forward函數(shù)的返回值,并且對(duì)這個(gè)返回值進(jìn)行操作,比如說算出具體是那個(gè)詞啊等等。
- 將數(shù)字變成字符,方便閱讀。好啦,至此我們的模型已經(jīng)構(gòu)建好啦,現(xiàn)在我們可以測試?yán)病?/li>
@overrides
def decode(self, output_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
class_probabilities = F.softmax(output_dict['logits'], dim=-1)
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
@overrides
def get_metrics(self, reset: bool = False) -> Dict[str, float]:
return {metric_name: metric.get_metric(reset) for metric_name, metric in self.metrics.items()}
訓(xùn)練模型
在這里就是使用JSON完成一個(gè)配置文件。
在這里調(diào)整了訓(xùn)練迭代數(shù),實(shí)際上這個(gè)實(shí)驗(yàn)我沒跑完,自己手動(dòng)寫的老有問題,我懷疑是文件路徑出錯(cuò)了,就把源文檔的test下載下來,跑通了。
看樣子以后要保存一個(gè)文件結(jié)構(gòu)。
{
"dataset_reader": {
"type": "s2_papers"
},
"train_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/train.jsonl",
"validation_data_path": "https://s3-us-west-2.amazonaws.com/allennlp/datasets/academic-papers-example/dev.jsonl",
"model": {
"type": "paper_classifier",
"text_field_embedder": {
"token_embedders": {
"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
}
}
},
"title_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"abstract_encoder": {
"type": "lstm",
"bidirectional": true,
"input_size": 100,
"hidden_size": 100,
"num_layers": 1,
"dropout": 0.2
},
"classifier_feedforward": {
"input_dim": 400,
"num_layers": 2,
"hidden_dims": [200, 3],
"activations": ["relu", "linear"],
"dropout": [0.2, 0.0]
}
},
"iterator": {
"type": "bucket",
"sorting_keys": [["abstract", "num_tokens"], ["title", "num_tokens"]],
"batch_size": 64
},
"trainer": {
"num_epochs": 10,
"patience": 2,
"cuda_device": -1,
"grad_clipping": 5.0,
"validation_metric": "+accuracy",
"optimizer": {
"type": "adagrad"
}
}
}
以下為訓(xùn)練結(jié)果
2019-03-10 17:43:05,747 - INFO - allennlp.common.util - Metrics: {
"best_epoch": 7,
"peak_cpu_memory_MB": 0,
"training_duration": "00:24:52",
"training_start_epoch": 0,
"training_epochs": 8,
"epoch": 8,
"training_accuracy": 0.9099333333333334,
"training_accuracy3": 1.0,
"training_loss": 0.24596523192334682,
"training_cpu_memory_MB": 0.0,
"validation_accuracy": 0.8095,
"validation_accuracy3": 1.0,
"validation_loss": 0.5315047986805439,
"best_validation_accuracy": 0.814,
"best_validation_accuracy3": 1.0,
"best_validation_loss": 0.5119817899540067
}