for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

【フォースタ テックブログ】RailsのAutoloadingをClassicモードにしていたらエラーに悩まされたのでZeitwerkモードに移行した話

 

こんにちは。エンジニアの藤井(@yutafujii)です。

今日は、RailsのAutoloadingとReloadingについて解説しつつ、これにまつわる設定ミスでdevelopment環境においてエラーに悩まされたというエピソードをご紹介します。

AutoloadingとReloadingって?

RailsのAutoloadingとReloadingという言葉を、より実務上のありがたみとしてイメージできるように素朴な疑問から考えてみたいと思います。

なぜRailsではrequireを書かなくてもよいのか?
Rubyでは他のファイルを読み込む時には当該ファイルを明示的にrequireしておく必要があります。ところがRailsではモデルでもコントローラーでも、requireを書かずに多くの処理がうまく動きます。

これは、RailsRubyのメソッドをオーバーライトしているためです。具体的には、Moduleクラスのconst_missingというメソッドを上書きしています。このメソッドはメモリ上にロードされてない未知の定数を参照したときに発火するのですが、RubyではNameErrorが出るのに対して、上書きされたRailsのconst_missingではエラーを出す前にその定数が定義されていそうなファイルを推測して自動で探すようになっています。だからAutoloadingと呼ばれています。なお、自動で探す範囲はautoload_pathという変数で管理されています。

そしてもうひとつの疑問。
なぜRailsで開発しているときにファイルに加えた変更がすぐ反映されるのか?

例えばの話ですがproduction環境で稼働しているサーバーに入り、Railsのコントローラーのファイルを書き換えたとしても、その変更はサーバーを再起動しない限り反映されません。しかしdevelopment環境だとファイルを修正すると画面をリロードするだけでその変更が反映されます。Railsがこのような開発体験の良さを実現しているのは、development環境ではファイルの変更履歴をウォッチして、変更を検知したらサーバーが次のリクエストを受理したときに当該ファイルを読み直せるようにしているからです。これをReloadingと呼んでいます。

RailsがAutoloadingとReloadingのためにしていること

概略は説明した通りですが、コードベースでも該当箇所を紹介しておきます。
Autoloadingで説明したconst_missingメソッドのオーバーライトはActiveSupport::Dependenciesというモジュールに記載されています。

また、Reloadingで説明したファイルをウォッチしているというクラスはActiveSupport::FileUpdateCheckerというもので、そのexecuteメソッド(端的に言えばここで変更が生じているファイルをメモリからアンロードする)をコールしているのがRails::Application::Finisherというモジュールです。

Reloadingについて説明を加えると、このFileUpdateCheckerがautoloadされた定数を一旦全てアンロードしますので、次のサーバーリクエストの処理において変更を加えたコントローラーやモデルが参照されると、const_missingが発火してAutoloadingされ、結果として修正後の内容がロードされるという仕組みです。

設定を間違えたらdevelopment環境で見知らぬエラーが

正直に言って、こんなRailsの仕組みを理解したうえで実務の世界に入ったわけはなく、エラーに遭遇して初めてちゃんと調べただけです。

ここからはそのバグについてご紹介します。

私が入社した頃はRailsフルスタックのフレームワークとして利用していたのですが、途中からVueやNuxtをフロントにしてAPIサーバーとしての機能に集約してきました。そうした開発を進めていくなかで、development環境において次のようなエラーが出るようになりました。

A copy of Api::One has been removed from the module tree but is still active!

Api::Oneのコピーはモジュールツリーから削除されたけどまだ利用されています。」とでも訳すのかもしれないですがエラーメッセージの言っていることがイマイチよくわからず、backtraceをみたのですがアプリケーションのコードに到達する前のRailsのコードでエラーになっていたので、これは少し根が深そうだと思ってGoogle検索を頼りました。

同様のエラーに関する記事はいくつか見つかったのですが、実際に効果があったのはdevelopment環境のconfigを変更するという対処でした。

config/environments/development.rbにおいて「クラスをキャッシュしておくか」という設定(config.cache_classes・config.action_controller.perform_caching)をtrueにすることで確かにエラーは出なくなったのですが、これは一度ロードしたクラスをキャッシュし続けるという設定なのでReloadingが効かなくなり、Rails部分のソースコードは(より厳密にはautoload_pathに含まれるファイルは)変更するたびにアプリケーションサーバーを再起動しないと内容が反映されなくなります。

これは開発体験が非常に悪いので、Railsの仕組みを調べながら、根本原因を探していきました。

結論として、エラーの直接的な原因はdevelopment.rbの別の設定にありました。

「クラスのリロードを変更があった場合に限定する」という設定(config.reload_classes_only_on_change)がfalseになっていたために、ソースコードを変更しなくてもリクエストの都度autoloadされた定数を全てアンロードしていました。

この設定そのものが問題ではないのですが、フロントエンドをコンポーネント化してきたことと複合してエラーを生じさせていました。

すなわち、コンポーネント化されたページを開くとページロード直後にJavaScriptが複数のリクエストをほぼ同時にAPIサーバーへ送る状況が生まれたところ、前述のRailsの設定が理由でAPIサーバー側ではリクエストの処理前にautoloadされた定数が全削除され、その結果全く同じモジュールのAutoloadingが2本同時に走るRace condition(競争状態)が発生していました。同一モジュールのRubyオブジェクトが2つできてしまったことで、処理途中のequal?評価(RubyではオブジェクトIDの一致を確認するメソッド)がfalseになり、くだんのエラー

Api::Oneのコピーはモジュールツリーから削除されたけどまだ利用されています。」

が表示された、というわけです。

ちなみに、実際にエラーを起こしたのはルーティングからコントローラーを取得する処理action_dispatch/http/request.rbのcontroller_class_forという部分でした。コントローラー名の文字列から定数を取得するRailsのconstantizeメソッドでコントローラーを示す定数(例えるならApi::Parent::ChildController)をAutoloadingする時にエラーになっていました。

なぜこのような設定になっていたのか

ところで、問題の一因となったconfig.reload_classes_only_on_changeの設定はRailsプロジェクトの初期値がtrueなので、なぜこれがfalseに変更されたのか気になりました。

この変更は3年前に行われており。当時のプルリクにも多くの情報はなかったので推測ではありますが、事の発端はApplicationというモデルを作成したことだったと思われます。

当社のシステムは人材紹介業に関連するものであるために、”応募”の英訳にあたるApplicationという単語をモデル名で利用していました。しかし想像がつくようにApplicationというクラスはRailsプロジェクトそのものにも存在し(config/application.rb)、何らかの機構でApplicationモデルのAutoloadingが上手くできなかったようです。そこでRailsのイニシャライズ直後にapp/models/application.rbをrequireしておくような設定がconfigに書かれていました。

悲しいかなRailsではrequireしたファイルは通常のReloading時にはアンロードされないという性質があるために、今度はApplicationモデルのReloadingができない悩みを抱えていたと思われます、だから強制的に都度定数削除をするconfig.reload_classes_only_on_changeをfalseにしたのではないかと考えています。

今回のバグ修正においてこの部分も見直し、結果的にrequire_dependencyを利用しました。一応ですがRailsガイドではrequire_dependencyはラストリゾートであり最初に検討すべき手段ではないと書かれているのでご注意ください。なお後述するZeitwerkモードの導入でこの対応も不要になりました。

Autoloadingに関するRailsの設計上の疑問と直近の動向

もう少しだけこの定数のAutoloadingについて触れておきましょう。

紹介したエピソードではdevelopment.rbの設定ミスとVueを用いたフロントエンドの分離が競争状態を生んだエラーの理由だと説明しましたが、このエラーはどのRailsプロジェクトでも一般的に再現性があります。

エラーが起きる条件は「2本以上の同時リクエストを受けとりReloading & Autoloadingが2本同時に走ること」ですが、通常の開発のなかでファイルを修正した場合この条件を満たしてしまいます。

実際、フロントのコンポーネントにおけるcreatedフックなどで2本以上のAPIが同時に呼び出されるページでは、Rails API側のファイルを修正すると直後の画面リロード時だけはこのエラーが出ます(出ない時もあります)。ReloadingとAutoloadingが2本走って競争状態が生まれるためです。そのままもう1度画面リロードするとエラーは出なくなりますが、これはReloadingもAutoloadingも走らないからです。

このエラー再現性についてはReproduce用の個人プロジェクトも作って確認しました。

github.com

「これ、フロント分離しているプロジェクトだと悩む人多いんじゃないか?」と思ってRailsのイシューが既にあるか見てみると、確かに1件「LoadError when multiple threads try to load the same namespaced class」というイシューで修正の議論もなされていたようですが、Rubyの改修も必要な内容になっており、最終的には修正は行われていない様子でした。

その代わりだったのかはわかりませんが、Autoloadingの新しいやり方がRails 6.0から導入されています。

Zeitwerk(ツァイトヴェルク)というgemが正式に導入され、そもそものconst_missingに依拠したAutoloadingが見直されました。

なので、Zeitwerkモードを利用していれば、今日紹介したエラーに悩まされる心配はありません。Rails 6.0以前のAutoloadingの方法はClassicモードと呼ばれていますが、これはRailsガイドでdeprecatedとされているので早めに移行しておきましょう。

config/application.rbに1行追加するとZeitwerkモードに移行できます。

config.load_defaults "6.0" # Zeitwerk
config.autoloader = :classic # Classic

私の所属するプロダクトではRailsのバージョンこそ6に上げていたものの、こうした周辺機能のマイグレーションに気づけていない部分もあったので、次回以降気をつけていきたいと思います。

参考リンク

RAILS GUIDES

https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html
https://guides.rubyonrails.org/autoloading_and_reloading_constants.html

Rails GitHub

https://github.com/rails/rails/blob/main/activesupport/lib/active_support/dependencies.rb
https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflector/methods.rb

Rails Issue

https://github.com/rails/rails/issues/33209

Zeitwerk GitHub

https://github.com/fxn/zeitwerk#pronunciation

【フォースタ テックブログ】「ユニコーン企業のひみつ」を読んで自社の開発組織と比べてみた

こんにちは。サーバーサイドエンジニアの速水です。
今回は、「ユニコーン企業のひみつ――Spotifyで学んだソフトウェアづくりと働き方」という書籍のレビューを投稿させていただきます。

www.oreilly.co.jp

フォースタートアップス(以下、フォースタ)でも、スクラムをベースにしたアジャイル開発を行っているわけですが、事業環境や組織は日々変化しており、どう変化に対応していくべきなのか、悩みは尽きません。
ユニコーン企業のひみつ」は、SpotifyAmazonGoogleFacebookといったユニコーン企業はどうやっているのか?というのをヒントに、何万人もの従業員を抱える企業がなぜスタートアップのような組織・環境であり続けられるのかを紐解いていく内容となっており、印象に残ったところ、自らに引き寄せて考えたこと、感想をまとめました。

今回の書評は、弊社の村林(@bayashimura)がこちらのツイートを見つけたことがきっかけになっています。

村林はフォースタの開発組織であるテックラボのアジャイル旗振り役として、振り返りやチームビルディングを浸透させてきました。社内向けのプロダクト「タレントエージェンシー支援システム(SFA/CRM)」を開発しているチームでは、振り返りのファシリテーターを交代で行うなど、スクラムの要素を取り入れながら開発プロセスの改善に努めています。

tech.forstartups.com

tech.forstartups.com

不確実性だらけだからこそ学習が大事

スタートアップで取り組むソフトウェア開発は、不確実性だらけであると語られています。

「答え」が既にわかっているというつもりなら、それは思い込みだ。

わからないから、たくさん実験をして、失敗をすることもあり、ただ毎回インパクトと価値を計測しているから、前に進んでいける。わからないから、「答え」を見つけるために、各自が考えて、手を動かして、学習していかなければならない、と説かれていました。

自らに引き寄せると、やはり普段の仕事の内容は、「答え」がわかっているとは言えません。議論した上で「この方向だよね」という合意はあれど、それが絶対に正解とは言い切れないし、アプローチ方法も様々です。

テックラボでうまくできていると感じるのが、アプローチへの寛容さと、失敗に対する心理的安全性(失敗をしてはいけないという思い込みは払拭する)です。フロント、バックエンド、様々な視点から実現方法を考えるのはもちろん、そもそも手動の作業で同じような物を見せて解決できないか、ヒアリングして課題をもう少し深堀りたい、といったこともフラットに出し合うことができます。失敗はしないに越したことはありませんが、バグが入ったまま機能をリリースしてしまった、レビューでバグを見逃してしまった、という時もあります。そういった時に、チーム全体で今できるリカバリ策を考え、対応する姿勢があるからこそ、苦手な領域の開発であっても挑戦してみよう!という気になります。

一方、インパクトと価値の計測においては、まだまだ課題があるように感じています。社内向けプロダクトは、ビジネス上の価値とプロダクトの価値がぴったり一致はしないため、指標の設定、評価には苦戦しています。

自律、権限、信頼

ユニコーン企業ではアジャイル開発が企業文化に染み込んでおり、わざわざスクラムマスターを置いて形式としてのスクラムをやる必要はないようです。とても自律したチームであり、信頼があるからこそ、権限もしっかり持っています。ですが企業としてバラバラにならないための仕組みとして、"カンパニーベット"を設定するということもやっているようです。
※カンパニーベット(Company Bet):会社が取り組みたい重要事項を、終わらせたい順に並べたリスト。小さな目標ではなく、大きな取り組みに関するもの。

企業においてチームに権限を渡すというのは、そう簡単なことではないと思います。しかし、学習のスピードを出すためには必要なことでもあります。本書の中では経営側の視点で「現場の言い訳を取り除く」と表現されていますが、現場としても権限をもらう以上、本当に自分ごと化できないと辛いことになりそうです。渡す側、もらう側、双方の信頼あっての権限移譲で、それがあっての自律したチームなのだと理解しました。

また、見習いたいポイントとして、データ(数値、ファクト)と解釈(どういう意味を見出したか、仮説)を、フレームワークとして切り分けている点がありました。本書では、DIBBという、やるべきことを系統立てて検証するための意思決定フレームワークが紹介されており、データ(Data)、インサイト(Insight)、確信(Belief)、ベット(Bet)に分解して、整理されていました。
私はこれまで、人への伝わりやすさという観点で「主張+根拠づけとしてのデータ」をまとめてとらえることが多かったのですが、学習の最中ということを考えると、データと解釈をわかりやすく分けて共有した方がチームとしての学習は進むのかもしれません。私も意識してやってみようと思います。

まとめ

著者Jonathan Rasmusson氏のSpotifyでの経験を中心に、ユニコーン企業におけるソフトウェア開発で組織として気をつけていることがわかりやすくまとまっている1冊でした。特に、アジャイル開発におけるスクラムの形式的イメージを強く持っている方にとっては、それが浸透した先を知れる内容となっているので、面白いと思います。

フォースタではエンジニアを募集中です!
開発スタイルや雰囲気はもちろん、事業や技術スタックに関するお話もさせていただきますので、ご興味をお持ち頂けましたら、下記「話を聞きに行きたい」ボタンより気軽にエントリーいただければ幸いです。

【フォースタ テックブログ】CTOが組織をサッカーで例えてみた話

始めに

こんにちは。フォースタートアップスでCTOをしている戸村(@KenjiTomura)です。

最近、サッカー界では欧州スーパーリーグ問題が話題ですね。

クラブ運営の枠に止まらず、ヨーロッパのサッカー文化にも波及する様々な問題が出てきおり実現は難しそうですが、私はサッカーを見ることが好きなので純粋に強いクラブ同士の試合が多く見られるようになるということは楽しみだったので少し残念です。

私がCTOを務めるフォースタートアップスの開発チームにおいても、私の好きなサッカーの哲学を取り入れながらチーム作りをしているので、今回は私が目指している開発組織について記事にさせていただきました。

最近の開発について

開発はプロダクト作りにおいても、技術においてもどんどん進化していっています。さらにはその進化のスピードもどんどん早くなっていっているのは、皆さんも感じているところだと思います。

プロダクトの規模も複雑性も増えていく中で、1人の人間が全ての最新の技術を押さえ、社内の全てのPull Requestを全て確認し、全てのコードを把握し、問題が起きないように開発をしていくことはどんどん難しくなっています。

そのため、CTOや開発責任者が開発の方向性を決めながら、細部はメンバーそれぞれに権限を与え、判断を委ねていくべきだと私は思っています。

この状態はターンのある野球などのスポーツよりは、ターンがなくリアルタイムに進むサッカーが近いのではないかと思います。

そこで私は、サッカーの戦略の中でも1974年W杯のオランダ代表が唯一完成させたと言われているトータルフットボールを目指しています。

トータルフットボールについて

トータルフットボールとはなんなのでしょうか。
以下はWikipediaのトータルフットボールの解説を少し抜粋しました。

1950年代初期に、オーストリア人のヴィリー・メイスルによって考案された「渦巻き」理論がトータルフットボールの原案であるとされている。
それは、個々の選手が思いのままにポジションチェンジを繰り返し、渦を巻くようにチームがダイナミックに機能するというものであった。

ただ、これを可能にするには、選手一人一人が同じくらい高い技術と戦術眼を併せ持ち、なおかつかなりのスタミナが必要だと考えられていた。

さらに、動きの連続性を持ったその渦が、自ら意思を持つように前進と後退を繰り返すためには、渦の中心にいながら、渦の外からその流れを俯瞰できる稀有なビジョンを有し、渦をコントロールするだけの並外れた影響力を具えた選手が必要であった。

何といっても、最大の特徴はポジションが存在しないことである。
当時のオランダ代表にとってポジションとは「キックオフ時の立ち位置」というだけのものであり、攻撃時には選手は積極的にボールを持つ選手を追い抜いて前線に飛び出し、守備時にはFW登録の選手もカバーリングに入る。

サイドバックの選手が前線へ飛び出せばウイングの選手がそのスペースを埋めに下がる。
まさに全員攻撃・全員守備である。

また、“スペース”を最大限活用する考え方から、ウイングを中心としたサイドアタックを積極的に使い、ワイドな攻撃を展開した。

このサッカーを支えたのは選手全員の高い技術、戦術眼、スタミナもさることながら全員が高い守備意識を持っていたことも忘れてはならない点である。

反面、この戦術は理想的だが非効率的とされ、完全分業でポジションを固定した方が効率が良いといった説もあるが、後に様々なチームによって改良され、現代サッカーの戦術に浸透していく事になる。

引用元:https://ja.wikipedia.org/wiki/トータルフットボール

このトータルフットボールは1974年W杯のオランダ代表ヨハン・クライフが唯一完成させたと言われています。 クライフ以後のサッカー界でトータルフットボールを実現したチームは無いとされています。
しかし、後にクライフはバルセロナというクラブの監督となり、その時に司令塔としてプレイしていたグアルディオラへと系譜が繋がれ、形を変えてバルセロナの中にトータルフットボールの新しい形が活きていると私は思っています。

そして、グアルディオラバルセロナの監督になり、メッシを中心とした美しいサッカーをバルセロナで実現したと思っています。

私はこの形の組織を目指しています。

トータルフットボールのような開発組織とは

トータルフットボールのような開発組織とはなんなのでしょうか。
重要なポイントとしては以下の3つです。

  • 激しいポジションチェンジにより、明確なポジションが存在しない
  • 高い位置からのプレス & オフサイドトラップ
  • 選手全員の高い技術、戦術眼、スタミナもさることながら全員が高い守備意識を持つ

一つずつ深掘りしていきましょう。

激しいポジションチェンジにより、明確なポジションが存在しない

クラウドインフラが出てきて以降、エンジニア1人が受け持つ領域はどんどん増えています。
その為、フロントエンドエンジニアがサーバサイドに業務領域をオーバーラップしていくこともあるだろうし、その逆も同じ様にあります。

それはさらにサーバレスなどの技術が進むことによりフロントエンドエンジニアがインフラを理解し、触っていくなどのオーバーラップも拍車がかかっていくと感じています。

弊社でも得意領域などはありますが、あまり領域を明示せずに様々なところにポジションチェンジしながら開発に取り組んでもらうことが多く、エンジニアはエンジニアリングだけではなく、ユーザヒアリングから仕様検討も行うことも多くあり、それが結果的に良いプロダクトを作るための基になっています。

高い位置からのプレス & オフサイドトラップ

サッカー用語で、プレスとはボールを持っている相手にプレッシャーをかけたり、パスコースを塞いだりすることを言います。

オフサイドトラップは説明が難しいので詳細な説明は割愛しますが、相手の攻撃を防ぐディフェンスの戦術の一つです。

このポイントを簡単に説明すると、プレスやオフサイドトラップなどのディフェンス戦術を用いて、守備の時間を減らすことです。

開発において攻撃とはアウトカムを出すこと、ストーリーポイントを消化することなどで、守備とはバグ・障害の対応、手作業(Toil)の運用、リファクタリングなど、やらないといけないが必ずしもプロダクトを前に進めるものではないことだと思っています。
開発においては、ここで定義している守備の時間を減らし、攻撃の時間をできるだけ増やすことが大事です。
守備の時間を減らすためには以下のようなものが挙げられますが、こういったものを適切に取り入れながら守備の時間を減らすという戦術が重要だと思っています。

  • CI / CD
  • Toilの撲滅
  • 明らかに負債となるコードを減らす
  • ポストモーテムによる失敗の共有
  • マネージドサービス、SaaSの活用
  • インフラやコードクオリティなどの適切なモニタリング

そして、より良質な攻撃をするためには、闇雲にコードを書き続けることは正しいとは思っていません。
戦略、プロダクト、ユーザなどの深い理解をした上で書かれたコードは良質なコードになりますし、長くリファクタリングをしなくてもバグを生みづらいコードになっていきます。

選手全員の高い技術、戦術眼、スタミナもさることながら全員が高い守備意識を持つ

特にこのポイントが一番重要だと私は思っています。

要は高いプロフェッショナル意識をもち、1つ1つの仕事に怠慢なプレーをしないということです。
さらには、選手全員というところも大きく重要なポイントです。

サッカーでいえば、一人でも連携が取れていない選手がいるだけで、その選手から連携が崩れボールが取られてしまったり、オフサイドトラップがうまくいかず失点しまったりということが容易に想像できます。
エンジニアも同じで、どんなに使いやすく美しく書かれた優れたコードでも、1人のミスによって、セキュリティの問題に発展し大切なデータを失ったり、サービスの信用を失うこともあります。

なぜサッカーでバルセロナというクラブが強いのかというと、この全メンバーのゴールへ向かうビジョンの一致、高い戦術理解の能力、ハイレベルな一つ一つの技術など、当たり前のレベルが高く設定されていることだと私は思っています。

海外のサッカーの試合を見ていると、同じリーグに属しているクラブチームでも上位のチームはパス精度、パススピード、トラップ精度が圧倒的に高いと思います。そこから相手の守備を崩していき、ゴールにつなげる。

パス精度が高いことによりトラップのしやすいパスになり、トラップ精度が高いことによりパススピードが上がってもトラップのミスが少なくなり、高速にパスを繋げていくことで相手の守備が崩れ、パスがどんどん通りやすくなっていきます。

しかし、この中にトラップ精度が悪い選手などが混じってくるとパススピードを下げないといけなくなり全体の精度が下がっていき、通るはずだったパスが通らなくなってしまいます。

開発組織も同じです。 それぞれの当たり前のレベルが高いからこそクオリティの高いプロダクトが作れます。 デザイナーがどんなに優れていても、フロントエンドエンジニアがそのデザインを再現できなければ意味がありません。 その逆も同じでフロントエンドがどんなに優れていても、デザイナーが優れていなければUXの優れたプロダクトは作れません。

サーバサイドも同じです。

最近マイクロサービスという話が良く出ていますが、分割するポイントを間違えるとリクエストが増え、あっという間に遅いサービスになってしまいます。

さらには一回マイクロサービス化して終わりではなく、サービスは成長していくものなので、新しい機能を追加する度に適切なサービスに機能を追加していかないといけないですし、既存ではなく新規のサービスとして作るなどの判断をしていかないといけません。 そんな状況でプロフェッショナルではない怠慢なプレイをしてしまうと、プロダクト全体のクオリティの低下に繋がってしまいます。

まだプロダクトのクオリティ問題で済んでいれば良いですが、これがセキュリティ問題に発展してしまったとしたら目も当てられません。

最後に

このトータルフットボールという考え方は完成形が難しく、元日本代表監督のイビチャ・オシムも「目指しているのはトータルフットボールだ。ただしそれは永遠に実現されないが。」と言っています。 このトータルフットボールは個人の力に大きく依存するので、得意領域や専門性は必要性ですがオールラウンダーな人を採用していかなければいけません。

それは求人票で厳密に定義された職務を遂行してもらうジョブ型雇用ではなく、優秀なメンバーを採用し、その人にやることを合わせていくようなメンバーシップ型雇用になっていくと思います。 ですが、組織は往々にして大きくなるにつれてジョブ型雇用に組織全体を変えていかないと難しくなると言われています。
フォースタートアップスの開発組織も大きくなるにつれ同じ悩みにぶつかる可能性もあるでしょう。

トータルフットボール的な組織の完成形はオシムの言うようになかなか難しいのかもしれませんが、最良の形を模索しながらより良い組織を作っていきたいと思っています。

【フォースタ テックブログ】タグの自動予測について。STARTUP DBに機械学習を組み込んだ話【後編】

f:id:forStartups:20211025162517p:plain

こんにちは。テックラボの松原です。

以前、投稿しました「タグの自動予測について。STARTUP DBに機械学習を組み込んだ話【前編】」の続きのお話です。

前編では、STARTUP DBに登録する企業のサービスに適したタグはどれなのか予測する仕組みをつくるため、AWSのコグニティブサービスを試して回り、「やりたいことを、それなりの精度でやるためには、自身で実装しないと厳しい」という結論になったというお話でした。

今回は、手前で開発したその仕組みについて、お話をさせてもらおうと思います。

学習モデル作成

前回も簡単に記載しましたが、学習モデルは、以下の図のようなフローで作成しています。

①学習を行うためのデータ取得と解凍・抽出

まず、言語の処理をするためには、自然言語の文章を構造化し大規模に集積したもの「コーパス」が必要になります。品詞などの情報を含んだ百科事典のようなものですかね。
「全集」とも呼ばれるそうです。

当初、STARTUP DBの中にあるデータだけで、コーパス作成を試みたですが、情報量が足りず、計算できなかったり、過学習が起きました。

後でベクトル化の話をしているのですが、ベクトル化できない言葉が多く、ベクトル化できたデータもベクトルの向きがおかしい…という現象が出てきたのです。
ベクトルは「大きさと方向を持つ量」です。[ 1, 3, 2.5 ]のような数のリストとして表現ができます。

もっと大量の、一般的な文章データが必要だということで、利用したのがWikipediaのデータです。

Wikipediaのコンテンツデータは、再配布や再利用のために利用できる一元化されたデータベース・ダンプでの提供が行われています。
日本語のダンプファイル(出力ファイル)は、こちらからダウンロードができるようになっています。

ダウンロードしたファイルは、bz2という圧縮ファイルになっています。
それを解凍すると、下の画像のようなXML形式のデータが確認できます。

このままだと使えないので、このXMLから本文を抽出する必要があります。

そこで抽出に使うのが、オープンソースで提供されているWikiextractorです。
名前の通りですが、Wikipediaのダンプファイルから本文を抽出するためのプログラムです。

ダウンロードした圧縮ファイルを指定して、このようなコマンドでWikiextractorを実行すると

python -m wikiextractor.WikiExtractor jawiki-latest-pages-articles.xml.bz2

本文が抽出された、複数のテキストファイルが保存されます。

テキストの中身はこんな感じです。

見て分かる通り、<doc ….>というような、不要な文が残ってはしまいます。
なので、このあたりの不要な文も除去して、学習の大元となるデータの完成です。

分かち書き

日本語は、英語などのスペースで区切られた言語と異なり、どこからどこまでが1つの単語なのか、判別が容易ではありません。

日本語の自然言語処理をするためには、その区切りを見つけるために、形態素解析を用いて分かち書きするというのが一般的です。

最近では、深層学習により適した前処理として、WordPiece、Byte-pair encoding (BPE)、 SentencePiece など、テキストを「サブワード」と呼ばれる単語よりも短い単位に分割する手法が用いられるようになってきていますが、深層学習を用いたいわけでもない、というのと、可能な限りブラックボックスにせず、見えるロジックで解いておきたいという思いがあり、昔ながらのやり方ではありますが、MeCab分かち書きを行うようにしました。

MeCab (和布蕪)とは、京都大学で開発された形態素分析のエンジンです。
2013年からバージョンは0.996のままですが、今でもよく利用されているエンジンです。

他の形態素解析のツールとしては、Sudachiというワークスアプリケーションズが開発したものや、JANOMEというオープンソースもあります。
検索・分析エンジンのElasticsearchでよく使われている、kuromojiというものもあります。

IPAdicやNEologdなどの辞書を使いたいとか、速度を重視した時にMeCabを使うのがお薦めです。(メモリの消費はよろしくないです)

このタグ予測の仕組みの場合、APIのリクエストを受けた時にも形態素解析を動かすようにするのですが、レスポンスに時間を掛けすぎないという目的もあり、MeCabを選択しています。

全部ひっくるめたモデルに文章をインプットして、タグが返ってくるようにするというよりは、途中途中の処理をホワイトにすることで、チューニングがちゃんと行えるようにしたいという意図もあり、APIのコントローラー上で形態素解析を動かす必要が出てきたという経緯です。

辞書は、新語・固有表現に強いNEologdを使っています。

新語に強いと言っても、スタートアップ界隈の言葉は全然カバーしきれず、独自の辞書が必要になりはするのですが…

MeCabを使い、①のテキストを分かち書きすると、以下のような結果を得ることができます。

ちなみに、品詞付での出力をするように設定して実行してます。

見て分かる通り、ノイズがとても多いですね。

「、」「。」などの記号、「の」「は」などの助詞、「きっと」「もっと」などの副詞、これらは、文章全体の意味を捉えるという目的に対しては、不要な情報と言っていいと思います。

なので、あらかじめ除外する品詞や単語を決めておき、分かち書きしながら、取り除くという処理を行います。

# 除外する品詞1リスト
except_main_features = ['記号', '助詞', '助動詞', '感動詞', '接頭詞', '副詞', '連体詞', '接続詞']
# 除外する品詞2リスト
except_sub_features = ['代名詞', '接尾','副詞可能', '自立', '非自立', '形容動詞語幹']

そうすることで、もう少し意味のある言葉のみを抽出することができます。

お見せしている例では助詞と副詞を除いてますが、結果次第では含めても良いかもしれませんね。

参考までに助詞、助動詞などを除いた場合、ベクトルの精度があがるという論文のリンクを貼っておきます。

分かち書き + ノイズの除去を行ったデータをS3にアップロードして、学習データの準備完了です。

③学習

前編でも紹介したSageMakerで提供されているアルゴリズムであるBlazingText、これを用いて、ベクトルデータを生成するモデルを作っていきます。

必要なプログラムは、sagemakerのパッケージに含まれているため、考慮することはハイパーパラメータ(機械学習を行うためのそのアルゴリズムに設定する値)をどうするかぐらいでしょうか?

自身で何度も試して決めることもできますが、自動でハイパーパラメータを調整する仕組みもAWSにはあるので、それを使うのもいいかと思います。

参考: https://t-redactyl.io/blog/2020/11/automatic-word2vec-model-tuning-using-sagemaker.html

まずは、BlazingTextのimageを取得します。

image = sagemaker.amazon.amazon_estimator.get_image_uri(region_name, "blazingtext", "latest")

そのimageを使って、Estimator(トレーニングとかするオブジェクト)を生成します。

estimator = sagemaker.estimator.Estimator(image,
                                          role,
                                          train_instance_count=2,
                                          train_instance_type='ml.c4.2xlarge',
                                          train_volume_size = 30,
                                          train_max_run = 360000,
                                          input_mode= 'File',
                                          output_path=s3_output_location,
                                          sagemaker_session=sess)

Estimatorに対して、ハイパーパラメータを設定後、

estimator.set_hyperparameters(mode="batch_skipgram",
                              epochs=5,
                              min_count=5,
                              sampling_threshold=0.0001,
                              learning_rate=0.05,
                              window_size=5,
                              vector_dim=100,
                              negative_samples=5,
                              batch_size=11, #  = (2*window_size + 1) (Preferred. Used only if mode is batch_skipgram)
                              evaluation=True,# Perform similarity evaluation on WS-353 dataset at the end of training
                              subwords=False) # Subword embedding learning is not supported by batch_skipgram

レーニングデータを指定し、

train_data = sagemaker.session.s3_input(s3_train_data, distribution='FullyReplicated',
                                        content_type='text/plain', s3_data_type='S3Prefix')
data_channels = {'train': train_data}

学習を開始します。

estimator.fit(inputs=data_channels, logs=True)

これでモデルの完成です。

出来たモデルに対して、単語を投げると、その単語のベクトルを得ることができます。

タグの予測

①サービスの説明文の分かち書きとベクトル化

STARTUP DBの管理画面にある、企業のサービス編集画面の「タグ予測」ボタンが押されると、Flaskで構築したアプリケーションサーバーにサービスの説明文が投げられます。
アプリケーションサーバーには、学習用のデータを作成したサーバーと同様に、MeCabがインストールされており、分かち書きが行われます。

アプリケーションサーバーは、その後、分かち書きされたサービスの説明文をBlazingTextで作成したモデルが乗っているエンドポイント(MLホスティングインスタンス)に投げて、各単語のベクトルを取得します。

②タグの一覧の取得とベクトル化

アプリケーションサーバーは、タグの一覧を、企業情報用のサーバーからAPIを叩いて取得します。

その後、アプリケーションサーバーはサービスの説明文と同様に、タグ一覧をモデルが乗っているエンドポイントに投げて、各タグのベクトルを取得します。

③ベクトルのクラスタリング

分かち書きで得た単語の数は、もちろん一つではなく、ベクトルの向きも様々です。
近似のタグを見つけるためには、それらを集約させる必要があります。
そのために、得られたベクトルをクラスタリングさせる必要があります。

複数のベクトルの集まりをテンソルと言いますが、テンソルはn次元の行列という形で表現ができ、行列の次元を削減するためにクラスタリングを利用しています。

次元を削減するというのは、n次元の行列をk次元に減らすという作業です。

次元削減自体にも、主成分分析 (Principal Component Analysis: PCA) や非負値行列因子分解(Non-negative matrix factorization: NMF)など次元削減の手法がありますが、今回の内容においては、クラスタリングでいい、という判断をしています。

SageMakerの提供しているアルゴリズムの中にも、クラスタリングを行えるアルゴリズムがあります。ただし、問題になるのは、そのアルゴリズムの種類です。

SageMakerで用意されているクラスタリングアルゴリズムは、K-meansです。
K-meansは、中心を仮定してクラスタリングする方法で、今回の目的とは実は合致しません。

と、言葉で言っても分かりにくいですが、Python機械学習ライブラリであるscikit-learnのWebページに非常に分かりやすい図があります。

参照: https://scikit-learn.org/stable/auto_examples/cluster/plot_cluster_comparison.html

K-meansは1番左の列の図になりますが、図にもみられるように、境界を越えてクラスタリングが行われてしまうのが見えますでしょうか?

また、密度連結成分を検出するDBSCANは、非常に綺麗に分けてくれるアルゴリズムではありますが、

境界がなさ過ぎるとすべて同じクラスタとして見なされてしまいます。

今回のケースだと、似たようなまとまりの集まりだったとしてもそれをいくつかの方向にクラスタリングして欲しいところです。

なので、タグ予測でやりたいクラスタリングを考慮し、アルゴリズムはSpectral Clusteringを選択しています。

Spectral Clusteringは、固有値によるスペクトル分解を行うもので、内部でK-meansやDBSCANのロジックが動いていたりします。

このSpectral Clusteringを利用するため、SageMakerは利用せず、アプリケーションサーバー内にscikit-learnを用意し、Spectral Clusteringを用いたクラスリングを行わせるという方針をとっている訳です。

クラスタリングしたのち、クラスタリングしたベクトルの平均合成を行い、クラスタの方向を導き出します。

④コサイン類似度を用いた類似のタグの算出

ベクトルの近似値は、「コサイン類似度」によって求めることができます。

ベクトルの行列を内積やらなんやらすることで計算することができます。

参考: https://www.cse.kyoto-su.ac.jp/~g0846020/keywords/cosinSimilarity.html#:~:text=%E3%82%B3%E3%82%B5%E3%82%A4%E3%83%B3%E9%A1%9E%E4%BC%BC%E5%BA%A6%E3%81%A8%E3%81%AF,%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%81%AB%E3%81%AA%E3%82%8B%E3%80%82

アプリケーションサーバーは、タグのベクトルと、クラスタリングされた説明文のベクトルを比較し、近い順にランク付けを行い、管理画面に返します。

最後、これを管理画面で表示してあげるだけです。

課題

この方法には、まだいくつか課題が残っています。

Wikipediaのデータが一般的過ぎて、サービス固有の名前や説明がうまくベクトル化できない → コーパスにスタートアップ関連の記事を多く取り込む必要がある

②辞書に無い言葉がサービスに多く出てくるため、うまくベクトル化できない単語がある → 独自辞書に随時言葉を登録していく作業が発生する

③あっちこっちで処理をさせるようになっており、仕組みが複雑 → SageMakerで動くモデルを自作し、そこに投げるだけでいい仕組みにしてもいいかもしれない

などが、大きな課題です。

まとめ

ブラックボックスにし過ぎず、理解できるロジックで組み上げたことで、細かい調整がきき、精度も確かに出るようになったと思います。ただし、課題にも挙げているように、予測したい文章と学習させる文章が合うかどうか、未知の言葉に対する対応など、潰しきれない課題はどうしても出てくるものだと実感させられました。

文中でも記載しましたが、深層学習に適した前処理なども確立されてきていますし、日本語で、BERTを用いた自然言語処理などもでてきており、 そういうものを導入することで、未知語への対応も、より高い精度の学習もできる可能性があります。そのような新しい仕組みに入れ替えることも考慮に入れた上で、引き続き、ブラッシュアップをしていこうと思っています。