AllenNLP 1.1 : Part1 クイックスタート : 貴方の最初のモデル (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 10/07/2020 (1.1.0)
* 本ページは、AllenNLP ドキュメントの以下のページを翻訳した上で適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
- お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
- Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。
◆ お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。
株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション |
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/ |
Facebook: https://www.facebook.com/ClassCatJP/ |
Part1 クイックスタート : 貴方の最初のモデル
この章では AllenNLP を使用して最初のテキスト分類モデルを構築していきます。
ガイドのこのセクションでは、AllenNLP で行なうことができる最も基本的なことから素早く開始します : テキスト分類です。テキスト分類への簡潔なイントロダクションを与えてから、映画レビューがポジティブかネガティブな感情を表しているかを決定する単純な分類器を実装します。
1 テキスト分類とは何か
テキスト分類は最も単純な NLP タスクの一つで、そこではモデルは、ある入力テキストが与えられたとき、テキストのためのラベルを夜おs句します。説明のための下の図を見てください。
スパム・フィルタリング、センチメント解析とトピック検出のような、テキスト分類の様々なアプリケーションがあります。幾つかの例は下の表で示されます。
アプリケーション | 説明 | 入力 | 出力 |
スパム・フィルタリング | スパムメールを検出してフィルタリングする | 電子メール | スパム / Not スパム |
センチメント分析 | テキストの極性 (= polarity) を検出する | ツイート, レビュー | Positive / Negative |
トピック検出 | テキストのトピックを検出する | ニュース記事, ブログ投稿 | ビジネス / Tech / スポーツ |
2. 入力と出力を定義する
NLP モデルを構築するための最初のステップはその入力と出力を定義することです。AllenNLP では、各訓練サンプルは Instance オブジェクトで表されます。Instance は一つまたはそれ以上の Field から成り、そこでは各 Field はモデルで使用されるデータの一つのピースを表します、入力か出力としてです。Field は tensor に変換されてモデルに供給されます。Reading Data 章 はテキストデータを表す Instance と Field を使用することについての詳細を提供します。
テキスト分類については、入力と出力は非常に単純です。モデルは入力テキストを表す TextField を取りそのラベルを予測します、これは LabelField で表されます :
# Input text: TextField # Output label: LabelField
3 データを読む
NLP アプリケーションを構築するための最初のステップはデータセットを読みそれをある内部的なデータ構造で表すことです。
AllenNLP はデータを読むために DatasetReaders を使用します、そのジョブは raw データファイルを入力 / 出力 spec に適合する Instance に変換することです。テキスト分類ための spec は :
# Inputs text: TextField # Outputs label: LabelField
入力のために一つの Field をそして出力のためにもう一つを望みます、そしてモデルは出力を予測するために入力を使用します。
データセットは単純なデータファイル形式を持つことを仮定しています : [text] [TAB] [label]、例えば :
I like this movie a lot! [TAB] positive This was a monstrous waste of time [TAB] negative AllenNLP is amazing [TAB] positive Why does this have to be so complicated? [TAB] negative This sentence expresses no sentiment [TAB] neutral
4 DatasetReader を作成する
DatasetReader クラスから継承することで貴方自身の DatasetReader を実装できます。最小限、_read() メソッドをオーバーライドする必要があります、これは入力データセットを読み Instance を yield します。
@DatasetReader.register('classification-tsv') class ClassificationTsvReader(DatasetReader): def __init__(self): self.tokenizer = SpacyTokenizer() self.token_indexers = {'tokens': SingleIdTokenIndexer()} def _read(self, file_path: str) -> Iterable[Instance]: with open(file_path, 'r') as lines: for line in lines: text, label = line.strip().split('\t') text_field = TextField(self.tokenizer.tokenize(text), self.token_indexers) label_field = LabelField(label) fields = {'text': text_field, 'label': label_field} yield Instance(fields)
これは最小限の DatasetReader で reader.read(file) を呼び出すとき分類 Instance のリストを返します。このリーダーは入力ファイルの各行を取り、テキストを tokenizer (ここで示される SpacyTokenizer は spaCy に頼っています) を使用して分割し、そして構築した語彙を使用してそれらの単語を tensor として表します。
Instance に渡される fields 辞書で使用されるテキストとラベル・キーには特別な注意を払ってください – これらのキーは後で tensor を Model に渡すときパラメータ名として使用されます。
理想的には、Instance を作成するときに出力ラベルはオプションとなるでしょう、その結果 (例えば、デモで) ラベル付けされていないデータ上の予測を行なうために同じコードを利用できますが、この章の残りのためには物事を単純に保つためにそれは無視します。
より柔軟で fully-featured なリーダーのためにこれをより良くできるである多くの箇所があります ; より深く潜るためには DatasetReaders のセクションを見てください。
5 モデルを構築する
必要な次のものは Model です、これは Instance のバッチを取り、入力から出力を予測して、そして損失を計算します。
私達の Instance はこの入力/出力 spec を持つことを思い出してください :
# Inputs text: TextField # Outputs label: LabelField
また、DatasetReader のフィールドのためにこれらの名前 (text と label) を使用したことも忘れないでください。AllenNLP はそれらのフィールドを名前でモデルコードに渡しますので、モデルで同じ名前を使用する必要があります。
モデルは何をなすべきでしょう?
概念的には、テキストを分類するための一般モデルは以下を行ないます :
- 入力の各単語に相当するある特徴を得る
- それらの単語-レベル特徴をドキュメント-レベル特徴ベクトルに結合する
- ドキュメント-レベル特徴ベクトルをラベルの一つに分類する。
AllenNLP では、これらの概念的ステップの各々をコードで利用できる一般的な抽象に仕立てます、その結果、各ステップのための様々な具体的なコンポーネントを利用できる非常に柔軟なモデルを持つことができます。
トークン ID でテキストを表す
最初のステップは入力テキストの文字列をトークン ID に変更しています。これは (貴方がそのためにコードを書かなくても構わない) データ処理パイプラインのパートで前に使用した SingleIdTokenIndexer により処理されます。
トークンを埋め込む
Model が行なう最初のことは (入力として得た) 各トークン ID をベクトルに変換する Embedding 関数を適用することです。これは各入力トークンのためのベクトルを与えます、そしてここで巨大な tensor を持ちます。
Seq2Vec エンコーダを適用する
次に各入力トークンのためのベクトルのシークエンスを取りそれを単一ベクトルに squash するある関数を適用します。BERT のような事前訓練された言語モデルの時代以前には、これは典型的には LSTM か畳込みエンコーダでした。BERT では [CLS] トークンの埋め込みを単に取るかもしれません (それをどのように行なうかのそれ以上については 後で)。
ラベルに渡る分布を計算する
最後に、(バッチの各インスタンスのために) 単一の特徴ベクトルを取り、そしてそれをラベルとして分類します、これはラベル空間に渡るカテゴリカル確率分布を与えます。
6 モデルを実装する — コンストラクタ
AllenNLP モデル基本
私達のモデルが何をするかを知った今、それを実装する必要があります。最初に、AllenNLP で Model がどのように動作するかについて少し話します :
- AllenNLP Model は単に PyTorch Module です
- それは forward() メソッドを実装して、そして出力が辞書であることを要求します
- その出力は訓練の間 loss キーを含み、これはモデルを最適化するために使用されます
訓練ループは Instance のバッチを取り、それを Model.forward() に渡し、結果としての辞書から loss キーを掴み、そして勾配を計算してモデルのパラメータを更新するために backprop を使用します。訓練ループを実装しなくてもかまいません — これの総ては AllenNLP が処理してくれます (貴方が望めば行なうことはできますが)。
モデルを構築する
Model コンストラクタで、訓練することを望むパラメータの総てをインスタンス化する必要があります。AllenNLP では、これらのパラメータの殆どをコンストラクタ引数として取ることを 勧めます、その結果モデルコード自身を変更することなくモデルの挙動を configure できて、その結果モデルが何をするかについて高位で考えることができます。私達のテキスト分類モデルのためのコンストラクタはこのように見えます :
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
AllenNLP コードで型アノテーションを多く使用していることに気付くでしょう – これはコード可読性のため (単に名前の代わりに引数の型を知ればメソッドが何をしているか理解することをそれは容易にする方法です) そして幾つかのケースではあるマジックを行なうためにこららのアノテーションを利用するからです。
それらのケースの一つはコンストラクタ・パラメータで、そこではこれらの型アノテーションを利用して configuration ファイルから embedder とエンコーダを自動的に構築できます。より多くの情報については configuration ファイル の章を見てください。この章はまた @Model.register() への呼び出しについても話します。
その結果は、configuration ファイルとともに allennlp train コマンドを使用している場合、このコンストラクタを呼び出さなくてもかまいません、それは総て貴方のために処理されます。
語彙を渡す
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
Vocabulary は (単語とラベルのような) 語彙項目とそれらの整数 ID の間のマッピングを管理します。事前構築された訓練ループでは、語彙は訓練データを読んだ後に AllenNLP により作成され、それから Model にそれが構築されたとき渡されます。使用する総てのトークンとラベルを見つけてそれら総てに個別の名前空間で整数 ID を割当てます。これが起きる方法は完全に configurable です ; より多くの情報については このガイドの Vocabulary セクション を見てください。
DatasetReader で行なったことはデフォルト “labels” 名前空間にラベルを配置して、そして行 10 で語彙からのラベルの数を把握することです。
単語を埋め込む
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
初期単語埋め込みを得るため、AllenNLP の TextFieldEmbedder を利用します。この抽象は TextField により作成された tensor を取りそして各々一つを埋め込みます。これは私達の最も複雑な抽象です、NLP でこの特定の演算を行なう多くの方法があるため、そしてコードの変更なしにこれらの間で切り替えられることを望みます。ここでは詳細に入りません ; この抽象がどのように動作してそれをどのように利用するかに深く潜ることに専念した このガイドの章 全体を持ちます。当面知る必要がある総てはこれを forward() で得るテキストパラメータに適用して、そして shape (batch_size, num_tokens, embedding_dim) で、各入力トークンに対して単一埋め込みベクトルを持つ tensor を取り出すことです。
Seq2VecEncoder を適用する
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
トークンベクトルのシークエンスを単一ベクトルに squash するため、AllenNLP の Seq2VecEncoder 抽象を使用します。名前が包含するように、これはベクトルのシークエンスを取り単一ベクトルを返す演算をカプセル化します。私達のモジュールの総てはバッチ化された入力上で動作しますので、これは (batch_size, num_tokens, embedding_dim) のような shape の tensor を取りそして (batch_size, encoding_dim) のような shape の tensor を返します。
分類層を適用する
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
私達の Model が必要とする最後のパラメータは分類層です、これは Seq2VecEncoder の出力をロジットに変換できます、可能なラベル毎に一つの値です。これらの値は後で確率分布に変換されて損失を計算するために使用されます。
これをコンストラクタ引数として取る必要はありません、何故ならば単純な線形層を単に使用するからです、これはコンストラクタ内で計算できるサイズを持ちます – Seq2VecEncoder はその出力次元を知り、そして Vocabulary は幾つのラベルがあるかを知ります。
7 モデルを実装する — forward メソッド
次に、モデルの forward() メソッドを実装する必要があります、これは入力を取り、予測を生成し、そして損失を計算します。思い出してください、コンストラクタと入力/出力 spec はこのように見えます :
@Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, vocab: Vocabulary, embedder: TextFieldEmbedder, encoder: Seq2VecEncoder): super().__init__(vocab) self.embedder = embedder self.encoder = encoder num_labels = vocab.get_vocab_size("labels") self.classifier = torch.nn.Linear(encoder.get_output_dim(), num_labels)
# Inputs: text: TextField # Outputs: label: LabelField
ここでは Model.forward() の内部でこれらのパラメータをどのように使用するかを示します、これは入力/出力 spec に適合する引数を得ます (何故ならばそれが私達が DatasetReader をどのようにコーディングしたかだからです)。
Model.forward()
forward では、入力を出力に変換するためにコンストラクタで作成したパラメータを利用します。出力を予測した後、真の出力にどれほど近く到達したかに基づいて損失関数を計算してから損失を返します (望む他のものが何であれそれとともに)、その結果、パラメータを訓練するためにそれを利用できます。
class SimpleClassifier(Model): def forward(self, text: Dict[str, torch.Tensor], label: torch.Tensor) -> Dict[str, torch.Tensor]: # Shape: (batch_size, num_tokens, embedding_dim) embedded_text = self.embedder(text) # Shape: (batch_size, num_tokens) mask = util.get_text_field_mask(text) # Shape: (batch_size, encoding_dim) encoded_text = self.encoder(embedded_text, mask) # Shape: (batch_size, num_labels) logits = self.classifier(encoded_text) # Shape: (batch_size, num_labels) probs = torch.nn.functional.softmax(logits) # Shape: (1,) loss = torch.nn.functional.cross_entropy(logits, label) return {'loss': loss, 'probs': probs}
forward() への入力
class SimpleClassifier(Model): def forward(self, text: TextFieldTensors, label: torch.Tensor) -> Dict[str, torch.Tensor]: # Shape: (batch_size, num_tokens, embedding_dim) embedded_text = self.embedder(text) # Shape: (batch_size, num_tokens) mask = util.get_text_field_mask(text) # Shape: (batch_size, encoding_dim) encoded_text = self.encoder(embedded_text, mask) # Shape: (batch_size, num_labels) logits = self.classifier(encoded_text) # Shape: (batch_size, num_labels) probs = torch.nn.functional.softmax(logits) # Shape: (1,) loss = torch.nn.functional.cross_entropy(logits, label) return {'loss': loss, 'probs': probs}
気付くべき最初のことはこの関数への入力です。AllenNLP 訓練ループが動作する方法は DatasetReader で使用したフィールド名を取りそして forward でそれらと同じ名前を持つインスタンスのバッチを与えます。そのため、私達のフィールド名として text と label を使用しましたので、forward への引数を同じ方法で名前付ける必要があります。
2 番目に、これらの引数の型に気付いてください。Field の各型はそれ自身を torch.Tensor にどのように変換するかを知り、それから Instance のバッチから同じ名前を持つ Field の総てからバッチ化 torch.Tensor を作成します。text と label のために見る型は TextField と LabelField から生成された tensor です (再度、TextFieldTensors のより多くの情報については using TextField の章 を見てください) 。知るべき重要な部分は (コンストラクタで作成した) TextFieldEmbedder が入力としてこのタイプのオブジェクトを想定して出力として埋め込み tensor を返すことです。
テキストを埋め込む
class SimpleClassifier(Model): def forward(self, text: Dict[str, torch.Tensor], label: torch.Tensor) -> Dict[str, torch.Tensor]: # Shape: (batch_size, num_tokens, embedding_dim) embedded_text = self.embedder(text) # Shape: (batch_size, num_tokens) mask = util.get_text_field_mask(text) # Shape: (batch_size, encoding_dim) encoded_text = self.encoder(embedded_text, mask) # Shape: (batch_size, num_labels) logits = self.classifier(encoded_text) # Shape: (batch_size, num_labels) probs = torch.nn.functional.softmax(logits) # Shape: (1,) loss = torch.nn.functional.cross_entropy(logits, label) return {'loss': loss, 'probs': probs}
私達が行なう最初の実際のモデリング演算はテキストを埋め込むことで、各入力トークンのためのベクトルを得ます。ここでは、どのようにその演算が成されるかについて何も指定していないことに気付いてください、単に、コンストラクタで得た TextFieldEmbedder はそれを行なっていきます。これは後で私達を非常に柔軟にさせます、モデルコードを変更することなく (ELMo と BERT を含む) 様々な種類の埋め込みメソッドや事前訓練された表現の間で変更します。
Seq2VecEncoder を適用する
class SimpleClassifier(Model): def forward(self, text: Dict[str, torch.Tensor], label: torch.Tensor) -> Dict[str, torch.Tensor]: # Shape: (batch_size, num_tokens, embedding_dim) embedded_text = self.embedder(text) # Shape: (batch_size, num_tokens) mask = util.get_text_field_mask(text) # Shape: (batch_size, encoding_dim) encoded_text = self.encoder(embedded_text, mask) # Shape: (batch_size, num_labels) logits = self.classifier(encoded_text) # Shape: (batch_size, num_labels) probs = torch.nn.functional.softmax(logits) # Shape: (1,) loss = torch.nn.functional.cross_entropy(logits, label) return {'loss': loss, 'probs': probs}
テキストを埋め込んだあと、次に (トークン毎に一つの) ベクトルのシークエンスをテキスト全体のための単一ベクトルに squash しなければなりません。コンストラクタ引数として得た Seq2VecEncoder を使用してそれを行ないます。異なる長さを持つ可能性があるテキストのピースを一緒にバッチ化しているとき正しく動作するため、embedded_text tensor で要素を mask する必要があります、それらはパディングのためにそこにあるだけです。TextField 出力からマスクを得るためにユティリティ関数を使用してから、そのマスクをエンコーダに渡します。
これらの行(s) の最後に、バッチの各インスタンスのための単一ベクトルを持ちます。
予測を行なう
class SimpleClassifier(Model): def forward(self, text: Dict[str, torch.Tensor], label: torch.Tensor) -> Dict[str, torch.Tensor]: # Shape: (batch_size, num_tokens, embedding_dim) embedded_text = self.embedder(text) # Shape: (batch_size, num_tokens) mask = util.get_text_field_mask(text) # Shape: (batch_size, encoding_dim) encoded_text = self.encoder(embedded_text, mask) # Shape: (batch_size, num_labels) logits = self.classifier(encoded_text) # Shape: (batch_size, num_labels) probs = torch.nn.functional.softmax(logits) # Shape: (1,) loss = torch.nn.functional.cross_entropy(logits, label) return {'loss': loss, 'probs': probs}
モデルの最後のステップは、バッチの各インスタンスのためのベクトルを取りそしてそのためのベクトルを予測することです。私達の分類器は torch.nn.Linear 層で各可能なラベルのためにスコア (一般にロジットと呼ばれます) を与えます。このモデルの消費者に返すことができる、ラベルに渡る確率分布を得るために softmax 演算を使用してそれらのスコアを正規化します。損失を計算するためには、予測するロジットと正解ラベル分布の間の交差エントロピーを計算する組込み関数を PyTorch は持ち、そしてそれを私達の損失関数として利用します。
And that’s it! これは単純な分類器のために貴方が必要なもの総てです。貴方が DatasetReader と Model を書いた後、AllenNLP は残りを処理します : 入力ファイルをデータセット・リーダーに接続し、インスタンスを一緒に賢くバッチ化してそれらをモデルに供給し、そして損失上で backprop を使用することによりモデルのパラメータを最適化します。このパートを次の章で調べます。
以上