Slide 1

Slide 1 text

NLPの研究を加速させる AllenNLP入門 北田 俊輔 理工学研究科 応用情報工学専攻 博士後期課程 3 年 彌冨研究室 所属 『実装』に特化した、NLPコミュニティ「NLP Hacks」, Jul. 22nd, 2022

Slide 2

Slide 2 text

● 理工学研究科 応用情報工学専攻 博士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 就活中...?

Slide 3

Slide 3 text

AllenNLP 開発終了のお知らせ 3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

AllenNLPについて 5

Slide 6

Slide 6 text

“ : 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

Slide 7

Slide 7 text

のよさと最近のアップデートを少しだけ ● 👍 バグが少なく・爆速で実験用のコードが書ける ○ NLP関連のコードを自分で書くと高確率でバグる・遅い ■ 可変長のデータの取り扱いは極力ライブラリに頼りたい ● 👍 注力すべき実装部分にのみ集中して取り組める ○ 興味のある ”モデル構築” や ”データのロード” 部分だけ に集中して取り組みたいのに... いろいろ書かなくちゃいけないコードが沢山… ○ 学習ループやロギング周りってだいたい面倒(でも重要) ■ tensorboard や wandb 用のコードって綺麗に書けないがち… ● 👍 Vision&Language の盛り上がりにより NLP の ライブラリなのに画像を扱うタスクにも容易に適用可能 ○ マルチモーダルなタスクや V&L タスクが簡単に ■ allennlp のみで画像分類もできちゃいます 7 AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand

Slide 8

Slide 8 text

深層学習モデル (特に 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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

深層学習モデル (特に NLP) の実験コードを書くときに困ること 12 を使うと解決! ● スクリプトのディレクトリ構成になやむ ➜ AllenNLP ライブラリの ディレクトリ構成を踏襲する! ■ 深層学習モデルを扱う アプリケーションは全部これでいい AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand

Slide 13

Slide 13 text

AllenNLP ことはじめ 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

AllenNLPことはじめ: テキスト分類を例に 15 ● テキスト分類は最も一般的なNLPタスクの一つ ○ 入力テキストから、モデルは入力からラベルを予測 ● テキスト分類のアプリケーションは様々 ○ スパムフィルタリング、感情分析、トピック検出 等 アプリケーション どんなタスク? 入力 出力 スパムフィルタリング スパムメールを検出 電子メール スパム / スパムではない 感情分析 テキストの極性を検出 ツイート・レビュー ポジティブ / ネガティブ トピック検出 テキストのトピックを検出 ニュース・ブログ記事 経済 / 技術 / スポーツ AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand

Slide 16

Slide 16 text

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 へ

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

@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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

AllenNLP でモデルを学習・予測 35

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

allennlp-server でラクラク API 化 42

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

AllenNLPを支える Registable 機構 44

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Registrable な Subcommand を元に オレオレ前処理コマンドを実装する 48

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

AllenNLP のサブコマンドと Subcommand クラス 50 ● AllenNLP には複数のコマンドが存在する ○ allennlp train, evaluate, predict, … ■ これらは Registrable な Subcommand を継承して 構築されている ● Subcommand クラス ○ AllenNLP で使われるサブコマンドを表す抽象クラス ■ 内部では argparse が使われている ○ 独自のコマンドを作りたい場合は、このクラスを継承 しつつ、Subcommand.register(name) のようにして コマンドを登録する必要あり AllenNLP について > ことはじめ > 学習・予測 > API 化 > Registrable 機構 > オレオレ Subcommand

Slide 51

Slide 51 text

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! 実際に実行すると...

Slide 52

Slide 52 text

● .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

Slide 53

Slide 53 text

@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: サブコマンドのロジックを実装

Slide 54

Slide 54 text

● .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

Slide 55

Slide 55 text

まとめ 55

Slide 56

Slide 56 text

AllenNLPをマスターすると研究が爆速進捗します ● いくつか allennlp のお作法があるものの、 使いこなすとサクッとモデルを構築できます ○ DatasetReader や Model を実装するだけ ○ 拡張性も高いので、柔軟にオレオレモデルが構築可能 ● 実験コードにありがちなハウルの動く城なコードが allennlp の流儀に従うと綺麗なまま保つことが出来ます ○ Subcommand 等を組み合わせながら前処理等も 整理整頓されながらコードを書くことができます ○ Model はそのまま Predictor に渡すことで API 化 ● 残念ながら開発終了してしまいましたが ○ コードの品質は高く、コードリーディングすると楽しい ○ 後継の tango も allennlp の Registrable な思想あり 56

Slide 57

Slide 57 text

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