四種常見NLP框架使用總結

哈工大SCIR發表於2019-08-13

引言

隨著人工智慧的發展,越來越多深度學習框架如雨後春筍般湧現,例如PyTorch、TensorFlow、Keras、MXNet、Theano和PaddlePaddle等。這些基礎框架提供了構建一個模型需要的基本通用工具包。但是對於NLP相關的任務,我們往往需要自己編寫大量比較繁瑣的程式碼,包括資料預處理和訓練過程中的工具等。因此,大家通常基於NLP相關的深度學習框架編寫自己的模型,如OpenNMT、ParlAI和AllenNLP等。藉助這些框架,三兩下就可以實現一個NLP相關基礎任務的訓練和預測。但是當我們需要對基礎任務進行改動時,又被程式碼封裝束縛,舉步維艱。因此,本文主要針對於如何使用框架實現自定義模型,幫助大家快速瞭解框架的使用方法。

我們首先介紹廣泛用於NLP/CV領域的TensorFlow框架——Tensor2Tensor,該框架提供了NLP/CV領域中常用的基本模型。然後介紹NLP領域的AllenNLP框架,該框架基於PyTorch平臺開發,為NLP模型提供了統一的開發架構。接著在介紹NLP領域中重要的兩個子領域,神經機器翻譯對話系統常用的框架,OpenNMT和ParlAI。通過這四個框架的介紹,希望能幫助大家瞭解不同開發平臺,不同領域下的NLP框架的使用方式。

框架名稱應用領域開發平臺
Tensor2TensorNLP/CVTensorFlow
AllenNLPNLPPyTorch
OpenNMTNLP-機器翻譯PyTorch/TensorFlow
ParlAINLP-對話PyTorch

四種常見NLP框架使用總結

一、Tensor2Tensor

Tensor2Tensor[1]是一個基於TensorFlow的較為綜合性的庫,既包括一些CV 和 NLP的基本模型,如LSTM,CNN等,也提供一些稍微高階一點的模型,如各式各樣的GAN和Transformer。對NLP的各項任務支援得都比較全面,很方便容易上手。

由於該資源庫仍處於不斷開發過程中,截止目前為止,已經有3897次commit,66個release 版本,178 contributors。在2018年《Attention is all you need》這個全網熱文中,該倉庫是官方提供的Transformer模型版本,後面陸陸續續其餘平臺架構才逐漸補充完成。

Tensor2Tensor(Transformer)使用方法

注意:有可能隨著版本迭代更新的過程中會有區域性改動

安裝環境

1. 安裝CUDA 9.0 (一定是9.0,不能是9.2)

2. 安裝TensorFlow (現在是1.12)

3. 安裝Tensor2Tensor (參考官網安裝)

開始使用

1. 資料預處理

這一步驟是根據自己任務自己編寫一些預處理的程式碼,比如字串格式化,生成特徵向量等操作。

2. 編寫自定義problem:

  • 編寫自定義的problem程式碼,一定需要在自定義類名前加裝飾器(@registry.registry_problem)。

  • 自定義problem的類名一定是駝峰式命名,py檔名一定是下劃線式命名,且與類名對應。

  • 一定需要繼承父類problem,t2t已經提供用於生成資料的problem,需要自行將自己的問題人腦分類找到對應的父類,主要定義的父類problem有:(執行 t2t-datagen 可以檢視到problem list)。

  • 一定需要在__init__.py檔案裡匯入自定義problem檔案。

3. 使用t2t-datagen 將自己預處理後的資料轉為t2t的格式化資料集【注意路徑】

  • 執行 t2t-datagen --help 或 t2t-datagen --helpfull。例如:

1cd scripts && t2t-datagen --t2t_usr_dir=./ --data_dir=../train_data --tmp_dir=../tmp_data --problem=my_problem
  • 如果自定義problem程式碼的輸出格式不正確,則此命令會報錯

4. 使用t2t-trainer使用格式化的資料集進行訓練

  • 執行t2t-trainer --help 或 t2t-trainer --helpfull。例如:

1cd scripts && t2t-trainer --t2t_usr_dir=./ --problem=my_problem --data_dir=../train_data --model=transformer --hparams_set=transformer_base --output_dir=../output --train_steps=20 --eval_steps=100

5. 使用t2t-decoder對測試集進行預測【注意路徑】

  • 如果想使用某一個checkpoint時的結果時,需要將checkpoint檔案中的第一行: model_checkpoint_path: “model.ckpt-xxxx” 的最後的序號修改即可。例如:

1cd scripts && t2t-decoder --t2t_usr_dir=./ --problem=my_problem --data_dir=../train_data --model=transformer --hparams_set=transformer_base --output_dir=../output --decode_hparams=”beam_size=5,alpha=0.6” --decode_from_file=../decode_in/test_in.txt --decode_to_file=../decode_out/test_out.txt

6. 使用t2t-exporter匯出訓練模型

7. 分析結果

附: (整體程式碼)
 1# coding=utf-8
 2from tensor2tensor.utils import registry
 3from tensor2tensor.data_generators import problem, text_problems
 4
 5@registry.register_problem
 6class AttentionGruFeature(text_problems.Text2ClassProblem):
 7
 8    ROOT_DATA_PATH = '../data_manager/'
 9    PROBLEM_NAME = 'attention_gru_feature'
10
11    @property
12    def is_generate_per_split(self):
13        return True
14
15    @property
16    def dataset_splits(self):
17        return [{
18            "split": problem.DatasetSplit.TRAIN,
19            "shards": 5,
20        }, {
21            "split": problem.DatasetSplit.EVAL,
22            "shards": 1,
23        }]
24
25    @property
26    def approx_vocab_size(self):
27        return 2 ** 10  # 8k vocab suffices for this small dataset.
28
29    @property
30    def num_classes(self):
31        return 2
32
33    @property
34    def vocab_filename(self):
35        return self.PROBLEM_NAME + ".vocab.%d" % self.approx_vocab_size
36
37    def generate_samples(self, data_dir, tmp_dir, dataset_split):
38        del data_dir
39        del tmp_dir
40        del dataset_split
41
42        # with open('{}self_antecedent_generate_sentences.pkl'.format(self.ROOT_DATA_PATH), 'rb') as f:
43        #     # get all the sentences for antecedent identification
44        #     _sentences = pickle.load(f)
45        #
46        # for _sent in _sentences:
47        #     # # sum pooling, FloatTensor, Size: 400
48        #     # _sent.input_vec_sum
49        #     # # sum pooling with feature, FloatTensor, Size: 468
50        #     # _sent.input_vec_sum_feature
51        #     # # GRU, FloatTensor, Size: 6100
52        #     # _sent.input_vec_hidden
53        #     # # GRU with feature, FloatTensor, Size: 6168
54        #     # _sent.input_vec_hidden_feature
55        #     # # AttentionGRU, FloatTensor, Size: 1600
56        #     # _sent.input_vec_attention
57        #     # # AttentionGRU with feature, FloatTensor, Size: 1668
58        #     # _sent.input_vec_attention_feature
59        #     # # tag(1 for positive case, and 0 for negative case), Int, Size: 1
60        #     # _sent.antecedent_label
61        #     # # tag(1 for positive case, and 0 for negative case), Int, Size: 1
62        #     # _sent.trigger_label
63        #     # # trigger word for the error analysis, Str
64        #     # _sent.trigger
65        #     # # trigger word auxiliary type for the experiment, Str
66        #     # _sent.aux_type
67        #     # # the original sentence for the error analysis, Str
68        #     # _sent.sen
69        #
70        #     yield {
71        #         "inputs": _sent.input_vec_attention_feature,
72        #         "label": _sent.antecedent_label
73        #     }
74
75        with open('../prep_ante_data/antecedent_label.txt') as antecedent_label, open(
76                '../prep_ante_data/input_vec_attention_gru_feature.txt') as input_vec:
77            for labal in antecedent_label:
78                yield {
79                    "inputs": input_vec.readline().strip()[1:-2],
80                    "label": int(labal.strip())
81                }
82
83        antecedent_label.close()
84        input_vec.close()
85
86
87# PROBLEM_NAME='attention_gru_feature'
88# DATA_DIR='../train_data_atte_feature'
89# OUTPUT_DIR='../output_atte_feature'
90# t2t-datagen --t2t_usr_dir=. --data_dir=$DATA_DIR --tmp_dir=../tmp_data --problem=$PROBLEM_NAME
91# t2t-trainer --t2t_usr_dir=. --data_dir=$DATA_DIR --problem=$PROBLEM_NAME --model=transformer --hparams_set=transformer_base --output_dir=$OUTPUT_DIR

Tensor2Tensor使用總結

T2T 是Google 非官方提供的倉庫,是社群廣大愛好者共同努力建設的簡單入門型框架,底層封裝TF,能滿足大部分CV 和 NLP的任務,很多主流成熟的模型也已經都有實現。直接繼承或實現一些框架內預設的介面,就可以完成很多工。入門起來非常友好,並且文件更新也較為及時。認真閱讀文件(或閱讀報錯資訊)就可以瞭解並使用該框架,方便許多非大幅創新模型的復現。

四種常見NLP框架使用總結

二、AllenNLP

AllenNLP是一個基於PyTorch的NLP研究庫,可為開發者提供語言任務中的各種業內最佳訓練模型。官網提供了一個很好的入門教程[2],能夠讓初學者在30分鐘內就瞭解AllenNLP的使用方法。

AllenNLP使用方法

由於AllenNLP已經幫我們實現很多麻煩瑣碎的預處理和訓練框架,我們實際需要編寫的只有:

1. DatasetReader

DatasetReader的示例程式碼如下所示。

 1from typing import Dict, Iterator
 2
 3from allennlp.data import Instance
 4from allennlp.data.fields import TextField
 5from allennlp.data.dataset_readers import DatasetReader
 6from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer
 7from allennlp.data.tokenizers import WordTokenizer, Tokenizer
 8
 9@DatasetReader.register('custom')
10class CustomReader(DatasetReader):
11
12    def __init__(self, tokenizer: Tokenizer = None, token_indexers: Dict[str, TokenIndexer] = None) -> None:
13        super().__init__(lazy=False)
14        self.tokenizer = tokenizer or WordTokenizer()
15        self.word_indexers = token_indexers or {"word": SingleIdTokenIndexer('word')}
16
17    def text_to_instance(self, _input: str) -> Instance:
18        fields = {}
19        tokenized_input = self.tokenizer.tokenize(_input)
20        fields['input'] = TextField(tokenized_input, self.word_indexers)
21        return Instance(fields)
22
23    def _read(self, file_path: str) -> Iterator[Instance]:
24        with open(file_path) as f:
25            for line in f:
26                yield self.text_to_instance(line)

首先需要自定義_read函式,寫好讀取資料集的方式,通過yield方式返回構建一個instance需要的文字。然後通過text_to_instance函式將文字轉化為instance。在text_to_instance函式中,需要對輸入的文字進行切分,然後構建fileld

self.tokenizer是用來切分文字成Token的。有Word級別的也有Char級別的。self.word_indexers是用來索引Token並轉換為Tensor。同樣TokenIndexer也有很多種,在實現自己的模型之前可以看看官方文件有沒有比較符合自己需要的型別。如果你需要構建多個Vocabulary,比如源語言的vocab 和目標語言的vocab, 就需要在這裡多定義一個self.word_indexers。不同indexers在vocab中,是通過SingleIdTokenIndexer函式初始化的namespace來區分的,也就是15行程式碼中最後一個的'word'

2. Model

與PyTorch實現model的方式一樣,但需要注意的是:

@Model.register('') 註冊之後可以使用JsonNet進行模型選擇(如果你有多個模型,可以直接修改Json值來切換,不需要手動修改程式碼)。

由於AllenNLP封裝了Trainer,所以我們需要在model內實現或者選擇已有的評價指標,這樣在訓練過程中就會自動計算評價指標。具體方法是,在__init__方法中定義評價函式,可以從在官方文件[3]上看看有沒有,如果沒有的話就需要自己寫。

1self.acc = CategoricalAccuracy()

然後在forward方法中呼叫評價函式計算指標

1self.acc(output, labels)

最後在model的get_metrics返回對應指標的dict結果就行了。

1def get_metrics(self, reset: bool = False) -> Dict[str, float]:
2    return {"acc": self.acc.get_metric(reset)}
3. Trainer

一般來說直接呼叫AllenNLP的Trainer方法就可以自動開始訓練了。但是如果你有一些特殊的訓練步驟,比如GAN[4],你就不能單純地使用AllenNLP的Trainer,得把Trainer開啟進行每步的迭代,可以參考[4]中trainer的寫法。

AllenNLP使用總結

關於AllenNLP的學習程式碼,可以參考[5]。由於AllenNLP是基於PyTorch的,程式碼風格和PyTorch的風格基本一致,因此如果你會用PyTorch,那上手AllenNLP基本沒有什麼障礙。程式碼註釋方面也比較全,模組封裝方面比較靈活。AllenNLP的程式碼非常容易改動,就像用純的PyTorch一樣靈活。當然靈活也就意味著很多複雜的實現,AllenNLP目前還沒有,大部分可能都需要自己寫。AllenNLP依賴了很多Python庫,近期也在更新。

四種常見NLP框架使用總結

三、OpenNMT

OpenNMT[6]是一個開源的神經機器翻譯(neural machine translation)專案,採用目前普遍使用的編碼器-解碼器(encoder-decoder)結構,因此,也可以用來完成文字摘要、回覆生成等其他文字生成任務。目前,該專案已經開發出PyTorch、TensorFlow兩個版本,使用者可以按需選取。本文以PyTorch版本[7]為例進行介紹。

OpenNMT使用方法
1. 資料處理

作為一個典型的機器翻譯框架,OpenNMT的資料主要包含source和target兩部分,對應於機器翻譯中的源語言輸入和目標語言翻譯。OpenNMT採用TorchText中的Field資料結構來表示每個部分。使用者自定義過程中,如需新增source和target外的其他資料,可以參照source field或target field的構建方法,如構建一個自定義的user_data資料:

1fields["user_data"] = torchtext.data.Field(
2    init_token=BOS_WORD, eos_token=EOS_WORD,
3    pad_token=PAD_WORD,
4    include_lengths=True)

其中init_token、eos_token和pad_token分別為使用者自定義的開始字元、結束字元和padding字元。Include_lengths為真時,會同時返回處理後資料和資料的長度。

2. 模型

OpenNMT實現了注意力機制的編碼器-解碼器模型。框架定義了編碼器和解碼器的介面,在該介面下,進一步實現了多種不同結構的編碼器解碼器,可供使用者按需組合,如CNN、 RNN編碼器等。如使用者需自定義特定結構的模組,也可以遵循該介面進行設計,以保證得到的模組可以和OpenNMT的其他模組進行組合。其中,編碼器解碼器介面如下:

1class EncoderBase(nn.Module):
2    def forward(self, input, lengths=None, hidden=None):
3        raise NotImplementedError
4
5class RNNDecoderBase(nn.Module):
6    def forward(self, input, context, state, context_lengths=None):
7             raise NotImplementedError
3. 訓練

OpenNMT的訓練由Trainer.py中Trainer類控制,該類的可定製化程度並不高,只實現了最基本的序列到序列的訓練過程。對於多工、對抗訓練等複雜的訓練過程,需要對該類進行較大的改動。

OpenNMT使用總結

OpenNMT提供了基於PyTorch和TensorFlow這兩大主流框架的不同實現,能夠滿足絕大多數使用者的需求。對於基礎框架的封裝使得其喪失了一定的靈活性,但是對於編碼器-解碼器結構下文字生成的任務來說,可以省去資料格式、介面定義等細節處理,將精力更多集中在其自定義模組上,快速搭建出需要的模型。

四種常見NLP框架使用總結

四、ParlAI

ParlAI是Facebook公司開發出的一個專注於對話領域在很多對話任務上分享,訓練和評估對話模型的平臺[8]。這個平臺可以用於訓練和測試對話模型,在很多資料集上進行多工訓練,並且整合了Amazon Mechanical Turk,以便資料收集和人工評估。

ParlAI 中的基本概念:

  • world定義了代理彼此互動的環境。世界必須實施一種parley方法。每次對parley的呼叫都會進行一次互動,通常每個代理包含一個動作。

  • agent可以是一個人,一個簡單的機器人,可以重複它聽到的任何內容,完美調整的神經網路,讀出的資料集,或者可能傳送訊息或與其環境互動的任何其他內容。代理有兩個他們需要定義的主要方法:

1def observe(self, observation): #用觀察更新內部狀態
2def act(self): #根據內部狀態生成動作
  • observations是我們稱之為代理的act函式返回的物件,並且因為它們被輸入到其他代理的observe函式而被命名。這是ParlAI中代理與環境之間傳遞訊息的主要方式。觀察通常採用包含不同型別資訊的python詞典的形式。

  • teacher是特殊型別的代理人。他們像所有代理一樣實施act和observe功能,但他們也會跟蹤他們通過報告功能返回的指標,例如他們提出的問題數量或者正確回答這些問題的次數。

ParlAI 的程式碼包含如下幾個主要的資料夾[9]:

  • core包含框架的主要程式碼;

  • agents包含可以和不同任務互動的代理;

  • examples包含不同迴圈的一些基本示例;

  • tasks包含不同任務的程式碼;

  • mturk包含設定 Mechanical Turk 的程式碼及 MTurk 任務樣例。

ParlAI使用方法

ParlAI內部封裝了很多對話任務(如ConvAI2)和評測(如F1值和hits@1等等)。使用ParlAI現有的資料,程式碼以及模型進行訓練和評測,可以快速實現對話模型中的很多baseline模型。但由於程式碼封裝性太強,不建議使用它從頭搭建自己的模型。想在基礎上搭建自己的模型可以詳細參考官網中的教程[10]。

這裡簡單介紹直接利用內部的資料,程式碼以及模型進行訓練和評測的一個簡單例子(Train a Transformer on Twitter):

1. 列印一些資料集中的例子

1python examples/display_data.py -t twitter
2*# display first examples from twitter dataset*

2. 訓練模型

1python examples/train_model.py -t twitter -mf /tmp/tr_twitter -m transformer/ranker -bs 10 -vtim 3600 -cands batch -ecands batch --data-parallel True
2# train transformer ranker

3. 評測之前訓練出的模型

1python examples/eval_model.py -t twitter -m legacy:seq2seq:0 -mf models:twitter/seq2seq/twitter_seq2seq_model
2# Evaluate seq2seq model trained on twitter from our model zoo

4. 輸出模型的一些預測

1python examples/display_model.py -t twitter -mf /tmp/tr_twitter -ecands batch
2# display predictions for model saved at specific file on twitter
ParlAI使用總結

ParlAI有自己的一套模式,例如world、agent和teacher等等。程式碼封裝性特別好,程式碼量巨大,如果想查詢一箇中間結果,需要一層一層檢視呼叫的函式,不容易進行修改。ParlAI中間封裝了很多現有的baseline模型,對於對話研究者,可以快速實現baseline模型。目前ParlAI還在更新,不同版本之間的程式碼可能結構略有不同,但是ParlAI的核心使用方法大致相同。

五、總結

本文介紹了四種常見框架構建自定義模型的方法。Tensor2Tensor涵蓋比較全面,但是隻支援TensorFlow。AllenNLP最大的優點在於簡化了資料預處理、訓練和預測的過程。程式碼改起來也很靈活,但是一些工具目前官方還沒有實現,需要自己寫。如果是比較傳統的編碼器-解碼器結構下文字生成任務,使用OpenNMT能節省很多時間。但是如果是結構比較新穎的模型,使用OpenNMT搭建模型依舊是一個不小的挑戰。ParlAI內部封裝了很多對話任務,方便使用者快速復現相關的baseline模型。但由於程式碼封裝性太強和其特殊的模式,使用ParlAI從頭搭建自己的模型具有一定的挑戰性。每個框架都有各自的優點和弊端,大家需結合自身情況和使用方式進行選擇。但是不建議每個框架都試一遍,畢竟掌握每個框架還是需要一定時間成本的。

參考資料

[1] https://github.com/tensorflow/tensor2tensor

[2] https://allennlp.org/tutorials

[3] https://allenai.github.io/allennlp-docs/api/allennlp.training.metrics.html

[4] http://www.realworldnlpbook.com/blog/training-a-shakespeare-reciting-monkey-using-rl-and-seqgan.html

[5] https://github.com/mhagiwara/realworldnlp

[6] http://opennmt.net/

[7] https://github.com/OpenNMT/OpenNMT-py

[8] http://parl.ai.s3-website.us-east-2.amazonaws.com/docs/tutorial_quick.html

[9] https://www.infoq.cn/article/2017/05/ParlAI-Facebook-AI

[10] http://parl.ai.s3-website.us-east-2.amazonaws.com/docs/tutorial_basic.html

相關文章