気まぐれエンジニア日記

ひよっこエンジニアの雑記

不確実性耐性と問題の樹形図

この記事は

道草 Advent Calendar 2025 - Adventar

23日目の記事です。

はじめに

ソフトウェアづくりでの不確実性をどのように操っているかを書いてみる。

ソフトウェアを開発にするにあたって、不確実性耐性が重要だと言われることがままある。これは、ソフトウェアで実現したいものが何か、どのように実現できる・すると良いのか、どのような順で実現していけるのかといった問題が、単純な問いではなく、曖昧で、ときに途中で変化するからだろう。やりたいことや実現方法、進め方が明確にならないと動けないのではソフトウェア開発は難しい。

著者はそういった曖昧な問いに対して、無意識的に樹形図を使って考えていることに気づいた。要するに「分解」と言えばそれまでなのだが、「分解」したものを全て解くわけではないというのが「分解」という言葉のイメージと異なるものだと考えている。

エディタ開発を例に

著者は自作のReact製のWYSIWYGエディタを持っているから、その開発を例にとって樹形図のイメージを説明しよう。
https://github.com/kumachan-mis/react-clay-editor
React製のエディタを開発にあたっては、いくつかの問題を解くことになる。今思いつくものを列挙してみると次のようになる。

  • 新しい表示要素のサポート
  • 冗長な表示要素の削除
  • レンダリングの高速化
  • APIとしての使いやすさの向上
  • 動作環境の拡大
  • Reactなどライブラリのバージョン追従
  • 内部ロジックのシンプル化
  • よりモダンな開発環境への移行

これらをざっくり分類しようとすると、以下のように分けたくなるように思う

  • エンドユーザー向け
  • パッケージ利用者向け
  • 開発者向け

こういった「名詞による分類」はしばしば用いられるが、ひとつひとつが扱う範囲が広く、曖昧になってしまう。何より、ひとつひとつの小さな問題たちが、一体何に繋がっているのかを見失いやすく、モチベーションを保つのも難しくなってしまうだろう。

問題を樹形図で考える

そこで問題を樹形図で考えてみよう。先では問題の名詞による分類を行ったが、問題を樹形図にするのであれば、分類もまた問題になる。

  • ユーザーにとって必要十分なエディタの機能はなにか?
  • 多くエンジニアに使われるパッケージであるために必要なことは何か?
  • 開発しやすいリポジトリであり続けるためにやるべきことは何か?

これらの問題は曖昧だが、より具体的な小さな問題を最初に考えているから、構えずに考えやすくなる。また小さな問題たちを解く価値も分かりやすくなる。そして究極的には、以下の問題を考え続けていることになる。まさに「根本の問題」と言ったところだろう。

  • 「良い」エディタライブラリとはどうあるべきか?

不確実性耐性が高い人は、一見すると「ユーザーにとって必要なエディタの機能はなんだろう?」のように、抽象度の高い問題を考えているように見えるかもしれない。しかしながら、実際には「表が書ける機能ってあった方がいいのかな?どう作ろうかな?」と考えていて、ただその価値を表す上位の問題を忘れていないだけなのである。

樹形図の探索点同士にポインタを張る

著者の問題の解き方が1本道ではなく樹形図である以上、複数の問題を同時に考える瞬間はしばしば訪れる。例えば、作ってしまったけれどいかにも使われない機能を消す問題は、同時に内部ロジックのシンプル化を呼び起こす。エンドユーザーを意識していた対応が、開発者のためにもなっているということはしばしば起こる。

こういったことに気づくために、樹形図の探索点にポインタを張っておく。より正確には、ある問題を解いたときに、実は同時に扱いたい問題も解いた、あるいは解きやすくなったという変化がないかを考えている。隣同士の問題もそうだが、上下の問題についても同じだ。今解いたこの問題をもって、ユーザーにとって必要十分な機能が揃っていて、実はさらに足そうとしていた別機能は必要なさそうだとわかる、なんてこともある。これが、冒頭に、問題の樹形図化が「分解」とは異なると述べた意味である。

終わりに

ソフトウェア開発での不確実性耐性について、問題を樹形図にするという立場から述べた。樹形図と言いながら、この文章の中に図は出てこなかったが、これを読みながら整理することも、また不確実性耐性の一環ということでご容赦願いたい。頭の中で図を書いて、解けるところから解いていき、探索を続ける。これが不確実性耐性だと著者は思っている。

おもちゃは気づいたら大きくなっている

この記事は

道草 Advent Calendar 2025 - Adventar

2日目の記事です。

書くこと

小さなおもちゃ≒大きな泥団子しか作れない - velengel++

って著者のべるくんが言っていたので、思ったことを書こう。ほとんど仲間内だけのアドカレなので、こういうのもありかな。

大きなおもちゃも作ってみたいけど、って思ってそうに読めたので、そのことを書いていこう。

ダラダラ書くから先にまとめておくよ

おもちゃは簡単に大きくなる

  • 当たり前に完成している状態の壁はきっと思った以上に高い
  • まだなにも作っていないはずなのに、いつの間にか大きくなるもの
  • それでも大きくならないんだとすると、きっとそれはまだPoCで未完成

手と頭を分けよう

  • 当たり前に完成している状態を日々目指す手
  • 作りたいものが1分でできたことになる頭
  • 手が頭に追いつこうとする限り、やることは無限
  • 頭だけを動かして置いておいてもいい

大きくする前提で作り始める人はいないんじゃないかな

大きなおもちゃ、元記事で言うところの巨大コードベースみたいなものは、結果としてできるものというイメージ。初めから巨大なものを作ろうとして作る人はいないんじゃないかな。

小さなプログラムで表現が事足りるなら、その方がいい。そのためにライブラリやフレームワークのような、すでにあるものを使う。その方が失敗がないし、使う労力も少ない。創作の最初の方は、えてして、今から自分が作ろうとするものは、実は作らずにすまないか、を考える。自分はプログラムしかわからないからプログラムを例に出してるけど、曲も、他の創作物も多分一緒だ。

それでも大きくなっていっちゃうんだよね

じゃあなんで世の中は大きなもので溢れているか。商売の秘密みたいな大人の事情を除いて、純粋に創作として考えてみると、やっぱり世の中のものと自分が今欲しいもの・作りたいものが微妙に違うから。微妙に違うだけで、世の中のものと同じ部分も多少は作らないといけない。ソフトウェアがソフトウェアとして成立するために必要な部分はどうしても必要。
成立させるために必要な部分は、本当は全く興味がないのだけど、必要だから仕方なく作っている。こういうことをやっていくと、自然とおもちゃは大きくなっていく。だって、3種類のデータのCRUDを作るだけで、APIは12本必要なんだから。テストも書くし、デプロイの環境構築もする。ほら、もう立派な大きさだね。

そうして思うことは: やった!興味のある部分をやっと作れる!次に思うことは: まだ作り始めてさえいないじゃないか…!!

それでも大きくならないとすれば、多分それは完成してないんだろう

コードベースは、こんな感じでいとも簡単に大きくなってしまう。おや、そんなことない?だとすれば、きっとそれはまだ完成していないはず。もっと分かりやすい表現にできないんだっけ?表現したかったことってこれだけだっけ?見ず知らずの人に自慢できるものだっけ?って考えていくと、「当たり前に完成している創作物」の壁は想像以上に高いことに気づくはず。それをやっていけば勝手に大きくなるし、「同じ部分」だったところが新しく「微妙に変えたい部分」になるかもしれないね。

手と頭を分けよう

こんな感じで、自分は、創作の時間のほとんどはその物をその物として成立させるための活動だと思っている。少なくとも手を動かしている目の前の時間は、創造的である方が珍しい。生成AIくんがいる世の中だから、だいぶ創造的な時間は増えたけど、それでも。それだけだと面白くない。簡単にいえばあっという間に飽きてしまう。

だから手と頭を分けよう。頭と手が分かれてるゲームのボスの話はしてないよ。手を動かしている目の前の時間はそれとして、頭を動かして面白いことを考える時間を別に作ると、きっと飽きない。思いついたことは「できたもの」として扱っていい時間は楽しい。手の1分は現実の1分だけれど、頭の1分は現実の1カ月にもなる。それだけのスピードで未来に行ける。早く1か月後を現実にしたいから、手も早くなる。

手と頭が分かれていれば、未完了のクエストリストが空になることはないし、完成が見えなくなることもない。頭だけ動かしておいて、手を動かすかはまた別で考えたっていい。作った気になるだけなっておいて、本当に作りたくなったら、その時に作るのでもいいよね。

スクラムマスターのおしごと!

このブログを動かすのはいつぶりでしょうか、くまちゃんです。

これは 道草 Advent Calendar 2024 - Adventar の8日目の記事です。

はじめに

ソフトウェア開発チームのスクラムマスターの役割を10月から担うようになったので、どんなことを意識しているのか、どんなことをやっているのか、を書いてみようと思います。

著者はソフトウェアエンジニアを仕事にしているのですが、社内で少し特殊なポジションにいる関係で、1年ごとに部署を変わりながら、様々なプロダクトの開発に関わっています。現在3年目で、この10月から3部署目で仕事をしています。1年目、2年目の経験を活かして、スクラムマスターとして貢献して欲しい、という前提でのチームジョインとなりました。

そうした状況で、ステークホルダーから見た価値 を最も意識し、価値提供のために日々動きを工夫できるチームにするための仕組みづくりをしています。

スクラム開発とスクラムマスター

前提としてスクラム開発、スクラムマスターについて振り返っておこうと思います。スクラム開発のバイブルであるスクラムガイドを参照しましょう。

スクラムの定義

スクラムとは、複雑な問題に対応する適応型のソリューションを通じて、⼈々、チーム、組織が価値を⽣み出すための軽量級フレームワークである。 簡単に⾔えば、スクラムとは次の環境を促進するためにスクラムマスターを必要とするものである。

  1. プロダクトオーナーは、複雑な問題に対応するための作業をプロダクトバックログに並べる。
  2. スクラムチームは、スプリントで選択した作業を価値のインクリメントに変える。
  3. スクラムチームとステークホルダーは、結果を検査して、次のスプリントに向けて調整する。
  4. 繰り返す。

スクラムマスター

スクラムマスターは、スクラムガイドで定義されたスクラムを確⽴させることの結果に責任を持つ。スクラムマスターは、スクラムチームと組織において、スクラムの理論とプラクティスを全員に理解してもらえるよう⽀援することで、その責任を果たす。

スクラムマスターは、スクラムチームの有効性に責任を持つ。スクラムマスターは、スクラムチームがスクラムフレームワーク内でプラクティスを改善できるようにすることで、その責任を果たす。 スクラムマスターは、スクラムチームと、より⼤きな組織に奉仕する真のリーダーである。

スクラムマスターは、さまざまな形でスクラムチームを⽀援する。 (中略) スクラムマスターは、さまざまな形でプロダクトオーナーを⽀援する。 (中略) スクラムマスターは、さまざまな形で組織を⽀援する。 (後略)

色々と書いてありますが、要するに以下と思って良いでしょう。

2つのコンポーネントの開発を担うチーム

著者の所属するチームは2つのコンポーネントの開発を担う10人弱のチームです。いわゆる刷新案件というべきもので、従来システムと同等の機能を持ったシステムに一部変更を入れながら、よりモダンなプロダクトに作り変えようというPJに関与しています。

1人は上長であり、目標・評価や他チームとの調整に責任を持っています。著者を含むそれ以外メンバーが半数ずつに分かれて2つのコンポーネントをそれぞれ開発している状態です。各コンポーネントの開発者のうちの1人(計2人)はコンポーネントリードと呼ばれ、企画チームとのやりとり、仕様の策定、コードレビューによる品質担保、開発スケジュール決定に責任を持ちます。

著者自身はというと、メインは片方のコンポーネントの開発者でありながら、チーム全体のスクラムマスターでもあるという立ち位置になっています。チームはスクラム開発をやってみたいとは思っているものの、どうすれば良いかがわからず、「なんちゃってスクラム」をやろうとしていたという状態でした。スクラムマスターのいないスクラム開発になりかけていたところに、著者がジョインしてサポートをしているという感じです。

著者は、プランニングにおけるコンポーネントリードのサポート、デイリースクラム・レトロスペクティブ・リファインメントの進行支援など、スクラム開発をチームに理解してもらい、イベントを成立させるためのサポート全般をおこなっています。チームの関与するものが刷新案件であって、仕様が最初から一定明確である都合上、スプリントレビューと名前のついた会議は設定していませんが、案件関係者が集まる定例の場は存在しているので、必要があれば、定例にチームの成果を持っていくように助言する役割も持ちます。同時に開発者でもあるので、もちろん実装やテストといった作業も行いますし、コンポーネントリードが叩きとして定めた仕様に対して議論も行います。どちらか一方のコンポーネントリードがお休みの時は、コンポーネントリード兼スクラムマスターとなることもあります。(まだ2人のコンポーネントリードが同時でお休みだったことがないのが救いです)

とにかく、スクラムチームを成立させるために必要なあらゆることを、状況に応じてロール変更もしながらしながら遂行していく、これが著者のスクラムマスターとしてのスタイルです。

スプリントゴールドリブンなチーム運営

「なんちゃってスクラム」をやろうとしていたチームに対して著者が一番最初に伝えたこと、それは ステークホルダーから見た価値 を意識しましょうということでした。

開発チームの関心は、しばしば、日々の作業とその進捗に向いてしまいがちです。〇〇の実装が完了するだとか、〇〇のリファクタリングであるとか、そんな具合です。そうではなくて、その作業をやった結果届けられる、ステークホルダーにとっての価値は何か?に向き合わなければいけないと考えています。これは言葉で書くと簡単ですが、実際には難しいことです。チームが日々向き合っているのは「作業」であるのに対して、それら俯瞰で見ることを意識づけようとする活動だからです。

そこで、著者がまず力を入れたのがスプリントゴールの設定です。再びスクラムガイドを参照しましょう。

スクラムチーム全体が協⼒して、そのスプリントになぜ価値があるかをステークホルダーに伝えるスプリントゴールを定義する。

スプリントゴールとは、そのスプリントになぜ価値があるかをステークホルダーに伝えるものである必要があります。

「〇〇の実装が完了する」だとか、「〇〇のリファクタリング」であるとかは、スプリントゴールとしては適していないという事になるでしょう。〇〇の実装が完了した結果、ステークホルダーに〇〇の機能のデモができるようになるなら、「ステークホルダーに〇〇の機能のデモができるようになる」をゴールにするべきでしょう。〇〇の機能を作るための前作業として、「〇〇のリファクタリング」を必要とするなら、「〇〇の機能のデモをダミーデータ・ダミーロジックで見せられるようにする」をゴールとする方が、内容としては限定的でも、ずっと意味があります。そういった意識でスプリントゴールを設定するように助言しますし、著者自身もスプリントゴールの内容を検査します。

スプリントの終わりに行うスプリントゴールの達成状況の振り返りでも、メッセージングとしては似たものにしています。達成状況の判定で著者がチームに問うていることは1つです。

今からスプリントレビューをやったら、その成果をステークホルダーに見せられますか?

見せられる状態にあるなら達成ですし、そうでなければ未達にしています。逆に、コードレビューの細部の指摘対応が残っているだとか、単体テストが一部実装できていないとかいうことは、達成判定には入れていません。そんなことはチーム内部の都合であって、ステークホルダーの興味ではないからです。しかしながら、そうした残作業は確実に開発者の工数を必要としますから、勘案しないのではなく、次スプリントのゴール設定において加味するように助言をしています。

やや強めの意志を含めたメッセージングではありますが、著者はそれだけ ステークホルダーから見た価値提供 を重視していますし、そのことを強くチームに伝えるスクラムマスターを演じています。

ゴール達成のために日々動きを工夫できるチームへ

スプリントゴールの設定と達成判定に思想を込めたら、最後に手を入れるのは日々の動き、デイリースクラムです。

著者のチームでは、デイリースクラムで、各スプリントゴールに対する状況とまずい場合のチームとしての今日の動きの工夫を1行で書く、という取り組みをしています。これによって、メンバー全員が、毎日、全てのスプリントゴールを意識し、必要であれば動きを変える、ということを実現できています。ここは状況がまずいからペアプロをしよう、アサインを変えようとか、逆に今スプリントは余裕があるからこういうこともやろうとか、こっちのゴールを落としてでもより重要なこっちのゴールを重視する動きに変えようとか、そういった判断を日々行えています。

もちろん、ゴールを達成することが絶対的に褒められるべきことで、未達は絶対的に責められるべきことというのでは決してありません。達成未達に一喜一憂しないでくださいというのは振り返りでも伝えています。お休みの予定がなかった人が休めば未達になりますし、着手してみて初めて、思っていたより高難易度だったということはしばしば起こるからです。そうであっても、ステークホルダーから見た価値提供 のために何ができるか、この問いを日々考え続けられるスクラムチームに育てることは極めて重要だと思っているので、こうしたやり方を採用しています。

おわりに

ソフトウェア開発チームのスクラムマスターの役割を担うようになったので、意識していることと具体的なチームでの取り組みを書きました。

著者のスクラムマスターとしてのスタイルは、スクラムチームを成立させるために必要なあらゆることを、状況に応じてロール変更もしながらしながら遂行していくものです。スクラムガイドで定義される意味でのスクラムマスター、コンポーネントの仕様策定や調整、開発者としての実装・テスト、これらのことを、チームの状況を考慮しながらウエイトを随時変化させて実施しています。

スクラムマスターとしてのチームへの最初にして最大のメッセージングは、 ステークホルダーから見た価値 を意識しましょうということでした。開発チームが行なっているのは具体的な作業なので、その関心もしばしば、日々の作業とその進捗に向いてしまいがちです。そうではなくて、その作業をやった結果届けられる、ステークホルダーにとっての価値は何か?に意識を向けることが重要だと考えています。

この前提のもとで、著者がこだわったのがスプリントゴールでした。スプリントゴールの設定とその達成判定において、ステークホルダーの存在を意識させ、チーム内部の都合と切り離すことを徹底しています。また、ゴール達成のために日々動きを工夫できるチームとするために、各スプリントゴールに対する状況とチームとしての今日の動きの工夫を1行で書く、という取り組みをしています。これによって、これによって、メンバー全員が、毎日、全てのスプリントゴールを意識し、必要であれば動きを変える、ということを実現できています。

ステークホルダーから見た価値 をより強く意識し、結果にこだわれるチームにするために、これからも著者は活動します。

そのアプリケーションから「ユーザの活動」は見えるか

今年もどうぞよろしくお願いします。くまちゃんです。
アドカレを書いて以来、書きたいことが増えたので、また書きます。

はじめに

多機能なアプリケーションで溢れかえる中で、いかに自分のアプリケーションを作っていくかについて、著者なりの考えを「モノ」と「ユーザの活動」に注目して書きます。

日頃使っているアプリケーションには様々な機能がついています。ものによってはその使い方を著した本が書店に並んでいるほどです。そういった超多機能なアプリケーションが身近にあると、自分がアプリケーションを作ろうと思ったときにも、様々な機能を作りたくなります。

けれども、機能を作りすぎるとかえって良くないということもしばしば言われます。使い方について書かれた本があるということは、裏を返せば、本を読まなければ使えないほどアプリケーションが複雑だということです。また、開発者からしても、使われない機能は保守コストを上げるだけとも言われます。

多機能であることが当たり前な一方で、機能のつくり過ぎは災いを招きかねない。こうした難しさの中で、著者がアプリケーションを作る際に心がけていることがあります。それは、アプリケーションが「モノ」中心になると使われにくい機能が増えてしまうから、「ユーザの活動」中心で考えるようにしようということです。

「モノ」中心のアプリケーション

アプリケーションの中には、様々なデータが存在します。そのデータの表現能力を高めることで様々なものを作れるようになったり、多様な表示ができるようになったりする方向性のアプリケーションのことを、ここでは「モノ」中心のアプリケーションと呼ぶことにします。

プレゼンテーション作成ソフトを例にとりましょう。プレゼンテーションには、聞いてくれる人に見せるための「発表資料」がモノとして登場します。多くのプレゼンテーション作成ソフトでは、その発表資料をリッチにするための様々な機能ついています。スタイリングを選べたり、動画や画像、図形を挿入できたり、グラフや表の種類やスタイリングを選択できたり、自由自在です。こうしたアプリケーションは非常に出来が良く、著者自身も頻繁にお世話になっています。

けれども、「こんな図形を使うことってあるんだろうか」あるいは「グラフや表の位置をピクセル単位で調整できる必要はないのに」とふと思う時があるのです。それは、「発表資料」というモノが中心になり、「発表資料」をリッチにしようとしすぎた結果だと著者は考えます。「フロントエンドはJSON色付け係だ」なんていう自虐ネタも、モノが前に出過ぎてしまったが故に生まれた言葉なのかもしれないと、そう思うのです。

「ユーザの活動」中心のアプリケーション

アプリケーションの見方を少し変えてみましょう。アプリケーションのユーザはそこにあるデータを見たり、作ったりすることを含めて、一連の活動を行なっています。その活動を体形的にサポートすることを目指すアプリケーションのことを、ここでは「ユーザの活動」中心のアプリケーションと呼ぶことにします。

先と同じように、プレゼンテーションを例にとりましょう。

プレゼンテーションをすることになったユーザは、どんな活動をするでしょうか。おそらく、いきなり発表資料を作ろうとする人は多くありません。むしろ「いきなり発表資料を作るな」と言われたことがある人もいるのではないかと思います。

著者自身の例を挙げておきましょう。何かの成果報告なら、まずは自分の成果を振り返ります。次に、発表資料のアウトラインを作ります。アウトラインができたら中身を埋めて資料をひとまず完成させます。資料ができたら練習をして、修正を加えます。そうして本番を迎え、フィードバックを受けます。その後、場合によっては、次のプレゼンテーションに向けての活動が始まります。

上記のような一連の活動がプレゼンテーションだと著者は思うのです。「発表資料」はこの活動の一部に登場するものにすぎず、「プレゼンテーション」それ自体ではないと思うのです。だからこそ、「こんな図形を使うことってあるんだろうか」あるいは「グラフや表の位置をピクセル単位で調整できる必要はないのに」と感じてしまうのです。

上手なプレゼンテーションができるようになる方が、発表資料がリッチになることよりも重要だと考えるアプリケーションがもっと増えて良い。そうなれば、使われなくなる機能はグッと減ると著者は考えます。なぜなら、それぞれの機能はプレゼンテーションという活動の上に置かれているからです。

「モノ」と「ユーザの活動」の関係性

ここまでのところで、「モノ」中心のアプリケーションと「ユーザの活動」中心のアプリケーションを対立させて話をしてきました。しかしながら、少し落ち着いて考えてみると、この2つは対立しているのではなく交差していることに気がつきます。

「モノ」中心のプレゼンテーション作成ソフトは、それが発表資料でさえあれば、どんなものでも対応してくれます。図形が入ろうが、動画が入ろうがグラフや表が入ろうが、自由自在です。しかしながら、資料作成以外のことは考えてくれません。それ以外のところは他のツールを使ってやれば良いという立場をとります。

一方、「ユーザの活動」中心のプレゼンテーション作成ソフトは、下準備、アウトライン作成、資料作成、練習、フィードバックという活動全体をサポートしてくれるモノになるでしょう。しかしながら、個々でできるものは決してリッチではありません。リッチなメディアは他のものを使って見せたり作ったりすれば良いという立場をとります。

これら2つは本来、互いに互いを補い合う存在のはずなのですが、多くのアプリケーションは「モノ」中心の考え方で作られています。だからこそ、著者自身は「ユーザの活動」を中心にアプリケーションを考えようと思っています。

アプリケーションの外まで、でも最低限に

「ユーザの活動」を中心にアプリケーションを考えるということをもう少し深掘りします。それは、アプリケーションの外にある活動まで考えること、それでいて活動の各部分において提供する機能は最低限にすることだと思っています。

ユーザの活動に目を向けてみると、「この作業は時間がない中でやらないといけない」だとか、「この時には他の人や別のアプリケーションと連携しながら何かをやる」だとか、アプリケーションの外に広がる感情や光景が浮かんできます。そうしたところも考慮して、うまく体験をデザインする必要があると考えています。

また、提供する機能が最低限であることも重要だと思っています。活動の各部分に対してあれこれ機能を提供しても、機能がユーザの活動のシナリオ、時間の流れの上に乗ってこなければ使われないと考えます。「その機能はいつどうやって使われることになるのか」を流れをもって説明できない機能は消すというのが著者のポリシーです。

おわりに

多機能なアプリケーションで溢れかえる中で、いかに自分のアプリケーションを作っていくかについて、著者なりの考えを書きました。

データの表現能力を高めることで様々なものを作れるようになったり、多様な表示ができるようになったりするアプリケーションのことを「モノ」中心のアプリケーションと名付けました。すでにあるアプリケーションの多くが「モノ」中心で作られており、そこにある機能にはリッチすぎると感じるものも少なくありません。

一方、ユーザが行なっている一連の活動を体形的にサポートすることを目指すアプリケーションのことを「ユーザの活動」中心のアプリケーションと名付けました。「ユーザの活動」中心のアプリケーションでは、それぞれの機能は活動の上に置かれるため、使われなくなる機能はグッと少なくなると考えています。

「モノ」中心のアプリケーションと「ユーザの活動」中心のアプリケーションの関係は、対立ではなく交差です。そして、多くのアプリケーションが「モノ」中心で作られているからこそ、著者はそれらとは別の軸を持った「ユーザの活動」中心のアプリケーションを考えるようにしています。その上で、「ユーザの活動」中心のアプリケーションを作るとは、アプリケーションの外にある活動まで見据えること、それでいて活動の各部分において提供する機能を最低限にすることだと考えています。

あれこれいろんなことはできなくても、ユーザの活動を最初から最後までサポートする。そんなアプリケーションがとても好きです。

思いを表現するということ

一度書き始めてみると、意外と書くことがあるものですね。くまちゃんです。

はじめに

これは 思いを持つということ、伝えるということ - 気まぐれエンジニア日記 の続編となる記事です。先の記事では、思いを持つ、他者の思いを伝えるということに注目しました。思いを持つとは、作るべきものを考え、多数の意見やアイデアから覚悟を持って選ぶことでした。その上で、著者なりのモノづくりとは、考え、覚悟を持って選び、表現するだと述べました。

この記事ではさらにその先、「思いを表現してモノを作るということ」について書こうと思います。「このアプリケーションはこういうものであれ、こういうことを理想とせよ。」という思いが固まった後、それをアプリケーションとして形にする際に著者が大切にしていることを整理します。

表現の制約を見つける

作ろうとするアプリケーションのあるべき姿を思いとして持ったとき、まず最初にやることは、思いをブレイクダウンして表現の制約に言い換えることだと考えています。

例を出しましょう。自分のお気に入りのテキストエディタを開発することを考えます。そのエディタは「文章を素早く編集できる」ことが理想であるとします。このとき「文章を素早く編集できる」ことはどのように言い換えられるでしょうか?例えば、以下のようなものが思いつきます。

  • 文章を素早く編集できる
    • キーボードから手を離さずに装飾を施せる
      • シンプルな記法を書くだけで装飾を施せる
      • キーボードショートカットで装飾を施せる
    • 文字を全部打たなくても書きたいことを書ける
      • 単語、短文の自動補完が出てくる
      • すでに書いた文章のコピペが推薦される
      • リンクを貼るだけでリンク先の文章が出てくる
    • 文章の推敲が支援される
      • 表記揺れの置き換えを一気にできる
      • 段落の入れ替えが簡単にできる

この段階では、すぐにプログラムにできそうなものも、どうやって作ればいいかがまだはっきりしないものもあると思います。でも、それで良いのです。重要なことは、アプリケーションに込めたい思いを分解して、具体的なイメージを描く上での制約として並べることです。アプリケーションの画面の見た目や持っている機能を考えるときに「この制約を満たしていないから、考え直さないとな」という試行錯誤を確実に行うことができます。

見たことがあるものをよく観察する

表現の制約をある程度列挙できたら、具体的なアプリケーションのイメージを描きます。画面の見た目を作ってみたり、持っている機能を整理したり、「なんかこれを作っていけば動きそう」なものがイメージできればゴールです。

ですが、全く何もないところから、具体的な画面、機能のイメージを描くことはとても難しいことです。そこで、すでにあるものをよく観察してみます。テキストエディタであれば、Word、Hack MD、VSCode、Notion、Scrapboxなんでも良いでしょう。どこかで見たことや使ったことがあるものは、何らか優れているところがあるから、目に触れるほど広まっているはずです。一方、自分で新しく作りたい物があるということは、見たことや使ったことがあるもののどれもが解決できていない課題やイケてないところがあるはずです。

この優れているところとイケてないところをしっかりと言語化して整理します。これをやることで、既存のものをベースにして具体的なアプリケーションのイメージを描きやすくなります。また、優れているところは取り入れるように、イケてないところはそうならないようにという形で、表現の制約にさらに磨きをかけることもできます。

ここで取り上げる既存のものは、「一生懸命調査をして探し出す」必要はあまりないと思っています。もちろん、最終的にアプリケーションが出来上がった後に、同じようなものがないかの調査を行うのは良いことです。ですが、ここでやりたいことは、具体的な画面、機能のイメージを描くための助けになるものを見つけることですから、今までに見たことがある、使ったことがあるというのが重要だと思っています。

「真新しい」と「よくある」を線引きする

具体的なアプリケーションのイメージが湧いてきたら、いよいよ作っていきます。この段階で大切にしていることは、「真新しい」と「よくある」を区分けすることです。

自分が時間をかけて新しく作るアプリケーションとなると「今までにないものを作りたい!」という気持ちが強くなるのは当然のことです。でも、このとき、どの部分を自分のアプリケーションにしかない「真新しい」特徴にして、どの部分を「よくある」ものに留めるのかの線引きをしっかりと行うことが重要だと思っています。

「真新しい」機能や操作、見た目ばかりでは、アプリケーションを使う人はその世界観についていくことができず、いいものとは感じてくれないでしょう。逆に「よくある」機能や操作が中心で「真新しい」部分が奥に隠れてしまっては、使う人のほとんどはその真新しい部分に気づくことはできず、「もうこれ〇〇っていうアプリとして世の中にあるよね」と言われてしまうでしょう。このアプリケーションだけの特徴として押し出す機能は一部のユーザがついてこられないリスクを覚悟で作り込み、そうでない部分は「よくある」ものに寄せて作ることが重要だと考えています。

先から例に挙げているテキストエディタを考えてみます。「シンプルな記法を書くだけで装飾を施せる」では、記法を突飛なものにすることは重要ではありません。独自の記法を一部取り入れるのは良いと思いますが、Markdown等の記法を真似た方が、Markdownを使ったことがある人がすぐに使えるという利点を得られます。逆に、「段落の入れ替えが簡単にできる」では、こういった機能を持ったエディタは多くないでしょうから、独自の特徴として押し出すのが良いです。機能をわかりやすい位置に配置したり、ドキュメントに特徴として書きます。

このバランスは非常に難しく、著者もまだまだ勉強中ですが、特徴として押し出す機能は全面に出すこと、普通でいいものをわざわざ奇抜に作らないことを強く意識しています。

フィードバックを得て改善する

アプリケーションがある程度動いて見えるようになったら、なるべくフィードバックを得ます。実際に使ってもらえることが一番ではありますが、難しければデモンストレーションを見てもらって意見を言ってもらうだけでも良いです。

自分では気づかなかったアプリケーションの良いところや足りていないことを気づくきっかけになります。作っている側は、根本にある思いの部分は徐々に当たり前になっていき、どうしても細部に目がいってしまうものです。他の人の目を通すことで、思いは揺らぎないものになっているか、思いを分解した表現の制約は適切だったかという根本を見直すことができます。根本に目を向けなければいけないのは、フィードバックの怖いところでもあるのですが、勇気を持ってフィードバックに望み、改善をしていくのも「考え、覚悟を持って選ぶ」のうちだと思っています。

フィードバックの中で自分が意識して作った部分にちゃんと他の人が気づいてくれたりすると、創作の意欲にもつながります。自分の思いを支持してくれる人もいると思いながらモノを作るのはとても楽しいです。

おわりに

この記事では、思いを持つということ、伝えるということ - 気まぐれエンジニア日記 の続編として「思いを表現してモノを作るということ」について書きました。

アプリケーションのあるべき姿を思いとして持ったとき、まずは思いをブレイクダウンして表現の制約を並べることから始めます。アプリケーションの画面や機能に対して、こんな表現でないといけないという制約を整理し、考えやすくします。

次に、すでにあるものを観察して、優れているところ、イケてないところを言語化します。具体的なものをベースにすることで、アプリケーションのイメージをより明確にします。また、優れている部分は取り入れるように、イケてない部分はそうならないように、表現の制約の磨き上げも行います。

具体的なアプリケーションのイメージが湧いてきたら、実際に作ります。作る上で重要なことは、「真新しい」と「よくある」を区分けすることです。特徴として押し出す機能は全面に出すこと、普通でいいものをわざわざ奇抜に作らないことを意識して、独自の特徴を十分に伝える一方で、ユーザを置いていかないように工夫します。

ある程度アプリケーションが動くようになったら、フィードバックを得ます。フィードバックには、アプリケーションの根本にあった思いを見直すきっかけとしての価値だけでなく、思いに共感してくれる他の人の存在に気づくきっかけとしての価値もあります。

以上が「考え、覚悟を持って選ぶ」の後の「表現する」において、著者が大切にしていることです。

思いを持つということ、伝えるということ

このブログを動かすのはいつぶりでしょうか、くまちゃんです。

これは 徒然 Advent Calendar 2022 - Adventar の23日目の記事です。

はじめに

4月から社会人、ソフトウェアエンジニア1年生になりました。研修やOJTも終わって、業務としてアプリケーション開発を本格的にやり始めています。優秀なチームメンバーに囲まれながら、少しでも貢献しようともがく毎日です。

そのかたわらで、学生時代にやっていた研究を趣味として続けています。研究といっても、自分の欲しいWebアプリケーションを作って修士論文にしちゃおうというものでした。趣味となった今では、自分の欲しいものをより良くするために、せっかく作ったものが死んでしまわないように、細々と活動を続けているという感じです。

ここでは、趣味として今も続く研究、そして新しく始まった業務の中で得た、「思いを持つということ、伝えるということ」について書こうと思います。思いを持ってモノを作るってどういうことなんだろう、思いを伝播させるってどういうことなんだろう、と考えて出した答えです。

思いを持つということ

何もないところから

学生時代に作っていたWebアプリケーションは、授業を受けて内容を整理する、理解するためのツールでした。「クソ真面目なテーマだな」と自分でも思いつつ、勉強をするってどういうことだったのかを学生最後に知りたかったというのもあり、時間をかけて取り組むことにしました。

著者自身の持ち込みで始めたことなので、正真正銘、何もないところからのスタートでした。初めのうちは、「こんな風に授業が整理できたらいいんじゃね?」ととりあえず画面の絵を描いてみて、とりあえず思うように作りました。何ヶ月かやると、なんか動いていそうなものができ、何もないところからモノが出来上がっていく過程を経験できました。でも、このとき、「思うように」作ってはいましたが、それは今考えれば「思い」ではありませんでした。

自分は何を作ったのか

自分の欲しいものを作るとはいえ、あくまで研究なので、成果発表会を乗り越える必要がありました。手元にあったのは「とりあえず思うように作ったもの」ですから、どう発表するのかに苦しむことになりました。どういうことを目指しているか、どんなことを重視して作ったのか、できたものはどんな特徴を持っているのか、といったことを自分の中から「掘り起こす」必要がありました。

自分は一体何を作ったと言えるのだろうか。

このことを日々自問自答して、周りの力も借りながら、懸命に「自分が大事にしていること、その結果できたもの」を言語化しました。この中で、思いの種のようなものが、できていったような気がします。

自分は何を作るべきなのか

「自分はこんなことを重視して、こんな特徴を持ったものを作りました!」

やっとのことで発表が成り立つようになった頃、次の関門が顔をのぞかせます。

「こういう風な機能とかコンセプトみたいなことは考えないんですか?」

こういった質問や意見が寄せられるようになりました。質問や意見が全く間違っているということは少なく、どれも一理あるものでした。けれども、相反する意見や作っているものの根本から変わってしまうような意見も、少なくありません。そんなとき、指導教員がこんな言葉をかけてくれました。

聴衆の言っていることが常に正しいとは限らないんだよ。重要なことは、君がどういう立場を取るかを決めて、言い切ることなんだな。

自分は何を作るべきなのかを徹底的に考える。どういう意見やアイデアを取り入れるのか、捨てるのかを覚悟を持って選ぶ。

これが思いを持つということだと、少しずつ自分の中で思えるようになりました。

自己表現としての、モノづくり

このアプリケーションはこういうものであれ、こういうことを理想とせよ。

こうした思いをどっしりと構えることができれば、雑多な意見も怖くありませんでした。「いいですね、そのご意見いただきます」ということもあったし、「そういうことなら別で作るのがいいかなと思います。ここでは見送りますね」ということもありました。
こうしたことを繰り返すうち、「作っているアプリケーションが自分の思いを含んでいる」から、「自分の思いをアプリケーションとして表現する」に変化していきました。

考え、覚悟を持って選び、表現する。

著者なりのモノづくりを見つけることができました。

思いを伝えるということ

思いに心を寄せる

社会人になると、自分の欲しいものを作ることから、社会が必要としているものを作ることに変化しました。けれども、意外なことに、やることはあまり変わっていないように思います。「何を作ったと言えるのだろうか」、「何を作るべきなのだろうか」と問う相手を自分ではなく、他人にすれば良いからです。

自分が何を作ったと言えるのか、何を作るべきなのか、なかなか見つけられなかった経験が役に立ち、他の人の「どうなると便利なのだろう」という悩みにも、心を寄せることができるようになりました。「しんどいですよね」、「例えばこういう風になると便利ですか?」と言って、相手の思いを懸命に引き出しています。自分が欲しかったものができた時の感動を知っているから、他の人が欲しいものも全力で作るのです。

そして、楽しそうにモノを作っていると、案外他の人も思っていることをぶつけてくれます。「私はこう思う、こういうのが欲しい」と意志を示してくれるので、覚悟を持って選ぶことが伝播した感覚になります。

他者代弁としての、モノづくり

他の人が考え、覚悟を持って選べるように心をよせる。他の人が覚悟を持って選んだものを受け止め、表現する。

これが著者にとっての思いを伝えるということでした。自分の思いを他の人に伝えるプレゼンテーションではなく、他の人の思いを引き出してモノを作る他者代弁が、思いを伝播させることだと思ったのです。学生時代に、モノに思いをのせるのではなく、思いをモノを通じて表現しようと思ったからこそ出せた答えだと思っています。

おわりに

「思いを持つということ、伝えるということ」について書きました。

学生時代の経験から、思いを持ったモノづくりを、著者なりに見つけることができました。思いを持つとは、作るべきものを考え、多数の意見やアイデアから覚悟を持って選ぶことでした。モノづくりをする中で、モノに思いをのせるということから、思いをモノを通じて表現することへと変化していきました。

社会人になってからは、他の人の思いに心を寄せ、覚悟を持って選ぶ手助けをしています。そうして引き出せたことをアプリケーションで表現し、他者の思いを代弁できるように心がけています。この活動こそが思いを伝えることだと感じています。

自分で思いを持ちつづけること、他者の思いを伝えられるようになること、これを大切にしながら、これからもモノづくりをしていきます。

条件付き確率場(CRF)概説 CRF編

前回の記事でCRF以前の多クラス線形識別器まで話をしました. 今回はその続きということで, 本題であるCRFについてみていくことにします.
その前回の記事というのがこれです
kumachan-math.hatenablog.jp
これを前提に話を進めるのでまだの方は先にこっちから読むことをお勧めします.
前置きが長くても仕方ないので早速本題に行きましょう.

1. CRFは何をするか

多クラス線形識別においては, 1つの入力 xに対して, 1つの識別結果 yを与えるのでした. 具体的には, ソフトマックス関数によって定義される"条件付き確率"のようなもの
 \displaystyle p(y|x) := \frac{\exp(\textbf{v}^T\textbf{f}(x, y))}{\displaystyle \sum_{y' \in Y} \exp(\textbf{v}^T\textbf{f}(x, y'))}
を定義し, 正解ラベル y^*に対するこの値の積による尤度, あるいはその対数を取った対数尤度
 \displaystyle L(Y|X) := \prod_{x \in X} p(y^*|x) =  \prod_{x \in X} \frac{\exp(\textbf{v}^T\textbf{f}(x, y^*))}{\displaystyle \sum_{y' \in Y} \exp(\textbf{v}^T\textbf{f}(x, y'))}
 \displaystyle \log L(Y|X) = \log\left(\prod_{x \in X} p(y^*|x)\right) =  \sum_{x \in X} \log p(y^*|x) = \sum_{x \in X} \log\left(\frac{\exp(\textbf{v}^T\textbf{f}(x, y^*))}{\displaystyle \sum_{y' \in Y} \exp(\textbf{v}^T\textbf{f}(x, y'))}\right)

の最大化問題として定式化されるのでした. けれども, 例えば自然言語や音声データを対象として何らかの識別問題を解きたいと思った時には, 入力 xの識別結果が xの情報のみによって決まると考えるのは安易です. 自然言語であれば単語単体ではなく文脈を考慮したような識別ができた方がいいですし, 音声でも前後の発話からの抑揚の変化だったり速度の変化だったりを考慮できた方が良いですよね.
CRFは, 何らか順序を持ったデータをそれ単体で扱うのではなく系列として扱うということをします. 入力は今まで xだったものが順序を持った複数の入力の系列 \textbf{x} = x_1x_2, \cdots, x_Mになるし, 出力も \textbf{y} = y_1y_2, \cdots, y_Mになります.
問題は複雑になりましたが, 式の上ではあまり問題はあまり変わっていません. 最大化するべき対数尤度は次のようになります.
 \displaystyle \log L(\textbf{Y}|\textbf{X}) = \log\left(\prod_{\textbf{x} \in \textbf{X}} p(\textbf{y}^*|\textbf{x})\right) =  \sum_{\textbf{x} \in \textbf{X}} \log p(\textbf{y}^*|\textbf{x}) = \sum_{\textbf{x} \in \textbf{X}} \log\left(\frac{\exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}^*))}{\displaystyle \sum_{\textbf{y}' \in \textbf{Y}} \exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}'))}\right)

ここで, 出力に関しては \textbf{Y} = Y^Mですが, 入力は順序関係の束縛があるため真部分集合 \textbf{X} \subset X^Mです.
以上です. CRFってつまりはこれだけです. 数式の上では. 大事なことなのでもう一度言います.
数 式 の 上 で は
これで終わりです. つまり, まだまだ実際に動くシステムには遠いということです. 問題点を挙げましょう.

  •  \textbf{f}(\textbf{x}, \textbf{y}^*)ってさらっと書いてありますが, これはどうやって見つけるのでしょうか. 実験的に頑張るのでもいいかもしれませんが, もう少し理論に裏打ちされたセンスのある見つけ方というものはないのでしょうか
  • 式を複雑にするのは結構ですが, コンピュータに計算させるとなると計算時間の問題は常について回ります. どうにかして高速に計算できないでしょうか.

例えば, 尤度の式の中に出てくる
 \displaystyle Z_\textbf{x} = \sum_{\textbf{y}' \in \textbf{Y}} \exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}'))
とかは計算が大変そうです. 他にもパラメタ更新を勾配法で行うとき, その更新式の計算は一筋縄では行きそうにありません.

これらの点を順に見ていきましょう.

2. マルコフ性の仮定の導入とCRFの識別

今, 推定したいラベル列を \textbf{y} = y_1\cdots y_{m-1}y_my_{m+1}\cdots y_Mとし, これについて各 y_m y_{m-1}, x_mのみに依存して決まるという仮定をおきましょう. この仮定は問題によって変更をするところですが, ひとまず説明のためにこうしておきます. この仮定の下では次の3種類の特徴を得ることができます.

  •  y_m y_{m-1} x_mが全て関係する特徴
  •  y_m x_mのみが関係する特徴
  •  y_m y_{m-1}のみが関係する特徴

 y_{m-1}の値はすでに決まっていますから,  x_m y_{m-1}のみが関係する特徴は考える必要がありません. これらの特徴をいくつか定義することができれば, それらを各成分にもつベクトルを \textbf{x}のうちの位置 mにおける局所的な特徴ベクトルと考えることができます. この局所的な特徴ベクトルを \pi(y_m, y_{m-1}, x_m)と書くことにします.  \textbf{x}全体の特徴ベクトルを得るときには, 最も単純にはそれらを足し合わせれば良いですから.
 \displaystyle \textbf{f}(\textbf{x}, \textbf{y}) := \sum_{m = 1}^M \pi(y_m, y_{m-1}, x_m)
と定義すれば良さそうです.
まずは, 学習のステップは考えないで, 学習が終わった後の識別のステップを考えましょう.  \textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y})の最大化に注目します. 最大化における変数は他でもなく識別結果のラベル列 \textbf{y} = y_1\cdots y_{m-1}y_my_{m+1}\cdots y_Mです.
 \displaystyle \textbf{v}^T \textbf{f}(\textbf{x}, \textbf{y}) = \textbf{v}^T \sum_{m = 1}^M \pi(y_m, y_{m-1}, x_m) = \sum_{m = 1}^M \textbf{v}^T \pi(y_m, y_{m-1}, x_m)
ですから,
 \displaystyle r_\textbf{x}(m, y_m, y_{m-1}) := \textbf{v}^T \pi(y_m, y_{m-1}, x_m)
と定義すれば,
 \displaystyle \textbf{v}^T \textbf{f}(\textbf{x}, \textbf{y}) =  \sum_{m = 1}^M r_\textbf{x}(m, y_m, y_{m-1})
とかけます. 改めて問題を明示的に式で書けば,
 \displaystyle \textbf{y}^* = \arg\max_{\textbf{y} \in \textbf{Y}} \sum_{m = 1}^M r_\textbf{x}(m, y_m, y_{m-1})
です.

これ, どうやってときましょうか.  \textbf{y} \in \textbf{Y} = Y^Mは組合せ爆発の問題を抱えていて, 時間軸上順番を持った系列を考えており, 各 y_m Yの元それぞれを選択肢として持っていて,  y_1\cdots y_{m-1}でどんなラベル列を選んできたかが y_mの選び方を束縛しないわけです. こうきたら, 典型的な動的計画法...ですよね. DPと分かれば漸化式を書いてみましょう. 時間的な流れ(今まで変数を mと書きました)を一方に, それぞれの選択肢たるラベル Yの元(今まで変数を yと書きました)を他方の次元に持つようなDP行列 R(m, y)を考えれば良さそうです.
 \displaystyle R(1, y) =  r_\textbf{x}(1, y, \mathrm{BOS}), ~~~ R(m, y) = \max_{y' \in Y} \{R(m-1, y') +r_\textbf{x}(0, y, y')\}
ここで,  \mathrm{BOS}はBeginning Of Sequenceのことで, 系列の開始を表すダミーのラベルです. あとはこのDP行列を計算して, 最大成分のところからバックトラックすれば, 識別結果のラベル列 \textbf{y} = y_1\cdots y_Mが得られます. バックトラック...説明した方が良ければコメントでください.

3. 分配関数の計算

学習のステップにおいて最大化したい尤度の中に
 \displaystyle Z_\textbf{x} = \sum_{\textbf{y}' \in \textbf{Y}} \exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}'))
という式が出てきました. この関数のことは分配関数と呼んでいます. これを愚直に計算しようと思うと |Y|^M回もの内積計算が必要になります. つまりラベルの種類の数に関して指数オーダーです. 誰かさんの言葉を借りれば, 恥ずかしいほど遅いを通り越して論外です. いいですか, 論 of the 外です. もう少し効率の良い計算方法はないものだろうか, そう考えるのは自然なことです.
せっかく上で r_\textbf{x}(m, y_m, y_{m-1})を定義したので, それを用いて分配関数の定義を書き直してみましょう.
 \displaystyle Z_\textbf{x} = \sum_{\textbf{y}' \in \textbf{Y}} \exp(\sum_{m = 1}^M r_\textbf{x}(m, y'_m, y'_{m-1})) = \sum_{\textbf{y}' \in \textbf{Y}} \prod_{m = 1}^M \exp(r_\textbf{x}(m, y'_m, y'_{m-1}))
式の見た目が煩雑になるのが嫌なので,
  \displaystyle \hat{r}_\textbf{x}(m, y'_m, y'_{m-1}) :=  \exp(r_\textbf{x}(m, y'_m, y'_{m-1}))
と置きます. この(因数分解と展開の意味で)展開された式を因数分解してみようと思うと
 \displaystyle Z_\textbf{x} = \sum_{\textbf{y'} \in Y^M}^K \prod_{m = 1}^M \hat{r}_\textbf{x}(m, y'_m, y'_{m-1}) = \sum_{y'_1 \in Y} \hat{r}_\textbf{x}(1, y'_1, \mathrm{BOS}) \sum_{y'_2 \in Y} \hat{r}_\textbf{x}(2, y'_2, y'_1) \sum_{y'_3 \in Y} \cdots \sum_{y'_M \in Y} \hat{r}_\textbf{x}(M, y'_M, y'_{M-1})
となります. 少々煩雑ですが, ただの因数分解なので, ゆっくり式を追えば必ず理解できます(頑張って!). ここまでくれば, この計算を再帰的に行うことができると言うことに気がつくと思います.
 \displaystyle \zeta_\textbf{x}(m, y'_m) = \sum_{y'_1 \in Y} \hat{r}_\textbf{x}(1, y'_1, \mathrm{EOS}) \sum_{y'_2 \in Y} \cdots \sum_{y'_{m-1} \in Y} \hat{r}_\textbf{x}(m-1, y'_{m-1}, y'_{m-2}) \hat{r}_\textbf{x}(m, y'_m, y'_{m-1})
とおくと, これらは漸化式
 \displaystyle \zeta_\textbf{x}(1, y'_1) = \hat{r}_\textbf{x}(1, y'_1, \mathrm{EOS}), ~~ \zeta_\textbf{x}(m, y'_m) = \sum_{y'_{m-1}\in Y} \zeta_\textbf{x}(m-1, y'_{m-1})\hat{r}_\textbf{x}(m, y'_m, y'_{m-1})
を満たします. あとはこの漸化式に従って行列を埋めていけば,
 \displaystyle Z_\textbf{x} = \sum_{y'_M \in Y} \zeta_\textbf{x}(M, y'_M)
として分配関数を求めることができました. 組み合わせ爆発で困ったら, とりあえずDPを検討すればいいみたいですね.

次の章のために, もう一種類の漸化式を考えておきましょう.
 \displaystyle \eta_\textbf{x}(m, y'_{m-1}) :=\sum_{y'_m\in Y} \hat{r}_\textbf{x}(m, y'_m, y'_{m-1})\sum_{y'_{m+1}\in Y} \cdots  \sum_{y'_M\in Y} \hat{r}_\textbf{x}(M, y'_M, y'_{M-1})
とおくと, これらは漸化式
 \displaystyle \eta_\textbf{x}(M, y'_{M-1}) = \sum_{y'_M\in Y} \hat{r}_\textbf{x}(M, y'_M, y'_{M-1}), ~~ \eta_\textbf{x}(m, y'_{m-1}) = \sum_{y'_m\in Y} \hat{r}_\textbf{x}(m, y'_m, y'_{m-1})\eta_\textbf{x}(m+1, y'_m)
を満たして,
 \displaystyle  Z_\textbf{x} =  \eta_\textbf{x}(1, \mathrm{EOS})
となります.

4. CRFのオンライン学習

最尤推定によって学習を進めるにあたって, 謎の変数がまだ1つ残っています. というより, この謎の変数が学習によって調整をするものな訳ですけれどね. そう,今まで定ベクトルとか言ってだましだまし考えていた \textbf{v}です. この \textbf{v}をうまく決めましょう, ただし評価関数は対数尤度
 \displaystyle \log L(\textbf{Y}|\textbf{X}) = \sum_{\textbf{x} \in \textbf{X}}\log p(\textbf{y}^*|\textbf{x}) = \sum_{\textbf{x} \in \textbf{X}} \log\left(\frac{\exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}^*))}{Z_\textbf{x}(\textbf{v})}\right)
にしましょうというのがCRFの学習そのものです. オンライン学習で勾配法によって \textbf{v}を更新するときの更新式は \alpha_tを学習率として,
 \textbf{v} \leftarrow \textbf{v} - \alpha_t (\nabla l)( \textbf{v})
とかけます. ここに,  \textbf{x}, \textbf{y}^*を新しく追加したデータとして
 \displaystyle l(\textbf{v} | \textbf{x}, \textbf{y}^*) := \log p(\textbf{y}^*|\textbf{x}) = \log\left(\frac{\exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}^*))}{Z_\textbf{x}(\textbf{v})}\right) = \textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}^*) - \log Z_\textbf{x}(\textbf{v})
です. このとき,
 \displaystyle (\nabla l)( \textbf{v}) = \textbf{f}(\textbf{x}, \textbf{y}^*) - \frac{1}{Z_\textbf{x}(\textbf{v})}(\nabla Z_\textbf{x})( \textbf{v})
となりますが,
 \displaystyle (\nabla Z_\textbf{x})( \textbf{v}) = \sum_{\textbf{y}' \in \textbf{Y}}  \exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}'))\textbf{f}(\textbf{x}, \textbf{y}')
ですから,
 \displaystyle (\nabla l)( \textbf{v}) = \textbf{f}(\textbf{x}, \textbf{y}^*) - \frac{1}{Z_\textbf{x}(\textbf{v})}\sum_{\textbf{y}' \in \textbf{Y}}  \exp(\textbf{v}^T\textbf{f}(\textbf{x}, \textbf{y}'))\textbf{f}(\textbf{x}, \textbf{y}') =  \textbf{f}(\textbf{x}, \textbf{y}^*) - \sum_{\textbf{y}' \in \textbf{Y}}  p(\textbf{y}'|\textbf{x}) \textbf{f}(\textbf{x}, \textbf{y}')
と綺麗な形に整理されました.

今から, 第2項を \zeta_\textbf{x}(m, y'_m) \eta_\textbf{x}(m, y'_{m-1})を使って表現することを考えましょう. もしこれがうまくいけば, 分配関数の計算過程で出てきた副産物を使いまわすことができるので計算時間はグッと短縮されます. 第2項の総和では Y^Mの元全てについて考えるのですから, 局所的にある連続した2つのラベルを見たときには,  Y^2の全てについて考えていると言えます. 入力として \textbf{x}が与えられたとき位置 mにおいて, 直前のラベルが y'であって位置 mのラベルが yとなるソフトマックス関数の意味での確率を p(y, y'|\textbf{x}, m)と書くことにしましょう. すると, 上の第二項は \pi(y_m, y_{m-1}, x_m)を用いて,
 \displaystyle \sum_{\textbf{y}' \in \textbf{Y}}  p(\textbf{y}'|\textbf{x}) \textbf{f}(\textbf{x}, \textbf{y}') = \sum_{(y, y') \in Y^2} p(y, y'|\textbf{x}, m) \pi(y, y', x_m)
とかけます.  p(y, y'|\textbf{x}, m)は, 分散関数の値を分母に持って, その計算過程において, (m-1, y')と(m, y)を通る値の総和を分子に持つ比によって書けるので, ある位置 mより先の値の総和を計算している  \eta_\textbf{x}(m+1, y_{m-1})とその直前の位置 m-1までの値の総和を計算している \zeta_\textbf{x}(m-1, y')用いれば,
 \displaystyle p(y, y'|\textbf{x}, m) = \frac{\eta_\textbf{x}(m+1, y)\hat{r}_\textbf{x}(m, y, y')\zeta_\textbf{x}(m-1, y')}{Z_\textbf{x}}
となります. 晴れて目標達成です.

5. まとめ

CRF概説はここまでとなります. 色々数式をいじりましたが, お気持ちをまとめると次の二点です.

  • CRFは, 何らか順序を持ったデータをそれ単体で扱うのではなく系列として扱い, 入力系列 \textbf{x}から識別ラベルの系列 \textbf{y}を推定するための手法です.
  • ソフトマックス関数によって確率のようなものを定義して, それによる尤度の最大化問題として学習ステップを行い, 内積によるスコアの最大化問題として識別ステップをおこないます.

はー長かったですね. お疲れした〜!