TECH PLAY

株式会社Insight Edge

株式会社Insight Edge の技術ブログ

174

開発チームの塚越です。Insight Edgeに加わって1年が経ちました。開発業務に携わらせていただき、日々楽しく過ごしています。 目次 1. 概要 2. PMへの挑戦を決めた経緯 3. 押さえたい初歩的PMポイント 3.1. 顧客とプロジェクトの方向性を一致させる 3.2. 成功する見込みがあるプロジェクトか確認する 3.3. 内部のコミュニケーション量を増やす 4. おわりに 1. 概要 私はこれまで開発ロールに専念していましたが、最近PMへ挑戦する機会に恵まれました。この記事では、挑戦するに至った経緯と、PMとして実践しようと考えている重要なポイントを共有します。 「PMに挑戦した経緯」の章では、なぜ私がPMロール担当しようと思ったのか、その背景と理由を説明します。 「押さえたい初歩的PMポイント」の章では、PMとして案件を遂行するために重要だと考える基本的な要点をまとめました。これらは、私自身が学び、実践しようとしているポイントです。 なお、この記事の内容は私個人の経験と見解に基づいています。組織や状況によって最適なアプローチは異なる可能性がありますので、ご了承ください。 2. PMへの挑戦を決めた経緯 自身のキャリアを考える中で、開発スキルに加えて別の強みも持ちたいと考えました。特に、PMの役割も担える開発者というキャリアパスに興味を持ちました。開発者としてのスキルだけでなく、プロジェクト全体を成功に導くための視点や力を養いたいと考えています。自分がリードする立場になり、プロジェクトに関わる人たちの力を引き出しながら成功を目指すという役割に魅力を感じました。 Insight Edgeはメンバーの成長を重視し、組織全体の能力を引き上げる文化が根付いていると感じています。この文化の中で、私も上司や同僚に相談を重ねる中で、PMを兼任する形での挑戦が実現しました。 PMを専任する選択肢もありましたが、PMと開発の齟齬を少なくするために、兼任可能な案件規模でのPM兼任開発者を希望しました。本記事では、この体制下でPM兼任開発者として成功するために重要だと考えるポイントをまとめています。 ここで紹介するポイントは主にPMとしての観点に焦点を当てており、開発者の視点を活かしたプロジェクト運営の利点も反映されていますが、強調するものではありません。重要なのは、前述の経緯を念頭に置きつつ、PMとしての役割を効果的に遂行することです。より効果的で満足度の高いプロジェクト遂行の実現に向けた取り組みの一部として、この経験を共有させていただきます。 3. 押さえたい初歩的PMポイント 3.1. 顧客とプロジェクトの方向性を一致させる プロジェクトの成功には、顧客との方向性の一致が不可欠です。方向性がズレると顧客の期待値を超えることは困難になります。この問題を防ぐために、以下の2つの重要なポイントに注目しました。 3.1.1. 顧客はITのプロではないことを認識する 最初に意識したいのは、顧客がITの専門家ではないということです。これは当たり前のように思えることですが、実務の中ではつい忘れてしまいがちです。私自身、技術的な背景を持つ立場から話してしまい、結果的に顧客との間で認識のズレを生むことがありました。この点を克服するため、私は以下の取り組みを行おうと考えています。 重要性と潜在的な問題: コミュニケーションギャップにより、認識のズレが生じやすい ズレに気づくのが遅れると、修復困難や手戻り、顧客の不満につながる 適切に対応すれば、プロジェクトの円滑な進行と顧客満足度の向上が期待できる 対策と実践のコツ: 会議や報告の前に、この点を自己確認する 専門用語を避け、わかりやすい言葉で説明する 技術的な説明には具体的な例や比喩を用いる 顧客からの質問を歓迎し、理解度を把握する機会とする 3.1.2. プロジェクトの目的を常に問い続ける プロジェクトの進行中に目的がぼやけてしまうことはよくある問題だと思います。その結果、顧客の期待に応えられない成果物ができてしまうリスクを防ぐため、私は目的意識を常に持つことを意識していきたいと考えています。 リスクと影響: 他の製品との差別化が曖昧になり、競争力が低下する 実際には使われない、または活用されない製品が完成する 表面的には要件を満たしているが、顧客満足度や費用対効果が低い プロジェクトの成果が組織の戦略やビジョンと乖離する 対策と実践方法: 定期的に「何のためにこのプロジェクトを始めたのか」を問い直す 方向性に違和感を感じたら、顧客自身に目的を語ってもらう チーム内でも同様のアプローチを取り、ときには他メンバーに顧客の前で語ってもらう 3.2. 成功する見込みがあるプロジェクトか確認する プロジェクトの成功とは、私の見解では以下のいずれかを達成することだと考えています: 顧客の期待値を超えること 費用対効果が十分に得られること プロジェクトにアサインされる段階で、以下の要素を慎重に評価することが重要です: 顧客側の期待度(案件の要件スコープ) 予定している工数 プロジェクト期間 アサインされるチームメンバー これらの要素を総合的に検討し、与えられた条件で顧客の期待を超えることができるか、または十分な費用対効果が得られるかを判断したいと思います。 もし、現状の条件でプロジェクトの成功が難しいと判断した場合、以下のアクションを取ることを検討します: 早期の情報収集: 契約提案段階から、営業部門と顧客とのやりとりを注意深く確認する 必要に応じて、追加情報の収集や不明点の明確化を要求する 上司との相談: プロジェクトの成功に懸念がある場合、早期に上司へ相談し、対策を検討する 工数、期間、またはチーム編成の見直しの必要性を議論する 契約締結前の調整: 情報収集と上司との相談結果に基づき、以下の調整を提案する: 顧客の期待値を適切なレベルに調整する 契約内容(スコープ、期間、予算など)の再交渉する プロジェクト体制の見直しを行う このように、プロジェクトの初期段階で成功の可能性を見極めることもPMとして重要な役割だと感じています。各要素を慎重に確認し、条件が揃っていない場合は早めに調整をすることでプロジェクトの成功率を高めたいと考えています。 3.3. 内部のコミュニケーション量を増やす 3.3.1. 阿吽の呼吸を生み出す プロジェクトは計画通りに進まないことが多々あると思っています。台本のなくなった講演会を想像してみました。そのような状況では、メンバー間の阿吽の呼吸で講演を進める必要があります。コミュニケーションが活発なチームほど、台本なしでも「講演」を続けられる可能性が高くなると思いました。 実践方法: 定期的なチームミーティングを設ける 内面を深く知る対話の機会を創出する(例:ランチミーティング) プロジェクトの進捗や課題を共有する場を頻繁に設ける これらの方法を通じて、チームメンバー間の理解を深め、予期せぬ状況にも柔軟に対応できる「阿吽の呼吸」を育みたいと思います。 3.3.2. 火種に気づいて炎上を防ぐ プロジェクト内の小さな問題(火種)は、放置すると必ず大きな問題(炎上)に発展すると思っています。コミュニケーションが活発なほど、火種に気付ける可能性が高くなるため、まずは単純に情報流通量を増やすことが大切だと思いました。 実践方法: メンバー全員が課題や違和感を容易に口に出せる場所を作る(例:Slackの雑談チャンネルを設置する) 言いやすい環境づくりをする(例:心理的安全性に関する書籍を読み、実践する) 4. おわりに この記事によって、私自身がPM兼任の役割を担うにあたっての心構えができました。完全に自分のための記事になってしまいましたが、同じような状況に直面している方々にとって、何かしらのヒントになれば幸いです。 謝辞 この記事を作成するにあたり、以下の方々からの貴重なインプットがありました: チームリーダーの方々 経験豊富なPMロールの方々 皆さんの洞察と助言が、この記事の内容をより良くしてくれました。 参考文献 また、オフィスで見つけた以下の本からも多くの学びを得ました。これらの知見は記事の随所に反映されています。 新装版 外資系コンサルが教えるプロジェクトマネジメント(山口周) PMとしての役割を理解し、効果的に遂行するポイントを知る上で非常に有益でした。興味のある方は、ぜひ参照してみてください。
アバター
はじめに こんにちは!昨年9月からInsight Edgeの開発チームに参画した広松です。Insight Edgeでは生成AI案件を担当しています。 2024年11月現在でも生成AIの進化は止まらず、日に日に技術的な課題を克服しています。各社のLLMが頻繁に大型アップデートされるので付いていくだけでも大変な日々を送っています。 さて、前回は 生成AIエージェントを使ったデータ分析の記事 を書いたので、今回は生成AIエージェントを使ったシステム開発について書こうと思います。ただ、この技術は非常に進化が早くすぐに記事が古くなり役に立たなくなってしまうので、2024年11月現在で実現できていることと、長期的な将来像の考察について論文紹介も含めて書いていきます。 昨今では、生成AIがコーディングの補助をしてくれることが常識になりつつありますが、システム開発の様々な工程をAIエージェントがどの程度自動でやってくれるのでしょうか?その点を確認しつつ、解決されるべき課題についても記載していきます。 目次 はじめに 目次 概要 現在(※2024年11月)までの生成AIでのシステム開発補助の経緯 チャット形式でコード生成をさせる時代 AIエージェントでコーディング計画を立てたりエラーの自己解決を試みる時代 現在のAIエージェントによるシステム開発が抱える課題 課題1. 論理的な思考が難しい 課題2. 推論や対話を繰り返すと指示を忘却してしまう 課題3. ハルシネーションが連鎖する 課題4. 人間と協業しづらい 課題5. 様々な専門家ロールの視点によるチェックがない AIエージェントを使ったシステム開発の将来 複数ロール(例:PM、アーキテクト、エンジニア、品質管理者)のマルチエージェントでシステム開発をする時代 MetaGPTとは 最後に 参考文献 概要 2024年11月現在までの生成AI及び生成AIエージェントによるシステム開発の補助について説明し、長期的な将来、生成AIエージェントがシステム開発にどう関わるのかについての予想を論文をベースに説明します。 現在(※2024年11月)までの生成AIでのシステム開発補助の経緯 チャット形式でコード生成をさせる時代 2023年までのシステム開発での主要な生成AIの活用方法は、チャットベースで生成AIに質問を投げるだけでした。 ChatGPTやGitHub Copilotに実装したい内容を伝えてコードを生成させる使い方がほとんどでした。コーディング補助以外ではドキュメント作成に使われることが大半でした。要求分析や要件定義、設計などでドキュメント作成に活用していた方もいると思いますが、方法が確立されておらずコーディング補助のように劇的な効果は得られていなかったと思います。 ただ、生成AIはそもそも論理的な思考ができないので、複雑な内容や中規模以上のコードを一気に生成させることは現実的ではありませんでした。 複雑な内容やある程度の規模のコードをまとめて生成させた事がある方は、それっぽいでたらめなコードが生成されるのを見てきたと思います。 また、生成AIはコードを生成するだけで、出力されたコードを実行して結果を確認する作業は人間の開発者が行なっていました。 AIエージェントでコーディング計画を立てたりエラーの自己解決を試みる時代 上述したような課題から、AIエージェントを使ったサービスが出てきました。 推論を繰り返すことで論理的な思考を誘導したり、コードの実行環境を操作できるようにしてエラーを自己解決させるアプローチが取られ始めました。 具体的なサービスだと、GitHubがissueからAIエージェントにコーディング計画を立てさせてコーディングをさせる GitHub Copilot Workspace が発表された他、フロントエンド開発をAIエージェントで支援する bolt.new などがリリースされました。 現在のAIエージェントによるシステム開発が抱える課題 課題1. 論理的な思考が難しい 生成AIは確率的に出力を生成しているので、論理的な思考ができません。推論を繰り返すことで論理的な思考のような出力を誘導させる事が限界です。2024年前半までは CoT(段階的に思考させて論理的な出力を誘導する手法) や ReAct(推論・実行・結果の観察をループさせることで論理的な出力を誘導する手法) が有力な手法でしたが、精度としてはそれほど高くありませんでした。 しかし、現在はこの論理的な思考のような出力を誘導させる技術が飛躍的に進歩しており、OpenAIが最近発表した o1-preview などは理系博士課程の学生に匹敵もしくは超えるほど高難度の問題が解けるようになっています。 推論を繰り返す方法は非常に威力があるので今後、この方法が進化していけば大抵のソフトウェアエンジニアよりも"論理的"な思考が出力されるようになるかもしれません。 課題2. 推論や対話を繰り返すと指示を忘却してしまう 課題1の論理的思考は今後の技術的な進歩である程度問題ないように思えます。しかし、推論を繰り返すことで別の問題が生じます。 それは推論を繰り返すうちに内容が要約されて、詳細な内容や初期の指示が忘れられてしまうことです。そのため詳細な長文の仕様などを伝えても正確な指示が残らない事があります。 課題3. ハルシネーションが連鎖する エージェントは内部でかなりの推論や対話を繰り返すので、一度のハルシネーションが連鎖して大きな混乱を発生させ、最終結果に大きな影響を与えてしまいます。 課題4. 人間と協業しづらい エージェントが自律的にどんどん開発を進めてしまうため、人間が介入しづらいです。 エージェントから必要な情報を人間に問うようなサービスもありますが、エージェントが勝手に判断してあらぬ方向に開発をどんどん進めてしまうことも多々あります。 課題5. 様々な専門家ロールの視点によるチェックがない エージェントは設計からテストまで自動で行ってくれますが、様々な専門家ロールの視点でチェックしてくれるわけではありません。 PM、アーキテクト、エンジニア、品質管理者など様々な視点で成果物をチェックしてくれるわけではありません。 AIエージェントを使ったシステム開発の将来 複数ロール(例:PM、アーキテクト、エンジニア、品質管理者)のマルチエージェントでシステム開発をする時代 先ほどあげた課題の中で、課題1, 課題2はLLMそのものの問題です。課題3, 4, 5については仕組み自体を工夫することで解決できると思います。 今回はシステム開発としては必須となるであろう課題5についての解決策とその未来について考察しようと思います。 実際のシステム開発ではPM、アーキテクト、エンジニア、品質管理者など様々なロールの専門家が協力してシステム開発を行います。 この実際のシステム開発のやり方を踏襲したアプローチで代表的なものが2つあります。 MetaGPT や ChatDev と呼ばれる手法です。 今回はより実際のシステム開発会社での開発に近いMetaGPTについて紹介します。 MetaGPTとは MetaGPT とは以下の図のように各専門家のエージェントをマルチエージェントで動かし、協業してシステム開発を行う手法です。 MetaGPTの概要の図 この手法で特徴的なのは、エージェント同士の会話でハルシネーションを連鎖させないために、SOP(標準化された作業手順書)を使用していることです。これはシステム開発会社などで用いられている実際の手順書を用いています。例えばウォータフォールであれば、要件定義、設計、実装、テストなどの一連の作業の流れを標準化したものです。 これにより標準化されたやり方でタスクを細分化でき、ハルシネーションの連鎖を発生させにくくできます。 また、このような人間によるシステム開発の標準に沿っているため各ロールのエージェントの成果物も人間が確認しやすいものとなっています。 例えば、アーキテクトのエージェントが出力するものは以下で、人間のアーキテクトやエンジニアでも簡単にレビューできるものとなっています。 アーキテクトのエージェントが出力したクラス図 アーキテクトのエージェントが出力したシーケンス図 上記だけでなくMetaGPTの興味深いところは、冗長な会話をする事なく知識の共有ができることです。 各エージェントが様々な情報を共有(Publish)しておき、他のエージェントは共有された情報を検索して必要な情報を取得(Subscribe)します。このPub/Sub形式での知識の共有を行うことで効率的で効果的な開発が可能になっているようです。 ロールごとに成果物の形式を標準化したり、知識のPub/Sub形式での共有は、人間による開発とかなり似ていますね。 最後に 生成AIの進化は凄まじいですが、システム開発の現場においてはコーディング補助以上に適用する方法はまだまだ確立されていないのが現状です。 しかし、要求分析から要件定義・設計・実装・テスト・品質チェックまでをマルチエージェントで自動で行う手法なども登場しています。各専門家ロールのエージェントが人間と同じ形式の成果物を出力して作業を進めていくので人間にもトレースでき、協業することも難しくないでしょう。 今後は同じロールの人を大勢集めたチームで働くのではなく、各ロールの専門家を集めた比較的小規模のチームでの開発が主流になっていくのではないでしょうか?そのためには今以上に他のロールの視点を理解しながら仕事をしていく必要がありそうですね。 参考文献 Jason Wei, Xuezhi Wang, Dale Schuurmans, et al., "Chain of Thought Prompting Elicits Reasoning in Large Language Models," arXiv, 2022. DOI: https://doi.org/10.48550/arXiv.2201.11903 Shunyu Yao, Jeffrey Zhao, Dian Yu, et al., "ReAct: Synergizing Reasoning and Acting in Language Models," arXiv, 2022. DOI: https://doi.org/10.48550/arXiv.2210.03629 Sirui Hong, Mingchen Zhuge, Jiaqi Chen et al., "MetaGPT: Meta Programming for a Multi-Agent Collaborative Framework," arXiv, 2023. DOI: https://doi.org/10.48550/arXiv.2308.00352 Chen Qian, Wei Liu, Hongzhang Liu, et al., "ChatDev: Communicative Agents for Software Development," arXiv, 2023. DOI: https://doi.org/10.48550/arXiv.2307.07924
アバター
はじめに 開発経緯 作成したアプリの構成および議事録作成手順 構成 議事録作成手順 半年間運用して得られた学び 議事録自動生成の精度がどうだったか? 使用したモデルおよびコストについて モデル コスト 安全フィルターについて プロンプトおよび生成過程について VertexAI Geminiの凄さについて 議事録を投稿できるまでの手順の制限について まとめと今後の期待 はじめに こんにちは、Insight Edgeで開発エンジニアをしているニャットです。 入社から1年以上が経ちましたが、まだまだ学ぶことが多く、日々、社内の勉強会やリソースをフル活用して新しい技術の習得に挑戦し続けています。 今回は、勉強会の時間を活用しつつ「社内用の議事録自動作成アプリ」を少しずつ開発し、半年間社内で運用しながら改善を重ねたので、その開発経緯と得られた学びを紹介します。 開発経緯 2024年5月頃、Google社が発表した「Gemini 1.5」モデル(Pro/Flash)は動画文字起こしの精度と速度が大幅に改善されたことから、SNSや様々なプラットフォームで話題となり、当社メンバーの間でもこのモデルの可能性に対する関心が非常に高まりました。 また、当社では多数のプロジェクトや勉強会、R&Dが同時進行しており、情報を社内全体に共有することが文化として根付いていますが、オンライン会議はGoogle Meetを使用しており、現時点ではMicrosoft Teamsのような標準機能としての日本語対応の文字起こし機能(※英語は対応可能)や自動議事録生成機能がないため、情報を共有したい場合は手動で議事録を作成して共有するしかない状況です。たくさんある会議から会議ごとに議事録を作成して展開する作業はメンバーにとって大きな負担となっており、何かいい方法がないかと常にメンバー間で模索していました。 こうした状況の中、ある日に当社のリードエンジニアTさんが開発チームのSlackチャンネル上で「会議後に自動で議事録を作成し、Slackに投稿できるアプリがあれば効率的なのでは?」と呟き、このアイデアに挑戦するメンバーを募り始めました。 私自身、当時は生成AIを活用するプロジェクトに関わる機会が少なかったため、Geminiを用いたアプリを一から構築してみたいと思いはじめ、 かつ、「やってみる」という弊社のValueの1つを実現できることをアピールしたく、Tさんの募集に挙手したのが開発のきっかけになります。笑 作成したアプリの構成および議事録作成手順 作成したアプリの構成と動作について紹介します。 構成 作成したAI議事録生成アプリ構成 このアプリでは、フロントエンドにNext.js(App Router)、バックエンドにFastAPIをフレームワークとして使用しております。 FastAPIのバックエンドアプリ内では、Vertex AI SDK for Pythonを用いて議事録生成処理を行い、Slack SDKを使って、生成された議事録をSlackに自動投稿できるようにしています。議事録生成はGemini APIを直接利用することも可能ですが、社内の会議データを扱うため、データが学習に使われないVertex AI経由で利用しました。 議事録生成に使用するファイルはGoogle Cloud Storageにアップロードし、VertexAIから参照しています。この理由は、Vertex AIのプロンプトに直接7MB以上のファイルを挿入できず、一旦Cloud StorageにアップロードしてからプロンプトからCloud Storageからインポートする方法しか使えないからです。 なお、フロントエンドのフレームワークとして、Next.jsのApp Routerを選んだ特別な理由はありませんでしたが、ちょうどその時期に私が主催するNext.js(App Router)の勉強会を開催していたため、実践も兼ねてNext.jsを使って開発することにしました。勉強会の参加メンバーに「現在検証中の議事録自動生成アプリでApp Routerを一緒に試してみませんか?」と提案したところ、協力を得られたため、勉強会のメンバーで開発を進めました。 議事録作成手順 会議の録画 社内のあらゆる会議で、会議の主催者に会議開始時から録画してもらうように依頼しました。 会議終了後には録画データおよびチャット履歴ファイルは会議の主催者の個人ドライブに届くため、私がダウンロードできるように主催者にデータを共通ドライブに移動してもらっています。 なお、私がカレンダーの主催者である場合は、共通ドライブに移動しなくてもダウンロード可能ですが、データの一元管理を目的として、すべてのデータを共通ドライブに移動する運用としました。 ファイルのアップロードと議事録の投稿 フロントエンドアプリを使って議事録の作成と投稿を行います。操作は至って単純ですが、主に以下の流れで行います。 アプリ画面・議事録投稿手順 ① 会議のファイル選択、会議情報入力 録画動画、チャット履歴ファイル、会議中にメンバーが共有したメモファイルなどを選択します。これらのファイルは後、Google Cloud Storageにアップロードされ、議事録生成に使用されます。 議事録をSlackに送信する場合は、Slackチャンネルも選択します。 日付、会議名、Google Drive(共通ドライブ)のURLなど、会議に関する基本情報も入力します。日付や会議名は議事録作成時の補完に使われ、Google DriveへのURLはSlack通知の際に動画の視聴リンクとして付け加えています。 ② 生成ボタン押下 画面上では生成ボタンを押すだけで議事録が自動的に生成され、Slackチャンネルに送信されますが、裏側ではFastAPIアプリへのAPIリクエストが行われています。具体的な処理は FastAPIアプリにAPIリクエストを送信し、各ファイルをGoogle Cloud StorageにアップロードするためのPresigned URLを生成します。 PresignedURLを取得できた後、FrontendアプリからPresignedURLを使ってGoogle Cloud Storageへアップロードをアップロードします。 ファイルアップロード完了後、再度FastAPIにリクエストを送り、議事録をVertex AIで生成し、生成された議事録をSlackに投稿します。 といった流れになっています。 プロンプトに関してはまだ最適案ではないですが、以下のようなものを使用しました。 あなたはプロフェッショナルな会議議事録作成者です。 あなたのタスクは、インプットデータを使用して、会議の議事録を作成することです。 # インプットデータ - 会議の録画動画: 会議中に録画された動画ファイル。 - チャット履歴: 会議中に送信されたチャットメッセージの履歴を含むPDFファイル。各メッセージには発言者名とタイムスタンプが含まれています。 - メモ: 会議中に作成されたメモ - その他資料: 会議中に使用されたその他の資料 会議の録画動画: [録画動画のURI] チャット履歴:[チャット履歴のURI] メモ: [メモのURI] その他資料: [その他資料のURI] # Steps 1. **会議内容の把握:** 会議の録画動画、チャット履歴、メモなどを参照し、会議の内容を把握してください。 - 会話と会議録画に投影された内容から主なトピックを特定し、内容を抽出してください。 - チャット履歴、メモ、その他資料を参照して、会議の内容を理解してください。 - トピックが変わるタイミングを検知し、トピックごとに発話内容を発言者に紐づけてグループ化してください。 - 決定事項やアクションアイテムを特定してください。 2. **情報の整理**: 抽出した情報をわかりやすいセクションに整理します。関連する情報をひとつのセクションにまとめて、読みやすさを優先します。 3. **議事録の作成**: Markdown形式で議事録を作成します。重要なポイントを適切な階層の見出し(#)で整理し、段落や箇条書きなどを活用して情報を明瞭に表現します。 4. **レビューと修正**: 完成した議事録を確認し、誤りや不足があれば修正してください。 # Output Format - 議事録はMarkdown形式で出力してください。 - タイトルやセクションには見出し(#)を使用し、適切な階層を設定する - 箇条書きや番号付きリストを適宜使用し、情報を整理する ③ 議事録を確認 : 生成された議事録はSlackに送信されると同時に、画面上でも表示されます。Slackへの送信したイメージは以下です。 Slack議事録投稿 半年間使用する中で、自動生成された議事録に調整が必要と感じる部分が多かったため、次の2つの方法で対応しました。 Slackへの自動送信機能を使用せず、画面上に表示された議事録をコピーし、VSCodeなどのエディターで修正してから正式な議事録を展開する。 Slackへの自動送信機能で議事録を自動的に送信し、送信後のスレッド上で内容を修正する。 半年間運用して得られた学び 議事録自動生成の精度がどうだったか? 今回、プロンプト設計についての深い調整は行いませんでしたが、ある程度質の高い議事録が生成できたと感じています。 生成AI特有のハルシネーション(生成AIによる誤情報や虚偽の提示)も一部見られましたが、会議に参加していなかったメンバーでも、会議で交わされた内容が把握できるレベルの議事録が生成されました。また、アクションアイテムや次回の予定も概ね正確だったと思います。 さらに今回は、音声データを使用するだけではなく、録画動画やチャット履歴ファイル、その他のメモファイルの情報を全て合わせてインプットし、マルチモーダルAIによる議事録生成を行ったため、会議中に投影された資料やチャット内容も参考情報として使用され、いい感じに議事録に統合されました。そのため、議事録の正確性がかなり向上できたと思います。 使用したモデルおよびコストについて モデル 検証開始時は議事録生成にマルチモーダル対応の Gemini-1.5-Pro-001 モデルを使いました。 Gemini 1.5 Flash と Gemini 1.5 Pro モデルの主な違いはコンテキストウィンドウの大きさ、処理速度、コストにありますが、その中で Gemini 1.5 Pro が優れているものが コンテキストウィンドウの大きさだけ だと思います。 Gemini 1.5 Pro は最大200万トークンのコンテキストウィンドウに対応しているのに対し、 Gemini 1.5 Flash は、100万トークンまでしか対応していないのです。 今回検証で使用した会議はほぼ30分や1時間の会議のため、 Gemini 1.5 Flash を使うとどうしても上限の100万トークンに引っかかってしまい、動画の分割などを行わないとそのまま使えませんでした。一方で、 Gemini 1.5 Pro を使うと、トークン上限にかかることがほぼなく、精度に関してもそこそこ良い結果のものが得られたので、今回はコスト面を多少無視して、 Gemini 1.5 Pro を使いました。ちなみに、1時間の会議で試した場合、約101万トークン( Flashモデルの上限に対して、約1万トークンオーバー )がかかりました。 また、2024年9月にGoogle社が Gemini-1.5-Pro-002 と Gemini-1.5-Flash-002 の新バージョンをリリースした時も、即座に Gemini-1.5-Pro-002 に切り替えて運用してみました。001バージョンとはしっかり比較できていないですが、生成速度および生成された議事録の質が多少改善されたように感じています。 コスト Gemini-1.5-Pro-002 を使った場合のコストについてですが、例えば1時間の会議の議事録を取得する場合、1時間の動画入力( 録画動画 )、1枚の画像( チャット履歴等のファイルをPDF化したもの、PDFは画像としてカウントされるため )、1000文字のテキスト入力( プロンプト等 )、3000文字のテキスト出力( 議事録の結果 )が必要になります。 VertexAIの料金 に基づいて、以下のように計算できます。 動画入力: 1時間(3600秒)× $0.00263/秒 = $9.468 画像入力: 1枚 × $0.00263/枚 = $0.00263 テキスト入力: 1000文字 × $0.0025/1000文字 = $0.0025 テキスト出力: 3000文字 × $0.0075/1000文字 = $0.0225 合計で、 約$9.50(約1,500円程度) が1回の議事録の生成にかかっていることが分かりました。 これを見ると、1回の議事録の生成に約1,500円もかかることは正直少し高く、コスパが悪いと感じました。今回は学習目的も含めて社内検証のために使用していたため、このコストは許容範囲内でしたが、実運用に移行する際には、コスト面も考慮しなければならないと感じました。 一方で、もしインプットデータのサイズが100万トークン以内に収まることができたら、 Gemini 1.5 Flash を使用し、コストをビックリするほど削減できます。 動画入力: 1時間(3600秒)× $0.00004/秒 = $0.144 画像入力: 1枚 × $0.00004/枚 = $0.00004 テキスト入力: 1000文字 × $0.0000375 /1000文字 = $0.0000375 テキスト出力: 3000文字 × $0.00015/1000文字 = $0.00045 なんと、合計で 約$0.145(約22円程度) しかかからないのです。このコストであれば、すごく魅力的だと思いました。 安全フィルターについて 検証初期には、 安全フィルター の基準が高く、社内の会議で使用している動画が安全フィルターの「少量をブロック( BLOCK_ONLY_HIGH )」設定でも PROHIBITED_CONTENT エラーに引っかかり、何度か議事録生成に失敗しました。当初、公式のドキュメントには「ブロックなし(BLOCK_NONE)」レベルの設定も可能とありましたが、設定しても 400 Bad Request エラーが発生し、VertextAIのコンソール画面上でもこの選択ができない状態を確認しました。 その後、2024年9月の Gemini-1.5-Pro-002 がリリースされた時点と同時期に、 BLOCK_NONE が設定できるようになったという情報を受け、この設定で運用したところ問題なく生成が成功するようになりました。 プロンプトおよび生成過程について 検証初期から、特別にプロンプト設計を意識するや、「文字起こしをしてから議事録を生成する」や「議事録生成後に事実チェックを行う」といった複数ステップの手順は導入せず、ほぼ一発で議事録を生成する方法を試してきました。この手順でも、社内での情報共有用途としてはある程度機能しましたが、半年にわたり様々な会議で試したところ、やはりハルシネーションが多少目立つように感じています。内容の90%程度は正確であるものの、残り10%に誤情報が含まれていると、議事録の信頼性が損なわれてしまうため、社内利用はできるものの、複数のステークホルダーに展開するプロジェクト議事録として使用するには難しいと感じました。 こうした点からも、プロンプトの調整やステップを踏むプロセスの重要性を実感しています。現在は、生成AI案件を多く手がける社内メンバーや、生成AIに特化した技術研究チームのメンバーにも意見を聞きながら、このプロンプトを改善し、さらに信頼性を高めた生成過程を模索していきたいと思っています。 VertexAI Geminiの凄さについて 今回、コスト面ではまだ改善されるべきなところもありますが、何よりもVertex AI Geminiの凄さを実感できました。 通常、議事録生成には音声データテキスト変換(文字起こし) を行い、そのテキストデータを元に議事録の生成処理をする必要がありますが、Vertex AIではこの変換を個別に行う必要がなく、録画動画やチャット履歴、メモなどをそのままインプットでき、直接議事録を生成できる点から、特に複雑なシステムを構築する必要がなく、簡単な構成とプロンプトで議事録を生成できる点が大きなメリットだと感じました。 録画動画から会議中に投影された資料の内容も参照されて、議事録作成に使用されるで議事録の内容がよりリッチになっていることも、とても感動しました。 議事録を投稿できるまでの手順の制限について 今回のアプリができたことによって、議事録を展開するところまでは少し楽になりましたが、主催者でない人が操作する場合、録画動画を共通ドライブに移動(または権限変更)しなければならない点や、各種ファイルを一度ダウンロードしてから、アプリを使って再度アップロードする手順がかなり手間と感じました。会議終了後、誰でもアプリから該当のカレンダー予定を指定するだけで、あとは自動的に処理されることが理想ですが、Google Meetでは録画がカレンダー主催者のマイドライブに保存される点や、Vertex AIがCloud Storageのみ参照できる点がボトルネックになってしまっています。 Google Workspace APIを使用して、Google Calendar、Google Meet、Google Driveと上手く連携できる仕組みを早く構築しなければと感じました。 また、Cloud Storageの料金もかかりますので、コスト面も配慮しなければならないですね。 まとめと今後の期待 VertexAI Geminiを使って社内用AI議事録生成アプリの開発と個人的に学んだことについて紹介させていただきました。 半年運用してみた結果、実用性のある成果を得られましたが、まだ改善点が多いと感じています。今後は以下の点を意識しながらアプリの向上を目指したいと思います。 プロンプトおよび生成過程の改善による精度向上 Google DriveやGoogle Calendarとの連携によるアプリ操作簡略化 社内のエンジニアだけでなく全メンバーが使えるようなホスティング環境とアクセス制限の整備 まだまだやることが多いですね。笑 やるべきことは多いですが、正直のところ、そのうちにGoogle Workspaceのコアサービスとしてこの機能がリリースされるのではないか!とその日をずっと期待しています。 標準機能のリリースを期待しつつ、せっかくある程度開発できたので、まずは社内での活用を広げ、改善を重ねていきたいと思います。
アバター
目次 1. はじめに 2. ログデータの収集 GCP インフラ構成の説明 各サービスの設定 ディレクトリ構成 共通リソースの作成 個別プロジェクトリソースの作成 3. ログデータの可視化 4. まとめ 1. はじめに こんにちは。Insight Edge で Developer をしている熊田です。 普段システム開発を進める上で、システムの利用者数や頻繁に利用されている機能を調べたいと思うことはありませんか? 特にPoC検証やシステム運用フェーズにおいては、そのようなニーズが多くあるのではないでしょうか。 そのようなニーズに応えるためには、ログを収集する必要があります。また、上記のようなニーズはプロジェクト共通のものであることが多いかと思います。 これら要望に応えるために、GCP の複数プロジェクトにまたがるログ収集及び可視化をするためのロギング基盤を検証構築してみたので、その紹介をしたいと思います。 2. ログデータの収集 GCP インフラ構成の説明 まず、インフラ構成図は以下の通りです。 簡単に構成を説明します。 ① 各プロジェクトにて Cloud Logging にログを出力することを前提としています。 ② ログ Sync を作成しフィルタ機能を利用して必要なログのみ、ログ収集用プロ  ジェクトの Cloud Storage(以下GCS) にログを転送します(1時間毎)。 ③ Eventarc を利用して、GCS にログが転送されたことをトリガーに Workflows を  起動します。 ④ Workflows にて、定義済みの Dataflow を実行し、ログを BigQuery に格納します。  なお、Dataflow 実行時に必要なファイル群は GCS に格納しています。 ⑤ BigQuery に格納されたログデータを Looker Studio で可視化します。 各サービスの設定 基本的には Terraform コードを利用し、一部は GCP コンソール作業やシェルスクリプト作業をしています。以下に実装詳細を記載します。 ディレクトリ構成 . ├── BQ │ └── create_table.sql ├── logger_sync │ ├── sample_service_A │ │ └── create-sync.sh │ └── sample_service_B │ └── create-sync.sh └── terraform ├── env │ ├── dev │ │ ├── common │ │ │ ├── common.tfvars │ │ │ ├── dataflow_config │ │ │ │ ├── schema │ │ │ │ │ └── usage_log.json │ │ │ │ └── udf │ │ │ │ └── transform.js │ │ │ ├── main.tf │ │ │ ├── variables.tf │ │ │ └── workflow.yaml │ │ ├── sample_service_A │ │ │ ├── main.tf │ │ │ ├── sample_service_A.tfvars │ │ │ └── variables.tf │ │ └── sample_service_B │ │ ├── main.tf │ │ ├── sample_service_B.tfvars │ │ └── variables.tf │ └── prd └── modules ├── aws ├── azure └── gcp ├── eventarc │ ├── main.tf │ └── variables.tf └── gcs ├── main.tf ├── output.tf └── variables.tf 共通リソースの作成 まずは、共通で利用するリソースを以下の通り作成していきます。 BigQuery 今回は BigQuery テーブルをコンソールから作成していきます。 データセットを作成し、その後クエリエディタに以下のステートメントを入力および実行してテーブルを作成します。 ./BQ/create_table.sql CREATE TABLE `[project].sample_dataset.usage_log` ( timestamp TIMESTAMP NOT NULL OPTIONS(description= " ログ日時 " ), service_name STRING NOT NULL OPTIONS(description= " サービス名 " ), user_id STRING NOT NULL OPTIONS(description= " ユーザID " ), activity_name STRING NOT NULL OPTIONS(description= " アクティビティ名 " ) ) PARTITION BY TIMESTAMP_TRUNC( timestamp , DAY) 各種 Terraform ファイル ./terraform/env/dev/common/common.tfvars project_id = [ ロギング基盤プロジェクトのプロジェクトID ] default_region = "asia-northeast1" prefix = "logging-common" env = "dev" ./terraform/env/dev/common/variables.tf variable "project_id" {} variable "default_region" {} variable "prefix" {} variable "env" {} ./terraform/env/dev/common/main.tf ## Provider ## provider "google" { project = var.project_id region = var.default_region } locals { dataflow_config_files = [ "schema/usage_log.json" , "udf/transform.js" ] } ## GCS ## resource "google_storage_bucket" "bucket" { name = "dataflow-config-$ { var.env } -$ { var.prefix } " location = var.default_region project = var.project_id storage_class = "STANDARD" public_access_prevention = "enforced" uniform_bucket_level_access = true force_destroy = true } ## Uplaod dataflow config files to GCS ## resource "google_storage_bucket_object" "dataflow_config_files" { for_each = toset (local.dataflow_config_files) name = each.value bucket = google_storage_bucket.bucket.name source = "$ { path.module } /dataflow_config/$ { each.value } " } ## Workflows ## resource "google_workflows_workflow" "dataflow_workflow" { name = "dataflow-$ { var.env } -$ { var.prefix } " description = "Workflow for dataflow" project = var.project_id region = var.default_region source_contents = file ( "$ { path.module } /workflow.yaml" ) } GCS 上記 main.py の通り、GCS を作成し Dataflow で利用する スキーマ定義ファイル、UDF スクリプト(ユーザ定義関数)を GCS にアップロードします。 ./terraform/env/dev/common/dataflow_config/schema/usage_log.json { " BigQuery Schema ": [ { " name ": " timestamp ", " type ": " TIMESTAMP ", " mode ": " REQUIRED ", " description ": " ログ日時 " } , { " name ": " service_name ", " type ": " STRING ", " mode ": " REQUIRED ", " description ": " サービス名 " } , { " name ": " user_id ", " type ": " STRING ", " mode ": " REQUIRED ", " description ": " ユーザID " } , { " name ": " activity_name ", " type ": " STRING ", " mode ": " REQUIRED ", " description ": " アクティビティ名 " } ] } ./terraform/env/dev/common/dataflow_config/udf/transform.js function transform ( data ) { const record = JSON . parse ( data ) ; // timestampをBigQueryで扱える形式に変換 timestamp = record . jsonPayload . timestamp . replace ( "T" , " " ) . replace ( "+0900" , "+09:00" ) ; // 新しいJSONオブジェクトを作成 const transformedRecord = { project_id : record . jsonPayload . project_id , timestamp : timestamp , service_name : record . jsonPayload . service_name , user_id : record . jsonPayload . user_id , activity_name : record . jsonPayload . activity_name , } ; // 変換後のJSONオブジェクトを文字列にして返す return JSON . stringify ( transformedRecord ) ; } Workflows 上記 main.py の通り、Dataflow ジョブを作成するワークフローを定義します。 その際に、下記の Dataflow 定義 yaml ファイルを参照しています。 ./terraform/env/dev/common/workflow.yaml # Dataflowのジョブを作成するワークフロー main : params : [ event ] steps : - init : assign : - project_id : ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")} - location : ${sys.get_env("LOCATION")} - input_bucket_name : ${event.data.bucket} - input_file : ${event.data.name} - service_name : ${sys.get_env("SERVICE_NAME")} - config_bucket_name : "dataflow-config-dev-common" // 設定ファイルを配置したバケット名 - bq_dataset : "sample_dataset" // BigQueryのデータセット名 - table_name : "usage_log" // BigQueryのテーブル名 - event_log : call : sys.log args : text : '${"input_bucket_name: " + input_bucket_name + ", input_file: " + input_file}' - create_dataflow : call : googleapis.dataflow.v1b3.projects.locations.templates.create args : projectId : ${project_id} location : ${location} body : jobName : dataflow parameters : javascriptTextTransformGcsPath : ${"gs://" + config_bucket_name + "/udf/transform.js" } JSONPath : ${"gs://" + config_bucket_name + "/bq_schema/" + table_name + ".json" } javascriptTextTransformFunctionName : transform outputTable : ${project_id + ":" + bq_dataset + "." + table_name} inputFilePattern : ${"gs://" + input_bucket_name + "/" + input_file} bigQueryLoadingTemporaryDirectory : ${"gs://" + config_bucket_name + "/temp" } gcsPath : gs://dataflow-templates-asia-northeast1/latest/GCS_Text_to_BigQuery result : output 個別プロジェクトリソースの作成 次に、各プロジェクト毎に必要なリソースを作成していきます。 Terrafom コードは以下の通りです。 ./terraform/env/dev/sample_service_A/sample_service_A.tfvars project_id = [ ロギング基盤プロジェクトのプロジェクトID ] default_region = "asia-northeast1" env = "dev" workflow_name = "dataflow-dev-logging-common" eventarc_service_account_email = [ Eventarc のサービスアカウント ] service_name = "sample-service-A" # サービスごとに変更 ./terraform/env/dev/sample_service_A/variables.tf variable "project_id" {} variable "default_region" {} variable "env" {} variable "workflow_name" {} variable "eventarc_service_account_email" {} variable "service_name" {} ./terraform/env/dev/sample_service_A/main.tf ## Provider ## provider "google" { project = var.project_id region = var.default_region } locals { prefix = "$ { var.env } -$ { var.service_name } " } ## GCS ## module "google_storage_bucket" { source = "../../../modules/gcp/gcs" project_id = var.project_id default_region = var.default_region prefix = local.prefix } ## Eventarc ## module "eventarc" { source = "../../../modules/gcp/eventarc" project_id = var.project_id default_region = var.default_region prefix = local.prefix service_account_email = var.eventarc_service_account_email gcs_bucket_name = module.google_storage_bucket.bucket_name workflow = "projects/$ { var.project_id } /locations/$ { var.default_region } /workflows/dataflow-dev-logging-common" } GCS 他プロジェクトから転送されるログを格納する GCS を作成します。 ./terraform/modules/gcp/gcs/main.tf # GCS resource "google_storage_bucket" "bucket" { name = "log-bucket-$ { var.prefix } " location = var.default_region project = var.project_id force_destroy = true } ./terraform/modules/gcp/gcs/variables.tf variable "project_id" {} variable "default_region" {} variable "prefix" {} ./terraform/modules/gcp/gcs/output.tf output "bucket_name" { value = google_storage_bucket.bucket.name } Eventarc GCS にログが転送されたことをトリガーに Workflows を起動するための Eventarc を作成します。 ./terraform/modules/gcp/eventarc/main.tf # Create an Eventarc trigger, routing Cloud Storage events to Workflows resource "google_eventarc_trigger" "default" { name = "workflows-trigger-$ { var.prefix } " location = var.default_region # Capture objects created in the specified bucket matching_criteria { attribute = "type" value = "google.cloud.storage.object.v1.finalized" } matching_criteria { attribute = "bucket" value = var.gcs_bucket_name } # Send events to Workflows destination { workflow = var.workflow } service_account = var.service_account_email } ./terraform/modules/gcp/eventarc/variables.tf variable "project_id" {} variable "default_region" {} variable "prefix" {} variable "gcs_bucket_name" {} variable "workflow" {} variable "service_account_email" {} ログ Sync の設定 サービスが配置されるプロジェクト(構成図での A プロジェクト)にて、ログ Sync を作成します。 フィルタ機能を利用して必要なログのみ、ログ収集用プロジェクトの GCS にログを転送します(1時間毎)。 今回の例では、 jsonPayload.usage_monitoring: true のログのみを転送するように設定しています。 スクリプト内容は以下の通りです。 ./logger_sync/sample_service_A/create-sync.sh SERVICE_NAME =sample-service-A # サービス名 DESTINATION_PROJECT_ID = [ project_id ] # ログ出力先のプロジェクトID DESTINATION_BUCKET_NAME =log-bucket-dev-sample-service-A # ログ出力先のバケット名 SINK_NAME = $SERVICE_NAME -sink-to-logging-project SINK_DESTINATION =logging.googleapis.com/projects/ $DESTINATION_PROJECT_ID /locations/global/buckets/ $DESTINATION_BUCKET_NAME \ gcloud logging sinks create $SINK_NAME $SINK_DESTINATION --log-filter ' jsonPayload.usage_monitoring: true ' ログの出力 別途 Cloud Function や Cloud Run 等を構築し、構造化ログを Cloud Logging に出力します。 今回は下記のようなログが JSON ペイロードで出力されることを前提としています。 { jsonPayload : { " usage_monitoring ": True ,  // ログ収集用のフラグ " timestamp ": " 2024-10-01T00:00:00 ", // ログ日時 " service_name ": " service1 ", // サービス名 " user_id ": " test@example.com ", // ユーザID " activity_name ": "act1_start" // アクティビティ名 } } 3. ログデータの可視化 収集したログデータを可視化するために、Looker Studio を利用しました。 選んだ理由は、お手軽にダッシュボードを作成できるからです。 というのも、Looker studio は 単純な集計であれば 直感的に UI 操作のみで可視化ができるため、専門的なスキルがなくても扱えます。 例えば、以下のようなアクティビティ及びユニークログイン数を集計したグラフを、ブラウザ操作のみで作成することができます。 Looker Studio 操作方法等については 公式ドキュメント をご参照ください。 4. まとめ 以上、ロギング基盤構築のご紹介でした。なお、構築はできたものの最低限の機能であり、まだまだ改善の余地があります。 今のところは共通プロパティのみの集計でしたが、実用性を考えると各プロジェクトで個別で集計したいプロパティもあると思うので、そちらも集計できるように改修したいと思っています。 また、今回は検証ということで GCP 環境のみを対象にしていましたが、当社では、AWS、Azure も使ってシステム開発を行っています。 今後は、マルチクラウドに対応したロギング基盤の構築にも取り組んでいきたいと考えています。また、ディレクトリ構成についても、より適切な構成を検討していきたいです。
アバター
Insight Edgeのデータサイエンティストのki_ieです。 数理最適化の専門家として、これまでさまざまな課題を数理最適化問題としてモデリングしてきました。 モデリングはアルゴリズム設計と比べて注目を集めることが少ないようですが、 実際には技術的な知見・調査を要求する骨の折れるタスクです。 このタスクを簡単にしたいとは日々思っていたところですが、最近 OpenAI o1 というモデルの論理的推論能力が高いらしいという聞きました。この賢いLLMがモデリングのお手伝いをしてくれたら嬉しいですね! 今日は数理最適化問題(混合整数計画問題)のモデリングをどれだけLLMに任せられるのか、簡単な実験結果をご紹介します。 数理最適化問題とは 数理最適化問題と混合整数計画問題の基礎知識がある方はこの節はスキップしましょう(面白いことは一つも書いてありません)! 数理最適化問題とその実行可能解・最適解 数理最適化問題とは「変数・制約・目的関数」が与えられたときに、 「制約を守るなかで最も目的関数を小さく(または大きく)する決定変数を選ぶ」 という問題です。 数理最適化問題は、一般に以下の形で表現できます: 数理最適化問題 変数 : $x \in D \ \ (\subseteq \mathbb{R}^n)$ 変数 $x$ は $D$ の中から選べる 制約 : $g_i(x) \leq b_i \ \ (i \in I)$ $x$ は $g_i(x) ≤ b_i$ がすべての $i \in I$ について成り立つように選ぶ 目的関数(最小化) : $\min f(x)$ 上記ルールを守ったうえで $f(x)$ を最小化したい。 上手に $D$, $g_i$, $f$ を設計してやることで、配送計画から証券ポートフォリオの最適化まで幅広い問題が 数理最適化問題として表現できることが知られています。 たとえばコンビニの店長の立場でおにぎりの仕入れ量を適正化する問題を単純化すれば、以下のような最適化問題としてモデル化できます: 例: おにぎりの仕入れ量の適正化 変数: おにぎりに仕入れ量 $x \in \mathbb{Z}_+$ 制約: おにぎりが店から溢れない $x \leq 100$ 目的関数: $\max f(x)$ $f(x)$ : おにぎり $x$ を仕入れたときの期待利益 $D$ の要素で制約をすべて守るものはその問題の実行可能解と呼ばれます。 実行可能解のうち目的関数を最小にするものは数理最適化問題の最適解(または解)と呼ばれます。 数理最適化問題で課題をモデリングする目的は、最適解を得ることにあります。 たとえばおにぎりの仕入れ量の適正化の文脈では最も利益があがる仕入れ量が $70$ であれば、$x=70$ が最適解です。 これが計算で求められれば、このコンビニでは利益を最大化する仕入れの意思決定ができるようなってハッピーです。 一方、問題によっては最適解を求めることが極めて難しいことがあります。 おにぎりの仕入れ量の適正化問題は$x=0, 1, 2 \cdots 100$ を全探索すれば簡単に最適解が求まりますが、 もっと複雑な問題ではそうはいかないこともしばしばあるのです。 そのような場合は「目的関数値の良い」実行可能解が求まったらそれでよしとすることが実務上は一般的です。 おにぎりの仕入れ量の問題で言えば、$x=68, 71$ といった解が見つかって、$x=70$ が見つけられなかったら、 $x=68, 71$ をとりあえずの答えとして採用するというイメージです。 この場合でも、あてずっぽうで仕入れ量を決定するコンビニよりは利益を大きくできるわけです。 混合整数計画問題 数理最適化問題の中でも特別に重要な問題クラスとして、混合整数計画(MIP; Mixed Integer Programming)問題があります。 これは実行可能領域 $D$ を$\mathbb{R}^n \times \mathbb{Z}^m$ として、制約・目的関数を線形なものに限定したものです。 混合整数計画問題 変数 : $x \in \mathbb{R}^n \times \mathbb{Z}^m$ 制約 : $a_i^\top x \leq b_i \ \ (i \in I)$ $a_i^\top$ を行ベクトルとして集めた行列を$A$, $b_i$ を集めたベクトルを $b$ としたら、まとめて $Ax ≤ b$ と書ける。 目的関数(最小化) : $\min c^ \top x$ 変数に別途上下限を許す場合も、等号制約を許す場合もある。いずれもここで示した形に容易に変形できる この問題クラスの重要性を説明するために、まず数理最適化を利用した課題解決の一般的な流れを説明します。 さまざまなタイプの数理最適化問題に対して、最適解または最適解ではないにせよ目的関数値の良い実行可能解を求めてくれるプログラムが作られています。これらのプログラムはよく「ソルバー」とよばれます。数理最適化の考え方とソルバーを利用して現実の課題を解くときの流れは以下のようになります。 課題の整理 課題を数理最適化問題としてモデル化 数理最適化問題をソルバーで求解する 得られた最適解(または目的関数値の良い実行可能解)を現実世界で採用する 課題を数理最適化問題に落とし込むことにも技術的な難しさがありますが、 その数理最適化問題を解くソルバーを設計・実装することもまた大変に難しいことです。 そのため、できれば既存のソルバーを用いて求解できるような範囲でモデル化をしたいというのが数理最適化エンジニアの本音です。 MIPによるモデリングはこの観点で大変すぐれています。MIPは多様な問題を表現でき、さらに優れたソルバーが存在するからです。 MIPは制約・目的関数が線形に限定されているため一見すると限られた表現力しかないように見えますが、0, 1の値しかとらないバイナリ変数を巧みに使うことで複雑な論理関係を表現でき、幅広い問題を表現可能です。表現できる問題の幅広さゆえ、学術的にも深く研究され、ソルバーの研究が積み重ねられてきました。その結果、優れた商用・OSSのソルバーが存在するのです。 このような背景から、MIPによるモデリングは数理最適化の実務においてなくてはならない基本技術となっています。 LLMに混合整数計画問題をモデリングさせたいモチベーション 課題のMIPモデリングというのはなかなか骨の折れる作業です。 論理的に正しいというだけではなくソルバーにとって解きやすいモデルを作ることが求められるため、 難しい課題の場合は最適化モデルの試行錯誤・調査の1サイクルだけで数日かかってしまう、ということも稀ではありません。 この作業、なるべくサボりたいですね。性能向上の著しいLLMに手伝ってもらえれば、 まるごと作業代替とはならなくとも効率化できるのではないか?というのが、本記事で検証したいポイントです。 数理最適化エンジニアとして確認したい具体的なポイントは以下の3つです。 有名問題をモデル化できるか / 有名定石を利用できるか? 有名問題に簡単な要件を追加してもモデル化がうまくいくか? 複雑な要件を上手にモデル化できるか? LLM に 1. ができたら、数理最適化初心者/学習者にはLLMによる補助が有用だと考えられるでしょう。 数理最適化問題のモデリングにはさまざまな「定石」が登場します。 例えば、0/1変数 $x, y \in \{0, 1 \}$ の論理積をMIPの枠組みで考えたいときには、補助変数 $z \in \{0, 1 \}$ を導入して 制約 $z ≤ x, z ≤ y, x+y-1≤z$ を加えれば $z$ は $x, y$ の論理積(AND)となる、というテクニックがあります。 知っていれば簡単に使えるテクニックですが、自分で思いつくのは難しいです。 初心者がこれをLLMに教えてもらえるなら、学習効率は上がりそうですね。 ある程度経験のある数理最適化エンジニアにとっても、モデル化方法の検討がつかないときのサーベイの代替として LLMに聞くというのが有用になるかもしれません。 2.ができたら、実務的なモデリングの補助としても使えそうです。 実際の問題は有名定石単体で解けないケースのほうが多く、 そのような場合定石を組み合わせたり、自分でテクニックを作ってモデル化する必要があります。 ここまでLLMがやってくれるなら、実務的なモデリングの補助として十分有用でしょう。 3.は、ここまでできたらモデリングの初手はLLMとの対話になりそうですね。 これで毎回完璧な答えが出てきたら私達は冷や飯を食わされることになりそうですが…どこまでできるか見てみましょう。 LLM に数理最適化問題をモデリングさせる 現在利用可能な o1 モデルの o1-mini モデルを利用して実験を行います。 確認したいポイントに対応させて、3つのレベルに分けて実験を進めましょう。 1. 有名問題をモデル化できるか/有名定石は使えるか? まずは有名問題・有名定石のモデリングをさせてみましょう。毛色の違う2つの有名問題を解かせてみます。 巡回セールスマン問題 o1-mini へ次の質問を投げました: 次の課題を混合整数計画問題としてモデル化してください。 「10個の都市をすべて回って出発地点に返ってくる必要がある。移動時間を最短化したい。」 結果は「パラメタ」「決定変数」「制約」「目的関数」の4セクションに分けてください。 これは巡回セールスマン問題という有名問題で、特殊なモデリングの技法が使われていますが「知っていれば書ける」タイプの問題です。 o1-mini の出力を見てみましょう。見出しのレベルだけ編集で本文に合うように揃えている点以外、出力そのままです。 パラメタ $ N = \{1, 2, \dots, 10\} $: 10個の都市の集合。 $ t_{ij} $: 都市 $ i $ から都市 $ j $ への移動時間($ i, j \in N $, $ i \neq j $)。 決定変数 $ x_{ij} \in \{0,1\} $: 都市 $ i $ から都市 $ j $ へ移動する場合に1、そうでない場合に0。 $ u_i $ (連続変数): 都市 $ i $ の訪問順序を表す補助変数(サブツアー除去のため)。ここでは都市1を出発地点とし、$ u_1 = 1 $ とする。 制約 各都市からちょうど1つの都市へ出発する $$\sum_{\substack{j \in N \\ j \neq i}} x_{ij} = 1 \quad \forall i \in N$$ 各都市へちょうど1つの都市から到着する $$\sum_{\substack{i \in N \\ i \neq j}} x_{ij} = 1 \quad \forall j \in N$$ サブツアーを防止するための順序変数に関する制約(ミラー・タミルゾーラックの制約) $$u_i - u_j + 10 \cdot x_{ij} \leq 9 \quad \forall i, j \in N, \, i \neq j$$ 順序変数の範囲制約 $$2 \leq u_i \leq 10 \quad \forall i \in N \setminus \{1\}$$ 目的関数 総移動時間を最小化する: $$\min \sum_{i \in N} \sum_{\substack{j \in N \\ j \neq i}} t_{ij} \cdot x_{ij}$$ このモデルは、10個の都市を全て一度ずつ訪問し、出発地点に戻る巡回経路(ツアー)を見つけるための混合整数計画問題として定式化されています。制約条件により、各都市の出発と到着が一度ずつ行われることを保証し、順序変数を用いることでサブツアーの発生を防いでいます。目的関数は、全体の移動時間を最小化することを目指しています。 正しく巡回セールスマン問題がモデリングできているようですね! 人名だけ間違っているようです。 制約 3., 4. は Miller-Tucker-Zemlin (MTZ) 定式化などと呼ばれます(正確には制約3., 4.を含むこのモデル全体を指す言葉だと思います)。 o1-mini の出力では「ミラー・タミルゾーラック」という知らない人の名前が出てきましたが、これはMTZの人名が日本語で書かれることが稀であるから起きる間違いかもしれませんね。 ちょっと怪しい言葉が登場してしまいましたが、o1-mini は巡回セールスマン問題を「知っている」ということがわかりました。 クラス分け問題 別の有名問題、クラス分け問題を定式化をしてみましょう。 o1-mini へ次の質問を投げました。 次の課題を混合整数計画問題としてモデル化してください。 「120人の学生を3つのクラスに分けたい。3つのクラスの人数は等しくしたうえで、クラス間の成績のばらつきを最小に抑えたい。」 結果は「パラメタ」「決定変数」「制約」「目的関数」の4セクションに分けてください。 次の結果が返ってきました: 以下に、指定された課題を混合整数計画問題としてモデル化します。 パラメータ $ N = 120 $ 学生の総数。 $ C = 3 $ クラスの数。 $ s_i $ 学生 $ i $($ i = 1, 2, \dots, 120 $)の成績。 $ M $ 大きな定数(必要に応じて使用)。 決定変数 $ x_{i,c} $ バイナリ変数。 $$ x_{i,c} = \begin{cases} 1 & \text{学生 } i \text{ がクラス } c \text{ に割り当てられる場合} \\ 0 & \text{それ以外の場合} \end{cases} $$ ここで、$ c = 1, 2, 3 $。 $ \mu_c $ クラス $ c $ の平均成績(連続変数)。 $ \delta $ クラス間の成績のばらつきを表す変数(連続変数)。最小化対象。 制約 各学生はちょうど1つのクラスに割り当てられる。 $$ \sum_{c=1}^{3} x_{i,c} = 1 \quad \forall i = 1, 2, \dots, 120 $$ 各クラスの学生数は40人である。 $$ \sum_{i=1}^{120} x_{i,c} = 40 \quad \forall c = 1, 2, 3 $$ クラスの平均成績を定義する。 $$ \mu_c = \frac{1}{40} \sum_{i=1}^{120} s_i x_{i,c} \quad \forall c = 1, 2, 3 $$ クラス間のばらつきを定義する(例として最大の平均成績差を用いる)。 クラス間の平均成績差を表すために、以下の制約を導入します。 $$ \mu_c - \mu_{c'} \leq \delta \quad \forall c, c' = 1, 2, 3 \text{ かつ } c < c' $$ $$ \mu_{c'} - \mu_c \leq \delta \quad \forall c, c' = 1, 2, 3 \text{ かつ } c < c' $$ 目的関数 クラス間の成績のばらつきを最小化する。ここでは、最大の平均成績差 $ \delta $ を最小化します。 $$\text{Minimize } \delta$$ モデルの概要 目的 : クラス間の平均成績差を最小化することで、成績のばらつきを抑える。 制約 : 各学生が1つのクラスに割り当てられ、各クラスに40人の学生が配置される。 決定変数 : 学生のクラス割り当てを表すバイナリ変数と、各クラスの平均成績およびばらつきを表す連続変数。 パラメータ : 学生数、クラス数、および各学生の成績。 このモデルにより、120人の学生を3つのクラスに均等に分けつつ、クラス間の成績のばらつきを最小化する最適な割り当てを求めることができます。 おおむね問題のないモデリングですね! 「クラス間の成績のばらつきを最小に」というあいまいな言葉を「最大の平均成績差 $ \delta $ を最小化」と解釈してMIPモデルを作っているのは、よくあるテクニックですが素晴らしいです。 なぜか $M$ という謎のパラメタが導入されていますが無視してあげましょう。 平均成績を変数+制約で定義するのは正しいですが少し無駄のあるモデルにも思えます。これは4セクションに分けてくださいという指示があるため仕方ないかもしれません。 2. 有名問題に簡単な要件を追加してもモデル化がうまくいくか? 有名な問題は知っている/モデリングできるということがわかったので、先程の2問題に少し要件を加えてみましょう。 巡回セールスマン問題 + α 巡回セールスマン問題に要件を加えた問題を投げてみます。 下線部 が追加要素です。 次の課題を混合整数計画問題としてモデル化してください。 「10個の都市をすべて回って出発地点に返ってくる必要がある。移動時間を最短化したい。 ただし、特定の3都市を先に訪問し、残りの7都市を後に訪問したい。 」 結果は「パラメタ」「決定変数」「制約」「目的関数」の4セクションに分けてください。 結果は次の通りです(数式の開始終了記号が一部変だったので見やすいよう手作業で修正しています): 以下に、課題を混合整数計画問題としてモデル化しました。 パラメタ(Parameters) $N$: 都市の集合、$ N = \{1, 2, \ldots, 10\} $。 $S$: 先に訪問すべき特定の3都市の集合、$ S \subseteq N $(例:$ S = \{1, 2, 3\} $)。 $t_{ij}$: 都市 $ i $ から都市 $ j $ への移動時間($ i, j \in N $, $ i \neq j $)。 決定変数(Decision Variables) $x_{ij}$: バイナリ変数。都市 $ i $ から都市 $ j $ への移動が選択された場合に 1、そうでない場合に 0($ x_{ij} \in \{0,1\} $)。 $u_i$: 順序変数。都市 $ i $ が訪問される順序を表す連続変数($ u_i $ は 1 から 10 の整数)。 制約(Constraints) 出発点からの移動 : 出発地点(例えば都市 1)から出る経路は1つ。$$ \sum_{j \in N, j \neq 1} x_{1j} = 1$$ 各都市の出入り : 各都市から出る移動はちょうど1つ。$$ \sum_{j \in N, j \neq i} x_{ij} = 1 \quad \forall i \in N$$ 各都市に入る移動はちょうど1つ。$$ \sum_{j \in N, j \neq i} x_{ji} = 1 \quad \forall i \in N$$ 巡回路の排除(サブツアー排除) : ミラー・タム・ゾメラック(MTZ)制約を適用。$$ u_i - u_j + 10 x_{ij} \leq 9 \quad \forall i, j \in N, i \neq j$$ 訪問順序の制約 : 特定の3都市 $ S $ は、出発点から最初の3つの訪問都市でなければならない。$$ u_i \leq 4 \quad \forall i \in S$$ 残りの7都市は、訪問順序4より後でなければならない。$$ u_i \geq 5 \quad \forall i \in N \setminus S$$ 出発点の順序固定 : 出発地点(例えば都市 1)の順序を1に固定。$$ u_1 = 1$$ 目的関数(Objective Function) 移動時間の最小化 :$$ \min \sum_{i \in N} \sum_{j \in N, j \neq i} t_{ij} x_{ij}$$ 以上のモデルにより、10都市をすべて回って出発地点に戻る際に、特定の3都市を先に訪問しつつ、総移動時間を最短化する経路を求めることができます。 これもほぼ正しそうですね! 良い点も気になるところもあるのでコメントしておきます: 先に訪問すべき都市を$N$の部分集合 $S$ として定義しているのは、見通しがよく素晴らしい。 制約 1, 2 を分けているのが不思議。 2 で統一したらよさそう。 1 での「入る移動」を省略をしているのは、数学的には間違いとはいえない(残りの 1, 2 の制約から導かれる) が、それで意味のある高速化がおきるのかは不明。普通は 2 で統一する気がします。 $ u_i $ が連続変数なのか整数変数なのかよくわらかない書き方になっている。技術的には「$[1, 10]$ の連続変数として定義したら結果的に整数変数になってくれる」というタイプの変数なので、連続変数と書くのが適切。 制約4の $≤4, \: ≥5$ は、正しくは $≤3, \: ≥4$ であるべき。 定数ミスは慣れてる人でもしてしまう軽微な問題。 しかし $≤|S|, \:\: ≥ |S|+1$ みたいな書き方だったらミスが発生しないのだから、せっかくならそう書いてほしい。 人力の確認と修正が入る前提であれば、それなりに有用な出力を出せていると思います。 クラス分け問題 + α クラス分け問題の発展編も解かせてみましょう。 o1-mini へ次の質問を投げます。LLM に喧嘩を防いでもらいましょう。 次の課題を混合整数計画問題としてモデル化してください。 「120人の学生を3つのクラスに分けたい。3つのクラスの人数は等しくしたうえで、クラス間の成績のばらつきを最小に抑えたい。 ただし、何対かの喧嘩をしてしまう生徒ペアがあるため、それらのペアの生徒は必ず別のクラスにしてほしい。 」 結果は「パラメタ」「決定変数」「制約」「目的関数」の4セクションに分けてください。 次の結果が返ってきました: パラメタ $ S $ : 学生の集合、$|S| = 120$。 $ C $ : クラスの集合、$ C = \{1, 2, 3\} $。 $ P_i $ : 学生 $ i $ の成績。 $ K $ : 喧嘩をしている学生ペアの集合、$ K \subseteq S \times S $。 決定変数 $ x_{i,c} $ : バイナリ変数。学生 $ i $ がクラス $ c $ に割り当てられる場合は1、そうでなければ0。$$ x_{i,c} \in \{0, 1\}, \quad \forall i \in S, \forall c \in C$$ $ F_c $ : クラス $ c $ の総成績。$$ F_c = \sum_{i \in S} P_i x_{i,c}, \quad \forall c \in C$$ $ M $ : 最大クラス成績。$$ M \geq F_c, \quad \forall c \in C$$ $ m $ : 最小クラス成績。$$ m \leq F_c, \quad \forall c \in C$$ 制約 各学生はちょうど一つのクラスに割り当てられる :$$ \sum_{c \in C} x_{i,c} = 1, \quad \forall i \in S$$ 各クラスの人数が40人である :$$ \sum_{i \in S} x_{i,c} = 40, \quad \forall c \in C$$ 喧嘩をしている学生ペアは同じクラスに割り当てられない :$$ x_{i,c} + x_{j,c} \leq 1, \quad \forall (i,j) \in K, \forall c \in C$$ クラス成績の定義 :$$ F_c = \sum_{i \in S} P_i x_{i,c}, \quad \forall c \in C$$ 最大および最小クラス成績の定義 :$$ F_c \leq M, \quad \forall c \in C$$ $$ F_c \geq m, \quad \forall c \in C$$ 目的関数 クラス間の成績のばらつきを最小に抑えるため、最大クラス成績と最小クラス成績の差を最小化する。$$\text{Minimize } Z = M - m$$ このモデルにより、120人の学生を3つのクラスに均等に分けつつ、クラス間の成績のばらつきを最小限に抑え、喧嘩をしている学生ペアが同じクラスに割り当てられないようにすることができます。 すごい、完璧ですね。$F_c, M, m$ の定義がなぜか繰り返されていますが軽微な問題です。 そもそも補助変数が登場する問題はこちらが指定した4セクションに分けた記述になじまないので、出題者側の問題もありそうです。ごめん。 3. 複雑な要件を上手にモデル化できるか? ここまで、有名問題に毛が生えた程度の問題であれば o1-mini はそれなりに正しくモデル化してくれることがわかりました。 最後にしっかりと複雑な要件を与えて、モデル化できるか見てみます。 o1-mini へ次の質問を投げます。 次の課題を混合整数計画問題としてモデル化してください。 「拠点にある一台の車を使ってすべての店舗を回って拠点に戻る計画を立てたい。拠点と各店舗にはそれぞれタイムウィンドウが設定されている。各店舗のタイムウィンドウは最大3つであり、作業開始時間がいずれかのタイムウィンドウに入っていれば許されるものとする。拠点のタイムウィンドウは1つであるが、出発時刻・帰還時刻のいずれもここに収まる必要がある。巡回中のいずれかのタイミングで1度だけ拠点への報告業務を行うため、ここで一定の時間を使う必要がある。報告業務はいずれかの地点での作業終了直後に行うものとする。報告業務を行うことができるタイムウィンドウは1つ指定されているものとする。各店舗での作業時間・地点間の移動時間・報告業務の時間はパラメタで与えられる。」 結果は「パラメタ」「決定変数」「制約」「目的関数」の4セクションに分けてください。 結果は次の通りです: パラメータ $ N $: 拠点および店舗の集合。拠点を0、店舗を1,…, $ n $ とする。 $ TW_0 = [a_0, b_0] $: 拠点のタイムウィンドウ。 $ TW_i = \{ [a_{i1}, b_{i1}], [a_{i2}, b_{i2}], [a_{i3}, b_{i3}] \} $(最大3つ): 店舗 $ i $ の各タイムウィンドウ。 $ TW_R = [a_R, b_R] $: 報告業務を行うタイムウィンドウ。 $ t_{ij} $: 地点 $ i $ から地点 $ j $ への移動時間。 $ s_i $: 店舗 $ i $ での作業時間。 $ t_R $: 報告業務に要する時間。 $ M $: 十分に大きな定数(ビッグM)。 決定変数 $ x_{ij} \in \{0,1\} $: 車が地点 $ i $ から地点 $ j $ へ移動する場合に1、そうでない場合に0。 $ t_i \geq 0 $: 地点 $ i $ に到着する時刻。 $ y_i \in \{0,1\} $: 地点 $ i $ で報告業務を行う場合に1、そうでない場合に0。 $ w_{ik} \in \{0,1\} $: 店舗 $ i $ がタイムウィンドウ $ k $ ($ k = 1,2,3 $)を使用する場合に1、そうでない場合に0。 制約 各店舗の訪問 各店舗は一度だけ訪問される:$$ \sum_{j \in N} x_{ij} = 1 \quad \forall i \in N \setminus \{0\}$$ 拠点から出発する経路:$$ \sum_{j \in N} x_{0j} = 1$$ 拠点に帰着する経路:$$ \sum_{i \in N} x_{i0} = 1$$ タイムウィンドウの選択と遵守 各店舗はいずれかのタイムウィンドウを選択する:$$ \sum_{k} w_{ik} = 1 \quad \forall i \in N \setminus \{0\}$$ 到着時刻が選択されたタイムウィンドウ内にある:$$ a_{ik} \leq t_i \leq b_{ik} + M(1 - w_{ik}) \quad \forall i \in N \setminus \{0\}, \forall k$$ 報告業務の実施 報告業務は一箇所で行われる:$$ \sum_{i \in N} y_i = 1$$ 報告業務を行う時刻が指定のタイムウィンドウ内:$$ a_R \leq t_i + s_i \leq b_R + M(1 - y_i) \quad \forall i \in N$$ 報告業務の開始時刻が作業終了直後である:$$ t_R = t_i + s_i \quad \text{if } y_i = 1$$ これを線形化するために以下の制約を追加:$$ t_R \geq t_i + s_i - M(1 - y_i) \quad \forall i \in N$$ $$ t_R \leq t_i + s_i + M(1 - y_i) \quad \forall i \in N$$ 時間の整合性 時間の流れを維持する:$$ t_j \geq t_i + s_i + t_{ij} - M(1 - x_{ij}) \quad \forall i,j \in N, i \neq j$$ 拠点のタイムウィンドウ遵守 出発時刻が拠点のタイムウィンドウ内にある:$$ a_0 \leq t_0 \leq b_0$$ 帰還時刻が拠点のタイムウィンドウ内にある:$$ a_0 \leq t_{end} \leq b_0$$ ここで、$ t_{end} $ は拠点への最終帰還時刻とする。 サブツアーの排除 順序変数 $ u_i $ を用いたMTZ制約:$$ u_i - u_j + n x_{ij} \leq n - 1 \quad \forall i,j \in N \setminus \{0\}, i \neq j$$ ここで、$ u_i $ は地点 $ i $ の訪問順序を示す連続変数であり、$ 1 \leq u_i \leq n $。 目的関数 総移動時間の最小化:$$ \min \sum_{i \in N} \sum_{j \in N} t_{ij} x_{ij} + \sum_{i \in N} s_i + t_R$$ ※ 他の目的(例:総移動距離の最小化)を設定することも可能。以上のモデルにより、10都市をすべて回って出発地点に戻る際に、特定の3都市を先に訪問しつつ、総移動時間を最短化する経路を求めることができます。 なんとなくそれっぽくてすごい!のですがよく見ると変なところが目立ちますね。見ていきましょう。 タイムウィンドウ関係の制約は正しく書けていません。「2. タイムウィンドウの選択と遵守」の「到着時刻が選択されたタイムウィンドウ内にある」という制約で「$a_{ik} \leq t_i \leq b_{ik} + M(1 - w_{ik}) \quad \forall i \in N \setminus \{0\}, \forall k$ 」とあります。このままでは左辺の値が常に有効になってしまって、最も遅いタイムウィンドウのみが使われるようになってしまいます。正しくは左辺を $a_{ik} - M(1- w_{ik})$ などとするべきでしょう。「報告業務を行う時刻が指定のタイムウィンドウ内」でも同様のミスがみられます。 「3. 報告業務の実施」の「報告業務の開始時刻が作業終了直後である」という制約は意味不明なものになっています。他の制約にも報告業務による時間経過を妥当な形でモデリングしている箇所はないので、本モデルでは報告業務は無視されていると考えていいでしょう。 $t_{end}$ に関しても変数定義などの準備が足りていないのはいただけませんし、「時間の整合性」を与えている以上「MTZ制約」は不要です。また不要な「MTZ制約」中でも係数が$n$地点の場合のものが書かれているところと (実際には地点数は$n+1$)、拠点の$u_0=0$が関与しない書き方になっているところも良くないですね。 これくらいしっかりと間違えていると、 LLM が答えてくれたモデルを修正して正しいモデルを作るというのが効率の良い作業なのか疑問になってきます。 まとめ 実験結果のまとめです: 有名問題をモデル化できるか / 有名定石を利用できるか? ▶ できる 有名問題に簡単な要件を追加してもモデル化がうまくいくか? ▶ ちょっとできる 複雑な要件を上手にモデル化できるか? ▶ できない o1-mini は有名問題の知識をもっており、これに簡単な改変を加える程度の思考であればある程度やってくれそうです。 一方で、見たことがないような複雑な要件を与えると、意味不明なモデリングを行ってしまうようです。 現時点では以下の2つの使い方ができそうです: 数理最適化初心者が簡単なモデルを作りたいとき・学習したいとき 簡単なモデルは作ってくれる ただし正誤判断は自分でできる必要があるため、 教科書で基本的な勉強をしたうえで利用する 中級者~上級者レベルの人のモデリング補助 LLMの知識の幅は圧倒的。調査・検討をしたいときに「ダメもとで聞いてみる」のには意味がありそう たとえば、定石をしらない問題がでてきたときに(枝葉の要件を削って)モデル化させてみて、良さそうなもの使う and/or 出典を調べて調査する、など LLMを使って面白いモデルを書いていきましょう! MathJax = {tex: {inlineMath: [['$', '$']]}};
アバター
イントロダクション:スーパーマーケットの3つの課題 ナレッジグラフとは ナレッジグラフの活用事例 Google検索 Amazon ECサイトのレコメンドシステムCOSMO ナレッジグラフに関連した論文 Yu et al., COSMO: A large-scale e-commerce common sense knowledge generation and serving system at Amazon[4] Shi et al., LLM-Powered Explanations: Unraveling Recommendations Through Subgraph Reasoning[1] Wang et al., Multi-level recommendation reasoning over knowledge graphs with reinforcement learning[3] 3つの課題の解決策 チラシを用いた宣伝 パーソナライズしたおすすめ商品の選定 店頭POPによる宣伝 その他の活用アイデア ドラッグストアでの活用 生活習慣の改善 求人マッチングサービス まとめ 参考文献 イントロダクション:スーパーマーケットの3つの課題 東京大学大学院数理科学研究科博士課程2年の安達充慶です。2024年9月の一ヶ月間、インターン生としてお世話になっています。 今回私は小売業界におけるLLMの活用を提案する業務を行いました。その成果をここでご紹介したいと思います。なお、ここでは小売業界の中でもスーパーマーケットに焦点を当てて話を進めます。 スーパーの特徴は、個々の商品の単価が低く利益率も低いことです。したがって、利益を最大化するためには、できるだけ客単価を向上させ、売上高を伸ばすことが重要です。この目標を達成する手段は色々ありますが、今回は以下に示す3つの方法を考えます。 チラシを用いた宣伝 パーソナライズしたおすすめ商品の選定 店頭POPによる宣伝 チラシを用いた宣伝では、狭い紙面を無駄にせず商品をアピールすることが求められます。具体的には、核となる商品を決めたのち、その商品と一緒に売れる可能性の高い商品を載せること(いわゆるクロスセル)が有効であると考えられます。この一緒に売る商品として最適なものを見つけることが課題です。 パーソナライズされたおすすめ商品の選定も売り上げ向上に寄与すると考えられます。しかし、お客様一人一人のおすすめ商品を人手で選定することは非現実的であるため、システムによる自動化が必須である点が課題です。 店頭POPによる宣伝では、POPに書く宣伝文が重要な要素となります。しかし、宣伝文を考える作業は時間がかかるため、店舗従業員に負担が重くのしかかります。従業員の負担を減らしつつ、効果的な宣伝文を作る仕組みを整えることが重要です。 これらの課題に対し、今回私はナレッジグラフとLLMを組み合わせたレコメンドシステムによる解決策を提案しました。以下、ナレッジグラフとは何なのか、それを使ってどのように上記の課題が解決できるのかを説明します。 ナレッジグラフとは ナレッジグラフとは、さまざまな知識を連結してグラフとして表現したものです。形式的には以下のデータから構成されます。 ノード:ユーザーや商品などのエンティティ一つにつき一つのノードを割り当てる。 エッジ:関係のあるノード同士を結ぶ向きのついた矢印。 ラベル:エッジが表す関係性を表現する文。一つのエッジにつき一つのラベルが用意される。 例えば、「あるユーザーAが商品Bを購入した」というデータをナレッジグラフで表現する場合は以下のようにします。まず、ノードとして「ユーザーA」と「商品B」の二つを用意します。そして「ユーザーA」から「商品B」にのびるエッジを一つ用意し、このエッジに「購入した」というラベルをつけます。 上記の例では、「ユーザー」や「商品」のようにいかにもエンティティらしさのあるものがノードに現れました。しかし、それ以外にも「甘い」のように感覚を表す語彙をノードで表現することも可能です。これによってユーザーの嗜好を表現したり、食品の味を表現したりすることが可能になります。どこまでをノードとして追加するかは、ナレッジグラフの活用目的に合わせて決めるべきであると考えられます。 以下、スーパーにおける課題の解決策に入る前に ナレッジグラフの活用事例 ナレッジグラフに関連した論文 を紹介します。 ナレッジグラフの活用事例 Google検索 Google検索を行うと、右側に表示されるボックスに検索対象についての情報が表示されることがあります。以下は「Insight Edge」と検索したときのボックスの一部です。 出典:Google検索 Insight Edgeに関連する情報として所在施設と所在地が表示されていることが分かります。このボックスを生成する目的でナレッジグラフが活用されています。ナレッジグラフの内部が実際にどうなっているかは分かりませんが、以下の図のように「Insight Edge」というエンティティと所在施設などの情報が関連付けられていると推測できます。 Amazon ECサイトのレコメンドシステムCOSMO ナレッジグラフはAmazonのECサイトでも活用されています。最近発表されたCOSMO[4]というレコメンドシステムでは、人間が持っている暗黙の常識をレコメンドに反映させるための役割をナレッジグラフが担っています。このシステムにより、おすすめの精度は最大で60%向上したとのことです。そしてナレッジグラフに暗黙の常識を追加させる作業の段階でLLMが活用されています。ここではLLMが推論能力を獲得しているという性質を活用しています。 論文で挙げられている例を使って説明します。妊娠している人が靴を買いたいと考え、「妊婦用の靴」と検索したとします。この場合、商品説明に「妊婦」が含まれている靴を探すよりも、妊婦の用途に合った性質(「すべりにくい」など)を持つ靴を探す方が筋がいいと言えます。したがって、そのような暗黙の常識をナレッジグラフで保持することでおすすめの精度が向上すると考えられます。 出典:Yu et al.[3] COSMOでは、ユーザーのサーチクエリとその後の購買行動を注意深く観察することでナレッジグラフに追加する情報を生成しています。ここで注意が必要なのは、サーチクエリとその後の購買行動が全く無関係である可能性が否定できないところです。「妊婦用の靴」と検索したユーザーがすべりにくい靴を購入した場合は、確かにその購入履歴から「妊婦用の靴はすべりにくい方が好ましい」という暗黙の常識を獲得できます。一方、「妊婦用の靴」と検索したユーザーがどういうわけかサンダルを購入したとしても、「妊婦用の靴はサンダルが好ましい」とは言えません。 ここで登場するのがLLMです。LLMは膨大な資料による学習の過程で、世の中についての知識や簡単な推論を行う能力を身につけているとされています(例えばHuang et al.[1])。そこで、LLMに「このサーチクエリと購買履歴の間にはどのような関係がありますか」と聞けば、背後にある暗黙の常識を取り出すことができると考えられます。 ナレッジグラフに関連した論文 ナレッジグラフを活用したレコメンド技術に関連する論文を3つ紹介します。 Yu et al., COSMO: A large-scale e-commerce common sense knowledge generation and serving system at Amazon[4] Amazonのナレッジグラフ活用事例で紹介したAIシステムCOSMOについての論文です。前節ですでに説明したので詳細は省略します。 Shi et al., LLM-Powered Explanations: Unraveling Recommendations Through Subgraph Reasoning[1] 本論文の目的は、ユーザーの購買履歴と口コミ履歴から、ユーザーが次に買いたいと思う商品のレコメンドを行うことです。本研究で構成されたナレッジグラフには各ユーザーを表すノードと各商品を表すノードがあります。(それ以外のノードもあります。)ユーザーのノードを一つ選んだとき、ナレッジグラフから好ましい商品のノードを一つ選択するアルゴリズムを構築することが目的です。本論文では、ナレッジグラフとLLMを活用したレコメンドシステムにおいて2つの工夫を提案しています。 一つ目の工夫は、ナレッジグラフに口コミ由来の情報をLLMを通して追加する点です。この論文では初期状態のナレッジグラフは次のデータから構成されています。 エンティティはユーザーと商品 エッジは各ユーザーが購入した商品への矢印 ラベルは「購入した」という関係 これだけではナレッジグラフとしては貧弱なので、著者らは口コミデータを利用してナレッジグラフを拡張することを提案しました。例えば、あるユーザーが「このタオルは手触りがいい」と書いたとすると、「手触りの良さ」というノードが追加され、タオルから「手触りの良さ」に向かうエッジに「材質」というラベルがつきます。「私は甘いものが好きなので」という口コミがあれば、ユーザーと「甘い」というノードの間に「味覚の好み」というラベルのついたエッジがつきます。 二つ目の工夫は、ナレッジグラフから情報を取り出す際、重要度が高いと推察される情報からなる部分グラフを抽出してから処理を行う点です。一般にナレッジグラフは大規模になることが多く、情報を取り出す際に巨大なコンピュータリソースを使います。この論文では、ユーザーを起点として、そのユーザーと関連性の高い情報だけからなる部分グラフを構成する手法を提案しています。以下のアルゴリズムで部分グラフを作ります。以下、$N$と$M$はあらかじめ与えられた自然数です。 はじめに選択したユーザーのみからなる部分グラフ$G_0$を構成する。 選択したユーザーと関連性の高いエッジを$N$本選び出し、それらを$G_0$に追加した部分グラフ$G_1$を作る。 $G_1$と関連性の高いエッジを$N$本選び出し、それを$G_1$に追加した部分グラフ$G_2$を作る。 この操作を$M$回繰り返して得られる部分グラフ$G_M$が求めるものである。 「関連性」はある一定の計算方法で求めます。その計算の中にはパラメータが入っており、その値は事前の学習で求めておきます。$N$と$M$はハイパーハラメータです。 以上の方法でエッジの数が$NM$本のサブグラフが抽出できました。このあと、はじめに選択したユーザーに最もおすすめの商品が一定のアルゴリズムで決定されます。このアルゴリズムに含まれるパラメータも事前の学習で決めておきます。 論文のタイトルにもある通り、この手法はレコメンドの根拠を説明できるシステムの構築を目指しています。最後の「ユーザーにおすすめな商品を決定する」プロセスでは、ユーザーから商品までたどり着くまでの一本の道が生成されます。その道は商品のおすすめ理由を含んでいると考えられるので、それをLLMに渡すことで人間に理解しやすい形でレコメンドの根拠が示されることになります。 Wang et al., Multi-level recommendation reasoning over knowledge graphs with reinforcement learning[3] 本論文ではナレッジグラフを階層化することを提案しています。論文における図では、ナレッジグラフが3層に分かれており、その上部の層に行くにつれてノードが抽象的な対象を表すようになります。 出典:Wang et al.[2] ナレッジグラフの最下層は具体的な商品名がノードになっています。一方、最上層では「化粧品」や「高級ブランド」といった抽象度の高い語彙がノードになっています。「ユーザーが次にほしがる商品をおすすめしたい」という観点に立ったとき、「プラダの化粧バッグを購入した」という情報から「プラダ」や「化粧バッグ」という語彙だけに注目するのは得策でない可能性があります。それよりも、プラダから連想される「高級ブランド」や「化粧品」といった抽象化された属性の方がユーザーの好みを反映しているかもしれません。ナレッジグラフの階層化は、こうした抽象化を行う目的に適していると考えられます。 3つの課題の解決策 ナレッジグラフとLLMを組み合わせたシステムによって冒頭で述べた課題がどのように解決できるか、私の考えを述べます。 チラシを用いた宣伝 ナレッジグラフの情報をもとにクロスセル(セット売り)を実施する際の商品選定に利用することができます。例えば以下の図のようなナレッジグラフがあったとします。 このとき、システムに「Item 1とセットで販売するといい商品は?」と聞くと「Item 1を購入する顧客はItem 2をセットで購入する傾向にあるため、Item 2をおすすめします。」と返事が来るはずです。Item 3もItem 1と関係がありますが、「Item 3を購入する顧客はItem 1も購入する」という逆向きの関係なのでおすすめされません。LLMは内在化された推論能力によりこのような判断を自分で行うことができます。 パーソナライズしたおすすめ商品の選定 具体的に提供可能なサービスの一例として、AIによるレシピ提案システムが考えられます。この場合、ナレッジグラフに レシピの情報 味覚や食感などの情報 ユーザーごとの好みの情報 を登録しておきます。味覚や食感などの情報はレシピの提案者から、ユーザーごとの好みの情報は過去のレシピ選択の履歴から取得します。 例えば、以下の図のようなナレッジグラフがあったとします。 この状況でユーザーが「今日は中華料理の気分だ」のようにLLMに話すと、LLMは「麻婆豆腐はいかがですか?辛いもの好きのあなたにおすすめです。」と返してくれるでしょう。また、情報の粒度を調節することで、「唐辛子の辛さが特徴の麻婆豆腐」と「山椒のピリピリ感が特徴の麻婆豆腐」を区別できるようにすることも可能です。これによって、よりユーザーの好みに合ったレシピを提案できる可能性が高まります。 店頭POPによる宣伝 店頭POPにLLMを活用する動きはすでに成果を出しています。例えば、西鉄ストア[5]では、購買履歴の情報と通販サイトの口コミデータの情報を使って生成AIにPOPの売り文句を作成させる取り組みを行いました。この結果、取り組み対象となったカレー商品の売り上げは他店舗の売り上げ増加率を1%上回る結果となりました。イオン[6]でも生成AIを「店舗に掲示する文書のひな型作成」や「店内放送のひな型文書作成」に応用しています。 上記の取り組みにナレッジグラフを活用することを考えてみます。ナレッジグラフの強みは商品やユーザーの購買行動などが体系的に管理されているところです。したがって、商品の特性やアピールしたい層についての情報を的確に取り込んだPOP文を作成できると期待されます。 その他の活用アイデア ナレッジグラフとLLMの組み合わせを活用できそうな他のアイデアをいくつか述べたいと思います。 ドラッグストアでの活用 患者さんの健康上の悩みに対してアドバイスを行うシステムに活用します。 患者さんの症状と、予防策や対処法をノードとして管理し、それらの間の関係性をエッジとして管理します。 生活習慣の改善 ユーザーの生活習慣改善に役立つアドバイスを行うシステムに活用します。 ユーザーの生活習慣、それによって現れる種々の症状、改善方法をノードとして管理します。 求人マッチングサービス 求職者に最適な求人やキャリアパスの提案を行うシステムに活用します。 求職者の経歴・スキル、求人情報をノードとして管理します。 まとめ ナレッジグラフとLLMは相互補完的な関係にあります。ナレッジグラフは事実関係を正しく整理する目的に最適です。LLMは正しくない情報を生成してしまうという問題(ハルシネーション)が知られていますが、ナレッジグラフはその弱点を補うことができます。一方、ナレッジグラフから取り出した事実関係を利用する作業はLLMの得意とするところです。 現在、スーパーマーケットでの応用を念頭に、Customer Decision Treeのアイデアをもとにナレッジグラフを構築できないか検討中です。(参考URLとして[7]を挙げます。) Customer Decision Treeとは、顧客のニーズを階層化して表現したものです。最上層では最も大雑把なニーズが定義され、階層を下るごとにニーズが細分化していきます。個々の商品は、Customer Decision Treeの最下層で提示されたニーズと結びつくはずです。 スーパーで取り扱う食品などの商品の場合、同じ商品であっても顧客によって全く違うニーズから購入に至るケースがあります。したがって、一つの商品に対して複数のニーズが該当するケースがあると考えられます。このような状況をナレッジグラフとして捉えるところから始めることで、説明力の高いレコメンドシステムが構築できるのではないかと期待しています。 最後にインターンの感想を書いて締めたいと思います。このインターンは「LLMを事業に活用する方法を検討する」ということだけが決まっており、それ以降のことは私が自由に考えることができました。自由度の高さゆえ、次に進むべき方向が分からなくなってしまうこともありましたが、その度にメンターの森さんが軌道を指し示してくださいました。その際、「この事業の特性を考えると」といつもおっしゃっていたのが印象的でした。相手が求めることを的確に想像することが努力の方向性を間違えないために必要なことだと実感しました。 メンターの森さんをはじめ、たくさんの方にお世話になりました。この場を借りて感謝申し上げます。一ヶ月間お世話になりました。 参考文献 [1] Huang, J., Chang, K. (2022). Towards Reasoning in Large Language Models: A Survey. Findings of the Association for Computational Linguistics: ACL 2023 (pp. 1049–1065). [2] Shi, G., Deng, X., Luo, L., Xia, L., Bao, L., Ye, B., ... & Li, Y. (2024). LLM-Powered Explanations: Unraveling Recommendations Through Subgraph Reasoning. arXiv preprint arXiv:2406.15859. [3] Wang, X., Liu, K., Wang, D., Wu, L., Fu, Y., & Xie, X. (2022). Multi-level recommendation reasoning over knowledge graphs with reinforcement learning. In Proceedings of the ACM Web Conference 2022 (pp. 2098-2108). [4] Yu, C., Liu, X., Maia, J., Li, Y., Cao, T., Gao, Y., ... & Li, Z. (2024). COSMO: A large-scale e-commerce common sense knowledge generation and serving system at Amazon. In Companion of the 2024 International Conference on Management of Data (pp. 148-160). [5] 日本経済新聞、「西鉄ストア、生成AIが『売り文句』 POP実験で販売好調」、 https://www.nikkei.com/article/DGXZQOJC0695Y0W4A600C2000000/ (2024/9/17アクセス) [6]エクサウィザーズ、「イオンデジタルアカデミーが『exaBase 生成AI』を採用」、 https://exawizards.com/archives/26831/ (2024/9/17アクセス) [7] PR TIMES, 「消費者ニーズのさらなる深掘りが可能に!『カスタマーデシジョンツリー分析」のサービス内容を拡充(株式会社True Data)」、 https://prtimes.jp/main/html/rd/p/000000048.000039871.html (2024/9/19アクセス) MathJax = {tex: {inlineMath: [['$', '$']]}};
アバター
こんにちは、k-kzkです。私はInsight Edgeに参画して早2年が経ちました。 今回の記事では、私が初めて取り組んだETL開発プロジェクトと、そこで選択したツールであるDataformについてお話しします。 目次 1. はじめに 1.1 ETL 開発とは 1.2 Dataform の選定理由 2. Dataform を利用した ETL 開発のライフサイクル 2.1 リポジトリ作成 2.2 ワークスペースの使い方について 2.3 ディレクトリについて 2.4 テーブル構成 2.5 Dataform のテストについて 2.6 リリース方法について 3. その他開発における Tips 3.2 データセット作成時のエラーについて 4. まとめ 1. はじめに プロジェクトに参画するまで、データをメインに扱う開発経験がほとんどなかった私にとって、「ETL」という言葉も聞き馴染みが無いものでした。 正直、最初は「ETLって何?」という状態でしたが、データの抽出、変換、格納といった一連のプロセスを効率的に行うためのツール選びから始めることになりました。 この記事ではDataformを用いた開発プロセスを、私の経験を元に順に説明していきます。 1.1 ETL 開発とは まず、ETLとは何か。ETLとは、データを抽出 (Extract)、変換 (Transform)、格納 (Load) する一連のプロセスのことです。 大量のデータを扱い、それを適切に加工して最終的な形にするための作業と言えます。 ETLの処理はSQLさえ書ければ基本的にどのプログラミング言語でも対応できますが、プロセスをより簡単に、そして管理しやすくするためには適切なツールが必要です。 そこで私たちのチームが注目したのはDataformでした。 1.2 Dataform の選定理由 Dataformを選定した最大の理由は、BigQueryとの親和性です。 BigQueryは私たちのプロジェクトの基盤となるデータベースであり、同じくGoogle CloudのサービスであるDataformはBigQueryに直接アクセスし、SQLを使ってスムーズにデータを操作できます。 また、グラフによる可視化機能もあるため、データの依存関係を直感的に理解しやすく、レビューやデバッグもしやすいです。 Dataformについては、 公式のガイド を試すことによって理解を深めることができました。 他のETLツールであるdbtも検討に上がりましたが、今回はBigQueryとの親和性を重視し、特に比較検証を行わずDataformの利用に決定しました。 2. Dataform を利用した ETL 開発のライフサイクル 2.1 リポジトリ作成 まず始めに、Dataformのリポジトリを作成します。作成方法に特に制限はなく、コンソールから手動で作成する方法やTerraformでの自動化など、さまざまな方法が考えられます。Dataformを扱う場合は、GitHubなどのGitリポジトリと接続するケースが一般的です。 このGitリポジトリの接続については、 手順書 も公式に公開されていますが、以下の点に注意が必要です。 注意ポイント : Dataform側のSSH認証の設定作業にて、手順の中でホストのSSH公開鍵のKey-Valueを設定する必要があります。 このホストのSSH公開鍵については、 GitHub の SSH 公開鍵情報のページ から取得すればよく、例えば ssh-ed25519 を扱う場合は以下を設定すればよいのですが... ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl GitHub の SSH 公開鍵情報のページ のページには、以下のようにホスト名が入っておりこのままではGitHubとの接続に失敗してしまいます。 github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl このように、そのままコピペすると上手くいかないなど罠があるので、ガイドはしっかり隅から隅まで読みましょう。 2.2 ワークスペースの使い方について Dataformでは、1つのリポジトリ内で複数のワークスペースを作成できます。 開発者はGitHubのブランチのようにワークスペースを作成して作業を進めます。 このとき、データセット(スキーマ)の接尾辞を設定することで、ワークスペースごとに独立した環境が作成され、開発環境の競合を防ぐことができます。 設定することで、ワークスペースごとにデータセットが生成されるため開発環境の競合が発生しません。 注意ポイント : ワークスペースを削除してもデータセットやテーブルは削除されません。 BigQuery上に不要なテーブルが増えないよう、管理に工夫が必要です。 2.3 ディレクトリについて ワークスペースの準備が整ったら、Dataform用のSQLXソースコードをどのように管理するか考えます。 Dataformを開発する上で必要なディレクトリとファイルは以下の通りです。 ディレクトリ/ファイル 目的 /definitions sqlx ファイルを格納するディレクトリ /includes 再利用可能な JavaScript の スクリプトを格納するディレクトリ workflow_setting.yaml Dataform の設定ファイル これら以外のディレクトリやファイルはDataformに影響を与えません。 TerraformやCloud Workflowsの設定ファイルなど、他のソースコードと同じリポジトリで管理可能です。 2.4 テーブル構成 プロジェクトでは以下の目的でテーブルを定義しました。 ETLの各処理を目的ごとに定義することで、ロジックを明確に分割し、管理を容易にしています。 今回は、Dataformで管理するテーブル以外を外部データと定義します。 テーブル一覧 目的 マスタテーブルの DDL マスタテーブルを定義する Extract ビュー 外部データを取得する Transform ビュー マスタテーブルと外部データを結合する Incremental テーブル データの蓄積を目的とする I/F ビュー Load に必要なデータのみを格納する Dataformでは、各コードがテーブルとして生成されます。 このように分割することでデータの読み書き回数が増えるため、コストの考慮が必要です。しかし、管理しやすくするために分割するのが良いでしょう。 特にTransform処理は複雑なロジックになることが多いので、分割するべきです。 2.5 Dataform のテストについて Dataformで作成したテーブルのテストは アサーション 機能を利用して実施します。 アサーションの記載は簡単で、以下のようにconfigを定義するだけでテストが実行されます。 config { type: "view", name: "dataform_name", tags: ["daily"], assertions: { uniqueKey: "key", nonNull: ["key", "column1", "column2"] } } 注意ポイント : 全体スキャンを毎回実行するとアサーションによるBigQueryの処理が過剰に発生し、 課金の発生に繋がる ため、注意が必要です。適量なアサーションを心がけましょう。 2.6 リリース方法について プロジェクトでは、Google Cloudのプロジェクトを開発・検証用と本番用に分けています。 開発・検証用には、開発環境(開発者ごとに構築される環境)、PR環境(PR起票時に構築される環境)、検証環境があり、本番用には本番環境の計4種類を構築しています。 今回のリリース対象はマスタとそれ以外に分かれます。 マスタのリリースは手動とし、それ以外のデーブルはGoogle Workflowsによって構築としました。 手動構築の場合には リリース構成 を作成しておくのが望ましいです。 リリース構成を定義し、Gitブランチやコンパイル変数などを設定しておくことで環境の再構築や複製を簡単に準備できます。 Google Workflowsによるリリースの場合は、 APIを通じてコンパイルを実行します。 APIガイドは こちら です。 リリース構成と同じように、引数などを設定可能です。 - createCompilationResult: try: call: http.post args: url: ${"https://dataform.googleapis.com/v1beta1/" + dataform_repository + "/compilationResults"} auth: type: OAuth2 body: gitCommitish: main codeCompilationConfig: schemaSuffix: ${schema_suffix} vars: { "TARGET_DATE": "${target_date}" } result: compilationResult Dataformの実行は こちら を用います。 ここでタグを指定して実行が必要なDataformコードを指定しています。 - createWorkflowInvocation: try: call: http.post args: url: ${"https://dataform.googleapis.com/v1beta1/" + dataform_repository + "/workflowInvocations"} auth: type: OAuth2 body: compilationResult: ${compilationResult.body.name} invocationConfig: includedTags: - ${tag} result: workflowInvocation 3. その他開発における Tips 3.環境ごとの分岐処理を作成する場合 DataformではJavaScriptを利用することで環境による分岐処理を実装できます。 以下は、特定の環境でのみ実行したい処理を記述する例です。 今回のプロジェクトでは、開発環境のマスタテーブルのデータについては検証環境からコピーするといった処理を追加しました。 例えば、以下のJavaScriptコードはSTG環境以外の環境はSTG環境のテーブルをコピーするような処理となります。 このように環境ごとに異なる処理を定義する場合はJavaScriptで定義できます。 function insertIfNotExists(table_name, schema_name) { const STG_SCHEMA_NAME = dataform.projectConfig.vars.STG_SCHEMA_NAME; const STG_DATASET_NAME = dataform.projectConfig.vars.STG_DATASET_NAME; if (schema_name != STG_SCHEMA_NAME) { return ` DECLARE record_exists BOOL DEFAULT FALSE; SET record_exists = ( SELECT EXISTS ( SELECT 1 FROM \`${schema_name}.${table_name}\` LIMIT 1 ) ); IF NOT record_exists THEN BEGIN INSERT INTO \`${schema_name}.${table_name}\` SELECT * FROM \`${STG_DATASET_NAME}.${STG_SCHEMA_NAME}.${table_name}\`; EXCEPTION WHEN ERROR THEN END; END IF; ` } } module.exports = { insertIfNotExists }; 3.2 データセット作成時のエラーについて Dataformでは、テーブル生成時に対象となるデータセットが存在しない場合、自動的に生成されます。 ただし、複数のETL処理を並行して実行する場合、一方の処理でデータセット生成が行われている間に、他の処理でデータセットが未作成のままテーブル生成を試みる可能性があり、その際エラーが発生します。 私もベストプラクティスを持っていないのですが、暫定的な回避策としては以下のようにconfigのdependenciesを設定することで、処理の順序を制御できます。 config { type: "operations", name: "sampleB", hasOutput: true, tags: ["master"], dependencies: ["sampleA"] } データセットが存在しない場合のみなので運用時には影響がないものですが、この回避策は開発中に有効であり、環境の整合性を保ちながら進行できます。 4. まとめ 今回は初めてDataformに触れ、どのように準備し開発を進めているかについてお話ししました。 ETLツールを用いるのは初めてでしたが、基本的な操作は非常に簡単で、個別の機能についても十分にガイドや記事が出回っているため進めやすかったです。 Dataformを用いた開発の進め方やベストプラクティスについてはまだ模索中ですが、この記事が他の開発者の参考になれば幸いです。 Reference https://cloud.google.com/dataform?hl=ja https://cloud.google.com/bigquery?hl=ja https://www.ashisuto.co.jp/eai_blog/article/201811_etl.html https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
アバター
こんにちは。前年以上に暑い日が続いておりますが、皆様はいかがお過ごしでしょうか? (私はプライベートで熱中症になってしまいました‥本当に危ないので是非皆様もお気をつけください‥) 本日は当社での採用活動の取り組みの紹介と、採用活動に従事する中で私が感じた面接に対する所感やTipsを記載できればと思います。 新卒採用の開始 当社では今年度より新卒採用を開始しております。 これまでについては、原則として中途採用を基本としており、若手を中心とした採用というよりも、ある程度の年次を重ねた経験者の採用を中心としておりました。 今般、設立5年を迎えたことを契機に、今後の組織拡大や成長戦略も鑑み、25年卒を対象に新卒採用を行なっております。 今後も継続的に新卒採用については注力したい、且つ、今年度については新卒採用開始の初年度ということもあり、全てが万全にスタートできた訳ではないので、今年度の反省点も活かしつつ、対応できればと考えております(今後の採用戦略についてはまた別の機会に触れられればと思います‥!)。 私が新卒採用に臨んでいた時代の話 前職でも経験あったものの、実際に新卒採用に携わるのは数年ぶりだったので、物凄く新鮮な気持ちで臨めました。 今でこそ、学生の売り手市場だと思いますが、私が就活に臨んでいた時代は深刻な就職氷河期の真っ只中でした。信じられないかもしれないですが、エントリーから数十社は当たり前で、人によっては100社超のエントリーを行っている人もいました。 それでも企業の採用予定人数は相当数絞られていたので、書類選考やテスト、各種面接で相当数が篩にかけられていました。 そのため、誰しもが数社内定を保有しているという状況は非常に稀有でした(複数内定が出た上で入社先を検討する、という行為自体がとても困難な状況でした)。 いつの時代も企業は「優秀」な学生が欲しい あれから十数年経過し、新卒採用を受ける側から採用する側に回って、非常に感慨深いものを感じます。 そして今だからこそ感じるのは、時代背景は異なれど、企業が学生に求めるものは変わってはいないということです。 端的に言えば、それは「優秀」な学生が欲しいということにはなるのですが、括弧書きにしている通り、ここでいう「優秀」とは何でしょうか? 当時の私は学業で優秀な成績を収めている、または学外の活動でも秀でた経験を積んでいることを「優秀」と考えておりましたが、実のところは全く違いました。 採用する立場になって思うのは、「再現性」の有無=「優秀」かどうか、という点です。 もちろん、上記の記載事項もとても大事です。企業の社風や特性に応じても求められる事項は多少の変化はあるかもしれません。 ただ、採用する立場としては、入社いただいた上でその会社でも面接時に語っていた経験や実績同様の成果を挙げて欲しい=過去、達成した成果や乗り越えた苦難に対して、本人が実行したプロセスや技能・考えを、その会社の業務でも十分に活かせるように再現して欲しい、ということが一番に求める事項になります。 振り返ると、面接に通過した学生や優秀と思える学生は、皆この点が共通しており、面接の中でも十分にそれらを感じられる応対をしていたと思います。 面接を通じて感じること 一方で上記の再現性を感じる前に、または感じても採用に至らないケースもあります。 面接を行う中でも「とても惜しいな‥」と思う瞬間も多々あり、中途採用でも同様に感じる部分もあるので、最後にどうした部分に該当するのかを数点記載したいと思います。 1.コミュニケーションに関して 面接を行なっている中で、面接官の質問に恐らく準備していただいた回答を纏めて回答するケースが散見されます。 例えば、自己紹介をお願いしたケースで、所属や趣味等の自己紹介に留まらず、志望理由や転職理由を述べてしまうケースがこれに該当します。 このような回答をしてしまうケースが割合少なくなく、特に私がもったいないと感じてしまいます。 面接というと、厳格なシチュエーションを想定され、相応に緊張もしてしまうので、このような状況になってしまうのも無理はないとは感じます。 ただ、面接官としては面接という試験ではあるものの、会話というコミュニケーションを通じて候補者の適性や評価を行いたい気持ちが強いため、いかに回答内容が素晴らしくとも、そもそも上記のようなコミュニケーションを取られてしまうと、難色を感じずにはいられなくなってしまうのです。 2. 回答に関して やや1とも共通する話ではあるのですが、面接官の質問に「答え過ぎてしまう」回答を行うケースも散見されます。 例えば、志望動機を確認しているケースで、就職に踏み切った背景や転職理由を述べてしまうケースです。 これも気持ちは物凄く分かります。候補者の中では、それら事項と志望動機の因果関係があるので、背景から語りたいと考えているのだと思います。 しかし、冷静に考えると、求められている質問に対する回答とは異なる回答をしてしまう時点で、正しく回答出来ているという評価にはなりません。例えるのであれば、数学の筆記試験で問題文に対しての回答と、その回答に至った見解まで回答しているようなものなのです(証明問題ならまた別かもしれませんが‥)。 加えて、先程も記載した通り、面接は会話というコミュニケーションを通じての試験になるので、先のケースであれば志望動機を確認した上で、その回答の背景も確認したいのであれば、面接官より追加の質問がなされます。 そうした会話を通じて回答されれば全く問題ないのですが、質問に望まれる回答を回答出来ないケースも多々見受けられ、これもまたもったいないと感じる瞬間の一つです。 理想の面接とは 少し辛口な記載になってしまい、恐縮ながら、では逆に面接を通じて良かったと思える瞬間はどのような瞬間なのでしょうか? それは「会話を楽しんでいる」、ということです。上記、偉そうに記載しておりますが、面接官も実際は物凄く緊張して臨んでおり、評価という場であることも相まって、色々なことを考え、気をつけながら面接を行っています。 例えば、どのような質問をすれば、候補者の魅力や強みを最大限引き出せるか?であったり、実務を想定した際にどのような職責が適切か、または案件毎に異なるステークホルダーと良好な関係性を築けるかであったり、何より候補者の価値観や将来のキャリアパスと弊社の方向性が合っているか、等です。 採用はともすれば、候補者の今後の人生を左右しかねない重要なイベントであるからこそ、面接官も本気で、かつ慎重に判断をしなければならないプレッシャーと戦いながら、面接に臨んでいます。故の緊張であったりするのですが、こうした状況下でも当人のパフォーマンスは最大限発揮しながら、各種質問にも楽しみながら、意見交換や考えをぶつけていただき、双方がWin-Winで面接を終えられる状態が理想だと思っています。 最後に いかがでしたでしょうか?当社の新卒採用開始の話題から始まり、もし当社への入社を検討されている方や次年度以降の新卒採用に興味がある方もいれば、ぜひ本記事も参考に面接対策を講じていただければ幸いです。 なお、新卒採用についても26年度採用もオープンしているため、興味ある方はぜひとも積極的なエントリーをお願いいたします。 recruit.insightedge.jp
アバター
こんにちは!Insight Edgeで営業を担当している塩見です。 今回は、私が生成AIと協力してWEBアプリ開発に取り組んだことについてお話しします。 私はエンジニアとしての業務経験がなく、フロントエンドとバックエンドのシステム開発に関する理解を深めることを目的に、個人でアプリ開発を行いました。 本記事にも記載していますが、生成AIを活用することで、エンジニアとしての業務経験の無い私でも、それほど時間をかけずアプリをゼロから開発することが出来ました。 どのような流れで開発を進めたかの情報を簡単にですが纏めていますので、気軽に読んでいただけますと大変うれしいです。 生成AIと協力して開発したアプリ アプリ概要 システム構成 開発の流れ 手描きイラストからページデザインを生成 ChatGPTと協力しながらアプリを開発 分からないところは生成AIに質問し学びを得る 開発の過程で得た学び 学習効率が高い ノーコードより柔軟・高速・簡単にアプリが開発可能 ChatGPTと正確に対話する技術が必要 まとめ 生成AIと協力して開発したアプリ アプリ概要 今回、下図のようなアプリを開発しました。 アプリのスクリーンショット 主に、以下のような機能があります。ユーザ登録が必要になりますが、各機能を自由に使用できる状態にしています。 チャット機能:複数名のペルソナを用意しており、各ペルソナと生成AIを使用したチャットでの会話ができます。 掲示板機能:掲示板のスレッドを作成することができ、作成したスレッドに対して、様々なペルソナがコメントを返します。 開発期間は約2ヶ月間で、週5-10時間程、開発方法を学習しながら実装を進めていました。 システム構成 システムの構成は下図のような内容になっています。 システム構成 各ツール/技術の概要は以下の通りです。 各ツール/技術の説明 これらの開発をChatGPTとの対話を通して行いました。 Next.jsやReactなど、初めて使用する技術要素が大半だったため、ChatGPTに質問しながら情報を得て実装を進めました。 開発の流れ 手描きイラストからページデザインを生成 アプリ開発を始めた当初、何もない状態からプログラムを書き始めることは、初学者にとって非常にハードルが高いと感じていました。 そこで、tldraw makerealというツール使用し、手描きのイラストからWEBアプリのデザインを作成することで、作業を開始しました。 tldraw makerealでは、手描きのイラストを基に、生成AIが自動的にデザインとHTMLを作成してくれます。 tldraw makerealの作業画面 tldraw makerealでは、ブラウザ上で作成したラフのイラストから、リアルなUIデザインを生成してくれます。 自然言語でコメントを入れることで、それに沿った内容もデザインに反映してくれます。 また、手描きイラストではなく、画像からデザインを生成することも可能です。 例えば、参考にしたいWEBアプリやサービスがある場合、その画面をキャプチャし、tldraw makerealにインプットすれば、それに近いデザインを出力してくれます。 初学者の方は、イラストを描くより、こちらの方が手軽で良いと思います。 tldraw makerealでは、デザインのHTMLも出力してくれるため、アプリ開発で必要な土台となるデザインのコードはこれで入手することが出来ます。 ある程度、自分の作りたいアプリのデザインが作成できたら、以降はChatGPTを使って作業を進めていきます。 tldraw makerealを使ってみたい方は、 こちらのページ が参考になるかと思います。 generativeinfo365.com ChatGPTと協力しながらアプリを開発 ここからは、ChatGPTと協力しながら、アプリ開発を進めていきます。 それほど特別な作業はしておらず、自分の理解度に応じて、ChatGPTへの質問を実直に繰り返していきます。 この取り組みでは、コーディング作業の多くをChatGPTに対応してもらいました。 作業者とChatGPTの関係 ChatGPTに質問を繰り返しながら環境構築を進め、環境構築後はtldraw makerealで作成したHTMLをベースにしながら、アプリのトップページ開発を開始しました。 上図のような流れで、実現したい機能や動作をChatGPTに伝え、コードの作成はChatGPTが実施し、コードの実行結果を私が確認するといった流れで対応を進めました。 ChatGPTが優秀なエンジニアとして一緒に作業を進めてくれたおかげで、私の理解度が限られている中でも非常に速いスピード感で開発を進めることが出来たと思います。 一方で、ChatGPTとの対話が長くなると、過去の会話内容を忘れてしまうことがあるため、定期的に新しいスレッドを作成し、最初から状況を説明する必要がありました。 また、開発が進むにつれて、共通処理をコンポーネント化したり、様々なAPIを作成したりすることが増え、参照するファイルが複数に渡ることも多くなったため、それらを全て説明するのも大変でした。 これらの作業が発生する点は、ChatGPTを活用してアプリ開発を進める上での課題感のように思います。 分からないところは生成AIに質問し学びを得る 今回は学習目的でアプリ開発を進めているため、実装を進める上で分からないところは正しく理解していきたいというモチベーションがあります。 ChatGPTは教育者としても非常に優れています。私はChatGPTが作成したコードについて、よく分からない箇所には全て解説のコメントを追記するよう指示し、それを確認することで、理解を深めました。場合によっては、ChatGPTの一次回答だけでは理解しきれない箇所もありますが、それについても深ぼって質問を行い、自身が理解できるまで対話を繰り返しました。 この作業については、ChatGPT上で実施するのではなく、Visual Studio Code等のエディタ上にてGitHub Copilotを活用すると、さらに学習効率が高まると思います。GitHub Copilotでは、不明なスクリプトを範囲選択し、すぐにコードの説明を出力させることが出来るので、ChatGPT上で質問する手間が少ないです。 開発の過程で得た学び 学習効率が高い 上記と少し重複しますが、生成AIと協力したアプリ開発は、学習効率がとても高い印象があります。 先ず、自分が作ってみたいアプリを開発しながら学習を進めることができるので、楽しみながらシステム開発に関する理解を深めることが出来ました。 また、コーディング作業が途中詰まり、長時間作業が進まないといったことも起こりづらく、進行スピードが速いので、途中で学習が挫折する可能性も低そうです。 その上、自分が理解したい箇所への深堀質問がいつでも気軽に出来る点も非常に有益です。プログラミング学習に関連する教育サービスは世の中に多くあり、いずれのサービスでもQA対応を行っていると思いますが、回答が来るまで時間がかかり、また回答の品質も講師に依存します。加え、何度も質問することを躊躇してしまうケースが多々あるかなと。こういった問題をChatGPTは解決しており、学習者の学習効率を高めてくれる作用があるものと思います。 ノーコードより柔軟・高速・簡単にアプリが開発可能 非エンジニアのアプリ開発を助けてくれる手段として、ノーコードツールがあります。私もBubbleなどのツールを使用した経験はありますが、それらと比べて、生成AIを活用したアプリ開発は、柔軟・高速・簡単に開発が出来るという印象を持ちました。 ノーコードツールも手軽にアプリ開発を行うことは出来ますが、まずはツールの使い方に関する理解が必要です。加えて、自分のアイデアをアプリに反映させたい場合、柔軟に内容を変更することが出来ず、ノーコードツールの出来ることからアイデアを考えていくといった動き方になりがちだと思います。 一方、生成AIを活用したアプリ開発では、自分のやりたいことを生成AIに伝えるだけで、コードをゼロから即座に作成してくれます。ノーコードツールの機能に制限されることなく、自分のアイデアを反映させることができ、また自然言語でのコミュニケーションだけで開発を進められます。 ChatGPTと正確に対話する技術が必要 ChatGPTと協力して開発を進める場合、正確な情報をChatGPTに伝える必要があります。回答が意図しないものになっている場合、こちらが伝えるべき情報が不足していたり、またはやり取りが長期化しChatGPTが過去のやり取りを忘れている可能性があります。 私がChatGPTによく伝えていた情報としては、アプリのディレクトリ構成と関連するコードの全量です。アプリの全体像について、ChatGPTが正しく理解出来ているほど、回答の内容が私の期待に近いものになる傾向がありましたので、可能な限り全体像を理解してもらうように対応していました。 しかし、ChatGPTは会話の量が増えると過去のやり取りを忘れてしまう点がやはり大きな課題でした。定期的にスレッドを切り替えるのですが、都度、ChatGPTに現況を説明する作業を行うのは手間があり、この点を解消したいとは考えていましたが、良い方法は思いつかず。今後、ChatGPT等の生成AIが覚えていられるテキスト量が増えてくれば、この課題も少しずつ改善してくるものと思います。 まとめ 以上が、私が生成AIと協力してアプリを開発したときのお話でした。 非エンジニアでも、自身のアイデアをWEBサービスとして実現出来る世の中になってきたと強く感じました。 また、プログラミング等の学習方法も変わってきていると思います。 プログラミング教室では、QA対応を講師が行っているケースが多いかと思いますが、即座に回答してくれて気兼ねなく質問できるChatGPT等の生成AIの方が、利便性は高い印象です。 今後は、学習のモチベーターやスケジュール管理をしてくれるような教育人材の重要度が高まるかもしれません。 今回は主にChatGPTを活用してアプリ開発を行ってきましたが、まだまだ不便な点は多くあります。 そんな中で、生成AIを活用したアプリケーション開発のプラットフォームも様々なものが出てきています。 例えば、 create というサービスでは、生成AIを活用することで、自然言語処理でアプリ開発が出来ます。 www.create.xyz 従来のGUIベースでのノーコードツールではなく、createのように、自然言語ベースで構築可能な新しいノーコードツールの人気は今後高まってくるかもしれませんね。 個人的には、アプリ開発の一式ファイルを自動で参照してくれて、各ファイルの修正や新規ファイル作成等を行ってくれるツールが出てくると、アプリケーション開発の市民化が一気に加速化する気がします。直近の生成AIに関する進化スピードを踏まえると、数年以内に解決しそうな気もします。 今回開発したアプリでは、まだ実現できていないアイデアがいくつかありますので、今後も順次開発を進めていきたいと思います。生成AIを上手く活用しつつ、様々な機能実装を通して、自身の学びを深めていきたいと思います。
アバター
Introduction こんにちは、データサイエンティストの善之です。 Insight Edgeの分析チームでは、有志が技術テーマについて1時間枠で講義し、チーム内でディスカッションを行う「技術研修会」を不定期に実施しています。 先日の研修会では、チーム内でのアンケート結果から最も希望が多かった「プレゼン資料作成術」をテーマに実施しましたので、そのレポートを行います。 技術とは少しテーマがズレますが、他の技術的テーマよりも希望が多く、他部署(開発チーム・管理部)からも参加希望があるなど、皆さん関心の高いテーマだと感じました。 今回は私の前職(コンサルティングファーム)での経験をもとに、プレゼン資料作成術についてお話ししました。 slackでのアンケート結果 目次 講義内容 ①全体のストーリーライン&各スライドのメッセージを作る ②各スライドのチャートを作成 ③ページをレイアウトする ④見やすいデザインに整える 当日の質疑内容 実施後アンケートの結果 まとめ 参考書籍 講義内容 まずはIntroductionとして「ありがちなスライド作成のプロセス」「あるべきスライド作成のプロセス」についてお話ししました。 私の経験上、ありがちなスライド作成のプロセスは ① 各スライドのチャートを作成 ② 各スライドのメッセージを作成 ③ 見やすいデザインに整える ④ 全体のストーリーラインを作成 という手順が多いです(私も以前はこの手順で作成していました)。 しかし、この手順でスライドを作成すると ボツになるスライドやストーリーからずれるスライドが大量に生成される 結局何が言いたいのかよくわからない資料になる場合が多い といった問題が起こりがちです。 そこで、今回の研修会で紹介した手順はこちらです。 こちらのプロセスは ① 全体のストーリーライン&各スライドのメッセージを作る ② 各スライドのチャートを作成 ③ ページをレイアウトする ④ 見やすいデザインに整える という順序です。 こうすることで メッセージとストーリーをサポートする必要十分なスライドが生成される 言いたいことがストレートに伝わる ようになります。 今回の研修会では、この4ステップについて、それぞれ詳細にお話ししました。 ①全体のストーリーライン&各スライドのメッセージを作る このパートでお伝えしたことは主に2つです: まずロジックツリーを書いてから、各スライドのメッセージとストーリーラインを作る(いきなりスライドを作り始めない) メッセージには必ず解釈を含める 特にロジックツリーをまず書くことが最も重要です。 有名な「雲雨傘」を例に、ロジックツリーからストーリーラインに落とし込む方法を紹介しました。 ②各スライドのチャートを作成 このパートでお伝えしたことは、「チャートは基本的にテンプレートから選びましょう」ということです。 一般的なビジネスのプレゼン資料であれば、チャートの「型」があるので、そこから選べばスライド作成時間も短縮されますし、受け手にとっても「よく見るチャート」なので理解がしやすくなります。 そこでこのパートでは、定性・定量に分けてチャートのテンプレートを紹介しました。 定性チャートとしては、ツリー、テーブル、マトリックス、分岐図などを紹介しました。 こちらはツリーの例です。定性的な要素分解にも使えますし、エンジニア/データサイエンティストであれば複雑な計算式を分解する際にも便利です。 定量チャートとしては、横棒グラフ、積み上げ棒グラフ、ウォーターフォール、折れ線グラフなどを紹介しました。 こちらは横棒グラフの例です。データサイエンティストであれば、アルゴリズムの性能比較などでもよく使えるグラフです。 ③ページをレイアウトする このパートでお伝えしたことは、レイアウトについてもチャートと同じくテンプレートから選びましょうということです。 基本的なテンプレートはこちらの3種類になります。 研修会では、実際に具体的なスライド例を示しながら、チャートのレイアウト方法について解説しました。 例えば、こちらは「単一チャート(補完情報あり)」の例で、データ処理の結果・解釈を補完する際に用いるレイアウトです。 ④見やすいデザインに整える 最後にここまでで作成したスライドを見やすいデザインに整えますが、 こちらについては素晴らしい書籍がたくさんあるので、詳しい解説は行いませんでした。 紹介した書籍は記事末尾の 参考書籍 に記載しました。 ただし、あまり書籍に書いていないポイントとして 文字の大きさは12pt以上に 細部にこだわる の2点を紹介しました。 当日の質疑内容 一通り説明を行った後、QAセッションを行いました。 当日は活発な議論があり、例として以下のようなQAがありました。 Q: 資料に情報をどの程度詰め込むべきでしょうか?今日紹介いただいた資料よりも情報を詰め込む場合もあれば、もっと情報量が少ない場合もあると思います A: 情報を詰め込みすぎると分かりづらくなるので、1スライド1メッセージが基本です。一方で、ビジネスのプレゼン資料は打ち合わせに出ていない人が読む場合があるので、口頭補足がなくとも意図が明確に伝わる程度には情報を入れた方が良いです。 Q: グラフは何で作ると良いですか?Matplotlibの場合、綺麗なグラフを作ることが難しいと感じています。 A: 分析時はMatplotlibが便利ですが、最終的に資料化する際はExcelで作成して見た目を綺麗にするのがオススメです。 実施後アンケートの結果 実施後に、今後の参考のためにアンケートを実施しました。 参加者37名中19名に回答いただきました。 Q.1 研修の内容は今後のご自身の業務に活用できると思いますか 5段階で評価いただきましたが、ありがたいことに回答者全員の方に「とてもそう思う」と回答いただきました。 Q.2 具体的に活用できそうなポイントを教えてください 自由記述で記載いただきましたが、研修会で紹介した①-③の内容全般について、さまざまなコメントをいただきました。 ①全体のストーリーライン&各スライドのメッセージを作る 資料作成の考え方(特に設計⇨作成)を活用すると資料・業務が変わると思いました。 メッセージを絞るところとか、先にストーリーを作るとか、全体的に色々ためになるお話でした! ②各スライドのチャートを作成 紹介いただいた定性チャートのテンプレートが今後活用できると思いました チャートの選び方が業務に活かせると感じました 業務における資料作成を行う指針ができたため、迷うことが減り早く作成ができそうです。 ③ページをレイアウトする 見やすいスライド構成(タイトル、メッセージ、チャート)やチャートの補完情報をどう書くかというのがとても勉強になり、活用できそうです。 Q.3 研修内容の改善点があれば教えてください こちらについては演習とフィードバックを設けた方が良いという意見をいただきました。 今回は一通り説明した後にQAセッションという形を取りましたが、もう少し時間を拡大して演習を入れると、より効果的かもしれません。 workshopなども開催して、体に染み込ませる取り組みがあると良いと感じました。 演習時間を実際に取って、作業内容に対してフィードバックする時間があると良いと感じました。 1スライド1メッセージの原則に則りメッセージについて考えるパートもあればより良いと感じました。 Q.4 プレゼン資料作成に関して、追加で取り扱ってほしい内容があれば教えてください こちらについては、より実践を見据えて社内の資料レビュー会であったり、そもそも資料の種別ごとに推奨のテンプレートを用意した方が良いというご意見がありました。 テンプレートの用意については、前々から社内で声が上がっていたこともあり、現在整理を進めている状況です。 社内でよく使われる具体的な資料テーマや実際の資料を使いながらのレビュー会も面白そうと思いました。 どのような資料を作れば良いかについても体系的に整理していただけるとありがたいです(相談があった時にどのような構成の提案資料とすれば良いか等)。 まとめ 研修会を実施してみて、アンケートでもたくさんのポジティブなご意見をいただきましたし、 その後実際に研修会の内容を参考に資料作成いただいているという嬉しい声を複数の方からいただきました。 技術者であっても資料作成の機会は意外と多く、その方法論が分からず困っている場合がそれなりにあるのだと感じました。 アンケート結果を踏まえつつ、今後もプレゼン資料の品質向上と作成効率化に向けた取り組みを行っていければと思います! 参考書籍 外資系コンサルのスライド作成術―図解表現23のテクニック (山口 周) 伝わるデザインの基本 増補改訂版 よい資料を作るためのレイアウトのルール(高橋 佑磨, 片山 なつ) ノンデザイナーズ・デザインブック [第4版](Robin Williams)
アバター
init_mathjax = function() { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true } }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ], processEscapes: true, processEnvironments: true }, displayAlign: 'left', CommonHTML: { linebreaks: { automatic: true } } }); MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } } init_mathjax(); addEventListener("DOMContentLoaded",function(){var a=document.getElementsByClassName("entry-content");if(a)for(var i=0;i こんにちは、InsightEdgeでデータ分析をしている新見です。 今日はUberAIからNeurIPS2019に出た 論文 を紹介します。 この論文では、ベイズ最適実験計画(Bayesian Optimal Experimental Design:BOED)における期待情報利得(Expected Information Gain:EIG)の推定を高速かつ正確に行う新しい手法を提案しています。 BOEDは、限られた実験リソースを効率的に活用するための枠組みですが、EIGの正確な推定が難しいという課題があります。今回紹介する論文では、変分推論の手法を用いることで、高速化を達成しています。 本記事では提案されたEIGの近似式を紹介し、その使用例をコードベースで紹介します。 もくじ もくじ ベイズ最適実験計画(BOED)とは EIGの近似指標 Variational Posterior $\hat{\mu}_{\text{post}}$ Variational marginal $\hat{\mu}_{\text{marg}}$ Variational NMC $\hat{\mu}_{\text{VNMC}}$ Implicit Likelihood $\hat{\mu}_{m+l}$ 近似式の使い分けについて プログラム例:短期記憶モデルの推定 まとめ ベイズ最適実験計画(BOED)とは 一般的に、何かしらの仮説を立証したいときは実験をし、その結果をデータとして取得します。ただし、実験には、時間的金銭的なコストが発生する場合があるので、そのようなケースではなるべく最小の実施回数で仮説を立証できることが望ましいです。このように、効率的な実験条件を設計すること実験計画法と呼びます。たとえば、ある化合物の収量を最大化するための条件の調整や心理学実験における適切な質問の作成等が代表的な例として挙げられます。また、機械学習の枠組みにおいても、最適なハイパーパラメータの探索にこの考え方が応用されています。 ベイズ最適実験計画(BOED)は、立証したい仮説をデータ生成プロセスとしてモデリングを行い、シミュレーションした結果得られる情報が最大となるような実験を実施する方法になります。 具体的には、次式で与えられるような期待情報利得(EIG)を最大化するような実験を設計します。変数$\theta$を目標変数とした時、EIGは実験$d$の結果$y$として $$ \text{EIG}(d)=\mathbb{E}_{p(y|d)}[\text{H}[p(\theta)] - \text{H}[p(\theta|y,d)]] $$のように書けます。$\text{H}[\cdot]$はエントロピーを表し、$p(\theta|y,d) \propto p(y|\theta,d)p(\theta)$は実験$d$の結果$y$を得た時の$\theta$の事後分布を表します。 式の意味は、実験とその結果によってどの程度知りたい変数の不確定性が減少したか、その得られた情報の期待値となります。これを最大とするような実験$d^{*}$を選択することがBOEDの目的になります。 しかしながら、一般的にEIGの計算は困難です。事後分布$p(\theta|y,d)$の評価に入れ子となったモンテカルロ積分(NMC)が必要で(この後すぐ説明)、高い計算コストになります。NMC計算の収束速度は、計算量を$T$とした時、$O(T^{-1/3})$となります。 今回提案する近似手法の収束速度は$O(T^{-1/2})$で、NMCよりも良い性質を持つことが証明されています。 EIGの近似指標 EIGの式をさらに書き変えると、 $$ \text{EIG}(d) = \mathbb{E}_{p(y,\theta|d)}\left[\log \frac{p(\theta|y,d)}{p(\theta)}\right] = \mathbb{E}_{p(y,\theta|d)}\left[\log \frac{p(y|\theta,d)}{p(y|d)}\right] $$となります。 EIGを先ほど述べたモンテカルロ(NMC)で計算する場合の推定式$\hat{\mu}_{\text{NMC}}$は次のように書けます。 $$ \hat{\mu}_{\text{NMC}} = \frac{1}{N} \sum_{n=1}^{N} \log \frac{p(y_{n}|\theta_{n},d)}{\frac{1}{M}\sum_{m=1}^{M}p(y_{n}|\theta_{n,m}, d)} \\ \text{where} \space \theta_{n,m} \sim p(\theta) \space y_{n} \sim p(y|\theta=\theta_{n,0},d) $$上式の計算量は$T=\mathcal{O}(NM)$で、収束速度は$O(T^{-1/3})$です。提案手法では、期待値中の分布関数を近似式で変分的に評価します。 Variational Posterior $\hat{\mu}_{\text{post}}$ 事後分布$p(\theta|y,d)$を変分分布$q_{p}(\theta|y,d)$で近似することを考えます。この時、近似EIGは真のEIGの下限となり、その推定量は$\hat{\mu}_{\text{post}}$は $$ \text{EIG}(d) \ge \mathcal{L}_{\text{post}}(d) = \mathbb{E}_{p(y,\theta|d)}\left[\log \frac{q_{p}(\theta|y,d)}{p(\theta)}\right] \\ \approx \hat{\mu}_{\text{post}} = \frac{1}{N} \sum_{n=1}^{N} \log \frac{q_{p}(\theta_{n}|y,d)}{p(\theta_{n})} $$と表せます。近似分布をパラメトリックに書けると仮定し、以下のSGDを用いて、$\mathcal{L}_{\text{post}}(d) $の最大値を求めます。 $$ \nabla_{\phi}\mathcal{L}_{\text{post}}(d;\phi) \approx \frac{1}{S} \sum_{i=1}^{S} \nabla_{\phi}\log q_{p}(\theta_{i}|y,d;\phi) \space \text{where} \space y_{i}, \theta_{i} \sim p(y,\theta|d) $$ここで、パラメータの更新回数を$K$回とすると、$\hat{\mu}_{\text{post}}$の計算量は$T=\mathcal{O}(SK+N)$で、収束速度は$O(T^{-1/2})$となります。収束の証明は論文を参照してください。 Variational marginal $\hat{\mu}_{\text{marg}}$ 次は、周辺分布$p(y|d)$を$q_{m}(y|d)$で近似することを考えます。この時、近似式はEIGの上限となります。 $$ \text{EIG}(d) \le \mathcal{L}_{\text{marg}}(d) = \mathbb{E}_{p(y,\theta|d)}\left[\log \frac{p(y|\theta,d)}{q_{m}(y|d)}\right] \\ \approx \hat{\mu}_{\text{marg}} = \frac{1}{N} \sum_{n=1}^{N} \log \frac{p(y_{n}|\theta_{n},d)}{q_{m}(y_{n}|d)} $$ $\hat{\mu}_{\text{post}}$の計算時と同様、$q_{m}(y|d,\phi)$の最適なパラメータは$\mathcal{L}_{\text{marg}}(d,\phi)$を最小化することで、計算できます。 Variational NMC $\hat{\mu}_{\text{VNMC}}$ $\hat{\mu}_{\text{post}}$と$\hat{\mu}_{\text{marg}}$はともにNMCよりも高速に収束しますが、近似分布が真の分布に対してバイアスが残る可能性があります。 ここでは、バイアスを低減する為にNMCの計算をベースに、提案分布に近似分布を用いることで高速化を考えます。この時、EIGの変分上限として $$ \text{EIG}(d) \le \mathcal{L}_{\text{VNMC}}(d;M) = \mathbb{E} \left[ \log p(y|\theta_{0},d) - \log \frac{1}{M} \sum_{m=1}^{M} \frac{p(y,\theta_{m}|d)}{q_{v}(\theta_{m}|y,d)} \right] \\ \approx \hat{\mu}_{\text{VNMC}} (d)= \frac{1}{N} \sum_{n=1}^{N} \left( \log p(y_{n}|\theta_{n,0},d) - \log \frac{1}{M} \sum_{m=1}^{M} \frac{p(y_{n},\theta_{n,m}|d)}{q_{v}(\theta_{n,l}|y_{n},d)} \right) $$が得られます。 サンプリングは$\theta_{n,0} \sim p(\theta)$,$y_{n} \sim p(y|\theta=\theta_{n,0},d)$, $\theta_{n,m} \sim q_{v}(\theta_{n,m}|y=y_{n},d)$で行います。1サンプル$(\theta_{n,0}, y_{n})$ごとに、$\{\theta_{n,m}\}_{m\ge1}$のサンプルを発生させているイメージです。 $\mathcal{L}_{\text{VNMC}}(d)$の第二項は$p(y|d)=\mathbb{E}_{p(\theta|d)}\left[p(y|\theta,d)\right]$を提案分布$q_{v}(\theta|y,d)$で重み付けした重点サンプリングで計算しています。 たとえ$q_{v}$が真の分布に収束しなくても、$L\rightarrow \infty$とした時、$\mathcal{L}_{\text{VNMC}}(d)$はEIGに収束することが証明されており、バイアスが生じないことが保証されています。 計算は、学習と評価の2ステップに分かれており、学習時の$y$のサンプル数を$S$、$\theta$のサンプル数を$L$、学習ステップを$K$回とした時、計算量は$T=\mathcal{O}(SKL+MN)$となります。NMCの計算量と同じのように見えますが、学習フェーズがあることにより、$M$の値を小さくでき、実際は早く収束します。 Implicit Likelihood $\hat{\mu}_{m+l}$ (なかなかわかりにくい名前ですが)、$y$のサンプリングはできるけど、明示的に尤度$p(y|\theta,d)$を計算できない場合に使える手法になります。 例えば、潜在変数$\psi$を有するモデル$p(y|\theta, d)=\mathbb{E}_{p(\psi|\theta)}\left[p(y|\theta, \psi, d)\right]$はよく見かけますが、これはサンプルは簡単に計算できるものの、尤度の計算は困難なことが多いです。 ここでは、周辺分布$q_{m}(y|d)$と尤度$q_{l}(y|\theta,d)$の2つを変分分布で近似した上で、EIGを計算します。 $$ \text{EIG}(d) \approx \mathcal{I}_{m+l}(d) = \mathbb{E}_{p(y,\theta|d)}\left[\log \frac{q_{l}(y|\theta,d)}{q_{m}(y|d)}\right] \\ \approx \hat{\mu}_{m+l} = \frac{1}{N} \sum_{n=1}^{N} \log \frac{q_{l}(y_{n}|\theta_{n},d)}{q_{m}(y_{n}|d)} $$ これまでの手法と違って、$\mathcal{I}_{m+l}(d)$は上限、下限ではないので、この値をターゲットとして、$q_{m}(y|d)$と$q_{l}(y|\theta,d)$を学習できません。 幸い、以下の不等式が成り立つのとで、次式の右辺を最小化することで学習可能です。 $$ |\mathcal{I}_{m+l}(d) - \text{EIG}(d)| \le - \mathbb{E}_{p(y,\theta|d)}\left[\log q_{l}(y|\theta,d) + \log q_{m}(y|d)\right] $$ $q_{l}$と$q_{m}$は別々のパラメータとしても、$q_{m}(y|d)=\mathbb{E}_{p(\theta|d)}\left[q_{l}(y|\theta,d)\right]$を利用してパラメータを共有しても学習可能です。 近似式の使い分けについて $\hat{\mu}_{\text{VNMC}}$は唯一バイアスが生じない手法なので、計算コストが許容できる場合は、優先して利用すべき選択となります。その上で、$y$と$\theta$それぞれの次元数の大小と尤度$p(y|\theta,d)$を陽的に計算できるかを基準に使い分けることができます。 $\hat{\mu}_{\text{post}}$と$\hat{\mu}_{\text{VNMC}}$は$\theta$に対する近似分布を、$\hat{\mu}_{\text{marg}}$と$\hat{\mu}_{m+l}$は$y$に対する近似分布を利用しています。よって、$\dim(y) \ll \dim(\theta)$の場合は、より推定が容易になる$\hat{\mu}_{\text{marg}}$と$\hat{\mu}_{m+l}$を利用することが望ましいです。その上で、陽的に尤度$p(y|\theta,d)$を計算できる場合は$\hat{\mu}_{\text{marg}}$を、できない場合は$\hat{\mu}_{m+l}$を利用することが推奨されます。$\hat{\mu}_{\text{post}}$は陽的な尤度評価が必要ないので、そのような時に利用できます。 プログラム例:短期記憶モデルの推定 今紹介した手法を、確率プログラムPyroで実装した例が 公式 にありましたので、紹介します。 人は最大7つのことまでは短期記憶で覚えることができると言われています。ここでは、ある集団の短期記憶を測定する実験を考えたいと思います。 まず、短期記憶をモデル化し、そのパラメータを推定するための実験計画を考えてみます。 短期記憶のモデルを簡単に以下のように設定します。 $$ p(\theta) = \text{N}(\mu_{pre}, \sigma_{pre}^{2}) \\ p(y|\theta,d) = \text{Bernoulli}(p=\text{sigmoid}(k(\theta - d)) $$$d$は実験で提示する数列のサイズを、$y$は答えられたかどうかの結果を表します。たとえば、$d=5$の場合、$(1,2,3,2,5)$のような5つの数列を提示された時、全て正しく答えられた場合は$y=1$となります。$\theta$はこの実験における短期記憶の容量を表します。モデル上では50/50の確率で正解できる最大の数列数を意味します。 以下のコードは、上記のモデルをPyroで実装したものです。 sensitivity = 1.0 prior_mean = torch.tensor( 7.0 ) prior_sd = torch.tensor( 2.0 ) # theta ~ p(theta), y ~ p(y|theta, d) def model (d): # Dimension -1 of `l` represents the number of rounds # Other dimensions are batch dimensions: we indicate this with a plate_stack with pyro.plate_stack( "plate" , d.shape[:- 1 ]): theta = pyro.sample( "theta" , dist.Normal(prior_mean, prior_sd)) # Share theta across the number of rounds of the experiment # This represents repeatedly testing the same participant theta = theta.unsqueeze(- 1 ) # This define a *logistic regression* model for y logit_p = sensitivity * (theta - l) # The event shape represents responses from the same participant y = pyro.sample( "y" , dist.Bernoulli(logits=logit_p).to_event( 1 )) return y 今回は、$\hat{\mu}_{\text{marg}}$を計算することでEIGを推定します。その為には、$q_{m}(y|d)$を変分近似する必要がありました。以下のコードでは、その実装です。候補となる実験それぞれにパラメータを割り振っています。 # q(y|d) def marginal_guide (design, observation_labels, target_labels): # This shape allows us to learn a different parameter for each candidate design l q_logit = pyro.param( "q_logit" , torch.zeros(design.shape[- 2 :])) pyro.sample( "y" , dist.Bernoulli(logits=q_logit).to_event( 1 )) 以下は、EIGを計算するコードになります。 marginal_eig 関数の引数として上で定義した$p(y,\theta|d)$と近似分布$q_{m}(y|d)$を与えています。 また、陽に観測値と潜在変数のラベルを指定する必要があります。これは、 model と marginal_guide 関数の中で指定したラベルと一致する必要があります。加えて、実験候補を candidate_designs として与えます。ここでは次元を [N, 1] として、 N 人に対して1回ずつ実験をを行っているイメージです。 from pyro.contrib.oed.eig import marginal_eig # The shape of `candidate_designs` is (number designs, 1) # This represents a batch of candidate designs, each design is for one round of experiment candidate_designs = torch.arange( 1 , 15 , dtype=torch.float).unsqueeze(- 1 ) pyro.clear_param_store() num_steps, start_lr, end_lr = 1000 , 0.1 , 0.001 optimizer = pyro.optim.ExponentialLR({ 'optimizer' : torch.optim.Adam, 'optim_args' : { 'lr' : start_lr}, 'gamma' : (end_lr / start_lr) ** ( 1 / num_steps)}) eig = marginal_eig(model, candidate_designs, # design, or in this case, tensor of possible designs observation_labels = [ "y" ], # site label of observations, could be a list target_labels = [ "theta" ], # site label of 'targets' (latent variables) num_samples= 100 , # number of samples to draw per step in the expectation num_steps=num_steps, # number of gradient steps guide=marginal_guide, # guide q(y) optim=optimizer, # optimizer with learning rate decay final_num_samples= 10000 # at the last step, we draw more samples ) ここでは、 marginal_eig 関数内でのlossの計算部分の実装を見てみます.$\hat{\mu}_{\text{marg}}$の計算に相当します。 def _marginal_loss (model, guide, observation_labels, target_labels): """Marginal loss: to evaluate directly use `marginal_eig` setting `num_steps=0`.""" def loss_fn (design, num_particles, evaluation= False , **kwargs): expanded_design = lexpand(design, num_particles) # Sample from p(y | d) trace = poutine.trace(model).get_trace(expanded_design) y_dict = {l: trace.nodes[l][ "value" ] for l in observation_labels} # Run through q(y | d) conditional_guide = pyro.condition(guide, data=y_dict) cond_trace = poutine.trace(conditional_guide).get_trace( expanded_design, observation_labels, target_labels ) cond_trace.compute_log_prob() terms = - sum (cond_trace.nodes[l][ "log_prob" ] for l in observation_labels) # At eval time, add p(y | theta, d) terms if evaluation: trace.compute_log_prob() terms += sum (trace.nodes[l][ "log_prob" ] for l in observation_labels) return _safe_mean_terms(terms) return loss_fn Pyroでは、 poutine.trace(model) を使って、モデルの内部状態をトレースすることができます。このオブジェクトに model と同じ引数を与えた上で get_trace を呼ぶことで、サンプリングを行い、内部変数の情報を取得できます。 上の例では、まず model の定義に従い$y$のサンプリングを行い、その結果を y_dict に格納しています。 次に、 guide で定義された$q(y|d)$を結果 y_dict で条件付けを行い、対数尤度を計算しています。得られた対数尤度の平均をlossとして$q(y|d)$のパラメータを最適化しています。 評価時では、学習時には不要だった$p(y|\theta,d)$の対数尤度を加算し、その値を最終的なEIGの推定値としています。 上の marginal_eig の結果をplotしたものが以下になります。 plt.figure(figsize=( 10 , 5 )) matplotlib.rcParams.update({ 'font.size' : 22 }) plt.plot(candidate_designs.numpy(), eig.detach().numpy(), marker= 'o' , linewidth= 2 ) plt.xlabel( "$d$" ) plt.ylabel( "EIG($d$)" ) plt.show() eig ここでは、EIGの最大値は$d=7$となりました。初回なので、事前分布の平均に引っ張られる形となりました。 紹介したリンクでは、その後上の操作を繰り返し、$p(\theta)$の更新とEIGの計算を繰り返し、最適な事後分布の算出しています。興味がある方は参考にしてみてください。 まとめ 今回は、実験計画法における期待情報利得の計算時の変分ベイズ法の適用について紹介しました。データの発生プロセスをモデル化するというハードルはあるので、使いどころは限定される印象です。しかし、実験のコストが高い場合において、より効率的に仮説を立証するためにどのような実験計画を組むべきかを定量的に評価できるのはメリットといえるでしょう。
アバター
こんにちは。InsightEdgeのDataScientistのSugaです。最近もサウナに通っていますが、サウナ好きのなかではオロポという飲み物があります。 オロナミンC+ポカリスエットというもので独特な味がして気にっています。さて、今回は、生成AIを駆使して、バーチャル水田で稲作をシミュレーションしてみようと思います。 取り組むきっかけ 最近のニュースから 最近のニュースを見ていたら、「農林水産省、「天穂のサクナヒメ」とコラボ」という記事がありました。知らない方もいると思うので、少し説明すると、「天穂(てんすい)のサクナヒメ」というゲーム作品が2020年にリリースされました。ゲームの中で米作りの工程をする必要があり、その内容がとてもリアルだと話題になりました。さらに、農林水産省の公式WEBサイトがゲームの攻略に使えるということがわかり、そのことがニュースになっていたりしました。 Youtube : アニメ「天穂のサクナヒメ」とのコラボに寄せて 坂本農林水産大臣メッセージ 農林水産省のプレスリリース より 2020年に原作ゲームが発売された当時、「田起こし」「種籾選別」「田植え」等の米作りの工程が非常にリアルだと大きな話題になり、また、農林水産省の公式ウェブサイトがゲーム攻略に使えると評判にもなりました。 このようなご縁もあり、この度のテレビアニメ化にあたり、正式にコラボする運びとなりました。サクナヒメたちとともに、「生きるための食べものを作る大切さ」、「仲間とともに食卓を囲む喜び」、そして「お米の魅力」をお伝えしてまいります。多くの皆さまに、食や農林水産業について興味や関心を持っていただけるきっかけになることを期待しています。< 農業xTech 近年の世界人口の増加や気候変動もあって食料安全保障の問題に対応するためにも農業分野には注目が集まっていると思います。特にIT技術を使った農業の発展には著しいものがあります。 技術革新の例としては、 IoTデバイスやセンサーデバイスの活用:作物や家畜のリアルタイム監視と管理を可能にします。センサーは土壌の湿度、温度、植物の健康状態を追跡し、自動化された灌漑システムは正確なデータに基づいて最適な灌漑をします。 精密農業:衛星データやセンサーデータなど様々な情報源から畑の詳細な区分レベルで管理をして、植え付け、施肥、収穫といった管理の意思決定をデータを用いて行います。 農業ロボット:従来は農業機械を人が操作して行っていた除草や収穫などを自律ロボットが自動で作業します。これにより単純な機械化よりも効率化して、労働コストを削減するものです。 再生農業:土壌の健康と生物多様性を回復させることに焦点を当てるなど、耕作しない農法、カバークロップの利用、輪作放牧などの技術を含みます。これにより、土壌構造が改善され、侵食が減少し、長期的な持続可能性と生産性向上を目指した取り組みです。 データ活用やクラウド技術活用:精密農業と関連しますが、広範なデータ収集と分析によって作物管理するものです。さらに発展させて、収穫量予測、灌漑量予測、可変施肥予測などを行います。 これらの取り組みでは従来の農業分野で活躍していた、農家や農業研究者、農学者、農業経済学者だけではなく他の分野の専門家も農業分野に進出して、協力して技術開発が行われています。 稲作について  米は世界で最も重要な穀物の1つで、多くの国で主食として広く消費されています。米の栽培は約10,000年前にアジアで始まったとされ、特に中国、インド、タイなどが古くからの主要産地です。現在では、世界中の熱帯・亜熱帯地域で広く栽培されています。米は温暖な気候を好みます。最適な気温は20℃から30℃で、特に生育期には十分な日光が必要です。多くの品種が水田で栽培され、水の管理が重要です。水田は、稲の根が常に水に浸かっている状態を保つため、灌漑システムが必要となります。 さらに、米は肥沃な粘土質の土壌でよく育ち、土壌のpHは5.5から7.0が適しているといわれています。米には数多くの品種があり、大きくジャポニカ種(短粒種)とインディカ種(長粒種)に分類されます。ジャポニカ種は、日本や韓国、中国北部で多く栽培され、粘り気が強く、寿司やおにぎりに適しています。インディカ種はインド、タイ、ベトナムなどで広く栽培され、パラパラとした食感が特徴です。バスマティ米やジャスミン米が代表的です。 日本では、米は主食として広く消費されており、稲作は農業の中心的な役割を果たしています。主要な稲作地域としては、新潟、秋田、山形などの県が挙げられます。近年では、品種改良や農業技術の進歩により、より高品質な米の生産が可能となっています。 稲作の基本プロセス 田んぼの準備 田んぼの準備が行われます。これは、耕起と呼ばれる土壌を耕す作業から始まります。耕起により、土壌が均一に整えられ、稲の根が張りやすくなります。次に、代かきが行われます。これは、田んぼに水を張り、土を泥状にして均一にする作業です。代かきにより、田んぼ全体が均一な状態となり、苗が均等に育ちやすくなります。 苗の育成 田んぼの準備が整ったら、次に苗の育成が始まります。まず、種もみを苗床にまき、苗を育てます。この段階を種まきと呼びます。苗床で育った苗は、苗代と呼ばれる場所に移され、一時的に育てられます。ここで苗が十分に育つまで待ちます。 田植え 苗が十分に育ったら、田植えを行います。田植えは、育てた苗を田んぼに植える作業です。手作業で行う手植えと、機械を使う機械植えがあります。機械植えは、大規模な稲作地帯でよく使われ、効率的に作業を進めることができます。 生育管理 田植えが終わったら、生育管理の段階に入ります。この段階では、水管理、施肥、除草などが行われます。水管理は、稲の成長に必要な水を適切に供給し、過剰な水を排水する作業です。施肥は、稲に必要な栄養分を補うために肥料を与えることです。除草は、田んぼの中の雑草を取り除く作業です。これらの作業を通じて、稲が健康に育つ環境を維持します。 収穫 稲が成熟したら、収穫の時期となります。収穫は、稲刈りと呼ばれ、手作業で行う手刈りと機械を使う機械刈りがあります。 乾燥と脱穀 収穫された稲をまず乾燥させます。乾燥により、稲から余分な水分を取り除きます。次に、脱穀が行われます。脱穀は、稲のもみを取り除く作業です。脱穀されたもみは、玄米として保存されます。 精米 最後に、玄米を精米して白米にする作業が行われます。精米により、玄米の外皮が取り除かれ、白くて美しい米が出来上がります。この米が最終的に食卓に並ぶことになります。 稲作は一連の精密なプロセスを経て行われ、このように各段階で適切な管理と作業が求められます。 対象の作物選定 今回のTechblogを書くにあたっては、作物の題材を決める必要があり、その題材として当初は「コーヒー」を選んでいました。単純に自分がコーヒー好きという理由からです。しかし、調査をしてみると、コーヒーはとても情報が少ないことに気づきました。 研究例としては、 コロンビアのサンタ・マルタ山脈地方でスペシャリティコーヒー栽培に作物モデルを適用した例(Vega Molina et al., 2018) がありました。この研究では特定の農業気象条件で生産性を予測することに成功したという内容です。ただ、コーヒーは主要作物ではないため、元となるデータが少なく手元で再現することが難しかったために、以後の対象作物は「米」にしています。 作物モデル研究 農業研究の分野として、作物モデルを用いるものがあります。これは、作物成長をシミュレーションするための数理モデルです。作物の生理学的過程の理解を深めて、農業生産の効率化や最適化を図る目的で研究されています。そのなかでも、いつくかの種類があり、経済モデルといわれる少し大きなマクロな視点で統計的にモデリングする手法やプロセスベースモデルといった、光合成、呼吸、蒸散など生理学的過程をシミュレーションするものなどがあります。 作物モデルを使うことによって、気候変動の影響評価や農業経営支援や農業政策など広範に利用することが出来ます。ですので、世界中の研究者によって様々な研究が行われてきました。 代表的な作物モデル: DSSAT(Decision Support System for Agrotechnology Transfer): 作物の生育、収量、リソース利用をシミュレートする総合的なモデルシステムです。多くの作物と気象条件に対応しており、研究や実務で広く使用されています​。特徴としては、多種多様な作物(穀物、野菜、果物など)をモデリング可能であり、土壌、水分、気象データを統合して作物の成長をシミュレートできます。作物管理の決定支援、収量予測、環境影響評価に使用されます。 APSIM(Agricultural Production Systems Simulator): 作物、家畜、気象、土壌の相互作用をシミュレートするためのモデルで、農業システムの管理と持続可能性を評価するために使用されます​。特徴としては、作物と気象条件の詳細なシミュレーションが出来て、土壌水分動態、栄養素循環、作物管理オプション、作物ローテーション、混作システムの評価に使用できます。 CERES(Crop Environment Resource Synthesis): トウモロコシ、米、小麦などの主要作物の成長と収量を予測するためのモデルです。土壌、水分、気象データを使用して作物の生理学的過程をシミュレートします。特徴としては、作物の成長、生理学的プロセスの詳細なシミュレーションを行い、天候、土壌、管理条件に基づく収量予測や作物生育の段階的なモデル化に対応していることです。 シンプルな作物モデル 稲作の成長をシミュレーションするにあたって、シンプルな作物モデル成長モデルを実装して、原理を確認しました。モデルとしては土壌栄養素と水分を消費して作物が育つようにしました。 サンプルコードは以下のようになります。 import numpy as np import pandas as pd import matplotlib.pyplot as plt from scipy.integrate import odeint # 作物成長モデルの定義 def crop_growth (y, t, params): C, N, W = y # C:作物バイオマス, N:土壌栄養素, W:土壌水分 k1, k2, k3 = params # 成長率、栄養素吸収率、水分吸収率 dCdt = k1 * N * W dNdt = -k2 * C * W dWdt = -k3 * C * N return [dCdt, dNdt, dWdt] # 初期条件とパラメータの設定 initial_conditions = [ 1 , 1 , 1 ] # 初期バイオマス、栄養素、水分 params = [ 0.1 , 0.05 , 0.02 ] # 成長率、栄養素吸収率、水分吸収率 t = np.linspace( 0 , 100 , 100 ) # シミュレーション期間 # シミュレーションの実行 result = odeint(crop_growth, initial_conditions, t, args=(params,)) C, N, W = result.T # 結果のプロット plt.figure(figsize=( 10 , 6 )) plt.plot(t, C, label= 'Crop Biomass' ) plt.plot(t, N, label= 'Soil Nutrients' ) plt.plot(t, W, label= 'Soil Moisture' ) plt.xlabel( 'Time' ) plt.ylabel( 'Quantity' ) plt.legend() plt.title( 'Crop Growth Simulation' ) plt.show() 実行結果は以下のようになります。時間の経過とともに作物が成長して、土壌栄養素と水分が減少する様子を示しています。このモデルは、作物成長における基本的な相互作用をシンプルに表現したものです。 シンプルな作物モデルのシミュレーション結果 米の作物モデル 今回のTechBlogのテーマとしては、実際に米の作物モデルを実験してシミュレーションしてみることを目的としたので、ChatGPTに米の作物モデルで使いやすいものはあるかを聞いて見ました。その結果、 PCSE(Python Crop Simulation Environment) というライブラリがあることや、シミュレーション手順についても教えてくれました。PCSEはオランダで開発されたPythonパッケージです。 適用できる作物モデルとして WOFOST(WOrld FOld Studies) というものがあります。 一年生作物の成長と生産を定量的に分析するためのモデルだそうです。 実行手順 PCSEを使用して米のシミュレーションを行うためには、まず米に関する適切なパラメータセット、天候データ、土壌データ、そして農業管理データを準備する必要があります。以下にその手順を詳細に説明します。こちらもChatGPTに聞いたら見事に答えてくれていました。 手順 1.パラメータセットの準備 米の成長に関するパラメータセット(作物データ、土壌データ、サイトデータ)を準備します。これらのデータは、PCSEの提供するフォーマットに従って準備する必要があります。 作物データ:米の成長段階、光合成効率、呼吸率、成長期間などの生理学的パラメータ。 土壌データ:土壌の物理的・化学的特性(水分保持能力、浸透率、栄養素含有量など)。 サイトデータ:地理的特性(水分バランス、CO2濃度など)。 2.天気データの取得 PCSEは様々な天気データプロバイダーをサポートしています。NASA Power Weather Data Providerを使用して、シミュレーションに必要な天気データ(温度、降水量、日照量など)を取得します。 3.農業管理データの作成 農業管理データ(例えば、播種日、灌漑スケジュール、施肥計画など)をYAML形式で作成します。 4.モデルの設定とシミュレーションの実行 これらのデータを使用して、PCSEのWOFOSTモデルを設定し、シミュレーションします。 シミュレーション実行 シミュレーションコードもChatGPTに聞いて見ましたが、こちらは全くうまくいかずにある程度の限界があることに気づきました。ですので、PCSEの本家のサイトを参考にシミュレーションコードを実行してみました。 関連モジュールのインポートとバージョン確認 まずはpip install pcseを実行してライブラリをインストールした後で、インストールがされているかの確認します。 %matplotlib inline import sys from pathlib import Path import matplotlib matplotlib.style.use( "ggplot" ) import matplotlib.pyplot as plt import pandas as pd data_dir = Path.cwd() / "data" import pcse print ( "This notebook was built with:" ) print (f "python version: {sys.version}" ) print (f "PCSE version: {pcse.__version__}" ) 作物パラメータの指定 作物固有のパラメータが提供されているので、ここでは作物と品種の指定を行います。このパラメータはフェノロジー(Phenology)と呼ばれる成長段階や成長ステージを表すものが表現されていて、特定の気象条件や時間経過に基づいて育成をシミュレートします。また、同化(Assimilation)と呼ばれる作物が光合成によって二酸化炭素を取り込み、有機物を生成するプロセスも含まれます。光合成の効率は光強度、温度、CO2濃度、葉面積などに影響されます。 さらに、呼吸(Respiration)という植物がエネルギーを消費して成長や維持活動を行うプロセスもあります。加えて、分配(Partitioning)と呼ばれる植物が生成した有機物を各器官(根、茎、葉、穂など)に対してどのように分配するかも考慮されています。 from pcse.fileinput import YAMLCropDataProvider, CABOFileReader cropd = YAMLCropDataProvider() cropd.set_active_crop( 'rice' , 'Rice_501' ) 土壌パラメータ 土壌の種類と土壌の物理的特製に関連するパラメータを決定します。今回は自由排水土壌(過剰な水分が土壌中に滞留せず、速やかに排出される特性を持つ土壌)、中細砂の土壌ファイルを指定しています。 soilfile = data_dir / "soil" / "ec3.soil" soild = CABOFileReader(soilfile) サイトパラメータ サイトパラメータは作物や土壌に関連しない補助的なパラメータです。初期土壌水分含有量(WAV) や初期および最大表面貯留量(SSI、SSMAX)などの水バランスの初期条件があります。また、大気中のCO2濃度も、典型的なサイトパラメータです。今回はデフォルト値を採用しました。 from pcse.util import WOFOST72SiteDataProvider sited = WOFOST72SiteDataProvider(WAV= 10 ) print (sited) {'IFUNRN': 0, 'NOTINF': 0, 'SSI': 0.0, 'SSMAX': 0.0, 'WAV': 10.0, 'SMLIM': 0.4} パラメータをパッケージ これまで指定してきたパラメータを1つの変数にまとめる。 from pcse.base import ParameterProvider parameters = ParameterProvider(cropdata=cropd, soildata=soild, sitedata=sited) 農業管理 作物を育てるときにどのようなオペレーションをするかを指定します。主な要素は種まきをいつ開始したかや収穫時期の指定などです。このファイルはYAML形式で入力します。今回はデフォルト値を参考にして、米で使えるように少し変更したファイルを用いました。 from pcse.fileinput import YAMLAgroManagementReader agromanagement_file = data_dir / "agro" / "rice_calendar.agro" agromanagement = YAMLAgroManagementReader(agromanagement_file) 天気観測 シミュレーションするには天気の情報も必要です。PCSEには天気データを読み取るためのデータプロバイダーの機能もあります。空間解像度0.5度(約50km)で世界の気象データを提供するNASA PowerDataの気象データを読み込むことが出来ます。今回は日本の新潟市の緯度経度を指定してみました。 from pcse.input import NASAPowerWeatherDataProvider wdp = NASAPowerWeatherDataProvider(latitude= 37.9123837 , longitude= 140.0106058 ) print (wdp) Weather data provided by: NASAPowerWeatherDataProvider --------Description--------- NASA/POWER CERES/MERRA2 Native Resolution Daily Data ----Site characteristics---- Elevation: 551.5 Latitude: 37.912 Longitude: 140.011 Data available for 1984-01-01 - 2024-07-20 Number of missing days: 3 作物シミュレーションの実行 PCSEのエンジンをインポートして、指定した構成ファイルとパラメータで初期化して起動します。その後、シミュレーションした結果を時系列データとして取得します。結果はpandasデータフレームに格納しておきます。 from pcse.models import Wofost72_WLP_CWB wofsim = Wofost72_WLP_CWB(parameters, wdp, agromanagement) wofsim.run_till_terminate() df_results = pd.DataFrame(wofsim.get_output()) df_results = df_results.set_index( "day" ) df_results.tail() シミュレーション結果(テーブル) シミュレーション結果 発育 (Development Stage, DVS) 発育段階 (Development Stage, DVS) は、作物の成長段階を数値化したものです。DVSは通常、0から2の範囲で表され、作物のライフサイクルを以下のように示します。 DVS = 0:播種(種まき)または発芽の段階 DVS = 1:花が咲く段階(開花) DVS = 2:作物が完全に成熟する段階(収穫準備) DVSは、作物の成長に伴う生理的な変化を示すため、モデル内で成長段階ごとの異なるプロセス(例えば、葉の展開、実の形成など)を管理するのに役立ちます。 総バイオマス (Total Above Ground Biomass, TAGP) 総バイオマス (Total Above Ground Biomass, TAGP) は、地上部における作物全体のバイオマス量を示します。これは、作物が光合成によって生成した有機物の総量であり、以下の要素を含みます。 葉:光合成を行う主要な部位 茎:作物の支持構造 穂や果実:作物の収穫部位 TAGPは作物の全体的な成長と生産性を評価する重要な指標です。 葉面積指数 (Leaf Area Index, LAI) 葉面積指数 (Leaf Area Index, LAI) は、単位地表面積あたりの総葉面積の割合を示す指標です。具体的には、以下のように定義されます。 LAI=葉の総面積/地表面積 例えば、LAIが3の場合、地表1平方メートルあたりに3平方メートルの葉面積が存在することを意味します。LAIは、作物の光合成能力を評価するために使用され、光エネルギーの捕捉効率や蒸散量に直接影響します。 根圏土壌水分 (Soil Moisture, SM) 根圏土壌水分 (Soil Moisture, SM) は、根が存在する土壌層の水分量を示します。これは、作物の水利用効率と成長に大きな影響を与えます。SMは以下のような要因に影響されます。 降水量:自然の雨水供給 灌漑:人工的な水供給 蒸発散:土壌からの蒸発と作物からの蒸散による水分損失 土壌の保水力:土壌の種類や構造による水分保持能力。 SMは、作物の水ストレスを評価し、灌漑スケジュールの最適化や水管理戦略の立案に役立ちます。 シミュレーション結果の可視化 シミュレーション結果を表示します。DVSは0からはじまり10月には収穫段階の2まで成長しています。TAGPも順調に量が増えていることがわかります。LAIは8月頃をピークに減少していますが、 過去の調査結果(堀江・桜谷, 2014) を参照する限りにおいては、出穂期ごろに最大値へ達して、その後は減少していくとの記述があるので、この時系列変化で概ねあっているように見えます。また、稲作では「中干し」「土用干し」と言われる夏の時期に田んぼの水を抜いて乾燥させる農法があるため、SMの推移もこれに近いグラフの形になっていると思われます。 fig, axes = plt.subplots(nrows= 4 , ncols= 1 , figsize=( 12 , 12 ), sharex= True ) for var, ax in zip ([ "DVS" , "TAGP" , "LAI" , "SM" ], axes.flatten()): ax.plot_date(df_results.index, df_results[var], 'b-' ) ax.set_title(var) fig.autofmt_xdate() シミュレーション結果(グラフ) まとめと今後 今回は作物モデルを用いて稲作をシミュレーションしてみることをやってみました。この分野自体は初めて調査してみたので、わからないことも多かったですが、あらためて農場分野の専門知識も大切だと感じました。データ分析だけでなくドメイン知識も大事と言われますが、まさにこのことだと思いました。 また、作物モデルにおいては指定するパラメータの数が多かったのでベイズ最適化のようなパラメータ探索と決定といった分野と親和性が高いとも思いました。例えば、収穫量を最大化するようなパラメータを探索してそれを営農に活用するなどまだまだデータを活用できる余地は大きいと思いました。
アバター
目次 はじめに GitHub Copilotとは 導入背景と期待効果 なぜGitHub Copilotを導入したのか 組織内の活用状況 GitHub Copilotの利用状況 利用者の声 まとめと今後の展望 まとめ 今後の展望 はじめに Insight Edgeで開発チームのLead Engineerをしている三澤です。 Insight Edgeではチームの開発効率の向上と品質の向上を狙い、さまざまなツールやサービスの検証と導入を進めています。その中で弊社内で活用しているGitHub Copilot Businessについて社内の活用状況を公開します。実際の利用統計の他、ソフトウェアエンジニア・データサイエンティストそれぞれの視点で感想を公開しますので是非ご覧ください。 GitHub Copilotとは GitHub Copilotは、GitHubが提供する開発者がコードをよりすばやく書き込みできるようになるAIを活用したコード補完ツールです。GitHub Copilotは、開発者がコードを書く際に、コードのコンテキストに合わせてコードの補完候補を提案してくれます。これにより、開発者はコードをより迅速に書くことができるようになります。 このGitHub Copilotには3種類のプランがあり、それぞれ図に示すような特徴があります。 我々が導入しているのはGitHub Copilot Businessです。このプランは、個人用アカウントであるGitHub Copilot Individualと比べて、価格が1か月のユーザーあたり19米国ドルと若干高額ですが、その分機能が充実しています。例えば、GitHubのOrganization全体にわたるポリシー管理や、指定したファイルをGitHub Copilotの学習から除外する機能があります。また、Copilot Businessは、Copilot Individualと比べて、パブリックコードに一致する候補をブロックする機能や監査ログの機能があります。これらの機能は、ビジネス用途でGitHub Copilotを使用する際には必要不可欠な機能です。 また、通常のコード中での補完だけでなく、GitHub Copilotと対話できるチャットインターフェイスのCopilot Chatも提供されています。Copilot Chatは、コードの説明や質問にも対応してくれるため、開発者はより効率的にコードを書くことができるようになります。 導入目的と期待効果 なぜGitHub Copilotを導入したのか GitHub Copilotを導入した背景には、以下のような4つの理由があります。 開発効率の向上 コード品質と一貫性の向上 学習曲線の短縮 最新技術トレンドへの適応 それぞれについて詳しく説明します。 開発効率の向上 コードの補完候補を提案してくれるため、開発者はコードをより迅速に書くことができるようになります。これにより、開発効率が向上することを期待できます。 コード品質と一貫性の向上 コードのコンテキストに合わせてコードの補完候補を提案してくれるため、コードの品質と一貫性が向上します。これにより、バグの発生を防ぐことができます。ただしこちらについては注意が必要です。Copilotはあくまで補完候補を提案するツールです。開発者が提案されたコードをそのまま採用するのではなく、提案されたコードを検証し、適切なコードを採用するようにすることが必要になります。 学習曲線の短縮 コードの補完候補を提案してくれるため、開発者はたとえ新しい技術やフレームワークに迅速に適応することができます。これにより、学習曲線が短縮されます。 最新技術トレンドへの適応 AI支援ツールを活用したソフトウェア開発は現在トレンドとなっており、早期に導入することで最新の技術トレンドに迅速に追従することが可能です。これにより、競争力を維持することができます。 以上の4点の効果を見込み、我々は導入を決定し運用してきました。 組織内の活用状況 ここからは、GitHub Copilotの社内活用状況についてご紹介します。 GitHub Copilotの利用状況 弊社では2023年3月からGitHub Copilot Businessを導入し、現在までに約40名の開発者が利用しています。まずは、実際にどれくらいの頻度でどのような言語に対して利用されているかをご紹介します。これらの情報はGitHubが提供するAPIを通じて取得したものです。 統計情報の取得 GitHub Copilotの統計情報はAPIを通じて取得できるため、利用頻度を分析することができます。ただし本APIは直近28日間の情報しか取得できないため、それよりも長い期間をそのまま分析することはできません。長きに渡る利用統計を分析するためには、現時点では1ヶ月1回等の定期的な頻度でAPIを実行し情報を取得保存して集計する必要があります。なお、今回はAPIが最近利用可能になったということもあり、直近28日間の情報を取得し利用頻度を分析しました。 APIを利用する最も簡単な方法は、Pythonのrequestsモジュールを使用することです。以下に、GitHub CopilotのAPIを使用して統計情報を取得するPythonのコードを示します。こちらのコードを実行することで直近28日間の統計情報を取得できます。 import requests # GitHub API URL enterprise_name = "XXX" url = f "https://api.github.com/enterprises/{enterprise_name}/copilot/usage" # APIにアクセスするためのトークン token = "YOUR_TOKEN" # リクエストヘッダー headers = { "Accept" : "application/vnd.github+json" , "Authorization" : f "Bearer {token}" , "X-GitHub-Api-Version" : "2022-11-28" , } # GETリクエストを送信 response = requests.get(url, headers=headers) # リクエストが成功したかどうかを確認 if response.status_code == 200 : data = response.json() # JSON形式のデータを取得 print ( "データの取得に成功しました:" ) print (data) # データを表示 else : print (f "データの取得に失敗しました。ステータスコード: {response.status_code}" ) print (response.text) # エラーメッセージを表示 利用頻度 弊社では1日平均12.0人がGitHub Copilotを利用しています。これは弊社の約1/3が毎日利用しているということになります。ちなみにCopilot Chatの利用者は1日平均0.16人であり、Copilot Chatの利用者は非常に少ないことが分かりました。こちらの機能については私自身以前は何回か使ったことがあるものの、日常の開発シーンにおいては利用はほとんどありません。利用開始当初は実装をしながらChatGPT等のサービスと行き来せずにVS Code内で自由に対話ができることに期待していたものの、実際に使ってみると利便性を感じることができず次第に利用しなくなってしまいました。他の開発者も同様の理由でCopilot Chatの利用がなくなったか、あるいは存在を知らない可能性があるため、改めてその機能について検証し、必要であれば周知をする必要があるかもしれないと考えています。 利用言語 以下は、直近28日間の利用言語で実際にCopilotが提案したコードから採択された行数を元に算出した結果になります。最も利用されている言語はPythonであり、次いでTypeScript、hcl、yamlと続いています。 弊社では、主にPythonやTypeScriptを使用しているため、これらの言語が上位にランクインしていることは自然な結果と言えます。また、インフラ管理のためにTerrformを使用しているため、hclとyamlも上位にランクインしていることはとても自然な結果と言えます。 提案されたコードの採択率 最後に、Copilotが提案したコードの採択率についても調査しました。提案されたコードの採択率は、提案されたコードのうち、実際に採用されたコードの割合を示しています。これは開発者がCopilotの提案をどの程度採用しているかを示す指標となります。言語問わずの平均は30.12%となりました。言語別では、採用された行数で最も多かったPythonに対して、JSONが最も高い採択率を示しています。これはテストデータの生成やAPIのレスポンスのモックなど、JSONを扱う機会が多いためと考えられます。 利用者の声 弊社のエンジニアの声をヒアリングしましたのでご紹介します。エンジニアの声を聞くことで、GitHub Copilotの導入による効果や課題についてより具体的に把握することができます。ちなみに弊社のエンジニアは大きく2つのグループに分かれています。システム開発を担う開発チームと、データ分析を担う分析チームです。今回はそれぞれ異なる視点からGitHub Copilotについてどのような定性効果があったのかをヒヤリングしましたので、その結果をご紹介します。 開発チーム (ソフトウェアエンジニア) 開発チームからは、生産性の向上や学習効果に関するポジティブな意見が多く聞かれました: 定型的なコードをいちいち書く必要がなくなり、開発スピードが格段に上がった。よりコアなロジックに集中できるようになった。 テストコードを書くのが面倒だったが、Copilotが自動生成してくれるので助かる。テストケースを考える時間もしっかり取れるようになった。 Copilotがないと生きていけない体になってしまった。 一方で、Copilotの提案の質や依存度に関する課題も指摘されています: Copilotが提案してくれるコードが、いつも最適な方法とは限らない。時には、自分で書いた方が早いし、分かりやすい場合もある。 クラウド環境のSDKを使う場合、Copilotが生成するコードが不安定で、エラーが発生することがある。最新のAPIに対応できていないこともある。 分析チーム (データサイエンティスト) 分析チームからは、特に学習効果や新しい知識の獲得に関するポジティブな意見が聞かれました: Copilotが提案してくれるコードを通して、知らなかったAPIやライブラリを知ることができ、学習の機会になっている。 データ前処理や可視化のコードを書く際に、Copilotの提案を参考にすることで、より効率的なアプローチを学べることがある。 Copilotのおかげで、コードを書く際のストレスが減り、より分析業務に集中できるようになった。 一方で、分析チームからもCopilotの提案の質や依存度に関する課題が指摘されています: Copilotが提案するコードは、あくまでも参考程度に捉えている。最終的には、自分の知識と経験に基づいて判断する必要がある。 以上のように、開発チームと分析チームともに、Copilotの利用による生産性向上や学習効果については肯定的な意見が多く聞かれましたが、提案の質や依存度に関する課題も指摘されています。ただし、総じて、Copilotの導入による効果は大きいと考えています。 まとめと今後の展望 ここまで、GitHub Copilotの導入背景と目的、組織内の活用状況についてご紹介しました。最後に、まとめと今後の展望について述べます。 まとめ GitHub Copilotは、我々の開発において生産性向上や学習効果の向上に大きく貢献しています。特に、定型的なコードの自動生成や馴染みのないAPIやライブラリの学習や定型的なコードの自動生成において、Copilotの提案は非常に有用であると感じています。一方で、提案の質や依存度に関する課題もあるため注意は必要ですが、総じて、Copilotの導入による効果は大きいと考えています。むしろ、なくてはならないツールとなっています。 今後の展望 今後は、GitHub Copilot Enterpriseの導入を検討しています。Copilot Enterpriseは、Copilot Businessよりもさらに高度な機能を提供しており、より大規模な組織での利用に適しています。弊社では7月中にCopilot Enterpriseの検証開始を予定しており、その結果を踏まえて、Copilot Enterpriseの導入を検討していきます。なお、その結果については今後の報告でご紹介しますので是非ご期待ください!
アバター
init_mathjax = function() { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true } }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ], processEscapes: true, processEnvironments: true }, displayAlign: 'center', CommonHTML: { linebreaks: { automatic: true } } }); MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } } init_mathjax(); DALL-Eで生成した「複雑な株価の動き」 こんにちは!Insight Edge データサイエンティストの伊達です。 昨年9月より、Kaggleにて株価予測コンペ「Optiver - Trading at the Close」が開催され、4000以上のチームが参加する盛り上がりを見せました。 私は参加できませんでしたが、Insight Edgeの分析プロジェクトでも時系列データを扱う場面があるため、今回の記事では、コンペ終了後に共有された上位解法から得られた学びを紹介したいと思います。 今回の記事の執筆にあたって、コンペで共有されたディスカッションやノートブックだけでなく、国内の金融データサイエンスコミュニティであるマケデコ(Market API Developer Community)様が今年5月に開催された「OptiverコンペKaggle上位解法勉強会」での発表内容も参考にさせて頂きました。 www.youtube.com zenn.dev 目次 Optiverコンペについて 問題設定 データ期間と評価 データ項目 Optiverコンペの上位解法まとめ 上位解法一覧 ポイントになったと思われる技術 まとめ Optiverコンペについて www.kaggle.com 今回のコンペの課題は、アメリカの代表的な株式市場の1つであるNASDAQにおいて、15時50分〜16時(取引終了)という最後の10分間で、各銘柄の株価がどう動くかを予測するというものです。 なぜ最後の10分間の株価予測なのか?その背景に、近年、米国株市場において取引終了直前の短時間での売買量が増加しており、市場に与える影響が大きくなっていることがあるようです。 米国株の取引時間は通常390分だが、ここにきて最後の10分間のみが相場にとって重要になりつつあるかもしれない。 アルゴリズム取引を開発するベストExリサーチがまとめたデータによると、S&P500種株価指数採用銘柄の株式売買では、全体の約3分の1が最後の10分間に執行されている。この割合は2021年の27%から上昇している。 www.bloomberg.co.jp また、NASDAQでは取引方式としてマーケットメイク方式が採用されており、オークション方式のように投資家同士が直接売買するのではなく、NASDAQが認可したマーケットメーカーと呼ばれる業者が投資家の売買注文を成立させます。 www.investopedia.com 今回のコンペの主催者であるOptiverはNASDAQにおけるマーケットメーカーであり、取引が集中する「最後の10分間」で各銘柄の株価がどう動くかを予測することはOptiverにとって非常に重要なタスクです。 問題設定 今回のコンペのターゲットは「各銘柄の株価が60秒後にどう動くか」です。 より正確に書くと、「各銘柄の加重平均価格(WAP)の60秒後のリターン」から「インデックスの加重平均価格の60秒後のリターン」を引いた値がターゲットとなっています: $$(\frac{\text{StockWAP}_{t+60}}{\text{StockWAP}_{t}} - \frac{\text{IndexWAP}_{t+60}}{\text{IndexWAP}_{t}}) * 10000$$ 加重平均価格(WAP):板における買い・売り値の加重平均 $$\frac{\text{買い値} * \text{売り数量} + \text{売り値} * \text{買い数量}}{\text{買い数量} + \text{売り数量}}$$ インデックス:日経平均のように、NASDAQを構成する各銘柄の株価を加重平均した値 評価指標としては、平均絶対誤差(MAE)が採用されていました。 データ期間と評価 今回のコンペの課題は時系列予測であるため、各チームは締切までにノートブックを提出し、Kaggle側が各チームの提出したノートブックと評価期間データ(サブミッション締切より後のデータ)を用いて精度を評価する「Code Competition」となっていました。 データ期間の長さは下記の通りになっていたようです: 学習用:96週分 Publicリーダーボード評価用:9週分 評価に使われないデータ:7週分 Privateリーダーボード評価用:6週分 データ項目 今回のコンペで提供されたデータは下記の通りです: 銘柄ID 日付ID クロージング・オークション開始(15:50)から経過した秒数( seconds_in_bucket ) ターゲット 価格系データ bid/ask_price :最良気配(最も高い買値、最も安い売値) wap :買値・売値の加重平均価格 far_price :オークション注文板(auction order book)で取引量が最大となる約定価格。板情報(order book)は考慮されない。 near_price :オークション注文板と板情報を合わせた時に取引量が最大となる約定価格 reference_price :最良気配の範囲(最も安い売値〜最も高い買値)に制限された near_price サイズ・インバランス系データ bid/ask_size :板情報において最良気配の買い/売り注文量 imbalance_size : reference_price で約定した時、約定しない注文量 imbalance_buy_sell :オークションのインバランスの方向(1: 買い側、-1: 売り側、0:インバランスなし) matched_size : reference_price で約定した時、約定する注文量 Optiverコンペの上位解法まとめ Optiverコンペの上位リーダーボード 上位解法一覧 本記事執筆時点で公開されている上位解法をまとめます。 今回は、特徴量、モデル構成、再学習の有無、後処理、バリデーション戦略という5つの観点で各解法を整理しました。 1位: hydさん(スコア: 5.4030) 1位解法 特徴量 よく使われる特徴量に加え、「秒群」を使ったグループ集計(後述)による特徴量を使用 変数選択:CatBoostの重要度で上位300個 モデル構成:CatBoost + GRU + Transformer 再学習(オンライン学習):あり 後処理:各銘柄の予測値の加重平均を引く バリデーション戦略:時系列分割(学習期間最後の81日分でバリデーション) その他工夫 polars使用によるデータ処理効率化? 学習データを日付ごとに保存しておくことで、推論時のメモリ制約の中で使える特徴量の数をできるだけ増やした 6位: Daniel FGさん(スコア: 5.4285) 6位解法 特徴量:よく使われる特徴量40個弱 モデル:Transformer(少し設定を変えた3個)+ GRU 再学習(オンライン学習):あり 後処理:一つのモデルを除き、予測値の合計が0になる制約を加えて学習させた バリデーション戦略:時系列分割(学習期間最後の121日分でバリデーション) 7位: NimaShahbaziさん(スコア: 5.4300) 7位解法 特徴量:よく使われる特徴量に加え、価格やサイズデータが日付/秒軸で計算した中央値からどれだけ乖離しているか("deviation features")を使用 モデル:LightGBM + LSTM + CNN 再学習(オンライン学習):あり 後処理:なし? バリデーション戦略:時系列分割 9位: ADAM.さん(スコア: 5.4352) 9位解法 特徴量 よく使われる特徴量に加え、テクニカル指標(MACD)などを使用 変数選択:特徴量を10〜30個ずつグループ分けし、各グループを追加したときにバリデーション結果が良くなるか確認。グループ単位の結果が良かったら、その中で一つずつ追加して同様に結果を確認し、グループ内で5〜10個程度のみ採用。 モデル:XGBoost 3個 学習期間最後の45日間のデータの重みを増やした 再学習(オンライン学習):あり 後処理:各銘柄の予測値の加重平均を引く バリデーション戦略:時系列分割 その他工夫 polars使用 pandas dataframeのメモリ使用量を減らす ポイントになったと思われる技術 特徴量 ほとんどの上位解法の特徴量エンジニアリングは、 こちらのノートブック で紹介されているような基本的なものと大きく変わらない印象です。 1位解法 では"magic features"として、「秒群」を使ったグループ集計による特徴量が紹介されていました。 これは、クロージング・オークション開始(15:50)から経過した秒数が、 0〜290秒であればグループ0 300〜470秒であればグループ1 480〜540秒であればグループ2 として、各銘柄について、同じグループ内の最初の秒の値・移動平均値との比率を計算するものだそうです。 グループの区切り方については、 公式のチュートリアル にもあるように、 15:55(300秒):NASDAQからclosing informationの公表が開始 15:58(480秒):Limit-On-Close (LOC) 注文の受付停止 というドメイン知識が使われたのだと思われます。 また、1位解法では、同じ日・秒について、銘柄軸で計算された平均値との比率やランクも使われていたそうです。 1位解法の"magic features"についての説明は、 マケデコ勉強会でのnishimotoさんの発表 が分かりやすかったので、おすすめです。 モデル モデルに関して、上位解法では GBDTとニューラルネット(1位、7位) GBDTのみ(9位) ニューラルネットのみ(6位) の3パターンになっていました。 興味深かったのは、1位解法が、CatBoostに加えて、GRUを時系列軸のモデリング(入力シェイプ:バッチサイズ × 55時点 × 隠れ層サイズ)に、Transformerを銘柄軸(クロスセクション)のモデリング(入力シェイプ:バッチサイズ × 200銘柄 × 隠れ層サイズ)に使うことで、アンサンブル全体でのモデル多様性を持たせていたことです。 コードコンペの制約があり、使用できるモデル数や特徴量が限られる中で、モデルの多様性を最大限広げている辺り、さすが1位だなという印象です。 再学習(オンライン学習) 今回のコンペでの評価はCode Competition形式で、評価期間の各時点において、それより前の時点のターゲットの値が随時提供される形になっていました。 つまり各参加者は、提出するノートブックの中で、随時提供される評価期間中のデータを使ったモデルの再学習(オンライン学習)を導入するチャンスがありました。 上記の上位解法の全てがこの再学習を使用しており、上位入賞の鍵になった要素と考えられます。 後処理 今回のコンペのターゲットは、 「各銘柄の加重平均価格(WAP)の60秒後のリターン」から「インデックスの加重平均価格の60秒後のリターン」を引いた値 でした。 ターゲットの計算で使われるインデックスは今回のコンペ用にOptiverによって作成されたもので、公式からはインデックスがどう構成されているか説明されていませんでした。 しかし、 有志によりインデックスの計算方法が推測 されており、今回のインデックスは各銘柄の株価ではなく60秒後リターンの加重平均になっていたようで、インデックス構成用の各銘柄の重みが公開されていました。 今回の1・9・15位の解法ではこの知見と重みを用いて、モデルが出力した各銘柄の予測値から、推測された重みを使った予測値の加重平均を引くという後処理が使われていました。 これは、各銘柄でインデックスのリターンを引いているため、ある時点での全銘柄の予測値を合計すると0になるという知識を使ったものです。 上記以外のアプローチとして、6位と14位の解法では、単純に各時点で全銘柄の予測値の合計を引くという処理を入れていたようです。 バリデーション戦略 今回確認した上位解法の全てが、バリデーション戦略として、学習期間の最後の部分を検証用データとする時系列分割(time-series split)を採用していました。 Public LB・Private LBともに、評価用データは学習期間よりも後の時期のデータとなっており、CVのようなランダム分割ではなく、より新しいデータでバリデーションすることで評価期間での精度を正確に推測することが重要だったようです。 他の解法ではCVを採用したものもあり、今回のようなタスクではCVを使うか、時系列分割を使うか、あるいは CPCV などのように、より複雑なものを使うか、判断が難しかったのではないかと思われます。 まとめ 上位解法の内容から、今回のコンペのキーになったと考えられるのは下記のポイントです: 特徴量:ドメイン知識に基づく特徴量エンジニアリング、実行制約がある中で有効な特徴量をできるだけ多く使うための実装上の工夫 モデルの多様性:GBDT + GRU/CNN(時系列方向のパターンを捉える)+ Transformer(クロスセクション:銘柄間の関係性を捉える) 再学習(オンライン学習):非定常なタスクにおいて、新たに手に入るターゲットデータを使ってモデルを更新 後処理:ドメイン知識に基づく予測値の後処理 バリデーション戦略:非定常なタスクにおいて、評価期間に近い期間のデータを使ったバリデーション 今回のように4000チーム以上が参加するような大きなコンペでは、特徴量、モデル、推論時に手に入るターゲットを使った再学習、予測値の後処理、バリデーション戦略と、全方位で高いクオリティの解法を作り上げる必要があるのだなと感じました。 今回確認した上位解法の全てがソロ参加のチームによるもので、3名がGrandmaster、1名が Masterの称号を獲得されているようです。 さらに、1位を獲得した hydさん は、本記事執筆時点でコンペ部門のランキングが世界1位となっています。 強者があらゆる創意工夫を尽くし、上位を争ったという意味で、非常に良いコンペだったと思いますし、自分にとっても上位解法の知見は学びが多かったです。 次回金融系のコンペが開催されたら、ぜひ次こそ自分も参加してみたいと思います。
アバター
こんにちは!Insight Edgeの小林まさみつです。 Insight Edgeでは、単一のプロジェクトでバックエンドとフロントエンド両方の開発を担当することがあります。 開発時にはバックエンドとフロントエンドをうまく連携することが求められます。 その際、それぞれで型情報を定義すると多重管理することになり、管理の手間がかかることに加えて整合性が保ちづらくなります。 本記事では、型情報を含むAPIをスムーズに連携することでこれらの問題を解決し、開発プロセス全体の効率化を実現する方法を紹介します。 目次 1. 概要 2. 使用する主要な技術 3. 本記事で扱うデータモデル 4. バックエンドの型情報と FastAPI の役割 5. フロントエンド開発の効率化 6. バックエンドとフロントエンド間の型同期 7. 注意点 8. まとめと今後の展望 1. 概要 型付けの重要性 型情報を明示することは、ソフトウェアをより堅牢かつ安全なものとするために重要です。 コードの可読性向上、開発者間のコミュニケーションの明確化、誤解の減少などのメリットがあります。 特に、バックエンドとフロントエンドが同じ型情報を共有することは、APIを介したシステム連携において重要な役割を果たします。 バックエンド・フロントエンド間の型情報連携の重要性 型情報の連携は、複数のシステム(今回はバックエンドとフロントエンド)が互いに連動するプロジェクトにおける整合性を保つ上で欠かせません。 型情報を共有することで、多重管理を避け修正もれや開発スピードの向上が見込めます。 2. 使用する主要な技術 本記事では下記の技術スタックを利用してスムーズな型情報連携を実現します。 openapi-zod-clientを除き、主要な技術については理解している前提で話を進めます。 バックエンド 言語: Python APIフレームワーク: FastAPI 型管理ライブラリ: Pydantic パッケージ管理ツール: poetry Pydanticに関しては本techblogの以下の記事にも登場しているのでご一読ください。 AdhocなPythonコードをProduction-readyにするために心掛けていること FastAPI,Pydantic,AWS DynamoDBを組み合わせた型安全なAPI構築方法について フロントエンド 言語: TypeScript 型管理ライブラリ: Zod 型情報生成: openapi-zod-client パッケージ管理ツール: pnpm 3. 本記事で扱うデータモデル 本記事では、サンプルとして「User」というデータモデルを利用します。 Userモデルは、名前(name)と年齢(age)という2つの属性を持ちます。 ここでの型付けルールは以下の通りです。 名前:1文字以上かつ、10文字以下の文字列であること。 年齢:0歳から130歳までの整数であること。 また、このデータを用いて以下の流れで開発していきます。 ③〜④の部分で、バックエンドで定義した型情報を含むAPIをフロントエンドに連携させます。 4. バックエンドの型情報と FastAPI の役割 ディレクトリ構造について 本記事では、以下のディレクトリ構成となるように進めていきます。 プロジェクトに導入する際には、プロジェクトごとに管理しやすいディレクトリ構成としてください。 / ├── backend/ │ └── app.py ├── frontend/ │ ├── index.ts │ └── services/ │ └── openapiClient.ts # 自動で生成する ├── build_openapi.py ├── openapi.json # 自動で生成する ├── package.json ├── pnpm-lock.yaml ├── poetry.lock └── pyproject.toml ①Pydanticモデルの作成 APIでやり取りをする際にどのような情報を扱うかを明確とするために、まずはPythonで型情報を定義します。 その際、バリデーションを行い型安全を保証するためにPydanticモデルを作成します。 初めにPydanticをインストールします。 poetry add pydantic 次に、Userクラスを作成しモデル定義をします。 # ./backend/app.py from pydantic import BaseModel, Field class User (BaseModel): name: str = Field(..., min_length= 1 , max_length= 10 ) age: int = Field(..., ge= 0 , le= 130 ) 以上で、要件を満たすPydanticモデルの定義ができました。 このようにすることで条件を満たさない入力がUserに対して行われた際、例外を返すようになります。 ②APIルートの作成 次に、作成したPydanticモデルを取得するためのAPIを作成します。 まずはFastAPIとそれを実行するためのuvicornをインストールします。 poetry add fastapi uvicorn 次に、FastAPIのルートを定義します。 今回は簡単にするため、APIは以下の仕様とします。 ルートへのgetリクエストを受け取り、固定のUserを返却する。 # ./backend/app.py (追記) from fastapi import FastAPI app = FastAPI() @ app.get ( "/" , response_model=User) def get_user () -> User: return User(name= "masam" , age= 20 ) 以下のコマンドを実行し、アプリを起動します。 その後、ブラウザからアクセスすることでAPIを呼び出せることを確認します。 poetry run uvicorn backend.app:app --host 0.0.0.0 http://localhost:8000/ にアクセスすると以下の内容が表示されます。 {"name":"masam","age":20} ③OpenAPIドキュメントの生成 作成したPydanticモデルとFastAPIルートを基にOpenAPIドキュメントを生成します。 OpenAPIドキュメントはFastAPIの機能を用いることで生成できます。 ルートディレクトリに build_openapi.py を作成し、以下を記述します。 ./build_openapi.py import json from backend.app import app with open ( 'openapi.json' , 'w' ) as f: json.dump(app.openapi(), f, ensure_ascii= False ) その後 build_openapi.py を実行します。 python build_openapi.py ルートディレクトリに openapi.json が生成されていることを確認します。 作成したAPIのコードからOpenAPIドキュメントを生成できました。 以上でバックエンド側の作業は完了です。 5. フロントエンド開発の効率化 ④Zodスキーマの生成 生成したOpenAPIドキュメントを基にZodスキーマとAPIクライアントを生成します。 まずは生成するためのライブラリと、ターミナル上でTypeScriptを実行するためのライブラリをインストールします。 pnpm add openapi-zod-client zod @zodios/core ts-node 次に、簡単に生成できるようにするため、 package.json のscriptsに以下を追加します。 "generate-openapi-zod-client": "openapi-zod-client ./openapi.json -o frontend/services/openapiClient.ts -a" 上記コマンドを実行する前に、生成するクライアントコードを配置するディレクトリを作成します。 mkdir -p frontend/services これで準備が整いました。 設定したスクリプトを実行し、必要なコードが生成されることを確認します。 pnpm generate-openapi-zod-client 以下のようなコードが生成されます。 frontend/services/openapiClient.ts import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; const User = z .object( { name : z.string().min( 1 ).max( 10 ), age : z.number().int().gte( 0 ).lte( 130 ), } ) .passthrough(); export const schemas = { User , } ; const endpoints = makeApi( [ { method : "get" , path : "/" , alias : "get_user__get" , requestFormat : "json" , response : User, } , ] ); export const api = new Zodios(endpoints); export function createApiClient ( baseUrl : string , options? : ZodiosOptions ) { return new Zodios(baseUrl, endpoints, options); } ⑤Zodを用いた開発 次に、生成されたAPIを実行するコードを作成します。 今回はAPIから受け取ったデータをコンソールに出力するだけにします。 // frontend/index.ts import { z } from "zod" ; import { createApiClient, schemas } from "./services/openapiClient" ; const BASE_URL = "http://localhost:8000" ; type UserType = z.infer < typeof schemas.User >; const getUser = async () => await createApiClient(BASE_URL).get_user__get(); getUser(). then (( user : UserType ) => { console .log( `name: ${ user. name} , age: ${ user.age } ` ); } ); 以下のコマンドで実行し、データを受け取れることを確認します。 ※取得できない場合はFastAPIを動かすuvicornが起動していない可能性があるので再度ご確認ください。 npx ts-node frontend/index.ts 6. バックエンドとフロントエンド間の型同期 これまでの内容により、バックエンドで定義した型情報をフロントエンドに取り込むことができました。 システム開発においては1度取り込んで終わりというわけにはいきません。 システム改修に伴い型情報や、APIでやり取りするモデルを変更することが多々あります。 特に、バックエンドとフロントエンドの両方を並行して開発している際には高頻度でモデルが変わります。 その際にモデル変更に対して素早く追随するために、簡単に同期をとるためのコマンドを開発サイクルに組み込むと良いです。 今回作成したコードを利用すると、以下の2コマンドを実行することで実現できます。 python build_openapi.py pnpm generate-openapi-zod-client 7. 注意点 現時点ではPydanticで定義したカスタムバリデーションをZodへ同期できていません。 そのため、厳密なバリデーションに関しては追加で実装するか、諦めてAPI側で担保するかの方針を決める必要があります。 この問題に対する解決策をご存じの方がいらっしゃれば、ぜひフィードバックをお寄せください。 8. まとめと今後の展望 バックエンドで定義した型情報を活用してフロントエンドコードを簡単に実装する方法を解説しました。 この連携がスムーズに行われれば、バグのリスクを減らしつつ開発速度を劇的に向上させることができます。 今後もAPIを介した型情報の活用して開発速度を上げていこうと思います。
アバター
はじめまして!2024年5月よりInsight EdgeにジョインしたData Scientistの市川です。 まだ入社して間もないですが、オルタナティブデータを活用した案件や、金融市場のオプション等を活用した分析などに携わっております。 今回は、先日人工知能学会(JSAI2024)に行ってきましたので、そのレポートをさせて頂きます。 イベントの概要 発表の概要 [2J1-KS-19] 金融分野における大規模言語モデルの活用 [2A2-PS-2] 進化する大規模言語モデル [2O4-OS-25a] 不動産とAI [2O4-OS-25a-01] 住宅価格予測モデルの経時的な精度の変化分析 [2O4-OS-25a-02] 地理空間ネットワークデータと機械学習を用いた説明可能性の高い賃料予測 [2O4-OS-25a-03] 機械学習を用いた物件設備スコアの推定:不動産データを使用したケーススタディ [2O4-OS-25a-04] Stable Diffusionによる部屋間取りを保持したホームステージング画像生成 [2I6-GS-10] AI応用:金融 [2I6-GS-10-01] 会社四季報のセンチメントを用いた株式銘柄選定の試み [2I6-GS-10-02] 事業セグメント関連語の拡張による金融文書に対するセグメント別センチメント分析の改善 [2I6-GS-10-03] コーポレート・ガバナンスに関する報告書のテキストマイニングによる人的資本情報開示の分析 [2I6-GS-10-04] 大規模言語モデルを利用したパンデミック期の事業等のリスクの記述分析 [2I6-GS-10-05] Graph Based Entropyと領域間相互作用を用いた株式市場の異常検知 [3Xin2] ポスターセッション1 [3Xin2-83] LLM-Traders : 大規模言語モデルを用いた金融時系列予測 [3Xin2-36] 大規模言語モデルの金融投資意思決定バイアスに関する評価指標の構築 [3Xin2-68] 衛星画像による雲量を用いた電力需要予測の基礎検討 [3A4-PS-3] AIは『鉄腕アトム』の夢をみるか?~生成AIによるコンテンツ制作の可能性と問題 [3D5-GS-2] 機械学習:時系列 [3D5-GS-2-04] 階層型ネットワークの階層構造推定と異常検知への応用 金融時系列データの階層構造分析より金融市場異常検知とその原因分析 [3D5-GS-2-05] 主成分等価法による残差リターン抽出 雑感 イベントの概要 人工知能学会 は1986年7月に設立された学術研究団体です。 詳細は上記リンクに譲るのですが、歴史がある学会ではあるものの、かなり広い領域の研究を扱っている認識で、近年LLMへの注目の高まりとともに人が集まっている学会という認識です。 スケジュールは以下の通りでした。 日時:2024年5月28日(火)~31日(金) 会場:アクトシティ浜松(静岡県浜松市) 参加人数は対面とオンラインで3500人超となっているようでして、巨大な学会になっています。2017の名古屋の大会では2000人を初めて超えたという状況を考えると、かなり大きい大会になったなという印象です。また、スポンサーのブースがとても多かった印象で、大学はもとより企業からの注目もかなり高い大会という認識です。 私は今回、29(水)〜30(木)に参加させていただきました。一部ですが、簡単にレポートさせていただきます。 主に、金融分野の研究を見てまいりました。 発表の概要 こちらの研究会はありがたいことに 各発表の概要pdfが公開されています 。 以下、著者の敬称略とさせて頂きます。 [2J1-KS-19] 金融分野における大規模言語モデルの活用 資料は公開されているものとされていないものがあるのですが、このセッションでは、東大の和泉研究室を中心に金融分野におけるLLMの適用についての解説になっていました。 興味深かった話としては、 営業員の練習として、LLMを用いた対話のシミュレーションを検討している RAGを社内に実装することを優先している。社内に散らばっている情報をRAGによって拾えるようにする といった話が、実務上大変興味深かったです。 [2A2-PS-2] 進化する大規模言語モデル NIIの 相澤先生 から、LLMについての説明がありました。 特に日本語に特化した取り組みである、 LLM-JP NII LLM研究開発センター の取り組みの話がありました。 また、 Token Crisis:良質かつ大量のテキストが必要だが、オープンデータでは枯渇しつつある トークナイザーの性能:言語間で不平等が生じている LLMの出現によって、使用頻度が増えてきた表現がある などについても触れられていました。 [2O4-OS-25a] 不動産とAI [2O4-OS-25a-01] 住宅価格予測モデルの経時的な精度の変化分析 〇寺﨑 海翔1、岡本 一志1、柴田 淳司2 (1. 電気通信大学、2. 東京都立産業技術大学院大学) 住宅価格予測モデルの経時的な精度の変化を分析した研究です。新築戸建住宅と賃貸マンションの価格予測において、学習から予測までの期間を設け、その期間の長さと予測精度の関係を明らかにしています。 新規性は、住宅価格予測モデルが時間経過により劣化する様子を定量的に評価した点です。先行研究では、時間経過によるモデルの劣化に焦点を当てた研究は少ないようです。 LIFULL HOME’Sデータセット を用い、学習から予測までの期間を設けることで、時間経過による精度の劣化をMAPE(Mean Absolute Percentage Error)を用いて評価しています。 データ:2015年7月〜2017年6月の新築戸建と賃貸マンションのデータ モデル:Ridge回帰、SVR、LightGBM、kNN 学習データ、検証データ、テストデータを時系列に沿って抽出し、期間を設けて各モデルの精度を評価しました。特に、12ヶ月後の予測精度の劣化を分析し、全てのモデルで劣化が見られることを確認しています。 [2O4-OS-25a-02] 地理空間ネットワークデータと機械学習を用いた説明可能性の高い賃料予測 Bramson Aaron1,2、〇三田 匡能1 (1. 株式会社GA technologies、2. Ghent University) 地理空間ネットワークデータを利用した賃料予測モデルの説明可能性の研究です。 既存のヘドニック価格モデルは単純な分析モデルに頼ることが多く、予測精度が低い傾向にあります。 新規性は、LightGBMを使用して、説明変数のみに依存するハイブリッドアプローチを採用している点です。 使用したデータは、 東京の住宅賃料データ(国道16号の内側) 地理空間データと交通ネットワークデータ 上記のデータなどから算出した、地理空間特徴量(駅、店舗、緑地など)のスコア モデルはLightGBMで、これらの特徴量が予測精度に与える影響を分析しています。 [2O4-OS-25a-03] 機械学習を用いた物件設備スコアの推定:不動産データを使用したケーススタディ 〇宋 宛丘1、尾𥔎 幸謙2、山内 翔大1、三田 匡能1、福中 公輔1 (1. 株式会社GA technologies、2. 筑波大学) マンション投資物件の設備状況を評価するための機械学習モデルを開発し、評価結果を数値化して再現することが目的です。 専門家の主観的な設備評価を基に採点表を作成し、その採点表を用いて機械学習モデルを訓練しました。 新規性は、専門家の主観的な評価を数値化して機械学習モデルに反映させる点のようです。 また、設備に関するデータが欠測している場合でも高精度な予測を行える点も特徴的です。 データと前処理については、 専門家へのインタビューを通じて作成した設備スコア採点表 販売図面 物件の構造化データ データが欠測している場合の処理方法として、欠測値を「numpy.nan」や「-1」を代入 モデルはLightGBMです。 350件の物件データを用いてモデルを訓練し、交差検証法(LOOCV)を用いてモデルの性能を評価しています。 評価指標としてRMSE(Root Mean Squared Error)や決定係数(R2)を使用し、複数のモデル間で性能を比較しています。 また、欠測データを生成して、欠測比率が予測精度に与える影響も見ています。 [2O4-OS-25a-04] Stable Diffusionによる部屋間取りを保持したホームステージング画像生成 〇服部 翔1、山崎 俊彦2 (1. アットホームラボ株式会社、2. 東京大学) 元々、ホームステージング画像を生成しようとすると、エアコンが消えてしまったり、間取りが変わったりしてしまっていた状況だったようです。 そこで、Stable diffusionに 家具配置LoRA 間取り保持Centrol Net 建具保持Control Net を準備し、そのような課題の解決を目指しています。 [2I6-GS-10] AI応用:金融 [2I6-GS-10-01] 会社四季報のセンチメントを用いた株式銘柄選定の試み 〇鈴木 雅弘1,2 (1. 日興アセットマネジメント株式会社、2. 東京大学) 会社四季報(日本の上場企業の動向をまとめたデータブック)のテキストデータからセンチメントスコアを算出し、そのスコアを基に株式銘柄を選定する研究です。 大規模言語モデル(LLMs)や金融極性辞書など、複数の手法を用いてセンチメントを計算し、それが株式の超過リターンに与える影響を分析しています。 四季報のテキストデータを用いてセンチメントスコアを算出する先行研究は少ない状況です。 既存のデータセットを学習したモデルやChatGPTを用いた複数のセンチメント算出手法を比較しており、特に小型銘柄に対して、高いセンチメントスコアが高い超過リターンを示すことを明らかにしています。 センチメントスコアの算出方法は、 金融極性辞書: テキストを形態素に分割し、各形態素のスコアを合計 chABSAモデル: 特定の語に対するセンチメントをラベル付けしたデータセットを学習 景気ウォッチャー調査モデル: 景気に敏感な人々のアンケートデータを基に5段階の分類タスクを学習。日本語DeBERTaV2、日本語Llama2を使用。テストデータによる評価のみを行うモデルとしてChatGPT、GPT-4を使用 ChatGPT: 直接入力テキストに対してセンチメントスコアを出力する方法 センチメントスコアと株式の超過リターンの関係を見るために、四季報の発行日から次の発行日の前日までの各企業のリターンを計算し、センチメントスコアの高低による超過リターンの違いを比較しています。 結果として、センチメントスコアの高い企業は高い超過リターンを示す傾向が確認されました。 [2I6-GS-10-02] 事業セグメント関連語の拡張による金融文書に対するセグメント別センチメント分析の改善 〇平松 賢士1、伊藤 友貴2 (1. 株式会社アイフィスジャパン、2. 三井物産株式会社) 金融文書に対して「ゼロショットで」事業セグメントごとのセンチメント分析を改善するための手法を提案しています。 LLMを用い、有価証券報告書などの文書から事業セグメント関連の語句を抽出し、既存の手法よりも精度の高いセンチメント分析を実現することが目的です。 新規性は、事業セグメント関連語の拡張することで、サービス名や関係会社名など、従来の手法では見逃されがちな情報も抽出可能になったことや、教師なしでの手法を開発し、新規上場企業や組織改組などの変化に柔軟に対応したことです。 事業セグメント関連語の拡張として、有価証券報告書から事業セグメントの説明記載を抽出し、セグメント関連語を用いて文書内の関連文を検索 抽出された段落に含まれる事業セグメント関連語に基づき、ポジティブ、ネガティブ、ニュートラルのセンチメントを付与。 実際のアナリストレポートを用いて提案手法を検証し、提案手法が既存のベースライン手法やGPTモデルを単独で使用した場合よりも高いF1スコアを達成していることに加え、アナリストレポートと決算短信の両方を用いた検証で、関連語の拡張によるセンチメント分析の精度向上を確認しています。 [2I6-GS-10-03] コーポレート・ガバナンスに関する報告書のテキストマイニングによる人的資本情報開示の分析 紙谷 青1、松岡 深雪1、〇中居 拓海1 (1. 大阪市立大学) コーポレートガバナンス報告書における人的資本管理に関する情報をテキストマイニングによって分析しています。 2023年3月期の財務諸表における人的資本管理情報の開示が義務付けられたことから、ステークホルダーの関心が高まっていることが背景です。 新規性は、年次有価証券報告書ではなく、コーポレートガバナンス報告書からの人的資本情報の分析をした点です。 階層クラスタ分析:KHCoderを使用して、特定のキーワード間の強い関連を調査 共起ネットワークグラフ:形態素解析はMeCabを用い、共起関係を視覚化 [2I6-GS-10-04] 大規模言語モデルを利用したパンデミック期の事業等のリスクの記述分析 〇梅原 武志2,3、武田 英明1,2 (1. 国立情報学研究所、2. 総合研究大学院大学、3. 株式会社日経リサーチ) 日本の上場企業の有価証券報告書における新型コロナウイルス感染症(COVID-19)のパンデミック前後での感染症リスク記述を、LLMを用いて分析する研究です。 新規性は、 有価証券報告書の「事業等のリスク」に関する定性的な記述情報を、LLMを用いて体系的に分析 特に感染症リスクに焦点を当て、パンデミック前後のリスク認識の変化を時系列で可視化 事前のデータラベルのアノテーションやモデルの追加学習を行わず、公開情報のXBRLファイルと学習済みモデルのみを利用している 手法としては、 2016〜2018年度(コロナ前)と2019〜2021年度(コロナ後)の有価証券報告書のXBRLファイルを使用 有価証券報告書の「事業等のリスク」のセクションから、「可能性があります」を含む記述を抽出 金融BERT(東大和泉研)の学習済モデルを用いて分散表現を作成し、K-means法でクラスタリング Sentence BERTを用いてリスク記述文を再度分散表現化 PacMAPを用いて2次元空間上に次元縮約し、コロナ前後のデータ構造の差異を可視化 のプロセスで実施しています。 感染症リスクの記述件数を年度別に集計し、パンデミック前後での変化を確認したことや、次元縮約後のデータをプロットし、コロナ前後の記述内容の変化を視覚的に分析しています。 [2I6-GS-10-05] Graph Based Entropyと領域間相互作用を用いた株式市場の異常検知 〇中田 喜之1、吉野 貴晶1、杉江 利章1、夷藤 翔1、関口 海良2、劉 乃嘉2、大澤 幸生2 (1. ニッセイアセットマネジメント株式会社、2. 東京大学) 株式市場における異常検出を目的とし、Graph Based Entropyと領域間相互作用を使用してリスク指標を計算する方法の研究です。 長期的なビジネスサイクルに関連する株式の売却に偏りがあることを定量化し、これを基にリスク指標を導出ししていることに特徴があります。 TOPIX 500、S&P 500、STOXX Europe 600という3つの株価指数を対象に、既存のリスク指標(ボラティリティ、流動性、相関性)と比較したところ、既存の指標では捉えられないリスクを検出できる可能性が示されています。特に、下方リスクを抽出できているようです。 ランダム行列理論とLouvain法を用いて、強い関係を持つ株式群を抽出 構成銘柄を領域分けする 上記2点から、領域間相互作用を計算 上記を週次ごとに計算し、その性質を評価 [3Xin2] ポスターセッション1 [3Xin2-83] LLM-Traders : 大規模言語モデルを用いた金融時系列予測 伊藤 克哉1、〇中川 慧2 (1.三井物産株式会社、2.野村アセットマネジメント株式会社) 金融時系列の分析にLLMを使用する新しいアプローチの研究です。 ノイズの多いデータ、複雑かつ多様なモデル、そして常に変化する市場動向という3つの主要な課題に対処するために、LLMのファインチューニングとプロンプトエンジニアリングを組み合わせた方法を提案しています。 手法として、 時系列データを用いた教師ありfine-tuning:時系列データを文字列データに加工して、few-shot学習を行う 遺伝的アルゴリズムを用いて、ランダムにプロンプトを生成する。良いパフォーマンスだったプロンプトを生存させ、良いプロンプト同士を交雑させつつ、ランダムにプロンプトを発生させ続ける 動的アンサンブル法: 複数のプロンプト応答を組み合わせることで、分析の精度と適応性を向上 データはKaggleの「JPX Tokyo Stock Exchange Prediction」コンペティションのデータです。 LLM-Tradersは他のモデル(例えば、Lasso、LightGBM、RandomForest)と比較して優れたパフォーマンスを示し、特にAccuracy(ACC)やCorrelation(CORR)などの指標で高い評価になったようです。 [3Xin2-36] 大規模言語モデルの金融投資意思決定バイアスに関する評価指標の構築 〇立花 竜一1、中川 慧2、伊藤 友貴3、高野 海斗2 (1.三井情報株式会社、2.野村アセットマネジメント株式会社、3.三井物産株式会社) LLMが金融教育支援サービスにおいて、投資助言におけるバイアスを持つ可能性を評価するための指標を開発が目的です。 行動経済学の概念を用いて、リスク選好、時間選好、社会的選好に基づいたバイアス評価手法を提案し、実際にGPT-3.5およびPaLM2といったLLMを対象に評価を行っています。 評価メトリクスの開発: リスク選好、時間選好、社会的選好に基づいてLLMのバイアスを評価するメトリクスを開発 プロンプト設計: プロフィール情報(年齢、国籍、性別)と選好質問を組み合わせたプロンプトをLLMに入力し、出力を分析 統計分析: 出力結果を統計的に分析することで、LLMが持つバイアスの存在を定量化 GPT-3.5とPaLM2に対して、提案した評価手法を適用し、各種選好(リスク、時間、社会)に基づく出力結果を分析しています。 具体的な質問リストを用い、年齢、国籍、性別ごとに出力結果を比較・統計分析し、各LLMのバイアスの傾向を評価しています。 [3Xin2-68] 衛星画像による雲量を用いた電力需要予測の基礎検討 〇新沢 陸1、浦野 昌一1 (1.明治大学) 衛星画像を用いて電力需要予測を行うための基礎研究です。 衛星画像から雲量データを計算し、電力需要予測の特徴量として使用することで、予測の精度向上と衛星画像の有効性を確認することを目的としています。 従来の電力需要予測は、主に気象データや過去の需要データに依存していましたが、衛星画像という新しいデータソースを取り入れることで、予測精度の向上を図っています。 また、雲量データを具体的に利用することで、天候の変動が電力需要に与える影響をより詳細に把握しようとしています。 衛星画像から計算した雲量データを実際の電力需要予測に適用し、その予測精度を評価しています。 [3A4-PS-3] AIは『鉄腕アトム』の夢をみるか?~生成AIによるコンテンツ制作の可能性と問題 手塚 眞1 (1. 有限会社ネオンテトラ) NEDOの研究として、慶應大学の栗原教授と共同で プロジェクトを行っている (TEZUKA2020, TEZUKA2023)。 現状のAIの力に限界があることを理解しつつも、積極的に活用しているようでした。粘り強く取り組まれている印象でした。 「AIでは手塚治虫のような作品は作れない」という話に対し「人間でも無理ではないですか?」という質問があったのが印象的でした。 → ある程度平均的な作品と、将来にインパクトを与える高度なクリエイティブを分けて考える必要があるとの主張でした。 [3D5-GS-2] 機械学習:時系列 [3D5-GS-2-04] 階層型ネットワークの階層構造推定と異常検知への応用 金融時系列データの階層構造分析より金融市場異常検知とその原因分析 〇劉 乃嘉1、大澤 幸生1、関口 海良1、吉野 貴晶2、杉江 利章2、中田 喜之2、夷藤 翔2 (1. 東京大学、2. ニッセイアセットマネジメント株式会社) 金融市場の異常検知に階層型ネットワークモデルを適用する手法を提案です。 確率的ブロックモデル(SBM)を拡張して、階層型確率的ブロックモデル(HSBM)を構築し、市場構造の変化を検出することを目指しています。これにより、リーマンショックやコロナショックのような大規模な市場変動前の構造変化を早期に検出することが可能となります。 既存のLouvain法や単層のSBMに対して、階層型モデルを導入することで、より詳細かつ早期に市場構造の変化を検出できる点が新規性となります。 研究のポイント: 階層型確率的ブロックモデル(HSBM)の構築: 既存のSBMを拡張し、複数の階層を持つモデルを構築 階層構造推定: 下層のブロックを上層のノードと見なし、層ごとの隣接行列を用いて階層構造を推定 異常検知: 構造変化を検出し、その変化の原因を階層構造を通じて分析 S&P100の株式インデックスを対象に、リーマンショック(2008年)とコロナショック(2020年)の前後の構造変化を分析し、Louvain法およびSBMと比較して、HSBMがより早期かつ明確に構造変化を検出できることを示しました。 [3D5-GS-2-05] 主成分等価法による残差リターン抽出 〇今城 健太郎1、中川 慧2、的矢 知樹1、平野 正徳1、青木 雅奈2、今長谷 拓2 (1. 株式会社Preferred Networks、2. 野村アセットマネジメント株式会社) 金融資産リターンの共通ファクターに含まれない残差リターンを抽出する新しい手法の提案です。 この手法は主成分分析(PCA)に基づいており、従来のPCAが持つ問題点を解決するために、リターンを2つのグループに分けて解析します。 従来のPCAは共通ファクターの数を事前に決める必要があり、ファクターの数を多くするとノイズも増えるという問題がありました。 提案手法は、リターンを2つのグループに分け、一方から主成分を抽出し、もう一方から固有値を推定することで、安定的に良質な残差リターンを抽出できるようです。 手法: リターンをランダムに2つのグループに分ける 一方のグループから主成分を抽出し、もう一方のグループから固有値を推定 変換行列を作成し、残差リターンを抽出 複数回の試行で得られる変換行列の平均を使用 人工データおよび実際の市場データを用い検証した結果、 人工データでは、PCA法と比較して固有値の推定がより正確 実データを用いた検証では、提案手法が従来のPCA法よりも良質な残差リターンを抽出可能 という結果になったようです。 雑感 上記で書かせていただいたもの以外にも、大変高名な先生の基調講演があったり、ポスターセッションも素晴らしい発表ばかりでした。 夜は懇親会等にも顔を出させて頂きまして、大変勉強になりました。 企業のスポンサーも多く、webを見ますと96の組織が出展したようでした。企業側の注目度も高い大会だったように思います。
アバター
はじめに Insight EdgeのLead Engineerの日下です。 弊社ではフロントエンドのスクラッチ開発にReactを採用することが多いのですが、フロントエンドの保守性はしばしば課題となっています。 というのも、要求仕様が曖昧なPoC(Proof of Concept)の段階からMVP(Minimum Viable Product)として開発を進めることも多く、 ビジネス側ユーザの意見にも左右されながらアプリを改善していくため、画面のレイアウトやデザイン、画面遷移の変更が多発するためです。 こうした状況の中でスピードと品質を両立するためには良質なテストコードが不可欠なのはもちろん、 プロダクトコードとテストコードの双方とも、変化に対応しやすく作る必要があります。 過去にテスト駆動開発を挫折した要因 実は、筆者は過去にReactのテスト駆動開発の実践、および社内普及活動に挑戦したことがあったものの、以下のような理由で挫折してしまいました。 要因① テストコードの書き方を調べるのに時間がかかり、開発のスピードが落ちた Reactのテスト駆動開発に挑戦した際、最初に直面した大きな壁はテストコードの書き方を理解することでした。 React Testing LibraryのWebサンプルや書籍を参考にしながらテストコードを書いていましたが、 複雑なDOM構造のテストや非同期処理のテストをしようとすると初歩的な教材では対応できず、 プロダクトコードは書けてもテストコードをスムーズに書けないという状況に陥ってしまいました。 特に、UIの操作が関係するテストの書き方は複雑になることが多く、 テストコードの書き方を調べるのに時間がかかり、開発のスピードが落ちてしまいました。 要因② 画面の仕様変更が頻発し、テストコードの保守が重荷になった 前述の通り、弊社の開発では画面の仕様変更が頻発するため、テストコードの保守性が重要です。 しかし、ReactテストプログラムのWebサンプルや書籍では、ロジックと画面レイアウトが密結合になっている入門的なコンポーネントの例が多く紹介されていました。 それを参考にしたことで画面の仕様変更に対してテストコードの修正箇所が多くなってしまい、テストコードの保守が重荷になってしまいました。 要因③ ロジックとビューの分離を難しく考えすぎた 上記の問題は、ロジックとビューが密結合だったことに起因するため、ロジックとビューの分離を試みました。 Reactの場合、ロジックはカスタムフックとしてコンポーネントの外に切り出すことが一般的です。 しかし、どういう固まりで切り出して1つのカスタムフックにするかの考え方に悩んだり、 ロジック部分をhooksディレクトリに移動したことでコーディング時のファイル切り替えの手間が増えたりしてしまいました。 その結果、テストは書きやすくなったものの開発効率は上がりませんでした。 再挑戦への道筋 はてさて、挫折した当時から時代は移り変わり、今ではGitHub CopilotやChatGPTなどの生成AIを活用できるようになりました。弊社ではGitHub Copilot Businessを導入しており、全エンジニアが利用できます。 今なら、、、今ならあの頃のリベンジを果たせるかもしれない! というわけで、今回はAIの力を借りてReactのテスト駆動開発に再挑戦してみることにしました。 リベンジに向けての方針は以下の通りです。 方針① はじめの一歩をAIに書いてもらう 生成AIを活用すれば、プロダクトコードからテストコードを生成できます。 仕様を網羅する完璧なテストケースとまではいかなくとも、典型的なテストケースを生成してもらえれば、 あとはそれを参考にして入出力値のバリエーションを変えたテストを追加するだけで良いため、 テストコード作成にかかる時間を大幅に短縮できるはずです。 方針② 完璧なテスト駆動にこだわらない テスト駆動開発は本来、テストコード→プロダクトコードの順に開発を進めますが、 そもそもプロダクトコードは書けるけどテストコードを書けないことが当初の挫折要因だったので、 テストコード作成で生成AIの力をフル活用したいところです。 そこで、まずは最低限の単純なプロダクトコードを書いて、AIに初期テストコードを書いてもらうことにします。 その上で、仕様を追加する際には生成されたテストコードを参考にすれば、テストコードの書き方に悩むことなくテスト駆動で開発を進められます。 また、画面レイアウトやUIコンポーネント等は動くモックアップとしてコーディングしながら仕様を固めていくことも多いため、テスト駆動で開発する対象はロジック部分に注力します。 ビュー部分については仕様が固まってからスナップショットテストを追加することとし、回帰テストを主目的とします。 (本記事ではビューのテストの詳細は割愛します。) 方針③ ロジックとビューの分離を単純化する ロジックとビューを分離してReactコンポーネントを記述しようとすると、そのコンポーネントでしか使われない(であろう)カスタムフックが多くなります。再利用性の高いロジックであれば単独のカスタムフックに切り出すことも重要ですが、一度しか使わないロジックまでhooksディレクトリに切り出すのは過剰な分離となり、凝集度が下がってしまいます。 そこで、再利用可能ロジックの部品化という観点とは無関係に、同一ファイル内でもできる単純なロジックとビューの分離方法をパターン化することで、デメリットを減らしつつコンポーネントの保守性を高めることを目指します。 コーディング例 前置きが長くなりましたが、ここからはサンプルコードを交えて上記内容の実践例を紹介します。 テスト対象コンポーネント(Before) ここではサンプルのコンポーネントとして、ごく簡易なユーザ登録フォームのUIを例にして説明します。 主な仕様は以下のとおりです。 メールアドレス、パスワード、パスワード再入力の3つの入力項目と、登録ボタンがある。 各入力項目は全て必須項目とする。 パスワード再入力の値がパスワードと一致しない場合はエラーメッセージを表示する。 入力内容にエラーがある場合、登録ボタンは非活性とする。 登録処理の結果をメッセージ領域に表示する。 まずはテストしにくいコードの例として、ロジックとビューが密結合になっているコンポーネントのコードを示します。 実際には、フォームを作るときはフォーム向け特化のhooksライブラリを使ったりしますが、今回はサンプルのため基本的なライブラリのみを使っています。 // import文は省略 export type ComponentProps = { onSubmit : ( data : { email : string ; password : string } ) => void ; signUpErrorMessage ?: string ; } ; export default function SignUpForm ( props : ComponentProps ) { const { onSubmit , signUpErrorMessage } = props; const [ email , setEmail ] = useState( "" ); const [ password , setPassword ] = useState( "" ); const [ passwordToConfirm , setPasswordToConfirm ] = useState( "" ); // パスワード再入力不一致のエラーメッセージ // メッセージ文字列の外部定義化は今回の論点ではないので、ここでは直接記述 const passwordToConfirmErrorMessage = password === passwordToConfirm ? "" : "Passwords do not match." ; // 入力エラーの有無(Submitボタンの活性/非活性に利用) const hasError = [ // 未入力チェック !email, !password, !passwordToConfirm, // エラーチェック passwordToConfirmErrorMessage, ] . some (( b ) => b); const handleSubmit = ( e : FormEvent ) => { e. preventDefault (); onSubmit( { email , password } ); } ; return ( < form onSubmit = { handleSubmit } > < div > < TextInput label = "Your Email" type = "email" value = { email } onChange = { ( e ) => setEmail(e. target .value) } /> </ div > < div > < TextInput label = "Your Password" type = "password" value = { password } onChange = { ( e ) => setPassword(e. target .value) } /> </ div > < div > < TextInput label = "Confirm Password" type = "password" value = { passwordToConfirm } onChange = { ( e ) => setPasswordToConfirm(e. target .value) } error = { passwordToConfirmErrorMessage } /> </ div > < Button type = "submit" disabled = { hasError } > Sign Up </ Button > { signUpErrorMessage && < Alert color = "red" > { signUpErrorMessage } </ Alert > } </ form > ); } このコードに対してCopilot Chatでテストを生成してもらうと、以下のようなテストコードが生成されました。 // import文は省略 describe ( "SignUpForm" , () => { let mockSubmit: jest.Mock < void , [{ email : string ; password : string }] >; beforeEach (() => { mockSubmit = jest.fn(); } ); it ( "フォームのレンダリング" , () => { const { getByLabelText } = render( < SignUpForm onSubmit = { mockSubmit } /> ); expect (getByLabelText( /your email/i )).toBeInTheDocument(); expect (getByLabelText( /your password/i )).toBeInTheDocument(); expect (getByLabelText( /confirm password/i )).toBeInTheDocument(); } ); it ( "パスワードが一致しない場合、送信されない" , async () => { const { getByLabelText , getByText } = render( < SignUpForm onSubmit = { mockSubmit } /> ); fireEvent.change(getByLabelText( /your email/i ), { target : { value : "test@example.com" } , } ); fireEvent.change(getByLabelText( /your password/i ), { target : { value : "password" } } ); fireEvent.change(getByLabelText( /confirm password/i ), { target : { value : "different" } , } ); fireEvent.click(getByText( /sign up/i )); await waitFor(() => { expect (mockSubmit).not.toHaveBeenCalled(); } ); } ); it ( "パスワードが一致する場合、送信される" , async () => { const { getByLabelText , getByText } = render( < SignUpForm onSubmit = { mockSubmit } /> ); fireEvent.change(getByLabelText( /your email/i ), { target : { value : "test@example.com" } , } ); fireEvent.change(getByLabelText( /your password/i ), { target : { value : "password" } } ); fireEvent.change(getByLabelText( /confirm password/i ), { target : { value : "password" } , } ); fireEvent.click(getByText( /sign up/i )); await waitFor(() => { expect (mockSubmit).toHaveBeenCalledWith( { email : "test@example.com" , password : "password" , } ); } ); } ); } ); 画面のDOM構造や入力項目のラベル文字列に強く依存したテストが生成されているため、画面レイアウトやラベル文字列が変更されるとテストコードも修正が必要になります。 また、このテストコードを参考にしながらロジックに対してテスト駆動開発をしようとすると、テストコードを書くときにビューのDOM構造を意識する必要があるため、 画面デザインが先に決まっていないとロジックの開発も進めづらいという問題があります。 テスト対象コンポーネント(After) 次に、ロジックとビューを分離したコンポーネントのコードを示します。 同一ファイルの中で、コンポーネントのロジックであるカスタムフック関数と、ビューを描画する関数に分割します。 // import文は省略 export type ComponentProps = { onSubmit : ( data : { email : string ; password : string } ) => void ; signUpErrorMessage ?: string ; } ; export function useComponent ( props : ComponentProps ) { const { onSubmit } = props; const [ email , setEmail ] = useState( "" ); const [ password , setPassword ] = useState( "" ); const [ passwordToConfirm , setPasswordToConfirm ] = useState( "" ); // パスワード再入力不一致のエラーメッセージ const passwordToConfirmErrorMessage = password === passwordToConfirm ? "" : "Passwords do not match." ; // 入力エラーの有無(Submitボタンの活性/非活性に利用) const hasError = [ // 未入力チェック !email, !password, !passwordToConfirm, // エラーチェック passwordToConfirmErrorMessage, ] . some (( b ) => b); const handleSubmit = ( e : FormEvent ) => { e. preventDefault (); onSubmit( { email , password } ); } ; return { email , setEmail , password , setPassword , passwordToConfirm , setPasswordToConfirm , passwordToConfirmErrorMessage , hasError , handleSubmit , } ; } type ComponentHookReturnType = ReturnType < typeof useComponent >; function ComponentView ( { props , hook , } : { props : ComponentProps ; hook : ComponentHookReturnType ; } ) { const { signUpErrorMessage } = props; const { email , setEmail , password , setPassword , passwordToConfirm , setPasswordToConfirm , passwordToConfirmErrorMessage , hasError , handleSubmit , } = hook; return ( < form onSubmit = { handleSubmit } > < div > < TextInput label = "Your Email" type = "email" value = { email } onChange = { ( e ) => setEmail(e. target .value) } /> </ div > < div > < TextInput label = "Your Password" type = "password" value = { password } onChange = { ( e ) => setPassword(e. target .value) } /> </ div > < div > < TextInput label = "Confirm Password" type = "password" value = { passwordToConfirm } onChange = { ( e ) => setPasswordToConfirm(e. target .value) } error = { passwordToConfirmErrorMessage } /> </ div > < Button type = "submit" disabled = { hasError } > Sign Up </ Button > { signUpErrorMessage && < Alert color = "red" > { signUpErrorMessage } </ Alert > } </ form > ); } // 親コンポーネントから呼ばれる、このコンポーネントのエントリポイント関数 export default function SignUpForm ( props : ComponentProps ) { const hook = useComponent(props); return < ComponentView hook = { hook } props = { props } /> ; } このコードのカスタムフック関数である useComponent に対してCopilot Chatでテストを生成してもらうと、以下のようなテストコードが生成されました。 // import文は省略 import { useComponent, ComponentProps, } from "@/components/features/file-upload/elements/SignUpFormAfter" ; describe ( "useComponent" , () => { let props: ComponentProps ; let onSubmitMock: jest.Mock ; beforeEach (() => { onSubmitMock = jest.fn(); props = { onSubmit : onSubmitMock, } ; } ); it ( "初期値が正しい" , () => { const { result } = renderHook(() => useComponent(props)); expect (result. current .email).toBe( "" ); expect (result. current . password ).toBe( "" ); expect (result. current .passwordToConfirm).toBe( "" ); } ); it ( "email, password, passwordToConfirmが更新される" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setEmail( "test@example.com" ); result. current .setPassword( "password" ); result. current .setPasswordToConfirm( "password" ); } ); expect (result. current .email).toBe( "test@example.com" ); expect (result. current . password ).toBe( "password" ); expect (result. current .passwordToConfirm).toBe( "password" ); } ); it ( "passwordToConfirmErrorMessageが設定される" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setPassword( "password" ); result. current .setPasswordToConfirm( "password2" ); } ); expect (result. current .passwordToConfirmErrorMessage).toBe( "Passwords do not match." ); } ); it ( "hasErrorが設定される" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setEmail( "" ); result. current .setPassword( "" ); result. current .setPasswordToConfirm( "" ); } ); expect (result. current .hasError).toBe( true ); } ); it ( "handleSubmitでonSubmitが呼ばれる" , async () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setEmail( "test@example.com" ); result. current .setPassword( "password" ); } ); act(() => { result. current .handleSubmit( { preventDefault : jest.fn() } as any); } ); expect (onSubmitMock).toHaveBeenCalledWith( { email : "test@example.com" , password : "password" , } ); } ); } ); ロジックのみを抽出したことによってフックの戻り値の各変数に対してのテストとなり、画面のDOM構造に依存しなくなったためテストコードの保守性が高まりました。 AIが生成したテストケースだけでは入力条件の網羅性は十分ではないものの、生成されたコードを参考にして入力値を変えるだけならば、イチからテストコードを書くよりもずっと容易にテストケースを追加できます。 テスト駆動で機能追加 さて、ここで新たな仕様として以下の機能を追加することを考えます。 メールアドレスの入力値がメールアドレス形式でない場合はエラーメッセージを表示する。 パスワードは8文字以上、英数記号の全てを含むことを必須とし、満たさない場合はエラーメッセージを表示する。 まず、フックの戻り値としてエラーメッセージ用の変数を追加し、固定値として空文字を返すようにしておきます(これだけ先に書いておかないとテストコードが型チェックにひっかかるため)。 // useComponent関数の戻り値 return { email, setEmail, emailErrorMessage: "" , // 追加 password, setPassword, passwordErrorMessage: "" , // 追加 passwordToConfirm, setPasswordToConfirm, passwordToConfirmErrorMessage, hasError, handleSubmit, } ; 次に、テストコードを追加します。 参考にすべきテストコードが既にある状態なので、テストケースの追加は容易です。 簡単なので自分でコーディングしても良いし、AIに書いてもらうこともできるでしょう。 筆者が以下のサンプルコードを書くときは自分で書き始めましたが、GitHub Copilotによる補完が良い感じに効いていました。 追加するテストケースの例を以下に示します。 it ( "Emailフォーマット正常" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setEmail( "test@example.com" ); } ); expect (result. current .emailErrorMessage).toBe( "" ); } ); it ( "Emailフォーマットエラー" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setEmail( "invalid-email-format" ); } ); expect (result. current .emailErrorMessage).toBe( "Invalid email address format." ); } ); it ( "パスワードが基準を満たす" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setPassword( "P@ssw0rd" ); } ); expect (result. current .passwordErrorMessage).toBe( "" ); } ); it ( "パスワードが基準を満たさない(長さ)" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setPassword( "P@ssw0r" ); } ); expect (result. current .passwordErrorMessage).toBe( "Password must be at least 8 characters, and contain upper-case and lower-case alphabet, and number." ); } ); it ( "パスワードが基準を満たさない(数字なし)" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setPassword( "SimpleLongPassword!" ); } ); expect (result. current .passwordErrorMessage).toBe( "Password must be at least 8 characters, and contain upper-case and lower-case alphabet, and number." ); } ); it ( "パスワードが基準を満たさない(使えない文字を含む)" , () => { const { result } = renderHook(() => useComponent(props)); act(() => { result. current .setPassword( "P@ss W0rd" ); } ); expect (result. current .passwordErrorMessage).toBe( "Password must be at least 8 characters, and contain upper-case and lower-case alphabet, and number." ); } ); この時点では、エラーメッセージが固定値なのでテストはもちろん通りません。 フックのロジックを修正して、メールアドレスの入力値がメールアドレス形式でない場合はエラーメッセージを表示するようにします。 たとえば以下のように実装してみました。 // ====== サンプルコードのため同一ファイルに記述 ====== function isValidPassword ( password : string ) { // !から~までの文字コードからなる8文字以上で、大文字、小文字、数字をそれぞれ1つ以上含む const conditions = [ /^[!-~]{8,}$/ , /[A-Z]/ , /[a-z]/ , /\d/ ] ; return conditions. every (( regex ) => regex. test (password)); } function isValidEmail ( email : string ) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/ . test (email); } // ====================================================== export type ComponentProps = { onSubmit : ( data : { email : string ; password : string } ) => void ; signUpErrorMessage ?: string ; } ; export function useComponent ( props : ComponentProps ) { const { onSubmit } = props; const [ email , setEmail ] = useState( "" ); const [ password , setPassword ] = useState( "" ); const [ passwordToConfirm , setPasswordToConfirm ] = useState( "" ); // メールアドレスのエラーメッセージ const emailErrorMessage = isValidEmail(email) ? "" : "Invalid email address format." ; // パスワードのエラーメッセージ const passwordErrorMessage = isValidPassword(password) ? "" : "Password must be at least 8 characters, and contain upper-case and lower-case alphabet, and number." ; const passwordToConfirmErrorMessage = password === passwordToConfirm ? "" : "Passwords do not match." ; const hasError = [ // 未入力チェック !email, !password, !passwordToConfirm, // エラーチェック emailErrorMessage, passwordErrorMessage, passwordToConfirmErrorMessage, ] . some (( b ) => b); const handleSubmit = ( e : FormEvent ) => { e. preventDefault (); onSubmit( { email , password } ); } ; return { email , setEmail , emailErrorMessage , password , setPassword , passwordErrorMessage , passwordToConfirm , setPasswordToConfirm , passwordToConfirmErrorMessage , hasError , handleSubmit , } ; } // ビューの描画関数は省略 これでテストが通るようになり、テスト駆動で機能追加ができました。 まとめ 本記事では、Reactのテスト駆動開発に再挑戦するための方針と、生成AIを活用した実践例を紹介しました。 生成AIを活用してテストコード作成の負担を軽減することでテスト駆動開発を導入するハードルが低くなり、 品質や保守性の高いプロダクトコードおよびテストコードを維持しやすくなることが期待できます。 まだ試行し始めたばかりですが、この方法でプロダクトリリースまで回してみたら、改めて振り返りの記事も書いてみたいと思います。
アバター
init_mathjax = function() { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true } }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ], processEscapes: true, processEnvironments: true }, displayAlign: 'left', CommonHTML: { linebreaks: { automatic: true } } }); MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } } init_mathjax(); addEventListener("DOMContentLoaded",function(){var a=document.getElementsByClassName("entry-content");if(a)for(var i=0;i はじめに Frank-Wolfe法とは 誘導されるスパース性 アルゴリズムの比較確認 問題設定 比較手法 結果 次元数$n = 10^{2}$の場合 次元数$n = 10^{3}$の場合 所感 まとめ 参考文献 参考:使用したコード (著者:分析チーム hawai06) はじめに ビッグデータや機械学習の技術を活用したDX推進/分析プロジェクトでは、計算量が少なく、機械学習に特有な問題構造を上手く取り扱えるアルゴリズムが必要と考えられる。特に弊社が行なっているようなビジネス課題を解決するプロジェクトの場合は、成果物をビジネスへ活用しやすいよう解釈性に富んでいる方が良く、出力がスパースなアルゴリズムが望ましいと思われる。  一方Frank-Wolfe法とは制約付き最適化問題を解くアルゴリズムであり、最適制御や均衡問題など幅広く用いられている。このアルゴリズムは計算量が少なく、さらに機械学習によく現れる制約条件を取り扱い易いため、近年では機械学習やデータサイエンスなどの分野にも適用されている。また出力される解がスパースになることが知られていることから、このアルゴリズムはビジネス課題の解決手法として候補たり得るものであると思われる。  そこで本稿では、Frank-Wolfe法の詳細およびスパース性が誘導される理由を確認するとともに、実務での有用性を簡易な数値実験を通して確認することとする。 Frank-Wolfe法とは  Frank-Wolfe法は、コンパクトな凸集合$\mathcal{C}$上で定義された、平滑で微分可能な関数$f$の最小値を求めるアルゴリズムである。 解の更新方法は適切なステップ幅を$a_k$を用いて として記述できる。ステップ幅は$f$の性質により決められる方法が異なり、連続微分可能であるならば直線探索や関数の曲率を用いて定めても良く、また凸であるならば$a_k = \frac{2}{k+2}$としても良い。  $s_k$を求める際は制約付きの線形関数の最小化問題を解くことになるが、制約条件の構造を考慮することで効率良く解ける。例えば$C = \lbrace x | || x|| \le 1 \rbrace$であるならば、Danskin-Bertsekas's Theoremから双対ノルムの劣微分$\partial || \cdot ||_{*}$を用いて$s_k = - \partial || \nabla f(x_k) ||_{*}$となり、計算で求められる場合は簡易に導出できる。$s_k \in \mathcal{C}$より、$x_{k+1}$は常に実行可能解となる。 誘導されるスパース性 初期点$x_0 = 0 \in \mathcal{C}$、$\mathcal{C} \subset \mathbb{R}^n$を仮定すると、Frank-Wolfe法はその解の更新方法より、$x_k$が$\min \lbrace k, n+1 \rbrace$個のデータ点で記述できるため、$k \ll n$の場合はスパース性を持つと言える。このことから$x_k$の非ゼロ要素数はアルゴリズムの収束レートに関連することがわかる。  スパースな解が得られる例として$\mathcal{C} = \lbrace x | || x ||_1 \le 1 \rbrace$を考えると、$\forall k \in \mathbb{N}$に対して$s_k = \text{sign} \left( \nabla f( x^{k-1} )_{i_{k-1}} \right) \cdot e_{i_{k-1}}$となる。よって$k \le n$の場合は$x_k$は非ゼロ要素数が$k$個となり、収束性が良いほどスパースな解が求まり得る。( ただし、$i_{k-1} = \operatorname{argmax}_{i=1,\dots,n} \left| \nabla f( x^{k-1})_{i} \right|$、$e_i$は$i$番目の要素のみ$1$でそれ以外の要素は$0$のベクトル、$\nabla f( x^{k-1})_{i}$は$\nabla f( x^{k-1})$の$i$番目の要素を指す)。  上記より収束性を向上させることでスパースな解を得られる様になるが、アルゴリズムを工夫することでもスパース性は達成され得る。例えば、Fully-Corrective Frank-Wolfe algorithm (以下、FCFW)は次のように解を更新することでFrank-Wolfe法よりもスパースな解が得られるとの報告がある。 初期値$\mathcal{S}_{0} = \lbrace x_0 \rbrace$とする。このアルゴリズムは、これまでの反復で選んだ頂点集合の凸包で最適化計算して解を更新するため、計算量が多くなるという課題がある。 アルゴリズムの比較確認 問題設定  実務でのFrank-Wolfe法の活用を検討するにあたり、機械学習にてよく見られる$l1$-ballを定義域にした次の問題を題材にする。 $x^{*}$は定義域に含まれないよう、定義域の極点をランダムに選んだ$m$点の凸結合に対して$2$倍したものとした。本数値実験では$n$および$m$を適当に変えた場合の、計算時間、出力された解の非ゼロ要素数の個数および$x^{*}$との$l2$-normを確認し、スパース性および解としての適性を評価した。Frank-Wolfe法は解の更新回数により得られる解のスパース度合い変わりうるため、最大更新回数を$10^{6}$と十分多くした。 比較手法 Frank-Wolfe法(以下、FW)、FCFWのスパース性との比較手法として、射影勾配法(以下、PGD)および一般的な方法としてSLSQPを用いた。FWはステップ幅は$\frac{2}{k+2}$および直線探索を別に用いることにし、前者をFW-naive、後者をFW-lineと表記する。PGDは、高次元の問題設定にも適用できるよう、確率単体への射影を基にした射影方法を採用しており、 こちら に掲載されているコードをまま使用した。またSLSQPを適用するにあたり、$l1$-ballに対してスラック変数を導入して線形緩和した。これら全ての手法はpythonやそのライブラリを用いて実装している。 結果 次元数$n = 10^{2}$の場合 $x^{*}$の非ゼロ要素数$m$変化させた時の結果を下に示す。結果の傾向は$n = 10^{2}$と同様になった。計算時間はFCFWが最も長く、SLSQPが次に長いという結果になった。他手法は十分計算時間が短い。$|| x-x^{*} ||_2$はPGDとSLSQPが一致しており、それぞれ最適解に到達していると考えられる。厳密な直線探索をしているFW-lineはFW-naiveと比較して最適解に近い結果が得られた一方、FW-naiveは次元数を増やしても値が減少していない。アルゴリズムの出力解の非ゼロ要素数を確認すると、PGDとSLSQPは$|| x-x^{*} ||_{2}$と同様に一致している。PGDは$x^{*}$の非ゼロ要素数$m$が$1$の場合、本実験においては数値的に不安定になり、原点を出力していたことから$0$の値を示している。FW-naiveは$m \ge 20$以上の場合、出力される解の非ゼロ要素数が$1$となっている。 次元数$n = 10^{3}$の場合 先と同様に$m$変化させた時の結果を下に示す。計算時間は、$n = 10^{2}$とは逆にSLSQPが最も長く、FCFWが次に長いという結果となった。他手法は$m$を増加させてもほとんど変わらず、$10^{-3}$秒以下で計算が済んでいる。アルゴリズムの出力解の非ゼロ要素数の傾向は$n = 10^{2}$と同様の結果となったが、縦軸を対数スケールにしているため、上述した$m=1$の時のPGDの値$0$は特異な挙動を示している。 所感 本数値実験において、FCFWは$n \le 10^{3}$においても1秒程度で計算が終了しており、最適解に近く、比較的スパースな解が得られたため、有用な手法であると思われる。しかし、$n = 10^{6}$とした予備実験において$m=10^{2}$では計算時間が3.5時間を超え、$m$を大きくするごとに計算時間が増大していった。内部問題の求解ではできる限り解析的に実装したが、大規模な問題への適用の際はCythonで実装するなど更なる工夫が必要である。  また、PGDとSLSQPは最適解が得られていると考えられる。その中でもPGDは計算時間がかなり短かった。予備実験においても、PGDは最も計算時間が短く$1$秒以内に解が得られた。そのため本問題を実務で取り扱う際は、活躍する可能性が非常に高いと思われる。  FWについては、ステップ幅の決定方法により大きく異なる結果が得られた。FW-naiveは出力される解の非ゼロ要素数が$1$などであり、解の更新がほとんどなされていないことがわかる。最適解が定義域の境界の場合、FW-naiveは収束性が非常に悪くなることが知られており、今回の問題設定はこれに該当する。そのためコスト関数の改善が十分小さすぎるために、解の更新がなされないまま、計算の終了基準を満たしてしまったと考えられる。一方、FW-linearは出力される解の非ゼロ要素数はFW-naiveに次いで小さく、また$|| x-x^{*} ||_{2}$も他手法と比してそれほど大きい値ではない。$|| x-x^{*} ||_{2}$を小さくしたい場合は、スパース性が犠牲になるものの計算の終了基準を厳しくすれば良く、実務では柔軟に利用できると考えられる。 まとめ  本稿では、Frank-Wolfe法は他の手法と比較して、得られる解がスパースになることを解の更新方法および数値実験を通して確認した。数値実験は$l2$ノルムを用いた$l1$-ballへの凸射影を題材にしており、厳密な直線探索を用いたFrank-Wolfe法は計算時間・最適解からの距離の観点から、実用的なスパース解が得られたと思われる。また本稿では実装について述べていないが、実装が比較的容易であった。これらのことから、ビッグデータを取り扱うような実務に際して、Frank-Wolfe法は採用しやすい最適化手法であると言える。 参考文献 Bertsekas, Dimitri P. Control of uncertain systems with a set-membership description of the uncertainty. Diss. Massachusetts Institute of Technology, 1971. Bomze, Immanuel M., Francesco Rinaldi, and Damiano Zeffiro. "Frank–Wolfe and friends: a journey into projection-free first-order optimization methods." 4OR 19 (2021): 313-345. Clarkson, Kenneth L. "Coresets, sparse greedy approximation, and the Frank-Wolfe algorithm." ACM Transactions on Algorithms (TALG) 6.4 (2010): 1-30. Combettes, Cyrille W., and Sebastian Pokutta. "Revisiting the approximate Carathéodory problem via the Frank-Wolfe algorithm." Mathematical Programming 197.1 (2023): 191-214. Duchi, John, et al. "Efficient projections onto the l 1-ball for learning in high dimensions." Proceedings of the 25th international conference on Machine learning. 2008. Jaggi, Martin. "Revisiting Frank-Wolfe: Projection-free sparse convex optimization." International conference on machine learning. PMLR, 2013. Lacoste-Julien, Simon. "Convergence rate of frank-wolfe for non-convex objectives." arXiv preprint arXiv:1607.00345 (2016). Pokutta, Sebastian. "The Frank-Wolfe algorithm: a short introduction." Jahresbericht der Deutschen Mathematiker-Vereinigung 126.1 (2024): 3-35. 参考:使用したコード 以下に本数値実験で使用したコードを掲載する。PGDは、先に述べたように こちら のコードを用いているため掲載は割愛する。 FW関連(optimization.py) import numpy as np def generate_problem (optimal_point : np.array): """ Function Explanation: 最適解を指定して、コスト関数とその勾配を生成する関数 Inputs: optimal_point : np.array 最適解 Returns: cost_function : callable コスト関数 grad_cost_function : callable コスト関数の勾配 """ def cost_function (x : np.array): """ Function Explanation: 二乗ノルムを用いたコスト関数 Inputs: x : np.array 点 Returns: (float.) 関数の値 """ return (np.linalg.norm(x - optimal_point, ord = 2 )** 2 )/ 2 def grad_cost_function (x : np.array): """ Function Explanation: 二乗ノルムを用いたコスト関数の勾配 Inputs: x : np.array 点 Returns: (np.array) 勾配 """ return x - optimal_point return cost_function, grad_cost_function def frank_wolfe (x0 : np.array, grad_f : callable , f : callable , step_size : str , max_iter : int = np.inf, tol : float = 1e-6 ): """ Function Exeplanetion: Frank-Wolfe法を用いて、最適化問題を解く関数。 Inputs: x0 : np.array 初期点 f : callable 目的関数 grad_f : callable 勾配関数 step_size : str 'diminish': 2/2+iteration 'line search' : 直線探索 max_iter : int 最大反復回数 tol : float 収束判定の閾値 Returns: x : np.array 最適解 """ # 初期条件の設定 iteration = 1 x = x0 improve = np.inf f_list = [f(x0)] while (iteration < max_iter) and (improve > tol): # 点の設定 x_k = x old = f(x_k) # 更新方向 index = np.argmax(np.abs(grad_f(x))) s_k = np.zeros( len (x)) s_k[index] = -np.sign(grad_f(x)[index]) # ステップ幅 if step_size == 'diminish' : step_size = 2 /(iteration + 2 ) elif step_size == 'line search' : norm_s_k = np.linalg.norm(s_k - x, ord = 2 ) if norm_s_k == 0 : break else : step_size = \ -np.dot(s_k, grad_f(x)) / norm_s_k** 2 if step_size < 0 : step_size = 0 elif step_size > 1 : step_size = 1 # 更新 print (iteration) x = x_k + step_size * (s_k - x_k) iteration += 1 improve = 1 - (f(x) / old ) f_list.append(f(x)) return x #, f_list def find (s, S): for i in range ( len (S)): if np.all(S[i] == s): return True return False def FullyCorrectiveFrankWolfe (x0 : np.array, grad_f : callable , f : callable , optimal_point : np.array, max_iter : int = np.inf, tol : float = 1e-6 , inner_max_iter : int = np.inf, inner_tol : float = 1e-6 ): """ Function Explanation: Fully Corrective Frank-Wolfe法を用いて、最適化問題を解く関数。 Inputs: x0 : np.array 初期点 f : callable 目的関数 grad_f : callable 勾配関数 max_iter : int 最大反復回数 tol : float 収束判定の閾値 optimal_point : np.array 本数値実験の最適解 inner_max_iter : int 内部問題の最大反復回数 inner_tol : float 内部問題の収束判定の閾値 Returns: x : np.array 最適解 """ # 初期条件の設定 iteration = 1 x = x0 S = list () S.append(x) impove = np.inf f_list = [f(x0)] while (iteration < max_iter) and (impove > tol): # 点の設定 x_k = x old = f(x_k) # Sの集合を構築 ## 要素取得 index = np.argmax(np.abs(grad_f(x_k))) s_k = np.zeros( len (x_k)) s_k[index] = -np.sign(grad_f(x_k)[index]) ## 要素の追加 if find(s_k, S) == False : S.append(s_k) # 内部問題を解いて解の更新 inner_improve = np.inf inner_iteration = 1 while (inner_iteration < inner_max_iter) and (inner_improve > inner_tol): # ループ情報の設定 inner_old = f(x) inner_iteration += 1 # 内部問題の求解 inner_index = np.argmin(np.dot(np.array(S), grad_f(x))) norm_s_k = np.linalg.norm(S[inner_index] - x, ord = 2 ) if norm_s_k == 0 : break else : step_size = \ -np.dot(S[inner_index],grad_f(x)) / norm_s_k** 2 if step_size < 0 : step_size = 0 elif step_size > 1 : step_size = 1 x = x + step_size * (S[inner_index] - x) # ループ情報の更新 inner_improve = 1 - (f(x) / inner_old ) # ループ情報の更新 iteration += 1 improve = 1 - (f(x) / old ) f_list.append(f(x)) return x #, f_list if __name__ == "__main__" : # ライブラリの読み込み import os from problem_setting import generate_optimal_point # 最適解の生成 dimensions = 100 combination_num = 10 optimal_point = generate_optimal_point(dimensions, combination_num, in_range = 'out' , out_coef = 2 ) # 問題の生成 f, grad_f = generate_problem(optimal_point) # 最適化アルゴリズムの実行 initial_point = np.zeros(dimensions) print ( "FW" ) FW_solution = frank_wolfe(initial_point, grad_f, f, step_size = 'diminish' , max_iter = 10000 , tol = 1e-6 ) print ( "FC" ) FCFW_solution = FullyCorrectiveFrankWolfe( initial_point, grad_f, f, optimal_point, max_iter = 1000 , tol = 1e-6 , inner_max_iter = 1000 , inner_tol = 1e-6 ) SLSQP(slsqp.py) import numpy as np from problem_setting import generate_optimal_point from optimization import generate_problem from scipy.optimize import minimize import os import copy class SLSQP : def __init__ (self, optimal_point : np.array, dimensions : np.array, initial_point : np.array): self.dimensions = dimensions self.optimal_point = optimal_point self.initial_point = self._generate_initial_point_for_slack_var( initial_point ) def _generate_initial_point_for_slack_var (self, optimal_point : np.array): copy_optimal_point = np.copy(optimal_point) initial_point_for_slack_var = np.concatenate( [optimal_point, copy_optimal_point], 0 ) return initial_point_for_slack_var def _upper_bound_const (self, index): return { 'type' : 'ineq' , 'fun' : lambda x: x[index + self.dimensions] - x[index]} def _genrate_upper_bound_const (self): const_list = list () for index in range (self.dimensions): const_list.append(self._upper_bound_const(index)) return const_list def _lower_bound_const (self, index): return { 'type' : 'ineq' , 'fun' : lambda x: x[index] + x[index + self.dimensions]} def _genrate_lower_bound_const (self): const_list = list () for index in range (self.dimensions): const_list.append(self._lower_bound_const(index)) return const_list def _genrate_l1_bound_const (self): const_list = list () const_list.append( { 'type' : 'ineq' , 'fun' : lambda x: 1 - np.sum(x[self.dimensions:])} ) return const_list def _generate_cost_function (self): """ Function Explanation: 二乗ノルムを用いたコスト関数 Inputs: x : np.array 点 Returns: (float.) 関数の値 """ return lambda x: np.sum(x[self.dimensions:]** 2 )/ 2 - \ np.dot(x[:self.dimensions], self.optimal_point) + \ np.sum(self.optimal_point** 2 )/ 2 def solve (self): # 問題の生成 cost_function = self._generate_cost_function() # 制約条件の作成 ## l1-ball constraints = [] constraints.extend(self._genrate_upper_bound_const()) constraints.extend(self._genrate_lower_bound_const()) constraints.extend(self._genrate_l1_bound_const()) ## box_const bounds = [] bounds.extend([(- 1 , 1 ) for i in range (self.dimensions)]) bounds.extend([( 0 , 1 ) for i in range (self.dimensions)]) solution = minimize( cost_function, x0 = self.initial_point, method = 'SLSQP' , constraints = constraints, bounds = bounds, options = { 'maxiter' : 1000 , 'disp' : True }, tol = 1e-9 ) return solution def solution_arrange (self, solution, eps = 1e-6 ): solution_mask = np.abs( solution.x[:self.dimensions] ) > eps optimal_solution = np.empty(self.dimensions) optimal_solution[solution_mask] = solution.x[:self.dimensions][solution_mask] optimal_solution[~solution_mask] = np.zeros(self.dimensions)[~solution_mask] return optimal_solution if __name__ == "__main__" : # 最適解の生成 dimensions = 10 combination_num = 2 optimal_point = generate_optimal_point(dimensions, combination_num, in_range = 'in' , out_coef = 2 ) #optimal_point = np.random.randn(dimensions) #optimal_point = optimal_point / np.sum(np.abs(optimal_point)) initial_point = np.zeros(dimensions) # 問題の生成 func = SLSQP(optimal_point, dimensions, initial_point) func.initial_point.shape solution = func.solve() optimal_solution = func.solution_arrange(solution) print (optimal_solution) print (optimal_point) 最適解の生成(problem_setting.py) import numpy as np def generate_optimal_point (dimensions : int , combination_num : int , in_range : str , out_coef : float = 1.0 ): """ Function Explanation: l1-ball上の極点の凸結合として定義される、スパースな点を生成する関数。 'in_range'を変更することで、生成される点がl1-ballの内外のどちらになるかを指定できる。 Inputs: dimensions : int 初期点の次元数 combination_num : int 初期点の生成にあたり、利用する極点の数 (注意:combination_num <= dimensions) in_range : str 生成される点がl1-ballの内外のどちらになるかを指定するパラメータ 'in' : l1-ballの内部に生成される 'out' : l1-ballの外部に生成される out_coef : float 'in_range'が'out'の場合に、l1-ballの外部に生成される点の係数 Returns: coordinates : np.array 生成された点 """ # スパースな点が生成されるかの確認 if combination_num > dimensions: raise ValueError ( "'combination_num' must be less than or equal to 'dimensions'." ) # 初期点生成にあたり、関与する座標の決定 # 各要素が0からdimensions-1までの整数値のベクトル rng = np.random.default_rng() coordinates_list = rng.integers(low = 0 , high = dimensions, size= combination_num, dtype=np.int64, endpoint= True ) # 初期生成のための重み作成 weights = rng.uniform(low=- 1 , high= 1 , size=combination_num) weights = weights / np.sum(np.abs(weights)) # 初期点生成 coordinates = np.zeros(dimensions) for i, index in enumerate (coordinates_list): coordinates[index] += weights[i] # l1-ballの外部に生成される場合 if in_range == 'out' : coordinates = coordinates * out_coef elif in_range == 'in' : pass else : raise ValueError ( "in_range must be 'in' or 'out'." ) return coordinates 数値実験(experiment.py) import os import pickle import numpy as np import pandas as pd import matplotlib.pyplot as plt from time import time plt.rcParams[ 'font.family' ] = 'Hiragino Sans' plt.rcParams[ 'xtick.direction' ] = 'in' plt.rcParams[ 'ytick.direction' ] = 'in' # 問題設定のライブラリ from problem_setting import generate_optimal_point # frank-wolfe法のライブラリ from optimization import frank_wolfe, FullyCorrectiveFrankWolfe,generate_problem # SLSQPのライブラリ from slsqp import SLSQP # PGDのライブラリ from pgd import euclidean_proj_l1ball # 実験の初期点を保存関数(確率的にエラーが出るため、保存) ################################################## def save_optimal_point (dimensions : int , non_zero_nums : dict , in_range : str , out_coef : int , save_name : str ): # 格納箱 dim_optimal_dict = {num : None for num in non_zero_nums} # データ点の生成 for num in non_zero_nums: optimal_point = generate_optimal_point( dimensions, combination_num = num, in_range = in_range, out_coef = out_coef ) dim_optimal_dict[num] = optimal_point # データ保存 with open (save_name, "wb" ) as f: pickle.dump(dim_optimal_dict, f) return None ################################################## # 実験の初期設定の準備 ## 準備 problem_setting_dict = { 100 : { 'non_zero_nums' : [ 1 , 5 , 10 , 20 , 50 , 80 , 90 ], 'method_names' : [ 'FW - naive' , 'FW - line' , 'FCFW' , 'PGD' , 'SLSQP' ], 'save_name' : 'dim_100_optimal_dict.pkl' , 'solution_save_name' : 'dim_100_solution_dict.pkl' , 'time_save_name' : 'dim_100_time_dict.pkl' }, 10 ** 6 : { 'non_zero_nums' : [ 1 , 5 , 10 , 50 , 100 , 500 , 1000 , 10000 ], 'method_names' : [ 'FW - naive' , 'FW - line' , 'FCFW' , 'PGD' ], 'save_name' : 'dim_10**6_optimal_dict.pkl' , 'solution_save_name' : 'dim_10**6_solution_dict.pkl' , 'time_save_name' : 'dim_10**6_time_dict.pkl' }, 1000 : { 'non_zero_nums' : [ 1 , 5 , 10 , 25 , 50 , 75 , 100 , 250 , 500 , 750 ], 'method_names' : [ 'FW - naive' , 'FW - line' , 'FCFW' , 'PGD' , 'SLSQP' ], 'save_name' : 'dim_1000_optimal_dict.pkl' , 'solution_save_name' : 'dim_1000_solution_dict.pkl' , 'time_save_name' : 'dim_1000_time_dict.pkl' }, } ## データ点生成+保存 #dimensions = 100 #dimensions = 10**6 #dimensions = 1000 #save_optimal_point( # dimensions, # problem_setting_dict[dimensions]['non_zero_nums'], # in_range = 'out', # out_coef = 2, # save_name = problem_setting_dict[dimensions]['save_name'] # ) # 問題を解く ## 初期設定 dimensions = 1000 ## 問題の読み込み with open (problem_setting_dict[dimensions][ 'save_name' ], 'rb' ) as f: dim_optimal_dict = pickle.load(f) # 各手法の計算 ## 計算結果の格納箱 solution_dict = { method : {num : None for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]} for method in problem_setting_dict[dimensions][ 'method_names' ] } time_dict = { method : {num : None for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]} for method in problem_setting_dict[dimensions][ 'method_names' ] } ## 計算 for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]: # 初期設定 ## 最適解 optimal_point = dim_optimal_dict[num] # FC、FCFW ## 問題、初期点の生成 f, grad_f = generate_problem(optimal_point) initial_point = np.zeros(dimensions) ## 実行 ### FW diminish print ( 'num:' , num, ' FW - naiveを開始' ) FW_naive_start = time() solution_dict[ 'FW - naive' ][num] = frank_wolfe( initial_point, grad_f, f,step_size = 'diminish' , max_iter = 10000 , tol = 1e-6 ) FW_naive_end = time() time_dict[ 'FW - naive' ][num] = FW_naive_end - FW_naive_start print ( 'num:' , num, ' FW - naiveの時間:' , time_dict[ 'FW - naive' ][num]) ### FW 直線探索 print ( 'num:' , num, 'FW - lineを開始' ) FW_line_start = time() solution_dict[ 'FW - line' ][num] = frank_wolfe( initial_point, grad_f, f,step_size = 'line search' , max_iter = 10000 , tol = 1e-6 ) FW_line_end = time() time_dict[ 'FW - line' ][num] = FW_line_end - FW_line_start print ( 'num:' , num, ' FW - lineの時間:' , time_dict[ 'FW - line' ][num]) ### FCFW print ( 'num:' , num, 'FCFWを開始' ) FCFW_start = time() solution_dict[ 'FCFW' ][num] = FullyCorrectiveFrankWolfe( initial_point, grad_f, f, optimal_point, max_iter = 10000 , tol = 1e-6 , inner_max_iter = 1000 , inner_tol = 1e-6 ) FCFW_end = time() time_dict[ 'FCFW' ][num] = FCFW_end - FCFW_start print ( 'num:' , num, ' FCFWの時間:' , time_dict[ 'FCFW' ][num]) # PGD print ( 'num:' , num, 'PGDを開始' ) PGD_start = time() solution_dict[ 'PGD' ][num] = euclidean_proj_l1ball( optimal_point, s= 1 ) PGD_end = time() time_dict[ 'PGD' ][num] = PGD_end - PGD_start print ( 'num:' , num, ' PGDの時間:' , time_dict[ 'PGD' ][num]) # SLSQP(次元数100、1000の時だけ実行) if (dimensions == 100 ) | (dimensions == 1000 ): print ( 'num:' , num, 'SLSQPを開始' ) SLSQP_start = time() # 初期設定 func = SLSQP(optimal_point, dimensions, initial_point) # 求解 slsqp_solution = func.solve() # 解の整形 solution_dict[ 'SLSQP' ][num] = func.solution_arrange(slsqp_solution) SLSQP_end = time() time_dict[ 'SLSQP' ][num] = SLSQP_end - SLSQP_start print ( 'num:' , num, ' SLSQPの時間:' , time_dict[ 'SLSQP' ][num]) # 結果の保存 with open (problem_setting_dict[dimensions][ 'solution_save_name' ], "wb" ) as f: pickle.dump(solution_dict, f) with open (problem_setting_dict[dimensions][ 'time_save_name' ], "wb" ) as f: pickle.dump(time_dict, f) # 結果を開く dimensions = 100 with open (problem_setting_dict[dimensions][ 'save_name' ], 'rb' ) as f: dim_optimal_dict = pickle.load(f) with open (problem_setting_dict[dimensions][ 'solution_save_name' ], 'rb' ) as f: solution_dict = pickle.load(f) with open (problem_setting_dict[dimensions][ 'time_save_name' ], 'rb' ) as f: time_dict = pickle.load(f) # 可視化 ################################################# plt.figure(figsize = ( 30 , 5 )) if dimensions == 100 : plt.suptitle( '$n=100$' ) elif dimensions == 1000 : plt.suptitle( '$n=1000$' ) plt.subplot( 1 , 3 , 1 ) for method in problem_setting_dict[dimensions][ 'method_names' ]: plt.plot(problem_setting_dict[dimensions][ 'non_zero_nums' ], [time_dict[method][num] for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]], 'x--' , label = method) plt.xlabel( '$x^*$の非ゼロ要素数' ) plt.ylabel( '時間' ) if dimensions == 100 : plt.xlim([ 0 , 100 ]) plt.xticks([ 20 * i for i in range ( 6 )]) plt.ylim([ 1e-5 , 100 ]) plt.yscale( 'log' ) elif dimensions == 1000 : plt.xlim([ 0.9 , 1000 ]) plt.ylim([ 1e-5 , 10e3 ]) plt.xscale( 'log' ) plt.yscale( 'log' ) plt.subplot( 1 , 3 , 2 ) for method in problem_setting_dict[dimensions][ 'method_names' ]: plt.plot(problem_setting_dict[dimensions][ 'non_zero_nums' ], [np.sum(np.abs(solution_dict[method][num]) > 1e-6 ) for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]], 'x--' , label = method) plt.xlabel( '$x^*$の非ゼロ要素数' ) plt.ylabel( '出力された解の非ゼロ要素数' ) if dimensions == 100 : plt.xlim([ 0 , 100 ]) plt.xticks([ 20 * i for i in range ( 6 )]) plt.ylim([ 0 , 50 ]) plt.yticks([ 10 * i for i in range ( 6 )]) elif dimensions == 1000 : plt.xlim([ 0.9 , 1000 ]) plt.ylim([ 0.1 , 1000 ]) plt.xscale( 'log' ) plt.yscale( 'log' ) plt.subplot( 1 , 3 , 3 ) for method in problem_setting_dict[dimensions][ 'method_names' ]: plt.plot(problem_setting_dict[dimensions][ 'non_zero_nums' ], [np.linalg.norm( solution_dict[method][num] - dim_optimal_dict[num], ord = 2 ) for num in problem_setting_dict[dimensions][ 'non_zero_nums' ]], 'x--' , label = method) plt.xlabel( '$x^*$の非ゼロ要素数' ) plt.ylabel( '$\| x -x^* \|_2$' ) plt.legend(loc= 'center left' , bbox_to_anchor=( 1 , 0.5 )) if dimensions == 100 : plt.xlim([ 0 , 100 ]) plt.xticks([ 20 * i for i in range ( 6 )]) plt.yscale( 'log' ) plt.ylim([ 0.01 , 10 ]) elif dimensions == 1000 : plt.xlim([ 0.9 , 1000 ]) plt.ylim([ 1e-2 , 10 ]) plt.xscale( 'log' ) plt.yscale( 'log' ) plt.show() #################################################
アバター
(with MIT Professional Education , and Global Alumni) At Insight Edge, many of our team members are involved in "extra-curricular activities": building our skills outside of our day jobs, and bringing that experience back home to IE. In a previous Tech Blog post, I shared about my experience working in collaboration with NASA and Google Cloud, as part of the "Frontier Development Lab" program (see A Data Scientist’s “Sabbatical”: Skill-building through external scientific collaboration ) Other members of IE supervise grad-student research projects, lecture at universities, or even tutor high-school students. Even of those jobs are not connected to IE projects directly, we believe our members benefit from gaining wide experience explaining technology and methods to folks outside of the Sumitomo Corp. network. Learning to teach effectively has a very healthy impact on our ability to engage with stakeholders, and explain the trade-offs of different DX implementations to professionals in SC and our many operating companies. Since September 2023, I have been acting as a "Learning Facilitator" for MIT Professional Education's " Applied Generative AI for Digital Transformation " online program: a kind of continuing education course, helping industry professionals from magazine writers to pharmaceutical executives update their skills and curiosity regarding Generative AI for Digital Transformation", which gives a mixed bag of GenAI nuts and bolts and best pracitces, as well as overall frameworks for thinking through larger business trends GenAI tools may bring about. As a Learning Facilitator, I host "Office Hours" with groups of 30-60 participants, taking their Q&A, and helping them digest all the different topics the course brings up. While its the faculty who give the lectures, its my role to go deeper and adapt to each participant's individual learning needs: via office hours, Q&A, written assignments feedback, and any other SOS or sanity check request. I've really loved this role as it meshes very well with what I often have to do at Insight Edge / Sumitomo Corp.: help our organizations many professionals resolve misconceptions surrounding advanced technology. Help them explore the complex trade-offs, and navigate the common pitfalls of new algorithm R&D and commercial implementation. "Can I use ChatGPT for the homework?" While this might seem a bit trivial, it comes up immediately with every cohort, and usually leads to some profound discussion! Naturally with a course about LLMs, students will be very eager to use them; or even guess that they are expected to use LLMs to generate their work. The trap that many fall into is copy-pasting the assignment instructions verbatim into ChatGPT, and then submitting verbatim response as their own work. Since these are working professionals rather than university students, my major concern is that folks are not then obtaining any added value from the tool: neither learning anything from it nor improving the quality of the result. This can have diasatrous consequences if scaled to their actual professional workloads, e.g. workplace memos, reports, or even consumer facing documents can end up reading extremely generically, and risk resembling content generated by competitors, etc. As a single user, many folks will find the output to look very good. But if we sample the output among many similar and generic prompts, the results tend to be very homogeneous. The general feedback I give folks here, is really quite similar to what many proactive university instructors are recommending: use LLM tools in a way that allows you to focus your effort on what you do best. Instead of having them write for you, use them to help you get started, to give structure, or to understand what a generic output would look like, so that you can avoid it. You can ask a chatbot for a list of common pros-cons on an issue, then go through that list one by one and respond with your own takes: sort of a way of coaching yourself or "gameifying" your task. After that, you don't even need to keep the text generated by the LLM. Sort of like temporary scaffolding. For more on the balance between empowering oneself and others with LLMs vs the potential risks of outsourcing thought or creativity to them, I've gathered a handful of papers I usually point students to: Impact of artificial intelligence on human loss in decision making, laziness and safety in education AI Art and its Impact on Artists "How could I be prompting this more effectively?" One of the most common questions folks ask is how to write effective prompts. Actually, that's not true: its actually one of the most chronically unasekd questions. Many students will cling to the most basic, minimal, or copy-pasted prompts --- because they work. Or at least, they appear to work well enough on the surface. And for folks whose primary motivation in using LLMs is simply to save time, they're typically so very impressed with the speed that the LLM will produce a pretty-looking e-mail that they won't look to improve. Thus I focus a lot on, not answering this question necessarily, but in getting students in the habit of asking themselves this question whenever they use an LLM --- especially when they are in a hurry, because that's where poor quality output tends to be overlooked. To help folks get in that habit, I make some very minimal suggestions for them to always use: Never use generic prompts: In other words, verbatim prompts copied from a prompt ``cheat sheet'', or any other widely-available source: while the overall structure of a prompt can be re-used -- and probably should be, so that you can experiment systematically --, copying a prompt exactly runs the risk that the response itself will be generic (and likely to resemble the work of others!) Structure the prompt with keys and values: While it may feel the most natural to prompt an LLM in conversational language, you might obtain better control over the result if you apply some more machine-like formatting. For example, when prompting for writing samples, you could choose a set of 3 keys, "task: , voice: , vocabulary level: ***" Remember that LLMs are trained not only on prose or conversational language, but also on a wide variety of data formats and programming languages. You can thus use some programming-like syntax in your request to try and remove some ambiguity. For students who code, I usually point them to JSON or YAML syntax. You can use such formats as templates for structuring a prompt, whether or not your request has anything to do with programming. For students who are less familiar with data and code, I usually just provide an example of key-values (typically ends up looking YAML-like), but tell them to keep it simple, and experiment. When in doubt, its always better to have clear structure to the prompt: that way you can easily revisit and tweak different parts of it, to hone in on the results you want. How can I use JSON to prompt more complex stories and tasks? Folks interested in prompting using JSON or other structured formats might have a look at the following Reddit thread: JSON as prompts : r/ChatGPT The thinking is that this makes the key context components more recognizable to the LLMs trained features, and takes some ambiguity out of the outputs. You could also try something simple to start with, maybe a quick JSON of 10 or so parameters for a software suite outline. Start the prompt off with something like “What follows is a JSON with parameters defining a desired brainstorming request. Please generate a response based on the parameters in the JSON”, then paste the JSON, and see what you can come up with. Based on that experience, you could also look into more systematic prompt-structuring tools like LangChain. How to Use LangChain, a Framework for Structured Responses from ChatGPT | by Prabhavith Reddy | Medium # Does RAG cure hallucinations? No! RAG is a great tool, but it's not a "set and forget" barrier against hallucinations or falsehoods. A lot of students are introduced to the idea of "Retrieval Augmented Generation" during this course or others, and are very excited about it. RAG is a very powerful technique for getting an LLM to stay relatively constrained to a particular set of documents. However it's not a subsitute for quality control. LLMs can still produce hallucinations even when connected to a RAG system. This is because the LLM model itself at the core of whatever such app you develop is not changed at all by the RAG system. Rather, its being carefully fed context in the form of chunks of your chosen "corpus", or document collection (Tom Yeh's "AI by Hand" seris contains a great guide for beginner's on how RAG works ) has produced a fantastic set of beginner's guides for AI topics. Have a look at These context clues then go into the LLM along with your prompt, strongly biasing the response towards that context. The LLM weights themselves are not changed, and we can still see false or misleading information appear in the output. RAG-enabled LLM apps are still going to be much more constrained to your task than a vanilla LLM implementation! So don't let this answer scare you. Just remember that whatever support system you use to filter, constrain, loop, or otherwise augment an LLM based system, you still have to be very careful to monitor the system for problematic outputs. For more on RAG and hallucinations, I typically point folks to references such as the following: [2311.04235] Can LLMs Follow Simple Rules? As for quality control approaches LLM-based apps and systems, Weight&Biases has put out several helpful resources: How to Evaluate, Compare, and Optimize LLM Systems Evaluating Large Language Models (LLMs) with Eleuther AI I find that students also benefit from a sense of caution about an LLMs adherence to "rules" we give them in our prompts. While our requests may be formatted in absolute terms, "Never use this word" or "You must use this pattern in every paragraph", there is no guarantee that the response will adhere to such terms. Often the results can be satisfactory or even well-above expectations, but LLMs should not be confused with something like a command line where variables and functions can be strictly defined. This follwoing article gives an interesting deep-dive into the ability of LLMs to adhere to rules set out in the prompts we provide them: * Can LLMs Follow Simple Rules? How restrictive are the guard rails on some of these AIs? They can be at the same time overly restrictive but also failing to prevent harmful uses. As folks have pointed out more and more biases, likely coming from poor training data curation and testing, the developers have resorted to more quick-and-dirty means of avoiding embarrassing outputs, e.g. simply filtering: this is applied both on the prompt itself, and again once the output has been generated. We have yet to see any sophisticated “constructive” guardrails, i.e. those that help the user understand what kind of responses are problematic, how to double-check them, and how to maintain quality overall. More recent methods have tried to add to this a kind of "intermediate" filtering, wherein the actual internal layers of the LLM are sampled for problematic features. If detected, this triggers the overall app serving the LLM from producing any output, whether or not the output itself explicitly contains any of the blacklisted words. Related References: Building Guardrails for Large Language Models What's the state of High privacy LLM tools and implementations? This is still a major challenge with LLM applications: currently, the most powerful models are closed-source, and require one to send requests via an API or web-interface to the developer’s external service. LLMs themselves are not inherently insecure, in the sense that just running “inference” (i.e. running an input through a trained model) does not require any data to actually be saved by the model. Sort of like running current through your house’s wiring doesn’t change anything about the wiring itself. The trouble is about how the LLM service provider’s overall web-app, and servers handle your data. If you are using such provider’s tools to “fine-tune”, or create “agents”, “assistants”, etc., the situation is completely different. This is because to create such customised or fine-tuned models, your data is being used to train the model. To what extent your training data is extractable from the assistant is still an active area of research. We have seen instances of “jail-breaking” (engineering prompts in specific ways such to bypass safety filters, or extract snippets of the original training data) wherein security researchers are able to cause such agents to “leak” sensitive data. Note: this in principle could happen whether you fine-tune using a third-party platform or on your own. As for training a model securely, the idea of “Federated Learning” has come up now and then: basically using a network of training nodes to collaboratively train a model; the data itself however never leaves its respective node. Let me share also some resources on data privacy of LLMs, and also on using LLMs themselves to mask sensitive data. Finally, a tool I’ve discovered recently that can be used to generate synthetic data that mimics sensitive data examples. [2307.08925] Federated Large Language Model: A Position Paper [2310.10049] FATE-LLM: A Industrial Grade Federated Learning Framework for Large Language Models Running Large Language Models Privately - privateGPT and Beyond | Weaviate - Vector Database Can a Model Be Differentially Private and Fair? Ai4Privacy ** ai4privacy/pii-masking-200k · Datasets at Hugging Face [2402.13659] Privacy-Preserving Instructions for Aligning Large Language Models LLM AI Cybersecurity & Governance Checklist Private LLMs: Data Protection Potential and Limitations - Skyflow Ethics vs. benchmarks for performance: testing as tools evolve, etc. The ethical concerns surrounding GenAI make the questions of benchmarks all the more important! Bias, security, quality control in general are very difficult to systematically measure without good benchmarks. My personal take is a tad different from the Faculty: while I understand they have their own personal experiences with OpenAI/ChatGPT, I don’t think an individual’s personal experiences with one tool or another are enough to make organisation-level decisions. On the other hand I agree with them in that I don’t think a single “Leaderbord”-style general accuracy benchmark is sufficient for such decisions: does a marginal % difference in accuracy score really mean one model is better for your specific needs than another? I recommend looking at a suite of benchmarks designed around specific domain applications, and even considering developing your own internal benchmarks and red-teaming. Let me share some examples of community-developed metrics for various domains. For more, have a look at the “Metrics” section of HuggingFace.co. Most of these are rather rudimentary statistical measures, but if you will also note a good number that are for evaluating language models for various tasks from “text simplification” and “information retrieval” to performance on standardized tests, etc. Example references The Open Medical-LLM Leaderboard: Benchmarking Large Language Models in Healthcare A Comprehensive Medical Benchmark in Chinese (2023) Open Portuguese LLM Leaderboard - a Hugging Face Space by eduagarcia Code Eval - a Hugging Face Space by evaluate-metric Competition MATH - a Hugging Face Space by evaluate-metric MATH Benchmark (Math Word Problem Solving) | Papers With Code Paper page - Prometheus 2: An Open Source Language Model Specialized in Evaluating Other Language Models Overcoming Hallucinations with the Trustworthy Language Model How have GenAI tools have changed in the past 6 months? When I first LF’d for the course back in September 2023, I felt the community was somewhat dismissive of things like: Potential of smaller LLMs Self Finetuning In-house (e.g. open source) model implementations. These approaches have been gaining more momentum recently. A lot more research has appeared in this time showing applications where smaller models might excel (constrained use-cases). Novel “multi-agent” architectures have also gained popularity, i.e. using multiple LLMs to parse tasks. Need for security and transparency of training data has also become louder: thus quieting some of the folks advocating for ChatGPT/OpenAPI as a one-stop-shopping solution. Open-source models have also made great strides in this time, most notably Meta’s “LLama”. At the same time, tools and instructional resources allowing folks to do their own fine-tuning, etc. have also become more common. Example References Understanding the Impact of AI Generated Content on Social Media: The Pixiv Case Model Collapse Demystified: The Case of Regression do smaller models hallucinate more HuggingChat Breakthrough: Zero-Weight LLM for Accurate Predictions and High-Performance Clustering - Machine Learning Techniques What's one major and direct worry about the current course of LLM evolution/propagation? I think the flood of generated content onto the internet would be a key issue to think about here. Not just how its being generated but how it affects the average user's perception of digital content. This preprint dives into that issue as it affects visual art on platforms like “Pixiv”: Understanding the Impact of AI Generated Content on Social Media: The Pixiv Case Please also consider the perceived challenge that some researchers are putting forward, wherein models become harder to train because the fraction of easily-scraped, genuine human-created examples on the internet may decrease. (see “Model Collapse”: Model Collapse Demystified: The Case of Regression ) Related References Best Practices from Students on Using ChatGPT in High School | by Jaya Ramchandani | We Learn, We Grow | Medium Best Practices in Generative AI Educational Prompts: A ChatGPT and Microsoft Prompt Analysis 404 Media Distributed AI Research Institute Mystery AI Hype Theater 3000 | DAIR How about those small models? This How-To by Intel gives a decent, objective break-down of model performance (in terms of hallucination rate) by parameter size (do smaller models hallucinate more) You’ll find tons of other blog posts on the subject, but many don’t actually provide hard statistics and/or are likely just advocating for their own offerings. (Granted, Intel is not exactly a “neutral observer” !) For more objective comparisons, you might consider taking a dive into ArXiv – just keep in mind that many papers there have not necessarily been peer-reviewed yet. Still, it can be a great source for understanding the latest R&D trends. As mentioned in the Office Hours, HuggingChat is a great platform for quickly comparing response quality for models of different sizes (HuggingFace.co/chat) Aside from the debate of large vs. small, I’ve found this novel approach gaining attention: Breakthrough: Zero-Weight LLM for Accurate Predictions and High-Performance Clustering - Machine Learning Techniques (the “zero-weight” description may be a bit of a misnomer) Which use-cases do we see most in organisations? Chat bots are the most visible ones! These can be consumer-facing, or for internal applications: e.g. HR policy search-bots, Data interaction. Also have a look at Sentiment Analysis. The biggest potential I’ve personally noted is however in the way individual contributors empower their workflows with GenAI tools via on-the-job learning and skills refining. FYI I found the following interview with the founder of “Writer” to be quite interesting: Leveraging Hugging Face for complex generative AI use cases. How is building GenAI applications/ products different from other SW applications/ products? I would say the biggest difference is in the quality control steps, lack of determinism in the outputs: it’s tricky to get LLMs to provide consistent QoS, and to have them adhere to strict rules -> rigorous testing becomes more of a “fuzzy” process. Otherwise the experimental nature of LLMs in their current state needs to be taken seriously. Developers are good at making these systems appear polished and professional, but we are only just beginning to map out the pitfalls of LLM-tech-at-scale. New model versions are being released much faster than the R&D community is able to assess and stably-integrate them, let alone for enterprses to fully comprehend how their customers will be impacted. This is not to scare folks away! Rather, just to remind us all not to confuse cutting-edge with well-vetted. Which roles and capabilities are needed in an LLM team? Of course new skills like “Prompt Engineering” are critical; not necessarily in the sense that you need to hire a “veteran”, but that your team needs to be accumulating knowledge and experiment results on the best ways to write prompts. But more than that I suppose not overlooking the traditionally required skills is critical! E.g. folks skilled in: Software testing, Scaleable cloud architectures, and system redundancy engineering (i.e. being able to swap in alternate models and providers in the case a major product (API) becomes unavailable – especially true for GenAI as the field is so fastly evolving!! And we are still very much in the “hype bubble”) I highly recommend following Eduardo Ordax (GenAI lead at AWS), and his thoughts on MLOps: basically, the hype of GenAI has distracted folks from MLOps and DevOps, but scaling GenAI is going to require us to get back to those.
アバター
はじめに Insight Edgeの社内部活 ボドゲ部創設者:JIMENEZさん 活動の様子 終わりに はじめに こんにちは!Insight Edgeの人事担当・合田です。 今回は当社メンバーが運営している社内部活についてご紹介します! Insight Edgeの社内部活 Insight Edgeでは、Slackチャンネルで各メンバーが自主的に部活を作成し、 チャンネル内では関心のあるトピックについての情報共有や、部活内容によってはイベントの実施なども行っています。 2024年5月時点でどんな部活があるか列記してみましたが、この一覧を見るだけでInsight Edgeメンバーの趣味趣向が見えてきますね・・・! 【代表的な部活】 eikaiwa(英会話) meal(レストラン情報) bodoge(ボドゲ) kosodate(子育て)※男女問わず、子育て中のメンバーが多い! movie(映画) karaoke(カラオケ) baceball(野球) manga(漫画) golf(ゴルフ) competitions(ゲーム大会) mahjong(麻雀) sauna_37(サウナ) hokkaido(北海道) ※Insight Edgeは北海道出身者が多い! tennis(テニス) cafe(コーヒー、カフェ情報) 本日はこの中から"ボドゲ部”の紹介をしたいと思います! ボドゲ部創設者:JIMENEZさん ボドゲ部の発起人は、当社のLead Data ScientistのJIMENEZ(ヒメネス)さん! 公式サイトのプロフィールに記載している通り、これまでに2つのボードゲームの出版実績のあるスペイン出身のメンバーです。 折角なので、JIMENEZさんとボードゲームのエピソードも少しご紹介。 高校時代に初めて日本に遊びに来た際、見た目の美しさに優れた”知恵の輪”と出会って衝撃を受け、そこからパズルのみならず、ボードゲームの面白さに徐々にハマっていったそうです(デザイン性へのこだわりは、後ほどご紹介するJIMENEZさんが作ったボードゲームにも引き継がれます)。 大学院のために来日し、修士課程の時代に、後に出版することになるボードゲームの初期版を作成し、友人と対戦しながら改善を重ねたエピソードもお伺いしました。 「いかに人に楽しんでもらえるか」を探究して、ボードゲームにさほど興味のない人の意見こそ積極的に取り入れているんだそうです。 「これはゲームにならないか?」と着想を得たら、100円ショップで材料を揃えてプロトタイプを作りアイデアを自分で形にしていくこともあるそうで、時期によっては週末のほとんどをボードゲーム作りや構想を練るのに使うこともあるんだとか! 日本の過ごしやすさ(安全面、人との距離感)を気に入って、大学の博士課程を修了した後も日本に残って就職。 過去に働いた企業でランチタイムに人を集めてボードゲームをして盛り上がったこと、 それから「一緒に働く同僚のことをよく知っていた方が、仕事がうまく進むことがある」という社会人としての経験則から、 Insight Edgeに参画した際、「折角ハイブリッドの就業環境なら、みんなでボードゲームをする時間を作りたい」と思って部活を発足させたそうです。 Insight Edgeのデータサイエンティストとしての顔と、ボードゲームクリエイターの顔と、マルチに活躍するメンバーです! 活動の様子 某日、就業終了後に希望者がそれぞれボードゲームを持ち寄ります。 最初に遊ぶのはJIMENEZさんが制作したボードゲームの1つ"Squadro(スクアドロ)”。 ルール説明をするJIMENEZさんと、対決するKoyanagiさん、Sugaさん。 5つの自分のコマを進め、Uターンさせて先に4つのコマをスタート地点に戻した方が勝ち、という2人用のゲームです! それだけ聞くと簡単ですが、 1度に進めるコマの移動数が往路と復路で変わる 相手のコマが自分のコマを飛び越えると自分のコマはスタート地点に戻らなくてはならない などなどルールが細かく決まっています。 「作った本人はやはり強いのか?」を確かめるためにJIMENEZさんと対決したのはKoyanagiさん。 先ほどまで和かに談話していたのに、一気に真剣な眼差しになります。 2人の対決を隣で見ていて、序盤と終盤で全く戦況が変わるのがこのゲームの面白い所だと感じました。(ちなみに勝者は作ったご本人!) 対決後、JIMENEZさん直々に勝つための考え方や、勝率の高い攻め方、制作秘話(デザインにとてもこだわったらしく、インテリアとしても使えそうな素敵なボードゲームです!)などを教えてくださいました。 もう1つJIMENEZさんが作ったボードゲームを紹介。 こちらは”マルコポーロの地図”。 対象年齢は6歳からで、プログラミング的思考力やひらめき力が身につくとか! 子供向けのゲームシリーズと言うことで、冒険家マルコポーロが旅した世界地図のような可愛いデザインにワクワクします。 このゲームは地図上のスタート地点からゴール地点まで、コンパスを模したピースを正しく配置する1人用のゲームです。 ピースによって進める数が違うので、方向と数に気をつけながら配置します。 私が挑戦しましたが、全40問のうち難易度の問題の低い方の問題にもそれなりに時間がかかってしまいました。 「うーん・・・」と悩んでいると、JIMENEZさんから「考え方を変えてみたらどうでしょう?こういうこともできますよ!」とヒントがもらえます。 ご本人からレクチャーを受けながらゲームに取り組めると言う贅沢な時間でした。 他のメンバーがそれぞれ遊んでいる間もコツコツと知恵の輪を解くSugaさん。 こうして日々、業務時間中の圧倒的な集中力を鍛えているに違いない。 そして最後はせっかくなのでみんなで遊べるものを、とKoyanagiさんが持参したボードゲーム”コヨーテ”で遊ぶことに。 数字やとある条件が書かれたカードを自分のおでこの前に掲げ、自分のカードだけが見えない状況で、その場の全員のカードの合計数を予測するゲームです。 とある条件と言うのが曲者で、合計数を2倍にするものや様々な効力のあるカードが混ざっています。推理力、人の表情を読み取る洞察力、そしてどんな時でも堂々と強気で振る舞う仕草など、あらゆる要素が必要な心理戦となります。 私もゲームに混ぜてもらいましたが、予測分析が得意なデータサイエンティストに囲まれ、惨敗でした(単純に暗算が苦手という説も)。次回は心理戦で勝てるようにトレーニングを重ねたいと思います。 一通りのゲームで遊んだ所でお開きとなりました。 普段一緒に仕事をしているメンバーと、いつもとは違う形で頭を使う時間はとても楽しかったですし、何よりボードゲームクリエイター直々にいろいろ話を聞く事が出来るという、社内部活の域を超えた贅沢な時間でした(Insight Edgeに入社するとこんな特典もありますよ!) そして冒頭でも触れた通り、Insight Edgeにはこれ以外にも沢山の部活があります。 おすすめの映画やレストラン、カフェ情報を交換するような部活チャンネルもあり、メンバー同士の大切なコミュニケーションの場となっています。 終わりに 総合商社ビジネスのDX推進をするInsight Edgeでは、その豊富な産業ドメインに対応するために専門性やバックグラウンドの異なるメンバーが集まっています。 多彩なメンバー同士で交流できる環境はとても恵まれていると改めて感じました。 Insight Edgeでは一緒に働くメンバーも積極募集中です! ご興味があればぜひ採用サイトにもお越しください。 recruit.insightedge.jp
アバター