TECH PLAY

株式会社Insight Edge

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

174

こんにちは!Insight Edge で Developer をしている Kobori です。 本記事では、Liferay を使用したアプリ開発で得た知見について、使い方とノウハウを交えてを紹介します! 関連記事が少なかったのも苦労した点の一つでもあるので、本記事が開発の助けになれば思いを込めて書きました。 ぜひ、最後までご覧いただけたらと思います。 この記事でわかること Liferay の概要 Liferay での実装方法 Liferay とは? 社内ポータル等の Web システム構築を比較的短期間でできる統合 Web ポータルプラットフォームです。 ユーザーが必要とする機能部品の多くが用意されており、足りない部品や独自のセクションも自作することができます。 Liferay 公式サイト Liferay で開発することになった背景 今回使用することになった背景としては、元々存在するサイトをリプレイスすることになり、ポータル内の検索向上や更新作業の負担軽減を目的に、別途知見のあった Liferay で開発することにしました。 Liferay での開発方法 実際の開発の中で使用した機能の使い方や特徴を紹介していきたいと思います。 今回は、下記の機能を使用してサイトの構築を行いました。 Liferay の使用した機能 サイトページ ナビゲーション ドキュメントとメディア タグ ページフラグメント ウィジェットテンプレート サイトページの基本操作 基本的には、サイトの作成、ページの作成を行い、ドラッグ&ドロップ操作による要素の追加によって画面を作成していきます。 標準レイアウトも用意されているため、標準レイアウトに沿って作ると短時間でサイトの構築ができます。 サイト構築前のポイント 事前に標準レイアウトの使い方を知っておくことで、標準レイアウトを使用するのか?独自レイアウトを使用するのか?といった、初手での UI 設計と実現性の判断に役立ちます。 Liferay ナビゲーション 遷移先とページを紐づける役割を持ち、その設定を使ってメニューを構成できます。 設定方法は、下記のようになります。 また、サブメニューの配下にページを配置するとメニューの中に遷移先のリンクを用意できます。 Liferay ドキュメントとメディア 画面に表示したい画像ファイル等を格納する場所です。 用途ごとにフォルダ分けすることをおすすめします。 ドキュメントとメディアでの工夫点 タグを設定することで一覧表示のルール(表示条件)に使用することができるようになります。 アップロード時の一括設定でタグを設定するのがおすすめです。 Liferay ページフラグメント 基本コンポーネントで表現しきれない部分は、下記のように HTML,CSS,JS を用いてページフラグメントを実装します。 ページフラグメントでのつまずき Liferay で持つ情報(ドキュメントに格納したファイル等)を動的に表示できるようにする \<lfr-widget-asset-list\>  等のタグの使い方を把握するのに苦労しました。 このタグを配置してページの編集画面にて表示される歯車アイコンを押下するとアセットパブリッシャー設定が行えるようになります。 アセットパブリッシャー設定画面 アセットパブリッシャー設定画面では、動的な情報のルール(表示条件)設定と表示形式の設定が行えます。 ルールは、カテゴリ/タグ/キーワードで絞ることができますが、キーワード検索でフォルダを指定すると余計なファイルも抽出されます。確実に抽出するには、付与したタグで絞ると良いと思います。 Liferay ウィジェットテンプレート 今回のアセットパブリッシャーのテンプレート実装では、FreeMarker 言語を選択して実装しました。 PHP のように<>で囲った部分に実装していきます。 if 文や list に entries を指定することでルールに基づいた情報を用いて、表示形式の設定ができます。 ウィジェットテンプレートでのつまずき ユーザーアイコンを取得する API の呼び出しが必要となったのですが、仕様がなかなか難しく把握するのに苦労しました。 今回の実装では、 serviceLocator.findService("com.liferay.portal.kernel.service.UserService") にて userService を取得し、getUserById で entry オブジェクトに持つ UserId を設定して user 情報を取得後、その user 情報の portraitURL を取得することでユーザーアイコンの URL を取得しました。 ユーザーアイコンの URL を img タグで使用することで画面上の任意の場所でアイコンが表示できます。 <#assign userService = serviceLocator.findService("com.liferay.portal.kernel.service.UserService")> <#assign user = userService.getUserById(curEntry.userId)> <#assign userPortraitURL = user.getPortraitURL(themeDisplay)> <img src="${userPortraitURL}" alt="UserIcon" /> Liferay を使ってみた感想 UI パーツが豊富にあるので縦横の標準セクションや部品を使うことで短時間でサイトの構築ができると感じました。 また、フッターなどの共通セクションを自作し利用するのは、とても有効であると感じました。 一方で 自作セクションを作成するには HTML, CSS, JS 等を用いた実装が必要なため、標準セクションと自作セクションの選択によって作成時間が大きく変わります。そのため、UI を考える際には、標準セクションでどこまで実現できるか意識すると良いと思います。
アバター
はじめまして!Insight Edge デザイナーの水上(ミズカミ)です。 2023年5月にジョインさせていただき、Insight Edgeが発信する様々なデザインを担当しております。 だんだん暖かくなってきて外を歩きたくなってしまい、どんどんテックブログ執筆が進まなくなってしまっています。(今日はちょっと我慢してPCに向き合っております😇) 今回は、昨年度からInsight Edgeとしての情報発信に力を入れている背景や、私がどんなアプローチでデザインを行なってきたかをご紹介いたします。 1.グループ内マーケティングの意義 2.実際に取り組んだこと ホワイトペーパー メールマガジン リアルイベント おまけ:採用サイト 3.半年(と少し)を振り返って 4.課題とチームの出会いをデザインする 5.これからはお店づくり 1.グループ内マーケティングの意義 住友商事にはグループ内だけでも約900社の企業を抱え、業界・事業フェーズも多岐にわたっていることもあり、Insight Edgeはこれまで多様なニーズに対応する技術専門集団としてプロジェクトに携わってきました。 また、住友商事のデジタル戦略推進部(DXセンター)やグループ会社の中でも圧倒的な社会的認知のあるシステム開発からあらゆるデジタル関連のサービスを提供している「SCSK」という強い協業パートナーの存在もあります。 まだまだポテンシャルのある案件が生まれる可能性と、”ChatGPT”という数多くの人々のワークスタイルに変化を与えるようなトレンドが来たことが、大きな機会を与えてくれました。 今までデータサイエンティストの分析やデジタル化による省力化・高度化がどんな影響を与えてくれるのか曖昧だった人たちにも、Insight Edgeが提供する価値への期待と組織の持つ知見を必要とする人達から多くの反応がありました。 Insight Edgeの行動指針である「みんなでやる」をもっと拡大解釈して「(住友商事)みんなでやる」の意気込みで、テクノロジーの専門集団として情報発信とコミュニケーションを図り、認知拡大、興味・関心のベクトルをInsight Edgeという組織に集める施策を仕掛けていきました。 2.実際に取り組んだこと まず取り組んだのは、マーケティング施策でのデザイン制作に向けたトンマナのチューニングです。 これまで、住友商事のグループ会社といえど、「Insight Edge」というエッジの立った個性を発揮しており、ブランドカラーは黒と白のみを使用していました。 ただ、マーケティングではある程度「注目を集める」ことや「親和性を感じる」ことも重要と考えたため、今まで使用してきたブランドカラーに住友商事の「浅葱色」を意識しながらデジタルのデバイスで鮮明に見えるようにチューニングしたグリーンをプライマリーカラーに追加しました。 また、Insight Edgeが向き合う部門数(2023年6月時点)に合わせたアクセントとして活用できるカラーパレットを揃えて、扱えるデザインの幅を広げていき、制作をスムーズに複数人で進められる準備を整えました。 ホワイトペーパー 過去経験してきたテクノロジー活用のケーススタディと技術者の目線でより活用の機会が広がるようなヒントをご紹介するお役立ち資料を制作しています。 Insight Edgeが取り扱う専門分野の知見をご紹介することで、現状のビジネスをよりテクノロジーの力で改善したいと思う方々とつながることができました。 ダウンロードは こちら 今年もこの活動を続けていく予定ですので、ぜひ次編もお楽しみにお待ちください🙏 メールマガジン 住友商事グループの方々に向けた社内で進行しているプロジェクトや、エンジニア・データサイエンティストがキャッチしているナレッジのご紹介をしています。 また、弊社の研修制度で国内外のイベントに参加したメンバーによるレポーティングも行っています。 バックナンバーをいくつかご紹介します。 リアルイベント 住友商事グループが活用できるコワーキングスペースを貸し切りリアルイベントを2023年2月下旬に開催しました。過去事例のモックアップを実際に触っていただいたり、弊社主要メンバーのトークセッションも催し、多種多様な事業と向き合われている方々にInsight Edgeの技術者が直接お話させていただくことで、一歩踏み込んだ技術活用の可能性を感じていただきました。 今までホワイトペーパーやメルマガを通して一方的な関係性の方々ともリアルイベントを通して直接コミュニケーションを取ることができました。 社内向けイベントということでイベント内での制作物や様子はなかなか見せられないのですが、約200名の方々にご参加いただきました。 足を運んでいただいた皆様、本当にありがとうございました。 おまけ:採用サイト マーケティグとは別軸にはなりますが、Insight Edgeの 採用サイト もリニューアルを行いデザインを刷新しています。社名に違わぬエッジのたった異能が集まる組織かつ、ブログや勉強会も通して学び合い共有し合うミクスチャー文化、組織が一枚岩となっているような様を表現できるようにモチーフやビジュアルを作成しデザインしました。 3.半年(と少し)を振り返って まだ1年経っていませんが、住友商事グループ内の方々にInsight Edgeのことを認知していただくきっかけとなるデザインを首尾一貫で見させていただけるのはなかなか自分にとってもない経験でした。 特にEdgeのたった専門家たちのこれまでの活躍を、イベント等を通してより多くの方々に知っていただける機会を広げられたのも達成感がありました。 4.課題とチームの出会いをデザインする 今までのプロジェクトをまとめながらデザインで落とし込む過程で、難しいと感じたのはシステムやサービス開発は世に出しても無形の資産であるということです。商品の物的な価値が明確でない分、それを受け取り、価値を感じるかどうかはお客様次第になることの方が多いです。 私たちのこれまで開発してきたソリューションやシステムに価値を感じていただける課題を持つ人々にお届けできるようにこれからも試行錯誤しないといけないと感じています。 また、市場的にも日進月歩新しい技術が生まれている分野でもあるとともに、チームのサイズも大きくなっているため、今ならまた違ったアプローチで解決できる課題も多くなっているかもしれません。 まずは、課題とチームのマッチングを生み出すために、これからもマーケティング活動は続けていきたいと考えています。 5.これからはお店づくり マーケティングを進める中で、今までのInsight Edgeの住友商事内の技術専門家集団という肩書きは総花的で、受け取り手にどんな存在なのか言語化するのを委ねるようなブランドになっているような気がしております。 主力となる商品やサービスの形が無くても、「うちにはこんな売り物がありまして…」「お客様にはこちらのソリューションがピッタリですね!」と言えるような、価値の定義が明確でぶれないモノも陳列し、お客様を迎えられるような存在を目指していきたいと考えている今日この頃です。 また今年度もイベントの出展やホワイトペーパーの更新も予定しているので、もし興味がある情報がありましたら、いつでもお問い合わせ下さい。 これからも社内外含めてブランドの発信に力を入れていきたいと思っています。
アバター
はじめに ししとうLTとは? アウトプットと採用と育成 Insight Edge 猪子 (@iN0Ti) チームが進化し続けるための改善プロセス アスエネ 石坂 (@ishisak) クライアントワークのエンジニアリング  フラー 韮澤 (@nirazo) 強みを伸ばすキャリアデザイン HRBrain 山口 (@yug1224) 雑談のすすめ IVRy 近藤 (@K0703K) 懇談会 最後に はじめに こんにちは!Insight Edgeの人事担当、合田です。 4/9(火)の夜に第1回「ししとうLT会」をオフラインで開催したので、今回はそのイベントレポートを書かせていただきます。 ししとうLTとは? 「ししとう」は見た目とは裏腹に、一口食べると意外な辛さがあることがあります。 ししとうのような「意外性や挑戦」をテーマに、新しい発見や刺激を提供できるコミュニティになればと思い、この名前をつけました。 IVRy、アスエネ、Insight Edge、HRBrain、フラーの5社に所属するエンジニアたちが、それぞれの得意分野や興味のあるテーマについて発表する場で、主催企業(会場運営)を輪番で担当することに決まったのが2023年12月。平素業務の合間を縫いながら、少しずつ準備を進めてきました! 記念すべき第一回の主催企業は当社Insight Edge!ということで早速レポートに入ります。 当日の全体司会はIVRyの堀田( @yuri_ivry )さん!(ありがとうございました) 当日のタイムテーブルはこの通り アウトプットと採用と育成 Insight Edge 猪子 ( @iN0Ti ) 1人目の登壇者は当社CTO猪子さん。 “忙しい時ほどアウトプットを大事に!”というテーマでした。 転職潜在層へのアプローチ、選考中の候補者へのアトラクトに加え、在籍エンジニアの育成観点でのアウトプットの場として立ち上げたテックブログの運用が1年半を超え、今まで投稿した記事数は延べ70本にも! 「継続的な投稿すごい!どうやって運用したんですか?」と来場者からの質問もいただきました。 Insight Edgeのテックブログの運用は、編集チームを立ち上げて、輪番制で指定している寄稿者に業務負荷がかかりすぎないよう、テーマ出しや校正のアドバイス、スケジュールリマインドなどを編集チームがしっかりサポートしています。 また選考面接官を務める猪子さん自身、採用候補者のテックブログに対する関心の高さを肌で感じているとのことでした。 ※ちなみにこのししとうLTも、「エンジニア達のアウトプットの場を作りたい」という気持ちで立ち上げたとの想いを語っていただきました。 チームが進化し続けるための改善プロセス アスエネ 石坂 ( @ishisak ) 2人目の登壇者は次世代サステナブル経営支援で5期目を迎えるアスエネのVPoE石坂さん!  最近ではシンガポールやアメリカにも進出されているアスエネの”組織進化に向けたプロセス“についての発表です。 石坂さんが考える「強いチームに必要なこと=強い文化」、という定義から始まり、「強い文化が暗黙的に浸透している状態」を実現するために行った施策を教えていただきました。 まずは週次ふりかえりをKPTで実践、ふりかえりで解決しきれない課題に関しては、ワーキンググループを立ち上げ、有志で集まったメンバーが解決まで推進してくれるそうです。 課題解決のために試した施策をまた振り返り、失敗だったなと感じればロールバックして前のバージョンに戻す。 つまり、効果的な改善フローはGitHub Flowに帰着する!という結論には会場内のエンジニア達も「ふむふむ」と聞き入っていました。 「物事を決める時に強いリーダーは必要?」という会場内からの質問に対しては「ルールを決めるための強いリーダーは不要で、ルールを決めた後に物事が浸透するまで言い続ける、体現し続けるリーダーは必要だと思う」との回答。 クライアントワークのエンジニアリング  フラー 韮澤 ( @nirazo ) 3人目の登壇者は、「全ての企業のデジタルパートナー」を目指すフラーの統括マネージャーの韮澤さん。 多岐にわたる事業領域の会社へのデジタル支援を行う上で大事にしている2つのことを発表していただきました。 1つは“開発しているプロダクトを自分ごと化”すること。実際にプロダクトを体験する、その体験をプロダクトに落とし込み、実際にその機能の有用性を確認する・・・徹底してパートナー企業に伴走する姿勢が素晴らしいです。 もう1つは攻守のバランスをとること。 様々な事業会社の支援を行うため、扱う事業領域が多岐に渡る。必然的にドメイン知識の理解や技術調査に多くの時間を使うことが多くなってしまうそう。 そんな中で、技術面のスイッチングコストはなるべく下げ(守)、SwiftUIなどメリットの大きい技術を見定めて導入するコストは掛ける(攻)、とのこと。 新潟県出身の韮澤さん。新潟に本社を構えるフラーが開発を行った長岡花火公式アプリを、実際に花火大会会場でユーザーが使っている様子を見たときは心から感動したというエピソードが印象的でした。 強みを伸ばすキャリアデザイン HRBrain 山口 ( @yug1224 ) 4人目の登壇者は、「人と企業の持続的成長を支える」、を企業理念として8つのプロダクトを開発しているHRBrainのエンジニアリングマネージャー山口さん。 フロントエンドエンジニアからスタートした山口さん、様々なチーム規模や組織で開発を行う中で、節目節目でクリフトンストレングス(旧:ストレングス・ファインダー)を活用してご自身の動き方を考えていたそうです。 ストレングスファインダーの結果は、時が経つと若干変わってくる所もありつつ、山口さんがずっと変わらなかった要素は「最上思考、戦略性」だったそうです。 「振り返ってみて、フロントエンド×マネジメントという希少性の高いキャリアを築くことができたことを自負している」と語る姿、流石“戦略家”!  最近はピープルマネジメントにもストレングスファインダーを活用し、部下に対して納得感のあるフィードバックや評価を落とせるようにしているとのことでした! 山口さんのLTを聞いて、 クリフトンストレングス(旧:ストレングス・ファインダー)の書籍購入のリクエストが早速Insight Edge内で上がっていました! 雑談のすすめ IVRy 近藤 ( @K0703K ) 最後は、あらゆる電話業務の自動化、効率化で多くの導入実績のあるIVRyのVPoE近藤さん。 忙しい日々を送る中で、どうしても生産性やスピード重視になってしまう中、IVRyが積極的に取り入れている雑談の効果についての発表でした。 IVRyではスケジュールに強制的に雑談の時間を設定してしまったり、オフィスに導入したコーヒーマシーンで豆を挽く音が聞こえたら雑談タイム、というルールを作ったり、思いついた時にキャッチボールが出来るようにと、メンバーの方がグローブとボールをオフィスに持ち込まれたそうです。 オフィスに行かなくとも働けるからこそ、自主的にオフィスに行きたくなる仕掛けは見習いたいもの。 ちなみに、直近では何かあった時に叩く用でドラを導入したとのこと(笑)。 雑談関連で盛り上がったという近藤さんの話すエピソードの数々こそ、IVRyの掲げる We make “Work is Fun”from nowを体現しているようなLTでした。 懇談会 各社のLTが終わったら、最後は飲食を交えながらの懇親会です。 今回のLTは、参加企業5社以外の方でも自由にご参加いただけるようにしました。 実際に外部から参加した方に、なぜ参加しようと思ったのか聞いてみたところ「フリーランスエンジニアで、本当は人と話すのが好きだけれど、中々そういう場がないので参加しました」という声や 「最近昇格して部下を持つようになったばかり。今回のLT登壇者はマネジメント経験者が多かったので、先輩として色々聞いてみたかった」との声を聞きました! 最後に ししとうLTは今後も継続的に開催する予定で、次回以降は登壇者も一般公募します。 アウトプットの場として活用してみたい、というエンジニアの方、ぜひご応募ください! (#2のLT枠は既に埋まってしまいましたが、ぜひ#3以降のご応募お待ちしています!) 4410.connpass.com ご登壇いただいた方のご厚意で、資料共有をいただいております。振り返り等にご活用ください! ▼connpass資料共有スペース 4410.connpass.com ※共有いただいた皆様ありがとうございます! レポートは以上です。 (おまけ)Insight Edgeでは一緒に働くエンジニアも積極募集中です! 少しでもご興味をお持ちいただけましたら、ぜひご連絡ください。 recruit.insightedge.jp
アバター
こんにちは!Lead Engineerの筒井です。 このテックブログでも何度かテーマに上がっていますが、住友商事グループでは生成AI活用の取り組みが進んでおり、Insight Edgeもその推進を技術面から強く支援しています。 様々な取り組みがありますが、LLMの使われ方として基本的にはRAGを用いて回答を行うような、単発の特定タスクを解くというものが多いです。 今後はLLMに様々なツールを与えておき、より多様なタスクを状況に応じて自律的にこなしてもらうような、エージェント的な利用も模索していきたいと考えています。 そこで、今回は簡単な自律型のエージェントを、Prompt flowとFunction callingを用いて作成してみました。 もくじ Prompt flowについて 自律型エージェントの概要 Prompt flowによる自律型エージェントの作成 format_inputノードの処理 agentノードの処理 functionノードの処理 作成したフローの実行 まとめ Prompt flowについて Prompt flowは、Microsoftによりメンテナンスされている、LLMアプリケーション開発のためのOSSです。 詳細は割愛しますが、特にAzure AI Studioとの統合により、LLMアプリケーションのライフサイクルにおける開発の効率化や、評価・改善を継続的に行うための基盤として利用ができるというところに強みを持っています。LLMアプリケーションの評価については、最終の出力のみでなく、例えばRAGであればユーザー入力から生成した検索フレーズ、検索結果から得られた参照ドキュメントなど、中間出力についても精度を検証していくことが重要となります。Prompt flowでは各処理をノードとして扱い、各ノードの出力を評価対象とすることができるため、処理ごとの評価・改善を効率的に実施することができます。 自律型エージェントの概要 今回作成する自律型エージェントには、以下のようなタスクを行わせたいと思います。 すでに用意されたFAQから答えられるものはそのFAQをベースに回答を返し、それ以外の質問についてはドキュメントから回答に合う部分を探して回答を返す、RAGをベースとした構成です。 RAGの実現方法は様々あると思いますが、ここではいずれもベクトル検索を用いて回答を返すこととします。 さて、これをPrompt flowでどのように構成するかですが、この構成は考えてみると不定な長さの繰り返し処理を含んでいます。 一方、Prompt flowはDAG(有向非巡回グラフ)でフローを構成する仕組みのため、繰り返し処理を含むフローを表現することができません(NodeCircularDependencyエラーが発生します)。 Pythonコードで書くならば、while文を使って繰り返し処理を行うことができますが、その場合は単一ノードで全ての処理を行うことになってしまい、上述したようなPrompt flowのメリットを活かすことができません。 というわけで結論としては今回取ろうとしているような自律型エージェントの構成をPrompt flowで実現するのは向いていないということになるのですが、 それでブログ記事が終わるとちょっと寂しいので、固定超の繰り返し処理を含むフローでやれるところまでやってみようと思います。 構成を以下のように変更します。 また、具体的に扱うデータとして、個人情報保護法のQ&Aとガイドラインを利用します。 Prompt flowによる自律型エージェントの作成 この構成をPrompt flowで以下のように作成しました。 せっかくなので、LLMの呼び出しにはLLMノードを使っています。 今回はベクトルDBとしてAI Searchを利用しているため、ベクトル検索もIndex Lookupノードを利用できるのですが、そもそも向いていない処理を無理やりやらせていることもあり、フローが複雑になりすぎるので、今回はPythonコード中で処理をしています。 また、Prompt flowでは同じコード(pyファイル)やプロンプト(jinja2ファイル)を複数のノードに割り当てることができるので、同じ処理を繰り返し実行するという元の構成の意図を残し、各ノードに個別の実装をするのではなく共通の実装を各ノードに割り当てるようにしています。 format_inputノードの処理 format_inputは、各ステップをまたいで状態を維持するためのオプジェクトデータを作成する処理です。 各ステップで適切な処理を行うことができるように、以下の状態を持つようにします。 finished 回答の生成が完了したかどうか。元の構成では回答が終了した場合にループを抜けますが、Prompt flowではそのような処理ができないため、各ノードでの処理をスキップするために使用します query ユーザーからの質問文 function_results 各ステップで実行したFunction callingの結果。使った関数の名前と引数、戻り値をリストにして保持します agentノードの処理 agentは3つのノードがありますが、先述の通りすべて同じプロンプト設定を持っており、上述の finished の値が True の場合は処理をスキップするように設定しています。 具体的なプロンプトは以下の通りです。ここで、 input は先述の状態オブジェクトです。 # system: Function callは必要に応じて利用してください。 あなたは個人情報保護法に関する質問に回答するアシスタントです。事前に用意されたQ&A資料やガイドライン資料を参照して、ユーザーからの質問に回答してください。 Q&A資料を参照して回答の根拠が見つからなかった場合、ガイドライン資料を参照してください。 回答を述べる際は、その根拠となる資料中の文章を引用して回答に含めてください。 {% for result in input .function_results %} # assistant: Function generation requested, function = {{result.name}}, args = {{result.arguments}} # function: ## name: {{result.name}} ## content: {{result.content}} {% endfor %} # user: {{ input .query}} プロンプトの記載方法は、こちら 1 を参考にしました。 # system や # assistant などはそれぞれOpenAIのAPI呼び出し時に、roleに変換されるようです。 2 執筆時点でサポート済みの記載が見つけられませんでしたが、 # function 内も特定の書き方をしないとエラーになるため、変換に対応しているようです。 各ノードには別途Function calling用の関数が指定してあり、内容は次のfunctionsノードの処理で説明します。 functionノードの処理 こちらも先述の通り3箇所すべてに同じ関数定義がされています。 agentノードがFunction callingを選択した場合はその処理を行い、agentノードが回答の出力を選択した場合はそれをそのまま次の処理に流します。 内部で呼び出しているsearch関数は質問のベクトル化とAI Searchでのベクトル検索を実行する関数ですが、ほぼ公式ドキュメントのSDKサンプル 3 を参考にしているので、ここでは省略します。 また、Function calling部分の実装はこちら[^1]を参考にしています。 def qanda_search (query): return search( "qanda" , query) def guideline_search (query): return search( "guideline" , query) @ tool def run_function (inputs: dict , agent_message: dict ) -> dict : # 直前のagentノードが実行されていない=すでに回答生成済みの場合 # 現在の回答をそのまま格納する if agent_message is None : return { "finished" : True , "content" : inputs[ "content" ] } # 直前のagentノードが実行され、回答が生成された場合 # 状態を回答済みとし、生成した回答を格納する if agent_message[ "function_call" ] is None : return { "finished" : True , "content" : agent_message.get( "content" , "" ) } # 直前のagentノードが実行され、Function callingが呼び出された場合 # 指定された関数を実行し、実行結果をリストの末尾に追加する function_call = agent_message.get( "function_call" , None ) function_name = function_call[ "name" ] function_args = json.loads(function_call[ "arguments" ]) result = globals ()[function_name](**function_args) function_results = inputs[ "function_results" ] + [{ "name" : function_name, "arguments" : function_args, "content" : [result] }] return { "finished" : False , "query" : inputs[ "query" ], "function_results" : function_results } agentノードでのFunction callingの設定は以下のようにしました。 Q&A資料が質問にうまくマッチする場合はその方が精度が高いというユースケースを想定し、先に参照するように指示しています。 [ { " name ": " qanda_search ", " description ": " Q&A資料を参照する時に利用します。すでに一度呼び出している場合は呼び出さないでください。検索用のクエリを指定して検索をします。 ", " parameters ": { " type ": " object ", " properties ": { " query ": { " type ": " string ", " description ": " 検索用のクエリ。基本的にはユーザーの質問そのものを使いますが、必要に応じてタイポの修正やベクトル類似度による検索に適した形への修正をしても構いません " } } , " required ": [ " query " ] } } , { " name ": " guideline_search ", " description ": " ガイドライン資料を参照するときに利用します。Q&A資料を参照して適切な回答がない場合はこちらを呼び出してください。検索用のクエリを指定して検索をします。 ", " parameters ": { " type ": " object ", " properties ": { " query ": { " type ": " string ", " description ": " 検索用のクエリ。基本的にはユーザーの質問そのものを使いますが、必要に応じてタイポの修正やベクトル類似度による検索に適した形への修正をしても構いません " } } , " required ": [ " query " ] } } ] 作成したフローの実行 今回作成したベクトルDBには、個人情報保護委員会が公開している個人情報保護法のQ&Aと、個人情報保護法のガイドラインが登録されています。 4 ガイドラインについては、「個人情報の保護に関する法律についてのガイドライン(通則編)」のPDFのみを利用し、Q&Aについては、同じく「ガイドライン(通則編)」に対応するQ&Aのうち、一部のみを登録しました。そうすることで、未登録の質問についてはQ&Aのみだと回答が見つからない状態にしています。 フローを実行すると、以下のような結果が得られました。まずは、Q&Aに存在するのと同じ質問を入力した場合です。 先に参照したQ&Aの内容で回答可能と判断してガイドラインの参照をスキップし、Q&Aの内容を引用して回答を返しています。 次に、Q&Aのみでは見つからない質問をしてみます。先に参照したQ&Aの内容では回答が見つからないため、さらにガイドラインも参照して回答を返しています。 今回作成したフローでは、プロンプトを含むRAGの精度については十分に精査していないため、必ずしもこれらのように適切な動作をするわけではないものの、少なくとも想定したような動作ができることは確認できました。 フローの評価方法についてはここでは詳細を割愛しますが、上記の通り各ノードごとに個別の入力と出力を取得できるため、単にユーザーの質問と最終出力のみでなく、各ステップごとの処理まで深掘りして評価することができます。 また、Azure AI Studio上でバルク実行をすることで、複数の質問に対して一括で回答を生成し、まとめて評価を行うことも可能です。 まとめ 今回はPrompt flowを使って、固定長の処理を行う自律型エージェントを作成してみました。 Prompt flowは事前に定義されたノードやツール、Azure AI Studioとの統合による評価の容易さなど、様々なメリットがある一方で、繰り返し処理が必要となる自律型のエージェントの作成には向いていなさそうです。 既にクローズされたIssue 5 にも言及がある通り、DAGという性質上の難しさはあると思いますが、なんらかの機能サポートに期待しつつ、他の方法での実現も模索していきたいと思います。 microsoft/promptflow ↩ LLM — Prompt flow documentation ↩ Python のサンプル - Azure AI Search | Microsoft Learn ↩ 法令・ガイドライン等 |個人情報保護委員会 ↩ [Feature Request] Achieve dynamic flow connections with logic conditions · Issue #386 · microsoft/promptflow ↩
アバター
はじめに はじめまして、InsightEdge 分析チームの中野です。 今回は自然言語からCypherクエリを生成する手法について、LLM(大規模言語モデル)を用いたアプローチを紹介します。 最近、RAG(Retrieval-Augmented Generation)という手法が注目されています。これは、LLMが外部の知識ベースを検索し、その情報を基に回答を生成するプロセスです。また、外部知識にナレッジグラフを利用することでデータを構造化し、より関連性の高い情報を抽出することも注目されています。 ナレッジグラフを使用するにはneo4jのようなグラフデータベースを使用することが一般的です。 しかし、このRAGプロセスではテキストからグラフクエリ言語であるCypherクエリを生成する必要があります。 この記事では、このRAGプロセスでCypherクエリを生成する際の課題と、Chain-of-Thought(CoT)を使用して精度向上を試みた結果について紹介します。 はじめに text-to-cypherの課題 対策と検証 Chain-of-Thought Promptingの実装 テストデータ 評価指標 結果 まとめ 参考文献 text-to-cypherの課題 ナレッジグラフを使用したRAGプロセスでは、テキストからCypherクエリを生成するステップが必要となります。 しかし質問文ではクエリの構成を具体的に指定しないため、必ずしも適切なクエリを生成できないという問題があります。 例として Twitchのソーシャルネットワーク を使い、簡単なクエリの生成を試みます。 このTwitchのネットワークは、ユーザーやゲームのノードで構成されており、どのユーザーがどのゲームをプレイしたか、誰が誰にコメントしたかといった情報が含まれています。 このグラフデータベースに対して「人気のゲームのタイトルを教えてください。」というテキストから、Cypherクエリを生成させてみます。 「人気のゲームのタイトルを教えてください。」という問いに対し、gpt-3.5-turboを用いた生成結果ではプレイされたゲームを一覧表示するクエリが生成されました。しかし、これは配信者がプレイしたゲームすべてを列挙するため出力が数万から数百万件に及ぶ可能性があり、適切なクエリとは言えません。 -- gpt-3.5-turboを使用して「人気のゲームのタイトルを教えてください。」から生成したクエリ -- NG:応答が多すぎて、適切なクエリではない。 MATCH (s:Stream)-[:PLAYS]->(g:Game) RETURN g.name 一方、gpt-4を用いた場合は配信者がプレイしたゲームを列挙した後、ゲーム名ごとにプレイ回数を集計し、人気のゲームを上位10件表示するクエリが生成されました。このクエリは、人気のゲームを知りたいならば、ゲームごとにプレイした人数を調べればよいという意図を反映した、適切なクエリと言えます。 -- gpt-4を使用して「人気のゲームのタイトルを教えてください。」から生成したクエリ -- OK:ゲームごとにプレイした人数が調べられているため、適切なクエリである。 MATCH (s:Stream)-[:PLAYS]->(g:Game) RETURN g.name AS Game_Title, COUNT (*) AS Popularity ORDER BY Popularity DESC LIMIT 10 このようにgpt-3.5-turboでは簡単な集計を必要とするクエリですら満足に生成することはできません。gpt-4では適切にクエリを生成できているもののコストがgpt-3.5-turboの約20倍、速度が10分の1️と非常に低速です。そのためエージェントなど何度もグラフに問い合わせを必要とする用途では、なんとかしてgpt-3.5-turboのような軽量なLLMに回答を作成させたいところです。 対策と検証 精度向上のために、今回はChain-of-Thought(CoT)を試します。CoTは、入力から出力に至るまでの中間過程を明示することで、複雑な問題にも対応できる手法です。 この手法を用いることで、軽量なLLMでも高精度な出力を実現できる可能性があります。 改善施策の効果を検証するために、10個程度の質問文とクエリのペアを手作業で作成し、gpt-3.5-turbo、gpt-4、それにCoTを加えたバージョンの4つについて、生成されたクエリとその応答結果を比較します。 クエリの精度はJaro-Winkler距離で、応答結果の精度はJaccard係数で測定します。 Chain-of-Thought Promptingの実装 実装はLangChain(0.1.0)を使用します。 まず通常のcypherクエリだけを出力するChainを以下のように定義します。 # Chain-of-Thought Promptingを使用しない class CypherResponse (BaseModel): query : str = Field(title= "Cypher Query" ) def get_cypher_chain (graph, model): parser = PydanticOutputParser(pydantic_object=CypherResponse) prompt = PromptTemplate( template= """Based on the Neo4j graph schema below, write a Cypher query that would answer the user's question: {format_instructions} {schema} Question:{question} JSON OUT:""" , input_variables=[ "question" ], partial_variables={ "schema" :graph.schema, "format_instructions" : parser.get_format_instructions() } ) chain = prompt | model | parser return chain 次にChain-of-Thoughtを使用する場合は以下のように定義します。queryの他に思考過程も出力させます。 # Chain-of-Thought Promptingを使用する class ChainOfThougthCypherResponse (BaseModel): chain_of_thought : str = Field(title= "Thought process in the process of generating a cypher query" ) query : str = Field(title= "Cypher Query" ) def get_cot_cypher_chain (graph, model): parser = PydanticOutputParser(pydantic_object=ChainOfThougthCypherResponse) prompt = PromptTemplate( template= """Based on the Neo4j graph schema below, write a Cypher query that would answer the user's question: {format_instructions} {schema} Question:{question} Lets think step by step. JSON OUT:""" , input_variables=[ "question" ], partial_variables={ "schema" :graph.schema, "format_instructions" : parser.get_format_instructions() } ) chain = prompt | model | parser return chain テストデータ テストデータとして質問文とCypherクエリのペアを10組程度作成しました。 data = [ { "question" : "韓国語(ko)を使用するストリーマを調べて。" , "cypher" : "MATCH (s:Stream)-[:HAS_LANGUAGE]->(l:Language) WHERE l.name = 'ko' RETURN s.name AS Streamer " }, { "question" : "人気のゲームのタイトルを5つ教えてください。" , "cypher" : "MATCH (g:Game)<-[:PLAYS]-(s:Stream) RETURN g.name AS game, COUNT(*) AS streamCount ORDER BY streamCount DESC LIMIT 5" }, ... ] 評価指標 評価方法については こちらのブログ を参考としました。 生成されたクエリの精度はJaro-Winkler距離を使用して測定し、応答結果はJaccard係数を使用して測定します。 Jaro-Winkler距離 は、2つの文字列の類似性を測定するための手法です。生成したクエリと正解のクエリがどれだけ似ているかを測定するために使用します。完全に一致する場合は1になり、完全に異なる場合は0となります。 Jaccard係数 は、2つの集合の類似性を測定するための手法です。生成したクエリを使い検索されたデータと正解のデータにどれだけ一致しているかを測定するために使用します。完全に一致する場合は1になり、完全に異なる場合は0となります。 結果 gpt-3.5-turbo、gpt-4、gpt-3.5-turbo+CoT、gpt-4+CoTに対して、生成されたクエリとクエリの応答結果を比較します。CoTを使用することでクエリの応答結果の精度が向上することを確認できました。 まとめ 本記事では、Chain-of-Thoughtを使用して、gpt-3.5-turboのような軽量なLLMでもgpt-4のような高精度な出力を実現できることを示しました。 今後は、Chain-of-Thoughtを活用して、テキストからCypherクエリを生成する手法をさらに精度向上させていきたいと考えています。 参考文献 https://towardsdatascience.com/evaluating-llms-in-cypher-statement-generation-c570884089b3
アバター
こんにちは。12月からInsight Edgeに参画したData Scientistのカイオと申します。 入社してから早速、ChatGPT案件やデータサイエンス案件をいくつか担当しています。 今回は、とある案件で使用したMLパイプライン管理ツール「Kedro」について紹介したいと思います。 目次 Kedroとは なぜKedroを選んだか メリット デメリット 他のツールとの違い まとめ Kedroとは まず、Kedroとは何かの説明から始めましょう。 パイプラインを表示する機能もあります(引用元: https://docs.kedro.org/en/0.17.7/03_tutorial/05_visualise_pipeline.html ) Kedroとは、データサイエンスのパイプラインを管理するツールです。データ収集、変換やモデル学習、ハイパーパラメータ調整の管理から精度評価までまとめて簡単にしてくれます。 パイプライン中の処理の中間成果物やモデルを簡単にアクセスできるようにしてくれます。 なぜKedroを選んだか 今担当している案件では、事前処理の段階で色々な関数を実行しています。datetime⇨datetime + timezoneに変換したり、ダミー変数を作成したりしています。 複雑な処理がたくさんある中で、途中の成果物を常に確認する必要があります。 例えば: before_preprocessing.csv ⇨ after_preprocessing.csv ⇨ train.csv, test.csv, validation.csv それぞれのステップの時点でファイルの中身を確認したいですね。毎回 .to_csv() を実行するのは面倒だし、フォーマットは簡単に変えられない。 Kedroを使うと自動で生成されるだけではなくて簡単にフォーマットを変えられます。もちろん、csvからPickleまで幅広いフォーマットをサポートしているだけでなく、バージョン管理やリモートアクセスもできます。変数名をcatalog.ymlに記入すると変数として使えるだけでなくファイルとして保存されます。 それがKedroを選んだ1つの理由です。以下はそのcatalog.ymlの一例です。 もちろん、products_rawなどもそのままパイプライン内で変数として使われます。 products_raw : type : pandas.CSVDataset filepath : data/01_raw/products_raw.csv products_preprocessed : type : pandas.CSVDataset filepath : data/02_intermediate/products_preprocessed.csv lgb_model : type : pickle.PickleDataset filepath : data/06_models/final_model.pkl results : type : pandas.ParquetDataset filepath : s3://customer_bucket/results.pq もう1つの理由は、プロジェクトの構成を決めてくれる機能です。 kedro new を実行すると自動でテストや、データ格納ディレクトリまで作ってくれます。 複数人で同じプロジェクトを開発するとき、個人のディレクトリ構成のばらつきを気にする必要がなくなります。 最後の理由は、それぞれのパイプラインを単独で実行できるところです。 何かパラメータを変えた時、 kedro run --pipeline=training というふうに実行でき、一部を簡単に飛ばせます。 他のツールとの比較 よく比較されるツールの中で、私の使ったことがあるのはOptunaとMLflowなので、Kedroと比較してみます。 MLflow MLflowは実験を管理するツールです。モデルやデータセットのバージョン管理ができて精度などの可視化ができる Kedroでcatalog機能を使えばデータセットやモデルのバージョン管理はできるが可視化やmetrics測定はそこまで得意ではありません Optuna Optunaはハイパーパラメータ調整フレームワークです。Kedroでも同じようなことができるが、そこまで向いていない気もします。ただし、2つを組み合わせることによってよりパワーフルなパイプラインを作ることができる(実際にその使い方をしている案件があります) SageMaker Pipelines(おまけ) SageMakerはAWSの機械学習向けのサービスです。クラウドのサービスと比較するのは少し無理がありますが、共通点が多いのも事実です S3にデータのバージョン管理ができるところはKedroのカタログ機能に似ていて同じようにパイプラインを設計できます メリット 中間成果物が自動で保存される(個人的にこれが一番大きい) プロジェクトごとによる構成の差分が少なくなる パイプラインの一部だけ実行できる(例:評価部分だけ実行したい) デメリット パイプライン実行中にエラーが発生した際、Tracebackが長く問題箇所まで辿りづらい 中間成果物のフォーマットによって、忠告なしに情報が失われる 例:PandasのDataframeをcsvとして保存したら、csvがサポートしていない型の変数は勝手に変換されて情報が消える Poetryとの相性が悪い 共存できるという方もいますが個人的にやってみたら色々難航して結局pip-compileでいきました 早いプロトタイピングには向いていない 簡単な使い捨てプログラムなどの場合はJupyterのほうがいい まとめ Insight Edgeに入社してから初めてKedroというツールを知りました。今まで数年間、機械学習関連の案件をやってきたのに、Kedroの存在を知らなかったのは少し恥ずかしいです。 色んな背景の方々や幅広い専門知識を持っている優秀なエンジニアが周りにいて毎日新しい知識を身に付けられるのは当社の素晴らしいところだと思います。これからも新しい技術やツールを学ぶとともにソフトなスキルを磨いて、周囲のエンジニアと切磋琢磨していき、プロジェクトで活用していきたいです!
アバター
はじめまして、今年の1月にInsight Edgeへ参画したData Scientistの山科です。 まだ加入して数ヶ月ですが、画像を用いた異常検知や予兆保全、オペレーションの最適化、生成AI用いたDX案件など、幅広い技術・分野に携わらせてもらっており、日々、調査にデータ分析の試行錯誤と四苦八苦しながら業務に取り組んでいます。 今回は、画像を用いた異常検知に関する案件でPatchCoreという手法を用いたのですが、使い勝手が良く様々なことに使えそうだなと感じましたので、間違い探しを題材にパラメータのケーススタディをした結果を評価し、どういったことに使えるかを考察していきたいと思います。 目次 画像に対する異常検知手法 AIで間違い探しを解く! 考察 画像に対する異常検知手法 画像認識技術を用いた異常検知の重要性については、 藤村さんの記事 でも紹介されている通りで、製造業や農業など幅広い分野で省力化・効率化のため用いられています。 画像に対する異常検知技術としては、近年、ディープラーニングベースのものが主流となっており、そのなかでもハイブリットモデルと呼ばれる、事前学習済みのニューラルネットワークモデルと異常検知として長く用いられているkNNを組み合わせた手法がSOTAを達成しています。ハイブリットモデルとしても、SPADE、PaDiM、PatchCore、といった手法が提案されていますが、今回は PatchCore を選択しました。 PatchCoreでは、学習済みモデル用いて正常画像から抽出した特徴量をMemory Bankに保存しておき、判定したい画像の特徴量との距離から正常/異常を判定します。この時、学習済みモデルを特徴量抽出器として用いているためニューラルネットワークの再学習が不要な点、学習済みモデルの最終層ではなく中間層から特徴量を取得するようにしたことで画像の位置情報も保持することで、局所的な異常(ピクセル単位での異常)を見つけやすくしている点が特徴となっています。 今回は、 ind_knn_ad で実装されているPatchCoreを用いて検証して行きます。 AIで間違い探しを解く! 間違い探しはよく似た絵から異なる箇所を探すパズルですが、異常検知も検証したい画像が正常な画像と比べてどこが似ているか、異なっているかを探すタスクですので異常検知と同様に考えられます。間違い探しの題材としましては、春といえばさくら、ということで下図の間違い探しに取り組んで行きたいと思います。画像サイズはそれぞれ、950✕1450となっており、間違い箇所は5箇所です。(私は2コしか見つけれませんでした。。。) Fig.1 間違い探し(左:正しい画像、右:間違い画像) まず、デフォルトの設定値で間違い箇所を検知した結果を下図に示します。左から学習画像(正しい画像)、検証画像(間違い画像)、検証画像+異常度を示しています。異常度の結果を見ると全体的に赤くなっており、異常度が高い、つまりほとんどの箇所で間違っていると出力してしまっています。また、Fig.1と比べると正方形に修正(リサイズ)されており、右側や左側に関しては異常検知できていないことがわかります。 Fig.2 デフォルト設定値での結果 リサイズされていることに関して、プログラムを見てみると画像の前処理として高さを256に(アスペクト比を維持して)リサイズしたあとに、CenterCropで224✕224のサイズで抽出していることがわかりました。これにより、周りの画像が切り取られてしまいますので(中心に検知対象が写っており、周りは背景になっているような画像を想定しているのだと思います。実際に製造現場ではそういった画像を扱うことが多いです。)、この前処理部分の修正として、CenterCropの処理をコメントアウトしていきます。 # 学習データの読み込み部 size= 224 class MVTecTrainDataset (ImageFolder): def __init__ (self, cls : str , size : int ): super ().__init__( root=DATASETS_PATH / cls / "train" ,◊ transform=transforms.Compose([ transforms.Resize( 256 , interpolation=transforms.InterpolationMode.BICUBIC), transforms.CenterCrop(size), transforms.ToTensor(), transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), ]) ) self.cls = cls self.size = size CenterCropの処理をコメントアウトした結果を以下に示します。画像全体に対して異常検知できるようになりましたが、相変わらず赤い箇所が多いままとなっています。 Fig.3 CenterCropの処理をコメントアウトした結果 これは、特徴数のサンプリングの割合(Memory Bankに保存するピクセルの割合)がデフォルトでは1%となっており、正常と学習しているピクセルのパターンが少ないため正しく判定できなかったと考えられます。このためサンプリングの割合を10%、50%、70%と変えて評価したいと思います。各ケースで異常度を算出した結果を下図に示します。これよりサンプリングの割合を増やすことで、部分的に異常度が高く(図中では赤く)なることが確認できます。サンプリングの割合を増やすことで画像中のピクセルを満遍なく学習でき、誤検知を減らすことができていると考えられます。 50%や70%のときの結果を見ると、①真ん中右より、②右側の桜の木の後ろ側、の2箇所で異常度が高くなっていることが確認できます。実際に、①さくらの木が無くなっている、②鹿が増えている、ので間違いを見つけることができました。 Fig.4 サンプリング割合を変えた場合の結果 さくらの木や鹿は比較的大きいので、他3箇所はもう少し小さい変化の可能性があります。ですので、次は前処理で行っているリサイズの割合をデフォルトの25%(正確には256/950=26.95%)から、50%、75%、100%に変更して評価します。サンプリング割合は50%としました。 リサイズの割合を変えた結果を以下に示します。これより、75%以上でより局所的に異常度が増加していますが、いずれのケースでも③間違いケースではなくなっているさくらの木の下側の通路沿い、④石階段の中断右側、で異常度が増加していることが確認できます。実際に、③では通路沿いに石が増えている、④階段に石が増えている、ので新たに間違いを②つ見つけることができました。あと1つですね。 [ ] Fig.5 画像解像度を変えた変えた場合の結果 次に、特徴量抽出器であるバックボーンのアルゴリズムを変えてることで間違いを見つけられるか確認していこうと思います。デフォルトでのバックボーンはEfficientNet-b0となっていますので、以下のアルゴリズムを検証しました(各アルゴリズムの詳細は割愛させていただきます)。分析条件は、サンプリング割合:50%、前処理:CenterCrop無し、リサイズ75%としています。 EfficientNet-b7(tf_efficientnet_b7) Efficientnet-v2-s(tf_efficientnetv2_s) Mobilenet-v3-large(tf_mobilenetv3_large_100) wide_resnet50_2 いずれのケースでもEfficientNet-b0と同様に、これまでに見つけた4箇所で異常度が増加していることがわかります。Efficientnet-v2-sでは局所的に異常度が増加している箇所が何箇所が増えています。 Fig.6 特徴量抽出器を変更した結果 これらの特徴量抽出器アルゴリズムの結果を眺めるて見ると、いずれも左側の桜の木の中段右横でわずかに異常度が増加していることがわかります。この部分を拡大して見てみると、確かに、花びらが1枚なくなっています!!! これにて、なんとか5コ全ての間違いを見つけることができました。 Fig.7 左側桜の木を拡大 ロバスト性評価 間違い探しの場合は、厳密に位置合わせができていますが、製造現場などの実際のユースシーンでは撮影時の角度や明るさといった撮像環境が学習時と検証時とで異なるケースのほうが多いかと思います。ここでは、ランダムに平行移動した画像を学習画像とした場合に、間違いを検知できるか確認したいと思います。 学習させた画像例は以下の通り。 Fig.8 ロバスト性検証用学習画像 評価条件としては、サンプリング割合:50%、特徴量抽出器:EfficientNet-b7(Fig.6~9の中では、誤検知が少ないため)、前処理:CenterCrop無し、リサイズ75%)としました。間違い以外の箇所でも異常度が増加していますが、5箇所の間違い箇所でも異常度が増加しており、位置ズレに対してもロバストに検知できることがわかりました! Fig.9 位置ズレ検証結果 技術の価値と今後の可能性 今回、間違い探しを例題に、画像の異常検知技術であるPatchCoreを試し、各パラメータを調整した際の影響を評価しました。結果として5箇所全ての間違いを見つけることができましたが、パラメータの調整やパラメータを変えたモデルのアンサンブル適用などが必要でした。 試してみて、PatchCoreの特徴として、次のことがわかりました。 特徴量抽出器に学習済みモデルを活用できるので、異常検知には少量の画像でも精度が出せ、モデルの学習にも時間を要しない ピクセル単位での異常検知が可能 位置ズレに関してもロバストに検知可能で、実問題にも適用しやすい このことから、適用先として以下のようなタスクの対しても適用できると考えられます。 どういった状態で流れてくるかわからない製造ラインでの異常検知 形・色等が様々な農業での品質管理など 製品組み立て後の配線などの確認 カメラ画像を用いた、特定箇所での位置判定 一方で、画像を用いる際の異常検知の課題として、以下が残っていますので、引続き技術動向を調査して行きたいと思います。 画像に映らない箇所に対しては、検知のしようが無いので、複数のカメラでもれなく撮影する必要がある 位置に関するロバスト性は確認できたが、明るさといった違いにも過敏に反応することもあったので、ご検知を減らすためには室内で撮像環境を一定とする必要がある
アバター
初めまして、2023年11月にInsight Edgeにジョインしたデータサイエンティストの ヒメネス(Jiménez) です!スペイン出身です。 入社から数ヶ月しか経っていませんが、この短い期間の中で生成AIの案件に携わるうえ、海外案件でDSコンサルタントとしても活動しています。 元々数学者ですが、プログラミングが大好きで、得意とするアルゴリズム構築を仕事にしました。コンサルティング企業にもしばらく参画した結果、ビジネスも少しずつ理解できるようになり、自分の視野や能力が広がりました。 この機会にWeb scrapingについて紹介したいと思います。Web scraping(スクレイピング)は、Webサイトから自動的にデータを収集する技術であり、研究者、マーケター、データアナリストなどにとって貴重な情報源となっています。 この記事では、Web scrapingの基本から、Seleniumを使用した高度なテクニックまでを紹介し、特に知られざるコツとスクレイピングの倫理に焦点を当てます。私たちの目的は、読者がWeb上のデータを効率的かつ倫理的に収集する方法を理解し、実践できるようになることです。このプロセスを通じて、あらゆるレベルの技術者がWeb scrapingの力を最大限に引き出し、新たな知見を得る手助けをすることを目指しています。 目次 Web scrapingとは Web scrapingの倫理 Seleniumの紹介 導入 ブラウザ起動 Webページに移動 基本操作(入力・クリック) 中級 待機 人間らしい入力 ヘッドレス 上級 タブ移動 JavaScript実行 HTML取得 マルチスレッディング おまけ: Selenium + Kaggle API 前提条件 Kaggle APIの準備 SeleniumとKaggle APIの組み合わせ 活用例: Squadroのプレイ歴収集 まとめ Web scrapingとは Web scraping(スクレイピング)またはweb crawling(クローリング)とは、プログラムを用いてWebサイトからデータを抽出する技術です。このプロセスには、HTTPリクエストを送信してWebページを取得し、必要な情報を解析・抽出するためのコードが含まれます。スクレイピングは、ネット上に転がっている大量のデータを迅速に収集する必要がある研究、マーケティング分析、競合他社の価格監視など、さまざまな用途で利用されます。 Web scrapingの倫理 スクレイピングは便利な技術である一方で、倫理的な懸念も伴います。Webサイトの利用規約に違反すること、サーバーへの過度な負荷、個人情報の不適切な取得といった問題が発生する可能性があります。そのため、スクレイピングを行う際は、対象サイトの利用規約を確認し、必要に応じてサイトの管理者から許可を得ることが重要です。また、アクセス頻度を制限してサーバーへの負担を軽減するよう配慮する必要があります。 Seleniumの紹介 Seleniumは、Webアプリケーションのテスト自動化のためのフレームワークですが、web scrapingにも広く使用されています。ブラウザを自動操作することで、JavaScriptで動的に生成されたコンテンツを含むページからもデータを取得できます。 まず、Seleniumをインストールしましょう。 pip install selenium そして、 webdriver-manager もインストールしましょう。 webdriver-manager は、Selenium WebDriverのドライバを簡単に管理するためのライブラリです。このライブラリを使用することで、ブラウザドライバ(ChromeDriverやGeckoDriverなど)の自動ダウンロードと更新が可能になり、Seleniumを使ったテストやスクレイピングをスムーズに実行できます。 pip install webdriver-manager この手順により、SeleniumでWebブラウザを自動操作する準備が整います。 ブラウザを手動でダウンロードして使用を指定したい場合、次のリンクよりChromeとFirefoxの最新版がダウンロードできます: ChromeDriver (Chrome)、 GeckoDriver (Firefox)。 導入 まずは、Seleniumの世界へようこそと言うことから始めます。初心者でもスムーズに取り組めるよう、ブラウザの起動からWebページへの移動、そして基本操作(テキスト入力やクリック動作)まで、丁寧に解説していきます。ここをマスターすることで、あなたもWebサイトからの簡単なデータ抽出が可能になります。 1. ブラウザ起動 まずはSeleniumを使ってブラウザを立ち上げます。このコード例では、 webdriver_manager ライブラリを使って、適切なChromeDriverを自動的にダウンロードし、インストールします。 from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) これにより、Seleniumは常に最新のChromeDriverを使用してブラウザを操作できるようになります。 ブラウザが規定のサイズで立ち上がりますが、以下のコマンドでウィンドウの最大化が可能です。 browser.maximize_window() 2. Webページに移動 ブラウザが立ち上がったら、 duckduckgo.com に移動させます。コマンドが順序良く実行されるため、基本的には前のコマンド(この場合だと、ブラウザ起動)が完了するまで次のコマンドは待機します。 driver.get( 'https://duckduckgo.com/' ) 3. 基本操作 ここからは実際のweb scrapingが始まります。そのために、まずは今立ち上がっているブラウザのエレメント(ボタン、入力など)のないところを右クリックし、最後の選択肢「検証」を選びます。右側に今閲覧しているページのHTMLコードが見えます。(エラーメッセージも下部に表示されているかもしれませんが無視しても大丈夫です) テキスト入力: send_keys DuckDuckGoの真ん中にある検索バーにテキストを入力するために、検索バーの中で右クリックし、「検証」をクリックします。 すると、右側の検証画面に青でハイライトされるエレメントが見えます。ハイライトの部分をマウスでホバーすると逆に検索バーがハイライトされます。これで操作したいエレメントがちゃんと選択できているか確認できます。 検索バーの XPath を取得する必要がありますので、右側にハイライトされているエレメントにもう一度右クリックし、「Copy」>「Copy XPath」を選択します。 Seleniumでそのエレメントにテキストを入力するために以下のように書きます。 driver.find_element( 'xpath' , '//*[@id="searchbox_input"]' ).send_keys( 'insight edge 会社' ) テキストクリア: clear 時々テキストエレメントに既にテキストが記入されています(多くの場合記入例として)。その場合 send_keys を使うと入力するテキストが元々のテキストに上書きされず追加されます。内容を消して書き直すためには clear コマンドを使います。 driver.find_element( 'xpath' , '//*[@id="searchbox_input"]' ).clear() driver.find_element( 'xpath' , '//*[@id="searchbox_input"]' ).send_keys( 'insight edge 会社' ) ボタンクリック: click 検索ボタン(青い🔍)の XPath を上記同様に取得し、今度クリックの命令を書きます。 driver.find_element( 'xpath' , '//*[@id="searchbox_homepage"]/div[1]/div/button[2]' ).click() テキスト取得: text 表示された検索結果の一番上にInsight Edgeのホームページが現れます。「insight edge 会社」のテキストを取得しましょう。いつも同様に XPath をコピーします。 driver.find_element( 'xpath' , '//*[@id="r1-0"]/div[2]/h2/a/span' ).text ところで、「insight edge 会社」のエレメントはクリックできるエレメントにもなっていますので、前のステップと同様にクリックすることも可能です。ただし、指導のためリンク先のアドレスを取得してからページに移動しましょう。右側のエレメントで「insight edge 会社」のエレメントは h3 のラベルがありますが、その一層上のラベル a のもの(リンク)の XPath を取得します。 link_url = driver.find_element( 'xpath' , '//*[@id="r1-0"]/div[2]/h2/a' ).get_attribute( 'href' ) driver.get(link_url) Insight EdgeのWebサイトに着いたでしょうか? ブラウザを閉じる: quit 作業した後に必ずブラウザを閉じましょう。そうしないとメモリが解放されないままお化けのブラウザが生き続けます。 quit でブラウザを終了します。 driver.quit() これはSeleniumの基礎です。次のセクションでもうちょっと高度なテクニックを共有します。 中級 次のステップでは、Seleniumの使いこなし方を一段階引き上げます。ページの読み込み待機処理、人間のような入力を模倣する方法、そしてヘッドレスブラウザの活用法を学び、より効率的かつ正確にデータを収集する技術を習得します。中級レベルをクリアすることで、あなたのスクレイピングはより洗練されたものへと進化します。 1. 待機 Webページが時々ロードするのに時間がかかります。エレメントがロードされる前にアクセスしようとすると「エレメントが存在しない」エラーが挙がります。エレメントが現れるまで待つには以下のメソッドを使います。先にメソッドをインポートします。 from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC そしてもう一度上記の処理を実行します。ただし、今回は待機機能を追加します。 driver.get( 'https://duckduckgo.com/' ) driver.find_element( 'xpath' , '//*[@id="searchbox_input"]' ).send_keys( 'insight edge 会社' ) driver.find_element( 'xpath' , '//*[@id="searchbox_homepage"]/div[1]/div/button[2]' ).click() # 待機。指定したXPathが現れなければ10秒までしか待たないように指定しています。その場合Exceptionを拾うべきです。 element = WebDriverWait(driver, 10 ).until(EC.visibility_of_element_located((By.XPATH, '//*[@id="r1-0"]/div[2]/h2/a/span' ))) element.click() ※ visibility_of_element_located (対象エレメントの表示)の他、 presence_of_element_located (対象エレメントの存在)や element_to_be_clickable (対象エレメントのクリック可能性)など、ご自身のニーズに合ったメソッドを使うことができます。 2. 人間らしい入力 スクレイピングを悪用する人がいるため、Webサイトによっては機械らしい行為に対してアクセスをブロックすることがあります。しかし、我々のような良い使い方をする人にとっては不公平なハードルとして感じられるかもしれません。その場合、人間らしい入力を再現するとてもシンプルな関数を用いてハードルを超えることができます。 def fill_slowly (driver, xpath, keys): import time, random for key in keys: driver.find_element( 'xpath' , xpath).send_keys(key) time.sleep(random.uniform( 0.2 , 1 )) # 平均0.2秒の正規分布に従う休憩時間を寝かせます driver.get( 'https://duckduckgo.com/' ) fill_slowly(driver, '//*[@id="searchbox_input"]' , 'insight edge 会社' ) driver.find_element( 'xpath' , '//*[@id="searchbox_homepage"]/div[1]/div/button[2]' ).click() 人間らしい入力イメージ: 3. ヘッドレス 自動化を高速化するために、一度実装したコードをヘッドレス(非表示)モードにします。例えば、テキスト取得に使ったコードを再利用しますが、今回はブラウザを非表示にし、結果を print で出力します。ブラウザ表示の場合とブラウザ非表示の場合の速さを比較します。 from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options import time def compare_speed (headless= False ): options = Options() if headless: options.add_argument( '--headless' ) driver = webdriver.Chrome(service=Service(), options=options) driver.get( 'https://duckduckgo.com/' ) driver.find_element( 'xpath' , '//*[@id="searchbox_input"]' ).send_keys( 'insight edge 会社' ) driver.find_element( 'xpath' , '//*[@id="searchbox_homepage"]/div[1]/div/button[2]' ).click() text = driver.find_element( 'xpath' , '//*[@id="r1-0"]/div[2]/h2/a/span' ).text driver.quit() return text # 表示 start_time = time.time() text = compare_speed(headless= False ) print (f '取得したテキスト: {text}' ) print (f '経過時間: {time.time() - start_time:.2f} 秒' ) # 非表示 start_time = time.time() text = compare_speed(headless= True ) print (f '取得したテキスト: {text}' ) print (f '経過時間: {time.time() - start_time:.2f} 秒' ) ヘッドレスモードの方が速かったでしょうか? 上級 この章では、複雑なWebページからのデータ抽出に挑むための、高度なテクニックを学びます。タブの操作からJavaScriptの実行、HTMLの直接取得に至るまで、さらにはマルチスレッディングを駆使した高速化テクニックまで、上級者向けの知識とスキルを身につけ、より深いスクレイピングの世界を探求します。 1. タブ移動 場合によって、クリックする際に新しいタブが開かれます。タブを移動することも可能です。 driver.get( 'https://dirdam.github.io/apps.html#squadro' ) driver.find_element( 'xpath' , '//*[@id="squadro_description"]/a' ).click() driver.switch_to.window(driver.window_handles[ 1 ]) element = WebDriverWait(driver, 30 ).until(EC.presence_of_element_located((By.XPATH, '//*[@id="welcome_bga_text"]' ))) print (element.text) driver.window_handles がタブを管理するリストになりますので、最初のタブの番号は 0 で、新しく開かれたタブの番号は 1 です。 タブを閉じる: close driver.quit() を使うとブラウザ自体が閉じてしまいます。タブだけを閉じるのに driver.close() を使います。 注意しなければならないことは、タブを閉じると再びアクセスできないので、引き続きスクレイピング作業をする場合、開いているタブに切り替える必要があります。例えば、規定のタブに戻るのに 0 番目のタブに戻ります。 driver.close() driver.switch_to.window(driver.window_handles[ 0 ]) 2. JavaScript実行 Webサイトに直接JavaScriptを投げることが可能です。便利な使用例がいくつか考えられます。 新規タブを開く(そのまま) ブラウザ上で新しいタブを開きます。(内容は空です) driver.execute_script( "window.open('','_blank');" ) 新規タブを開く(特定のURL) ブラウザ上で新しいタブを開きます。(内容は特定したURL) driver.execute_script( "window.open('https://duckduckgo.com');" ) メタデータ取得 例えば、ページタイトルの取得。 driver.execute_script( 'return document.title;' ) 3. HTML取得 全HTML取得 Webページのソースコード(HTML)を取得することも可能です。ページ全体のソースコードを取得するには以下のコードを使用します。 page_source = driver.page_source print (page_source) 一部HTML取得 一方、特定のXPathのエレメント配下のHTMLのみ抽出したい場合、 excute_script メソッドを活用して以下のコードで取得可能です。 driver.get( 'https://duckduckgo.com/' ) parent_element = driver.find_element( 'xpath' , '//*[@id="__next"]/main/article/div[1]/div[1]/div/section[2]' ) html_code = driver.execute_script( "return arguments[0].outerHTML;" , parent_element) 読み込んだHTMLを保存したい場合、HTMLファイルとして保存(再生)できます。 with open ( "temp.html" , "w" , encoding= "utf-8" ) as file : file .write(html_code) また、そのファイルを開いて、そのHTMLに対して改めてSelenium経由でスクレイピングすることも可能です。URLではなく、ローカルHTMLを開くのに以下使います。 import os driver.get( "file://" + os.path.abspath( "temp.html" )) 4. マルチスレッディング 異なるURLから何度もデータを取得する場合、一つ一つ順番に処理を行う方法もありますが、効率を考えるとマルチスレッディングを使用することがより効果的です。マルチスレッディングを使うことで、複数の処理を並行して実行できるため、全体の処理時間を大幅に短縮することが可能になります。 例えば、Wikipediaの複数のオリンピックの記事から、それぞれの参加人数の情報を取得したい場合、以下のようなコードで取得することができます。まずはオリンピック記事から参加人数を抽出する関数を用意します。 def find_participants (url): driver = webdriver.Chrome(service=Service()) driver.get(url) for i in range ( 1 , 2 + 1 ): # テーブル番号は1か2のどちらか for j in range ( 6 , 7 + 1 ): # 行番号は6から7のどちらか try : element = driver.find_element( 'xpath' , f '//*[@id="mw-content-text"]/div[1]/table[{i}]/tbody/tr[{j}]/th' ) if element.text == '参加人数' : # 参加人数の行を見つけたら、隣の列のテキストを返す participants = driver.find_element( 'xpath' , f '//*[@id="mw-content-text"]/div[1]/table[{i}]/tbody/tr[{j}]/td' ).text driver.quit() return participants except : continue return '見つかりませんでした' 以下のコードで情報を順番に取得します。 titles = [ '2020年東京オリンピック' , '2016年リオデジャネイロオリンピック' , '2012年ロンドンオリンピック' , '2008年北京オリンピック' ] participants = {} start_time = time.time() for title in titles: participants[title] = find_participants(driver, f 'https://ja.wikipedia.org/wiki/{title}' ) print (f '経過時間: {time.time() - start_time:.2f} 秒' ) print (participants) ただし、それぞれの記事へのアクセスは独立しているので、それぞれのデータを並行して取得するようにします。 from multiprocessing.pool import ThreadPool start_time = time.time() titles = [ '2020年東京オリンピック' , '2016年リオデジャネイロオリンピック' , '2012年ロンドンオリンピック' , '2008年北京オリンピック' ] participants = {} pool = ThreadPool(processes= len (titles)) # 同時に実行するスレッド数を指定 threads = [] # スレッドとタイトルの対応を保存するリスト for title in titles: t = pool.apply_async(find_participants, (f 'https://ja.wikipedia.org/wiki/{title}' ,)) threads.append((title, t)) pool.close() # プールを閉じる。これ以降新しいスレッドは作成しない pool.join() # 全てのスレッドが終了するまで待つ for title, thread in threads: try : participants[title] = thread.get() # コールの結果を取得 except Exception as e: print (f 'エラー: {e}' ) print (f '経過時間: {time.time() - start_time:.2f} 秒' ) print (participants) 私の実験環境で順番に処理すると処理が約30秒かかり、マルチスレッディングだと10秒以内に終了します。 ご覧の通り、節約できる実行時間がとても大きいです。取得する情報が多ければ多いほど、このようなアプローチがもっと大事になってきます。並行して実行するブラウザが多ければ多いほどメモリも使いますのでそれとの兼ね合いで調整する必要があります。 おまけ: Selenium + Kaggle API 最後に、SeleniumとKaggle APIを組み合わせて、自動的にデータセットをKaggleにアップロード(更新)する方法について説明します。下記で習うことを活かして、記者がオンラインボードゲームサイトのSquadroというゲームの各々のプレイの情報をスクレイピング経由で収集してデータセットとしてKaggleにアップロードしています。Kaggleは、データサイエンスと機械学習のコミュニティで広く使用されており、自分のデータセットを公開して他の研究者と共有することが可能です。 ※ データセットを作成する場合、手動で初回のアップロードを行なってください。こちらで見せるコードはデータセットの自動更新用のコードです。 1. 前提条件 Kaggleアカウントが必要です。まだお持ちでない場合は、 Kaggle でアカウントを作成してください。 Kaggle APIのトークンを取得します。Kaggleのアカウント設定ページから「Create New API Token」をクリックしてダウンロードします。ダウンロードした kaggle.json ファイルは、安全な場所に保存してください。 2. Kaggle APIの準備 Kaggle APIを使用するためには、まず kaggle パッケージをインストールする必要があります。 pip install kaggle 次に、ダウンロードした kaggle.json ファイルを、Kaggle APIの設定ファイルが読み込まれるディレクトリに配置します。通常、このディレクトリは ~/.kaggle です(LinuxおよびMacOSの場合)。Windowsの場合は、 C:\Users\<Windows-username>\.kaggle\ になります。 mkdir ~/.kaggle cp path/to/kaggle.json ~/.kaggle/kaggle.json chmod 600 ~/.kaggle/kaggle.json 3. SeleniumとKaggle APIの組み合わせ Seleniumを使ってスクレイピングを行います。毎日更新される情報を集めるのに便利です。スクレイピングしたデータを dataset_path 配下に保存します。 # スクレイピング df = scraping_call() # 適当にデータを収集する関数 # データ保存 dataset_path = '/path/to/your/dataset' # データセットのローカルディレクトリを指定 dataset_name = 'my_data.csv' # データファイル名 df.to_csv(f '{dataset_path}/{dataset_name}' ) # 保存関数 Kaggle APIを呼び出すのに必要なライブラリをインポートします。そして、Kaggle APIに認証します。 from kaggle.api.kaggle_api_extended import KaggleApi api = KaggleApi() api.authenticate() Kaggle APIを使用して、データセットをアップロード(更新)します。 # データセットの新しいバージョンを作成 api.dataset_create_version(folder=f '{dataset_path}/' , version_notes= "メモ" ) このプロセスを使用することで、データセットの管理と更新を効率的に行うことができます。Seleniumを介して追加のWeb操作を自動化することと組み合わせることで、完全に自動化されたデータアップロードのワークフローを構築することが可能です。 重要なのは、自動化プロセスを利用する際にスクレイピングの倫理的な実装を行うこととKaggleの利用規約に違反しないようにすることです。 4. 活用例: Squadroのプレイ歴収集 このセクションの冒頭で述べた通り、上記を実装し、記者が Squadro というボードゲームの デジタル版 でプレイされるゲームの情報を2日に1回ぐらい集めて自動的に こちらのデータセット を更新しています。更に、そのデータセットを自動でダウンロードし、読み込んでデータを可視化する Streamlitアプリ も開発しました。 まとめ この記事では、スクレイピングの基本から応用まで幅広くカバーしました。スクレイピングは、Webサイトからデータを抽出するための強力な技術であり、研究、マーケティング分析、競合他社の価格監視など様々な用途で利用されています。しかし、スクレイピングを行う際には、Webサイトの利用規約に違反しないよう注意し、サーバーへの負荷を最小限に抑える必要があります。 スクレイピングは、適切に行えばデータ収集の効率を大幅に向上させることができますが、技術的な面だけでなく、倫理的な観点からも慎重に取り組む必要があることを忘れてはいけません。皆さんも是非良いスクレイピングを実装してみてください!
アバター
目次 はじめに クリーンアーキテクチャとは 例として考えるアプリケーション 生成AIアプリにおける難しさ 実際に作ってみる 実装を差し替えてみる まとめ はじめに こんにちは、InsightEdgeの開発チームに参画させていただいています伊藤です。 InsightEdgeでは現在、LLM/生成AIを用いたアプリ開発を多く手掛けています。 私もOpenAI等の生成AIを使ったアプリケーションの実装に関わることが増えてきており、 ある程度理解が進んできたところで、改めてアーキテクチャを洗練させたいと思うようになりました。 この記事では生成AIを組み込んだアプリを構築する際の、クリーンアーキテクチャを考えていきたいと思います。 クリーンアーキテクチャとは まずは、クリーンアーキテクチャについて軽く振り返りましょう。 クリーンアーキテクチャではソフトウェアの理解・開発・デプロイ・運用・保守を容易にするために、フレームワークやDB・UI・外部サービスなどへの依存を最小にします。 この時の下記のような多層構造に分割し、それぞれの層に役割に合わせたクラスを配置していきます。 The Clean Architecture より抜粋 ここで登場する層はそれぞれ下記のような役割を持っています Entities この層にはビジネスルールやビジネス上の概念を表すクラスを格納します。 ドメイン駆動開発(DDD)やオニオンアーキテクチャ的にいうとドメインモデル/ドメインサービスのレイヤに該当します。 Use Cases アプリケーション固有のビジネスが含まれます。 Use Case層のクラスはEntity層のクラスを使ってアプリケーションの処理フローを実行します。 Controllers/Presenters Controllerは入力をUse Caseに合わせた形に変換した上で、Use Caseを実行します。 PresenterはUse CaseがUIへの出力をする時に呼び出されるクラスになります。 Gateways GatewayはEntity層/Use Case層で扱うクラスをDBや外部サービスに合わせた形に変換し、永続化やAPIコールなどを行います。 その外側 DBそのものやUI、外部のサービスなどはすべてControler/Presenter/Gatewayを通じてやり取りするものになります。 これらの各層に含まれるコードは、必ずより内側の層のコードだけに依存するようにすることで、 サービス/ビジネスの本質的なロジック部分を表現するコードが、フレームワークやUIといった部分の変更に 影響を受けないように構成します。 詳細についてはオリジナルのWebサイトや書籍のほか、いろいろが出ていますのでここでは割愛します。 参考 The Clean Architecture 書籍:Clean Architecture 達人に学ぶソフトウェアの構造と設計 例として考えるアプリケーション アーキテクチャを考える上での想定アプリケーションとして、ここではRAGを加えたChatGPTのようなアプリケーションを作るものとします。 ユーザはブラウザでアクセスする 自然言語で質問を入力すると、自然言語で回答が表示される 回答作成時には、それまでのやり取りなどの文脈が考慮される 回答作成時には、あらかじめシステムに登録しておいた関連情報が加味される 回答は生成完了してから一度に表示されるのではなく、生成された文字から逐次表示される 生成AIアプリにおける難しさ 前述のようなアプリをクリーンアーキテクチャで作成しようとした際に、特に下記の点は検討が必要でした。 どの要素がEntityになるのか ストリーミング形式のアウトプットはクリーンアーキテクチャで実現できるのか それぞれ、どんな課題に対して、どのように考えていったのかを説明していきたいと思います。 どの要素がEntityになるのか 論点 クリーンアーキテクチャのEntity層にはアプリの核となるビジネスロジックが置かれます。 一方で生成AIアプリ開発の中心となるのは、どうやってGPT等の生成AIに望むクオリティのアウトプットを生成してもらうかだと思います。 しかし、生成AIそのものはAPI経由で呼び出すのでアプリ外にあり、これはクリーンアーキテクチャで言うところのGatewayの先の部分のはずです。 では、どんな部分をEntityとして依存関係の中心に置くべきなのでしょうか。 考え方 今回想定しているアプリが提供する機能は「質問を受けて、今の文脈に合わせて、関連情報を使って、回答を生成する」です。 ここから考えるとEntityとして表現するべき概念は 質問 ・ 文脈 ・ 関連情報 ・ 回答 であるはずです。 ここにはプロンプトなどの生成AI的な要素は出てきませんが、そもそもAIは実装/実現手段の詳細なので、 適切な回答を返してくれるのであれば、背後にあるのはGPTでもGeminiでも、ただのデータベースでも良いということです。 これまで開発のたびに色々と工夫してきたプロンプトは本質的な部分ではないのかと一瞬ショックを受けましたが よく考えるとパフォーマンス重視のサービスでSQL部分のチューニングをしているようなものですね。 どんなにチューニングが大切なアプリであってもSQLそのものがアーキの中心とはならない、というのと同じです。 ストリーミング形式のアウトプットはクリーンアーキテクチャで実現できるのか 論点 GPTを使ったアプリケーションでは、UX向上のためにGPTからの回答を逐次画面に表示するUIが一般的だと思います。 これを行うためにはアプリのフロントエンドとバックエンドがHTTPストリーミングのような形で通信しなければなりません。 このとき、GPTの出力からアプリのフロントエンドまでの間の、 回答 に関連するすべてのクラスがストリーミング形式のデータに対応できている必要があります。 しかし、UIがストリーミング出力だからといって、そこに合わせてUse Case層の仕様を変更することはクリーンアーキテクチャの重視している依存関係が逆転してしまっている気がします。 クリーンアーキテクチャの考え方に沿ったままストリーミング型のUIに対応するにはどうしたらよいのでしょうか。 考え方 上記について考えている中で、自分の捉え方に思い込みがあることに気づきました。 変数の値が決まってから返却するのが「普通」で、ストリーミング出力は「UIに合わせた特殊な形」だと考えていたのです。 pythonは言語自体にGeneratorやPromise等の「後で値が決まる仕様」の型を持っています。 使うプログラミング言語で表現できるのであれば 回答 を「逐次生成されるもの」として定義してしまってもよいのではないでしょうか。 回答 という概念が、そもそも逐次生成される性質を持っている、という捉え方です。 今回は回答(Answer)というEntityをGeneratorの一種として定義しました。 Answer: TypeAlias = Generator[ str , None , None ] この書き方もEntityを Generator[str, None, None] 型なんかにしてしまってよいのかという違和感があったのですが、 list[str] 等で表現するのと違いはないはず、と思い納得することにしました。 回答 の持つ性質をコードとして表現した時に、たまたま Generator という型が一番適切だったのだと解釈します。 実際に作ってみる 上記を踏まえ、実装をしてみます。 Entity・Use Case・Gateway・Controller/Presenterそれぞれを見ていきましょう。 全体の構成は以下のようになっています。 ドメイン駆動開発(DDD)をする場合だと domain や repository といった表現の方がよく使われるかもしれませんが、 今回はクリーンアーキテクチャとの対応がわかりやすいように entity や gateway といった表現に統一しています。 src ├── entity: エンティティ層 │   ├── answer: 回答モデル │   ├── context: 文脈モデル │   ├── knowledge: 背景知識モデル │   └── question: 質問モデル ├── usecase: ユースケース層 │   ├── knowledge: 背景知識モデル │ └── chat: 質問回答ユースケース ├── adapter: アダプタ層 │   ├── controller: Use Caseを起動するためのController実装 │   ├── gateway: 各種Gateway/Repositoryの実装 │   │   ├── answer: AnswerGateway実装 │   │   ├── context: ContextGateway実装 │   │   └── knowledge: KnowledgeGateway実装 │   └── presenter: 出力をハンドリングするPresenter実装 └── main_*.py: 起動のためのエントリポイント Entity定義 まずコアとなるEntitiy層には、前述した下記の4要素と、その取得を担当するGatewayの抽象クラスを作成します。 質問(Question) 文脈(Context) 背景知識(Knowledge) 回答(Answer) 質問(Question)関連 question.py Question: TypeAlias = str """ 質問文を表す型。ただの文字列 """ 文脈(Context)関連 context.py ContextId: TypeAlias = int """ これまで文脈を取得するためのキーにする値を表す型。今回は整数として定義する。 実際はユーザやセッションに関する値をキーする想定。 """ Context: TypeAlias = str """ 文脈を表す型。今回はただの文字列として定義する。 実際は過去の会話の履歴などを含むことが考えられる。 "" context_gateway.py class ContextGateway (metaclass=ABCMeta): """文脈(Context)を取得するためのGateway""" @ abstractmethod def get_context (self, context_id: ContextId) -> Context: """ContextIdに基づくContextを取得する""" pass 背景知識(Knowledge)関連 knowledge.py Knowledge: TypeAlias = str """ 背景知識1つを表す型。今回はただの文字列として定義する。 実際は参照先や、関連度のスコアなどを含むクラスにすることが考えられる。 """ Knowledges: TypeAlias = list [Knowledge] """ 複数の背景知識を表す型 """ knowledge_gateway.py class KnowledgeGateway (metaclass=ABCMeta): """背景知識(Knowledge)を取得するためのGateway""" @ abstractmethod def find_knowledges (self, question: Question) -> Knowledges: """Questionに関連する背景知識(Knowledge)を取得する""" pass 回答(Answer)関連 answer.py Answer: TypeAlias = Generator[ str , None , None ] """ 回答を表す型。文字列が逐次生成されるものとして定義する。 """ answer_gateway.py class AnswerGateway (metaclass=ABCMeta): """回答(Answer)を取得するためのGateway""" @ abstractmethod def get_answer ( self, question: Question, context: Context, knowledges: Knowledges ) -> Answer: """Question/Context/Knowledgeに基づくAnswerを取得する""" pass Gateway実装 (ダミー版) まずは動作確認するためにダミー版のGateway実装を作成します。 それぞれ固定値を返すようになっています。 今回は以下の例でダミー文を作成しました。 文脈: 「組織のデータを活用するためのOpenAIアプリの開発で困っている」 背景知識: 「InsightEdge社はOpenAIを使ったアプリ開発ができる」 「InsightEdge社にはデータサイエンスの専門家がいる」 回答: 「InsightEdgeを検討してみてはいかがでしょうか?」 dummy_context_gateway.py class DummyContextGateway (ContextGateway): """ ダミーの文脈情報を返すContextGateway """ def get_context (self, context_id: ContextId) -> str : return Context( "組織のデータを活用するためのOpenAIアプリの開発で困っている" ) dummy_knowledge_gateway.py class DummyKnowledgeGateway (KnowledgeGateway): """ ダミーの背景知識を返すKnowledgeGatway """ def find_knowledges (self, question: Question) -> Knowledges: return [ Knowledge( "InsightEdge社はOpenAIを使ったアプリ開発ができる" ), Knowledge( "InsightEdge社にはデータサイエンスの専門家がいる" ), ] dummy_answer_gateway.py class DummyAnswerGateway (AnswerGateway): """ ダミーの回答を返すAnswerGateway """ def get_answer ( self, question: Question, context: Context, knowledges: Knowledges ) -> Generator[ str , None , None ]: yield f "「{context}」という文脈において、 \n " sleep( 1 ) yield f "「{question}」という質問に対して、 \n " sleep( 1 ) for knowledge in knowledges: yield f "「{knowledge}」" sleep( 1 ) yield "という背景知識をもとに、 \n " sleep( 1 ) yield "ダミーの回答をします。 \n " sleep( 1 ) yield "InsightEdgeを検討してみてはいかがでしょうか? \n " get_answer() の戻り値は Generator である Answer として親クラスで定義されていますが、こちらの実装は str 型を yield することで定義を満たしています。 なお、今回はストリーミングの動きがわかるように sleep() を挟んでいます。 Use Case/Port実装 続いて、Use Case層を作ります。 ここではEntityを使用してアプリとしての処理フローを定義するUse Caseと、 その出力の仕方を定義するOutputPortを作成します。 後ほど出てくるControllerはこのUse Caseを呼び出す形となり、 PresenterはOutputPortを継承したクラスとして、Use Case層の定義に従って実装することになります。 これはクリーンアーキテクチャの図では右下の部分で表現されている部分となります。 図ではUseCaseの親となるInput Portも記載されていますが、今回は省略します。 図に厳密に従ってUse Case Input Portを作成しControllerからの依存がそちらに向かう構造にすることで、 Use Caseの変更をした場合にControllerに影響しにくくできますが、 把握しておくクラス/依存関係を減らすことの方がメリットが高いと考えており 自分の場合はInput Portは作らない、という選択をすることが多いです。 では、実装の中身です。 まずは「質問を受けて回答を生成する」というユースケースを実現するためのクラスを作ります。 chat_usecase.py class ChatUsecase : """ 質問に対する回答を返すユースケース """ def __init__ ( self, context_gateway: ContextGateway, knowledge_gateway: KnowledgeGateway, answer_generator: AnswerGateway, ) -> None : """ 使用するGatewayをセットする """ self.context_gateway = context_gateway self.knowledge_gateway = knowledge_gateway self.answer_generator = answer_generator def execute ( self, question: Question, context_id: ContextId, output_port: ChatUsecaseOutputPort, ) -> None : """ ユースケースを実行する """ # 文脈を把握する context: Context = self.context_gateway.get_context(context_id) # 背景知識を取得する knowledges: Knowledges = self.knowledge_gateway.find_knowledges(question) # 回答を作成する answer = self.answer_generator.get_answer(question, context, knowledges) # 回答を出力する output_port.emit(answer) そしてUse Caseのアウトプットを受け取るOutput Portを定義します。 このOutputPortを実装したPresenterであれば、アウトプットの仕方はなんでも良いということになります。 chat_usecase_output_port.py class ChatUsecaseOutputPort (metaclass=ABCMeta): """ ChatUsecaseの出力口の定義""" @ abstractmethod def emit (self, answer: Answer) -> None : """Answerを出力用に受け取る""" pass Presenter/Controller(コマンドライン実行用) 最後にPresenterとControllerを作成します。 まずはシンプルにコマンドラインで呼び出す想定で作ってみます。 Controllerは標準入力から質問を受け付け、Entityである Question 型に変換してから、ユースケースを起動します。 cli_chat_controller.py class CliChatController : # CLI上で質問を受け取り、ユースケースを実行するコントローラ def __init__ (self, usecase: ChatUsecase, presenter: ChatUsecaseOutputPort): # 実行するユースケース self.usecase = usecase # ユースケースのOutputPortの実装 self.presenter = presenter def handle (self): # コンテクストIDを取得する(今回はサンプルなので固定) contextId = 1 # 質問文を受け取る question_str = input ( "質問を入力してください: " ) question: Question = Question(question_str) # ユースケースを実行する self.usecase.execute(question, contextId, self.presenter) # 出力はPresenterが担当するので、ここでは何もしない 続いてPresenterを作ります。 CLIでの操作を想定し標準出力への表示をするものを用意します。 ChatUsecaseOutputPort の実装(の1つ)になります。 stdout_chat_presenter.py class StdoutChatPresenter (ChatUsecaseOutputPort): """標準出力にAnswerを出力するPresenter""" def emit (self, answer: str ) -> None : """Answerを出力用に受け取る""" # Answerの内容を逐次出力する for answer_chunk in answer: sys.stdout.write(answer_chunk) sys.stdout.flush() 実行 さてここまでの実装を繋げて動かしてみます。 エントリポイント エントリポイントとして、手動でDependency Injection(以下DI)した上で、コントローラを呼び出す処理を作ります。 main_cli.py """ CLI上で実行するためのエントリーポイント """ # DI済みのUse Caseを作成する context_gateway = DummyContextGateway() knowledge_gateway = DummyKnowledgeGateway() answer_gateway = DummyAnswerGateway() usecase = ChatUsecase(context_gateway, knowledge_gateway, answer_gateway) # DI済みのコントローラを作成する presenter = StdoutChatPresenter() controller = CliChatController(usecase, presenter) # コントローラを呼び出す controller.handle() 実行結果 上記のエントリポイントからアプリを動かすとこのようになります。 DummyAnswerGateway によって生成された結果が、標準出力に逐次出力されています。 さて、これでアプリの全体像は完成しました。 いわゆる生成AIアプリであっても、アーキテクチャの観点ではAIそのものからは切り離されているということが改めて確認できたと思います。 実装を差し替えてみる ここからはGateway/Controller/Presenterの「詳細」を変えていきましょう。 クリーンアーキテクチャを目指したことで変更に強いものになっているでしょうか。 AnswerGatewayをOpenAI利用版にしてみる まずは回答の生成を実際にAzure OpenAIを使って行うようにしてみます。 DummyAnswerGateway を置き換えるための別クラスを作ります。 親クラスは AnswerGateway です。 openai_answer_gateway.py class OpenAIAnswerGateway (AnswerGateway): """ OpenAIを用いて回答を生成するAnswerGateway""" def __init__ (self): api_key = os.getenv( "AZURE_OPENAI_API_KEY" , "" ) api_base = os.getenv( "AZURE_OPENAI_API_BASE" , "" ) api_version = os.getenv( "AZURE_OPENAI_API_VERSION" , "" ) self.deployment_name = os.getenv( "AZURE_OPENAI_DEPLOYMENT_NAME" , "" ) # AzureOpenAIクライアントを用意する self.client = AzureOpenAI( api_key=api_key, api_version=api_version, azure_endpoint=api_base, ) def get_answer ( self, question: Question, context: Context, knowledges: Knowledges ) -> Answer: # OpenAIに質問を投げて回答を取得する response = self.client.chat.completions.create( model=self.deployment_name, messages=[ { "role" : "user" , "content" : [ { "type" : "text" , "text" : f "{context}という文脈において、" + f "{knowledges}という背景知識をもとに、" + f "{question}という質問に対して回答してください。" , } ], }, ], stream= True , ) # OpenAIからの回答を逐次返す for chunk in response: choices = chunk.choices if choices and choices[ 0 ].delta and choices[ 0 ].delta.content is not None : yield str (chunk.choices[ 0 ].delta.content) # strをyieldすることで、Answer型つまりGenerator[str, None, None]型を満たす こちらの get_answer() も DummyAnswerGateway の時と同じく str 型を yield しているので、戻り値の型 Answer 型に合致しています。 エントリーポイント エントリーポイントは、 先ほどのCLI入力/表示用の実装から、 DummyAnswerGateway の代わりに上記の OpenAIAnswerGateway を使って実行するよう、DIしている1行だけを変更します。 今回は簡易化のため ContextGateway と KnowledgeGateway はダミーのままとします。 実際にRAGを行う場合は KnowledgeGateway の部分にEmbeddingやVectorStoreを実装することになると思います。 main_cli_openai.py """ CLI上で実行するためのエントリーポイント """ # DI済みのUseCaseを作成する context_gateway = DummyContextGateway() knowledge_gateway = DummyKnowledgeGateway() answer_gateway = OpenAIAnswerGateway() # <= OpenAIAnswerGatewayを使う usecase = ChatUsecase(context_gateway, knowledge_gateway, answer_gateway) # DI済みのコントローラを作成する presenter = StdoutChatPresenter() controller = CliChatController(usecase, presenter) # コントローラを呼び出す controller.handle() 実行結果 上記のエントリポイントから実行してみます。 はい、これだけで実際にAzure OpenAIを組み込んだアプリに変わりました。 以下のポイントのおかげで修正箇所が非常に限定されています。 各Gatewayの実装はEntity層で定義した抽象Gatewayに従うようになっている Use Caseも抽象のGatewayの定義に依存している 実際にどの実装を使うかはDIによって決まる Presenter/ControllerをHTTPストリーム版にしてみる 次はControllerとPresenterを差し替えて、APIとして動作する形にしてみましょう。 ControllerはFastAPIを使ってHTTPリクエストを受け取るものを作成します。 http_chat_controller.py class HttpChatController : # Httpリクエストで質問を受け取り、ユースケースを実行するコントローラ def __init__ (self, usecase: ChatUsecase, presenter: AbstractHttpChatPresenter): # 実行するユースケース self.usecase = usecase # ユースケースのOutputPortの実装 self.presenter = presenter # FastAPIのルーター設定 self.router = APIRouter() self.router.add_api_route( "/{context_id}" , self.handle, methods=[ "GET" ]) def handle (self, context_id: int , q: str ) -> Response: # 質問文を取得する question = Question(q) # ユースケースを実行する self.usecase.execute(question, context_id, self.presenter) # レスポンスを返す return self.presenter.get_response() CliChatController とほぼ一緒ですね。 Presenterとしては ChatUsecaseOutputPort を継承しつつ Response の形で取り出すための get_response() を持たせたいので、 その点を表現した抽象クラスをもう一段階挟みます。 class AbstractHttpChatPresenter (ChatUsecaseOutputPort): """HTTP経由でAnswerを出力するPresenterの抽象クラス""" @ abstractmethod def emit (self, answer: Answer) -> None : """Answerを出力用に受け取る""" pass @ abstractmethod def get_response (self) -> Response: """HTTPレスポンスの形で回答を返す""" pass Presenterの実装は emit() で設定された Answer を HttpStream で返却するものを用意します。 http_stream_chat_presenter.py class HttpStreamChatPresenter (AbstractHttpChatPresenter): """HTTPストリーミングでAnswerを出力するPresenter""" def emit (self, answer: Answer) -> None : """Answerを出力用に受け取る""" self.answer = answer def get_response (self) -> Response: """HTTPストリームの形で回答を返す""" response = StreamingResponse(self.answer, media_type= "text/event-stream" ) return response こちらはFastAPIの定義する StreamingResponse を使っています。 Entity層で定義した Answer が Generator 型なので、簡単にストリーミング対応ができます。 エントリーポイント 上記のControllerをFastAPIで実行するようなエントリポイントを用意します。 変更箇所は、Controler/Presenterの差し替えとサーバの起動処理の追加です。 main_http_openai.py """ Httpサーバ上で実行するためのエントリーポイント """ # DI済みのUseCaseを作成する context_gateway = DummyContextGateway() knowledge_gateway = DummyKnowledgeGateway() answer_gateway = OpenAIAnswerGateway() # OpenAIを使って回答を行うAnswerGateway usecase = ChatUsecase(context_gateway, knowledge_gateway, answer_gateway) # DI済みのコントローラを作成する presenter = HttpStreamChatPresenter() controller = HttpChatController(usecase, presenter) # コントローラをサーバ上で呼び出し待ち状態にする app = FastAPI() app.include_router(controller.router) uvicorn.run(app, host= "127.0.0.1" , port= 8000 ) 実行結果 上記を起動した上で、ブラウザでアクセスしてみます。 無事にAPI化もできました。 アプリの処理そのものはUse Case層がEntity層のクラスを用いて行う Controller/PresenterはUse Case層のクラスに従う この2点のおかげで、入出力をCLIからHTTP形式の変更する際に、Use Case層/Entity層に影響を与えることなく行えています。 ストリーミングではないHTTPレスポンスにしたい場合はどうするのか ここまでは内部のEntityも出力のPresenterも回答を逐次処理するようにしていました。 では「やっぱりストリーミングではなく一度のHTTPレスポンスで回答を返却したくなった」という場合はどのような影響があるでしょうか。 出力形式が変わるので、新たにPresenterを用意します。 HTTP Responseを返す get_response() を持っていて欲しいので、 AbstractHttpChatPresenter の子クラスとして作成します。 http_response_chat_presenter.py class HttpResponseChatPresenter (AbstractHttpChatPresenter): """HTTPレスポンスでAnswerを出力するPresenter""" def emit (self, answer: Answer) -> None : """Answerを出力用に受け取る""" self.answer = answer def get_response (self) -> Response: """HTTPレスポンスの形で回答を返す""" result = "" for chunk in self.answer: result += chunk print (result) response = Response(content=result, media_type= "text/plain" ) return response Entity層の Answer は Generater 型のまま、生成される回答をすべてPresenter内で受け取ったあとに1回のHTTPレスポンスを返すようにしています。 エントリーポイント 呼び出しはHTTP経由で変わらないので、 HttpChatController のまま、PresenterだけDIで変更します。 """ Httpサーバ上で実行するためのエントリーポイント """ # DI済みのUseCaseを作成する context_gateway = DummyContextGateway() knowledge_gateway = DummyKnowledgeGateway() answer_gateway = OpenAIAnswerGateway() usecase = ChatUsecase(context_gateway, knowledge_gateway, answer_gateway) # DI済みのコントローラを作成する presenter = HttpResponseChatPresenter() # <= 通常レスポンスを返すPresenterに変更 controller = HttpChatController(usecase, presenter) # コントローラをサーバ上で呼び出し待ち状態にする app = FastAPI() app.include_router(controller.router) uvicorn.run(app, host= "127.0.0.1" , port= 8000 ) これでストリーミングではなく、一括で回答が返却される形になりました。 Entityである Answer が Generater として定義されていても、Presenterが適切に処理することで 外からはただの str であるかのように表示に使うことができます。 余談 今回は実施しませんでしたが、バッチ処理で回答を生成して何かに格納したい場合も、 バッチ処理用のControllerとPresenterを用意することで対応できるはずです。 ただしその場合には、Use Caseとして表現するべきものが本当にユーザ操作とバッチ処理で同じなのかを検討する必要があります。 まとめ 今回は生成AIアプリの内部をクリーンアーキテクチャで構成する方法について検討しました。 生成AIを使ったアプリであっても、クリーンアーキテクチャの考え方に沿って層を分け、 アプリが扱う要素/概念を生成AI自体の使い方と切り離して捉えて抽象化し、 依存関係が一方向に向かうように構成することで 回答生成方法の変更 アプリ起動方法の変更 出力形式の変更 などを、影響範囲を最小にした上で実施できることが確認できました。 また 回答(Answer) を Generator としての性質を持ったものとしてモデル化するという工夫をすることで、 生成AIアプリケーションでよく使う逐次表示形式と、Request/Response形式の両方の出力に対応できることが分かりました。 感想 今回クリーンアーキテクチャを目指す設計をしたことで 生成AIアプリケーションについて自分が持っていたメンタルモデルが、よりシンプルなものに更新されたと感じます。 新たにアプリを作るときよりも、既存のアプリの方が概念やフローが分かっていて検討しやすいと思いますので 既存のアプリについてクリーンアーキテクチャで作ったらこうなるのでは、というのを一度考えてみるのも面白いかもしれません。
アバター
はじめに 実施背景 概要 事例展示 トークセッション 相談窓口 その他 当日の様子 最後に はじめに こんにちは! 昨年10月にInsight Edgeへコンサルタントとして参画した、楠です。 今回は、2月21日(水)に実施いたしましたInsight Edge初のオフラインイベントについてご報告いたします。 是非最後までお読みいただけますと幸いです。 実施背景 Insight Edgeは、2019年7月の設立以来、アジャイル開発・データ分析・AIモデル開発を内製エンジニアで実現する組織として、住友商事及び住友商事グループの事業会社の事業価値向上に寄与するべく、数々のDX案件に取り組んできました。 この度、間もなく設立5年を迎えるというタイミングで、これまでの足跡やInsight EdgeのCapabilityについて紹介し、課題解決のヒントを得ていただくためのオフラインイベントを企画・実施しました。 イベントの実施概要は以下の通りです。 開催日時: 2024年2月21日(水)11:00-17:30 場所: MIRAI LAB PALETTE HUB 対象:住友商事及び住友商事グループ事業会社 全役職員 MIRAI LAB PALETTE HUBを貸し切って実施しました。 イベントのキービジュアル。住友商事グループの「Digital Level UP」をテーマとしました。 概要 イベントでは、大きく以下3つのコンテンツを企画・実施しました。 事例展示 Insight Edgeが過去に取り組んだ案件のうち9つの案件について、売上向上・業務高度化といった多様な現場課題に対するDXソリューションを分かりやすく展示しました。 各案件の説明は実際に案件を担当したエンジニアが行い、いくつかの案件については実際に触っていただけるデモアプリも紹介し、来場者に臨場感をもって体感頂きました。 トークセッション 弊社CEO小坂らで住友商事グループ内外のDXについて語るオープニングセッションを皮切りに、4つのトークセッションを実施しました。 内2つのセッションでは、住友商事のユーザ部署やグループ事業会社の担当者にも登壇いただき、実際のエピソードを交えて、生成AI活用やDX内製力強化等のInsight Edgeとの協業案件について振り返っていただきました。 それ以外にも、データ活用のよくある事例や相談について弊社データサイエンティストのリーダー陣が語るセッションも実施しました。 相談窓口 DX 推進に必要なAIやデータ分析、開発など、様々な課題に関して気軽に相談可能な窓口を設置しました。 その他 Insight Edgeのロゴやイベントキービジュアルをあしらったノベルティや、美味しいドーナツで来場してくださった方々をおもてなしました! ノベルティのボールペン。Insight Edgeとして初めて作成しました。 ご来場者全員にイベントのキービジュアルを掲載したチョコレートを配布しました。 ご来場者向けにはドーナツやコーヒーを準備しました。 当日の様子 当日は合計で約200人の方にご来場いただき、DXに関する関心の高さを伺うことができました。また、トークセッションについても、用意したスペースがほぼ満席となったほか、会場質問も多数いただくことができ、非常に盛況となりました。 特にオープニングセッションでは、弊社CEOの小坂からInsight Edgeの位置づけやイベントの目的についてご説明した後、マネージャーの猪子・森から「内製化集団が果たす役割」や「技術進展を取り込むこと・技術のリスクを見極め正しく使うことの重要性」についてプレゼンを行いました。会場からはDXの推進方法や運用方法のノウハウ、弊社の取り組みが住友商事グループのDXに及ぼした影響など様々な質問が飛び交い、来場者の高い温度感や具体的な課題感をイベント開始すぐに感じられました。 会場にPCを配置し、ご来場者にデモ機を触っていただきました。 事例展示スペースの様子。(写真を一部加工しています。) オープニングセッションには弊社CEOの小坂も登壇しました。 最後に Insight Edgeは、「技術の力で世界をRe-Designする」をMissionとして、今後も住友商事グループのDX推進を加速して参ります。各職種について採用を強化しておりますので、是非 採用ページ もご覧ください!(2025年度については新卒採用も実施予定です。) ここまでお読みいただき、ありがとうございました!
アバター
はじめに こんにちは!昨年9月からInsight Edgeの開発チームに参画した広松です。Insight Edgeでは生成AI案件を担当しています。 参画してからまだ間もないですが、技術選定や顧客との調整を含む多くの業務を裁量を持って自由にできるところが良いと感じています。また、Insight Edgeでは業務時間の10%を勉強会などの自己啓発に充てることが奨励されています。この文化のおかげで、LangChainの勉強会を運営したりTech Blogに記事を寄稿したりと、いろいろな事に挑戦できてとても良い環境だなと感じています。 さて、最近私が担当する案件で、 生成AIに定性的な分析だけでなく定量的な分析もさせたい という要望がありました。そこで今回は、生成AIにデータ分析をさせる手法を検証してみました。具体的には、生成AIのAgentにKaggleのチュートリアルとして有名なタイタニック号の乗客のデータセットを分析させ、分析結果を評価したので紹介しようと思います。 目次 はじめに 目次 概要 ※補足 案件で構築するシステムの概要 事前知識 自律エージェントとは ReAct(Reasoning and Acting)とは LangChainとは ※補足 Code InterpreterではなくAgentで実装する理由 技術選定 LangChainのAgent関連のコンポーネントについて 選定理由 事前準備 実装内容 実行結果 考察 分析結果 分析過程 Agentによる自律的なエラー解決 まとめ 検証結果 Code Interpreterとの比較 今後に向けて 感想 概要 私が担当している案件で、生成AIに定量的な分析をさせたいという要望がありました。しかし、生成AIは 確率的にデータを生成するため、数学のような論理的思考や数値計算が苦手 です。 そこでどのように生成AIにデータ分析をさせるのか技術調査をしました。生成AIに計算やデータ分析をさせる場合、 Agent を使ってPythonのコードを生成・実行させる手法 が一般的です。 今回は、生成AIのAgentにKaggleのチュートリアルとして有名なタイタニック号の乗客のデータセットを分析させ、分析結果を評価しました。 ※補足 案件で構築するシステムの概要 今回の検証とはあまり関係がないので、興味が無い方は読み飛ばしてください。 分析テーマの入力 : ユーザが分析したいテーマを入力 定性的な分析の実施 : 生成AIが入力されたテーマに関連する情報をElastic Searchのハイブリッド検索機能を用いたRAGで収集し、その情報に基づいて定性的な分析を実施 定量的な分析の実施 : 生成AIのAgentが定性的な分析結果を元に社内データの分析をすることで定量的な分析も実施(※ここが今回の技術検証に関連する部分です。) 最終的なレポートの生成 : システムが 定性的と定量的な視点を融合させた 最終的な分析レポートを生成 事前知識 自律エージェント、ReAct、LangChainについての簡単な説明です。ご存知の方は 技術選定 まで読み飛ばしてください。 自律エージェントとは Agentという単語が出てきましたが、あまり馴染みのない方もいると思うので簡単に説明します。生成AIやLLMの文脈で出てくるAgent=自律エージェントと思っていただいて構いません。 自律エージェントは、与えられた最終目的に対して、自ら思考して計画をし、実際に行動をして最終目的を達成しようとするシステムです。 この行動の際に、 自律エージェントはツールを使って現実世界のシステムに干渉 できます。 この説明だと抽象度が高く、よく分からないと思うので今回の検証を例に説明します。 自律エージェントには、「タイタニック号の乗客のデータセットを分析する」という最終目的だけを与えます。 すると自律エージェントは、最初に思考してどのように分析するか計画を立てます。次に、計画に従いインターネット検索をするツールやPythonコード生成・実行ツールを使ってデータ分析を実施して最終目的を達成しようとします。 生成AIが内部で考えているだけでなく、インターネット検索ツールやPythonコード生成・実行ツールを使って、現実世界のシステムとやりとりをしているところが、通常の生成AIの利用と大きく異なります 。 今回はReActという手法でAgentを動作させているので、詳細な仕組みは次の ReAct(Reasoning and Acting)とは で説明します。 自律エージェントについて詳細を確認したい方は、 Autonomous Agents & Agent Simulations を参照してください。 ReAct(Reasoning and Acting)とは ReAct(Reasoning and Acting)は生成AIに論理的な思考をさせるPrompt Engineeringの技法の1つです。生成AIに推論と行動を繰り返させることで論理的思考を促すもので、次の三段階のプロセスを繰り返します。 思考(Thought) : 目的を理解し、行動計画・タスクの詳細化を行い、具体的な解決策を考案する。観察(Observation)からフィードバックを受け取り、適宜行動計画を修正する。 行動(Action) : 思考(Thought)段階で考案された解決策を実行する。 観察(Observation) : 行動(Action)の結果を観察し、次の思考(Thought)へフィードバックする。 LangChainのAgentは、この 行動(Action)の部分にツールを使用 できます。SerpApi等の検索ツールやPythonREPL等のPythonコード生成・実行ツールを使用します。利用可能なツールの説明を理解し、目的達成に必要なものをAgent自ら選択します。 つまり、 与えられた最終目的に対して自ら思考して計画を立て、適切なツールを選択して行動し、結果の観察とフィードバックのループを目的達成まで繰り返す自律エージェント が作成されます。 ReActについて詳細を確認したい方は、ReActが提唱された論文 ReAct: Synergizing Reasoning and Acting in Language Models を参照してください。 LangChainとは LangChainとは大規模言語モデル(LLM)を用いたアプリケーションを開発するためのフレームワークです。 様々なエコシステムと連携しやすくなっています。 LangChainにはモジュールが6つあるので簡単に紹介します。今回は主にAgentsのモジュールに関係があります。 モジュール 概要 Model I/O LLMに対するプロンプト入力、呼び出し、結果の受け取りの実装を簡単にする。 Retrieval LLMに未知の情報にアクセスする機能を提供する、Retrieval-Augmented Generation(RAG)を扱うモジュール。外部の検索エンジンやデータベースと連携して、LLMに事前学習していない情報に対応させる機能を簡単に実装できるようにする。 Memory 過去の会話履歴をデータベースに保存したり、呼び出す機能を簡単に実装できるようにする。 Chains 複数のモジュールの組み合わせを簡単に実装できるようにする。 Agents 自律的に思考し、外部に干渉する自律エージェントの構築を簡単にする。 Callbacks 様々なイベント発生時の処理の実装を簡単にする。ログの実装などに用いる。 LangChainについて詳細を確認したい方は、 LangChain公式ドキュメント を参照してください。 ※補足 Code InterpreterではなくAgentで実装する理由 こちらも今回の検証とは直接関係がないので、興味が無い方は読み飛ばしてください。 技術検証するのは生成AIのAgentにデータ分析をさせる部分です。OpenAIのCode Interpreterと似た機能ですが、Code Interpreterは外部環境(インターネット等)にアクセスできない等の制約があります。 一方、Agentであれば不明点や解決できないエラーが発生した際に、検索ツールを使って検索する等、他のツールを使った行動を取ることもできます。そのためAgentで実装した方が顧客の要望に柔軟に対応できます。 また担当する案件は、 案件で構築するシステムの概要 のように複雑で今後も拡張されそうな要件です。 Agentであれば行動やツールについて自由にカスタマイズができる ためAgentで実装しました。 技術選定 生成AIにデータ分析をさせる部分を検証します。 検証はLangChainで実装しました。 LangChainのAgent関連のコンポーネントについて 選定理由を話す前に、LangChainのAgent関連のコンポーネントの説明をしておきます。ご存知の方は読み飛ばしてください。 コンポーネント 説明 AgentExecutor AgentExecutorは、Agentの実行を管理するコンポーネントで、Agentが実行するタスクを制御し、結果を処理する。 Agent Agentは、特定のタスクやプロセスを自動化するためのエンティティで、一連のToolsを使用する。ツールのDescriptionを読んで適切なツールを選択する。 Tools Toolsは、Agentがタスクを実行する際に使用する具体的な機能やサービスの集合。Toolsには外部のAPI呼び出しを可能にするツール、Google検索などを可能にする検索ツール、Pythonのコード生成・実行を可能にするツールなどがある。 ※LangChainで利用可能なTools一覧は こちら 。 また、独自のToolsの作成も簡単にできる。独自のRetrieverを定義しておけば独自実装したRAGを組み込んだAgentを作ることができる。 Toolkit Toolkitは、複数のToolsを組み合わせて構成されるセットで、特定のタスクを効率的に実行するために最適化されている。 Pythonのコード生成・実行に特化したToolkitなどもある。 ※LangChainで利用可能なToolkit一覧は こちら 。 選定理由 技術要素 選定理由 LLM: GPT-4 Turbo 精度とコストを重視したため。 フレームワーク: LangChain 生成AIのAgentを容易に実装・管理できるフレームワークであり、デファクトスタンダードになりつつあるため。 Tools: PythonREPL Pythonのコード生成・実行を可能にするツール。データ分析タスクにおいて、Pythonを使用して分析をするため Tools: DuckDuckGoSearch インターネット検索を可能にするツール。不明点や解決できないエラーが発生した場合に検索して対処するため。 今回はツールとして独自定義した。 Agent: ReAct Agentの汎用性と論理的な思考能力を評価するため ReAct を採用。案件で構築するシステムがチャット形式のため、LangChainのAgentTypeはCHAT_ZERO_SHOT_REACT_DESCRIPTION を採用。 事前準備 事前にタイタニック号の乗客のデータセットをダウンロードし、S3などのダウンロード可能な場所に配置してください。タイタニック号の乗客のデータセットはKaggleからダウンロードできます。 データセットはKaggleにユーザ登録をし、 Kaggleのタイタニックデータセットのページ からダウンロードできます。 また、OpenAIのAPIキーは環境変数に事前に登録しておいてください。 OPEN_API_KEY="ご自身の保有しているAPIキーを記載してください" 実装内容 下記のコードで、タイタニック号の乗客データセットをダウンロードし、タイタニック号で生存に寄与した乗客の属性を分析させます。 コードの内容は 選定理由 に記載した通りなので詳細な説明は割愛します。 案件では再現性の高い分析が求められているため、LLMのtemperatureを0に設定しました。 Agentに与える指示は以下にしています。 タイタニック号の乗客のデータセットを分析してください。生存に寄与した属性をランキングで表示し、影響度を数値で表示してください。データセットは、https://xxxxxxxxxxxx/train.csv を使用してください。 データセットをインターネットからダウンロードするよう指示したのは、OpenAIのCode Interpreterができない外部環境へのアクセスが可能か検証するためです。コードを実行する場合は、 事前準備 にあるようにダウンロード可能なURLに書き換えてください。 from langchain.agents import AgentType, initialize_agent, load_tools, Tool from langchain.chat_models import ChatOpenAI from langchain.tools import DuckDuckGoSearchRun chat = ChatOpenAI( temperature= 0 , # 再現性を重視するため、temperatureを0に設定 model= "gpt-4-turbo-preview" ) tools = load_tools( # Agentが使用可能なツールの配列を読み込む(複数指定可能、Agentがツールのdescriptionを読み適切なツールを選択する) [ "python_repl" # Pythonのコードを実行するためのTool ] ) search = DuckDuckGoSearchRun() # 不明点やエラーの解決策を検索するためDuckDuckGoSearchを追加 tools.append( # 追加したDuckDuckGoSearchをtoolsに追加 Tool( # ツールとして検索ツールを独自定義 name= "duckduckgo-search" , func=search.run, description= "This tool is used for conducting internet searches to address uncertainties or unresolved errors that arise during tasks or development processes." # Agentが選択できるようにツールの説明を記載 ) ) agent = initialize_agent( tools=tools, # Agentが使用することができるツールの配列を指定 llm=chat, # Agentが使用する言語モデルを指定 agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, #ReActを実行するAgentを指定 verbose= True # 実行中の詳細なログを表示する ) result = agent.run( "タイタニック号の乗客のデータセットを分析してください。生存に寄与した属性をランキングで表示し、影響度を数値で表示してください。データセットは、https://xxxxxxxxxxxx/train.csv を使用してください。" ) print (f "実行結果: {result}" ) 実行結果 実行すると、論理的に推論と行動を繰り返し、最終的にランキングを出力できました。 出力結果 parch : 0.6250 sex : 0.5365 sibsp : 0.5110 pclass : 0.3639 fare_quantile : 0.3567 age_group : 0.1178 この分析結果では、parch(同乗している家族(親子)の人数)、sex(性別)、sibsp(同乗している兄弟姉妹、配偶者の数)、pclass(客室等級)、fare_group(運賃)、age_group(年齢)の順に生存への影響が強い属性だったようですね。 実行結果を見てみましょう。(※実行結果中のデータセットのダウンロードURLは書き換えています。) 実行結果全文(非常に長いので詳細を確認したい方のみ、この文章をクリックして展開してください。) > Entering new AgentExecutor chain... Question: Analyze the Titanic passengers dataset to determine which attributes had an impact on survival. Provide the analysis results in a ranking with numerical values indicating the degree of impact on survival. Download the dataset using Python from the provided URL ( https://xxxxxxxxx/train.csv ) . Thought: To analyze the Titanic dataset, I will first need to download the data using Python. After downloading, I will load the data into a pandas DataFrame and perform exploratory data analysis ( EDA ) to determine which attributes had the most significant impact on survival. Action: { " action " : " Python_REPL " , " action_input " : " import pandas as pd \n df = pd.read_csv('https://xxxxxxxxx/train.csv') \n print(df.head()) " } Python REPL can execute arbitrary code. Use with caution. Observation: pclass survived name sex age sibsp parch ticket fare cabin embarked boat body home.dest 0 1 1 Allen, Miss. Elisabeth Walton female 29 . 0000 0 0 24160 211 . 3375 B5 S 2 NaN St Louis, MO 1 1 1 Allison, Master. Hudson Trevor male 0 . 9167 1 2 113781 151 . 5500 C22 C26 S 11 NaN Montreal, PQ / Chesterville, ON 2 1 0 Allison, Miss. Helen Loraine female 2 . 0000 1 2 113781 151 . 5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON 3 1 0 Allison, Mr. Hudson Joshua Creighton male 30 . 0000 1 2 113781 151 . 5500 C22 C26 S NaN 135 . 0 Montreal, PQ / Chesterville, ON 4 1 0 Allison, Mrs. Hudson J C ( Bessie Waldo Daniels ) female 25 . 0000 1 2 113781 151 . 5500 C22 C26 S NaN NaN Montreal, PQ / Chesterville, ON Thought:The dataset has been successfully loaded into a pandas DataFrame. The next step is to perform exploratory data analysis ( EDA ) to determine which attributes had the most significant impact on survival. I will start by checking for missing values in the dataset, as this can affect the analysis. Then, I will proceed to calculate the survival rates based on different attributes such as ' pclass ' , ' sex ' , ' age ' , ' sibsp ' , ' parch ' , ' fare ' , and ' embarked ' . Action: { " action " : " Python_REPL " , " action_input " : " print(df.isnull().sum()) " } Observation: pclass 0 survived 0 name 0 sex 0 age 263 sibsp 0 parch 0 ticket 0 fare 1 cabin 1014 embarked 2 boat 823 body 1188 home.dest 564 dtype: int64 Thought:The dataset contains a significant number of missing values in the ' age ' , ' cabin ' , ' boat ' , ' body ' , and ' home.dest ' columns. The ' boat ' and ' body ' columns are not typically used for survival analysis, as they relate to post-accident events ( lifeboats and body recovery ) . The ' cabin ' data is also largely missing, which may limit its usefulness in the analysis. However, ' age ' and ' embarked ' are important factors that are often considered in survival analysis. The missing value in ' fare ' is negligible as it is only one entry. The next step is to analyze the impact of various attributes on survival. I will calculate survival rates based on ' pclass ' , ' sex ' , ' age ' , ' sibsp ' , ' parch ' , and ' fare ' . Since ' age ' has missing values, I will need to decide how to handle them, either by imputing a value or by excluding those rows from certain parts of the analysis. For the initial analysis, I will focus on the attributes with complete data. I will start by calculating the survival rates for ' pclass ' and ' sex ' , as these are commonly known to have had a significant impact on survival. Then I will proceed with ' sibsp ' , ' parch ' , and ' fare ' . For ' age ' , I will consider how to handle the missing values before proceeding. Action: { " action " : " Python_REPL " , " action_input " : " survival_rates = {} \n for column in ['pclass', 'sex', 'sibsp', 'parch']: \n survival_rates[column] = df.groupby(column)['survived'].mean() \n print(survival_rates) " } Observation: { ' pclass ' : pclass 1 0 . 619195 2 0 . 429603 3 0 . 255289 Name: survived, dtype: float64, ' sex ' : sex female 0 . 727468 male 0 . 190985 Name: survived, dtype: float64, ' sibsp ' : sibsp 0 0 . 346801 1 0 . 510972 2 0 . 452381 3 0 . 300000 4 0 . 136364 5 0 . 000000 8 0 . 000000 Name: survived, dtype: float64, ' parch ' : parch 0 0 . 335329 1 0 . 588235 2 0 . 504425 3 0 . 625000 4 0 . 166667 5 0 . 166667 6 0 . 000000 9 0 . 000000 Name: survived, dtype: float64 } Thought:The survival rates based on ' pclass ' , ' sex ' , ' sibsp ' , and ' parch ' show clear trends. Higher class passengers ( pclass 1 ) had a higher survival rate than those in lower classes. Females had a much higher survival rate than males. The number of siblings/spouses ( sibsp ) and parents/children ( parch ) aboard also had varying impacts on survival rates, with some numbers showing higher survival rates than others. To provide a more comprehensive analysis, I will now calculate the survival rate for ' fare ' by categorizing it into quantiles, which will help to understand if higher fares ( which could correlate with higher socio-economic status or cabin location ) had an impact on survival. I will also handle the missing ' age ' data by filling in the missing values with the median age, and then categorizing age into groups to see if there is a trend in survival rates across different age groups. First, I will handle the ' fare ' attribute by categorizing it into quantiles. Action: { " action " : " Python_REPL " , " action_input " : " df['fare_quantile'] = pd.qcut(df['fare'], 4, labels=False) \n survival_rates['fare_quantile'] = df.groupby('fare_quantile')['survived'].mean() \n print(survival_rates['fare_quantile']) " } Observation: fare_quantile 0 . 0 0 . 228487 1 . 0 0 . 284375 2 . 0 0 . 435976 3 . 0 0 . 585139 Name: survived, dtype: float64 Thought:The survival rates for the ' fare_quantile ' show a positive correlation between fare price and survival, with passengers who paid higher fares having a higher survival rate. This suggests that socio-economic status , which could be inferred from the fare paid, had an impact on survival. Next, I will address the missing ' age ' data. Since age is an important factor and cannot be ignored, I will impute the missing values with the median age. After imputing the missing values, I will categorize the ' age ' into groups and calculate the survival rates for each group to see if there is a trend in survival rates across different age groups. Action: { " action " : " Python_REPL " , " action_input " : " median_age = df['age'].median() \n df['age'].fillna(median_age, inplace=True) \n df['age_group'] = pd.qcut(df['age'], 4, labels=False) \n survival_rates['age_group'] = df.groupby('age_group')['survived'].mean() \n print(survival_rates['age_group']) " } Observation: age_group 0 0 . 435435 1 0 . 317597 2 0 . 425532 3 0 . 394410 Name: survived, dtype: float64 Thought:The survival rates for the ' age_group ' show that the youngest and older age groups had slightly higher survival rates than the middle age groups, although the differences are not as pronounced as with other attributes like ' sex ' and ' pclass ' . Now that I have calculated the survival rates for ' pclass ' , ' sex ' , ' sibsp ' , ' parch ' , ' fare_quantile ' , and ' age_group ' , I can rank these attributes by their impact on survival. To do this, I will calculate the difference in survival rates between the highest and lowest groups within each attribute to determine the range of impact. Then I will rank the attributes based on these ranges. Action: { " action " : " Python_REPL " , " action_input " : " impact_ranges = {} \n for attribute, rates in survival_rates.items(): \n impact_ranges[attribute] = rates.max() - rates.min() \n impact_ranking = sorted(impact_ranges.items(), key=lambda x: x[1], reverse=True) \n for rank in impact_ranking: \n print(f'{rank[0]}: {rank[1]:.4f}') " } Observation: parch: 0 . 6250 sex: 0 . 5365 sibsp: 0 . 5110 pclass: 0 . 3639 fare_quantile: 0 . 3567 age_group: 0 . 1178 Thought:The ranking of attributes based on their impact on survival, with the numerical values indicating the degree of impact, is as follows: 1 . `parch` ( number of parents/children aboard ) : 0 . 6250 2 . `sex` : 0 . 5365 3 . `sibsp` ( number of siblings/spouses aboard ) : 0 . 5110 4 . `pclass` ( passenger class ) : 0 . 3639 5 . `fare_quantile` ( fare price category ) : 0 . 3567 6 . `age_group` : 0 . 1178 It is important to note that the `parch` attribute shows the largest range of impact, which might be surprising as it is not typically considered the most significant factor. However, this could be due to the way the survival rates are calculated across different numbers of parents/children aboard, which might not be linear and could be influenced by small sample sizes for certain values. The `sex` attribute has the second-highest impact, which aligns with historical accounts that women were given priority during the lifeboat boarding process. The `sibsp` attribute is third, suggesting that having siblings or a spouse aboard had a significant impact on survival, potentially due to the support system or prioritization of families during evacuation. The `pclass` attribute is fourth, indicating that passenger class was a significant factor, with first-class passengers having a higher survival rate. The `fare_quantile` is fifth, which is related to socio-economic status and potentially cabin location, with higher fares correlating with higher survival rates. Lastly, the `age_group` has the smallest range of impact, suggesting that age was a less significant factor compared to the others, although there was still some variation in survival rates across age groups. Final Answer: The ranking of attributes by impact on survival, from highest to lowest, is as follows: 1 . `parch` : 0 . 6250 2 . `sex` : 0 . 5365 3 . `sibsp` : 0 . 5110 4 . `pclass` : 0 . 3639 5 . `fare_quantile` : 0 . 3567 6 . `age_group` : 0 . 1178 > Finished chain. 実行結果: The ranking of attributes by impact on survival, from highest to lowest, is as follows: 1 . `parch` : 0 . 6250 2 . `sex` : 0 . 5365 3 . `sibsp` : 0 . 5110 4 . `pclass` : 0 . 3639 5 . `fare_quantile` : 0 . 3567 6 . `age_group` : 0 . 1178 中身を見てみましょう。はじめにQuestionで最終目的を定義しています。 Question: Analyze the Titanic passengers dataset to determine which attributes had an impact on survival. Provide the analysis results in a ranking with numerical values indicating the degree of impact on survival. Download the dataset using Python from the provided URL ( https://xxxxxxxxx/train.csv ) . きちんと指示通りに、生存に寄与した属性をランキング形式で表示するタスクを理解していますね。 その後は、以下6ステップに分けて実行しているようです。 データセットのダウンロードと読み込み : 指定されたURLからタイタニックのデータセットをダウンロードし、PandasのDataFrameに読み込みます。読み込んだデータの先頭部分を表示して、データが正しく読み込まれたことを確認します。 欠損値の確認 : データセットに含まれる欠損値の数を確認します。特に「年齢(age)」、「キャビン(cabin)」、「乗船した港(embarked)」などのカラムで欠損値が多いことが分かります。これらの欠損値は分析に影響を与えるため、適切に処理する必要があります。 生存率の計算 : 生存に影響を与えた可能性のある属性(「客室等級(pclass)」、「性別(sex)」、「兄弟姉妹や配偶者の数(sibsp)」、「親や子どもの数(parch)」)について、グループごとに生存率を計算します。 料金(fare)に基づく生存率の計算 : 乗船料金を四分位数で分割し、料金のカテゴリーごとに生存率を計算します。料金が生存率にどのように影響したかを分析します。 年齢(age)に基づく生存率の計算 : 年齢データの欠損値を中央値で補完し、年齢をグループに分けて生存率を計算します。年齢が生存率にどのように影響したかを分析します。 属性の影響度のランキングの表示 : 最後に、各属性に基づいて計算された生存率の差を用いて、生存に最も影響を与えた属性をランキング形式で表示します。ランキングは、各属性が生存に与えた影響の度合いを数値で示しています。 考察 分析結果 Agentによる分析と人による分析を比較して妥当性を確認します。 Kaggleで今回のデータセットの分析をされているこちらの投稿 を見ても、生存に影響を与えている属性が同じですね。人が行った分析と比較しても、今回のAgentの行った分析は妥当と言えます。タイタニック号の史実と照らし合わせても客室等級、性別、子供を連れた家族などの属性が生存に影響したという分析は妥当と言えます。 分析過程 今回の検証では分析結果よりも分析過程自体の考察が重要です。 生存に寄与した属性をランキング表示するという大目標のみを与えていましたが、 Agentは目的達成のために自律的に思考し自ら詳細なタスクを作り出して実行 していました。特にデータを確認して自らの判断で欠損値を確認して補完したり、生存率を計算した後、関連のありそうな属性を詳細に分析するタスクを自ら作り出して実行している点は非常に興味深いです。 今回のデータセットを分析している記事をいくつか拝見しましたが、 欠損値を埋める等の指示をしていないタスクも人と同様の手順 で実行できていました。 このような論理的な思考ができる理由は ReAct の推論フレームワークを使用したからだと考えられます。 該当箇所抜粋 Thought:The dataset has been successfully loaded into a pandas DataFrame. The next step is to perform exploratory data analysis ( EDA ) to determine which attributes had the most significant impact on survival. I will start by checking for missing values in the dataset, as this can affect the analysis. Then, I will proceed to calculate the survival rates based on different attributes such as ' pclass ' , ' sex ' , ' age ' , ' sibsp ' , ' parch ' , ' fare ' , and ' embarked ' . Action: { " action " : " Python_REPL " , " action_input " : " print(df.isnull().sum()) " } Observation: pclass 0 survived 0 name 0 sex 0 age 263 sibsp 0 parch 0 ticket 0 fare 1 cabin 1014 embarked 2 boat 823 body 1188 home.dest 564 dtype: int64 Thought:The dataset contains a significant number of missing values in the ' age ' , ' cabin ' , ' boat ' , ' body ' , and ' home.dest ' columns. The ' boat ' and ' body ' columns are not typically used for survival analysis, as they relate to post-accident events ( lifeboats and body recovery ) . The ' cabin ' data is also largely missing, which may limit its usefulness in the analysis. However, ' age ' and ' embarked ' are important factors that are often considered in survival analysis. The missing value in ' fare ' is negligible as it is only one entry. The next step is to analyze the impact of various attributes on survival. I will calculate survival rates based on ' pclass ' , ' sex ' , ' age ' , ' sibsp ' , ' parch ' , and ' fare ' . Since ' age ' has missing values, I will need to decide how to handle them, either by imputing a value or by excluding those rows from certain parts of the analysis. For the initial analysis, I will focus on the attributes with complete data. Agentによる自律的なエラー解決 何度か検証をしていると、Agentが生成したPythonのコードが実行時にエラーを吐くことがありました。エラーが発生した際にAgentは、 ReAct の推論フレームワークに沿って、エラーを自律的に解決していました。具体的には発生したエラーの解決策を検索ツールで検索して解決していました。 自律的なエラー解決のプロセス 観察(Observation) : 前回の行動(Action)の結果、エラーの発生を確認する。 思考(Thought) : 解決策を探すために、適切なツールの使用する。「不明点やエラーの解決策を検索するためのツール」という説明の検索ツールを見つけ、解決策をインターネット検索することを計画する。 行動(Action) : 検索ツールを用いて解決策を検索する。 観察(Observation) : 検索結果から有益な情報を抽出する。 思考(Thought) : 抽出された情報から具体的なアクションを考案する。 行動(Action) : Pythonコード生成・実行ツールで解決策を実行する。 このように 予期せぬ事態に遭遇しても、推論と行動を繰り返すReActのAgentは柔軟に対応 できます。状況に合わせて臨機応変に計画を変更し、ツールを使い分け、目的達成に向けてタスクを継続できる点がReActで動作するAgentの強みです。 下記の事例は、seabornというモジュールがインストールできないエラーが発生し、解決策を検索ツールで検索しています。結局インストールできないという問題は解決できませんでしたが、他のモジュールを使うことでエラーを回避して分析を進めていました。 Agentが発生したエラーの解決策を検索したり、回避策を探して分析を続けている事例 Action: { " action " : " Python_REPL " , " action_input " : " !pip install seaborn " } Observation: SyntaxError ( ' invalid syntax ' , ( ' <string> ' , 1 , 1 , ' !pip install seaborn\n ' , 1 , 2 )) Thought:It seems that I made the same mistake again. I need to use the duckduckgo-search tool to search for the correct way to install the seaborn module. Action: { " action " : " duckduckgo-search " , " action_input " : " install seaborn module in Python using pip " } Observation: Latest version Jan 25 , 2024 Project description seaborn: statistical data visualization Seaborn is a Python visualization library based on matplotlib. It provides a high-level interface for drawing attractive statistical graphics. Documentation Online documentation is available at seaborn.pydata.org. We can install the seaborn package by running the below command . pip install seaborn Kindly ensure that you have pip installed on your device before running this command . For Python 3 , we can use the pip3 command instead of pip. We can also specify the required version of the module we wish to install, as shown below. pip3 install seaborn = = 0 . 9 . 0 The pip show seaborn command will either state that the package is not installed or show a bunch of information about the package, including the location where the package is installed. # Make sure your IDE is using the correct Python version If the package is not installed, make sure your IDE is using the correct version of Python. How can I Install a Python module within code? Ask Question Asked 11 years, 5 months ago Modified 8 months ago Viewed 552k times 503 I need to install a package from PyPI straight within my script. Getting Started First of all, let us install Seaborn. Seaborn can be installed using the pip. Type the below command in the terminal. pip install seaborn In the terminal, it will look like this - After the installation is completed you will get a successfully installed message at the end of the terminal as shown below. Thought:Based on the search results, the correct way to install the seaborn module is by using the pip command . I will now install seaborn using the pip command . Action: { " action " : " Python_REPL " , " action_input " : " !pip install seaborn " } Observation: SyntaxError ( ' invalid syntax ' , ( ' <string> ' , 1 , 1 , ' !pip install seaborn\n ' , 1 , 2 )) Thought:I apologize for the repeated mistake. It seems that I am unable to install the seaborn module using the pip command in this environment. However, I can still analyze the dataset using other available tools and libraries. I will proceed with analyzing the dataset without using seaborn. まとめ 検証結果 生成AIは一般に確率的にデータを生成するため、論理的な思考やデータ分析のような数値計算は苦手です。 しかし、今回検証したように生成AIのAgentに ReAct のような推論フレームワークとPythonコード生成・実行をするツールを組み合わせることで、論理的思考や数値計算が必要なデータ分析等のタスクも実行できる ようになります。Agentによる分析も人による分析と比較しても妥当性の高いものでした。 また、生成したPythonのコードでエラーが発生した際に検索のツールを使って、エラーの解決策を検索しに行くなど臨機応変な対応をしていました。 想定外の事態が起きても、計画を変更しツールを使い分けて対応できる点も、推論と行動を繰り返すReActのAgentの強み と言えます。 Code Interpreterとの比較 OpenAIのCode Interpreterではこれほど推論と行動を繰り返してタスクを実行することはありません。エラー解決のためにインターネットで検索するなどの臨機応変な対応もできません。そもそもインターネット等の外部環境にアクセスができません。上記の点からも今回のようなAgentの実装に優位性があります。 逆にいうとAgentは行動を予測しづらく、行動回数が増えて返答まで時間がかかったり、GPT-4 Turboの呼び出し回数も増加して高コストになる可能性があります。要件に応じて最大行動回数を設定する等の対策をすると良いと思います。 今後に向けて 案件では、生成AIでRAGを用いてプライベートデータの定性的な分析をし、得られた定性的な分析を元に定量的な分析をします。今回の検証よりも複雑で高い精度を求められるので今後も改良が必要です。今回は単一のAgentで実装しましたが、今後は複数のAgentを使った構成も試したいと思います。マルチエージェントと呼ばれる、複数の異なる役割を持ったエージェントが協力するシステムや、 BabyAGI などの高度な自律エージェントを検証し、高精度でより複雑なタスクへ可能なシステムを実装しようと思います。 感想 生成AIのAgentを使ったデータ分析をしてみましたが、 ReAct の論理的思考には驚かされました。特に、Agentが生成したPythonのコードでエラーが発生した際に検索を始めた時には、こんなことができるのかと非常に驚きました。 最近では、複数の異なる役割を持ったエージェントが協力するマルチエージェント等、複雑なタスクに対応できるアーキテクチャも流行っています。また、最近のLLMはマルチモーダル(テキストだけでなく、画像、音声、動画など様々なデータ形式に対応したモデル)にもなってきています。 マルチモーダルとマルチエージェントを組み合わせるとかなり幅広いタスクができるようになる のではないかと思います。 限界を越えて進化し続ける生成AIは、非常に面白いので今後も積極的に関わっていこうと思います! 本記事がどなたかの役に立てば幸いです!
アバター
初めまして、去年の11月にInsight Edgeへ参画したEngineerの田島です。 現在の業務では生成AI用いたDX案件や住友商事グループ事業のソフトウェア開発に携わっています。 まだ加入してから数ヶ月ですが、Insight Edgeの特徴として「小回りの良さと技術選定の幅の広さ」を強く感じています。 出島組織なので開発を柔軟に進めていくことが可能であり、 幅広いビジネスをしている総合商社ならではの様々な課題に対応すべく、 幅の広い技術選定が求められるように感じます。 また、エンジニア・データサイエンティスト・コンサルタントが互いにリスペクトし、プロフェッショナルとして働いてるので、成長の場としても適しているかと思います。 本記事ではComputer Visionのシステムを題材に、生成AIの活用で注目が集まっているAzureを用いたMLOpsの実現について紹介したいと思います。 概要 想定ケース ビジネスシーンとして、各カメラから取得される人流情報を防犯や空調の最適化に活かすことを想定します。 想定ケース 複数のカメラを用いたシステムの課題 カメラシステムを実際に運用する上で、設置条件は個体毎に異なります。 これにより、カメラ毎の認識精度がばらつくと想定されます。 例えば、暗所による撮影における感度自動制御で、下図のようなノイズが生じます。 (感度自動制御とはカメラ信号処理の一部で、適正な明るさが得られない場合などに電気的に信号を増幅させることで適正な明るさの画像を得る処理のことです。) 同じカメラにおいても長期期間システムを運用するうえでは、季節や周辺環境の変化により影響度が変化します。 このカメラの精度ばらつきが、本ユースケースの人流情報の効果的な利用の問題になると考えられます。 暗所によるノイズの例 システムの概要 本記事では、認識精度のばらつきを自動的に補正する画像認識システムを考えます。 これにより、安定した認識率のシステムを構築するだけでなく、 従来はカメラの設置条件などを変えるなどしていた作業をシステムとアルゴリズムが肩代わりすることで、保守工数を削減したシステムを構築します。 このシステム構築にあたっては、Azure Custom VisionのOptimization機能を用いて、カメラ毎の条件に合わせて再学習します。 (*) 本記事ではセンサーノイズや画像処理の詳細には踏み込みません。あくまでシステム構築に主眼を置きます。 システムの内容 MLOps 機械学習の社会実装をするうえで、他のシステムと同様に継続的なインテグレーションとデリバリーが求められます。 MLOpsはこれを実現するエコシステムを指し、Googleの提唱する 実現レベル が有名です。 この実現レベルの概要は以下の通りです。 レベル0:手動による学習プロセス。モデルの変更が稀なケースを想定します。 レベル1:学習パイプラインの自動化。パイプライントリガーによって自動的にモデルを更新します。 レベル2:CI/CDパイプラインの自動化・効率化。モデルの変更アイデアを迅速に反映・検証します。 そこで本記事ではレベル1を題材に、継続的にモデルを更新することで、設置環境の変化によるノイズ影響の解決を考えます。 インフラ構成 本記事で構築するシステムのアーキテクチャは以下の通りです。 インフラ構成 Azure Custom Vision Azure Custom Visionは特定のドメインに特化した画像認識モデルを作成可能なサービスです。 利用にあたっては機械学習モデルの専門知識は必要なく、WebUIまたはREST APIでサービスを利用できます。 レベル1のMLOpsに必要不可欠な要素であるモデルのテストやエッジ環境などへデプロイに対応しています。 2024/02現在は、1枚の画像をクラス分類するClassificationと画像中に物体位置を認識するObject Detectionに対応しています。 本記事では、画像中から対象物の数を抽出するためにObject Detectionを採用し、REST APIを用いてカメラ毎に適切な学習を自動的に行います。 インフラ構成の説明 カメラで取得された画像データは、AzureのBlobストレージにアップロードされるものとします。 このアップロードをトリガーとして、Azure Functionsを起動させ、人物検出します。 人物検出には上のAzure Custom Visionで生成したONNXモデルをStorageからダウンロードします。 そして、検出された人物の数がSQLデータベースに記録されます。 また、Azure Functionsの定期実行を用いて、モデルを更新します。 モデル更新用に撮影された複数枚の画像から統計的なばらつきを推定し、それらの影響を学習データに付加して、カメラ環境に合った学習データを生成します。 この学習データをAzure Custom Visionにアップロードし、モデルの再学習します。学習終了後にStorageのモデルを更新します。 今後エッジでの検出処理を想定し、モデルデータのエクスポートします。 仮にクラウドでの処理に完結させる場合は、モデルのAzure Custom VisionのAPIエンドポイントとしてパブリッシュすることで、システムを簡略化可能です。 ソースコード ソースコードは以下のレポジトリで公開しています。 GitHub Link デプロイメント ソースコード中のterraformを用いてクラウドリソースのデプロイが可能です。 一方でCustom Visionについては、Microsoftの「AIの責任ある使用」に関するポリシーに同意する必要があるためAzure Portalで行います。 MarketPlaceよりCustom Visionを選択し、「作成オプション」を「トレーニング」にしてリソースを作成します。 モデルをエクスポートせずに、学習済みモデルをREST APIとして実行する場合は「両方」を選択してください。 トレーニングの価格レベルは要件に合わせて選択しますが、PoC段階ということで「Free F0」を選択します。 Custom Visionのリソース作成 MLワークフロー 本記事ではソースコードのMLワークフローおよびAzure Custom Visionについて説明します。 学習データの準備 人物検知に向けた学習データの作成にあたり、ここでは商用可能なものを選定または作成する必要があります。 しかし、このデータセットの用意は本記事のシステム構築の趣旨から外れるため、検証用としてお菓子袋の検出を考えます。 ここで用意した画像に、以下のような検出用のバウンディングボックスを設定し、それらの情報を保存します。 ここまでの準備をシステム構築前に行います。 お菓子とバウンディングボックス 再学習時には、この学習データにガウシアンノイズを付与します。 ガウシアンノイズの分散値には、被写体の時間変化が少ない期間に撮影された複数枚の画像から、画素ごとに分散値を算出し、全画素の最頻値を採用します。 これにより、被写体が一定であるときのランダムノイズの代表値を近似的に取得します。 (*) この方法では信号量に依存したノイズ量を正しく見積もることができません。より精度を求める場合は改善の余地があります。 ノイズ量の算出 モデル学習 Custom VisionをPython向けのSDK越しに用いて、学習データをアップロードします。 モデル学習のタスクをキューに追加し、一定期間待機することで学習が終了します。 以下はソースコードの一部を抜粋し、コメントを加えたものです。 from azure.cognitiveservices.vision.customvision.training import ( CustomVisionTrainingClient, ) from azure.cognitiveservices.vision.customvision.training.models import ( Region, ImageFileCreateEntry, ImageFileCreateBatch, ) from msrest.authentication import ApiKeyCredentials ``` 省略 ``` credentials = ApiKeyCredentials(in_headers={"Training-key": TRAINING_KEY}) trainer = CustomVisionTrainingClient(ENDPOINT, credentials) # ドメインの作設定 # エッジデバイスへのデプロイを見越して`compact`を選定 # `compact`を選定しない場合はWebAPI越しの実行しかできない obj_detection_domain = next( domain for domain in trainer.get_domains() if domain.type == "ObjectDetection" and domain.name == "General (compact)" ) # プロジェクトを作成 project = trainer.create_project( "People Counting Project", domain_id=obj_detection_domain.id, ) # タグの作成 person_tag = trainer.create_tag(project.id, "Person") # アップロード画像のリスト化 # 上で保存した情報(ファイル名とバウディングボックス)が"json_data"に格納 tagged_images_with_regions = [] for data in json_data: image_file = data['fileName'] tags = data["tags"] regions = [ Region( tag_id=person_tag.id, left=tag["left"], top=tag["top"], width=tag["width"], height=tag["height"], ) for tag in tags ] with open(image_file, "rb") as image_contents tagged_images_with_regions.append( ImageFileCreateEntry( name=image_file, contents=image_contents.read(), regions=regions, ) ) # ファイル群のアップデート trainer.create_images_from_files( project.id, ImageFileCreateBatch(images=tagged_images_with_regions) ) # 学習タスクをキューに入れる(ここでは1イテレーションのみ) iteration = trainer.train_project(project.id) プロジェクトの作成とファイル群のアップデートに成功すると、Custom Visionポータルからアップロードされたファイルとタグ情報を確認できます。 また、Custom Visionポータルからも画像のアップロードとタグ付けが可能です。 Custom Vision Portal 学習の確認 トレーニング終了後に、学習にデータにおける精度、再現率、平均精度を確認できます。 これにより、学習が成功したかを確認できます。 この内容はCustom VisionポータルのPerformanceタブからも確認可能です。 # interation.status == "Completed" で学習終了 iteration_id = iterations[0].id # 最新のイテレーション iteration_performance = trainer.get_iteration_performance( project_id, iteration_id ) # パフォーマンス指標の表示 print("Precision: {:.2f}".format(iteration_performance.precision)) print("Recall: {:.2f}".format(iteration_performance.recall)) print("Average Precision: {:.2f}".format(iteration_performance.average_precision)) パフォーマンス指標 モデルデプロイおよび実行 SDKよりモデルURLを取得し、ダウンロードできます。 ダウンロードされたzipファイルには、実行用のPythonソースコードとモデルデータ("*.onnx")が含まれます。 本記事のシステムでは、このモデルデータをBlobストレージに置き、ソースコードはAzure Functionにデプロイします。 # モデルのエクスポート export = trainer.export_iteration(project_id, iteration_id, "ONNX") while export.status != "Done": export = trainer.get_export(project_id, iteration_id, export.id) time.sleep(10) # URLの取得とダウンロード model_url = export.download_uri export_file = requests.get(model_url) with open("export.zip", "wb") as file: file.write(export_file.content) 実際にコードを実行すると以下のように出力され、検出物のリストが出力されます。 このリスト長を人流データとして記録します。 $ python python/onnxruntime_predict.py test_001.png [ {'probability': 0.96306247, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.6113374, 'top': 0.1495378, 'width': 0.23059763, 'height': 0.18790943}}, {'probability': 0.93494284, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.31712056, 'top': 0.43989411, 'width': 0.28013505, 'height': 0.18385402}}, {'probability': 0.86241835, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.18591469, 'top': 0.15347767, 'width': 0.22360932, 'height': 0.20324681}}, {'probability': 0.86202902, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.38832647, 'top': 0.13814446, 'width': 0.25019577, 'height': 0.22316483}}, {'probability': 0.83257216, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.04233004, 'top': 0.41069869, 'width': 0.17654971, 'height': 0.19467201}}, {'probability': 0.78080565, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.58802276, 'top': 0.39514219, 'width': 0.20800796, 'height': 0.2052871}}, {'probability': 0.66069514, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.72162047, 'top': 0.49268371, 'width': 0.24379058, 'height': 0.19499978}}, {'probability': 0.39772633, 'tagId': 0, 'tagName': 'Person', 'boundingBox': {'left': 0.18672522, 'top': 0.35396666, 'width': 0.23256654, 'height': 0.19639073}} ] 今後の展望 ブログの趣旨としてCustom Visionを用いたシステム構築のPoCに終始してきました。 その他の構成については、上で紹介したソースコードにて公開しています。 現状は学習パイプラインの起動をスケジュール実行しており、 学習データやカメラ台数の増加により計算リソース面で破綻することが想定されます。 「MLOpsレベル1:MLパイプラインの自動化」を実現するにあたり、モデル認識精度を監視し、 効率的な学習のパイプラインのトリガー策定が不可欠です。 また、実用化や精度向上にむけて、学習データの拡充やエッジ処理によるスループットやプライバシーリスクの改善を検討したいと考えています。 まとめ Azure Custom Visionを用いて、簡易的なMLフローを構築しました。 このサービス強みとして、既存の学習モデルに無いドメインや実用上の課題を解決した画像識別モデルを、容易に生成できることにあると思います。 ただし、今後ChatGPT4 with VisionなどのLLMによるプロンプトエンジニアリングで解決できるようになる可能性があり、このようなサービスはLLMや生成形AIがリーチしにくいエッジ領域にシフトしていくかと思います。 今後の発展を注視しつつ、Insight Edgeでは現状でベストな選択をしていきたいと思います。
アバター
はじめまして、Insight Edge 開発チームの綱島です。昨年4月からInsight Edgeに参画し、生成AI関連の案件を中心に、PMやエンジニアロールを担当しています。 元々エンジニアリング経験が少ないバックグラウンドなので、非技術者でもとっつきやすいローコード・ノーコード領域などを中心に積極的に調査していきたいと思っています。 今回は上記領域の中でも比較的ホットなMicrosoft Copilot Studioを使ったChatbot作成について紹介できればと思います。 この領域に興味がある方、または軽く読んでみたいと思う方、ぜひお付き合いいただければ幸いです。 はじめに Microsoft Copilot Studioを使ったChatbotの作成 1. 作成画面までの操作 2. Copilot Studioにおけるアップデート部分の組み込み ① Webサイト情報の連携 ② 独自のアップロードデータの使用 ③ 生成的な回答を可能にするトピックの紹介 ④ トピックの自動提案 3. Copilotの公開(Microsoft Teams上での利用) つまずきポイントの紹介 最後に(今後試したいこと) はじめに 「Microsoft Copilot Studio(以下Copilot Studio)」は、昨年11月に開催された「Microsoft Ignite 2023」でMicrosoftが発表した会話型AIプラットフォームで、ローコードでMicrosoft 365 Copilotを独自にカスタマイズした生成AIツール(=Copilot)を作成できるというものです。 ちなみに、Insight Edgeが内製組織としてDX推進のサポートを行っている住友商事では、昨年からCopilotの導入が進められており、Copilot Studioの生成AI機能についても、今後の業務活用に向けた検証が始まっています。それらの導入や検証のサポートにあたって、Insigh Edgeでは各種Copilotツールを検証出来る環境が整っており、日々調査を進めています。 現時点でCopilot Studioを使ってみた印象ですが、以前より提供されていたPower Virtual Agents(ノーコードのChatbot作成ツールで、現在はCopilot Studioに統合。以下PVA。)上で、生成AI機能が使えるようになった!という感じです。 Microsoft Copilot Studioを使ったChatbotの作成 今回はPVAからアップデートされた部分を中心に試しつつ、Chatbotを作成してみましたので、以下の流れでご説明します。 作成画面までの操作 Copilot Studioにおけるアップデート部分の組み込み ① Webサイト情報の連携 ② 独自のアップロードデータの使用 ③ 生成的な回答を可能にするトピックの紹介 ④ トピックの自動提案 Copilotの公開(Teams上での利用) 1. 作成画面までの操作 ホーム画面にて [+ Copilotを作成する] をクリックします。 Microsoft Copilot Studioホーム画面の抜粋。 [コパイロットを設定する]画面で、「コパイロットの名前」と「コパイロットが話す言語」を設定し、[作成]をクリックします。 言語は「英語」を指定※します。 ※現時点で、日本語では生成AIによる会話強化を利用できないため。 ※ただ、作成画面は日本語UIで、チャットのやり取りも日本語に対応していました。 下段の「生成型の回答で会話を強化する」欄では、データソースとして使用するWebサイトを設定できます。こちらは[作成]ボタンクリック後でも設定可能※です。(※次セクションでご説明します) Microsoft Copilot Studioの初回設定画面。 以下、概要画面に遷移し、ここからChatbotを作成していきます。 Microsoft Copilot Studioの概要画面。 前提として、Copilot Studioでは「トピック」と呼ばれる回答シナリオを予め定義し、基本的にはそれら既定トピックをもとに回答します。トピックに該当しない質問が投げられた際に、“生成AIによる会話強化機能”を使い、各種データソースをもとに生成型回答を返すというイメージです。 なお、トピックは[プラグイン(プレビュー)]タブで設定でき、下記キャプチャのようにノーコードでシナリオを設定することができます。 トピック設定画面。 トピックのシナリオ設定画面。 今回は上述の“生成AIによる会話強化機能”を中心に、PVA→Copilot Studioにおけるアップデート部分を試していますので、その前提で以下ご覧頂ければと思います。 2. Copilot Studioにおけるアップデート部分の組み込み ① Webサイト情報の連携 画面左の[生成AI]タブに移動し、[Webサイト]欄に、データソースとしたいWebサイトのURLを入力します。今回はInsight Edge HPの[ Company ]および[ Business ]ページを設定しました。 WebサイトURL入力画面。 上記データソースを設定のうえ、いくつか質問を試してみました。概要サマリ系とピンポイントの情報検索系の質問を投げてみましたが、どちらも正しく回答している印象です。 ちなみに、日本語で同じ質問をした場合にも正しく回答が返ってきました。 Insight Edge HPに関する質問に対するChatbot回答。 ② 独自のアップロードデータの使用 続いて、データアップロードについてもご紹介します。 先ほどと同じ [生成AI] タブの [ドキュメントをアップロードする(プレビュー)] 欄でアップロードします。 今回は Insight Edge求人サイト の、 データサイエンティスト募集要項 を英訳のうえ.txt化して取り込んでみました。 データアップロード画面のイメージ。 アップロードしたファイルにのみ記載がある標準労働時間について質問したところ、正しくデータを引用して回答が返せていました。 アップロードデータに関するChatbot回答。 ③ 生成的な回答を可能にするトピックの紹介 上述の Webサイト情報やアップロードデータを基にした生成型回答を返すためのトピック(「Conversational boosting」)について触れておきたいと思います。 本トピックは、既定トピックに定義されていない値が入力された場合に、下図の通り、”Unknown Intent”として生成AIに流し、予め設定した独自のデータソースを基に回答を生成する仕組みとなっています。 規定シナリオに基づく正確性の高いQ&Aと、生成AIによる柔軟なQ&Aのハイブリッド実装を可能にする注目機能と捉えています。 「Conversational boosting」トピックのフロー。 ④ トピックの自動提案 最後は少し毛色が違いますが、Chatbotからのトピック提案についてです。Microsoft公式ドキュメント( Microsoft Copilot Studio のシステム トピックを使用する - Microsoft Copilot Studio | Microsoft Learn )では、「システムトピック」(デフォルトで設定されているトピック)の一つとして紹介されていますが、システムが自動にやってくれると少し嬉しい機能だったのでご紹介します。 端的に言うと、ユーザーの入力内容が複数のトピックに該当しそうな場合に、それら該当トピックを表示し、ユーザーに選択するように求めるものです。試しに、Insight Edgeの福利厚生に関するトピックを2つ作成のうえ、どちらにも当てはまるような質問を投げてみました。すると、予め定義した2トピックを提示して選択を求めるフローとなりました。 (ちなみに、以下例示キャプチャの通り、使用言語は英語に設定しているものの、日本語でもやり取り可能です。) 作成トピックとChatbotの回答。 3. Copilotの公開(Microsoft Teams上での利用) Copilotの公開についても少し触れたいと思います。[公開]タブの[チャネルの構成]から、[チャネルに移動]をクリックし、[Teamsを有効にする]ことでMicrosoft Teamsでの利用が可能になります。 Microsoft Copilot Studioの公開設定画面イメージ。 Microsoft Teams上での利用イメージ。 つまずきポイントの紹介 今回Chatbot作成にあたって、少しつまずいたポイントをいくつかご紹介します。 アップロードファイル ファイル容量に制限(3MB以下)があります。 ファイルが英語でないと参照されませんでした。(今後、日本語対応が進むに連れて解消するかと思います) 作成時の設定 セカンダリ言語として日本語などを設定することも可能ですが、プライマリ言語(英語)でないと、各種トピックの更新が出来ませんでした。 最後に(今後試したいこと) 今回は少量データ且つ既定トピックも多くないので、回答精度が良かった印象ですが、今後はデータソースやトピックを増やしたうえで精度を確認していきたいと思います。 また、SharePointやOneDriveとの連携機能※1や、会話プラグイン機能※2はまだ試せていないので、それらも組み合わせたChatbotの作成にも取り組みたいと思います。 ※1: 生成型の回答に SharePoint または OneDrive for Business のコンテンツを使用する - Microsoft Copilot Studio | Microsoft Learn ※2: Microsoft Copilot 向け会話プラグインを作成する (プレビュー) - Microsoft Copilot Studio | Microsoft Learn 以上、最後まで読んでいただきありがとうございました!
アバター
こんにちは、Insigt Edgeで働いているデータサイエンティストの藤村です。 画像認識モデル開発に携わっています。 今回は、画像上の異常検知において一般的な問題である学習データ不足に着目し、その解決方法としての画像生成AI活用について解説します。 異常検知の現場課題とその重要性 品質管理における製品の微細なキズや欠陥の検出は非常に重要です。これらを解決する手段の1つとして画像認識による欠陥の異常検知がよく使われます。しかしながら、これらの欠陥は非常に稀であるため、「異常あり」の画像データを十分に集めることは一大課題となっています。具体的な例としては: 自動車製造 :ラインでの塗装の不具合や組立のズレ。 電子機器の組み立て :微細な接続エラーや部品の不良。 農業 :作物の病気や害虫による損傷。 公共インフラ :線路の点検での微細な亀裂や異物の発見。 航空機メンテナンス :機体表面の微小な損傷やひび割れ。 これらの例では、異常が極めて稀であるにもかかわらず、多様なデータセットが機械学習モデルの訓練に必要です。特に、線路点検などの公共インフラの安全管理では、稀な異常の正確な検出が事故防止に直結します。 画像生成AIのInpaint技術 Inpaint技術は、画像の一部分だけを変更・修正するためのAI技術です。これにより、元の画像の大枠を維持しつつ、必要な部分のみの変更が可能です。特に、Stable Diffusionを用いたInpaint技術は、従来の手法と比較して、異常や変更点を文章で具体的に定義することにより、AIがより精度の高い画像を生成できる点が特徴的です。 Inpaintに必要な要素は以下の通りです。 元の画像 :変更する前の基本画像。 マスク画像 :変更したい箇所を塗りつぶした画像。 変更後のプロンプト :変更を指示するテキスト。 以下の生成例は、Hugging Faceで公開されています 元画像 マスク画像 プロンプト a tiger sitting on a park bench Inpaintされた画像 出展: Hugging Face この技術により、少量の現実データから多様な学習データの生成が期待できます。 異常画像生成への応用 この技術を異常検知に応用することで以下のメリットが得られます。 既存の正常な画像に人工的に「異常」を挿入 :実際には存在しない欠陥を持つ画像の生成。 迅速かつコスト効率的なデータセット作成 :従来のデータ収集方法よりも効率的。 この技術を用いることで、既存の正常な画像に人工的に「異常」を挿入することで、実際には存在しない欠陥を疑似的に生成できます。これにより、機械学習モデルのための豊富なデータセットが容易に作成可能となります。 異常画像生成の自動化プロセス 「Stable Diffusion Web UI」や「Adobe Firefly」といったGUIベースの生成ツールもありますが、今回は異常検知を認識させるための学習データ生成が目的なので大量生成できる仕組みが必要です。 Stable Diffusion XLを用いた異常画像生成のプロセスは以下の通りです: 異常が発生する可能性のあるピクセルエリアの特定 :製品の特性や過去のデータに基づく。 Stable Diffusion XLを用いた異常画像の大量生成 :異常をランダムな位置に生成し、複数のプロンプトを用いて大量に生成。 サンプルコード 以下は、スマートフォン画面に傷がある異常を生成する例です。 以下のサンプルコードは、Stable Diffusion XLを用いた異常画像生成のプロセスを示しています。 from diffusers import StableDiffusionInpaintPipeline import torch from PIL import Image, ImageDraw # モデルの初期化 pipe = AutoPipelineForInpaint.from_pretrained( "diffusers/stable-diffusion-xl-1.0-Inpaint-0.1" , torch_dtype=torch.float16, variant= "fp16" ).to( "cuda" ) # マスク画像の準備 mask_image = Image.new( 'RGB' , ( 640 , 640 ), color= 'black' ) draw = ImageDraw.Draw(mask_image) mask_size = 400 top_left_corner = ( 110 , 110 ) bottom_right_corner = (top_left_corner[ 0 ] + mask_size, top_left_corner[ 1 ] + mask_size) draw.rectangle([top_left_corner, bottom_right_corner], fill= 'white' ) # 異常の種類を定義 anomalies = [ "a crack in the smartphone screen" , ... ] # 画像の生成 for anomaly in anomalies: inpainted_image = pipe( prompt=prompt, image=original_image, mask_image=mask_image, num_inference_steps= 20 , strength= 0.99 , guidance_scale= 10 , ).images[ 0 ] inpainted_image.save(f 'path/to/inpainted_image_{anomaly}.jpg' ) このコードを通じて、特定の異常をシミュレートし、モデルの訓練に役立つ画像を生成できました。 結論:技術の価値と今後の可能性 Stable Diffusion XLのInpaint機能により、異常検知のための学習データ不足という問題に効果的に対処できます。これは品質管理の精度を高め、多くの産業に適用可能です。また画像生成は、異常検知以外にも、新製品のデザインやシミュレーション、エンターテイメント産業など、さまざまな分野での応用が期待されています。今後の動向にも注目していきたいです。
アバター
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 (通称テックリ)という、東京工業大学・多摩美術大学・一橋大学で組成する教育プログラムについても記載していました。 が、ブログ編集から「ストーリー・メッセージがぼやける」とコメントをいただき、「たしかに…」と我に返ってゴソッと消しました笑 このテックリは想いや今後に向けてとつながっていますので、次の機会にご紹介できればと思います。それでは、また。
アバター