TECH PLAY

株式会社Insight Edge

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

180

1.はじめに 2.私がIEに入社を決めたわけ 3.IEにおける現在の仕事 4.入社後に感じたこと(長所と課題) 5.IEへの入社を検討されている非技術者の方に向けて 1.はじめに  はじめまして、Insight Edge(以降IE)でコンサルタントを務めております根本と申します。今回は非技術者である私が、新入社員ならではの視点で感じたことを素直にお伝えしようと思っています。本題に入る前に私の経歴を簡単にご紹介させていただきます。IEに入社するまでに4社を経験しており、一貫してIT業界で働いてきましたが、モバイル向けのコンテンツ企画から始まり、経営企画やオペレーション、数値管理、カスタマーサービス、営業支援など、様々な経験をしてきました。2023年9月よりコンサルタントとしてIEに入社いたしました。 2.私がIEに入社を決めたわけ  私は下記の3点が決め手となりIEへの入社を決断しました。 ①日本を代表する総合商社である住友商事の100%子会社であり企業としての安定性が高いこと ②これまでの経験で関わったことが無いような非常に幅広い領域の案件を担当できること ③会社規模がこれまでのキャリアの中では比較的小さく今後の成長の可能性が高いこと  ①について、IEは住友商事の100%子会社です。これまでに経験してきた企業と比較して会社の存続に影響を与えるような経営・事業リスクは低く、安定して仕事に取り組める環境であると感じました。転職活動の中で様々なスタートアップや成長著しい企業をご紹介いただいたこともありましたが、家族のこと(妻と娘、猫が5匹います)を考えると安定して仕事に集中できる環境に身を置けることは大きな魅力でした。  ②が今回挙げた3つの理由の中で最も魅力的に感じた点です。これまでのキャリアでは主軸となるビジネスを中心に業務を行うため、メインビジネス以外のドメインや事業について経験することはありませんでした。一方IEについては住友商事がカバーする非常に幅広い業界における様々な課題解決に取り組む必要があり、業務面積という観点ではこれまでに無いほど広く、ここでしかできない貴重な体験ができると感じました。キャリアを重ねてきたこのタイミングで、過去に経験したことが無い様々な領域で課題解決の支援をすることで自身の成長に繋がると確信しています。  また、③に挙げたように自身の業務が会社自体の成長にも直結する環境である点もIEならではの魅力だと感じています。前職は大手外資系企業でしたが、自らの仕事にやりがいや達成感は感じていた一方で業績に直結するという感触は比較的薄い環境でした。応募を検討する中でIEは社員数も比較的少なく、今後更なる発展と大きな変化を伴う環境であることを知りました。これまでも変化が激しい環境でキャリアを積んできましたが、自身と会社の成長を肌で感じられる環境において、変化に対して前向きに取り組み、変革に直接関わることができるという点もIEへの入社を決めた理由の1つです。 3.IEにおける現在の仕事  冒頭でコンサルタントとしてIEに参画したことをお伝えしましたが、もう少し具体的な業務内容についてご説明したいと思います。IEにおけるコンサルタントは、一般的なコンサルタントのイメージとは少し異なり、比較的営業職の意味合いが強いです。親会社である住友商事をはじめ、多くの関連企業からDXの相談を受けますが、最初に話を聞くのが我々コンサルタントです。現状の課題からはじまりDXで実現したいこと、目標等をまずはヒヤリングし、IEとしてどのように支援できるのか内部の技術部門と連携を取りながら顧客窓口として様々なやり取りをするのが主要業務となっています。案件が具体化し、開発を含めた実働フェーズになると開発部門にプロジェクトオーナーを引き継ぐことが多いですが、それまでの前捌きを含め、プロジェクトを軌道に乗せるためのプロジェクトオーナーとしての役割を担っています。当然ある程度のITの知識や業務知識は必要になりますが、専門的な技術に関する知識や特定の事業ドメインに関する知識が必須、というわけではなく、今のところ非技術者の私でも問題無く業務には入れています。もちろんそれらを持っていることでより品質高く業務を遂行できることは間違いないですが、テックカンパニーであるIEのコンサルタントとしてそれらが「必須」ではなく、様々なバックグラウンドをお持ちの方でもご活躍いただけるポジションだと思います。 4.入社後に感じたこと(長所と課題)  続いては入社して4か月が経過したこのタイミングで、個人的に感じたIEの長所と課題について率直にお伝えしたいと思います。まず入社の決め手として挙げた3点について、入社前後で印象が変わったという点は全く無く、想定通りの環境で想定通りの仕事ができていると感じます。既に10を超える案件に関わっていますが、製造業、通信サービス、小売り、テレマティクスなど様々な事業ドメインの関連企業とDX推進に取り組んでいます。内容についてもデータ分析が主体の案件からソリューション開発が伴う案件、技術研修を提供する案件など多岐に渡ります。大半の案件で住友商事と共に動くため、住友商事の様々な知見やリソースを借りながら業務ができるのも長所の1つだと思います。また、ワークライフバランスについても意識的に重視されており、会社としてプライベートについても配慮いただける環境は非常にありがたく思っています。  続いて課題と感じる点についてお伝えしたいと思いますが、第1に人員不足を感じる局面が非常に多いです。新しい相談が来た時に我々コンサルタントがアサインされるのですが、各自既に多数の案件を抱えているため、対応キャパシティという点においてあまり余裕が無い状況です。幸いなことに生成AIをはじめとした様々な相談が急速に増えている状況でありますが、人材面でも案件ペースに沿った拡充が必要であり、いかに人材採用を加速させるかというのは会社としての1つの重要課題であると感じます。また、会社としても急拡大・急成長のフェーズにあるため、様々な整備が必要である点も課題の1つです。コンサルタントの業務領域においては「業務の属人化」や「マニュアル作業による管理業務」など、まだまだ効率化・仕組化ができていない部分は正直多いと感じます。また新入社員の教育体制や社内各部署との連携体制など、様々な部分で改善が必要です。ただし、これらについては課題である一方で、ポジティブに考えると改善余地が多く、改善を率先して行うことで自身と会社双方の成長を実感できる点はむしろ良い点であるとも感じています。最後に、課題というよりはIEならではの難しさになりますが、住友商事と足並みを揃えてビジネスを進めていく必要があるという点もIEならではの特徴だと思います。IE単体で戦略を練るのではなく、住友商事のビジョンに合わせてその中でどう会社として成長していくかを描く必要があります。案件推進においても、IEとしての意向だけでなく住友商事の意向も踏まえて動く必要があり、必然的にステークホルダーも多くなるためその点はこれまでのキャリアの中では経験することが無かった難しさであると感じます。 5.IEへの入社を検討されている非技術者の方に向けて  最後に、IEへの入社・応募を検討されている非技術者の皆様に向けていくつかお伝えしようと思います。まず働く場所としてIEを推奨するかという点においては強くお勧めします。会社として安定している環境で様々なビジネスドメインに関わることができ、その中で最先端の技術を用いて課題解決に貢献していくという仕事はなかなか他の会社でできることではありませんし、この点をエキサイティングと感じていただけるのであればきっと後悔はしないと思います。現状少数精鋭で多くの案件を対応しており、営業部門(コンサルタント)、管理部門、技術部門それぞれで非常に優秀な人材が揃っているレベルの高い環境で仕事ができる点も是非お勧めしたいポイントです。前述のとおりコンサルタントというポジションにおいては技術的な専門知識も必須ではありませんので、技術職以外の方でもご活躍いただける点はご安心ください。一方で、IEで働くうえで必須となる要素もあると考えており、「変化が多い環境を楽しむことができるか」、また「自ら率先して様々な業務に取り組む姿勢を持っているか」、この2点はIEのどのポジションにおいてもマストであると感じています。これまで書いてきたように、IEは成長著しい会社であり、必然的に様々な部分で変化が多く生じます。会社の体制やルールもそうですし、会社としての目標・ゴール、重視すべき点なども今後大きく変わっていくことが容易に予想されます。変わらない環境の中で自らの業務を淡々とこなすということではなく、日々変わる環境を楽しむことができ、その中に身を置いて活躍したい、という意識は必要な要素です。また、変化が多いということはそれだけ社員に求められる内容も変化し、増えていきます。その中で受け身の姿勢でいるのではなく、求められるものを自ら察知し率先して取り組んでいく姿勢も求められます。今後の組織拡大に向けて、自らの知見を様々な分野で活用して会社に貢献したい、そういった意識をお持ちの方にはぜひおすすめしたい職場です。  ここまで色々と書かせていただきましたが、少しでもIEの実情について理解を深める手助けになれば幸いです。「百聞は一見に如かず」ということで、少しでも興味をお持ちの方がいらっしゃいましたら是非採用担当までご連絡ください。カジュアル面談も常時受け付けておりますので、ご連絡お待ちしております!
はじめまして、今年度5月にInsight Edgeへ入社した合田(ごうだ)です。 私が入社する前の採用業務は CTOの福井 が主として行っていたのですが、今後さらに採用活動をスケールしていくにあたって、一部採用業務を引き継ぐ形で参画させていただきました。 当記事では、入社してまず行った採用オペレーションの効率化について書きたいと思います。 これまでの採用オペレーション ペインポイントの洗い出し HERPの導入 導入結果 最後に これまでの採用オペレーション 入社後に業務引継を受けた当時の採用オペレーションは下記の通りです。 候補者やエージェントとのやり取りはメールベース 社内の情報連携はSlack。センシティブな内容はDMを使用 個人情報の保管場所は閲覧制限をかけたGoogleドライブ 面接用の会議室手配はOutlook 面接官の予定はGoogleカレンダー 採用業務は個人情報を多く扱うので、情報管理がとにかく大変です。 しかしながら、メール/Slack/GWS/Outlookと複数のサービスをまたぐオペレーションは非常に煩雑で、1つの面接を調整・実施・評価するにも多くの時間を割いてしまい、どうしても同時に進められる選考数が頭打ちになるという課題感がありました。 「いかに効率的に採用フローを回せるか」がこの先の採用活動のポイントになるため、採用管理ツールATSの導入検討を始めました。 ※CTOの福井から「新たに入社する採用担当(=合田)が使いやすいと判断したATSを導入してもらおうと思っていた」と引き継ぎを受け、入社後最初のタスクとなりました。入社数日でこの判断を任せてもらえるのか、と裁量の大きさを感じました! ペインポイントの洗い出し 費用をかけてATSを導入するからにはと、現状の把握と課題整理をしました。 採用情報の一元管理が出来ていない(複数ツール/サイト/エージェント利用でオペレーションミスがいつ起きてもおかしくない状態) 会議室手配を含めたスケジュール調整の簡略化(※結果的にここだけは解決できなかった…!無念。後述) 採用活動のPDCAを回したい、KPI管理も始めたい HERPの導入 3種類のATSを比較検討しましたが、最終的にHERP導入に決めた理由は下記になります。 HERPが提唱している「スクラム採用」(採用業務をHR担当だけの業務とせず、全社で採用に取り組むこと。  ※「スクラム採用」は株式会社HERPの登録商標です )が、Insight Edgeが既に実践している採用体制に近いこと。 無料トライアル利用が出来て、トライアル中もサポートメンバーがしっかりつくので機能面での懸念点を払拭出来たこと(質問に対する回答も数分以内に返ってくることが多くありがたかったです) 分かりやすいUI(HERPに新しくメンバーを招待した時、機能説明を都度行っていないのですが、“問題が起こっていない、質問もほとんど来ない”のは良いプロダクトである証拠!) Slack連携機能が強く、HERPを開かなくてもタイムライン・メッセージの確認ができてノンストレス。SlackのアイコンがそのままHERP上も反映されるのも個人的に好きなポイントです。 自動連携できる求人媒体数が多く手作業が大幅に減る 導入結果 トライアルから非常にスムーズな導入を終え、運用を開始して早くも半年が経過しました。 結果から言うと導入して大正解!(もはやHERPなしでの採用は考えられないレベル)。 2023年6月以降の月次ごとの面接実施回数ですが、面接実施回数がどんどん上がってきました。 それだけ効率よく採用フローを回せるようになっているのだなと感じています。 月毎の採用面接数の推移 また、採用活動におけるリードタイムも出せるようになり、中長期目線での採用活動の計画も立てやすくなりました。同時に、ボトルネックになっているフェーズを洗い出して打ち手を考えることも出来、PDCAが回り出しました。(ペインポイント3.の解消!) 平均リードタイム また採用管理にかかる時間コストが大幅に減った分、採用メディア、人材エージェントの契約数も6社ほど新規契約することが出来、導入から4ヶ月でエントリー数を約2倍まで増やすことができました!私自身も「どんどんエントリー数が増えてきたな」と日々有り難く感じる半年間でした。 一方、ペインポイント2.に挙げていた会議室手配を含めたスケジュール調整の簡略化は出来ませんでした。 というのも、当社のスケジュール管理と会議室予約システムはGoogleカレンダーとOutlookを併用しておりますが、ATSに連携できるサービスはどちらか一方だったためです。他のフローが改善された分、こちらは仕方ない部分かなと思っています。 最後に PMの加藤 や デザインシンカー(デザインストラテジスト)の飯伏 も採用募集中と記事を書いていましたが、他複数ポジションでInsight Edgeに参画いただける仲間を引き続き探しています! HERPの導入と共に、会社の魅力を発信するために リクルートサイトのリニューアル もしました!こちらのコンテンツもどんどん充実させていきたいと計画していますので、お楽しみに!!
はじめに 社内ランチ共有アプリ 概要 API仕様 テーブル定義 ソースコード FastAPI,Pydantic,AWS DynamoDBの組み合わせた型安全なAPIの構築 Pydanticの基本的な使い方 入力データのバリデーションおよび型変換 データのシリアライズ(シリアル化) FastAPIとPydanticの組み合わせ リクエストボディのバリデーションチェック レスポンスデータのバリデーションおよびJSON Schemaへ自動変換 DynamoDBとPydanticの組み合わせ DynamoDBデータ格納時のPydanticモデル活用 DynamoDBからデータを取得した後のPydanticモデル活用 DynamoDBのデータ型とPydanticのデータ型の対応表 バリデーションエラーのハンドリング 実装上の注意点 まとめ はじめに こんにちは。 今年の7月からInsight Edgeに参画したDeveloperのニャットと申します。参画後すぐにも新規PoC案件のバックエンド/フロントエンドの設計から開発までを任せてもらえました。初めは慣れないアーキテクチャ選定や新しい技術の習得に少し頭を抱えていましたが、最近やっと形になって、毎日とても楽しく過ごせています。 近年のAPI開発では、データの型安全性とドキュメンテーションの容易さが重要視されています。弊社もフロントエンド開発ではTypescriptを積極的に採用しており、バックエンド開発におけるPythonプロジェクトでもモダンなPythonフレームワークを採用し、型を記述しながら開発を進めることを奨励しています。その中でFastAPIフレームワークは注目されているものの1つです。 本記事では、直近私がプロジェクトで実際使用しているFastAPIフレームワークでのAPI開発において、DynamoDBデータベースおよびデータ検証Pydanticライブラリの組み合わせ方および型安全なAPIの構築方法について皆さんに紹介したいと思います。説明のために「社内ランチ共有アプリ」を例として紹介します。 社内ランチ共有アプリ 概要 「社内ランチ共有アプリ」とは、実在しないアプリであり、今回のTechBlogに架空のアプリのテーマとしてChatGPTが提案してくれたものです。 当社で実施している シャッフルランチ というイベントに利用できそうで、このブログのきっかけで将来的にこのアプリが採用されることを期待しています。(笑) ChatGPTによると、アプリの機能は以下となります。 社内の全ての投稿を確認する 社員がランチに関する感想や場所を投稿する 社員が投稿に対してリアクション(いいね)を送る API仕様 上記のアプリのために、今回は以下のAPIを作成するとします。 ランチ投稿API GET /posts: すべてのランチ投稿を取得します。 POST /posts: ユーザーが新しいランチ投稿を作成します。 リアクション(いいね)API POST /posts/{post_id}/likes: 特定のランチ投稿にいいねを押します。 OpenAPIを使用して以下のように設計しました。 API仕様書 APIの仕様詳細は こちら をご覧ください。 テーブル定義 今回はこのようなDynamoDBテーブルとします。 PK (Partition Key) SK (Sort Key) UserId Timestamp Content ImageKey Restaurant Likes Posts Post#2f1fa759-72c2-a68e-364f-54fb3f1e4ed1 user123 1702355804 今日ここ行ってきました!とても良かった! lunch.jpg AAA店 [user456, user789] Posts Post#429d4ee2-44f1-d25b-9912-41c604eabef0 user456 1702442204 チーム〇〇でラーメンを食べてきました! ramen.jpg BBB店 ["user123"] Posts Post#f3c512e1-faad-b4cc-1bee-955db716bdca user789 1702528604 〇〇さんとランチしました。デザートがとても美味しかった! dessert.jpg CCC店 ["user123", "user456", "user789"] ソースコード 開発したソースコードは以下をご参照ください。この記事で紹介しているソースコードは以下のリポジトリに含まれているものになります。 サンプルコード 本記事では以下の内容を記事のスコープ外とします。 環境構築手順に関する説明 FastAPIアプリケーションの公開手順に関する説明 実際に手を動かしてみたい方はサンプルコードをご利用ください。 FastAPI,Pydantic,AWS DynamoDBの組み合わせた型安全なAPIの構築 Pydanticの基本的な使い方 Pydanticを使用する場合、通常は事前にモデルを作成し、データのスキーマや型などを定義する必要があります。Pydanticのデータモデルは、 BaseModel というクラスを継承した新しいクラスで構成されます。各フィールドで定義される型は、データの型ヒントとして機能します。 from pydantic import BaseModel from typing import List class LunchBlog (BaseModel): id : str user_id: str timestamp: int content: str image_key: str restaurant: str likes: List[ str ] = [] Pydanticはたくさんの機能を持っていますが、一般的に以下のために利用されることが多いかと思います。 入力データのバリデーション(検証)および型変換 データのシリアライズ(シリアル化) 入力データのバリデーションおよび型変換 以下のようにモデルクラスのインスタンスを作成すると、バリデーションチェックが自動的に行われます。 成功例 input_dict = { "id" : "2f1fa759-72c2-a68e-364f-54fb3f1e4ed1" , "user_id" : "user123" , "timestamp" : "1702355804" , "content" : "今日ここ行ってきました!とても良かった!" , "image_key" : "lunch.jpg" , "restaurant" : "AAA店" , "likes" : [] } lunch_blog = LunchBlog(**input_dict) print (lunch_blog) # 出力: id='2f1fa759-72c2-a68e-364f-54fb3f1e4ed1' user_id='user123' timestamp=1702355804 content='今日ここ行ってきました!とても良かった!' image_key='lunch.jpg' restaurant='AAA店' likes=[] 上記の例では input_dict が LunchBlog モデルスキーマに従っているため、 lunch_blog インスタンスが作成されました。 気づいている方もいるかもしれませんが、 timestamp の型を int としていたにもかかわらず、入力となっている input_dict の timestamp は文字列になっています。ここで、生成された lunch_blog インスタンスの timestamp は数字に変換されています。これはPydanticのデータ変換機能が働いているからです。一部のデータ型では入力されたデータの型が間違っていても正しい型に変換されるため、安心で便利だと思います。ただし、一部のクリティカルな機能ではデータの損失などが発生しないように気をつけるべきところでもありますね。 失敗例 モデルインスタンス時、もし以下のようにバリデーション違反のフィールドがあると、エラーが発生します。 # idフィールドを含まないinput_dict input_dict = { "user_id" : "user123" , "timestamp" : "1702355804" , "content" : "今日ここ行ってきました!とても良かった!" , "image_key" : "lunch.jpg" , "restaurant" : "AAA店" , "likes" : [] } lunch_blog = LunchBlog(**input_dict) # <-失敗処理 print (lunch_blog) # エラー: # pydantic_core._pydantic_core.ValidationError: 1 validation error for LunchBlog # id # Field required [type=missing, input_value={'user_id': 'user123', 't...: 'AAA店', 'likes': []}, input_type=dict] # For further information visit https://errors.pydantic.dev/2.5/v/missing 以上のように、pydanticのデータモデルを使用することでバリデーションチェックやデータの変換が可能になります。 データのシリアライズ(シリアル化) Pydanticモデルインスタンスから model_dump というPydanticのメソッドを使って簡単に辞書型に変換することもできます。 lunch_blog_dict = lunch_blog.model_dump() print (lunch_blog_dict) # 出力: {'id': '2f1fa759-72c2-a68e-364f-54fb3f1e4ed1', 'user_id': 'user123', 'timestamp': 1702355804, 'content': '今日ここ行ってきました!とても良かった!', 'image_key': 'lunch.jpg', 'restaurant': 'AAA店', 'likes': []} API開発においては、データシリアライズが常に求められますので、この機能があると便利ですね。 FastAPIとPydanticの組み合わせ 上記でPydanticの基本方を紹介しました。普段の開発でもたくさんの場面で活用できるPydanticですが、実はFastAPIと組み合わせて利用すると、FastAPI開発がとても便利になります。この理由はFastAPIはPydanticのベースでできており、Pydanticのための機能をたくさん用意しているからです。 サンプルコードにあった POST /posts APIのための以下のルーター関数を例として説明させていただきます。 以下のコードでは以下の機能を簡単に実装できています。 POST APIのリクエストボディのバリデーションチェック APIレスポンスのバリデーションチェックおよびJSON Schemaへ自動変換 # api/api/routers/post.py @ router.post ( "/posts" , status_code=status.HTTP_201_CREATED, response_model=PostCreatedResponse) def create_posts (request: Request, post: PostCreateRequest): """新しいランチ投稿を作成""" try : # リクエストしたユーザーのユーザー情報を取得 user_info = get_user_info_from_request(request) # DynamoDBに格納するための新規投稿レコード情報を準備 new_post_item = PostCreateToDB( **post.dict(), user_id=user_info.username, ) # DynamoDBに新規投稿レコードを格納 lunch_blog_table.create_post(new_post_item) except ClientError as err: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) else : # 処理が成功した場合、作成した投稿情報を返却 return new_post_item リクエストボディのバリデーションチェック FastAPIでは、リクエストボディの型チェックはPythonのほとんどのデータ型を指定しても検証できるようですが、最終的に全てPydanticで処理されるそうです。そのため、全てのパワーや利便性を発揮させるために、FastAPIはリクエストボディの型指定にはPydanticモデルの使用を推奨しています。 上記の create_posts ルーター関数のパラメター post はリクエストボディになります。 post の型として PostCreateRequest を指定しています。 def create_posts (request: Request, post: PostCreateRequest): class PostCreateRequest (BaseModel): """新しいランチ投稿を作成するためのリクエストモデル""" content: str = Field(validation_alias= "content" ) image_key: str = Field(validation_alias= "imageKey" ) restaurant: str = Field(validation_alias= "restaurant" ) 上記によって、リクエストされたリクエストボディオブジェクト post が PostCreateRequest モデルに従わない場合、バリデーションチェックが失敗します。例えば、リクエストボディに content が含まれていない場合、バリデーションエラーを示す 400 Bad Request を返します。 レスポンスデータのバリデーションおよびJSON Schemaへ自動変換 リクエストボディのバリデーションと共に、FastAPIではレスポンスのバリデーションも簡単にできます。 これは@routerデコレーター内に response_model を指定することで実現できます。 ※ @app デコレーターを利用している場合、 @app デコレーターのパラメーターとしてご指定ください。 @ router.post ( "/posts" , status_code=status.HTTP_201_CREATED,response_model=PostCreatedResponse ) class PostCreatedResponse (BaseModel): """新しいランチ投稿のレスポンスモデル""" id : str = Field(serialization_alias= "id" ) pk: str = Field(exclude= True ) sk: str = Field(exclude= True ) user_id: str = Field(serialization_alias= "userId" ) timestamp: str = Field(serialization_alias= "timestamp" ) content: str = Field(serialization_alias= "content" ) image_key: str = Field(serialization_alias= "imageKey" ) restaurant: str = Field(serialization_alias= "restaurant" ) likes: list = Field(serialization_alias= "likes" ) これによって、ルーター関数が値を return した際、自動的に response_model  で指定したモデルに従って、リクエストボディのバリデーションチェック同様、レスポンスのバリデーションが行われます。 指定したモデルと一致しないデータが返された場合、FastAPIはエラーを発生させ、型安全性を保つことができます。 一般的に、APIのレスポンスとしてJSON Schemaを生成してからreturnするケースがありますが、FastAPIでは response_model パラメータを使用することで、この手間を省くこともできます。ルーター関数の戻り値(他のPydanticモデルインスタンス、普通のPython辞書型オブジェクト等)から自動的にJSONを生成してクライアントに返します。 PostCreatedResponse レスポンスモデルを見てみますと、 exclude=True や serialization_alias="userId" などのオプションを使用しています。これはレスポンスシリアライズ時に適用されるオプションになります。 exclude=True : ルーター関数の戻り値から指定したキーを除外します。ここではpk, skのキーを除外しています。 serialization_alias="userId" : モデルインスタンスにおけるフィールド名をそのままシリアライズに使用するのではなく、他の形に変換するためにシリアライズエイリアスを指定します。フロントエンドアプリケーションのためのAPIである場合、キャメルケースに変換したいという使用用途もたくさんあるでしょう。 上記の仕組みにより、開発者はデータの構造や型について心配せずに、簡潔なコードで型安全なAPIを構築できます。FastAPIの便利な機能を駆使して、より効率的で信頼性の高いAPI開発が可能です。 DynamoDBとPydanticの組み合わせ 上記でPydanticとFastAPIの組み合わせについて説明しました。ではPydanticとDynamoDBの組み合わせはどうでしょうか。 DynamoDBデータ格納時のPydanticモデル活用 先ほどの POST /posts APIのコードをもう一度見てみますと、データベースに関連するコードは以下のみで成り立っていると思います。 # DynamoDBに格納するための新規投稿レコード情報を準備 new_post_item = PostCreateToDB( **post.dict(), user_id=user_info.username, ) # DynamoDBに新規投稿レコードを格納 lunch_blog_table.create_post(new_post_item) 実はDynamoDBにデータを格納する前に、 PostCreateToDB モデルを活用してデータの準備を行っています。 class PostCreateToDB (BaseModel): """新しいランチ投稿のデータベース登録用モデル""" id : str = Field(exclude= True ) # model_dump()で除外されるように設定 pk: str = Field(default= "Posts" , serialization_alias= "PK" ) sk: str = Field(serialization_alias= "SK" ) user_id: str = Field(serialization_alias= "UserId" ) timestamp: str = Field(default_factory=generate_current_timestamp, serialization_alias= "Timestamp" ) content: str = Field(serialization_alias= "Content" ) image_key: str = Field(serialization_alias= "ImageKey" ) restaurant: str = Field(serialization_alias= "Restaurant" ) likes: List[ str ] = Field(default=[], serialization_alias= "Likes" ) @ model_validator (mode= "before" ) @ classmethod def generate_values (cls, values: dict [ str , Any]) -> dict [ str , Any]: """id, skを生成するメソッド""" id = generate_uuid() values[ "id" ] = id values[ "sk" ] = f "Post#{id}" return values 一見複雑に見えますが、このモデルでは以下のことを実現しています。 DynamoDBに格納するための「ランチ投稿」Itemの必要なスキーマを定義しています。 POST /posts APIのリクエスト情報から得られなかったが、データベース内のデータ管理に必要なフィールドの値を生成します。 PKに"Posts"という文字列をデフォルト値と設定 投稿の識別番号として、PostIdのUUIDを生成(DynamoDBでは次のSKの形で管理) SKに「Post#{自動的に生成されたUUIDのPostId}」文字列を格納 Timestampに格納するためのUNIX Timestampを生成 Likesに空のリストをデフォルト値として設定 DynamoDBに格納する際、各フィールドのキー名をPython実装用のスネークケースからDynamoDBのスキーマ用のパスカルケースに変更設定(DynamoDBでは特にAttributeの命名規則が決まっていませんが、可読性を上げるためパスカルケースを使用することが多いでしょう) 上記によって、 new_post_item を PostCreateToDB のモデルインスタンスで作成すると、DynamoDBにデータを格納する際、以下の簡単な記述ができます。 # api/api/databases/tables/lunch_blog.py class LunchBlogTable (DynamodbTable): """LunchBlogテーブルを操作するためのクラス""" def __init__ (self): # (省略) def create_post (self, post: PostCreateToDB): """新しいランチ投稿を作成""" try : self.table.put_item( Item=post.model_dump(by_alias= True ), ) except ClientError as err: logger.exception(err) raise 上記で by_alias=True を指定した理由は PostCreateToDB インスタンスを辞書型に変換する際、キー名を serialization_alias で指定したものを使用するようにするためです。 データモデリングを全てPydanticで一元管理することによって、DynamoDBの処理コードでは、特に複雑なコードを追加せずに、型の管理も不要になります。 DynamoDBからデータを取得した後のPydanticモデル活用 先ほど POST /posts APIを例としてみていましたが、 GET /posts APIはどうでしょうか。 ルーターの関数は以下になっています。 # api/api/routers/post.py # (省略) @ router.get ( "/posts" , status_code=status.HTTP_200_OK, response_model=PostInfoListResponse) def get_posts (): """全てのランチ投稿を取得""" try : posts = lunch_blog_table.get_posts() except ClientError as err: logger.exception(err) raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) else : return posts lunch_blog_table.get_posts() 関数でデータベースからデータを取得し、そのまま return しています。 get_posts() の中身はどうなっているのでしょうか。 # api/api/databases/tables/lunch_blog.py def get_posts (self): """全てのランチ投稿を取得""" try : response = self.table.query( KeyConditionExpression=Key( "PK" ).eq( "Posts" ) & Key( "SK" ).begins_with( "Post#" ), ) except ClientError as err: logger.exception(err) raise else : return response[ "Items" ] これも至ってシンプルの実装で response["Items"] を返却しています。詳細はboto3の仕様をご確認いただけますと幸いですが、これはboto3の query() 関数の戻り値で、 Items Attributesを切り取ったものになっていますね。 response["Items"] にはDynamoDBの各カラムがキーになっているDynamoDBのItemオブジェクトの配列になっており、以下のようなものを想定しています。 [ { "PK": "Posts", "SK": "Post#ddba3f07-9079-4328-a779-e9708576f6fd", "UserId": "bde2a27e-1611-4f00-ae5b-df597500409a", "Timestamp": 1702853246, "Content": "今日ここ行ってきました!とても良かった!", "ImageKey": "lunch.jpg", "Restaurant": "AAA店", "Likes": [] } ] ルーター関数では上記の配列をreturnしていますが、実際のAPI仕様では、PK, SKの情報は不要としており、その代わりに id 情報が必要のように、設計していました。また、POST API同様、フロントエンドのためのAPIであるため、各キーはパスカルケースではなく、キャメルケースで返したいと思います。 ここで、レスポンスをAPI仕様通りに実装するために、 @router デコレーターのパラメータとして response_model=PostInfoListResponse を指定しています。これを指定することによって、レスポンスを返却する前に以下の動作が行われます。 データベースから取得したデータが PostInfoListResponse モデルのスキーマに従っているかのバリデーションチェック データを PostInfoListResponse モデルのシリアル設定(excludeオプション、serialization_aliasオプションの設定等)に従ってデータのシリアライズを行って、クライアントに返却する。 PostInfoListResponse は以下のように定義しています。 class PostInfoInDB (BaseModel): """DynamoDBに格納されているランチ投稿情報のモデル""" pk: str = Field(validation_alias= "PK" ) sk: str = Field(validation_alias= "SK" ) user_id: str = Field(validation_alias= "UserId" ) timestamp: str = Field(validation_alias= "Timestamp" ) content: str = Field(validation_alias= "Content" ) image_key: str = Field(validation_alias= "ImageKey" ) restaurant: str = Field(validation_alias= "Restaurant" ) likes: List[ str ] = Field(validation_alias= "Likes" ) class PostInfoReponse (PostInfoInDB): """ランチ投稿情報のレスポンスモデル""" id : str pk: str = Field(validation_alias= "PK" , exclude= True ) sk: str = Field(validation_alias= "SK" , exclude= True ) user_id: str = Field(validation_alias= "UserId" , serialization_alias= "userId" ) timestamp: str = Field(validation_alias= "Timestamp" , serialization_alias= "timestamp" ) content: str = Field(validation_alias= "Content" , serialization_alias= "content" ) image_key: str = Field(validation_alias= "ImageKey" , serialization_alias= "imageKey" ) restaurant: str = Field(validation_alias= "Restaurant" , serialization_alias= "restaurant" ) likes: list = Field(validation_alias= "Likes" , serialization_alias= "likes" ) @ model_validator (mode= "before" ) @ classmethod def generate_values (cls, values: dict [ str , Any]) -> dict [ str , Any]: """idを生成するメソッド""" id = values[ "SK" ].split( "#" )[ 1 ] values[ "id" ] = id return values PostInfoListResponse = RootModel[List[PostInfoReponse]] 先ほど、 GET /posts APIのレスポンスは PostInfoListResponse モデルに従わないといけないことを説明しました。ただし、上記の定義を見ると PostInfoListResponse は PostInfoListResponse = RootModel[List[PostInfoReponse]] と定義されています。これは GET /posts APIのレスポンスは PostInfoReponse モデルに従っている要素のリストである必要があると意味しています。 PostInfoReponse では、以下のように user_id: str = Field(validation_alias= "UserId" , serialization_alias= "userId" ) validation_alias オプションと serialization_alias を使用しているところがたくさんありますが、なぜ?と疑問に思っている方がいるかもしれません。これについて説明します。 先ほどDynamoDBから取得したデータをそのまま返却し、 PostInfoListResponse によってバリデーションチェックを行っていると説明しました。ここの過程では以下の動作が行われています。 データベースからデータを取得した後、 PostInfoListResponse のインスタンスを作成し、同時にバリデーションチェックを行っています。インスタンス作成のための入力データはDynamoDBから取得したものになりますので、各キーはパスカルケースになっています。そのためバリデーションチェックが失敗しないよう、 validation_alias オプションを使用して、DynamoDBのパスカルケースのキーを指定します。 インスタンスを作成後、インスタンスからシリアルデータを生成し、クライアントに返却します。ここではAPI利用者の利便性のために、キャメルケースに変換するために serialization_alias オプションを使用します。 上記のように、Pydanticモデルを使用することで、様々な命名規約に柔軟に対応できると思います。 また、記事の先頭あたりで説明があったように、pydanticのモデルは通常 BaseModel というクラスを継承して作成すると話しましたが、 PostInfoReponse モデルは PostInfoInDB モデルを継承しています。実はこの書き方すると、 PostInfoReponse も BaseModel を継承していることに変わりはないですが、 PostInfoInDB クラスを別途で作成した理由はこのクラスで単純にDynamoDBの構成を定義したいんです。上のコードから分かるように GET /posts APIのレスポンスのために PostInfoReponse では色々カスタマイズを入れており、 GET /posts APIのレスポンスモデル専用になっています。ただし、実際の開発では様々なケースでDynamoDBの構成から別のカスタマイズをしたいというケースもあると思います。 PostInfoInDB を定義することで、別の使用ケースでもこのクラスを継承してカスタマイズできます。ここを意識すると実装も簡単に進めることがでいるでしょう。 DynamoDBのデータ型とPydanticのデータ型の対応表 DynamoDBの主な型定義をPydanticで定義する場合の対応表をまとめました。 DynamoDB データ型 Pydantic モデルのフィールド例 String field_name: str Number (Integer) field_name: int Number (Float) field_name: float Boolean field_name: bool List (一種要素リスト) field_name: List[element_type] List (混在要素リスト) field_name: List[Union[element_type_1, element_type_2]] Map (Nested Object) field_name: Dict[str, element_type] Binary field_name: bytes Set field_name: Set[element_type] Null field_name: Optional[None] = None バリデーションエラーのハンドリング 記事の内容で、所々「バリデーションエラーが発生してしまいます」という書き方をしていたかと思います。 FastAPIの仕様により、バリデーション検証において、以下のエラーが発生します。 RequestValidationError: リクエストのバリデーションチェックエラー時に発生 ResponseValidationError: レスポンスのバリデーションチェックエラー時に発生 上記を使用して、アプリケーション全体を共通して、以下のエラーハンドリングの設定すると便利です。 RequestValidationError発生時: リクエストに不正なデータが含まれているため、 HTTP 400 Bad Request エラーを返却します。 ResponseValidationError発生時: 予想したレスポンスと異なるデータを返却している処理が行われているため、 HTTP 500 Internal Server Error エラーを返却します。 # api/api/main.py # (省略) from fastapi.exceptions import HTTPException, RequestValidationError, ResponseValidationError # (省略) # リクエスト不正エラーハンドリング @ app.exception_handler (RequestValidationError) async def request_validation_exception_handler (request, exc): """リクエストのバリデーションエラーをハンドリングする RequestValidationErrorが発生する時、400 Bad Requestを返す """ logger.error(f "RequestValidationError: {exc}" ) return JSONResponse(status_code= 400 , content={ "message" : "Bad Request." }) # レスポンス不正エラーハンドリング @ app.exception_handler (ResponseValidationError) async def response_validation_exception_handler (request, exc): """レスポンスのバリデーションエラーをハンドリングする ResponseValidationErrorが発生する時、500 Internal Server Errorを返す """ logger.error(f "ResponseValidationError: {exc}" ) return JSONResponse(status_code= 500 , content={ "message" : "Internal Server Error." }) 実装上の注意点 この記事ではPydantic(v2)を使用しています。今年リリースされたばかりのバージョンであり、インターネット上ではv1の書き方をたくさん紹介されていると思います。実装時は必ずPydantic(v2)の仕様を確認してから実装してください。 Pydanticモデルの型定義をAPI仕様、DynamoDB仕様に合わせるため、型の記述に注意してください。int, strなど簡単な型は問題がないと思われますが、他の型にするためには多少工夫を加える必要があると思います。APIの型確認は実装しながら自動生成されたOpenAPIドキュメントを開きながら、元のOpenAPI設計書と比較したほうが確実でしょう。 Pydanticを採用することで、データモデリングの一元管理が実現され、コードの可読性が向上する一方で、モデルを適切に設計せずに複雑なモデルを作成すると、逆にモデルの内容を理解しにくくし、可読性を低下させる可能性があります。モデルの作成は慎重に行う必要があり、特にチーム内で共有された設計ガイドラインに基づいて作成されるべきでしょう。 まとめ サーバーレス構成で、FastAPI、DynamoDBを利用しながら、Pydanticで型安全の実装方法をご紹介しました。Pydanticを使用すると型安全性が保たれるだけでなく、バリデーション、レスポンスのシリアライズなども簡単に実装できて、安全性を保ちながら実装速度を上げることができるかと思います。
こんにちは!Insight Edge で Developer をしている Matsuzakiです。 弊社ではよくバックエンドをFastAPIで構築し、Lambdaにコンテナとしてデプロイする構成が取られています。 FastAPIをLambdaで動かす方法としては今までMangumを使っていましたが、どうやらかなり簡単に導入できるらしい(一行追加するだけ?!)+新しい試みということで、今回『Lambda Web Adapter』を使ってコンテナ製FastAPIアプリケーションをLambdaで動かしてみました。 本記事では、Mangumと比較しながらその設定方法などをご紹介します。 Lambda Web Adapter、気になっていたけれど試したことがないという方など、ご参考になれば幸いです。 本記事でわかること Lambda Web Adapterの概要 FastAPIで利用する際のLambda Web Adapterの導入方法 Mangumと比較したLambda Web Adapterのユースケース ※ 今回、Lambda Web Adapterに関する性能の検証は実施していません。 目次 Lambda Web Adapterとは Lambda Web Adapterのメリット 導入方法 Mangumを使用した実装方法 Lambda Web Adapterを使用した実装方法 MangumとLambda Web Adapter 感想 Lambda Web Adapterとは 一言で言うと、「様々なWebフレームワークで記述されたアプリケーションを(ほぼ)共通の書き方でLambda上で動かせるようにするツール」です。 通常、フレームワークをLambdaで動かす際にはフレームワークが受け取るリクエストの形式とLambdaが受け取るイベント形式間のギャップを埋めるため、各フレームワーク毎に特定のライブラリやアダプタを使うことが一般的かと思います。 しかし、このLambda Web Adapterを使うと、どのフレームワークを使うにせよ、”基本的には” 以下のようなコード一行をDockerfileに追加するだけでLambda上で任意のWebフレームワークを動かせるようになります。 COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter ※ "基本的には"と言ってますが、環境や使うイメージによっては上記一行だけではなく、少々追加の設定が必要になります。 詳しくはLambda Web Adapterの 公式GitHub をご参照ください。 Lambda Web Adapterのメリット 個人的には、Lambda Web Adapterを使うメリットは大きく以下3点と考えます。 導入の手軽さ 移植性の高さ 一貫した記述方法 導入の手軽さ 1つ目のメリットは、「 導入の手軽さ 」です。 先程も触れましたが、基本的にはDockerfileにコードを一行(または数行)加えるだけで導入ができます。追加ライブラリのインストールや、アプリケーションのコード自体に手を加える必要はありません。 まずは1回試してみようと気軽に挑戦しやすいと思います。 移植性の高さ 2つめのメリットは「 移植性の高さ 」です。 Lambda Web Adapterを使用すると、「同じDockerイメージを異なるプラットフォームで利用できる」という利点があります。これは非常に大きなメリットです。 例えば、「同じイメージをLambdaでもFargateでも利用したい」というケースや、「Lambdaで実行していたアプリケーションををFargateに移行したい」というケースがあると思います。 通常、異なるプラットフォーム間でアプリケーションを移行する際には、環境ごとにコードの修正や設定の変更が必要になる場合があります。 しかし、Lambda Web Adapterを使用すると、同じDockerイメージをLambda、EC2、Fargate、ローカルコンピューター上で実行することができます。 異なる実行環境間でイメージを変更せずに実行できるというのは非常に便利です。 一貫した記述方法 最後3つ目のメリットは「 一貫した記述方法 」です。 一般的に、異なるWebフレームワークをLambdaで実行する際、各フレームワーク専用のアダプターやライブラリが必要になります。これには学習コストが伴うと共に、システム内で複数のフレームワークを使用する場合、その管理はさらに複雑になります。 それもLambda Web Adapterを使用することにより、フレームワークによらず共通の書き方で導入することができます。 これにより、学習コストや管理の煩雑さを減らすことができます。 上記、「 導入の手軽さ 」「 移植性の高さ 」「 一貫した記述方法 」はLambda Web Adapterを使用する大きなメリットと言えるのではないでしょうか。 ※ Lambda Web Adapterの採用が最適な選択かどうかは、プロジェクトの具体的な要件や状況によって異なります。 導入方法 さて、ここからはようやくメインである、Lambda Web Adapterの導入方法を記載していきます。 また、今回はこれまで利用していたMangumの実装方法との比較も行いながら、FastAPI製コンテナをLambda上で動かす方法を以下の順でご紹介します。 Mangumを使用した実装方法(※これまでのやり方) Lambda Web Adapterを使用した実装方法 ぜひ、実装内容を比較しながら見ていただければと思います。 ※ 今回はLambda Web Adapterの設定方法に焦点を当てるため、FastAPIのセットアップやLambdaなどのインフラ構築は完了していることを前提とします。 Mangumを使用した実装方法 ここでは、Mangumを使っていた今までの実装内容を載せていきます。 ※ Mangumは、FastAPIのようなASGIアプリケーションをLambdaで動作できるように変換するためのアダプタです。 詳しくは Mangum公式ドキュメント をご参照ください。 アプリ側のコード アプリケーションのエントリーポイントとなる main.py は以下のように実装しています。 FastAPIのインスタンスをMangumでラップしています。 これによって、FastAPIアプリケーションはLambdaのイベントとコンテキストに対応するようにラップされ、Lambdaの関数として実行可能になります。 from fastapi import FastAPI from mangum import Mangum app = FastAPI() # FastAPIのルーティングやビジネスロジックの実装(省略) lambda_handler = Mangum(app) Dockerfile AWS提供のLambda用イメージを使用しています。 先程の main.py 内の lambda_handler をLambdaのハンドラとして登録するために、CMDを上書きしています。 FROM public.ecr.aws/lambda/python:3.11 # 依存関係のインストールなど(省略) CMD [ "main.lambda_handler" ] 導入の手軽さという意味では、Mangumもかなり手軽なものの、ライブラリの追加とアプリコードの修正が必要となります。 以上が、Mangumを使ってLambda上でFastAPIを使えるようにする方法です。 Lambda Web Adapterを使用した実装方法 今回のメインです。 ここからは実際にLambda Web Adapterを使った実装方法を記載していきます。 アプリ側のコード main.py は、以下となります。 from fastapi import FastAPI app = FastAPI() # ヘルスチェック用 @ app.get ( "/" ) async def health_check (): return { "message" : "success" } # FastAPIのルーティングやビジネスロジックの実装(省略) Mangumを使用しないため、単純にFastAPIインスタンスを作成して利用しています。 Lambda Web Adapterではデフォルトでルートパス( / )に対して自動的にヘルスチェックを行うため、ルートにヘルスチェック用の関数を追加しています。 Dockerfile Dockerfileはこのようになります。 一行目以外は、Mangum利用時と異なっています。 FROM public.ecr.aws/lambda/python:3.11 COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter # 依存関係のインストールなど(省略) ENTRYPOINT [ "uvicorn" ] CMD [ "main:app" , "--host" , "0.0.0.0" , "--port" , "8080" ] 具体的に中身を見ていきましょう。 まず、冒頭でも触れた以下の部分です。 COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter この一行でLambda Web Adapterがイメージに含まれ、Lambda上で使用可能になります。言語や利用するイメージによっては、既存のDockerfileにこの行を追加するだけでLambda上で動作するようになります。 ただ、今回の条件(FastAPIを利用かつAWS管理イメージを利用)では追加の設定が必要となります。 追加した以下2行について解説していきます。 (このたった2行に辿り着くまでになかなかハマりました。 日下さん 、その節はありがとうございます!) ENTRYPOINT [ "uvicorn" ] CMD [ "main:app" , "--host" , "0.0.0.0" , "--port" , "8080" ] Lambda Web Adapterではサーバープログラムがコンテナ内で実行されます。このため、FastAPIアプリケーションをホストするためにUvicornサーバーを起動します。 また、Lambda Web Adaptorは HTTP GETリクエストを8080ポート(デフォルト)で送信するため、8080ポートでリッスンするように指定しています。 今回のようにAWS管理イメージを使用する場合は、AWSのエントリーポイントによってデフォルトのCMDがオーバーライドされるため、ENTRYPOINTも上書きします。これにより、Uvicornサーバーが適切に起動し、FastAPIアプリケーションがLambda環境で正常に動作するようになります。 ※ AWS管理イメージを利用しない場合はENTRYPOINTの上書きは不要であるため、以下のように設定します。 CMD [ "uvicorn" , main:app ", " --host ", " 0.0.0.0 ", " --port ", " 8080 " ] 補足 ※ Lambda Web Adapterがデフォルトでアクセスするポート番号を変更する場合は、以下のようなコードをDockerfileに追加して環境変数 PORT を上書きします。 ENV PORT {ポート番号} ※ ヘルスチェックのパスを変更する場合は、以下のようなコードをDockerfileに追加して環境変数 READINESS_CHECK_PATH を上書きします。 ENV READINESS_CHECK_PATH {ヘルスチェックのパス} ※ M1/M2チップ搭載のMacを利用する際は、異なるアーキテクチャ間の互換性を確保するために以下のオプションをビルドコマンドに追加します。 --platform linux/amd64 MangumとLambda Web Adapter 本記事では、Lambda Web Adapterを使用してFastAPIをLambda上で動かす方法をMangumを使用した方法と比較しながらご紹介しました。 では今回のようにFastAPIとLambdaを組み合わせる場合、どちらのアプローチをどのようなケースで選択するのが良いのでしょうか。 個人的な考えとしては、 Lambdaだけで動かすのであれば Mangum 他のプラットフォームでも動かす可能性があるのであれば Lambda Web Adapter が適しているのではないかと思いました。 「 導入の手軽さ 」で言えば、Lambda Web Adapterの方が若干勝るものの、両者とも手軽に設定できるため大差はありません。 違いは、「 移植性の高さ 」と「 イベントデータの取得の容易さ 」にあると考えます。 「移植性の高さ」では、同じイメージを他プラットフォームで再利用可能であるLambda Web Adapterが勝ります。 一方、Lambdaからの「イベントデータの取得の容易さ」に関しては、Lambdaイベントの生データに直接アクセス可能であるMangumの方が勝ります。 例として、LambdaとAPI Gatewayを統合してFastAPI製のAPIを構築する際に、APIの認可としてCognitoユーザープールをオーソライザーとして使用し、「ユーザーの認証情報(クレーム)をLambdaイベントのデータから取得する」というケースを考えてみます。 ログイン中のユーザー名をアプリに表示したい場合、ユーザー属性ごとにアクセス制御したい場合などにこのような認証情報が必要となるでしょう。 ではこのケースにおいて、MangumとLambda Web Adapterではデータの取得方法にどのような違いがあるのでしょうか。 以下で見ていきましょう。 まず、Mangumを使用する際は、以下のようなコードで直接的かつ簡潔にLambdaイベントのデータから認証情報を取得できます。 auth_info = request.scope[ "aws.event" ][ "requestContext" ][ "authorizer" ][ "claims" ] Lambdaイベントの生データに直接アクセスできるため、結果として認証情報の取得を容易にします。 一方Lambda Web Adapterでは以下のようにいくつかの追加のステップが必要になります。 # headersを探す headers = dict (request.scope[ "headers" ]) # 'x-amzn-request-context'の値を文字列に変換 x_amzn_request_context_str = headers[b "x-amzn-request-context" ].decode( "utf-8" ) # JSON形式に変換 x_amzn_request_context = json.loads(x_amzn_request_context_str) auth_info = x_amzn_request_context[ "authorizer" ][ "claims" ] Lambda Web Adapterを使用してイベントデータを取得する場合は、上記のようにデコードやJSONへの変換処理などが必要になる場合があり、やや複雑さが増します。 比較すると、Mangumを使用したアプローチは、Lambdaイベントデータへのアクセスがより容易で直感的であることが明らかです。 今回性能面については検証できておりませんが、他記事として こちら を拝見する限り、Lambda Web Adapterを利用することで特別遅くなるということはなさそうです。 ただ、利用するフレームワークによって異なる可能性があるため、実際にご利用する際はご自身で性能を検証することをお勧めします。 感想 今回初めてLambda Web Adapterを使ってみましたが、「一瞬で設定できそう?」という想定とは裏腹に、利用するイメージや環境ならではの設定など結構ハマりどころがありました。しかし、ハマった反面学ぶことが多く、新たな知識を得る良い機会となりました。 Lambda Web Adapterは、様々なフレームワークに対応した非常に便利なアダプタなので、気になっていた方は一度試してみてはいかがでしょうか。 本記事がどなたかのご参考になれば幸いです!
introduction はじめまして!今年の7月からInsight Edge開発チームに加わった塚越です。 ChatGPT関連のPoCに携わっています!開発だけでなく、分析の要素も経験もさせていただき、とても楽しく取り組んでいます。 また、Insight Edgeではコワーキングスペースの利用が可能なので、家の近くで集中できる環境を手に入れ、快適な日々を過ごしています。 今回は、LLMを使用したチャットボットへの会話履歴の実装方法について、試行錯誤した経験を共有したいと思います。 目次: introduction 概要 環境 実装 ハマりどころ まとめ 感想 概要 チャットボットへの会話履歴の実装方法は色々ありますが、今回はLangChainから以下のモジュールを採用しました。 ChatPromptTemplate.from_messages SystemMessagePromptTemplate ConversationTokenBufferMemory(こちらはConversationBufferMemoryでも他のものでも可) ConversationChain そしてSystemMessagePromptTemplateに入力値を組み込んだ独自のシステムテンプレートを実装することが今回の目標です。 本記事では扱っていませんが、ベクトルストアから取得した文書をシステムテンプレートに組み込みたかったことが発端です。 公式ドキュメントを参照した個々の実装は容易ですが、そのまま組み合わせてもエラーが発生しました。 ConversationChainではプロンプトに利用する入力値を変えられないという点でつまづき、SystemMessagePromptTemplateに入力値を組み込んだまま実装する方法を試行錯誤しました。 上手くいった方法は 実装 にてご紹介し、つまづいた点については ハマりどころ にてご紹介します。 環境 Python 3.10.12 langchain 0.0.225 実装 環境変数 以下の .env ファイルを同階層に配置してください。 OPENAI_API_KEY=(ご自身のキーを記載してください) なお、実際の案件では下記AzureChatOpenAIを利用しています。 from langchain.chat_models import AzureChatOpenAI # langchain 0.0.225 使い方の詳細は割愛します。 上手くいった方法 以下の点に注意することで上手くいきました。 ConversationChainで利用するプロンプトの入力値 history と input を公式ドキュメント通りに利用すること。 システムテンプレートに独自の入力値を持たせたい場合は、Pythonの文字列フォーマットを使って値を入れてからSystemMessagePromptTemplateで利用すること。 参考①: ChatPromptTemplateとSystemMessagePromptTemplate.from_templateの利用 参考②: Memory機能を持たせたChatPromptTemplateの利用 参考③: ConversationTokenBufferMemoryの利用 参考④: ConversationChainの利用 from langchain.llms import OpenAI from langchain.chains import ConversationChain from langchain.memory import ConversationTokenBufferMemory # 下記は参考サイトをベースに自分で取捨選択して記載 from langchain.prompts import ( ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, ) # 環境変数の読み込み from dotenv import load_dotenv load_dotenv() def answer_query (query, input_language, output_language): # ChatPromptTemplateとSystemMessagePromptTemplate.from_templateの利用(参考①) # Memory機能を持たせたChatPromptTemplateの利用(参考②) template= "You are a helpful assistant that translates {input_language} to {output_language}." template_filled=template.format(input_language=input_language, output_language=output_language) chat_prompt = ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(template_filled), MessagesPlaceholder(variable_name= "history" ), HumanMessagePromptTemplate.from_template( "{input}" )]) # ConversationTokenBufferMemoryの利用(参考③) llm = OpenAI() # トークン数に応じて会話履歴の抽出数が調整されることを max_token_limit で確認する memory = ConversationTokenBufferMemory(llm=llm, max_token_limit= 30 , return_messages= True ) memory.save_context({ "input" : "hi" }, { "output" : "こんにちは" }) memory.save_context({ "input" : "not much you" }, { "output" : "私はあまりあなたを知りません" }) # ConversationChainの利用(参考④) conversation = ConversationChain(prompt=chat_prompt, llm=llm, verbose= True , memory=memory) # LLMへ質問 return conversation.predict( input =query) # 質問を入力 answer=answer_query( "Hi there!" , input_language= "English" , output_language= "日本語" ) # 回答を表示 print (answer) 出力内容 > Entering new chain... Prompt after formatting: System: You are a helpful assistant that translates English to 日本語. Human: not much you AI: 私はあまりあなたを知りません Human: Hi there! > Finished chain. AI: やあ! これらの出力から下記を確認し、意図した挙動が実現できたことを確認しました。 システムテンプレートに"English"、"日本語"という入力値が反映されている。 会話履歴が反映されいる(verbose=Trueによって出力可能)。 max_token_limit=30 により、会話履歴の抽出数が調整されている(「hi」「こんにちは」の会話が削除されている)。 ハマりどころ ポイント1:ConversationChainで利用するプロンプトの入力値を変えることができなかった 参考②のLLMChainではプロンプトの入力値を変更しても問題なく動作しました。 しかし、ConversationChainでは名前を変更できず、ここにハマりました。 実際に試した上手くいかない例1 上手くいった方法 からプロンプトの入力値 history と input を別の名前に変更したところ、期待される値が見つからない旨のエラーが発生しました。 (省略) MessagesPlaceholder(variable_name="chat_history"), HumanMessagePromptTemplate.from_template("{human_input}")]) (省略) # LLMへ質問 return conversation.predict(human_input=query) # 質問を入力 answer=answer_query("Hi there!", input_language="English", output_language="日本語") # 回答を表示 print(answer) ポイント2:入力値付きSystemMessagePromptTemplateをConversationChainに指定することができなかった 上手くいった方法 では template を単純な文字列 template_filled に変換して利用しています。 他のモジュールでは、複数の入力値を持つプロンプトテンプレートの扱い方が見受けられます。 今回の目標条件下での応用を試みましたが、入力値の挿入タイミングの特定が困難であり、適切な実装方法を見つけることができず、この点で大きくつまづきました。 以下は、複数の入力値を持ったままプロンプトテンプレートを扱うLLMChainの例です。 参考: LLMChainの入力例 from langchain.chains import LLMChain from langchain.llms import OpenAI from langchain.prompts import PromptTemplate # Multiple inputs example template = """Tell me a {adjective} joke about {subject}.""" prompt = PromptTemplate(template=template, input_variables=[ "adjective" , "subject" ]) llm_chain = LLMChain(prompt=prompt, llm=OpenAI(temperature= 0 )) print (llm_chain.predict(adjective= "sad" , subject= "ducks" )) 出力結果 Q: What did the duck say when his friend died? A: Quack, quack, goodbye. これをComversationChainとSystemMessagePromptTemplateを使って試行錯誤した記録は以下の通りです。 実際に試した上手くいかない例2 from langchain.llms import OpenAI from langchain.chains import ConversationChain from langchain.memory import ConversationTokenBufferMemory from langchain.prompts import ( ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate, MessagesPlaceholder, ) # 環境変数の読み込み from dotenv import load_dotenv load_dotenv() def answer_query (query, input_language, output_language): template= "You are a helpful assistant that translates {input_language} to {output_language}." chat_prompt = ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(template), MessagesPlaceholder(variable_name= "history" ), HumanMessagePromptTemplate.from_template( "{input}" )]) llm = OpenAI() memory = ConversationTokenBufferMemory(llm=llm, max_token_limit= 30 , return_messages= True ) memory.save_context({ "input" : "hi" }, { "output" : "こんにちは" }) memory.save_context({ "input" : "not much you" }, { "output" : "私はあまりあなたを知りません" }) conversation = ConversationChain(prompt=chat_prompt, llm=llm, verbose= True , memory=memory) return conversation.predict(input_language=input_language, output_language=output_language, input =query) answer=answer_query( "Hi there!" , input_language= "English" , output_language= "日本語" ) print (answer) 下記の2行を conversation = ConversationChain(prompt=chat_prompt, llm=llm, verbose=True, memory=memory) の直前に追記してみたり・・・もちろんエラーが発生します・・・。 from langchain.prompts import PromptTemplate chat_prompt = PromptTemplate(template=chat_prompt, input_variables=[ "input_language" , "output_language" ]) 紹介はしませんが、思いつくままに試してたくさん失敗しました。 まとめ SystemMessagePromptTemplateに入力値を組み込みながら、ConversationChainで利用する方法は見つかりませんでした。 代わりに、Pythonの文字列フォーマットを使って値を入れてからSystemMessagePromptTemplateを利用する方法でなんとか対応できました。 感想 ConversationChainではなくLLM Chainを使ったり、ChatPromptTemplateではなくPromptTemplateを使ったりする選択肢もありました。 しかし、その変更が性能に影響を与える可能性があったので、できるところまで挑戦し続けました。 LangChainで色々なモジュールを組み合わせる際は、自分で一から試行錯誤するよりも、実装例を探してその通りに実施する方がスムーズだと感じました。 モジュールの細部を完全に理解すれば、自分で解決策を見つけることも可能かと思いますが、それはそれでかなりの労力が必要そうだったので、まずは色々と試行錯誤してみました。 今回は会話履歴についてご紹介しましたが、QAや要約タスクに関しても精度の向上を目指して日々努力しています。 機会があれば、その成果もシェアしたいと思います!
はじめまして、飯伏(いぶし)と申します。今年度4月からInsight Edgeの一員に加わりました。 私は、デザイン思考のアプローチを軸に、DXの初期フェーズにあたる課題探索やアイデア想起からご支援するコンサルタントをしています。自己紹介では、よくデザインシンカーと名乗っています。 このように話すと、たいてい「なんだそれ」という反応をされます。そこで今回は「実はこんな仕事をしているメンバーがInsight Edgeにいるよ」をお伝えできればと思います。 Insight Edgeは住友商事グループのDXを加速することがミッションの技術専門会社ですが、DXに必要な(広義の)技術を提供するメンバーとして飯伏のような人間もいると知っていただければ嬉しいです!  ※ PM/PL に負けじと、こっそりながら人材募集を開始しています。ご興味を持たれた方はぜひご連絡いただけるとありがたいです!(カジュアル面談のエントリーは こちら ) デザインシンカーとしての仕事 1. 取り組むべきことや必要なものの整理(相談対応/案件化) 2.事業や業務の現状リサーチ、課題の抽出(課題設定) 3.テクノロジー/データ活用のアイデア発想のリード(アイデア想起) デザインシンカーとしての仕事への想い 今後に向けて 最後に デザインシンカーとしての仕事 1. 取り組むべきことや必要なものの整理(相談対応/案件化) 日々、住友商事の部署や住友商事グループの事業会社からDXに関する様々な問い合わせを頂いており、その初期的な対応が出番の一つです。 問い合わせの中には、すぐにPoCやソリューションの提案に進める明確な相談もあれば、取り組むべきこと・必要なものがモヤっとしている相談もあります。「努力しているが、これ以上どうしたらよいのかわからない」「打ち手の案はあるが何となくしっくりこない」など。 このようなモヤっとした相談に対し、数回のディスカッションを通じて、モヤを晴らすよう協力させていただいています。たとえば、対象となる事業や業務の内容、ご相談いただいた方の抱えている課題感や今後に向けた想い・気持ちなど、お話を引き出しながら可視化して、一緒に整理しています。 整理結果に応じて、具体的なPoCやソリューションの企画、あるいは更なる課題探索やアイデア発想の機会など、取り組むべきこと・必要なものを見極めて提案しています。ご相談いただいた方の視点に立ち「Insight Edgeとの連携が必要と限らない」とわかれば、よりふさわしい連携先や社内組織での対応の提案もしています。 2.事業や業務の現状リサーチ、課題の抽出(課題設定) 相談対応/案件化の結果「ビジネスの現状課題探索が必要」となった場合には、プロジェクトの企画・提案・実対応の通しで出番になります。 いくつかの、または特定の事業や業務を対象に、担当者へのインタビューやオペレーション中の現場観察による現状リサーチに取り組みます。このようなリサーチは色々な企業で様々な目的のもとに取り組まれていると思います。 技術専門会社としては、現場と同じ目線でより具体にリアルに見聞きし、生々しく理解・共感することが、その後のテクノロジー活用の検討・実装・浸透に効いてくると考えています。実際に改革が起きる・改革を起こす現場にとって何が必要か、どのような形が受け入れるか、どうあると持続するか…の解決すべき課題のヒントを得られるためです。 実際のプロジェクトとして、現在、事業会社内のデータ分析チームと連携し、複数本部門の横断的にリサーチしています。各部署担当者の業務の流れ、想い・課題感をインタビューで引き出して整理した上で、データ活用に着目しながらDX視点での課題の抽出を進めています。 ちなみに、リサーチの事前に基本情報のインプットをお願いするのですが、同じ住友商事グループの一員として協力的な方々ばかりで、とても助かっています(Insight Edgeの恵まれている点ですね…感謝)。 3.テクノロジー/データ活用のアイデア発想のリード(アイデア想起) テクノロジー/データを起点に「この技術を活用するアイデアが出てこない」「データ活用のアイデアはあるが他視点でも発想したい」といった相談があると、これもまた出番になります。 理想を言えば課題設定から取り組みたいところですが、実際にはテクノロジーやデータを起点としたアイデア想起スタートのご要望も多く頂きます。そのような際に「どのようにしたらアイデアを出せるか(+課題も引き出せるか)」と、うんうん頭をひねりながら、既存のフレームワークを組み合わせたり、1からアプローチを考えたりしてお応えしています。 たとえば、最近は「ChatGPTを活用するアイデア出しに困っている」がホットな相談です。そこでChatGPTチームのメンバー( こちら と こちら の執筆者)と連携し、ChatGPTの活用アイデアを発想するためのツールとワークショップを作りました。 具体的にはChatGPTを3系統9種類のキャラクターとして擬人化したカード=ChatGPTキャラクターカードと、それを材料に業務での活用アイデアを発想する3時間のワークショップを企画・設計・作成しました。ありがたいことに、すでに複数社で実施させていただいています。 デザインシンカーとしての仕事への想い ずらっと書いてしまったので、今回ご紹介するのはここまでにします。 今年度からInsight Edgeに加わり、各所からご相談いただいたり、プロジェクトで実対応したりする中で、現在取り組んでいることへのニーズの高さを改めて感じています。 各社ともDX推進に取り組んで数年経ち、前述した「努力しているが、これ以上どうしたらよいのかわからない」などといった状況が増えているのだと捉えています。 そのような中で、ビジネス上の数字やシステムだけでなく、事業推進者や現場担当者などの人が抱える課題・想いの視点で寄り添い、デザイン思考のアプローチを基に試行錯誤して「より良くする」活動を一緒に進めるのが、デザインシンカー飯伏です。 この視点やアプローチが常に正解とは限りませんが、事業会社のみなさまやその先のユーザー・お客さまのうれしさを生み出すと信じています。 今後に向けて 想いの一方、足元では対応リソースの都合から、数々いただくお問い合わせにお待ちいただいたり、残念ながらお断りしていたりしているのが実情です。 多くのご相談にしっかりお応えできるよう、今後は仲間を増やし「テクノロジーとクリエイティブによる価値創出」をデザインの力で担う組織を作っていきたいです。併せて、Insight Edgeの高い技術力を活かした課題設定やアイデア発想の手法・アプローチも開発したいです。 最後に 以上になります。いつものTech Blogとは毛色の違う内容でしたが、いかがでしたでしょうか。DX推進の技術専門会社におけるデザインシンカーの仕事から、「実はこんな仕事をしているメンバーがInsight Edgeにいるよ」をお伝えできていれば幸いです。  ※繰り返しになりますが、人材募集を開始しています。ご興味を持たれた方はぜひご連絡いただけるとありがたいです!気軽に色々とお話させていただけるだけでも本当に嬉しいです!(カジュアル面談のエントリーは こちら ) 実は、現在参加中の Technology Creatives Program (通称テックリ)という、東京工業大学・多摩美術大学・一橋大学で組成する教育プログラムについても記載していました。 が、ブログ編集から「ストーリー・メッセージがぼやける」とコメントをいただき、「たしかに…」と我に返ってゴソッと消しました笑 このテックリは想いや今後に向けてとつながっていますので、次の機会にご紹介できればと思います。それでは、また。
こんにちは! 分析チームの梶原(悠)です。今回はクラス間不均衡問題について議論します。 目次 クラス間不均衡問題とは 問題の定式化にまつわる議論 比較シミュレーションの設定 比較シミュレーションの結果 考察 クラス間不均衡問題とは 実務で扱われる多くの分類問題はクラス間のサイズに偏りがあります。 いくつかのクラスのデータが他のクラスよりも著しく多いとき、分類器は小さなクラスを無視しやすくなります。 これは、クラス間不均衡問題と呼ばれます。 問題の定式化にまつわる議論 クラス間不均衡問題は、「分類問題の構造の複雑さ」「訓練データの大きさ」「クラス間のサイズの偏り」などの条件が絡みあって起きます [1] 。 一方で、Lingらは、学習器が正解率(accuracy)の最大化を目標として訓練される分類タスクの設定自体に問題があると指摘しています [2] 。 そして不均衡問題には、ビジネスコストを考慮した学習で効果的に対処できると主張しています。また、訓練データが小さいことの悪影響は、不均衡とは別の問題であるとする立場をとっています。 本稿では「クラス間不均衡問題への対処にあたって、ビジネスコストを正確に考慮する必要があるか?」という論点を実務寄りの観点から検討してみます。 比較シミュレーションの設定 実務上は、ビジネスインパクトに強く影響する要因が重要で、影響しない要因は無視できます。 今回は、きわめて単純化された仮想企業の損益を、乱数実験によりシミュレートし、クラス間不均衡問題への対処手法のビジネスインパクトを比較してみます。 仮想企業Y社のビジネス設定. 仮想企業Y社は、農作物の種子を売り掛け販売し、一定の支払猶予期間ののち、代金を回収するビジネスを営んでいます。 Y社は、種子のメーカーZ社から単価95円で種子を調達し、顧客企業に単価120円で販売します。 顧客企業Z社は、購入した種子から花を育てて花市場で販売し、得られた売上からY社に代金の支払いを行います。 顧客企業Z社は、花の不作や価格下落により資金繰りができなくなるとY社に代金を支払うことなく倒産します。 仮想企業Y社は、1,000社の顧客企業のそれぞれと月に数回の頻度で取引をします。倒産による貸し倒れの割合は1%です。 仮想企業Y社のビジネスプロセス ダミーデータの生成. 乱数シミュレーションにより、上記設定のY社と顧客企業との契約データを生成しました。 ビジネスインパクトの評価. 契約時点のデータから倒産による貸し倒れを予測するモデルを構築します。 倒産が予測される場合は、業務に介入し、契約を未然に取り止めます。 介入しない場合と比較した貸し倒れや機会損失の影響を考慮すると、種子1個あたりのビジネスコストは以下になります。 実績="継続" 実績="倒産" 予測="継続" 0円 0円 予測="倒産" +(120-95)円 -95円 このコストを累積したものは、介入あり・なしの累積損益の差に一致します。 これを介入のビジネスインパクト(pl_impact)とします。 ビジネスインパクト=累積損益(介入あり)-累積損益(介入なし) 不均衡対応の手法 倒産の予測モデルはすべてlightGBMで作成し、不均衡対応は以下の5手法を比較しました。なお、説明変数は、「種子の個数」「支払い猶予期間」「花の市場価格」の3つのみを用います。 クラス間不均衡に対処しない. (NAIVE) モデルの出力した倒産確率をビジネスインパクトが最大になる閾値で切る. (OPTIMIZED_THRESHOLD) サンプルの重みによるコスト考慮学習で対処する. (COST_SENSITIVE_LEARNING) [4] SMOTE (Synthetic Minority Oversampling Technique)で前処理する. (SMOTE) [5] 損失関数をFocal lossにする. (FOCAL_LOSS) [6] 学習・検証データ分割 学習データは顧客企業500社との契約データからランダムサンプリングし、 検証データは残り500社の顧客企業との契約データの全量とします。 比較シミュレーションの結果 不均衡問題の確認. 不均衡に対処しないモデル(NAIVE)による予測の精度を確認します。今回のデータとモデルはかなり構造が単純ですが、不均衡の悪影響が見られます。また、学習データが小さくなると、不均衡問題の悪影響も強まっています。(各値は、学習における乱数シードを変えた20回の試行の中央値。) 学習サンプルサイズ 倒産確率 (実績値) 倒産確率 (推定値) MCC 正解率 (Accuracy) 再現率 (Recall) Cohen's kappa AUC Brier score 500 個 0.014 0.011 0.620 0.991 0.556 0.604 0.775 0.0088 20,000 個 0.014 0.011 0.715 0.993 0.649 0.712 0.823 0.0060 手法のビジネスインパクトの比較 ビジネスインパクトの比較では、ビジネス構造の知識を持たないSMOTEが、コスト考慮学習よりも良い結果となりました。この事例の不均衡データでは、タスクの定式化にビジネス構造を取り込むことよりも、訓練データが小さいことへの対処の方が、実務上重要であると考えられます。(各値は、学習における乱数シードを変えた20回の試行の中央値。) 各手法のビジネスインパクト 考察 今回はダミーデータが小さすぎて確認できませんでしたが、訓練データのサイズを大きくしていけば、漸近的には、どの手法のビジネスインパクトも同じになると期待されます。すなわち、漸近的にはビジネス構造の知識を持つ手法と持たない手法は同等になるはずです。 一方、訓練データのサイズが小さい非漸近的な領域では、ビジネス構造の知識を持たないSMOTEの方が、コスト考慮学習より大きなビジネスインパクトを持つ現象が確認できました。この事例においては、不均衡問題の本質が、分類タスクの定式化におけるビジネス構造の不在ではなく、マイナークラスの少数データ問題側にあることを示唆すると思われます。 だとすると、コスト考慮学習による方向を追求しても、実務上の意味のある形で不均衡問題への視座を得ることはできないのではないか、という疑いを覚えます。Jamalらはクラスの均衡化をドメイン適応の問題として捉える視点を紹介しています [3] 。クラス不均衡問題の正しい定式化は、あるいは、こうした方向にロバスト性が加えられたようなものかもしれないと想像します。 [1] Japkowicz, N., & Stephen, S. (2002). The class imbalance problem: A systematic study. Intelligent data analysis, 6(5), 429-449. [2] Ling, C. X., & Sheng, V. S. (2008). Cost-sensitive learning and the class imbalance problem. [3] Jamal, M. A., Brown, M., Yang, M. H., Wang, L., & Gong, B. (2020). Rethinking class-balanced methods for long-tailed visual recognition from a domain adaptation perspective. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 7610-7619). [4] Elkan, C. (2001, August). The foundations of cost-sensitive learning. In International joint conference on artificial intelligence (Vol. 17, No. 1, pp. 973-978). Lawrence Erlbaum Associates Ltd [5] Chawla, N. V., Bowyer, K. W., Hall, L. O., & Kegelmeyer, W. P. (2002). SMOTE: synthetic minority over-sampling technique. Journal of artificial intelligence research, 16, 321-357. [6] Lin, T. Y., Goyal, P., Girshick, R., He, K., & Dollár, P. (2017). Focal loss for dense object detection. In Proceedings of the IEEE international conference on computer vision (pp. 2980-2988).
Introduction こんにちは、住友商事のDXを加速する為の技術専門会社である Insight Edge にてデータサイエンティストをしております佐藤優矢です。 今回は、前職のメガベンチャーで担当していたレコメンドエンジンを、その歴史的経緯を踏まえて、協調フィルタリングからニューラルネットを用いたモデルの構築•評価までご紹介したいと思います。 目次 初期のレコメンドエンジン 協調フィルタリングの改善策 協調フィルタリングを超える精度のレコメンドエンジン 独自アルゴリズムの精度評価基盤の構築方法 レコメンドエンジンの精度を測る指標 まとめ 初期のレコメンドエンジン レコメンドエンジン初期は協調フィルタリングによるレコメンドが主な方法でした。 概要としては、ユーザーとアイテムからなるマトリックスを作成し、このユーザーがこのアイテムを購入したなどの情報をマトリックスに埋めていく方法をとります。 よく使われるのは、Alternating Least Square という最適化アルゴリズムであり、このソルバーがpythonで使えるので、それを用いると推定スコア(おすすめ度)が得られます。 長所としては、手軽に精度の良いレコメンドエンジンが作れることで、短所としては、特にマトリックスがスパースな状況では精度が悪いことです。 協調フィルタリングの改善策 歴史的に、協調フィルタリングには様々な改善策が考えられています。 アイテムベクトルによる類似度と混ぜてみる アイテムの説明文をベクトル化するなど アイテムの類似度と、協調フィルタリングによるおすすめ度を混ぜ合わせる形でおすすめ度を算出する。「正係数による距離の重み付け和はまた距離になる」に近い考え方で算出する 私が試したところ、あまり効果はなかった ユーザーベクトルによる類似度と混ぜてみる ユーザーのbioをベクトル化するなど 私が試したところ、これもあまり効果はなかった ちなみに、協調フィルタリングをナイーブにニューラルネット化したモデルでは精度が逆に落ちることがわかっています。 協調フィルタリングを超える精度のレコメンドエンジン 最近になって、Variational Auto-Encoder を用いたモデルなど、協調フィルタリングを超える精度のニューラルネットベースのレコメンドエンジンが開発されてきています。 それらを一気に試すフレームワークにRecBoleがあり、メガベンチャーの人と話したところ、多くのメガベンチャーの会社が使っているようでした。 使い方は、クックパッドでの 紹介記事 に詳しく載っています。 どのモデルを選べば良いかですが、データによって異なるので一概には言えません。ただ、マトリックスがスパースな時に精度が良かったモデルは、グラフニューラルネットを使ったモデルでした。ユーザーに繋がっているアイテムがあり、それに繋がっているユーザーがあるという形に連結していき、同じものを購入したユーザー同士を繋げるようなアルゴリズムになっています。 これらは先のクックパットの結果と大きく異なり、データによって最適な手法は異なることを表しています。 独自アルゴリズムの精度評価基盤の構築方法 最新のアルゴリズムはRecBoleにアップデートされることが多いですが、if-thenなどの独自アルゴリズムを後処理として付け加える(フィルターなど)ことが考えられます。その際は独自に評価基盤を整えます。 特定の精度指標による評価をアルゴリズムとして実装し、その評価指標による精度をもとに、モデルがいままでより良いものかどうかを判断していきます。 指標は Precision@k と Recall@k がよく用いられます。 レコメンドエンジンの精度を測る指標 Precision@k と Recall@k の定義の違いを考えると、異なる部分は分母のうち、正解データに絞るかどうかという点です。 専門書などでも触れられているのをみたことはありませんが、Precision@kだと、取れた正解データ(実際のユーザーの遷移データなどから取ってきます)の量が少ないと小さく見積もられてしまいます。データの量に応じて精度が変わってくるということが起こるわけです。 それに対して、Recall@kではその影響が抑えられています。なので、レコメンドエンジンのモデルの精度を測る時の指標はRecall@kがよく用いられるということかと思います。 まとめ 本稿では、歴史的によく用いられてきた協調フィルタリングから、ニューラルネットによるモデルのツールによる実装、またその最新モデルをカスタムした状況で必要になる評価基盤についてざっと振り返りました。この記事からさらに深い議論が起こると嬉しいです。
目次 導入 動画紹介1 動画紹介2 動画紹介3 動画紹介4 まとめ 導入 こんにちは。InsightEdgeのデータサイエンティストの小柳です。 本記事では前回の記事で紹介したPyMCの使い方を学ぶ一環として、PyMConの紹介をしようと思います。 突然ですが、PyMCに限らずモジュールの使い方を学ぶ際にはいくつか困難がありますよね。 PyMC固有の話とそうでない話を混ぜて挙げてみると 使う人があまり多くない。機械学習ならいざしらず、統計系の人は組織に少なくなりがち。したがって詳しい人が近くにおらずあまり人に聞けない。 公式ドキュメントを読んでもなんだかよくわからなかったり、なぜ例としているコードになったのかがわからないことがある。使用例のコードがない時もある。 教科書がない。あったとしても新しくて効率的な構文のキャッチアップができないことが多い。 これらを克服するためには忍耐強くデモコードを読み解いたり、実装を遡る必要があり大変です。 今回取り上げるベイズ統計用のMCMCサンプラーであるPyMCはコミュニティ作りを意識的にしており、PyMConウェブシリーズという名前でYouTubeに動画投稿をしています。 このシリーズの動画はPyMCの開発者や世界中で活躍するエンジニア、学校の先生たちがケーススタディー的に教えてくれていてすごくありがたいです。最新の機能のキャッチアップにもなります。 今回はこの動画シリーズの紹介をしようと思います。自分のやりたい分析に近いものがあったら見てみると新たな発見があるかもしれません。 PyMConは2020年位に始まったWebシリーズで、2020年に投稿されたものは'PyMCon 2020'とサムネに書いてあリますが、それ以降のやつは年度を切るのをやめてしまったみたいです。 全部紹介しているときりがないので、2023年に投稿されたものをいくつか紹介していきます。 動画紹介1 リンク: https://www.youtube.com/watch?v=0B3xbrGHPx0 テーマ:Automatic Probability スピーカー:Ricardo Vieira (PyMC Labs所属) Automatic Probabilityは自動微分のアナロジーです。 自動微分は偏微分を実行するルーティンであり、チェインルール、指数の微分ルール、積や商の微分ルール等を用いて機械的に微分を計算します。機械学習には必須で、最適化の勾配法やMCMCのNUTS、変分法に使われます。 一方自動確率は確率を自動で計算できるようにしたルーティンのセットで、列挙や、変数の交換などなどを行い確率を計算します。ベイズ統計には必須であり当然PyMCの内部でも使われている技術で、その紹介動画です。 内容はPyMCとそのバックエンドであるPyTensorを使った単純な分布の確率密度の計算や、変数変換時の確率密度計算、打ち切り分布の密度計算、条件付き確率密度の計算、混合分布の確率密度計算等です。 いくつか例示してみます。例えば確率密度の計算は以下の通りです。\ ->のところは出力だと思ってください。 import pymc as pm x = pm.Normal.dist() z = x + 5 def prob(rv, value): return pm.logp(rv, value).exp() prob(x,0.98146052).eval() -> array(0.24645622) prob(z,5.98146052).eval() -> array(0.24645622) このように、zの分布を指定していないのに変数変換ができています。 これは正規分布が平行移動しているだけなので難しくないですがそうでない場合も対応可能です。 例えば1対1変換で import pytensor.tensor as pt x = pm.Normal.dist() z = pt.exp(x) prob(x,-1.18149583).eval() -> array(0.1985122) prob(z,0.30681945).eval() -> array(0.64700006) と計算できます。ヤコビアンをかけてきちんと変数変換できているか確認してみましょう。 import numpy as np prob(z,0.30681945).eval()*np.exp(-1.18149583) -> 0.19851219823788108 うまくいっていますね。 次は多対1変換。実はこれもできます。 x = pm.Normal.dist(0,1) z = pt.abs(x) np.isclos( prob(x,0.3).eval(), (prob(z,0.3) /2).eval() ) -> True 打ち切り変換もできます。 rv = pm.Normal.dist(shape=(3,)) clipped_rv = pt.clip(rv, -1, 2) censored_rv = pm.Censored.dist(rv, lower=-1, upper=2) clipped_value = [-1, 0.5, 2] print( prob(clopped_rv, clipped_value).eval(), prob(censored_rv, clipped_value).eval(), sep="/n" ) ->[0.159 0.352 0.023] ->[0.159 0.352 0.023] 確率変数が2つ以上の時の確率密度や、条件付き確率密度も算出できます。 import pytensor import pytensor.tensor as pt from pymc.logprob.basic import conditional_logp x = pm.Normal.dist() y = pm.Normal.dist() z = x-y def conditional_prob(rvs_to_values: dict, fn=True): logps = conditional_logp(rvs_to_values).values() probs = [logp.exp() for logp in logps] if fn: values = list(rvs_to_values.values()) return pytensor.function(values, probs) else: return probs x_value = pt.scalar('x_value') y_value = pt.scalar('y_value') z_value = pt.scalar('z_value') prob_fn = conditional_prob({x: x_value, y:y_value}) print(prob_fn(x_value=0.5, y_value=0.9)) ->[array(0.35206533), array(0.26608525)] prob_fn = conditional_prob({x: x_value, z:z_value}) print(prob_fn(x_value=0.5, z_value=0.9+0.5)) ->[array(0.35206533), array(0.26608525)] prob_fn = conditional_prob({y: y_value, z:z_value}) print(prob_fn(y_value=0.9, z_value=0.5-0.9)) ->[array(0.26608525), array(0.35206533)] このような感じで、条件付き分布の確率密度が柔軟に計算できます。 直接使う方法はすぐには出てきませんが、PyMCとPyTensorの柔軟性を垣間見られて面白いかと思います。 動画紹介2 リンク: https://www.youtube.com/watch?v=b47wmTdcICE&t=1455s テーマ:Bayesian Causal Modeling スピーカー:Thomas Wiecki (PyMC Labs所属) 最近界隈で盛り上がっている因果推論系がテーマです。前半は実際にベイズを使った因果推論ですが、後半は最近出たPyMCシリーズのマーケティング用ツールPyMC-Marketingを使ったケーススタディで、ベイズとMMMを使って広告効果モデリングをした話です。 動画の詳細欄を見ればわかるのですが、スライドとコードが公開されているので詳細には立ち入らずちょっとした紹介程度にします。 前半はオーソドックスな因果推論の話で、扱う題材も広告効果です。 背景としてある商品の売上を上げるために、Google Adsでネット広告とTVCMを行った時に、本当にGoogle Adsに効果があるのかを知りたいという状況を考えます。 Google Adsを打った時と打っていない時での架空の売上のヒストグラムは以下の通りで、 引用: https://www.pymc-labs.com/blog-posts/causal-analysis-with-pymc-answering-what-if-with-the-new-do-operator/ t検定やベイズ的検定のどれを使ってもバッチリ差があると判定される結果です。 実はこのデータは人工的に作られたもので、その生成過程的には効果なしと出て欲しいものです。 さて、この状況に対し3つの変数とそれらの生成過程を以下のようにを考えます。 C:TVCM費用。連続値を取る。売上に影響を及ぼすし、相乗効果を狙うためGoogle Adsのオンオフにも影響を与える。 Z:Google Adsのオンオフ。0か1の二値を取る。売上に影響を与える。 Y:売上。連続値を取る。 この時のデータ生成過程を以下のように考えます。 このモデルに対し適当な事前分布をおいてやり、 の事後分布を見てやれば良いわけです。 今回の話は本来それで終わらせられるのですが、PyMCは5.8から因果推論系の機能を足したようでdo-operator等が紹介されています。本動画はそれを用いており、モデルの書き方は以下のようになります。ここまでは通常のモデリングと同じです。 with pm.Model() as model: # CからY,Zへの効果の事前分布 beta_c = pm.Normal('beta_c', shape=3) # ZからYへの効果の事前分布 beta_z = pm.Normal('beta_z', shape=2) # Yの観測ノイズ sigma_y = pm.HalfNormal('sigma_y') c = pm.Normal('c', mu=0, sigma=1, observed=tv_spend) z = pm.Bernoulli('z', p=pm.invlogit(beta_z[0] + beta_z[1]*c), observed=search_spend) y_mu = pm.Deterministic('y_mu', beta_c[0] + (beta_c[1] * z) + (beta_c[2] * c)) y = pm.Normal('y', mu=y_mu, sigma=sigma_y, observed=sales) idata = pm.sample() これに対して、do-operatorを用いてzをCから切り離して決定するモデルを定義しさらにそこからYをサンプリングするのは以下のコードになります。 from pymc_experimental.model_transform.conditioning import do model_z0 = do(model, {'z': 0}) model_z1 = do(model, {'z': 1}) with model_z0: idata_z0 = pm.sample_posterior_predictive(idata) with model_z1: idata_z1 = pm.sample_posterior_predictive(idata) そして、平均介入効果の計算も簡単です。 ATE = idata_z1.predictions - idata_z0.predictions このように非常に簡単にATEが計算できるようになっています。実際にこのデータでATEを算出すると、-0.1から0.1のあたりに分布しており、無事効果がないことがわかります。 今回挙げたような線形な関係性であればこれを使う必要はありませんが、非線形な効果が絡んでくるようなモデルを考える場合は便利になりそうです。 後半はMMMをベイズと組み合わせ分析のケーススタディになっています。実はここで紹介されているものはPyMC Labsが分析した事例で、PyMCon2020の動画に詳しい説明があるので興味があればご覧ください。 あまりMMMに詳しくないので、この事例では通常固定値とされるMMMのパラメータ時間とともに推移するようにモデリングしたようで、同様のモデルがPyMC-Marketingで使えるようです。 PyMC-Marketingは名前の通り広告効果分析に特化していて、ROAS(広告の費用対効果らしいです)や広告費を事後的にあの時いくらにしていたら的な分析、CLV(Customer Lifetime Value)推定等マーケティングで知りたくなりそうな分析は揃っているようです。 動画紹介3 リンク: https://www.youtube.com/watch?v=2dGrN8JGd_w テーマ:A Soccer-Factor-Model スピーカー:Maximilian Goebel (Università Bocconi所属) そもそもサッカーファクターモデルとは何でしょうか? 私は聞いたことがありませんでした。 これは金融業界で使われるファクターモデルというものから派生したものだそうです。 ファクターモデルとはリスク資産のリターンのモデルで、資産iのリターンを 1.その資産固有のリターン、2.すべての資産に影響を与える複数の要因の影響の線形結合、3.エラー項の3つの和として分解したもののようです。 サッカーファクターモデルよりは知名度があるようで、ファクターモデルでGoogle検索すると このページ を筆頭にいくつもヒットしますし、Wikipediaにも ページ があります。サッカーの方はヒットしません。 そのファクターモデルを使うことのご利益は2つあります。1つ目はファンドが市況の影響をどのように受けるかが推定できることです。そして2つ目はファンドのパフォーマンスから市況の影響を除去することでマネージャーがどれだけ優秀か=どれだけリターンを高められるかを推定できることです。 この2つ目のご利益を横展開して、チームスポーツにおいて個人の強さを推定してやろうというのが今回の動画のテーマです。 本動画ではサッカーをモチーフに各選手が試合でゴールを決める確率をモデリングします。 この が以下の確率を持つベルヌーイ分布に従うようなモデルを考えます。 これの各パラメータ、特に を見ることで、その選手のパフォーマンスから例えばチーム全体の調子や、チームのアシスト力のような要素を除去し本来のパフォーマンスを測ることができるのだそうです。 当然この手法の応用先は別にサッカーに限らず、チームスポーツだったり営業チームのパフォーマンスだったりと様々な例が考えられそうです。 モデルの良し悪しはROC、LogProb、RMSE等で測定していましたが、これは取り扱うテーマ次第でしょう。 分析に関してはそれほど難しい話ではないので省略します。 アイデアの面白さや応用先の広さ、ファクターをどうするか等を考えると分析者の力量が出そうなトピックでした。 動画紹介4 リンク: https://www.youtube.com/watch?v=ri5sJAdcYHk テーマ: Introduction to Hilbert Space GPs in PyMC スピーカー:Bill Engels(PyMC Labs所属) この動画のテーマはヒルベルト空間ガウス過程です。 私はそこまでガウス過程に詳しくないので正直このまとめはあまり自信がありません。 この動画ではそんなガウス過程初心者のために通常のガウス過程の特徴や長短所そしてその短所の乗り越え方としてのHSGP(Hilbert Space Gauss Process)を解説してくれています。といっても別に理論を詳細に扱うわけではありません。 ガウス過程の長所として 柔軟なモデリング。線形モデルもニューラルネットワークも可能 解釈可能。モデリング次第だが解釈可能なモデルも作れる。 不確実性の定量化。各地点の分散も計算可能 一方圧倒的な短所として 遅い。計算量が 。 が挙げられます。この短所に対して多くのアプローチが提案されており、その1つが今回の動画で紹介されているヒルベルト空間ガウス過程です。もちろんいつでも使えるわけではなく、 PPL(Probabilistic Programming Language:確率的プログラミング言語)を使う必要あり 入力変数次元は最大4 カーネル関数に制限あり 等の制約があります。このような特徴なので他のPPLでも実装できて、StanやBRMS、NumPyroで実施した例があるようです。 さて、PyMCでの実装です。まず通常のガウス過程をPyMCで実施する際には以下のようにします。 (注意:このコードはこのままでは動きません。詳細は https://github.com/bwengals/hsgp/blob/main/cherry_blossoms_hsgp.ipynb を参照ください。動画と同じコードがあります。) with pm.Model(coords=coords) as model: # Set prior on GP hyperparameters eta = pm.Exponential("eta", lam=0.25) ell_params = pm.find_constrained_prior( pm.InverseGamma, lower=10, upper=200, init_guess={"alpha": 2, "beta": 20}, ) ell = pm.InverseGamma("ell", **ell_params) # Specify covariance function and GP cov = eta**2 * pm.gp.cov.Matern52(1, ls=ell) gp = pm.gp.Latent(cov_func=cov) f = gp.prior("f", X=df["year"].values[:, None], dims="year") これをHSGPにするには gp = pm.gp.HSGP(m=[400], c=2.0, cov_func=cov) とするだけで、非常に簡単です。 PyMCを使ってHSGPを行うことにはいくつか長所があります。 インプットデータの次元を変えるのが簡単でpm.gp.HSGPの引数mを変えればよいだけ。 共分散関数を加法的に定義できる。covを2つ書いてHSGPの引数にcov_func=cov1+cov2をとるだけ。 好きなカーネル関数が書きやすい。よく使われる二乗指数カーネル、指数カーネル、Maternカーネルあたりはすでに実装されていますが、変わったものが使うことも可能。 基底と共分散を線形化した形で出力できる。これを使うことで予測時に高速に計算可能。 私があまりガウス過程に詳しくないのでこの機能の素晴らしさや使いやすさが伝えきれなかったかもしれませんが、ガウス過程の理解に役立てば幸いです。 まとめ 本記事ではPyMConの紹介をするための動画をいくつか紹介しました。各動画のテーマはバラエティに富んでいて、この記事を読むような方々なら恐らくどれかにはご興味いただけるかと思いますし、新しい知識を得るのに非常に役立ちそうな予感は感じられると思います。 PyMC界隈、特に日本語コミュニティはまだまだ活発になる余地は十分にあると思います。本記事がその一助になれば幸いです。 Insight EdgeではPyMCに限らず、様々な技術に興味を持っているデータサイエンティス・エンジニアを募集しています! カジュアル面談も行っていますので、興味がある方は以下サイトを御覧ください! recruit.insightedge.jp
こんにちは。Insight EdgeでDeveloperをしている熊田です。 昨今はインフラ環境を構築する際に、Infrastructure as Code(IaC)を検討することが多くなっているかと思います。 これまで私自身はIaCに触れてきませんでしたが、Terraformを使ってWebアプリを構築してみたいとは常々思っていました。 そんなときに、Streamlitフレームワークで開発したアプリをGCP上に構築する機会がありましたので、そのとき使用したコード等を本記事で紹介したいと思います。 目次 Streamlitについて インフラ/セキュリティ要件 構築してみた Terraformで構築する つまずきポイント まとめ/感想 Streamlitについて Streamlitをご存知でない方に向けての説明になりますが、StreamlitはPythonのWebアプリケーションフレームワークです。一番の特徴は、フロントエンドもPythonで書けることかと思います。 加えて、直感的にUI実装できるので学習コストが低く、幅広くチャートライブラリをサポートしています。そのような特徴から、データ分析結果をお手軽にダッシュボードで可視化したいときに便利です。 そういった利点があり、データサイエンティストを多く抱える弊社では、Streamlitを使ったアプリ開発が増えてきています。 以下はStreamlitを使ったサンプルコードです。 ### main.py ### import streamlit as st import pandas as pd import numpy as np # タイトル st.title( "Streamlit Sample" ) # サイドバー with st.sidebar: st.title( "[MENU]" ) option = st.sidebar.radio( "表示形式を選択してください" , ( "折れ線グラフ" , "棒グラフ" , "面グラフ" , "テーブル" )) # ランダムなデータを生成 chart_data = pd.DataFrame(np.random.randn( 20 , 3 ), columns=[ "a" , "b" , "c" ]) # チャートを表示 if option == "折れ線グラフ" : st.line_chart(chart_data) elif option == "棒グラフ" : st.bar_chart(chart_data) elif option == "面グラフ" : st.area_chart(chart_data) elif option == "テーブル" : st.table(chart_data) アプリ画面 インフラ/セキュリティ要件 Webアプリを構築するにあたってのインフラ要件を確認していきます。 Webアプリ開発の前段であるデータ分析にてGCPのサービスが使われており、Webアプリも同クラウドプラットフォーム上で完結させたかったため、今回はGCPで構築します。 StreamlitがWebSocket通信をするため、コンテナ実行環境はWebSocket通信をサポートしているCloud Runを利用することとしました。 セキュリティ要件としては、HTTPS通信、DDos対策、ユーザ認証を行う必要がありました。 加えて、今回のアプリは弊社開発メンバとエンドユーザの双方がアクセスするのですが、以下のような制約がありました。 弊社開発メンバ エンドユーザ Googleアカウントあり Googleアカウントなし VPNなし (固定IPなし) VPNあり (固定IPあり) そのため、弊社開発メンバとエンドユーザの認証方法を分けることにし、弊社開発メンバはSSO認証、 エンドユーザはEmail/PW認証&IP制限を設けて、セキュリティ要件を満たすようにしました。 セキュリティ対策では、GCPの以下サービスを利用して構築します。なお、本記事では各サービス機能の説明はしませんので、ご了承ください。 Load Balancer Cloud Armor Identity-Aware Proxy(IAP) Identity Platform 全体のインフラ構成図は以下の通りです。 構成図 構築してみた 実際に使用したコードをお見せして、つまづいたポイントなどを紹介したいと思います。 ### Dockerfile ### FROM --platform=linux/amd64 python:3.10 RUN pip install streamlit COPY src /app/src WORKDIR /app/src CMD ["streamlit", "run", "main.py"] ### docker-build.sh ### PROJECT_ID = < your-project-id > REGION = < your-region > PREFIX = < your-prefix > REPOSITORY = $REGION -docker.pkg.dev/ $PROJECT_ID / $PREFIX -repo IMAGE =streamlit-image TAG =latest APP = $PREFIX -app docker image build -t $IMAGE : $TAG . docker tag $IMAGE : $TAG $REPOSITORY / $IMAGE : $TAG docker push $REPOSITORY / $IMAGE : $TAG gcloud run deploy $APP \ --project $PROJECT_ID \ --image $REPOSITORY / $IMAGE : $TAG \ --platform managed \ --region $REGION \ --ingress internal-and-cloud-load-balancing \ --allow-unauthenticated \ --memory 2Gi \ --cpu 1 \ --max-instances 1 \ --timeout 900 \ --concurrency 80 \ --port 8501 Cloud Runをデプロイするために、Dockerfileとshellスクリプトを作成しています。 初期構築時はTerraformからshellスクリプトを実行します。初期構築後、Cloud Runをデプロイし直したい時はshellスクリプトのみを実行することを想定しています。 なお、外部ネットワークからCloud Runへ直接アクセスさせず、必ずLoad Balancerを経由させたいので、 gcloud run deploy のオプションに --ingress internal-and-cloud-load-balancing を指定しています。 Terraformで構築する Terraformを使って構築するにあたり、以下作業を行うことを前提としています。  1. Terraform実行用サービスアカウントの作成  2. ドメインの取得、DNSレコードの設定  3. Google Cloud APIsをコンソールから有効化  4. OAuth同意画面をコンソールから設定(こちらの記事の OAuth同意画面を設定する を参考にしました)  5. Identity Platformの設定(こちらの ユーザ作成 まで参照ください) 実行するTerraformコード(ver:1.4.7)は以下の通りです。 ### variables.tf ### variable "project_id" { description = "project id" type = string default = <your-project-id> } variable "project_number" { description = "project number" type = string default = <your-project-number> } variable "default_region" { description = "The default region for resources" default = <your-region> } variable "credentials_file" { description = "The path to the credentials file" default = <your-credentials-file> } variable "prefix" { description = "The prefix to use for all resources" default = "streamlit-app" } variable "oauth_domain" { description = "The domain to use for oauth" default = <your-oauth-domain> } variable "non_oauth_domain" { description = "The domain to use for non-oauth" default = <your-non-oauth-domain> } ### main.tf ### ## Provider ## provider "google" { credentials = file (var.credentials_file) project = var.project_id region = var.default_region } data "google_cloud_run_service" "default" { name = "$ { var.prefix } -app" location = var.default_region depends_on = [ google_artifact_registry_repository.my-repo ] } ## Artifact Registry ## resource "google_artifact_registry_repository" "my-repo" { location = var.default_region repository_id = "$ { var.prefix } -repo" description = "docker repository" format = "DOCKER" provisioner "local-exec" { command = "sh docker-build.sh" } } ## SSL Certificate ## resource "google_compute_managed_ssl_certificate" "default" { name = "$ { var.prefix } -ssl-certificate" managed { domains = [ var.oauth_domain, var.non_oauth_domain, ] } } ## Network Endpoint Group ## resource "google_compute_region_network_endpoint_group" "default" { name = "$ { var.prefix } -neg" region = var.default_region network_endpoint_type = "SERVERLESS" cloud_run { service = data.google_cloud_run_service.default.name } } ## Load Balancer Backend Service ## resource "google_compute_backend_service" "oauth" { name = "$ { var.prefix } -oauth-backend-service" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = google_iap_client.oauth_client.client_id oauth2_client_secret = google_iap_client.oauth_client.secret } depends_on = [ google_iap_client.oauth_client ] } resource "google_compute_backend_service" "non-oauth" { name = "$ { var.prefix } -non-oauth-backend-service" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = google_iap_client.non-oauth_client.client_id oauth2_client_secret = google_iap_client.non-oauth_client.secret } security_policy = google_compute_security_policy.non-oauth.id } ## Load Balancer ## resource "google_compute_url_map" "default" { name = "$ { var.prefix } -url-map" default_service = google_compute_backend_service.oauth.id host_rule { hosts = [ var.non_oauth_domain ] path_matcher = "non-oauth" } path_matcher { name = "non-oauth" default_service = google_compute_backend_service.non-oauth.id } } ## Load Balancer Proxy ## resource "google_compute_target_https_proxy" "default" { name = "$ { var.prefix } -https-proxy" url_map = google_compute_url_map.default.id ssl_certificates = [ google_compute_managed_ssl_certificate.default.id ] } ## Load Balancer Forwarding Rule ## resource "google_compute_global_forwarding_rule" "oauth" { name = "$ { var.prefix } -oauth-forwarding-rule" target = google_compute_target_https_proxy.default.id ip_address = google_compute_global_address.oauth.address port_range = "443-443" } resource "google_compute_global_forwarding_rule" "non-oauth" { name = "$ { var.prefix } -non-oauth-forwarding-rule" target = google_compute_target_https_proxy.default.id ip_address = google_compute_global_address.non-oauth.address port_range = "443-443" } ## IP Address ## resource "google_compute_global_address" "oauth" { name = "$ { var.prefix } -oauth-global-address" } resource "google_compute_global_address" "non-oauth" { name = "$ { var.prefix } -non-oauth-global-address" } ## IAP OAuth Client ## resource "google_iap_client" "oauth_client" { display_name = "IAP OAuth Client" brand = "projects/$ { var.project_number } /brands/$ { var.project_number } " } resource "google_iap_client" "non-oauth_client" { display_name = "IAP Non OAuth Client" brand = "projects/$ { var.project_number } /brands/$ { var.project_number } " } ## IAP IAM ## resource "google_iap_web_backend_service_iam_binding" "default" { web_backend_service = google_compute_backend_service.oauth.name role = "roles/iap.httpsResourceAccessor" members = [ "domain:<your-domain>" , "user:<your-user>" , ] } ## Cloud Armor ## resource "google_compute_security_policy" "non-oauth" { name = "$ { var.prefix } -non-oauth-security-policy" description = "non-oauth security policy" rule { action = "allow" priority = 0 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = [ "<your-ip>" ] } } description = "allow rule" } rule { action = "deny(403)" priority = 2147483647 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = [ "*" ] } } description = "default deny rule" } } 上記Terrafomコードですが、IAPのEmail/PW認証に関する設定が不十分な(いま現在、Terraformがサポートしていない)ため、 ここからはコンソール上で設定していきます。まずは、外部IDの承認をします。 外部ID承認 認証ページ設定 これにより、Email/PWを利用しての認証ページが自動作成されます。アクセスしてみると以下画面になります。 認証ページ なお、システム管理者のみにユーザ追加、削除させたいのでIdentity Platformの設定をコンソールから変更しています。 Identity Platform設定 手っ取り早く外部IDを利用した簡単な認証を作成できるのが良いですね。 Email以外にもMicrosoftアカウントやX(旧Twitter)アカウントなども選択できるので、いつか試してみたいです。 つまずきポイント Terrafom実行でつまずいたところは色々ありましたが、大きなところでいうと2点です。 まず1点目は、OAuth同意画面の設定です。前提④で実行した内容ですが、 当初は google_iap_brand を利用してTerraformで設定しようと考えていました。しかし、applyしようとすると Error 400: Support email is not allowed のエラーが出てしまいました。 調べてみても有効な解決策を見つけられませんでしたが、どうやらOAuth同意画面の設定はTerraformでの管理に向かないようです。 というのも、APIを介してのOAuth同意画面の変更、削除はできないとのことです。変更するにはコンソールから操作する必要があります。 であれば、Terraformで管理することは必須でありませんので、コンソール上から設定することにしました。 ※調査の際は、こちらの記事を参考にさせていただきました。 Terraform で IAP 設定する 2点目は、Terraform構築が無事完了しアクセスしてOAuth認証も成功して、 アプリのトップ画面を開こうとすると The IAP service account is not provisioned のエラーが出たところです。 公式ドキュメント によると、 IAPサービスアカウントを作成しないとCloud Runを起動できず、このエラーになるようです。 gcloud beta services identity create --service=iap.googleapis.com --project=<your-project-id> 上記を実行したところ無事アプリを開くことができました。以前にコンソールでIAPを有効化したときは実施していなかったので戸惑いましたが、 公式ドキュメントをよく読むと、APIを介してIAPを有効化した際は、上記コマンドでIAPサービスアカウントを作成する必要があるようです。 まとめ/感想 以上、Terraformを使ってGCP上にでセキュアなアプリを構築してみたときのお話でした。 認証機能を自前で実装となるとかなり大変なので、こういったIAPやAWS Cogniteのような認証サービスを利用できるのはとてもありがたいですね。 ただ、今回はIAP周りのIaC化がとてもややこしく手間どりました。触ってみて、IAP周りのTerraformおよびGCP APIは両方とも現在も開発中っぽく、ここの部分については無理にIaC化しなくても良さそうと感じましたが、必要作業について自分なりに整理できたのでやってみてよかったです。 社内に限らず、社外でもStreamlitを使ったアプリ開発が今後増えていくと思われますので、この記事が参考になれば幸いです。 参考 Cloud Run+Cloud SQL構成をTerraformで管理する GCP上でセキュアなCloud Runを構築する方法(Terraform事例有)
こんにちは!リードプロジェクトマネージャーの加藤です。つい先日までは残暑に四苦八苦しておりましたが、いつの間にか、金木犀の香りが漂う季節となり、とても過ごしやすい季節になりましたね。皆様いかがお過ごしでしょうか? さて、今回の記事では、タイトルにある通り、Insight EdgeでのPM/PL採用について言及してみたいと思います。背景としてはInsight Edgeが設立4年を経過し、組織拡充に向けてより採用を強化している点も理由の1つにあるのですが、私が入社して丁度1年が経過しようとしており、その1年過ごした実績を踏まえ、PM/PL採用の応募要項では伝えきれないリアルな求める人物像について語りたい点が理由としては大きいです。具体性を持って赤裸々に語れればと思いますので、最後までどうぞお付き合い頂ければ幸いです。 (当社の業務内容、及びそれを踏まえた私が考えるプロジェクトマネジメント業務そのものの要諦については 前回記事 もご参照ください) techblog.insightedge.jp 前回記事のおさらい 先ずは前回記事のおさらいからになりますが、当社業務の主軸となるのはDXを通じた企業価値向上、ないしは新規事業創出、業務オペレーションの変革等々と言った、所謂デジタルを介した企業/業務変革と称しても過言ではありません。その中でPM/PLとしての活動領域の中心となるのは企画/構想段階におけるマネジメント、及び開発段階での開発マネジメントとなる点は前回もお伝え致しました。具体的な業務プロセスについては前回触れておりますので、今回はそれぞれの具体的な情景から実体験も踏まえ、どのような人物像がPM/PLとして相応しいか(我こそは、と言う人に是非応募して欲しい!) を記載したいと思います。 多岐に亘るステークホルダーを主体的に巻き込み、業務推進可能な事 Insight Edgeで対応している各DX案件の何よりの特徴として、関係者が非常に多岐に亘る点が挙げられます。基本的にDX案件では、これまで取り扱っていない未知の事業領域に参入する、ないしはこれまで行ってきた業務を様々な事業戦略に基づき変革を試みる事が要求される為、該当案件のビジネスオーナーとなる事業部(事業会社)だけではなく、協業先の事業部(事業会社)、リスクや法務での関連部署、特定の技術提供を行うパートナー会社、実顧客等々、多種多様なステークホルダーと協業する必要があります。また、住友商事、及び住友商事グループ各社で取り扱っている事業領域の多寡にも起因し、A案件では金属業界、B案件ではエネルギー等、アサイン状況によっては全く異なる業界のDX案件の推進を複数任せられるケースもあります。この様に担当案件によって、全く異なる業界、且つ担当案件に如何に関わらず、非常に大勢の関係者との協業が求められる為、特定期間に集中して関係者を巻き込んで着実に案件推進を成功裡に収める必要があります。PM/PLとしては基本ではありながらも、関係者それぞれの思惑や意向が異なる中でプロジェクト奏功に向けて、各位のそれらを纏め上げる事に対して前向きに取り組める人物が相応しいと感じております。 リーダーシップを発揮し、如何なる困難にも屈せず立ち向かう姿勢 上記の関係者の多寡にも掛かる話ではありますが、関係者全員のデジタル素養、開発経験と言ったITリテラシーやプロジェクトマネジメントの知見も異なる点から、関係者毎に適切なレベル感での情報共有を行いながら、主体的にリードしていく気概も非常に重要です。再掲となりますが、基本的には前例や経験のない事業戦略立案がDX案件には内包されている為、各々が考える期待値の相違やどう進めていけばいいのか、ないしはリテラシーの濃淡によっては、そもそも一方が意図する事が適切に伝わらず、意思統率の時点でそれが困難になるケースも散見されます。更に言えば、前例がないからこそ、企画/構想としてはある程度煮詰まってきた段階で、ビジネス・技術何れかの要因で、これまで検討してきた方針では立ち行かなくなる可能性が生じるノックアウトファクターに直面し、推進が難航するケースもあり得ます。プロジェクトマネジメントの観点で言えば、共通言語としてのプロジェクト憲章やコミュニケーション定義書、用語集やコンティンジェンシープランを事前に策定し関係者合意を図る事が重要でありながらも、こうした状況に陥ったとしても諦めるのではなく、常にリーダーシップを発揮しながら、時にはプロジェクト外の有識者や社内メンバー等、凡ゆるネットワークを駆使し、問題解決に向けて愚直に歩みを進められる・進めていこうと言う姿勢こそが必須と私は考えます。 エンジニア・ユーザと適切にコミュニケーションを取りながら、前向きに前進出来る事 ここからは主に実開発のマネジメント経験を踏まえての記載となります。手前味噌な記載で恐縮ながら、入社して一番想像通りであり、実際に目の当たりにした際に感嘆とした点が、Insight Edgeに所属する各社員の専門性、及び技術力の高さです。実際のエンジニアリング能力もさることながら、技術領域に対する知見の深みは目を見張るものがあり、具体見当が進んでいない柔らかい状態で技術相談をしても、真摯、且つ即座に具体案の提示やそれに伴うリスク等の開示も行なって頂ける為、その点は本当に有難いです。一方で、開発案件を進めていく中では時には技術観点ではより高度な手法を取るべき所を、ビジネス観点ではコストや期間見合いで一旦は見送りたい、または折衷案の採択を行いたいケースが出てくる場合も散見されます。逆も然りで、ビジネス観点で実現したい事と実際に技術的に出来る限界値、または技術あり気ではなく、ビジネス観点での協力や歩み寄りによってのみ実現出来る要件等、現時点の技術で実現出来る範囲と実際にユーザが技術で実現したい点でギャップが生じるケースもあります。PM/PLとしては、各々の背景や意向を具に確認した上で、双方が納得する折衷案の検討や、そもそも両者が共通で掌握出来るレベル感に情報粒度を統一して開示する必要もあります。こうした事から、主体的に適切なコミュニケーションを適宜実施し、単なる情報の橋渡し役に留まらず、常に当事者意識を持って全体の納得感を得ながらプロジェクト推進を行える姿勢も求められます。 QCDを適切に調整・折衝可能である事 Insight Edgeで取り扱う開発案件の多くがPoC案件となります。商用開発前の実証実験の段階になる為、ユーザとしては極力コストを抑えながら、如何に早く出来上がったプロダクトを実際に触りながら検証を行えるか、を求められるケースが多々有ります。一方であまりにもコスト・納期に傾倒し過ぎると品質がおざなりになり、そもそも検証に耐えうるプロダクトにならなかった、または余計な追加コスト・期間が計上されてしまうリスクも出てきてしまう為、QCDの按分を見極め、適切な開発スコープ策定、及び体制とスケジュール検討をする点はPM/PLとして非常に腕の見せ所となる部分です。もう少し踏み込んで言えば、その後の開発リスクを勘案して企画/構想段階より、それらQCDの調整を実現案ベースにリスク事項を含め可視化し、適切に関係者間で合意形成を図れる事が非常に重要で有り、求められる要素だと考えております。 本気で技術の力で世界を”Re-Design”したいと思えるかどうか 最後の記載は具体経験とは異なり、採用過程で言うところの所謂、志望動機や熱意になります。私が入社を志した理由の一つに、様々な業界、産業分野の社会課題を技術の力でDXを通じて一つでも多く解決したいと考えていた点が有ります。この点については、入社前も入社後も変わらず、当社の同僚、及び住友商事/住友商事グループ各社における関係者全ての人も上記飽くなき情熱を持って、業務遂行に励んでいます。個人的には職種毎にある程度のスキル・経験・(会社の求める)パーソナリティの一致、と言うのは重要と考える一方で、一番大事なのは組織全体が追い求める全社ミッションや理念に心から賛同し、同じ志を持てるかどうか、だと考えています。 最後に 如何でしたでしょうか?今回はInsight Edgeで求めるPM/PL人物像の詳細について記載させて頂きました。冒頭でも触れた通り、Insight Edgeは設立4年を経過し、当社が抱えるミッション達成をより高度化・加速化させるべく、組織拡充を担い、採用を強化しております。PM/PL募集についてもこれまで以上に採用強化していく予定につき、上記具体情景を踏まえ、我こそは!と言う方には是非ともご応募頂きたく、何卒よろしくお願い致します!一緒に働ける事を楽しみにしております!!
こんにちは、Insight Edge(以下、IE)のData Scientistの石倉です。私はIEにジョインしてから約1年が経ったところで、振り返るとまだ1年かと思う時もあれば、もう1年かと思うこともある不思議な感覚です。最近は様々なプロジェクトに携わっており、ChatGPT (LLM)活用プロジェクトもその1つです。今回は、IE内のChatGPT活用の一部を紹介しようと思います! 目次 はじめに 従来のテキストマイニング ChatGPTを用いた分析 Streamlit for LLM まとめ 参考文献 はじめに 皆さん既にご存知の通り、ChatGPTをはじめとした大規模言語モデル(以降LLM)が最近注目を集めており、本Tech Blog内でも既にいくつかChatGPT関連の記事を出しています。 総合商社ビジネスにおける生成AI活用 - Insight Edge Tech Blog ChatGPT(LLM)のビジネス現場での活用に向けた技術的な課題と取り組み - Insight Edge Tech Blog Insight Edgeは住友商事グループ各社(国内外約900社ほど)のDXを推進するために設立された企業で、生成AIのビジネス活用もその一部です。本稿では、従来のテキストマイニングで行ってきた分析タスクをChatGPT(LLM)を用いてどのようなことができるか処理プロセスの比較も含めてお話しできればと思います。 従来のテキストマイニング テキストマイニングは、大量のテキストデータの中からパターンやトレンドを識別することを目的としており、以下が主要なプロセスとなります。 データ収集: データはウェブページ、社内文書、SNSの投稿、レビューなど、さまざまなソースから収集され、目的に応じて異なります。 前処理: 収集したテキストデータは、しばしばノイズや不要な情報が混じっているため、それをクリーニングする必要があります。このステップでは、トークン化、ストップワードの除去、ステミングや形態素解析、正規化などの処理が行われます。 特徴量の抽出: テキストから特徴を形成します。例として、TF-IDF(Term Frequency-Inverse Document Frequency)やWord2Vec、BERTなどの深層学習ベースの手法が挙げられます。 データ探索: 統計的な手法やビジュアル化を利用して、データ内のパターンやトレンドを探るためのステップです。 統計的分析: テキストデータの頻度分析や共起分析などを行い、どの単語やフレーズが多く使われているのか、どの単語が他の単語と一緒によく現れるのかを調査します。 可視化: ワードクラウドや共起ネットワークの可視化などを利用して、テキストデータの特性を直感的に理解する。 モデリング: データ探索で得られた知識を基に、特定のタスクを達成するためのモデルを構築するステップです。 テキスト分類: サポートベクトルマシン(SVM)、ランダムフォレスト、深層学習(CNN、RNN、Transformerベースのモデルなど)を用いて、テキストを定義されたカテゴリに分類します。例えば、感情分析やスパムメールの識別などがこのタスクに該当します。 クラスタリング: K-meansや階層的クラスタリング、トピックモデリングなどの手法を用いて、テキストを自然にグループ化します。これは、カテゴリが事前に定義されていない場合や新しいトピックを発見したいときに役立ちます。 関連性分析: コサイン類似度やJaccard係数などのメトリクスを用いて、テキスト間の距離や関連性を測定し、レコメンデーションや変数間の関連性の探索などに使用されます。 評価: 生成されたモデルのパフォーマンスを評価します。モデルの精度や再現性を確認し、必要に応じてモデリング段階に戻って改善を図ります。 前処理/特徴量抽出/モデル作成/評価などのステップが多く、モデル評価後でも、収集されたデータを処理にかける度にビジネス側のユーザーが分析の視点を持って都度出てきた結果を解釈する必要があります。 ChatGPTを用いた分析 上記で触れたテキストマイニングを行なってどの様な活用をしたいかは各企業/ユーザーによって異なりますが、世間を賑わせているChatGPT(LLM)を用いて、従来行われていたテキストマイニングの処理/解釈プロセスを大幅に削減できる可能性があります。本記事ではChatGPTを用いた分類と要約タスクを行なってどの様なことができるのかを紹介しようと思います。 イメージしやすいようにテーマとしてはとある旅館の口コミを分析することを想定します。 ※本例で取り扱うもの(口コミや分類カテゴリ)は全てChatGPTを用いて生成したもので、架空のものです。 分類タスク 以下の例では、口コミを与えられた分類カテゴリに分けています。従来のテキストマイニングでは前処理/特徴量の抽出/関連性分析などのプロセスによって与えられたカテゴリとの類似度を計測し、ある程度の分類は可能です。また、クラスタリングを行うことでテキストデータのグルーピングが可能です。しかし、関連性分析やクラスタリングなどで詳細なラベル付や解釈は難しく試行錯誤の時間を要します。そのような点で、以下の例では、ChatGPTを用いることにより階層構造のカテゴリにも十分に対応できていることが見て取れ、大幅な分析タスクの簡略化が見込めそうです。 分類タスク 分類結果 要約タスク 要約については、過去のTech blogにもある通り、Map ReduceやRefineなどの手法を用いて要約が可能です。しかし、一言で”要約”と言えど、以下の様に注意すべき点は多いと思います。 情報の抽出:本質的な情報を正確に抽出し、不要な情報を省く必要がある。 コンテキストの理解:コンテキストを理解し、本質的な情報を適切に伝える必要があります。コンテキストを見失うと、要約が誤解を招く恐れがあります。 長さの制約:情報を簡潔に伝えるために、一定の長さに制約されます。重要な情報を省略せずに、分かりやすく伝えるのは難しい場合があります。 対象者の認識:要約の対象者やその知識レベル、ニーズに応じて、要約の内容やスタイルを適切に調整する必要があります。 バイアスの排除:要約者の主観やバイアスが要約に影響を与えないように、客観性と公平性を保つ必要があります。 LLM関連論文の出稿が最近多く、幅広く追いかけることが難しくなっている今日この頃ですが、難しい要約タスクについてプロンプトエンジニアリングのみで質の向上を図った比較的実装も容易そうなものを見つけたのでご紹介しようと思います! 論文紹介 ~ From Sparse to Dense: GPT-4 Summarization with Chain of Density Prompting 本稿ではOpenAIの提供するGPT-4を用いて、「Chain of Density(CoD)プロンプト」と呼ぶ方法を提案し、要約の質の向上を図ったものになります。 既にGPT-4で要約タスクを試されたことある方はご存じかと思いますが、シンプルな”以下の文を要約してください。”といったプロンプトを投げるだけでは冗長な要約文が生成されることが多く、情報として十分な場合はあるものの、質的観点からはあまり良しとはされていません。 今回提案されたCoDプロンプトの手法では、最初に冗長な要約を生成したのち、文の長さを増やさずに欠けている重要な要素を反復的に加えていきます。CoDによって生成された要約は、標準的なプロンプトによって生成されたGPT-4の要約と比較して、より抽象的かつ、より情報統合されており、冒頭部分への偏りが少ないという結果が出ています。 Chain of Density prompt しかし、情報過多になりすぎると人間の好みとはかけ離れていくことについて言及されており、明瞭さと情報量のトレードオフの関係を対象者に応じて変更させて調整する必要があると考えられます。本手法のメリットはChain of Density stepにより明瞭さと情報量をコントロールできることを示唆していると思われ、開発側でプロンプトを試行錯誤しながら要約内容を都度確認し、ユーザーの求める要約の状態まで上手く生成できるように都度調整する手間が省けそうです。 本論文ではニュース記事のみを取り扱っているとのことでした。現状のままだと、入力する記事のトークン数が非常に大きくなりやすく、実装する上でボトルネックになる可能性があります。しかし、プロンプトエンジニアリングだけでここまでのことができるとは大変興味深いです。 CoD step Streamlit for LLM 去年にIE社内の勉強会でStreamlitを触り始めてから、色々と遊んでいたのですが、最近は案件でプロトタイプ提供のために使う機会も増えてきました。既にご存知の方も多いと思うので Streamlit の詳細はここでは割愛します。 触り始めてから今に至るまで次々と新しいバージョンがリリースされ、最近のLLMブームの影響を受けてか、 LLM用の機能 も新たに追加されています(まだ少々扱いづらい点はありますが早期に改善されていくと思います。)。 下図はStreamlitを用いた試作です。前節の分析にて一度分類タスクが完了すると、カテゴリ毎の件数などの可視化も容易になります。また、生成AIの得意とする要約などの要素も追加することで、ユーザー側での解釈性の大幅な向上が期待でき、従来のワードクラウドや共起ネットワークによる分析解釈をする必要がなくなりそうです。さらに、Retrieval-augmented generation (RAG)の機能も追加することでユーザー側でチャットボットとして対話をすることも可能になります。 streamlit まとめ 今回はChatGPTの活用例について紹介しましたが、従来のテキストマイニングのプロセスと比較するとかなり短縮でき、ユーザー側でも十分な解釈性の向上が期待できそうです。まさに生成AIブーム真っ只中にいる私たちですが、生成AIのできるタスクを複合的に組み合わせることで様々な便利サービスを作ることができそうです。そのためにも技術のキャッチアップを怠らず、日々注視していきたいと思います!(最近論文の出稿が多すぎてついていけるか不安です 笑) 参考文献 Griffin Adams, Alexander Fabbri, Faisal Ladhak, Eric Lehman, Noémie Elhadad "From Sparse to Dense: GPT-4 Summarization with Chain of Density Prompting"
Introduction こんにちは、データサイエンティストの善之です。 Insight Edgeでは社内のコミュニケーション活性化を目的として定期的にシャッフルランチを開催しています。 企画の全体像については以前ntさんに投稿いただいた 社員同士の距離を縮める!シャッフルランチ会開催レポート をご覧ください。 今回は、ntさんの記事で詳しく触れていなかった「グループ分けを最適化するアルゴリズム」の詳細をご紹介したいと思います。 目次 実現したいことと課題 要件の整理 アルゴリズムの概要 Pythonで実装 実行結果 引き継ぎのためにStreamlitでUIを作成 まとめ 実現したいことと課題 ランチ会の目的は以前の記事にも記載の通り、コミュニケーション活性化のために社員同士の接点を増加させることでした。 したがって、 普段あまり接点がない人どうしをランチ会でできる限り巡り合わせる ことが最大のミッションでした。 さらに、企画の要件から次の制約条件もありました。 各社員が3回参加(月に1回、3ヶ月間) 1グループあたり4-5名 参加者は31名 これらの制約を満たしながら、普段あまり接点がない人どうしの接点を最大化させる必要があったのですが、全て手作業で決めるのは至難の業です。 手作業でもある程度良い組み合わせは見つけられるかもしれませんが、労力がかかるうえに最適解よりも接点がない人どうしの組み合わせ数が少なかったり、 制約を一部満たせなかったりするかもしれません。 そこで、今回はPythonで遺伝的アルゴリズムを使って最適解を探索し、組み合わせを自動で決定するアルゴリズムを開発しました。 要件の整理 あらためて、今回の要件を整理したいと思います。 最大化する目的関数 普段あまり接点のない人どうしが同じグループになる回数 (ただし、重複も1回とカウント。たとえば普段あまり接点のない同一ペアが3回とも同じグループになっても1回とカウント) 制約条件 企画段階から決まっていた要件 各社員が3回参加(月に1回、3ヶ月間) 1グループあたり4-5名 参加者は31名 アルゴリズムを作成する過程で加えた方が良いと判断した制約 マネージャーどうしは同じグループにはならない(普段から接点が非常に多いので) メンバーの中身がほとんど同じチームは2回以上できない(今回はチームのうち3人以上が他の回と同じ組み合わせになるチームを除外) 3回とも同じ人と同じチームにはならない 今回はこれらの条件を満たすアルゴリズムを開発しました。 アルゴリズムの概要 基本的なアイデア まずは基本的なアイデアを紹介します。 今回は1-3月に毎月1回ずつランチ会を開催しましたので、はじめに全メンバー(31名)のリストを計3回分作ります(3×31の2次元配列になります)。 そして、月ごとにメンバー31名の順番をランダムに並べ替えておきます(①) 次にこのリストを左から順に5人→5人→5人→4人→4人→4人→4人に区切って分割します(②) これで月ごとに全31名を5人グループ×3と4人グループ×4に分割できました。 あとは、ランダムな並べ替えパターン全通りの中から制約条件を満たすパターンだけに絞り込み、 その中で普段あまり接点のない人どうしが同じグループになる回数が最大のものを抽出すれば完了になります。 ですが、今回はこのパターン数が膨大なので、計算に時間がかかり過ぎてしまいます。 実際に月ごとにメンバーの順番をランダムに並べ替えるとすると、全パターンが 31!×31!×31!=5.56e+101通りあり、とても計算できないです。 もう少しスマートに、ランダムな並べ替えではなくメンバーの組み合わせパターンのみを考えることで試行数は減らせますが それでも計算量は膨大です(1ヶ月分の組み合わせだけでも9.96e+19通りあります) そこで、今回は 遺伝的アルゴリズム を使って効率的に解を探索することにしました。 遺伝的アルゴリズム 遺伝的アルゴリズムとは、自然進化の原理をもとにした最適化技術であり、選択・交差・突然変異といったステップを通じて解の質を逐次的に向上させるアルゴリズムです。 (解の効率的な探索手法であって、必ずしも最適解が求まるとは限らないことに注意が必要です) アルゴリズムの概要を図にまとめました。 このアルゴリズムの手順は ① 初期個体をランダムにN個生成 ② N個の個体から、目的関数のスコアが良い個体Best2を選択 ③ ②で選定した2つの個体から、交叉と変異を用いて新しい個体をN個生成 ④ ③で生成されたN個の個体を新たに②のインプトットとして、②-③の手順をM回繰り返す(このMは世代数と呼ばれる) となります。 ただし、今回は最適化したい配列が1次元ではなく、3ヶ月分の3次元の配列ですので、このアルゴリズムをそのまま適用できません。 そこで今回は、遺伝的アルゴリズムをカスタマイズして実装しました。 実装した方法を図に示します。 基本的なアイデアは先ほどと同じですが、交叉と変異は月ごとのメンバー全員の配列ごとに行いました。 なお、②の個体の選択では、1-3月の合計スコアから最良の個体を判定しています。 以上が今回実装した遺伝的アルゴリズムになります。 制約条件の反映 さて、ここまでのアイデアを実装すれば - 各社員が3回参加(月に1回、3ヶ月間) - 1グループあたり4-5名 - 参加者は31名 これらの制約を満たしつつ、普段あまり接点のない人どうしが同じグループになる回数をできる限り最大化できそうです。 一方で、以下の制約については別途考える必要があります。 - マネージャーどうしは同じグループにはならない(普段から接点が非常に多いので) - メンバーの中身がほとんど同じチームは2回以上できない(今回はチームのうち3人以上が他の回と同じ組み合わせになるチームを除外) - 3回とも同じ人と同じチームにはならない これらの条件については、満たさないケースでは目的関数の値を大きく減らすことで、 極力これらの制約を満たさないものは選ばれないように工夫しました。 具体的には、制約を満たさないケースがある個体(たとえば、マネージャーどうしが同じグループになるケースがある個体)は、 条件を満たさなかったケースごとに目的関数のスコアを-100するようにしました。 Pythonで実装 では、ここまで説明してきたアルゴリズムをPythonで実装していきます。 1. 事前準備 まず事前準備として、組み合わせをExcelにしてまとめておきます。 図のような表形式で、普段あまり接点がなさそうな人どうしの組み合わせに◯をつけていきました (○はマネージャー陣につけてもらいました)。 なお、画像は本記事のために作成したダミーデータです。○は乱数をもとにつけています。 2. 必要なモジュールのimport ではPythonでの実装に入ります。まず必要なモジュールをimportしておきます。 今回は遺伝的アルゴリズムも自前で実装するため、pandas以外に外部ライブラリは用いません。 import itertools import random import sys from collections import Counter import pandas as pd 3. チーム分け用関数の作成 はじめに、メンバーの配列を5人チームと4人チームに分割する関数を作成します。 基本的なアイデア における「② 各月で左から順に5人グループと4人グループに分割」の部分です。 たとえば、 people = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] の場合は [["A", "B", "C", "D", "E"], ["F", "G", "H", "I"]] のように、引数 people を4人と5人のグループに分割した配列が返却されます。 def divide_teams (people): n = len (people) result = [] # 4人チームの最大数 max_fours = n // 4 # 5人チームと4人チームの組み合わせを検討する valid_division = False for i in range (max_fours + 1 ): if (n - i * 4 ) % 5 == 0 : four_person_teams = i five_person_teams = (n - i * 4 ) // 5 valid_division = True break if not valid_division: raise ValueError (f "この人数({n}人)は4人チームと5人チームに分割できません" ) # 5人チームの分割 for _ in range (five_person_teams): team = people[: 5 ] result.append(team) people = people[ 5 :] # 4人チームの分割 for _ in range (four_person_teams): team = people[: 4 ] result.append(team) people = people[ 4 :] return result 4. 普段あまり接点がない人どうしをカウントする関数の作成 ここでは、生成されたグループのうち、普段あまり接点がない人どうしの組み合わせが何回発生しているかを計算します。 この値を最大化するのが、今回の目的になります。引数 groups は、 divide_teams 関数で生成された配列です。 また、 count_good_combinations の引数 good_pairs は1.事前準備で作成したExcelにおいて○がついていたメンバーの組み合わせを格納したタプルとなっています。 def make_unique_pairs_from_groups (groups): # 全グループでのメンバー同士のユニークな全組み合わせを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの順番を問わずに重複ペアを削除 unique_pairs = list ( set ( tuple ( sorted (pair)) for pair in pairs)) return unique_pairs def count_good_combinations (groups, good_pairs): # 新しい組み合わせ数を計算する関数 # 全体のペアをユニークに取得 unique_pairs = make_unique_pairs_from_groups(groups) # まだ話したことがない組み合わせの数を計算 count = 0 for pair in unique_pairs: if pair in good_pairs: count += 1 return count 5. 制約条件の追加 次に制約条件を追加していきます。 各制約条件を 満たさない ケースをカウントする関数になります。 まずは、マネージャーどうしが同じグループになっている件数をカウントする関数です。 引数 to_divide_members には、マネージャーの名前が入っている想定です。 def count_to_divide_combinations (groups, to_divide_members): # グループごとにペアを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの中身をsort sorted_pairs = list ( tuple ( sorted (pair)) for pair in pairs) # 分割したい人たちのペアを作成 to_divide_pairs = [ tuple ( sorted (pair)) for pair in itertools.combinations(to_divide_members, 2 ) ] # countする count = 0 for sorted_pair in sorted_pairs: if sorted_pair in to_divide_pairs: count += 1 return count 次に、メンバーの中身がほとんど同じチームの件数をカウントする関数です。 def count_overlap_groups_over_threshold (groups, threshold): # メンバーの重複がthreshold以上のグループをカウントする # たとえば、thresholdが3の場合、3人以上が被っているグループがいくつあるかをカウント count = 0 for i in range ( len (groups)): for j in range (i + 1 , len (groups)): # 重複要素を見つける overlap_set = set (groups[i]) & set (groups[j]) # threshold以上の重複がある場合、カウントを増やす if len (overlap_set) >= threshold: count += 1 return count 最後に、3回とも同じ人と同じチームになった件数をカウントする関数です。 def count_multi_pairs_over_threshold (groups, threshold): # グループごとにペアを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの中身をsort sorted_pairs = list ( tuple ( sorted (pair)) for pair in pairs) # 重複をカウント # threshold以上同じグループになったペアをカウント counter = Counter(sorted_pairs) return sum ( 1 for count in counter.values() if count >= threshold) 以上の関数をもとに、これらのケースができる限り起こらないような目的関数を作成していきます。 6. 制約を加えた目的関数の設定 それでは、制約を加えた最終的な目的関数を定義します。遺伝的アルゴリズムでは、このスコアが最大になるように解を探索していきます。 制約条件を満たさない場合は、 penalty_score を乗算して1件につきスコアを-100する設計にしています。 これによって、これらの制約条件をできる限り満たしながら、普段あまり接点がない人どうしの組み合わせを最大化できます。 なお、後ほど引数で渡しますが、今回 multi_pair_threshold は3(3回とも同じ人と同じになることは避ける)、 overlap_threshold も3(3人以上同一メンバーが含まれるチームの生成は避ける)にしています。 def calculate_score ( groups, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, penalty_score= 100 , ): # 普段あまり接点がない人どうしの組み合わせをカウント good_count = count_good_combinations(groups, good_pairs) # 分割したい人たち(マネージャー陣)のペアをカウント to_divide_count = count_to_divide_combinations(groups, to_divide_members) # 一定回数以上同じペアをカウント(例: ランチ会3回中3回とも同じ人と同じ) multi_pair_count = count_multi_pairs_over_threshold( groups, multi_pair_threshold ) # ほとんど同じグループをカウント(例: メンバー4人中3人が、前回と同じ人たち) overlap_groups_count = count_overlap_groups_over_threshold( groups, overlap_threshold ) return ( good_count - to_divide_count * penalty_score - multi_pair_count * penalty_score - overlap_groups_count * penalty_score ) 7. 遺伝的アルゴリズムの実装 これで準備が整いましたので、遺伝的アルゴリズムを実装していきます。 まず、遺伝的アルゴリズムの計算に必要な要素となる関数を定義します。 def initialize_population (size, employee_list, lunch_session_times): # 初期の個体群をランダムに生成。「① 初期個体をランダムにN個生成」に相当。 return [ [ random.sample(employee_list, len (employee_list)) for i in range (lunch_session_times) ] for _ in range (size) ] def evaluate_solution ( solution, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, ): # 個体のスコアを評価する groups = [] for solution_per_session in solution: groups += divide_teams(solution_per_session) return calculate_score( groups, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, ) def select_parents (population, scores): # ベストスコアの2個体を選別 # 「② N個の個体からスコアの良い個体を2個選択」に相当 parents = sorted ( zip (population, scores), key= lambda x: x[ 1 ], reverse= True ) return [parents[i][ 0 ] for i in range ( 2 )] def crossover (parent1, parent2): # 実施月ごとに交差を実施 child1 = [] child2 = [] for session_time in range ( len (parent1)): # 一点で交叉 idx = random.randint( 0 , len (parent1[session_time]) - 1 ) child1.append( parent1[session_time][:idx] + [ x for x in parent2[session_time] if x not in parent1[session_time][:idx] ] ) child2.append( parent2[session_time][:idx] + [ x for x in parent1[session_time] if x not in parent2[session_time][:idx] ] ) return child1, child2 def mutate (child, mutation_rate): # 一定の確率で2点間の位置をスワップ for session_time in range ( len (child)): for i in range ( len (child[session_time])): if random.random() < mutation_rate: swap_idx = random.randint( 0 , len (child[session_time]) - 1 ) child[session_time][i], child[session_time][swap_idx] = ( child[session_time][swap_idx], child[session_time][i], ) return child では、いよいよこれらの関数を用いて遺伝的アルゴリズムを実行しましょう。 # 設定値 population_size = 100 # 世代ごとに生成する個体数。 generations = 1000 # 変異と交叉を繰り返す回数 mutation_rate = 0.01 # 変異が発生する確率 lunch_session_times = 3 # ランチセッションの開催回数 overlap_threshold = 3 # N人以上重複するグループを避ける際のN # 実行 population = initialize_population( population_size, employee_list, lunch_session_times ) best_solution = None best_score = -sys.maxsize for generation in range (generations): scores = [ evaluate_solution( solution, good_pairs, to_divide_members, lunch_session_times, overlap_threshold, ) for solution in population ] if max (scores) > best_score: best_score = max (scores) best_solution = population[scores.index(best_score)] parents = select_parents(population, scores) new_population = [] for i in range (population_size // 2 ): child1, child2 = crossover(parents[ 0 ], parents[ 1 ]) new_population.append(mutate(child1, mutation_rate)) new_population.append(mutate(child2, mutation_rate)) population = new_population この計算によって、制約条件をできる限り満たしながら、普段あまり接点がない人どうしの組み合わせをできる限り最大化した結果が得られました。 それでは、次のセクションで実行結果をご紹介します。 実行結果 ここでは、本記事用のダミーデータでの実行結果を記載します。 マネージャーはAさん~Gさんの7名と想定しています。 実際に分割されたメンバーリスト まずは、遺伝的アルゴリズムで得られたグループを見てみましょう。 マネージャーは 太字 にしています 第1回(1月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Rさん Gさん Wさん Uさん Xさん グループ2 Tさん Kさん Lさん Vさん Eさん グループ3 Bさん Oさん Zさん Sさん Yさん グループ4 cさん eさん Pさん Cさん グループ5 Mさん Fさん Nさん aさん グループ6 Jさん Iさん Qさん Dさん グループ7 bさん dさん Hさん Aさん 第2回(2月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Vさん Kさん Gさん Sさん Hさん グループ2 Aさん Oさん Xさん Jさん Pさん グループ3 aさん Tさん Yさん Cさん Qさん グループ4 eさん bさん Bさん Mさん グループ5 Lさん Rさん Iさん Fさん グループ6 dさん Wさん Uさん Dさん グループ7 Nさん Zさん cさん Eさん 第3回(3月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Nさん Sさん Pさん Gさん Jさん グループ2 Aさん Kさん Zさん Mさん Qさん グループ3 Yさん Dさん Lさん Vさん Rさん グループ4 Uさん Tさん Bさん Hさん グループ5 aさん dさん Cさん eさん グループ6 Xさん Eさん Iさん bさん グループ7 Oさん Fさん cさん Wさん マネージャーが毎回7チームに分散しており、同じようなチームもなく、うまくチーム分けがなされているように見えます。 検算したところ、実際にこのチーム分けは、制約条件を全て満たしていました。 更に全194件の普段あまり接点がない人どうしの組み合わせのうち、118件の組み合わせを実現できています! 最適化の時間推移 では、最適化によってスコアがどのように推移したかを見てみましょう。 横軸は学習ステップ数(世代数)、縦軸は学習スコア(各世代のbest_score)を表しています。 序盤に大きくスコアが改善し(制約条件を満たさないケースが淘汰されています)、200ステップ以降はほとんどスコアに変動がありません。 今回のケースでは、200世代程度でも十分に良い解が導けていたということになります。 ランダム探索との比較 では、今回の学習で遺伝的アルゴリズムを利用せず、ランダムに探索した場合は結果がどうなるかを検証して結果の考察を終えたいと思います。 ここではランダムにメンバーをシャッフルした個体を評価し、最もスコアの高い個体を選択します。 今回は各世代の個体が100で、世代が1,000だったため、100×1,000=100,000通りをランダムに探索してみました。 その結果、遺伝的アルゴリズムが実現できた普段あまり接点のない人どうしの組み合わせは 118件 だったのに対し、ランダムに探索した場合は 77件 となりました (制約条件は全て満たせていました)。 遺伝的アルゴリズムの方が 35% も実現できた組み合わせ数が多く、解の効率的な探索に有効であったことがわかります。 引き継ぎのためにStreamlitでUIを作成 今回は私が実際にPythonコードを動作させて組み合わせを決めましたが、今後もランチ会を実施する際に非エンジニアでもこのアルゴリズムを動かせるように、StreamlitでUIを実装しました。 コードの詳細は省略しますが、簡単にUIをご紹介したいと思います。 まず、普段接点がなさそうな人の組み合わせに◯をつけたExcelファイルをアップロードします。 次に、マネージャーを選択します。選択肢はアップロードしたExcelファイルに含まれる人名が表示されます。 最後にランチ会の開催回数を入力して、計算実行ボタンを押すと最適化計算が開始されます。 計算中は学習が進む様子をビジュアルで確認できるようにしました。 計算が終わると、アルゴリズムが導き出した最適なグループのリストが表形式で表示されます。 (以降のグループは省略) このように簡易なUIにすることで、非エンジニアの方でも気軽にグループ分けを実施できるようになりました。 まとめ かなりボリュームのある記事になりましたが、最後までお読みいただきありがとうございます。 もし同じような課題を抱えている方がいれば、本記事を参考にしていただけると嬉しいです。 また、Insight Edgeはランチ会のグループ分けでも技術的なチャレンジが自由にできる、データサイエンティストの技術を存分に発揮できる環境です。 Insight Edgeに興味を持たれた方は、ぜひ 公式サイト の採用ページからカジュアル面談をお申し込みください!
こんにちは!Insight Edgeコンサルタントの山田です。最近体重増加が著しく、16項目の計測が可能なAnkerの体重計を購入したのですが、毎朝データを取り、日々の変化をグラフ化することでダイエットのモチベーションが維持できています。改めてデータの可視化の重要性を実感しているところです。 さて、この記事では例にもれず生成AIをテーマに、総合商社における生成AI活用についてまとめたいと思います。Insight Edgeは住友商事グループの内製エンジニア組織ですが、グループ全体のCoE組織である 「SC-AI Hub」 の中核を担っており、生成AI関連だけでも現在数十件のプロジェクトを推進しています。住友商事グループの生成AI活用に向けた取り組みを一部紹介しているので、是非ご覧ください。 生成AIのビジネス活用 総合商社が生成AI活用するインパクト ビジネスドメインの多様性 蓄積された豊富な情報 住友商事グループにおける活用事例 社内情報検索 問い合わせチャットボット VoC(Voice of Custmor)分析 経営の意思決定サポート まとめ、今後の展望 生成AIのビジネス活用 生成AI技術のアップデートのスピードはすさまじく、毎日どこかで新たな発表がされているような状況です。同時にビジネスシーンにおいても紙面で生成AIの文字を見ない日はないほど、生成AIの導入・活用が当たり前となっています。 私が現在担当するプロジェクトも、6~7割が生成AI関連となっており、実感としても業界業種問わず活用への機運が急速に上昇している印象があります。 8/4付の日本経済新聞の記事では、調査対象とした約100社のうち7割の企業が具体的な労働時間削減の効果を見込んでおり、社員の生産性向上に寄与すると期待されています。同時に、機密情報漏洩や著作権侵害など懸念される事項もあり、各社導入とルール整備を急速に進めているフェーズです。 生成AIで企業の7割時短 100社調査 - 日本経済新聞 総合商社が生成AI活用するインパクト 業界業種を問わず大きなインパクトをもたらしている生成AIですが、実際に住商グループの生成AI活用をリードする立場としてプロジェクトを進める中で、総合商社は活用ポテンシャルが特に大きい業態の一つと感じています。その理由は大きく以下2点です。 ビジネスドメインの多様性 ご存知の通り総合商社は、資源化学品、エネルギー、輸送機、インフラ、消費財、小売り、不動産、金融、IT、物流、ヘルスケア等、多岐にわたるビジネスを展開していますが、生成AIの実装という観点で見ると、それぞれのビジネスで共通なユースケースと業界特有のユースケースがあります。情報検索や高度なタスクの自動化など、共通ユースケースの実装が比較的先行しますが、業界特有のユースケースも、着眼点次第で活用方法は多岐に渡り、ブレイクスルーをもたらす可能性があります。多様なビジネスドメインにおいて同時並行で活用アイデアを探ることができるのは大きな強みと感じます。 蓄積された豊富な情報 上記に派生するポイントでもありますが、総合商社は多数のステークホルダーやマーケットから日々膨大な情報を収集しており、社内に長年のナレッジとして蓄積されています。これら情報を活用し、また時には業界を横断したデータ活用により、新しいビジネスアイデアを生み出すことができます。また、過去の情報からビジネスの成功要因や失敗要因を把握し、新しいビジネスの成功確度を高めることも期待できます。(後述の意思決定支援ツールの事例にも通じます) 住友商事グループにおける活用事例 特にビジネスドメインの多様性による強みは大きいと感じており、社内情報検索や問い合わせ対応など代表的なユースケースはもちろん、業界特有のペインポイントを生成AIで解決するようなユニークな活用方法があるはずで、そのアイデアやニーズにいち早くアクセスできる点は非常に強みと感じます。 一方で、ビジネス面・技術面の両輪での試行錯誤がきわめて重要となるこの領域において、ビジネスアイデアを高速で形にしながらトライ&エラーができる環境も同時に重要であり、この点はInsight Edgeが内製エンジニア組織として価値を発揮しているポイントでもあります。 以降では、ビジネス現場とInsight Edgeがともに試行錯誤しながら取り組む具体事例をいくつかご紹介します。 生成AIのユースケース分類 社内情報検索 LLM自身が持つ情報だけでなく、企業内のクローズドなデータを組み合わせて、企業や部署専用の情報検索ツールとして活用する方法で、企業利用における最も代表的なユースケースの一つです。RAG(Retrieval Augmented Generation)などが有名な手法です。あらかじめ参照したい社内外の情報をデータベース(ベクトルストア)に用意しておき、ユーザーからの質問に対してベクトルストアの中で類似度の高い文書を検索し、その文書を質問と合わせてプロンプトとして入力することで、既存のLLMだけでは回答できない質問にも精度高く回答できるようにする使い方です。 企業の規模が大きくなるほど、社則や業務ルールも膨大になり、かつ主管部署も異なることも多々あり、求める情報にたどり着くまでに時間を要することも少なくありません。本来は時間をかけるべきでない作業を削減し、より価値のある業務に時間を使えるようにすることは、生成AIを積極的に活用するべき最大の理由でしょう。 Insight Edgeでは社内情報検索ソリューションのβ版をリリースし、住友商事をはじめ、複数のグループ企業に導入するべく目下準備を進めています。 社内情報検索ソリューションの画面イメージ。住友商事の一部メンバーを対象にトライアル導入中。 問い合わせチャットボット 生成AIを活用したFAQチャットボットも、代表的なユースケースです。①の情報検索の仕組みと同様に、業務マニュアルや過去のQ&A履歴、それに対する主管部署の回答などをベクトルストアに用意しておくことで、従来のルールベースのAIチャットボットよりも回答精度の高いFAQシステムができることが期待できます。 このユースケースも、確認すべき業務ルールが多いほど、また業務遂行に際し社内ルールだけでなく、法令や業法など一般的なルール順守が求められる場合にも、一次回答を迅速に得るための強力な武器になりえます。(法令や業法など参照する場合は正確性が求められるため、ハルシネーションの理解には注意が必要で、ユーザーのリテラシーが問われます) 特に人事や総務、法務、情シスなど、コーポレート機能を担うメンバーの問い合わせ対応工数を大きく削減できる可能性があります。 VoC(Voice of Custmor)分析 VoC分析は、顧客の意見やフィードバックを収集、分析することで、顧客満足度向上や製品・サービス改善を目指す手法です。toCビジネスも数多く手掛ける商社では、VoC分析の重要性は非常に高く、ここにも生成AIは活用できます。 日々寄せられる大量のVoCをカテゴリごとに分類・要約することはもちろん、そこから示される顧客感情の示唆、ポジネガ分析、さらには改善に向けたアクション提案などが可能となります。 従来のAI技術でもある程度のVoC分析は可能でしたが、例えば感情分析において業界や企業ごとに判断基準が異なり、VoCの内容に対して、一概にポジティブかネガティブかの判断が難しい場合がありました。一方生成AIでは、プロンプトに判断基準の例を示すことで、業界や企業ごとに重視する観点を反映した分析が可能となります。専門知識や経験が乏しくても、筋のいい分析をするためのサポートツールになりえます。 経営の意思決定サポート 総合商社における生成AI活用の本命の一つが、経営判断における意思決定支援だと思います。総合商社はトレーディングから事業投資/事業経営へとビジネスを拡大してきましたが、社内には投資判断を行うための調査資料や分析レポート、意思決定にあたってのマネジメントの所感・コメント等が残されています。 これらを活用し、新規投資案件など意思決定が必要な場面において、過去に類似の案件がなかったか、その際にはどのような観点やリスクが議論され、結果としてどうなったかなどの情報を生成AIにより抽出・要約することで、意思決定において重要となる情報提供が可能となります。 決断までをAIに委ねる未来は恐らく来ることはないですが、意思決定における強力なサポートアイテムになるのではと期待しています。 Insight Edgeでも住友商事のリスクマネジメント部と連携し、意思決定支援ツール開発のプロジェクトを進めています。 意思決定支援ツールを活用した質問例 これら事例は、下記メディアでも取り上げて頂いているので、よければこちらもご一読ください。 5大商社はChatGPTをどう活用?伊藤忠は「同僚化」、住商は鋼管取引と投資判断に【プロンプト実例紹介】 | コピーですぐに使える!ChatGPT100選 職種別・業種別・部署別 | ダイヤモンド・オンライン まとめ、今後の展望 総合商社は過去100年にわたって、トレーディングから事業投資、事業経営と、常に時流を読み柔軟にその姿を変えてきました。そして現在においては、先端テクノロジーをいかにビジネスに適用し付加価値を創造できるかが強く求められています。 生成AIの登場という、これまでにないエポックメイキングな技術革新の中においても、常に市場を先取りし新しいビジネスを開拓し続ける住友商事グループの内製エンジニア企業として、Insight Edgeもワンチームとして生成AIの社会実装に貢献していきたいと思っています。
こんにちは!Insight Edgeの塩見です。 2023年7月11日(火)~13日(木)の3日間、東京ビッグサイトにて「AI World 2023」が初開催されました。本展示会では、ChatGPTを活用したソリューションが数多く展示されていたため、その内容をご紹介したいと思います。 AI Worldとは? ChatGPTとは? 各社のChatGPTソリューション紹介 株式会社AVILEN 「研修、アイディアソン、ソリューション開発」 株式会社ギブリー 「法人GAI・行政GAI」 パーソルプロセス&テクノロジー株式会社 「TIMO Meeting」 Allganize Japan株式会社「Alli」 まとめ AI Worldとは? AI Worldは、ビジネス変革と業務効率化をテーマとした大型展示会です。この展示会では、DX(デジタルトランスフォーメーション)、業務効率化、チャットボット、機械学習、画像・音声認識、自然言語処理、対話AIを始めとする最新のAIソリューションが一堂に展示されます。商談を求める企業にとっては、課題解決のための貴重なヒントを手に入れることができ、またAI技術の最先端を一目で確認することができる場となっています。  AI World 2023 さらに、この展示会はリアルな場所での展示の他、オンラインでのセミナー視聴や製品資料のダウンロードも可能です。これにより、遠方からの参加者や、展示会場に来ることができない方でも情報収集が容易になっています。 場所としては、東京ビッグサイト(西1・2ホール)を使用して開催され、展示製品数は同時開催された他展示会と合わせると、日本最大級とされる約870製品が出展されました。また、デジタル庁や総務省、東京都などの公的機関からの後援を受けており、その規模と重要性を物語っています。実際、来場者数は計22,295名であり、業界関係者やビジネスパーソンからの高い関心が伺えます。 ChatGPTとは? ChatGPTは、OpenAIによって開発された大規模な言語モデルを活用したAIチャットサービスです。ユーザーとの対話形式で情報提供や問い合わせ応答が可能であり、その高度な自然言語処理能力により、様々なビジネスやサービスに応用されています。ChatGPTは2022年末に公開され、その後、急速に利用者数が増加しました。 2023年2月、本展示会と類似内容の展示会であるDX EXPOが開催され、その場でも多くのAIソリューションが展示されていましたが、ChatGPTの公開から間もない為、ChatGPTに関連したソリューションを展示していた企業は1社程度しかありませんでした。 一方、本展示会では、10を超えるChatGPTソリューションが展示されており、多くの企業がChatGPT公開後に関連サービスの開発を進めていたことがわかります。実際、これらのソリューションは展示会見学者の注目を多く集めており、企業だけでなくユーザの期待値も高いことが想定できます。 各社のChatGPTソリューション紹介 ここからは、AI Worldにて展示されていたChatGPTソリューションを一部ご紹介します。各ソリューションの詳細が記載されたWebページへのURLリンクも併せて載せていますので、こちらもご参照ください。 株式会社AVILEN 「研修、アイディアソン、ソリューション開発」 株式会社AVILENでは、ChatGPTをビジネス活用する上で、各社の課題やフェーズに応じたソリューションを一気通貫で支援するサービスを提供していました。 具体的には、以下の支援サービスがあります。 ChatGPTビジネス研修 ChatGPTアイディアソン ChatGPTソリューション開発(Chat Mee, Chat Mee Pro) ビジネス研修では、「そもそもChatGPTで何ができるのかわからない」というユーザ向けに、ChatGPTの基礎知識をE-ラーニングで学習してもらいながら、ChatGPTを実際に体験するワークを実施します。 アイディアソンでは、「ChatGPTを自社でどのように活用できるかアイディアが欠けている」と感じるユーザ向けに、企画立案から実行まで、コンペ形式でAVILENがサポートします。 ソリューション開発では、「ChatGPT導入・開発に必要なノウハウやリソースが不足している」と感じるユーザ向けに、ChatGPTに精通したデータサイエンティストが課題の明確化から開発、保守まで一気通貫で支援します。 現在、ChatGPTの利用は日本国内でも増加していますが、まだChatGPTを利用したことがないユーザが大多数であることを考えると、ChatGPTの基礎教育・業務課題やユースケースの整理・ソリューション開発といった一連の流れをサポートするこのサービスは、非常に有益だと感じました。 株式会社AVILEN ChatGPT活用支援サービスの詳細はこちら 株式会社ギブリー 「法人GAI・行政GAI」 株式会社ギブリーでは、法人・行政でChatGPTを安全に使用できるGAIサービスを展開しています。ChatGPTを法人・行政内で使用する場合、機密情報が漏洩する可能性がありますが、これらのGAIサービスでは、情報が外部に漏洩するリスクをなくしつつ、ChatGPTを法人・行政内で使用することが可能です。 その他、NGワードの登録、ユーザの利用状況をモニタリングできるダッシュボード機能、実務における様々な利用シーンを想定したプロンプトレシピ機能等、ChatGPTを実務で活用する上での利便性の高い機能を多く揃えています。 セキュアな環境でChatGPTを活用する為のサービスは、直近続々とリリースされている印象がありますが、プロンプトレシピやダッシュボード機能など、ユーザに選んでもらう上では、プラスアルファの機能が重要になってくるのかなと思います。 株式会社ギブリー 法人GAIの詳細はこちら 株式会社ギブリー 行政GAIの詳細はこちら パーソルプロセス&テクノロジー株式会社 「TIMO Meeting」 TIMO Meetingは、会議プロセスのデジタル化を通じて、生産性向上を実現する国内初のミーティングマネジメントツールです。本ツールにもChatGPTが活用されており、「AIアシスタント」と「AIアドバイザリー」の2つの機能があります。 AIアシスタントでは、会議予定に添付された資料を読み込み、資料を元にして、会議の目的・背景・論点などを端的にまとめてくれます。これにより、会議参加者は事前にこの会議がどのようなものかを理解することができ、会議の効率性や効果の改善に繋がります。 AIアドバイザリーでは、会議予定の添付資料に関する質問をチャット形式で投げかけることで、資料内容を要約してもらう等、AIからの様々なアドバイスを受けることができます。 TIMO Meetingでは、自社独自ソリューションにChatGPTを組み込み、他社と差別化を計りつつソリューション価値を向上することが出来ている良い事例かと思います。 パーソルプロセス&テクノロジー株式会社 TIMO Meetingの詳細はこちら Allganize Japan株式会社「Alli」 Allganize Japan株式会社は、AIチャットボットのAlliを展示していました。就業規則などの社内資料を元に、ユーザの質問に対して、ChatGPTの自然な応答を元に回答をしてくれるようです。 Allganize Japan株式会社は、2023年2月に開催されたDX EXPOでも自社ソリューションの展示を行っていましたが、DX EXPOの中で唯一、ChatGPTを活用したソリューションの展示を実施していました。この時の展示スペースはとても小さかったのですが、今回は非常に大きなスペースでAlliの紹介をしていました。 スタッフの方にお話を伺ったところ、前回の展示会以降、ChatGPTに関する問い合わせが大きく増え、今回ブースをさらに拡大することになったようです。ChatGPTのサービスリリースは昨年末でしたので、わずか1~2ヶ月で展示会にソリューションを出展したことになります。この迅速な対応は非常に印象的であり、今回のAI Worldでも、多くのユーザから注目を集めていました。 Allganize Japan株式会社 Alliの詳細はこちら まとめ 近年、ChatGPTの活用範囲は日本国内でも拡大しており、多くの企業が各社独自のビジネスモデルにChatGPTを取り入れて、自社サービスの成長を促進しようとしています。 今回の展示会では、セキュアな環境でChatGPTを使用できるソリューションが数多く展示されていました。実際、このようなソリューションへのニーズは増加しているように感じます。 一方で、2023年8月にはOpenAIから法人向けのChatGPT Enterpriseが発表され、更なる付加価値を持つChatGPTソリューションの提供が開始されました。このような状況下、ChatGPTの単なる機能提供を行うソリューションでは、今後の差別化は難しいと感じられます。 パーソルプロセス&テクノロジー株式会社のTimo Meegingのように、自社独自ツールの価値を向上させるためにChatGPTを活用する、または株式会社AVILENのように、ChatGPTのユースケース整理やコンサルティングサービスを提供する、といった取り組みが重要であると感じました。これは、今回の展示会を通じて得た私の個人的な感想です。 今後もChatGPTの進化と、それを取り入れる企業の斬新なアイディアに注目していきたいと思います。
はじめに  データサイエンティストの五十嵐です。今回は「帰納バイアスと理論研究」というタイトルでお送りします。  機械学習のモデルは、与えられたデータからパターンを学習し、未知のデータに対する予測を行います。この機械学習モデルにおいて、帰納バイアスが存在する場合があり、多くのモデルではそれらの帰納バイアスを上手く利用することにより様々な成果を上げてきています。しかし、近年のLLM (Large Language Model)では、帰納バイアスの弱い手法により大きな成果を上げており、大量のデータセットを利用可能な場合、帰納バイアスが弱くても高い性能を発揮することができることが明らかになってきました。しかし、これらは実験的に得られた知見、すなわち経験則である場合が多く、その理論研究は十分には進展していないのが現状です。そこで、本記事では帰納バイアスについて簡単に紹介した後、今年の6月にarxivに掲載された帰納バイアスの理論研究について踏み込んだ論文、 “Scaling MLPs: A Tale of Inductive Bias“ について紹介しようと思います。  本記事により、帰納バイアスに関する今後の理論研究についての動向を探る一助になれれば嬉しいです。 帰納バイアス(Inductive Bias)とは  帰納バイアスとは、学習モデルや学習アルゴリズムが学習する際の前提や仮定、構造上生じるバイアスのことを指します。これは、モデルがデータから一般的なルールを導き出す際のバイアスや傾向を形成します。  例えば、決定木はデータ階層的に分割するというバイアスをもち、ロジスティック回帰では特徴空間において線形の決定境界を保つため、データが線形に分離可能であるという前提をおいています。また、入力特徴が互いに独立であるという仮定も持っています。CNN(Convolutional Neural Network)は局所結合により構成されており、局所特徴を層ごとに集約するという帰納バイアスがあります。  このように、様々な手法・モデルは多くの帰納バイアスを持ちます。(尚、上記で紹介した学習モデルについて、紹介した以外にも帰納バイアスは存在します。) 帰納バイアスの影響  帰納バイアスは、モデルの一般化能力に大きな影響を与えます。  帰納バイアスにより、得られるメリットには下記のようなものがあります。 計算効率の向上.  帰納バイアスにより、学習すべき仮説空間が制限されるために計算効率が向上します。無限の仮説空間から最適なモデルを見つけ出すのは難しいですが、制約されることにより難易度が低下します。 一般化能力の向上.  適切な帰納バイアスを持つことで、未知のデータに対するモデルの予測性能、すなわち、一般化能力が向上する可能性があります。帰納バイアスが実際のデータ生成プロセスに近い場合、新しいデータに対する予測精度が向上します。 知識の事前組み込み.  特定の分野やタスクに関する専門的な知識を帰納バイアスとしてモデルに組み込むことで、学習の効率や精度を向上させることができます。 解釈可能性の向上.  一部の帰納バイアスが、モデルの解釈性を向上させる効果があります。例えば、線形モデルや決定木などのシンプルなモデルは、その構造上の帰納バイアスによって、結果の解釈が容易になります。 学習に必要なデータ数の削減.  これについて詳細は後述しますが、適切な帰納バイアスを用いることで学習に必要なデータ数が帰納バイアスが弱い場合よりも少なくて済むことが知られています  このように、学習モデルや学習アルゴリズムは帰納バイアスを上手く利用しているのです。 帰納バイアスについて  近年、LLM (Large Langage Model)が話題ですが、これらのLLMはTransformerが用いられています。このTransformerは先述したような仮定を置いていないので、帰納バイアスが弱いです。帰納バイアスが弱いということは、帰納バイアスの恩恵を受けられないということなので、データが大量に存在しない場合は帰納バイアスが強いモデルに比べて高い精度が出しにくい可能性が考えられます。実際、ViT (Vision Transformer)で有名な論文 "An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale (2021)" では、データ量が大規模でない場合は帰納バイアスの強い従来のCNNベースのモデルの方が強く、データ量が大規模な場合は帰納バイアスが弱いViTの方が強い、ということが示されています。  また、Transformerの性能はモデルサイズ(パラメータ数)、データサイズ、計算能力の3変数に依存する冪乗則に従うこと( "Scaling Laws for Neural Language Models (2020)" )も知られており、データ量が大きくなる程、性能が向上することが示唆されています。  これらのことから、学習データが大規模な場合、帰納バイアスが弱い学習モデルを用いた方が良いことが示唆されます。しかし、このことを理論的に解明しようとしている研究はまだあまり多くありません。 本記事で紹介する論文  前置きが長くなりました。本記事で紹介する論文は “Scaling MLPs: A Tale of Inductive Bias“ です。 この論文の概要を下記に記します。 概要  2023年6月23日にarXivに掲載されました。著者はスイス連邦工科大学所属です。  この論文は、これまで経験的に示されてきた、帰納バイアスが小さい場合データセットや計算の能力、モデルサイズが大きい方が性能がよくなるという事象を理論的に解明する足掛かりを作るため、深層学習の最も基本的な構成要素であるMLP (Multi Layer Perceptron)を用いて実験を行っています。  MLPを用いることで数学的に単純にできるため、MLPは深層学習の理論分野では主に使われています。これは、より複雑な構造で観察される現象をシンプルな構造で説明するという発想で、理論分野ではよく用いられています。MLPは帰納バイアスが非常に弱いという点からも理想的です。 実験設定  本論文では、帰納バイアスを理解することが容易であることからvisionタスクで実施しています。 利用したデータセットは下記です。 CIFAR10 CIFAR100 STL10 TinyImageNet ImageNet1k ImageNet21k など事前学習でおなじみのデータセットです。  また、activation function、skip connection、regularization、data augmentation、inverted bottleneckなど、通常用いられる工夫は都度利用しています。 実験結果  上表からデータ数やモデルサイズが大きい方が精度が高くなっていることがわかります。モデルについては、B-L / Wi-mという形式で表現されています。。Bはボトルネック(今回はinverted bottleneck)、Lはネットワークの深さ(ブロック数)、Wi-mはネットワークの幅(各ブロックのノード数)です。  上表はモデルとパラメータ数です。これらのモデルを用いて、CIFAR10とImageNet1kに関して、計算能力毎の性能を評価したグラフが下図です。  上図は、先述したモデルごとのLinear Evaluation Errorを示したグラフです。左図はCIFAR10、右図はImageNet1kについてのTest errorを表しており、横軸は計算能力です。この結果から、MLPに関しても性能はそのスケールや計算能力により劇的に改善することが分かります。 結論  MLPの性能はそのスケールとともに劇的に改善することがわかりました。これは、帰納バイアスがなくてもスケールによりカバーできることを示しています。つまり、大量のデータと十分な計算能力があれば帰納バイアスが重要ではないことを示唆されています。  また、MLPは実際に使われているようなモデルによって示されている経験的な進歩を反映しているとも考えられます。つまり、MLPが深層学習における理論の理解においては重要であることを示しています。 まとめ  本記事では帰納バイアスについての説明と理論研究論文の紹介を行いましたので、全体をまとめます。 データが大規模な場合、帰納バイアスが弱い学習モデルの方が良いことが実験的に示されてきてはいたが、理論研究はあまり進んでいない “Scaling MLPs: A Tale of Inductive Bias“ では、深層学習の基本的な構成要素であるMLPを用いて、visonタスクで同様の傾向が示されるのか研究を行った MLPの性能はそのスケールとともに劇的に改善することが示され、大量のデータと十分な計算能力があれば帰納バイアスは重要でないことを示唆された MLPは実際に使われているようなモデルによって示されている経験的な進歩を反映しているとも考えられる。つまり、MLPが深層学習における理論の理解には重要であることが示唆されている 最後に  最後まで読んでくださりありがとうございました。  色々前置きを書きましたが、人工知能研究の最終達成目標の一つであろう汎用人工知能(AGI)の達成には、間違いなく帰納バイアスが非常に弱いモデルや手法が必要不可欠かと思います。そのためには、理論研究が必須なので、この分野の研究動向は今後も探っていこうと思います。Transformerがより発展していくのか、全く別の手法が開発されるの個人的には非常に楽しみな分野です。
こんにちは。ビジネスイノベーションスペシャリストの森です。 最近コードを書くときは、Github CopilotとGPT-4を使っていますが、実装スピードが10倍(体感)になりました。 微妙な部分を書き換えながら使うので、初心者がコーディングできるようになるのは難しいと思いますが、 コーディングの単純作業部分を全部任せられるのは非常に便利です。 こんなこともあり、Tech業界は最近生成AI一色です。Insight Edgeでも、数多くのChatGPT(LLM)活用プロジェクトに取り組んでいます。 この記事では、本格的なLLMのビジネス現場活用に向け、日々取り組んでいるテーマと、その技術的な課題を紹介します。 また、本記事のタイトル含め、LLMと記載すべきところをChatGPTという単語を使っている箇所があります。 OpenAI社のChatGPTというサービス名称ではなく、概念(一般名詞)としてChatGPTという言葉の使い方をしていることを予め補足します。 目次 はじめに LLMを業務に活用する場合の課題 外部データの取り込み 出力形式の指定 (Function Calling) パイプラインの管理 終わりに はじめに 2023年3月のGPT-4リリース以降、ChatGPTをはじめとした大規模言語モデル(以降LLM)が注目されています。 Insight Edgeとしても、住友商事グループの生成AI活用に向けたグループ横断のワーキンググループである SC-AI HUB に参画し、生成AIのビジネス活用に取り組んでいます。 企業や組織におけるLLMの活用では、いわゆる「セキュアなChatGPT」の取り組みが有名です。多くの企業や自治体が、Microsoft社のAzure OpenAI Serviceで提供されているエンタープライズ用途のChatGPTを用いたセキュアなChatGPTの導入を進めています。 一方、セキュアなChatGPT以外の大規模な本格導入例は、ほとんど出てきていません。 一時期話題となったAutoGPTなどエージェント機能を組み合わせるアイデアや、ChatGPT Pluginで色々なサービスを利用する機能もリリースされていますが、 現状、アーリーアダプターがX(旧:Twitter)で使い方を紹介するくらいで、大規模に実用されるまでは至っていません。 企業活動において最も期待されているユースケースとして、企業内のデータを組み合わせてLLMを使う用途があります。 RAG(Retrieval Augmented Generation) や REALM(Retrieval Augmented Language Model) などの論文で提案された手法を使って実現することが有名ですが、本記事では説明を割愛します。 Microsoft Azureでも、このユースケースを実現するために Add Your Data や、 Cognitive Search のベクトルサーチ対応など、新しい機能を積極的にリリースしています。 しかしながら、企業内の情報検索のユースケースとして、ビジネス現場で活用されている例は、ほとんど出てきていません。 Insight EdgeもこのユースケースのPoCに数多く取り組んでいますが、ビジネス現場での実運用に向けては様々な課題に直面しています。 例えば、元データのPDFがスキャン画像だったり、テキスト化が難しいフローチャートや表に重要な情報が含まれていたりと、アルゴリズム以前に元データの整備が必要なケースがあります。 また、Stuffと呼ばれる最もシンプルな情報検索のパイプラインでは対応できない質問が業務上想定されるユースケースだったりもします。 後述しますが、与えた文書に対する要約と質問の両方を自然に回答することですら、割と難しかったりします。 現在リリースされているほとんどのサービスは、最もシンプルな情報検索のユースケースしか実装されていない状況です。 少し凝ったことをやろうとすると、Langchainやsemantic kernelを使って、スクラッチで実装する必要があります。 LLMを業務に活用する場合の課題 外部データの取り込み ChatGPT PluginsのPopular Pluginsを見てみると、上位3件はPDFの取り込みプラグインです。 エンタープライズ用途同様に、ChatGPTに独自の情報を加えるユースケースは、一般的にも最も注目されているようです。 実際にPDFを取り込むプラグインを使って、課題を説明します。 Insight EdgeでPoCを実施する中で困っていることの1つが、ユーザーは文書に対する質問(情報検索)と要約を意図せずに利用することです。 OpenAIなど、LLMをクラウド型で提供するサービスでは、一度のリクエストで取り込めるトークン数(文字数)の制限があります。そのため、情報検索と要約は同じ処理では実現できません。 情報検索は、文書中の該当部分だけを抽出してChatGPTにプロンプトとして入力します。 要約は、全文を参照する必要がありますが、全文を一定文字数ごとに区切って、順番に入力して処理をしていきます。要約を実現する処理パイプラインとしては、Map Reduce型と、Refine型に大別されます(後述)。 つまり、ユーザーからのリクエストが情報検索か要約かによって、システム側で処理を切り替える必要がでてきます。 ChatGPTの人気プラグインが、この問題にどうやって対処しているのか確認してみましょう。 一番人気のプラグインである「AI PDF」を使って、全体で20ページほどの論文の要約を指示してみます。 ChatGPT Pluginでは、以下のように、APIにどのようなリクエストが飛んだかを確認できます。 実際のリクエストを確認してみると、要約の指示をしたのにも関わらず、APIリクエストには文書の冒頭2ページ分しか入力されていませんでした。 幸い論文だと、冒頭にAbstractがあるため何となく良さそうな結果が返ってきますが、他のデータだとうまくいかなそうです。 実行結果の抜粋が以下になります。 やはり、明らかに冒頭2ページしか参照していません。つまり、このプラグインでは要約の処理は実装されておらず、要約の指示であっても質問回答用の処理しか実行されないことがわかりました。 人気のプラグインであっても、この程度のようです。まだまだこのユースケースの実用にはハードルがあることがわかります。 特に、冒頭2ページしか読み込んでいないのに、全文を処理しましたと出力する仕様となっているのは、ちょっといただけません。 補足ですが、Azureでは、Azure OpenAI Studio Add Your Dataというサービスでこのユースケースをサポートしています。 しかし、現状Azure OpenAI StudioのUI経由でしか利用できないため、システムに組み込みたい場合や、企業内ユーザーへの公開用途には使えません。 また、 Bing Chat Enterprise では、 ユーザーがPDFデータをアップロードする機能が搭載されています。 上述したOpenAIのPlugin同様、まだ実用に課題はあるものの、ビジネス用途で利用できるサービスの一つです。 このBing Chat Enterpriseは、セキュアなChatGPT用途にも使用することができます。 冒頭記載した通り、多くの企業でセキュアなChatGPTの導入が進んでいます。 元々、ベンダーへの委託を含め自社で開発する選択肢しかありませんでしたが、今ではBing Chat Enterpriseのような外部サービスをそのまま利用する選択肢もあります。 住友商事では、セキュアなChatGPTをはじめ、シンプルなユースケースには外部のサービスを利用し、 業務に特化した複雑なユースケースをInsight Edgeを中心に開発する取組み方をしています。 出力形式の指定 (Function Calling) ChatGPTを含むLLMからの回答を使って、別の処理を実施したい場合、プロンプトで回答形式を指定することが一般的です。 しかし、jsonなど決まったフォーマットで出力させようとしても、思ったように出力できないことが多いです。 体感ですが、GPT3.5を使った場合、上手く出力できる確率は良くて5割程度、GPT4だと8~9割です。 パラメータ数の少ないLLMでは、Finetuneしない場合、決まったフォーマットでの出力は絶望的です。 また、決まったフォーマットでの回答ニーズは、通常のチャット用途においても高まっています。 Chain-of-Thought Prompting や、 メタ認知プロンプティング のように、LLMに段階的に推論をさせることで回答の精度を上げる手法が考案されています。 これらの手法は、以下の画像のように、LLMに対して思考の過程をプロンプトで定義します。 このように、段階的な思考の指示することで、回答の精度を向上・担保します。 この手法を使う場合、段階的な推論の全てのログをユーザーに表示してしまうと、回答量が多くなりUXを損ねる場合があります。 多くの場合において、ユーザーに提示するのは段階的な推論の最終段階の結果のみで十分です。 つまり、システムへの組み込み用途だけでなく、LLMからの回答精度を上げる場合にも、出力形式の指定が必要となります。 ChatGPTでは、出力を決まった形式に指定するための機能として、Function Callingというサービスを提供しています。 これだけでも他のLLMではなくChatGPTを使う理由となるくらい、今後重要となる可能性があるサービスだと思います。 パイプラインの管理 セキュアなChatGPT用途では、単にAPIにユーザーからの質問を投げて、APIからの回答をそのままユーザーに提示するだけの処理で実現できます。 しかしながら、社内情報を使った回答の用途を含め、業務的に実用レベルのユースケースを実現しようとすると、 単にAPIに質問を投げて回答を取得するだけの処理では実現できません。 以下に一般的なパイプラインを紹介します。 外部情報を組み合わせたユースケースで最もシンプルなパイプラインは、 Stuff Documents パイプラインです。 仕組みは簡単で、プロンプトと参考情報をセットにしてAPIにリクエストを投げるパイプラインです。 上述の通り、外部情報を取り込める既存サービスのほとんどは、このStuffが実装されています。 一方、これまでのプロジェクトの経験上、Stuffを実装しただけではユーザーに提供できるクォリティの回答が得られません。 セキュアなChatGPT以降の事例が出てこない理由の1つはここにあると思っていて、 いろいろな企業がこのStuffを試し始めているものの、なかなか上手くいってないのではないかと思います。 文書の要約では、別のパイプラインが必要です。 Stuffは、文書の必要な部分を渡す処理ですが、要約では、全文を取り込む必要があります。 しかし、文字数の多い文書の場合は、一度のAPIリクエストで全文を取り込むことができないため、パイプラインで工夫する必要があります。 Map Reduceは、一定量ごとに区切った文書(chunk)をそれぞれ1回要約して、最後に全ての要約結果を集約して2回目の要約を実施する処理です。 1回目の要約部分は並列処理できるので、速度重視の場合はこのパイプラインを選びます。 Azure OpenAI Serviceなど、クラウド提供型のLLMの場合、Rate Limitが厳しいため、 Rate Limitの問題をケアして実行する必要があります。 要約では、Refineというパイプラインも選択肢として考えられます。 Refineは、一定量ごとに区切った文書(chunk)を順番に読み込んでいき、都度要約を繰り返す方式です。 処理が直列なので、Map Reduceと比較すると時間がかかります。 上述しましたが、このように、情報検索と要約は同じパイプラインで実現できません。 利用者の立場からすると、意図せずに情報検索と要約の両方を質問することに加え、要約の後に質問を続けたいという利用ニーズもあります。 この他にも、回答精度を上げようとしたり、利用者の業務ニーズに応じて複雑なパイプラインが必要となります(prompt chaining)。 プロジェクトでは、パイプラインが複雑になるにつれて、開発したエンジニアしか処理内容を把握していない状況になりがちです。 継続的な運用を考えると、パイプラインの設計や管理を実施する必要が出てきます。 Microsoft Azureでは、prompt flowというパイプライン管理ツールを提供しています。 一見ローコードツールのような外見ですが、肝心の処理自体はpythonで記述する必要があり、 Airflowやkedroのように、パイプライン管理の用途を想定して作られていると思います。 まだプレビュー版で、prompt flowも実用的なレベルではありませんが、LLMops領域のサービスは重要となってきそうです。 終わりに 本記事では、本格的なLLMのビジネス現場活用に向け、日々取り組んでいるテーマと、その技術的な課題を紹介しました。 現状では、スクラッチでトライアンドエラーしながら開発できるエンジニアがいないと、LLMを活用してやりたいことを実現するのは難しいです。 LLMを本格的にユーザー企業が活用して成果を出すには、まだまだ技術的なハードルが高いように感じています。 例えばディープラーニングも、色々なツールが徐々に整備され、今では誰でもちょっとした学習ならできるようになりましたが、 LLMの活用もディープラーニングの黎明期と同じような雰囲気を感じます。 現在、アカデミア含め、ものすごいスピードで目下の技術課題が解決されています。 最新技術のキャッチアップを欠かさずに、着実にできることを増やしていきたいと思っています。
MathJax = {tex: {inlineMath: [['$', '$']]}}; Insight Edgeのデータサイエンティストのki_ieです。 今日の記事ではJupyter notebookをはてなブログで公開できるマークダウンに変換する方法を紹介します。 はじめに 数式とコード、さらにコードの実行結果を含む技術的な記事を書くには、Jupyter notebookが便利です。 しかしJupyter notebookは、そのままでははてなブログに公開できる形ではありません。 下書きだけJupyter notebookで行って最後は手作業でmarkdownに移行して仕上げる という方法もありますが、ソースが2つできてしまい記事の修正時が大変です。 notebookファイルだけをソースとして、ブログ記事は変換スクリプト一発生成できる方が望ましいですよね! 本記事では、この変換を実現するスクリプトを紹介します。 変換スクリプト 想定環境 実験は以下の環境で行いました。 Pythonのバージョンあまり依存しないと想定しています。 jupyterのバージョンによっては、正しく動作しない可能性があります。 Python: 3.11 jupyter: nbformat: 5.8.0 notebook: 6.5.4 スクリプト スクリプトは以下の通りです。 # FILENAME: nb_hatena_converter.py """ Usage: python nb_hatena_converter.py YOUR_NOTEBOOK.ipynb OUTPUT.md """ import nbformat, nbconvert import sys import re bs = " \\ " nl = " \n " NO_CONV_TAG = "<!-- NO_NB_HATENA_CONVERSION -->" special_chars = r"\*_`#+-.!{}[]()^" # backslash は文字列末尾には置かない ("がエスケープされるため) math_range_re = re.compile( r"(\${1,2}[^\$]+\${1,2})" ) special_chars_re = re.compile( "" .join([ '[' ] + [bs + ch for ch in special_chars] + [ ']' ]) ) mathjax_html_tag = r''' <script> MathJax = {tex: {inlineMath: [['$', '$']]}}; </script> <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script> ''' # 参考URL: https://o-treetree.hatenablog.com/entry/mathjaxexample def convert_str (s: str , cell, cell_no) -> str : # 数式部分またはそれ以外の部分の文字列 s を入力とし、必要な変換を施します。 full_match = math_range_re.fullmatch(s) res = s # 特に変換がなかった場合、もとの入力を返します。 # セルが NO_CONV_TAG で始まる場合、そのセルは変換しません。 if cell.source.find(NO_CONV_TAG) == 0 : pass # s が数式部分でない場合、変換しません。 elif not full_match: pass # s が数式部分の場合、特殊文字をエスケープします。 elif full_match: res = special_chars_re.sub(repl= lambda match_obj: bs + match_obj.group( 0 ), string=s) if res != s: print (f '{s} --> {res} @ {cell_no=} beginning with {cell.source[:15].replace(nl, bs + "n")}' ) else : raise NotImplementedError () assert isinstance (res, str ) return res def main (path_to_input, path_to_output): # 1. ノートブックを読み込みます。 notebook = nbformat.reads( open (path_to_input).read(), as_version= 4 ) md_exporter = nbconvert.MarkdownExporter() # 2. ノートブックのセルを, convert_str を使って書き換えます。 # (メモリ上での操作なので、ファイルに変更は加わりません) print ( 'Substitutions: ' ) for cell_no, cell in enumerate (notebook.cells): # markdown セルのみ変換処理の対象です。 if cell.cell_type == 'markdown' : # 2.1. cell の内容を、数式部分とそうでない部分に分割します。 # 例: source = "abc $d$ ef $$g$$" # --> # substrs = ["abc ", "$d$", " ef ", "$$g$$"] substrs = math_range_re.split(cell.source) # 2.2. 各部分に対して、 convert_str で必要なエスケープ変換を施します。 new_source = '' .join([convert_str(s, cell, cell_no) for s in substrs]) cell.source = new_source else : pass # 3. nbconvert.MarkdownExporter を使って、markdown に変換します。 # この変換処理は MarkdownExporter のデフォルトの処理を利用します。 converted_md, md_info = md_exporter.from_notebook_node(notebook) # 4. MathJax を利用できるように、ファイルの最初に HTML タグを追加します result_md = ' \n ' .join([mathjax_html_tag, converted_md]) # 5. 出力します with open (path_to_output, 'w' ) as f: f.write(result_md) if __name__ == '__main__' : main(sys.argv[ 1 ], sys.argv[ 2 ]) 使い方 1. jupyter notebookの準備 notebookに特別な工夫は不要です。数式を含むnotebookを用意しましょう。数式は、インラインでも、別行立て数式でも大丈夫です。 ここでは、以下の数式がどのように変換されるか確認しましょう $n = p \cdot q$ $$f(x)=\frac{{\sum_{i \in I} |x_i|}^2}{\sum_{i \in I} x_i^2}$$ 2. 変換 以下の方法でプログラムを起動し、変換結果のmarkdownファイルを得ます。 python nb_hatena_converter.py YOUR_NOTEBOOK.ipynb OUTPUT.md 3. 変換結果の確認 先程の数式は以下のように変換されます: $n = p \cdot q$ $$f(x)=\frac{{\sum_{i \in I} |x_i|}^2}{\sum_{i \in I} x_i^2}$$ (この記事自体も nb_hatena_converter.py で生成されたものです。上記の数式がはてなブログで正しく表示されているなら、それはこのスクリプトが正しく動作した結果です!) 4. 仕様 仕様概要は次の通りです。詳細は、プログラムとコメントを確認してください。 変換対象 markdownセルだけが変換対象です。codeやrawセルは変換されません。 markdownセルで数式部分と判定された部分に対して、変換処理が施されます。 セル単位のスキップ <!-- NO_NB_HATENA_CONVERSION --> で始まるmarkdownセルは変換を受けません。 このタグが含まれていても、セルの最初でなければ変換の対象となります。 数式部分の判別 $ または $$ で囲まれた部分を数式部分だと認識しています(詳細: math_range_re )。大雑把な判別なので、必要に応じて適宜改造してください。 変換処理の概要 はてな上での表示とjupyter notebook上での表示のずれは特殊記号の処理の優先順位が異なるため発生します。 このスクリプトは数式部分内に存在するmarkdown上の特殊記号をエスケープすることで、特殊記号が数式部分の開始/終了記号( $ , $$ )よりも優先されて処理されることを防いでいます。 おわりに このスクリプトで数式のあるブログ執筆のハードルが少しでも下がったら幸いです👍
こんにちは!小林と申します。この間、九州縦断旅をしたのですがどこに行っても温泉があって毎日のように温泉に入れて幸せでした。 つい先日、社内のAWS・GCPのインフラコストの削減施策を担当いたしました。この経験を通じて、コストがどこで発生し、どのようにそれを削減できるかについて深く理解する機会を得ました。 そこで、これらの知見を共有し皆さんが同様の課題を抱えてしまった際の参考になることを目指して、今回ブログで紹介させていただきます。 目次 インフラコスト削減施策の目的 調査 AWS のコスト GCP のコスト 実施項目 インスタンス自動起動停止ツール インスタンスごとの利用料金を監視・通知 AWS の場合 GCP の場合 EBS 棚卸し まとめ 結果 振り返り reference インフラコスト削減施策の目的 当社では、開発チームがAWSやGCPについて自由に検証できるよう、検証用アカウントを提供しています。しかし、このような自由度の高い環境では、不要なリソースが放置され、それが過剰なコストとなってしまうという問題がありました。 特に、高価なインスタンスが利用されていないのにも関わらず、起動し続けられていることがしばしばありました。 これらの問題に対応するため、以下の2つの施策を当初の目標として設定し、コスト削減に取り組みました 利用金額が一定の閾値を超えた場合に、その情報を通知する仕組みを導入し、過剰なリソースの使用を抑制する。 週末や使用が予想されない時間帯に、リソースを自動的に停止する機能を追加し、無駄なコストを削減する。 以上の施策により、リソースの最適化とコスト削減を達成することを目指しました。 調査 実際の取り組みへの着手前に、現状のコストが何に発生しているのかを詳細に調査し、インスタンス以外にもコスト削減の余地がないか探ります。AWSとGCPの両方で調査を実施しました。 AWS のコスト CostExplorerを利用して施策前のコスト分布を確認しました。たとえば、2023年3月時点の当社のコスト内訳を見ると、EC2インスタンス関連のコスト(EC2インスタンス、EC2その他)が月額コストの50%を占めていることが分かります。したがって、まずはEC2インスタンスに対して今回の施策を適用するのが有効であると推察できます。さらに、「EC2その他」という項目も非常に高額であることが明らかになりました。この項目には、EIP(AWS Eastic IPアドレス)やEBS(Amazon EBS)が含まれます。 EIPやEBSはインスタンスが停止している場合でも課金が発生し続ける特性を持っています。それゆえ、使用されていないEIPやEBSが存在する可能性も考慮に入れるべきです。この結果を踏まえ、AWSについては、インスタンスだけでなくEIP・EBSについても棚卸し等の施策が必要となりそうです。 図1 GCP のコスト 続いて、GCPのコスト状況についても確認します。 GCPについても全体のコストのうち半分ほどがCompute Engine(図2のグラフ青色部分)によって占められています。 AWSと同様にインスタンスに対する削減施策を実施する必要がありそうです。 図2 実施項目 調査結果から、当初の目的であったインスタンスのコスト削減施策が有効であると考えられました。さらに、AWSではEBSの棚卸しも行うことを決定しました。したがって、今回の取り組みは以下の3点となります。 インスタンス自動起動停止の仕組み作り 土日にインスタンスを自動で起動・停止できるようにし、インスタンスの管理を容易にする。 インスタンスごとの利用料金を監視・通知 インスタンスごとの利用料金を算出する仕組みを整え、規定値を超えた場合に通知するシステムを導入する。 EBS(ボリューム)の棚卸しを実施 あまり使用されていないインスタンスにアタッチされているEBSボリュームを確認し、必要ないものは削除する。 次に、それぞれの取り組みについて詳細を説明します。 インスタンス自動起動停止ツール まず、インスタンスを自動的に起動・停止する手段について考えてみましょう。 AWSのインスタンス自動起動停止には一般的に、Amazon EventBridge + Lambda、EventBridge Scheduler、またはInstance Schedulerのようなアーキテクチャが用いられます。 GCPではデフォルトでインスタンススケジューラを設定できます。 しかし、これらのアーキテクチャでは、新たにインスタンスを作成するごとにスケジュールを設定する必要があり、これが開発者の負担となってしまいます。理想的には、ユーザがインスタンスを作成した際に、自動的に設定された自動起動・停止される仕組みが欲しいです。 そこで今回は、スプレッドシートとGASを用いてインスタンスの自動起動・自動停止するツールを開発し、それを利用することにしました。スプレッドシートを利用する利点としては、ユーザが使い慣れていることと、一覧性があり、管理しやすいという点が挙げられます。 また、今回作成するツールには自動起動・停止するだけでなく、より管理しやすくするため、以下のような機能を追加しました。 インスタンスの利用者名を記載する 誰が責任を持ってインスタンスを利用しているのかを明確にする インスタンスの起動時刻を記載する 起動状態で長期間放置されていないかをチェックする インスタンスタイプを表示する 長期間稼働しているインスタンスが高額なものでないかを確認する ツールの使い方・仕様は、以下の通りです。 定期的に一覧を自動更新する 常に最新のインスタンス状況が確認できるようにする ユーザが手動でインスタンスリストを更新できるようにする 追加したばかりのインスタンスについて、手動で更新し、すぐに設定できるようにする 自動起動・自動停止欄にチェックをつけることでそれぞれ定時に自動で実施される インスタンス作成時のデフォルトでは自動停止のみチェックされる インスタンスの停止忘れによる無駄な課金を防ぐ 以下は作成したツールのイメージです。 図3 最後に、ツールの作成手順を簡単に説明します。 インスタンスを管理するためのIAM/サービスアカウントを作成する ただし、アカウントは誰でも利用できる状態になるので、必要最低限の権限のみを付与します。 スプレッドシートとGASを作成する スプレッドシートは図を参照ください。今回、 GASの作成には、以前当社のブログでも紹介されたClaspを利用しました。(参考: リンク ) トリガーを設定する 今回GASを起動するトリガーとしては、自動起動(定時にインスタンスを起動する)・自動停止(定時にインスタンスを停止する)・更新(インスタンス一覧を更新する)を用意しました。 インスタンスごとの利用料金を監視・通知 インスタンスごとの利用料金を監視・通知するためにどのような技術を利用したかをAWS・GCPそれぞれ紹介します。 AWS の場合 AWSにおいて、インスタンスごとの利用料金を把握するためには、コスト配分タグの利用が効果的です。設定したタグを利用して、以下のような内訳を確認可能です。ただし、コスト配分タグを利用する際は以下の点に注意してください。 利用できるのはOrganizationsの管理アカウントのみ タグの有効化には1日程度かかる場合がある 月の途中でタグを付与した場合、その月の残り期間はタグ未付与とみなされる。 次に、コストの通知について説明します。今回は、AWS LambdaとSlackを組み合わせて通知機能を実現しました。具体的な開発方法については、以下の参考記事などをご覧ください。 https://dev.classmethod.jp/articles/notify-slack-aws-billing/ なお、今回設定した通知の閾値は当社での利用額に対して、少し大きめの額で設定しました。あくまで異常な課金の発生を発見するために設定した値となります。 GCP の場合 GCPはAWSと異なり、コスト分配タグのような機能がないため、Compute Engineに設定できるラベルを用いることにしました。ラベル付は、インスタンス自動起動停止ツールのGAS内で自動に行いました。 ラベル付された結果は、課金のレポートから確認ができます。 ラベル付には こちら のAPIを利用しました。 EBS 棚卸し EBS(Elastic Block Store)ボリュームとは、EC2インスタンスに接続され、データを永続的に保存するためのブロックレベルのストレージデバイスです。EC2インスタンスから物理的に分離されているため、インスタンスを停止または終了してもデータは保持されます。EBSボリュームは確保した容量に対して料金が発生します。そのため、長期間使用されていないインスタンスに接続されているEBSボリュームでも、料金は継続的に発生します。 このような背景から、以下の条件を満たすEBSボリュームについては棚卸しを実施しました。 利用されてから1年以上経過していること 容量が100GiB以上であること 具体的な棚卸しの手順は次の通りです。 対象となるEBSボリュームがアタッチされているインスタンスの利用者に連絡をする EBSボリュームのスナップショットをとる EBSボリュームをデタッチする もしくは インスタンスを削除する EBSボリュームを削除する なお、スナップショットを作成しないとデータは完全に復元できなくなります。したがって、データの保持が必要な場合はスナップショットの作成を忘れないようにしてください。 まとめ 結果 EC2インスタンスの利用料金については、月ごとの使用状況により大きく変動するため、7月度は高額になってしまいましたが、5月と6月では施策導入前よりも大幅に低く抑えることができました。 また、特にEC2その他のコストについて、EBSの棚卸しとともにEIP(Elastic IP)についても棚卸しを実施した結果、当初の半額程度まで削減できました。 GCPについては、Compute Engineの使用料が一定水準を保っており、大幅なコスト増は抑えられていると言えます。 特に注目すべきは、EC2のその他のコストです。これは長期間放置されているとかなり高額になるため、今まで特に組織的に棚卸しをしていなかった分削減効果が大きくなりました。 振り返り この施策を通じて、ツール自体の効果以上に、それぞれのリソースがどれほどのコストを生じているかを理解し、自分自身でコストに対する意識を持つことができました。 さらに、今回の試作を通して運用面での課題も明らかになりました。 定期的な棚卸しの必要性 年単位で放置されているようなリソースが存在し、その所在が分からないものもあります。そのため、棚卸しの手順を繰り返し行えるように準備する必要があります。 アカウントの管理方法の課題 検証用アカウントを共用する方式では、誰がどのリソースを使用すればよいのか判断が難しいです。これに対する対策として、タグ付けのルール化などが考えられますが、検証用アカウントに多くのルールを設けるとメンバーへの負担が大きくなります。ユーザー個人ごとに検証用アカウントを発行する手段もありますが、管理が複雑化するなどの課題もあります。 また、自身でコスト削減を試みるだけでなく、Trusted Advisorのようなツールを利用する手段も検討できます。 この施策を通じて得られた新たな学びは多いです。今後は、より効率的なコスト管理を実現するために、システムを整備していきたいと思います。 reference 社内開発用 EC2 インスタンス料金を節約したい cron で時間指定して ec2 インスタンスを自動起動・停止する AWS のコストをサクッと削減 ~ EBS 編 ~ AWS コスト配分タグを使ってみよう AWS コスト配分タグの運用に入門してみた AWS のコスト配分タグを正しく理解する Lambda x Amazon SNS で、AWS の請求額を毎日メールで通知する AWS Trusted Advisor で無駄なコストを見つける
目次 はじめに ブランチ戦略 Git Flow GitLab Flow GitHub Flow 開発チームのブランチ戦略 GitHub Flowの採用理由 運用の基本ルール 実際にGitHub Flow運用してみて 良かったこと 悪かったこと まとめ はじめに Insight EdgeのLead Engineerの三澤です。 Insight Edgeでは開発プロジェクトに応じてその時々でチームを編成し、プロジェクトの内容に合わせてチーム単位で技術やアーキテクチャや開発ルールの選定・決定をしています。 今回は直近の開発で運用してきた開発ルール(ブランチ戦略)の1つGitHub Flowについて、実際の運用方法や良かった点・悪かった点をご紹介したいと思います。 本記事ではGitとブランチを既知のものとして進めることにご了承ください。 ブランチ戦略 GitHub Flowの話へいきなり入る前に、まずはなぜブランチ戦略を考える必要があるのかを考えてみます。 皆さんもGitを用いて複数人で開発を進めることがあるかと思いますが、その際には各自がブランチを作成し開発を進めるのが一般的です。しかしこの時、もしブランチ戦略を決めずに各開発者が勝手にブランチを作成してしまうとどうなるでしょうか?各ブランチの意味が不明瞭になるだけでなく、ルールを決めずに開発を進めますので各ブランチのコードをマージするのも一苦労になります。さらに、ブランチが乱立することになりますので、どのブランチが正しく動くソースコードなのかが分からなくなるといった問題を引き起こす可能性があります。ブランチ戦略は、そのような問題に対してルールを定めそれに則って開発を進めることによって解決を図るものです。 ここでは3つの代表的なブランチ戦略を簡単にご紹介します。 Git Flow Git Flow は、Vincent Driessenが2010年に提案したブランチモデルです。masterを安定ブランチとして、そこから分岐したdevelopブランチを開発用のブランチとして開発を進めます。開発者はdevelopブランチから機能毎にfeatureブランチを作成して開発を進め、developブランチへマージを繰り返すというのが基本動作です。その後、developブランチで一定の機能開発やバグフィックスが完了するとreleaseブランチを作成します。このreleaseブランチではリリースの準備としてコードの整理やテストなどが行われます。ここで問題がないことを確認できたらいよいよreleaseブランチの内容をmasterブランチへマージされ本番環境へリリースされます。このようにブランチを用途によって適宜使い分けながらタスクやリリースの管理や安定性の確保しますので、各ブランチの役割が明確になるというメリットがあります。一方で、パッと見てわかる通りブランチの種類が多く管理が複雑になるというデメリットがあります。 GitLab Flow GitLab Flow は、GitLabが提唱するブランチ戦略です。Productionブランチモデル、Environmentブランチモデル、Releaseブランチモデルといったいくつかのパターンがあるのですが、ここではEnvironmentブランチモデルを紹介します。GitLab Flowでの機能開発はGit Flowと異なりdevelopではなくmasterブランチから分岐したfeatureブランチで行います。featureブランチでの開発が完了すると、その内容をmasterへマージし検証環境でテストするという作業を繰り返します。その後、ある程度開発が進んだタイミングでリリース準備を行います。リリース準備はmasterブランチから分岐したpre-productionブランチで行い、問題がなければ本番環境へリリースされます。このリリースしたコードはproductionブランチにマージされ、終了するという流れです。Git Flowほどの厳密なリリース管理が不要になるというメリットがありますが、それでもやはりまだ複雑というデメリットがあります。 GitHub Flow GitHub Flow は、GitHubが提唱するGitLab Flowよりもさらにシンプルなブランチ戦略です。開発はmainブランチから派生したfeatureブランチで行います。featureブランチで開発が完了するとmainブランチにマージされ、このマージされた内容が本番環境へリリースされます。このmainブランチには常にデプロイ可能な安定したソースコードが格納されていることを前提としているのが特徴です。ブランチが2本しか存在しないのでとても理解しやすく、mainブランチにマージしたことを基本としてリリースされるので、開発した内容を短いサイクルでリリースにつなげられることがメリットになります。一方でmainブランチの内容が常にリリースとなることから、検証環境へのリリースやリリースバージョン管理が出来ないというデメリットがあります。なお、我々のチームではGitHub Flowをベースに一部改変することでそのデメリットを克服しています。その内容については後ほど紹介します。 開発チームのブランチ戦略 GitHub Flowの採用理由 タイトルにも書いている通り、我々のチームではGitHub Flowを採用しています。一方で、私の開発経験としてはこれまでGit Flowのブランチ戦略を採用することが多かったです。そのためGit Flowで進めるという選択肢もありました。では、なぜ我々のチームがGitHub Flowを採用したかというと、Insight Edgeでは小回りの効く比較的小規模なチームで素早く開発をすることが多かったから、が理由になります。Git Flowのような厳密な管理をするブランチ戦略は、先の説明の通りリリースまでの工程や確認が多くなります。そのため、こまめに機能をリリースしながらシステムをアップデートしていくというアジャイル的な我々の開発スタイルにミートしませんでした。GitHub Flowのようにシンプルでこまめにリリースを重ねていける開発スタイルが小規模チームは適していると考えました。 一方で先にも触れた通り、GitHub Flowはmainブランチの内容が常にリリースとなることから、検証環境へのリリースやリリースバージョン管理が出来ないというデメリットがあります。そのために我々はGitHub Flowに一部改変を加えています。ここでもしかすると、リリース管理を行うGitLab Flowでも良いのではないかという疑問が湧いてくるかもしれません。しかし、こちらについては管理するブランチ数はなるべく減らしたいこと、リリース管理自体はproductionブランチがなくてもタグで代用できることから採用を見送りました。 運用の基本ルール Insight EdgeではGitHubをプラットフォームとして採用していますので、以降は基本的にGitHubでの用語を用いて我々の開発ルールを説明します。 開発環境 リリースする環境としては、以下の3つを準備しています。 dev 開発者がいつでも任意のタイミングでリリースして良い開発環境。 stg mainブランチへのマージ後に必ずリリースされる本番適用前の検証環境。 prod stgでの検証後にリリースされる本番用環境。 ブランチ 作成するブランチは下記の2本になります。 mainブランチ 安定ブランチです。常にリリース可能なソースコードを管理します。 feature/xxxxブランチ mainブランチから切ります。開発する機能に応じて作成します。xxxxには開発する機能の内容が書かれます。 Pull Request featureブランチで開発が完了するとmainブランチにマージするためPull Requestを開発者が発行します。レビューはリポジトリ管理者と、開発メンバの2人で実施します。2人のレビューによるレビューで問題がなければマージされますが、このマージはfeatureブランチの作成者が行うのではなくリポジトリの管理者が責任を持って行います。 手順 以下が簡単な開発の流れになります。 mainブランチからfeatureブランチを作成 featureブランチで開発完了後、mainブランチへPull Request featureブランチの内容をレビュー。問題なければmainブランチにリポジトリ管理者がマージ mainブランチにマージ後、stg環境へリリース stg環境にてマージされた内容の動作を確認 stg環境で問題なければリポジトリ管理者がリリースバージョンを示すtagを付与 (前回の本番環境へのリリース内容と今回のリリース内容の差分を示すリリースノートを作成) tagの付与されたコミットをprd環境へリリース 通常のGitHub Flowは、手順1-4の流れを繰り返しますが、我々は6以降の手順を追加して運用しています。tagを活用することによって追加のブランチを作成する必要が無くなりますし、prd環境へのリリースバージョンが明確になるという嬉しさがあります。 手順7についてはプロジェクトによっては作成しないこともあるためカッコ付きとしています。我々の開発では、GitHub Actionsで前回のtagと差分を取得しリリースノートを自動作成するようにしています。tagと一緒にリリースノートも作成することで前回との差分が一目瞭然となるため非常に便利です。 また、stg環境へのリリースもGitHub Actionsでmainブランチへのマージをトリガーとしてリリースされるようにしています。prd環境へのリリースについてもGitHub Actionsでリリース出来るようにしていますが、こちらについては最後に人手の承認を挟んでからリリースするようにしています。 上記の手順でそれぞれどの環境にどの内容がリリースされているのかを示したのが下記の図になります。mainにマージされたものは全てstgにリリースされますが、prdへはtagのついたものだけがリリースされます。 その他、開発時のポイント GitHub Flowだからというわけではありませんが、次のような点にも気をつけて開発を進めています。 レビュアーのレビュー負荷を下げるために Pull Requestのサイズはなるべく小さくしています。目安は1-2時間でレビュー出来る分量です。 lintに加えてSonarCloudによるソースコードの静的解析をPull Request時に適用しています。静的解析が通らないとマージできないようにしており、一定の品質が保たれるようにしています。 実際にGitHub Flowを運用してみて GitHub Flowの採用により現在のところ大きな課題はなく運用ができていると感じています。もちろん今後開発の規模が大きくなった時等には新たな課題が出てくることが予想されますが、小規模チームで高速にプロダクトをリリースしていくのに良くフィットした開発ルールと言えます。 最後に運用してみて良かったこと、悪かったことを紹介して終わろうと思います。 良かったこと シンプルな運用フローであるため学習コストが低い 小さい単位でのPull Requestとコードレビューにより、品質管理とフィードバックのサイクルがスムーズになった 悪かったこと こまめにPull Requestを発行するのでレビュー頻度が高くなりがち。特にレビューはリポジトリ管理者に集中し負荷が高い状態が続いてしまった 対策:Pull Requestの発行時に、変更内容の詳細やレビューしてほしい観点を細かく記載することで認知負荷を下げる。チーム編成時のコーディングルールの事前すり合わせを徹底する ブランチが2本しかないのでmainブランチへのマージ時にコンフリクトが発生することがあった 対策:こまめにPull Requestとは言うものの、その単位が大きいことがしばしばあったため、変更を小さな単位で頻繁にレビュー・マージすることを徹底することで、マージ時の競合の範囲を減らしていく まとめ 小規模開発チームが取るべきブランチ戦略としてGitHub Flowに着目し、我々のチームがどのように運用してきたかをご紹介しました。GitHub Flowは素早くプロダクトがリリースできることからPoCなど、価値検証を高速に回していくことが求められる開発現場に特にフィットすることが確認できました。 Insight Edgeでは一緒に変革しながら価値検証ループを高速に回していけるメンバを募集中です!技術選定はもちろん、このような開発ルールについても柔軟に変革できる環境となっていますので、興味がある方はカジュアルにお話させて頂ければと思います。お気軽に 公式サイト の採用ページからお申し込みください!