2021年で面白かったTransformer関連論文

この記事はみらい翻訳アドベントカレンダー14日目の記事です。

2021年も終わりを迎える中、個人的には転職してからちょうど1年経ちました。

機械翻訳の研究開発に携わることもあり、自然言語処理や深層学習全般を中心にいろいろな論文を読んだ一年でした。

年末にありがちな今年の論文BEST10のようなランキングを作ってみようと考えたが、選定とランキング基準がなかなか定まらず、それだけで数日かかりそうだったので、Transformer関連論文に絞ってまとめてみようと思います。

今年も昨年に続きTransformer is all you needの色が一層強くなったと感じます。Transformer自体は自然言語処理を題材に提案されたモデルですが、最近は画像領域や音声領域でも高い性能を発揮しています。 強く注目されているモデルということもあり、構造の細部にフォーカスした多くの研究がありましたので、その一部を紹介したいと思います。

Attention関連

Attention Is All You Needのタイトルが示すように、TransformerはAttentionが中心的な役割を果たすと考えられてきました。 ところが昨年から従来考えられてきたAttentionの役割を覆す研究が出現し、従来のAttentionに代わる構造がいくつか提案されました。

  • Synthesizer: Rethinking Self-Attention in Transformer Models

    • TransformerのAttention層を学習させることなく、ランダムな値を維持した状態でも学習させた場合と比べてあまり精度劣化しないという衝撃的な実験結果を明らかにしました。
    • これまではTransformerの心臓部と考えられたAttentionでしたが、実はそれほど重要ではないかもしれないという問題提起を行った重要な論文です。これを機にAttentionの再考がされはじめた印象があります。
  • Pay Attention to MLPs

    • Attention内のクエリーとキーのsoftmax計算をすることなく、入力に対する残差計算をそのままに、シンプルなゲート構造を導入することで、softmax計算を排除したgMLP構造を提案。
    • オリジナルのTransformerと同程度の性能を維持しつつ、少数のパラメータと推論時間の高速化を実現。
    • タイトルと論文中ではMLPを強調していることから、MLPがTransformerを代替えしたと勘違いがちですが、提案されたモデルの構造は極めてTransformerに似ているので、正確にはAttentionの代替といったところでしょう。
    • また、gMLP構造では入力の空間情報を利用できることで、位置エンコーディングが不要になるという性質はとても興味深いです。これに関する研究はまだまだ多くされる予感がします。
  • FNet: Mixing Tokens with Fourier Transforms

    • gMLPと同様に、Attention内部のsoftmax計算の代わりにフーリエ変換を適用したモデルです。
    • フーリエ変換処理ではパラメータを学習させる必要がないため、学習や推論時は速度は大きく向上します。一般的にはオリジナルのTransformerと比較すると精度は劣るが、入力系列が長いほど精度が向上するのは興味深いです。

位置エンコーディング

Attentionに注目が集まりやすいTransformerですが、位置エンコーディングも陰でいろいろ工夫されてきました。

Transformerで採用された位置エンコーディング三角関数を用いて各トークンが系列における絶対位置を表現します。BERT以降は位置エンコーディング自体を学習させたり、絶対位置ではなく相対位置を利用するといった工夫がなされてきました。一方それらの工夫に関する詳細な説明は割愛されることが多く、位置エンコーディングに対しては個人的に理解が曖昧な存在でした。

文字レベル分割

自然言語処理では昔から単語分割は変わらず重要な位置づけです。コーパスサイズが大きいほど語彙の扱いが難しくなり、推論時の未知語問題を考えなければなりません。 近年ではサブワードの登場により、語彙に関する悩みは減りましたが、それでも語彙集や分割手法に気を配る必要があります。

こういった背景から、単語ではなく文字単位を用いることで全体の処理が簡潔化され、一連の処理をエンドツーエンドで完結させることが期待されます。文字単位の問題点としては、1文字が1トークンとして扱われることにより、一度で処理できる系列の長さが限られてしまうことです。前述でも述べましたが、一般的にTransformerでは系列の長さに対してO(n2)の計算量が要されるので、この問題点は無視できません。

Transformerを用いながら、効率的に文字単位の分割を処理できる論文が注目を浴びました。

  • CANINE: Pre-training an Efficient Tokenization-Free Encoder for Language Representation

    • Transformerモデルで文字レベルの入力をうまく扱うモデルです。
    • 入力文の各文字に対して複数のハッシュ化を通じて数値化し、それらを連結して文の数値表現とする。
    • Attentionは対象は文字トークンの前後一定幅のみ計算したのち、複数の文字トークンに対して一定幅で畳み込みを行うことで疑似的に単語にあたる表現を得る。
      • 入力サイズが2048の場合、ストライド幅4にすることでサイズが512になる。
    • その表現をTransformerへ入力することで、あらゆるタスクが一般的な事前学習されたモデルに対する微調整と同様に扱うことができます。
  • Charformer: Fast Character Transformers via Gradient-based Subword Tokenization

    • CANINEと同様なモチベーションをもとに、Transformer内で尤もらしい分割を学習させるモデル。
    • ソフトな単語分割も許容しており、ハードな分割であるサブワードよりも柔軟な表現が期待できそうです。

微調整

事前学習された言語モデルを用いた微調整では、タスクごとに専用モデルが必要だったり、微調整の過程で事前学習された知識の忘却が発生するといった問題があります。

これに対して、Adapterと呼ばれる微調整の仕組みが興味深いです。

  • Parameter-Efficient Transfer Learning for NLP
    • 学習済みのTransformerのパラメータを固定し、Transformer内にAdapter Layerと呼ばれる残差接続を持つブロックを挿入し、これのみ学習させる。
    • これにより、複数のタスクを解く際は事前学習された言語モデルを一切変更することなく、Adapter Layerのみを差し替えることで小量なリソースで多くのタスク が対応可能に。
    • (この論文自体は少し前のものだが、最近ではAdapterを発展させた手法が多く提案されています)

まとめ

無作為に興味を持った論文を選びましたが、なんと全部Googleの研究者によるものでした。改めて自然言語処理研究におけるGoogle社の存在の大きさを感じました。

Transformerベースの大規模言語モデルの登場により多くのタスクのSOTAが立て続けに更新され続けたことで、最近はモデルのサイズばかりに注目が集まる傾向があるように感じます。 単純なパラメータ数増加のみならず、上で紹介したようなTransformerの内部構造に関する研究はとても面白いので、来年も引き続き面白い論文と出会えることに期待しています。

勉強会でJAXとFlaxを紹介した

多分1年以上ぶりに、発表枠 で勉強会に参加しました。

資料はこちらです

speakerdeck.com

もともとはGPT-3について解説する予定だったけど、15分で説明するのは無理と感じたため、急遽JAXとFlaxに話題を切り替えた。

JAXについては、すでに界隈で十分に評価されていたため、今更紹介しても新規性はなさそうだったため、Flaxを加えたことでまとまった情報になったかと思う。

Flaxについては、これまでは存在は知っているけど、JAXの深層学習フレームワークってくらいの理解しかなかったです。 実装の詳細やサンプルコードを見ていくうちに、強い関数型指向の影響を受けていて、新鮮味を感じました。 特に設計思想で、余計な抽象化を嫌うのはとても共感します。

この主張の強さは、果たして深層学習コミュニティに受け入れられるかどうかは、正直疑問を感じます。一方で、このIssue のように、関数型言語の世界の住人にも響いていることから、新しいコミュニティが形成される可能性は少なからず感じさせます。

普段PyTorchを使うことが多い自分には、学習周りでラッパーライブラリを使うのは実はあんまり好きではなくて、pytorch-lightningやcatalystなど、それらを使わずにフレームワーク自体の機能でスッキリ書けるライブラリをずっとほしいと思った背景があります。また、PyTorchやTensorFlowの内部のややこしさに疲弊している実感もあります。

Flaxは行列演算や自動微分などの細かい数値計算は全部JAXにまかせていて、あくまでニューラルネットという巨大な関数という役割に徹していることで、中身は本当にスッキリしています。

設計の綺麗さもさながら、プログラミングの楽しさを感じさせます。 しばらくは趣味として堪能しようと思います。

MNTSQからみらい翻訳に転職しました

2020年11月末でMNTSQ株式会社を退職し、2020年12月より株式会社みらい翻訳に入社しました。

MNTSQは自然言語処理技術を主力とした法務領域向けプロダクトを展開するスタートアップです。みらい翻訳は機械翻訳プロダクトを提供している会社で、どちらも自然言語処理技術をコアとしたプロダクトを作っている会社です。

MNTSQでは、法務というドメインの固有の課題に取り組む面白さを感じていたところだったのですが、みらい翻訳での機械翻訳という自分が最も固有の価値を発揮できそうなドメインに出会い、転職を決意しました。

AI(機械学習)という季節が常に冬か春しかない業界にいて、自分がキャリアについて考える際に、よく尊敬する人たちの入社/退職エントリを参考にしました。 自分の話が誰かの役に立つことは少ないと思うものの、一例として残してみようと思います。

自分について

私のMNTSQ入社までの話は、過去に入社エントリ にまとめましたので、よければご参考いただければと思います。要点をまとめると

この記事を書いたのは、MNTSQ入社間もない時期であり、その後を踏まえてもう少し詳細を書きます。

MNTSQでやったこと

自然言語処理での取り組み

MNTSQ在職中に二つのプロダクト開発に携わりました。一つが 法律事務所向けに、自然言語処理技術を活用して弁護士の業務をサポートするプロダクトです。もう一つは一般企業向けに、契約書管理のためのデータベースを提供するプロダクトです。プロダクトの詳細説明は割愛しますが、自然言語処理観点の主な作業として、契約書に対しておなじみの文セグメンテーション、クラス分類、NERなどの技術を適用していきます。

MNTSQのようなドメイン特化のスタートアップでは、機械学習エンジニアに求められる役割はモデル構築のみならず、ドメイン知識への理解が重要です。モデルの精度は機械的に計測できますが、実際の予測のエラーを見ながら、学習データの偏りを発見したり、テストデータの分割ミスを見極めることが重要です。また、対象のタスクに対して、データの追加が必要かどうかや、機械学習で解くのが本当に最適なのかどうかの感覚を早い段階で掴むことも重要だと思います。

そのためにはドメイン知識の吸収が必須です。もちろん弁護士と同じレベルでの理解は厳しいため、70~80%くらいは自分で正しくアノテーション出来ることを目標にしました(あくまで目標)。

各タスクごとに、弁護士やパラリーガルが詳細なアノテーション基準をマニュアルに記述してもらいました。私たち機械学習エンジニアがそれをもとに理解を深めます。このマニュアルはタスクによっては数十ページに及ぶこともあります。

私がMNTSQに入社しなければ、おそらく一生得られないであろう体験として、真っ先に頭に浮かぶのは弁護士やパラリーガルとの濃密な会話です。 エラー分析やドメイン知識のレクチャーなど、それまでの人生で接点が無い属性の人たちと、複雑な構造の問題を一緒に解いていくことは今後ほぼ無いと思います。

機械学習エンジニアとして

機械学習モデルの精度や振る舞いが基準に達したあと、機械学習エンジニアの手で製品に組み込みます。

正直のところ、私は入社時は自然言語処理関連のコーディングは一定出来ていたものの、ソフトウェア開発関連の知識が不足していました。GitHubを用いたチーム開発やモジュール設計、テストなど、広い範囲での知識不足を埋めることに初期はかなりの労力を費やしました。幸いなことに、手本になるような優秀な同僚はたくさんおり、多くのことを参考にさせてもらいました。

ネット上では 機械学習エンジニアの守備範囲はどこまでか に関する議論を多く見かけます。私の考えとしては可能な限り、ソフトウェア全体を見れるようになるべきだと思います。

ここでいう「見れる」とは、実装レベルに至らなくとも、頭の中で全体の構成要素やデータの流れを意識できることを指します。 プロダクトは成長するにつれて、データ構造や処理は少しずつ変化していきます。その際、機械学習部分がボトルネックにならないよう、他の領域に合わせて機械学習部分が対応したり、もしくは機械学習のパフォーマンスを向上させるために、他の領域に要望を出すこともプロダクトを成長させていく上で必要です。そのためにも全体への理解が重要だと私は思います。

なぜ転職したのか

MNTSQで私が目指した姿は「高い自然言語処理技術を保ちながら、パラリーガルの50%ほどの法務知識を有する」でした。自然言語処理技術は必要に応じてインプットしていきつつ、法務知識を自身のコアバリューとして、他の自然言語処理技術者と差別化を図る考えです。

平坦な道のりではない事を覚悟しつつ、かなり長期的な取り組みとして意識しました。

そんな中、たまたま届いたスカウトメールに「みらい翻訳」が記載されており、興味本位で話を聞いてみようと思いました。

日本で自然言語処理に携わるものであれば、一度は耳にしたことがある企業だと思います。機械翻訳プロダクトを提供する会社であり、登場当初はGoogle翻訳より自然な日本語を出力するとして、一時SNS上で話題になりました。最近はDeepLとの比較をTwitter等でよく目にします。

軽い気持ちでカジュアル面談を受けたところ、自分の目指す方向性と自分ならではの強みを活かせると確信し、その後入社を決意しました。

なぜそう思ったかというと、私自身について少し補足します。
私はもともと中国生まれです。10歳に来日し、それ以降日本で生活しています。中国語の読み書きは少し衰えてがあるものの、多少リハビリすれば短い期間でネイティブに戻れるレベルにあると思います。日本語はすでに中国語より利用期間が長く、一部の発音アクセントを除けばネイティブと大きな差はないと思います。

MNTSQ勤務で感じたドメイン知識の大切さと、その習得には大変時間と労力を要することに対して、日中の機械翻訳という領域であれば最短距離を歩めると直感しました。

もちろんネイティブスピーカーだからといって、高品質な翻訳ができるとは限らず、翻訳の専門家が技術としていることを身につけるには、多くの努力が必要だと認識しています。それでも言語をゼロから習得するよりは、遥かに距離が近いと考えました。

転職にあたって考えたこと

  • 折角身についた法務知識を捨てるのか?
    • 面接を通じて法務領域での翻訳は重要な市場であると知り、いずれそれに着手する際は、翻訳の精度評価には法務知識は活用できると感じました
  • 素人が機械翻訳の世界に飛び込めるのか?
    • これは最も悩んだポイントです。機械翻訳はパソコンよりも歴史が古く、多くの研究がなされてきた領域です。正直やっていけるかの不安もありますが、そんな領域にチャレンジできる嬉しさが勝りました
  • どうせ転職するのであれば、一度自然言語処理から離れてみるのもありでは?
    • ここ数年自然言語処理に携わった感覚としては、ますます関心深まっていくばかりです。変わったこととしては、昔ほど論文を読み耽ることは少なくなり、よりソフトウェア領域へシフトしているが、自然言語処理領域の軸は変わっていません。今後も長期的に携わっていきたいです

みらい翻訳に入社してから

本記事執筆時点で、オンボーディング期間を除き、まだ2週間程度しか実務に携わっていません。現時点で感じたことを少し書きます。

手始めに機械翻訳システムの全体構成を確認しました。詳細はここでは書きませんが、

  • 由緒正しき自然言語処理が随所で使われている
    • 昔から自然言語処理に携わる人が好きそうな処理がたくさんあります
  • 大規模深層学習モデルの実務運用
    • インフラやデータサイズは桁違いのスケールであり、モデリング以外にも意識すべきことがたくさんある

特にこの二点は今後の楽しみを増幅させました。かなり大規模なシステムであり、詳細を理解するには時間を要しますが、複雑なものに楽しみながら取り組めることは重要です。

みらい翻訳のユニークなコンテンツとして、翻訳チーム10本ノック(実際は10本以上あります)というカリキュラムが用意されています。名前でピンとくる人も多いのではないでしょうか。これは機械翻訳システムを各処理ごとに切り分け、それを一つずつ動かしていきながら全体を理解していくものです。1週間はほぼこれのみに費やしました。おかげでシステム全体への解像度がかなりあがりました。これは他の会社にもおすすめできる取り組みです。

実務では日中の機械翻訳チームに参加しました。すでに中国語の語学力が活きる場面に多々遭遇し、現時点で目指していたドメイン知識を十分に備えた機械学習エンジニアに近い動きが出来ていると感じます。

これから

まずは機械翻訳の進化の歴史を抑えようと考え、現在読むべき論文リストを整理しています。ソフトウェア観点でも、これまで経験のない大規模モデルの学習や運用について理解すべきことが多くあります。

やるべきは多いものの、言語知識あることが幸いし、少しは余裕を持って取り組めると感じています。また、ゼロから法務領域にチャレンジできたことで、なんとかなるだろうという自信に多少繋がっています。

おわりに

長くなりましたが、好き勝手書かせてもらいました。

MNTSQ在職中に、組織課題として常にトップに位置したのが採用でした(おそらく今もそう)。にもかかわらず抜けてしまい、若干申し訳無さを感じています。 ここまで読んでくださった人には、転職がネガティブな要因ではないことは伝わったと思いますので、自然言語処理領域で転職をお考えの際はぜひ一度MNTSQも検討してみてください。

みらい翻訳での機械翻訳に興味ありましたら、ぜひ私宛にDMください。

2020年は色々と自粛を意識した年でしたが、まさか最後に転職するとは思いませんでした。

引き続き仕事を楽しみながら、成長していこうと思います。

GPT-3の学習データはどのように作られたか

OpenAIが発表した言語モデルGPT-3はパフォーマンスの高さから各方面で注目されており、ついにはMicrosoftが学習済みモデルの利用を独占化しました。
私個人の所感としてこれまで学習済みモデルは無料公開するという流れを無視し、(アーキテクチャではなく)学習済みモデルが商品化するのはAIビジネスの一つの転換期と感じています。

深層学習による自然言語処理分野で巨大化していくモデルを十分に学習させるためにはWebデータの活用が大きな役割を果たしています。一方、その量に関する話題はあるものの、利用にあたっての細かな前処理に関する議論はあまりなされていない印象です。

そこで本記事は学習データの構築にフォーカスします。

GPT-3の論文でも言及されている通り、学習データはGoogle Researchが発表したT5のデータを踏襲したと書かれていますので、まずはT5のデータから見て行きましょう。

T5

Google Researchが発表したExploring the Limits of Transfer Learning with a Unified Text-to-Text Transformerで提案されたモデルです(タイトルの単語頭文字にTが5つ含まれるからT5)。

モデルの解説はここでは割愛しますがこちらの記事、及びその続編に当たるこちらの記事が大変わかりやすいです。

テキストを入力しテキストを出力する直感的なモデルであり、翻訳や質問応答の分野で応用がイメージしやすいでしょう。 特筆するべきはクラス分類などのタスクも対象ラベルをテキスト生成の枠組みで解くことにあり、言語モデルによる生成のポテンシャルを見せつけた研究である点です。

学習データは非営利団体Common Crawlが公開しているデータであり、同団体では2011年以降インターネット上のWebページを無作為に広範囲に収集し公開しています。
誰でも自由にアクセスできますが、データサイズはテラバイト単位ですので気軽にダウンロードしないように気をつけましょう。

クロールされたWebページはHTMLタグは除外されるものの、依然スクリプトやメインコンテンツと関係ない部分が多くを占めます(ここはスクレイピング経験者が大きくうなずく部分ですね!)。

更に重複コンテンツや誹謗中傷等、モデルに学習してほしくない情報も含まれます。

これらの問題に対処するためにT5の学習ではCommonCrawlデータに対していくつか前処理を施しています。

  • 末尾が句読点で終わる文のみ利用
  • 5文未満のページと3語未満の行を除外
  • 誹謗中傷や卑猥な語を含むページを除外
  • javascriptを含む行を除外
  • ダミーテキストを示す“lorem ipsum"を含むページを除外(海外の出版ではダミーテキストの先頭に“lorem ipsum"を表記させる文化があるらしい)
  • ソースコードと思われるページを削除("{”で判別)
  • 3文以上の重複があるページを除外
  • 言語判定を行い、英語ページのみを対象

CommonCrawlで公開された2019年4月時点のデータ(約20TB)を対象に、上記の前処理を経て約750GBの前処理済み学習データを構築しました。

この前処理済みデータは“Colossal Clean Crawled Corpus”(通称C4)と名付けられTensorFlow Datasetsで公開されています。

T5の論文ではC4以外のデータセットで学習させた結果を比較し、重要な示唆が多く得られています。この記事はあくまでデータセットの作成にフォーカスしていますので考察はしませんがご一読することをおすすめします。

GPT-3

OpenAIが発表したLanguage Models are Few-Shot Learnersで提案されたモデルです。これまでに発表したGPT, GPT-2の構造を踏襲しパラメータ数約1750億という巨大なモデルです。学習にかかったコストはクラウド日本円で約5億円とのことです。

GPT-3が画期的だったのはモデルのアーキテクチャ以上にタイトルでも強調されているFew-Shot学習です。詳細はこちらの記事でわかりやすく解説されています。

さて学習データですが、T5と同じくCommonCrawlのデータを用いています。 T5での前処理を経てもまだ低品質なデータが残るためことを指摘し、独自の前処理の行っています。

  • 低品質データのフィルターを学習

    • 手動で整備された高品質コーパスであるWebTextと(前処理されていない生の)CommonCrawlを判別する分類器(ロジスティック回帰)を構築
    • 分類器の出力を品質スコアとして、各文書にスコアを付与
    • 一定ノイズデータも取り入れるために閾値に乱数を入れる(np.random.pareto(9)>1−document_score)
  • ファジー重複の発見

    • sparkのMinHashLSHを用いて文書間の類似度を計算し、重複を削除

(上記2つの前処理を経て約45TBから約570GBへデータが絞られました。)

  • 高品質テキストを混ぜる
    • 整備された高品質テキストであるWikiTextとWikipediaを学習データに追加

T5で行われていたルールベースの処理と異なり、機械学習と文字列アルゴリズムによる処理が印象的ですね。GPT-3の前処理ではT5のようにソースコードを一律削除するといったことがなされていないため、自然言語からコードを生成するといったパフォーマンスが可能になったと思われます。

同様に英語以外の言語を除外しないことで翻訳といったパフォーマンスも可能にしたと思われます。

まとめ

T5及びGPT-3で使用されるデータの構築方法についてまとめさせていただきました。

最近は化学や金融など様々な分野特化のテキストで言語モデルを学習させる取り組みが各所でなされており、言語モデルの実用化が進んでいるように感じています。その一方でモデルの性能を引き出すためには学習データに対する前処理が極めて重要であることにも注意が必要と感じました。

また、上記2論文ではいずれもデータに対して文としての品質に着目し、低品質な文をフィルタリングしていますが、今後は人種差別やフェイクニュースといったトピックや意味を把握して、そこからデータを構築していくことが次のホットトピックになるようにも感じました。

MNTSQ株式会社では機械学習エンジニア及び各ポジションで人材を募集しています。ぜひ採用ページをご覧ください

www.wantedly.com

flairを使って最速でNLPのベースラインモデルを作る

f:id:nmoriyama:20200710133947p:plain

自然言語処理に限らず、機械学習関連のプロジェクトではスタート時は、なるべく複雑なコーディングをせずにシンプルなベースラインモデルを低コストで作成し、そこからデータの傾向やタスクの複雑さを把握することが重要です。

ところが自然言語処理では前処理のコストが高く、最低限でも単語分割、ベクトル化、深層学習を用いる場合は事前学習された埋め込みベクトルを準備する必要があります。その後は他のタスクと同様にモデルの保存方法や、予測のパイプラインで悩みポイントを抱えることが多いと思います。

最近はAutoMLを始めとした機械学習の自動化が進歩し、初手から高性能なモデルをブラウザ上で数クリックで作成できますが、中身がブラックボックスである故に前述のデータの傾向やタスクの複雑さを把握することを目的とした場合には適切とは言えない側面があります。

本記事では自然言語処理を対象にモデルの中身が参照可能でかつ少ないコーディング量でモデルを作成する方法を紹介します。具体的には自然言語処理ライブラリであるflairの機能を活用し、日本語のオリジナルデータに対して学習と予測を行います。

flairでは前処理や学習と予測のパイプライン等はすべて抽象化されており、独自で定義せずともライブラリの機能を呼び出すことで数行のコードで動かすことができます。初手のベースラインとしてすぐに実行できます。

前準備

文書分類として株式会社ロンウィットが配布するlivedoorコーパスを使って文書のカテゴリ推定を行います。

livedoorコーパスはNHN Japan株式会社が運営する「livedoor ニュース」から作成したものです。各ニュースの記事とともにカテゴリ情報が含まれます。 記事にはクリエイティブ・コモンズライセンス「表示 – 改変禁止」が適用されますので、取り扱う際はご注意ください。

解凍したファイルと同じディレクトリで以下の前処理を行います。 DataFrameに記事本文とカテゴリラベルを保持させ、train, text, devの3つに分けてCSVに保存します。

import glob
import pandas as pd
from sklearn.model_selection import train_test_split

categories = [
    "sports-watch", "topic-news", "dokujo-tsushin", "peachy",
    "movie-enter", "kaden-channel", "livedoor-homme", "smax",
    "it-life-hack",
]

docs = []
for category in categories:
    for p in glob.glob(f"./text/{category}/{category}*.txt"):
        with open(p, "r") as f:
            url = next(f)
            date = next(f)
            title = next(f)
            body = "\n".join([line.strip() for line in f if line.strip()])
        docs.append((category, body))
        
df = pd.DataFrame(docs, columns=['label', 'text'])
train_df, test_df = train_test_split(df, test_size=0.4, shuffle=True)
test_df, dev_df = train_test_split(test_df, test_size=0.5, shuffle=True)

train_df.to_csv('./processed/train.csv', index=False, header=False)
test_df.to_csv('./processed/test.csv', index=False, header=False)
dev_df.to_csv('./processed/dev.csv', index=False, header=False)

今回は簡易化のためにラベルと本文しか扱いませんが、必要に応じてタイトル文などの情報も利用してみると面白いでしょう。

これで準備は完了です。

flair

flairを用いてシンプルな文書分類モデルを作ります。

flairは固有表現認識(NER)や品詞タグ付け(POS)タスクを中心に最先端モデルを簡単に利用出来ることが特徴です。バックエンドにPyTorchが利用されています。

固有表現認識や品詞タグ付けで注目されていますが、テキスト分類もサポートされています(こちらは最先端は強く意識されていないようです)。

開発頻度は高く、ネットから入手できるサンプルコードは最新バージョンで動かないことが多いです。以降コードで使用するflairは執筆時点で最新バージョン(0.5.1)を使います。

flairでは単語分割や事前学習された埋め込みベクトルの利用など、多くのタスクに共通する処理がそれぞれモジュールとして組み込まれており、それらを入れ替えることで実験がしやすくなるようにデザインされています。

日本語サポートとして単語分割にMeCabやSudachiなど各種分割器を簡単に利用できるkonohaが使われています。 学習済み埋め込みベクトルもfasttextなど数種類が用意されています。

モジュールインポート

from flair.data import build_japanese_tokenizer
from flair.datasets import CSVClassificationCorpus
from flair.embeddings import WordEmbeddings, DocumentLSTMEmbeddings
from flair.models import TextClassifier
from flair.trainers import ModelTrainer

japanese_tokenizer = build_japanese_tokenizer(tokenizer="MeCab")

build_japanese_tokenizerの内部ではMeCabやSudachiなどの形態素分割器を扱えるkonohaが呼び出されるが、flair経由でkonohaを使う場合は執筆時点でMeCabしか利用できません。

学習データとしてとしてCSVファイル読み出しがサポートされており、ファイルパスと対象のカラムを指定するのみです。ミニバッチのサンプラーを書かずに済むのは便利ですね。

# データフォルダ。ここにtrain.csv, test.csv, dev.csvが含まれることが想定される
data_folder = './processed/'

# カラムのインデックスを指定
column_name_map = {1: "text", 0: "label_topic"}
corpus = CSVClassificationCorpus(
    data_folder,
    column_name_map,
    skip_header=True,
    tokenizer=japanese_tokenizer  # 日本語トークナイザーをセット
) 

学習データが用意できました。

次にモデルを考えます。比較的に目にすることが多い学習済みの単語埋め込みベクトルを入力に、LSTMの隠れ層から文書ベクトルを作成し、その文書ベクトルからクラス分類を行うモデルを構築します。

flairを使わずにこれをスクラッチで書く場合はデータのミニバッチサンプラーニューラルネットの各レイヤーの設定、学習ループ、モデルのアーキテクチャとパラメータの保存、予測のパイプラインをそれぞれ書く必要があります。

また、学習のloggingをしっかり取ろうとすると更に書く内容が増えます。

以下のflairの機能を活用して少ないコードで学習させます。

# ラベルをIDに変換
label_dict = corpus.make_label_dictionary()

# 単語ベクトルとして学習済みの日本語のfasttextベクトルを指定
word_embeddings = [WordEmbeddings('ja-crawl')]

# 文書ベクトル化のモデルを指定。ここではLSTMを選択
document_embeddings = DocumentLSTMEmbeddings(word_embeddings, hidden_size=256)

# 分類器
classifier = TextClassifier(document_embeddings, label_dictionary=label_dict)

# 学習ループを設定
trainer = ModelTrainer(classifier, corpus)
trainer.train('trained_model/',  # モデルの保存先
              learning_rate=0.01,
              mini_batch_size=32,
              anneal_factor=0.5,
              patience=5,
              max_epochs=10)

学習ループのtrainerは大変シンプルですね。上記の処理をスクラッチで書く場合のコード量は短くても上の10倍はかかると思います。

また、勾配を持たない埋め込みベクトル層はCPUメモリに置き、勾配を保持させるパラメータだけをGPUメモリに置くなど、GPUメモリを節約してくれるテクニックをデフォルトで実行してくれます(学習時間は増えますが)。

更にearly_stoppingといった機能もデフォルトで用意されていることは大変便利です。

学習後にtrainer.train()の第一引数で指定したフォルダにモデル、学習のログ、スコアなどの情報が保存されます、学習済みモデルを呼び出して予測を行います。

学習中にKeyboardInterrupt で中断される場合は。その時点のモデルを保存してくれる設計も親切です。

デフォルトの設定では以下のようなログが出力されます

2020-07-10 09:53:03,398 epoch 5 - iter 13/139 - loss 1.67030419 - samples/sec: 0.76
2020-07-10 10:00:33,105 epoch 5 - iter 26/139 - loss 1.69300615 - samples/sec: 0.94
2020-07-10 10:07:57,656 epoch 5 - iter 39/139 - loss 1.68367987 - samples/sec: 0.95
2020-07-10 10:14:45,635 epoch 5 - iter 52/139 - loss 1.68211064 - samples/sec: 1.05
2020-07-10 10:21:46,314 epoch 5 - iter 65/139 - loss 1.67768772 - samples/sec: 1.00
2020-07-10 10:29:30,703 epoch 5 - iter 78/139 - loss 1.68330931 - samples/sec: 0.91
2020-07-10 10:37:07,806 epoch 5 - iter 91/139 - loss 1.68172456 - samples/sec: 0.92
2020-07-10 10:44:45,063 epoch 5 - iter 104/139 - loss 1.67901101 - samples/sec: 0.92
2020-07-10 10:51:53,324 epoch 5 - iter 117/139 - loss 1.67862995 - samples/sec: 0.99
2020-07-10 10:59:34,579 epoch 5 - iter 130/139 - loss 1.67751856 - samples/sec: 0.93
2020-07-10 11:04:07,374 ----------------------------------------------------------------------------------------------------
2020-07-10 11:04:07,376 EPOCH 5 done: loss 1.6784 - lr 0.0100000
2020-07-10 11:30:03,295 DEV : loss 1.616660714149475 - score 0.7924
2020-07-10 11:30:19,412 BAD EPOCHS (no improvement): 0
saving best model

予測の確認として学習データに含まれない2020/7/10時点のlivedoorニュースのスポーツカテゴリの記事を読み込ませます

https://news.livedoor.com/article/detail/18550966/

from urllib.request import urlopen
from bs4 import BeautifulSoup
from flair.data import Sentence

# 記事本文をダウンロード
url = 'https://news.livedoor.com/article/detail/18550966/'
soup = BeautifulSoup(urlopen(url))
text = soup.find(class_="articleBody").text

# 保存モデルを呼び出し予測する
classifier = TextClassifier.load('trained_model/best-model.pt')
sentence = Sentence(text, use_tokenizer=japanese_tokenizer)
classifier.predict(sentence)
print(sentence.labels)

>>>
[sports-watch (0.571)]

NEXTアクション

ベースラインモデルの予測結果から様々なエラー分析が可能です。livedoorコーパス固有のエラー分析は今回では取り上げませんが一般的な精度向上カスタマイズを紹介します

  • 語彙に含まれない単語が多い
    事前学習された単語埋め込みに含まれない単語がデータに多く出現する場合、FlairEmbeddingが有効です。これは事前学習された文字の埋め込みから単語の埋め込みを構築する手法でflairの目玉機能です。日本語では2000種ほどの文字が学習されていますので、多くの単語に変換することが可能です。

  • LSTM以外の方法で文書ベクトルを試したい
    より高機能なBERTを使った文書ベクトル化はTransformerDocumentEmbeddingsがサポートされていますので、上記のDocumentLSTMEmbeddingsと差し替えることでBERTを組み込むことができます。

  • 文書ベクトル以外の情報を扱いたい
    少し発展的な構造になりますがflairの特徴である固有表現情報と文書を組み合わせて分類するモデルを書く場合はflairの機能で完結します。

以上flairで簡単にモデルを作成する方法を紹介しました。

私が勤めるMNTSQでは自然言語処理エンジニアを募集しておりますので、自然言語処理を活用する仕事を探していましたらぜひ一度ご検討お願いします。

BERTを量子化して高速かつ軽量にする

こんにちは、@vimmodeです。自然言語界隈ではBERTを始めとしたTransformerベースの手法の進化が目覚ましいですが、実運用されている話はあまり聞きません。

その理由としてモデルのサイズの大きさと推論速度の遅さに一定起因すると感じており、この記事はその解消になり得る量子化と呼ばれる手法の紹介とPyTorchで実装されたBERTモデルに量子化を適応する方法を紹介します。

量子化とは

量子化という単語は数学や物理など様々な領域で使われています。ここで述べる量子化情報理論における量子化であり、主に連続値を離散値で表現することを考えます。
機械学習の枠組みで考えるとモデルのパラメータや学習時の勾配(場合によっては入力と出力データも含める)の数値表現を浮動小数点から整数に変更することを目的にします。

ディープラーニングではパラメータ等をfloat32で表現することが多いですが、もしこれをint8に置き換えることができればそれだけでサイズを1/4に減らすことが出来ます。計算速度はモデルの構造に強く依存しますが、シンプルな線型結合を多用するBERTは恩恵を強く受けます。

量子化により数値の小数点精度が落ちるため、モデルの精度が低下することは想像出来ます。如何に精度を落とすことなく、軽量化することが出来るかがこの分野のチャレンジと言えます。

モデルの軽量化と同じ文脈で蒸留と呼ばれる、モデル自体を圧縮する手法もありますが量子化とは異なる考え方を持ちますので、量子化とは異なるものとして考えて良いと思います。

また、float32の精度にほぼ近似しつつもfloat16で学習と推論を可能にする混合精度演算と呼ばれる技術もありますが、これも量子化と仕組みが異なりますので、これも別物と考えて良いと思います。

量子化の要素整理

一概に量子化と言っても、どの要素を何で量子化するかによって必要な処理が変わるので、ニューラルネットワークで考慮する要素を以下のように整理しました。

これらの要素を考慮しつつ精度を落とさない工夫はグーグルの研究者によるこちらの論文で解説されています。どれも専門性の高い話なのでここでの説明は割愛いたします。

BERTを量子化する

以降PyTorchで実装されたBERTモデルを量子化します。今回は学習済みのBERTモデルに対して量子化を適用し、適用前後のモデルのサイズと推論速度、精度を比較します。
PyTorch公式チュートリアルに詳細な手順が記載されていますので、ここでは簡略化したコードと補助的な情報をまとめます。 pytorch.org

実験は以下の内容で行います。

前準備として学習済みBERTにファインチューニングを行います。学習コードはこちらであり、以下のように引数を与えて学習を行います。

export GLUE_DIR=./glue_data
export TASK_NAME=MRPC
export OUT_DIR=./$TASK_NAME/
python ./run_glue.py \
    --model_type bert \
    --model_name_or_path bert-base-uncased \
    --task_name $TASK_NAME \
    --do_train \
    --do_eval \
    --do_lower_case \
    --data_dir $GLUE_DIR/$TASK_NAME \
    --max_seq_length 128 \
    --per_gpu_eval_batch_size=8   \
    --per_gpu_train_batch_size=8   \
    --learning_rate 2e-5 \
    --num_train_epochs 3.0 \
    --save_steps 100000 \
    --output_dir $OUT_DIR

学習後にPythonから学習済みモデルをロードします

import torch
from transformers import BertConfig, BertForSequenceClassification, BertTokenizer

tokenizer = BertTokenizer.from_pretrained(MODEL_DIR)
model = BertForSequenceClassification.from_pretrained(MODEL_DIR)

次にBERTを量子化します。 PyTorchの量子化変換としてtorch.quantation.quantize_dynamic()関数が提供されており、これに定義済みのモデルを第一引数に、第二引数としてモデル内で量子化するレイヤーを指定します(省略する場合は全レイヤーに対して量子化が適用されます)。

BERTの内部では線形結合層(torch.nn.Linear)が多く使用します。今回はここを指定して量子化を実行します。

quantized_model = torch.quantization.quantize_dynamic(
    model, {torch.nn.Linear}, dtype=torch.qint8
)

この時点で量子化前と後のモデルのサイズを比較すると以下のようになります。

def print_size_of_model(model):
    torch.save(model.state_dict(), "temp.p")
    print('Size (MB):', os.path.getsize("temp.p")/1e6)
    os.remove('temp.p')

print_size_of_model(model)
print_size_of_model(quantized_model)

>>>
Size (MB): 437.980564
Size (MB): 181.440471

モデルサイズが約40%に圧縮できました。  

次に推論速度(1スレッド及び4スレッド)と精度を確認します。

def time_model_evaluation(model, configs, tokenizer):
    eval_start_time = time.time()
    result = evaluate(configs, model, tokenizer, prefix="")
    eval_end_time = time.time()
    eval_duration_time = eval_end_time - eval_start_time
    print(result)
    print("Evaluate total time (seconds): {0:.1f}".format(eval_duration_time))

# Evaluate the original FP32 BERT model
time_model_evaluation(model, configs, tokenizer)

# Evaluate the INT8 BERT model after the dynamic quantization
time_model_evaluation(quantized_model, configs, tokenizer)

>>>

| Prec | F1 score | Model Size | 1 thread | 4 threads |
| FP32 |  0.9019  |   438 MB   | 160 sec  | 85 sec    |
| INT8 |  0.8953  |   181 MB   |  90 sec  | 46 sec    |

この段階でF1スコアで0.005程度の悪化にとどまる一方で、サイズは40%に圧縮され、推論速度は4スレッドで約50%に減少させることが出来ました。 Kaggleのようなコンペションではこの精度の悪化は課題となりますが、実務で速度パフォーマンスを重視する場合は十分検討価値があると思います。

今回は学習済みモデルに対して量子化を適用させましたが、学習時から量子化を適用することで1段階float32のモデルの精度に近づけることが出来ます。それはまた他の機会に紹介できればと思います。

最後に私が所属するMNTSQ自然言語処理エンジニアを募集しております。関心がある方はぜひご連絡ください!

論文紹介: DIFFERENTIABLE REASONING OVER A VIRTUAL KNOWLEDGE BASE

最近ICLRの論文を読んでおり、以下の論文がとても興味深かったため簡単にまとめました。

openreview.net

 

最近自然言語処理領域であらゆるタスクをBERT+ファインチューニングで解くことが流行っており、実際高精度な推論結果を得られています。

ただBERT内のデータ構造はブラックボックスであり、個人的には一定構造化された知識を扱いたい感覚があります。

この論文では質問応答システムとして、コーパスを擬似的にナレッジベースのように扱う手法を提案しています。

 

グレイトフル・デッドの歌手の誕生日は?

この質問に答えるには、まずグレイトフル・デッドの歌手が誰であるかを知る必要があり、そこから誕生日を探す必要があります。

このようなマルチホップ推論が必要な問題は直接学習することが難しく、ナレッジベースのような推論が効果的です。

この論文の提案手法ポイントとして

  • コーパスの文からエンティティを取り出して、各分のエンティティの共起からエンティティ同士の関係をTF-IDFベースで計算
  • 質問をBERTをつかってエンコードし、コーパス内の各文(TF-IDF)関連性を学習
  • 上の過程で、回答のための質問文に登場しない潜在エンティティを抽出しながら回答候補を探索

個人的にはこの処理はとても感覚に合っていて、納得感があります。

具体的な処理は以下にまとめましたので良かったらご覧ください。

www.slideshare.net

 

最後となりますが私が働いているMNTSQでは自然言語処理エンジニアを募集しておりますので、興味がありましたらぜひ私までご連絡ください!