$30 off During Our Annual Pro Sale. View Details »

NLPの研究を加速させるAllenNLP入門 / Introduction to AllenN...

NLPの研究を加速させるAllenNLP入門 / Introduction to AllenNLP to Accelerate NLP Research

爆速でNLPの研究を進める上でオススメな AllenNLP を紹介します。

■ イベント:【NLP Hacks vol.6】『実装』に特化した、NLP勉強会コミュニティ開催!
https://nlp-hacks.connpass.com/event/250755/

■ 登壇概要
タイトル:NLPの研究を加速させるAllenNLP入門

Shunsuke KITADA

July 22, 2022
Tweet

More Decks by Shunsuke KITADA

Other Decks in Technology

Transcript

  1. • 理工学研究科 応用情報工学専攻 博士3年 • 2021年度から学振特別研究員 DC2 🎓 研究分野: 人工知能・深層学習

    • 🤖 自然言語処理 (Natural Language Processing; NLP) ◦ 文字形状を考慮した自然言語処理 ▪ 日本語の漢字に着目 [Kitada+ AIPRW’18, Aoki+ AACL SRW’20] ▪ アラビア語のアラビア文字に着目 [Daif+ ACL SRW’20] ◦ 摂動に頑健で解釈可能な深層学習 [Kitada+ IEEE Access’21, Kitada+ CoRR’21] • 🏥 医用画像処理 (コンピュータビジョン) ◦ 皮膚画像を用いた画像認識による悪性黒色腫の自動診断システム [Kitada+ CoRR’18] • 📝 計算機広告 (マルチモーダル) ◦ 配信効果の高い広告クリエイティブの作成支援 [Kitada+ KDD’19] (データサイエンスの最難関国際会議) ◦ 配信効果の低い広告クリエイティブの停止支援 [Kitada+ Appl. Sci.‘22] 自己紹介 北田 俊輔 Shunsuke KITADA ホームページ: shunk031.me 2 就活中...?
  2. AllenNLP, 開発終了してメンテナンスモードに入るってよ • Disable dependabot, add notice of future sunsetting

    by epwalsh · Pull Request #5685 · allenai/allennlp https://github.com/allenai/allennlp/pull/5685 ◦ 2022年の12月16日で allennlp は新規開発終了し メンテナンスモードへ • 後継の allenai/tango も allennlp の思想を受け継いでいる ◦ せっかくチュートリアル資料を用意してきたので、 追悼の意味も込めて発表します 😂 4
  3. “ : PyTorchをベースにした オープンソースのNLPライブラリ” がやっぱり凄い • 🥰 NLPのための深層学習モデル構築を簡単に ◦ Allen

    Institute for AI (AI2)によって開発された PyTorchベースの OSS (AI2 はNLPのトップ会議に多数論文採択) ◦ 高品質な深層 NLP モデルを構築したいと考える 研究者・エンジニア・学生 などを対象として設計 • 🥰 NLPタスクに対する高レベルな抽象化とAPIを提供 ◦ 一般的なコンポーネントや (最新) モデルをサポート ◦ NLP実験の実行と管理を容易にする、拡張可能な枠組み • 😂 日本語での情報がまだまだ少ない?だから今日改めて紹介!! ◦ v1.0ぐらいまでの資料しかない(2022/07現在は v2.10.0) ▪ v2.0 になってからも、allennlp は進化しています ◦ 便利すぎてロックインされてしまう (良いのか悪いのか…) 6 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  4. のよさと最近のアップデートを少しだけ • 👍 バグが少なく・爆速で実験用のコードが書ける ◦ NLP関連のコードを自分で書くと高確率でバグる・遅い ▪ 可変長のデータの取り扱いは極力ライブラリに頼りたい • 👍

    注力すべき実装部分にのみ集中して取り組める ◦ 興味のある ”モデル構築” や ”データのロード” 部分だけ に集中して取り組みたいのに... いろいろ書かなくちゃいけないコードが沢山… ◦ 学習ループやロギング周りってだいたい面倒(でも重要) ▪ tensorboard や wandb 用のコードって綺麗に書けないがち… • 👍 Vision&Language の盛り上がりにより NLP の ライブラリなのに画像を扱うタスクにも容易に適用可能 ◦ マルチモーダルなタスクや V&L タスクが簡単に ▪ allennlp のみで画像分類もできちゃいます 7 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  5. 深層学習モデル (特に NLP) の実験コードを書くときに困ること • プロジェクトの root にオレオレスクリプトが生えがち ◦ 愉快な

    preprocess.py, preprocess_01.py, train.py, predict.py, train_sugoi.py みたいな仲間たちが爆誕🔥しやすい • スクリプトのディレクトリ構成になやむ ◦ とりあえず util.py みたいなものを作って突っ込む!? • vocabulary を管理したり可変長テキストを頑張ったり ◦ NLP実験におけるバグの温床。いつも秘伝のタレを使う ▪ padding とか masking まわり、愚直に書くと遅いがち • (固定長の画像を扱うような研究はやりやすそう…) • そのままシュッとAPIサーバ化してデモを作りたい ◦ なんも考えずに実験コード書いてたら、無理です 😂 8 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  6. AllenNLP の全体像 と 本日話す範囲 9 データ セットの 読み込み データ セットの

    前処理 モデルの 構築 モデルの 学習と評価 モデルの API化 allennlp で サポート allennlp-server で サポート allennlp DatasetReader allennlp Model $ allennlp train /path/to/config.jsonnet -s /path/to/serialization-dir $ allennlp serve … 集中して実装するのはここだけ! allennlp Trainer • AllenNLP の一般的な実装のお作法 ◦ DatasetReader と Model を実装し、allennlp train でポン! • AllenNLP の枠組みに乗ったいい感じの前処理コマンドの実装法 ◦ Subcommand を使ってプロジェクトrootを汚さずに前処理等を書く! • allennlp-server で学習済みモデルを API 化する方法 ◦ 「そのままそれデモにしてよ〜」という無茶振りに対応する! AllenNLPを使う上での 前処理ベストプラクティス のようなものを話します AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  7. 深層学習モデル (特に NLP) の実験コードを書くときに困ること • プロジェクトの root にオレオレスクリプトが生えがち 問題 ➜

    AllenNLP の Subcommand を使って、前処理用の コマンド (例えば: allennlp preprocess-dataset 的な) を準備する! ▪ 前処理以外にも必要そうなコマンドを自前で用意可能 • スクリプトのディレクトリ構成になやむ 問題 ➜ AllenNLP ライブラリのディレクトリ構成を踏襲する! ▪ 深層学習モデルを扱うアプリケーションは全部これでいい • vocabulary を管理したり可変長テキストを頑張ったり 問題 ➜ すべて AllenNLP に頼る! ▪ 必要な操作はほとんどすべて AllenNLP にあります • そのままシュッとAPIサーバ化してデモを作りたい 問題 ➜ allennlp-server を使えばそのまますぐ API 化できる! 10 を使うと解決! AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  8. 深層学習モデル (特に NLP) の実験コードを書くときに困ること • プロジェクトの root にオレオレスクリプトが生えがち 問題 ➜

    AllenNLP の Subcommand を使って、前処理用の コマンド (例えば: allennlp preprocess-dataset 的な) を準備する! ▪ 前処理以外にも必要そうなコマンドを自前で用意可能 • スクリプトのディレクトリ構成になやむ 問題 ➜ AllenNLP ライブラリのディレクトリ構成を踏襲する! ▪ 深層学習モデルを扱うアプリケーションは全部これでいい • vocabulary を管理したり可変長テキストを頑張ったり 問題 ➜ すべて AllenNLP に頼る! ▪ 必要な操作はほとんどすべて AllenNLP にあります • そのままシュッとAPIサーバ化してデモを作りたい 問題 ➜ allennlp-server を使えばそのまますぐ API 化できる! 11 を使うと解決! AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  9. 深層学習モデル (特に NLP) の実験コードを書くときに困ること 12 を使うと解決! • スクリプトのディレクトリ構成になやむ ➜ AllenNLP

    ライブラリの ディレクトリ構成を踏襲する! ▪ 深層学習モデルを扱う アプリケーションは全部これでいい AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  10. AllenNLPことはじめ Your first model @AllenNLP Guide 14 • テキスト分類を例に What

    is text classification • 入出力を定義 Defining input and output • データを読み込む Reading data • DatasetReaderを実装 Making a DatasetReader • Modelを構築 Building your model • Modelを実装 Implementing the model - the constructor Implementing the model - the forward method AllenNLP を用いたテキストの分類について説明 テキスト分類について簡単に説明した後、 映画のレビューが肯定的な感情を表しているか 否定的な感情を表しているかを判断する分類器を実装 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  11. AllenNLPことはじめ: テキスト分類を例に 15 • テキスト分類は最も一般的なNLPタスクの一つ ◦ 入力テキストから、モデルは入力からラベルを予測 • テキスト分類のアプリケーションは様々 ◦

    スパムフィルタリング、感情分析、トピック検出 等 アプリケーション どんなタスク? 入力 出力 スパムフィルタリング スパムメールを検出 電子メール スパム / スパムではない 感情分析 テキストの極性を検出 ツイート・レビュー ポジティブ / ネガティブ トピック検出 テキストのトピックを検出 ニュース・ブログ記事 経済 / 技術 / スポーツ AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  12. AllenNLPことはじめ: 入出力を定義 16 • AllenNLP ではそれぞれのデータは Instance として扱う ◦ Instance

    は 1 つ以上の Field からなる ▪ 各 Field はモデルで使われるデータ (入力/出力) を表す ◦ Field はテンソルに変換され、モデルに入力される • テキスト分類の場合、入力と出力はシンプル ◦ 入力はテキストを表す TextField ◦ 出力はラベルを表す LabelField AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand Instance # Input text: TextField # Input text: LabelField torch.Tensor へ torch.Tensor へ
  13. AllenNLPことはじめ: データを読み込む 17 • データを読み込むために DatasetReader を使う ◦ 生データ (e.g.,

    txt, csv, json) から入出力の 仕様に合うよう Instance に変換する役割 # Input text: TextField # Output label: LabelField 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 • 生データは以下のような形式を仮定: [text] [TAB] [label] AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  14. AllenNLPことはじめ: DatasetReaderを実装 18 • DatasetReader を継承して独自の DatasetReader を実装 ◦ 最低限必要なのは

    _read() メソッドの override @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) • reader.read(file)を呼ぶと Instance のリストが返却 ◦ 入力ファイルの各行に対して tokenizer を使ってテキストを 単語に分割し、token_indexers を使って単語IDに変換 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  15. @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) AllenNLPことはじめ: DatasetReaderを実装 19 • DatasetReader を継承して独自の DatasetReader を実装 ◦ 最低限必要なのは _read() メソッドの override • reader.read(file)を呼ぶと Instance のリストが返却 ◦ 入力ファイルの各行に対して tokenizer を使ってテキストを 単語に分割し、token_indexers を使って単語IDに変換 Instance に渡される fields 辞書で設定 されている text と label の key に注目 これらの key は、のち に Model にテンソルを 渡すときに仮引数名 として使われます AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  16. AllenNLPことはじめ: Modelを構築 20 • 入力から出力を予測し、loss を計算する Model を構築 ◦ Model

    は Instance の batch を受け取る • DatasetReader の Fields に text や label を 使用していたことを思い出してください ◦ AllenNLP は Field の key を仮引数名として渡します # Input text: TextField # Output label: LabelField allenai/allennlp/gradient_descent_trainer.py#L403 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  17. AllenNLPことはじめ: Modelを実装 (1)-1 21 • AllenNLP で Model がどのように動作するか ◦

    AllenNLP の Model は PyTorch の nn.Module がベース ◦ forward()メソッドを実装し、出力は Dict[str, Any] ◦ 出力には学習時の loss key が含まれ、モデルの最適化に利用 • 学習ループは以下の通り: ◦ Instance の batch を Model.forward() に通す ◦ 結果の辞書から loss key の値を取得 ◦ backprop を使って勾配を計算しモデルのパラメータを更新 • 学習ループを明示的に実装する必要なし! AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  18. AllenNLPことはじめ: Modelを実装 (1)-2 22 • Model のコンストラクタでは、学習させたいすべての パラメータをインスタンス化する必要あり ◦ AllenNLP

    ではこれらのパラメータのほとんどを コンストラクタの引数として渡すことを推奨している ▪ モデルの実装自体を変更せずにモデルの動作を設定可能 ▪ モデルの動作をより高いレベルで考えられるようになる @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) この SimpleClassifier を例に 順にコードを見ていきます AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  19. AllenNLPことはじめ: Modelを実装 (1)-2 23 • Model のコンストラクタでは、学習させたいすべての パラメータをインスタンス化する必要あり ◦ AllenNLP

    ではこれらのパラメータのほとんどを コンストラクタの引数として渡すことを推奨している ▪ モデルの実装自体を変更せずにモデルの動作を設定可能 ▪ モデルの動作をより高いレベルで考えられるようになる @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 では 型アノテーション が重要な役割を担っている • コードが読みやすくなる • (最高機能)型アノテーションを使って 設定ファイルから embedder や encoder を 自動的に構築することができる!!!(後述) allennlp train コマンドを設定ファイルと共に 使っているならば、追加の実装なしで簡単に実現可能 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  20. AllenNLPことはじめ: Modelを実装 (1)-3 24 • 語彙を司る Vocabulary インスタンスを渡す ◦ Vocabulary

    は語彙に関連する情報 (単語, ラベル etc.) と そのID間のマッピングを管理している ◦ 学習ループでは、学習データを読み込んだ後に AllenNLP が Vocabulary を使って語彙を作成し、Model に渡す (隠蔽済み) @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 は学習に使用される すべての token と label を見つけてくる ◦ それぞれを別の名前空間でIDを付与する ▪ Token ID: 0 -> [CLS] ▪ Label ID: 0 -> positive • DatasetReader では、ラベルをデフォルトの labels 名前空間に置いている ◦ “labels” を指定して、ラベル数を取得 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  21. AllenNLPことはじめ: Modelを実装 (1)-4 25 • TextFieldEmbedder を使って単語IDから埋め込みを得る ◦ TextField によって作られたテンソルを受け取って埋め込む

    ▪ NLPで頻発するこの処理をAllenNLPでは高度に抽象化 • 実装を変更することなく、設定ファイルから切り替え可能 ◦ forward()で得られるtextパラメータに適用することで 入力トークンに対して埋め込みテンソルが得られる @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) • embedder を通して得られる テンソルのサイズは (batch_size, num_tokens, embedding_dim) AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  22. AllenNLPことはじめ: Modelを実装 (1)-5 26 • Seq2VecEncoder を適用する ◦ 複数の token

    embeddings を1つ sentence embedding に まとめるため、AllenNLP における Seq2VecEncoder を使用 ▪ 名前の通り sequence から vector を返す抽象クラス ▪ RNN, CNN を始め Transformerベースのモデルも使用可能 @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) • encoder を通して得られる テンソルのサイズは (batch_size, encoding_dim) AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  23. AllenNLPことはじめ: Modelを実装 (1)-6 27 • これまで計算してきた特徴ベクトルから分類する ◦ 分類用のレイヤーを全結合層で定義 ▪ Seq2VecEncoder

    の出力を logit に変換し 各ラベルごとに1つの値を出力する ▪ これらの値は後に確率分布に変換されて損失計算に使用 @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) • Seq2VecEncoder.get_output() により 出力次元を得ることができるため、 その値を使って全結合層を定義 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  24. AllenNLPことはじめ: Modelを実装 (2)-1 28 • Model.forward() を実装する ◦ 入力を受け取り、予測を出力し、損失を計算する役割 •

    これまで扱ってきた SimpleClassifier と入出力を再度確認: ◦ DatasetReader で作成した Instance の Field key と 一致するように forward() の引数を定義する @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) # Input text: TextField # Output label: LabelField AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  25. AllenNLPことはじめ: Modelを実装 (2)-2 29 • Model.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} # Input text: TextField # Output label: LabelField AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  26. AllenNLPことはじめ: Modelを実装 (2)-2 30 • Model.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} # Input text: TextField # Output label: LabelField メソッドの入力に注目💡 • AllenNLPの学習ループでは、DatasetReader で 作った Field 名を forward で同じ Field 名を 持つインスタンスのバッチを渡す • Field 名として text と label を使ったので、 forward の引数も同じように名前をつける AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  27. AllenNLPことはじめ: Modelを実装 (2)-2 31 • Model.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} # Input text: TextField # Output label: LabelField 引数の型に注目💡 • 各 Field は自身を torch.Tensor に変換する 方法が実装されているため、Instance のバッチ から torch.Tensor のバッチに変換される • text と label は TextField と LabelField TextFieldTensor と torch.Tensor へ変換 • TextFieldEmbedder は TextFieldTensor を 期待し、出力として torch.Tensor を返す AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  28. AllenNLPことはじめ: Modelを実装 (2)-3 32 • TextFieldEmbedder を用いてテキストを埋め込む ◦ テキストを埋め込んで各入力トークンに対してベクトルを取得 ◦

    抽象化により、様々な埋め込み方法を実装の変更なく試行可能 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} • 埋め込み取得の操作は高度に隠蔽されている ◦ コンストラクタで設定した TextFieldEmbedder が操作するとだけ記述 • この抽象化によって実装を大きく変更することなく Word2Vec, ELMo, BERT 等を使用可能 ◦ 実験の変更に対する頑健性が大きく向上 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  29. AllenNLPことはじめ: Modelを実装 (2)-4 33 • Seq2VecEncoder を適用する ◦ token embeddings

    を 1つの sentence embedding へ変換する ◦ 可変長テキストをバッチ化したときの padding まわりの処理がラク 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} • 異なる長さのテキストはバッチ化されるときに パディングされているため、その箇所を mask する ◦ get_text_field_mask() が便利 • encoder に embedded_text を渡すときは、 padding の箇所を示す mask も忘れずに渡す AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  30. 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ことはじめ: Modelを実装 (2)-4 34 • これまで計算した特徴ベクトルを元に予測を出力 ◦ nn.Linear 層で定義された classifier を使いラベルの候補ごとに スコア (一般的に logit と呼ばれる) を出力させる • logits に softmax を適用して確率分布を得る ◦ 最終的にモデルの予測結果を見るときに便利 • logits に cross_entropy を適用して損失を得る ◦ PyTorch の cross entropy は内部で softmax を適用していることに注意 (よく間違えます😂) AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  31. AllenNLP でモデルを学習・予測させる Training and prediction @AllenNLP Guide 36 • 自作スクリプトによるモデルの学習

    Training the model with your own script • allennlp trainによるモデルの学習 Training the model with your own script • モデルの評価 Evaluating the model • ラベルのない入力に対する予測 Making predictions for unlabeled data テキスト分類モデルを学習し、新しい入力に対して予測させる2通りの方法を説明します • 自分でスクリプトを書いてDatasetReaderとModelを構築し、学習ループを実行する方法 • 設定ファイルを書いて allennlp train コマンドを使う方法 AllenNLP は allennlp train コマンドを使うことでさらに強力な実験PDCAサイクルを実現します! スクリプトを書くなら pytorch lightning とかでもいいのでは?とも思います😂 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  32. def build_model(vocab: Vocabulary) -> Model: print("Building the model") vocab_size =

    vocab.get_vocab_size("tokens") embedder = BasicTextFieldEmbedder( {"tokens": Embedding(10, vocab_size)}) encoder = BagOfEmbeddingsEncoder(embedding_dim=10) return SimpleClassifier(vocab, embedder, encoder) AllenNLP学習予測: allennlp trainコマンドで (1) 37 • 設定ファイルから DatasetReader や Model を定義 ◦ json 形式の設定ファイルに、様々なオブジェクトの コンストラクタ・パラメータを定義しておく • 👈の json は👇の python script と同じ意味 ◦ type に合わせてコンストラクタパラメータを定義 ◦ SimpleClassifier は Model の subclass で あり、BagOfEmbeddingsEncoder は Seq2VecEncoder の subclass であることに注意 ▪ Model.register 等で登録されている "model": { "type": "simple_classifier", "embedder": { "token_embedders": { "tokens": { "type": "embedding", "embedding_dim": 10 } } }, "encoder": { "type": "bag_of_embeddings", "embedding_dim": 10 } } json python script AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  33. AllenNLP学習予測: allennlp trainコマンドで (2) 38 • 設定ファイルを用いて allennlp train でモデルを学習させる

    $ allennlp train /path/to/config.json -s /path/to/serialization-dir • json 設定ファイルから学習に必要なコンポーネントを 自動的に構成する Registrable 機構 ◦ DatasetReader, Model, その他の component を allennlp コマンドで認識させるためには .register() の呼び出しを実行する必要あり ▪ 各componentをインスタンス化する際に実行 ◦ Registrable 機構を活用するためには 以下のどちらかが必要: ▪ 実行時に --include-package [my_python_module] というフラグを追加 ▪ allennlp の plugin 機構を利用 (オススメ; 後述) "model": { "type": "simple_classifier", "embedder": { "token_embedders": { "tokens": { "type": "embedding", "embedding_dim": 10 } } }, "encoder": { "type": "bag_of_embeddings", "embedding_dim": 10 } } json AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  34. 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) self.accuracy(logits, label) return {'loss': loss, 'probs': probs} AllenNLP学習予測: モデルの評価 (1) 39 • AllenNLP の評価指標まわりを抽象化した Metric を使用 ◦ 学習中の各種メトリクスを追跡するのに便利な機能を提供 ▪ 正解率を追跡する CategoricalAccuracy の使用例 @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) self.accuracy = CategoricalAccuracy() 👇コンストラクタで追加して 👉 各 forward pass で logit と label を渡して更新 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  35. 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) self.accuracy(logits, label) return {'loss': loss, 'probs': probs} AllenNLP学習予測: モデルの評価 (2) 40 • allennlp evaluate コマンドで評価(のみ)を実行 ◦ 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) self.accuracy = CategoricalAccuracy() 評価結果は指定した serialization_dir (allennlp train コマンド時), output_file (allennlp evaluate コマンド時) に json 形式で保存される {'accuracy': 0.855, 'loss': 0.3686505307257175} AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  36. AllenNLP学習予測: ラベルのない入力に対する予測 (1) 41 • Predictor を実装する ◦ AllenNLP はラベル無しデータに対して予測を出力するような

    デモ環境での予測用に、Predictor でモデルを wrap する ▪ json形式の入力を受け取り、DatasetReader を使って Instance に変換したのちに Model へと入力する ▪ 最終的な予測結果を再度 json 形式に変換して返却 @Predictor.register("sentence_classifier") class SentenceClassifierPredictor(Predictor): def _json_to_instance(self, json_dict: JsonDict) -> Instance: sentence = json_dict["sentence"] return self._dataset_reader.text_to_instance(sentence) def predict(self, sentence: str) -> JsonDict: # This method is implemented in the base class. return self.predict_json({"sentence": sentence}) 学習済み Model を wrap する Predictor があるおかげで、 容易に allennlp-server を 使用した API 化が可能 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  37. allennlp-server でラクラク API 化 43 • 学習済み allennlp Model を使って爆速でデモを作る

    ◦ allennlp では allennlp train コマンドで設定した serialization_dir にモデルと学習状態を tar.gz 形式で出力 • allennlp-server でデモサーバーを構築する (参考: allenai/allennlp-demo) ◦ とても簡単な 2 step で完結 python allennlp-server/server_simple.py \ --archive-path model/model.tar.gz \ --predictor sentence_classifier \ --field-name sentence --include-package my_text_classifier pip install allennlp-server curl --header 'Accept:application/json' \ --header 'Content-Type:application/json' \ -v -X POST -d '{"sentence": "Did Uriah honestly think he could beat The Legend of Zelda in under three hours?"}' http://localhost:8000/predict/named-entity-recognition いい感じの demo 画面が立ち上がり、 シュッと結果を確認可能 curl 等を使って API として使うことも可能 (1) (2) AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  38. AllenNLP を支える Registrable 機構 (1) • 複数ある前処理やモデルを “名前” で選んで実行したい ◦

    比較実験をする際 様々な組み合わせが存在 ▪ dict に “名前” と前処理や モデルの定義を詰めた 比較実験用 registry みたいなものを爆誕させる ▪ 便利な一方で、逐一この dict を更新しなくてはならず󰷹 45 MODELS: Dict[str, type(nn.Module)] = {"bert": BERT, "roberta": RoBERTa, "gpt": GPT} bert = MODELS["bert"].from_pretrained('bert-base-uncased') PREPROCESS_FUNCS: Dict[str, Callable] = { "prep01": preprocess01_func, "prep02": preprocess02_func, "prep03": preprocess03_func, } ret = PREPROCESS_FUNCS['prep02'](*args, **kwargs) • Registrableを継承したクラスはそのサブクラスに 対して名前付き registry へのアクセス得る ◦ registry へ登録するにはデコレータ BaseClass.register(name) をつけるだけ AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  39. class Registrable(FromParams): _registry: ClassVar[DefaultDict[type, _SubclassRegistry]] = defaultdict(dict) @classmethod def register(

    cls, name: str, ... ) -> Callable[[Type[_T]], Type[_T]]: registry = Registrable._registry[cls] ... AllenNLP を支える Registrable 機構 (2) 46 • Registrable クラス内部では同じように dict に追加している ◦ Registrableの_registry に追加されたクラスの情報 が管理されている • Registrable を使用する ことで、デコレータで登録 したクラスを簡単に呼び出し可能 ◦ 設定ファイルの type を読んで、インスタンス化 @Model.register('simple_classifier') class SimpleClassifier(Model): def __init__(self, ... @DatasetReader.register('classification-tsv') class ClassificationTsvReader(DatasetReader): def __init__(self): self.tokenizer = SpacyTokenizer() ... AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  40. Registrable クラスを継承した サブクラスを構築する上でのハマりポイント 47 allennlp コマンド実行時に、対象のサブクラスが import されるようにする(2パターンあり) • --include-package

    オプションで指定する ◦ 指定したディレクトリ配下の import を再帰的に実行 ▪ __init__.py に実装したサブクラスを書いておけば OK • (オススメ) .allennlp_plugins で指定する ◦ パッケージ名を書いておくことで、 そのパッケージの __init__.py が読み込まれる ▪ パッケージ root の __init__.py に実装したサブクラスを 書いておく必要あり • ref. allenai/allennlp-template-config-files AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  41. Registrable な Subcommand を元に オレオレ前処理コマンドを実装する • プロジェクト root に オレオレ前処理用スクリプトの

    残骸が散りばりがち問題 ◦ 試行錯誤しているうちに増殖 ◦ 最初に見るプロジェクト root が 汚いとやる気が無くなり、実験が 嫌になり、研究も嫌になる… • AllenNLP の Subcommand を 使いつつ、前処理スクリプト等 を綺麗なディレクトリ構造を 保って実装していきたい 49 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand 📁 datasets 📁 utils 📝 train.py 📝 preprocess.py 📝 preprocess_01.py 📝 preprocess_02_sugoi.py 📁 datasets 📁 configs 📁 my_project 📝 .allennlp_plugins Before w/o AllenNLP After w/ AllenNLP
  42. AllenNLP のサブコマンドと Subcommand クラス 50 • AllenNLP には複数のコマンドが存在する ◦ allennlp

    train, evaluate, predict, … ▪ これらは Registrable な Subcommand を継承して 構築されている • Subcommand クラス ◦ AllenNLP で使われるサブコマンドを表す抽象クラス ▪ 内部では argparse が使われている ◦ 独自のコマンドを作りたい場合は、このクラスを継承 しつつ、Subcommand.register(name) のようにして コマンドを登録する必要あり AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand
  43. Subcommand を元にオレオレサブコマンドを実装 (1) 51 世の中にあまりサンプルコードがなかったので実装を公開 shunk031/allennlp-custom-subcommand-sample • hello-subcommand というコマンドを作ってみる AllenNLP

    について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand $ allennlp --help 2022-07-13 22:12:44,209 - INFO - allennlp.common.plugins - Plugin my_project available usage: allennlp [-h] [--version] ... Run AllenNLP optional arguments: -h, --help show this help message and exit --version show program's version number and exit Commands: train Train a model. evaluate Evaluate the specified model + dataset(s). predict Use a trained model to make predictions. ... hello-subcommand This is the first custom subcommand $ allennlp hello-subcommand --message world! 2022-07-13 22:09:51,665 - INFO - allennlp.co… Hello world! 実際に実行すると...
  44. • .allennlp_plugins を作成 ◦ プロジェクトの root に作成し、 パッケージ名 (例: my_project)

    を記述する • my_project/commands 以下に ディレクトリとファイルを作成 ◦ hello-subcommand 用のディレクトリ • my_project/commands/hello_ subcommand 以下にファイルを作成 ◦ function.py と sub_command.py の2つに分けて作成することを おすすめします Subcommand を元に オレオレサブコマンドを実装 (2) 52 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand 📁 my_project 📝 .allennlp_plugins project root dir 📁 commands 📝 __init__.py my_project/ 📁 hello_subcommand 📝 __init__.py my_project/commands/ 📝 __init__.py 📝 function.py 📝 sub_command.py my_project/commands/hello_ subcommand
  45. @Subcommand.register("hello-subcommand") class HelloSubcommand(Subcommand): def add_subparser( self, parser: argparse._SubParsersAction ) ->

    argparse.ArgumentParser: description = "custom subcommand for just say hello with your message" # create subparser from the parser subparser = parser.add_parser( self.name, description=description, help="This is the first custom subcommand", ) # add arguments subparser.add_argument("--message", type=str, default="world") # set the function to be called when this subcommand is executed. subparser.set_defaults(func=lambda args: hello_function(msg=args.message)) return subparser • my_project/commands/hello_ subcommand 以下のファイルを実装 Subcommand を元に オレオレサブコマンドを実装 (3) 53 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand my_project/commands/hello _subcommand 📝 __init__.py 📝 function.py 📝 sub_command.py def hello_function(msg: str) -> None: print(f"Hello {msg}") sub_command.py: Registrable な Subcommand を継承して 必要なメソッドをオーバーライド function.py: サブコマンドのロジックを実装
  46. • .allennlp_plugins で指定した パッケージから再帰的に実装した サブコマンドが読み込まれるように準備する • allennlp コマンド実行時、 .allennlp_plugins に記述された

    my_project の __init__.py を import しにいくので、最終的には HelloSubcommand が import される allennlp コマンド実行時に実装した サブコマンドが読み込まれるようにする 54 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand from my_project.commands import * # NOQA from my_project.commands.hello\ _subcommand.sub_command import HelloSubcommand # NOQA 📁 my_project 📝 .allennlp_plugins project root dir 📁 commands 📝 __init__.py my_project/ 📁 hello_subcommand 📝 __init__.py my_project/commands/ 📝 __init__.py 📝 function.py 📝 sub_command.py my_project/commands/hello_ subcommand
  47. AllenNLPをマスターすると研究が爆速進捗します • いくつか allennlp のお作法があるものの、 使いこなすとサクッとモデルを構築できます ◦ DatasetReader や Model

    を実装するだけ ◦ 拡張性も高いので、柔軟にオレオレモデルが構築可能 • 実験コードにありがちなハウルの動く城なコードが allennlp の流儀に従うと綺麗なまま保つことが出来ます ◦ Subcommand 等を組み合わせながら前処理等も 整理整頓されながらコードを書くことができます ◦ Model はそのまま Predictor に渡すことで API 化 • 残念ながら開発終了してしまいましたが ◦ コードの品質は高く、コードリーディングすると楽しい ◦ 後継の tango も allennlp の Registrable な思想あり 56
  48. AllenNLPを入門する上で参考になる資料たち • A Guide to Natural Language Processing With AllenNLP

    ◦ AllenAi が公開している AllenNLP のチュートリアル。 まずはここを眺めて手を動かして動作を確認することを おすすめ。本資料の作成にも大変お世話になりました。 • 実践!AllenNLPによるディープラーニングを用いた 自然言語処理 - Speaker Deck ◦ 具体的なタスクと紐付け、付属の notebook を用いて 実際にコードを動かせる高品質な説明資料です。 • AllenNLPを使った開発 - Speaker Deck ◦ AllenNLPの全体を把握するのにわかりやすい資料です。 今回の資料を作る前はこちらの資料を借りて説明していました。 • kagglerのためのAllenNLPチュートリアル - Speaker Deck ◦ 研究もそうですが、Kaggle でも使える AllenNLP! 57