TECH PLAY

株式会社Insight Edge

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

165

はじめに 実施背景 概要 事例展示 トークセッション 相談窓口 その他 当日の様子 最後に はじめに こんにちは! 昨年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 (通称テックリ)という、東京工業大学・多摩美術大学・一橋大学で組成する教育プログラムについても記載していました。 が、ブログ編集から「ストーリー・メッセージがぼやける」とコメントをいただき、「たしかに…」と我に返ってゴソッと消しました笑 このテックリは想いや今後に向けてとつながっていますので、次の機会にご紹介できればと思います。それでは、また。
アバター
こんにちは! 分析チームの梶原(悠)です。今回はクラス間不均衡問題について議論します。 目次 クラス間不均衡問題とは 問題の定式化にまつわる議論 比較シミュレーションの設定 比較シミュレーションの結果 考察 クラス間不均衡問題とは 実務で扱われる多くの分類問題はクラス間のサイズに偏りがあります。 いくつかのクラスのデータが他のクラスよりも著しく多いとき、分類器は小さなクラスを無視しやすくなります。 これは、クラス間不均衡問題と呼ばれます。 問題の定式化にまつわる議論 クラス間不均衡問題は、「分類問題の構造の複雑さ」「訓練データの大きさ」「クラス間のサイズの偏り」などの条件が絡みあって起きます [1] 。 一方で、Lingらは、学習器が正解率(accuracy)の最大化を目標として訓練される分類タスクの設定自体に問題があると指摘しています [2] 。 そして不均衡問題には、ビジネスコストを考慮した学習で効果的に対処できると主張しています。また、訓練データが小さいことの悪影響は、不均衡とは別の問題であるとする立場をとっています。 本稿では「クラス間不均衡問題への対処にあたって、ビジネスコストを正確に考慮する必要があるか?」という論点を実務寄りの観点から検討してみます。 比較シミュレーションの設定 実務上は、ビジネスインパクトに強く影響する要因が重要で、影響しない要因は無視できます。 今回は、きわめて単純化された仮想企業の損益を、乱数実験によりシミュレートし、クラス間不均衡問題への対処手法のビジネスインパクトを比較してみます。 仮想企業Y社のビジネス設定. 仮想企業Y社は、農作物の種子を売り掛け販売し、一定の支払猶予期間ののち、代金を回収するビジネスを営んでいます。 Y社は、種子のメーカーZ社から単価95円で種子を調達し、顧客企業に単価120円で販売します。 顧客企業Z社は、購入した種子から花を育てて花市場で販売し、得られた売上からY社に代金の支払いを行います。 顧客企業Z社は、花の不作や価格下落により資金繰りができなくなるとY社に代金を支払うことなく倒産します。 仮想企業Y社は、1,000社の顧客企業のそれぞれと月に数回の頻度で取引をします。倒産による貸し倒れの割合は1%です。 仮想企業Y社のビジネスプロセス ダミーデータの生成. 乱数シミュレーションにより、上記設定のY社と顧客企業との契約データを生成しました。 ビジネスインパクトの評価. 契約時点のデータから倒産による貸し倒れを予測するモデルを構築します。 倒産が予測される場合は、業務に介入し、契約を未然に取り止めます。 介入しない場合と比較した貸し倒れや機会損失の影響を考慮すると、種子1個あたりのビジネスコストは以下になります。 実績="継続" 実績="倒産" 予測="継続" 0円 0円 予測="倒産" +(120-95)円 -95円 このコストを累積したものは、介入あり・なしの累積損益の差に一致します。 これを介入のビジネスインパクト(pl_impact)とします。 ビジネスインパクト=累積損益(介入あり)-累積損益(介入なし) 不均衡対応の手法 倒産の予測モデルはすべてlightGBMで作成し、不均衡対応は以下の5手法を比較しました。なお、説明変数は、「種子の個数」「支払い猶予期間」「花の市場価格」の3つのみを用います。 クラス間不均衡に対処しない. (NAIVE) モデルの出力した倒産確率をビジネスインパクトが最大になる閾値で切る. (OPTIMIZED_THRESHOLD) サンプルの重みによるコスト考慮学習で対処する. (COST_SENSITIVE_LEARNING) [4] SMOTE (Synthetic Minority Oversampling Technique)で前処理する. (SMOTE) [5] 損失関数をFocal lossにする. (FOCAL_LOSS) [6] 学習・検証データ分割 学習データは顧客企業500社との契約データからランダムサンプリングし、 検証データは残り500社の顧客企業との契約データの全量とします。 比較シミュレーションの結果 不均衡問題の確認. 不均衡に対処しないモデル(NAIVE)による予測の精度を確認します。今回のデータとモデルはかなり構造が単純ですが、不均衡の悪影響が見られます。また、学習データが小さくなると、不均衡問題の悪影響も強まっています。(各値は、学習における乱数シードを変えた20回の試行の中央値。) 学習サンプルサイズ 倒産確率 (実績値) 倒産確率 (推定値) MCC 正解率 (Accuracy) 再現率 (Recall) Cohen's kappa AUC Brier score 500 個 0.014 0.011 0.620 0.991 0.556 0.604 0.775 0.0088 20,000 個 0.014 0.011 0.715 0.993 0.649 0.712 0.823 0.0060 手法のビジネスインパクトの比較 ビジネスインパクトの比較では、ビジネス構造の知識を持たないSMOTEが、コスト考慮学習よりも良い結果となりました。この事例の不均衡データでは、タスクの定式化にビジネス構造を取り込むことよりも、訓練データが小さいことへの対処の方が、実務上重要であると考えられます。(各値は、学習における乱数シードを変えた20回の試行の中央値。) 各手法のビジネスインパクト 考察 今回はダミーデータが小さすぎて確認できませんでしたが、訓練データのサイズを大きくしていけば、漸近的には、どの手法のビジネスインパクトも同じになると期待されます。すなわち、漸近的にはビジネス構造の知識を持つ手法と持たない手法は同等になるはずです。 一方、訓練データのサイズが小さい非漸近的な領域では、ビジネス構造の知識を持たないSMOTEの方が、コスト考慮学習より大きなビジネスインパクトを持つ現象が確認できました。この事例においては、不均衡問題の本質が、分類タスクの定式化におけるビジネス構造の不在ではなく、マイナークラスの少数データ問題側にあることを示唆すると思われます。 だとすると、コスト考慮学習による方向を追求しても、実務上の意味のある形で不均衡問題への視座を得ることはできないのではないか、という疑いを覚えます。Jamalらはクラスの均衡化をドメイン適応の問題として捉える視点を紹介しています [3] 。クラス不均衡問題の正しい定式化は、あるいは、こうした方向にロバスト性が加えられたようなものかもしれないと想像します。 [1] Japkowicz, N., & Stephen, S. (2002). The class imbalance problem: A systematic study. Intelligent data analysis, 6(5), 429-449. [2] Ling, C. X., & Sheng, V. S. (2008). Cost-sensitive learning and the class imbalance problem. [3] Jamal, M. A., Brown, M., Yang, M. H., Wang, L., & Gong, B. (2020). Rethinking class-balanced methods for long-tailed visual recognition from a domain adaptation perspective. In Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition (pp. 7610-7619). [4] Elkan, C. (2001, August). The foundations of cost-sensitive learning. In International joint conference on artificial intelligence (Vol. 17, No. 1, pp. 973-978). Lawrence Erlbaum Associates Ltd [5] Chawla, N. V., Bowyer, K. W., Hall, L. O., & Kegelmeyer, W. P. (2002). SMOTE: synthetic minority over-sampling technique. Journal of artificial intelligence research, 16, 321-357. [6] Lin, T. Y., Goyal, P., Girshick, R., He, K., & Dollár, P. (2017). Focal loss for dense object detection. In Proceedings of the IEEE international conference on computer vision (pp. 2980-2988).
アバター
Introduction こんにちは、住友商事のDXを加速する為の技術専門会社である Insight Edge にてデータサイエンティストをしております佐藤優矢です。 今回は、前職のメガベンチャーで担当していたレコメンドエンジンを、その歴史的経緯を踏まえて、協調フィルタリングからニューラルネットを用いたモデルの構築•評価までご紹介したいと思います。 目次 初期のレコメンドエンジン 協調フィルタリングの改善策 協調フィルタリングを超える精度のレコメンドエンジン 独自アルゴリズムの精度評価基盤の構築方法 レコメンドエンジンの精度を測る指標 まとめ 初期のレコメンドエンジン レコメンドエンジン初期は協調フィルタリングによるレコメンドが主な方法でした。 概要としては、ユーザーとアイテムからなるマトリックスを作成し、このユーザーがこのアイテムを購入したなどの情報をマトリックスに埋めていく方法をとります。 よく使われるのは、Alternating Least Square という最適化アルゴリズムであり、このソルバーがpythonで使えるので、それを用いると推定スコア(おすすめ度)が得られます。 長所としては、手軽に精度の良いレコメンドエンジンが作れることで、短所としては、特にマトリックスがスパースな状況では精度が悪いことです。 協調フィルタリングの改善策 歴史的に、協調フィルタリングには様々な改善策が考えられています。 アイテムベクトルによる類似度と混ぜてみる アイテムの説明文をベクトル化するなど アイテムの類似度と、協調フィルタリングによるおすすめ度を混ぜ合わせる形でおすすめ度を算出する。「正係数による距離の重み付け和はまた距離になる」に近い考え方で算出する 私が試したところ、あまり効果はなかった ユーザーベクトルによる類似度と混ぜてみる ユーザーのbioをベクトル化するなど 私が試したところ、これもあまり効果はなかった ちなみに、協調フィルタリングをナイーブにニューラルネット化したモデルでは精度が逆に落ちることがわかっています。 協調フィルタリングを超える精度のレコメンドエンジン 最近になって、Variational Auto-Encoder を用いたモデルなど、協調フィルタリングを超える精度のニューラルネットベースのレコメンドエンジンが開発されてきています。 それらを一気に試すフレームワークにRecBoleがあり、メガベンチャーの人と話したところ、多くのメガベンチャーの会社が使っているようでした。 使い方は、クックパッドでの 紹介記事 に詳しく載っています。 どのモデルを選べば良いかですが、データによって異なるので一概には言えません。ただ、マトリックスがスパースな時に精度が良かったモデルは、グラフニューラルネットを使ったモデルでした。ユーザーに繋がっているアイテムがあり、それに繋がっているユーザーがあるという形に連結していき、同じものを購入したユーザー同士を繋げるようなアルゴリズムになっています。 これらは先のクックパットの結果と大きく異なり、データによって最適な手法は異なることを表しています。 独自アルゴリズムの精度評価基盤の構築方法 最新のアルゴリズムはRecBoleにアップデートされることが多いですが、if-thenなどの独自アルゴリズムを後処理として付け加える(フィルターなど)ことが考えられます。その際は独自に評価基盤を整えます。 特定の精度指標による評価をアルゴリズムとして実装し、その評価指標による精度をもとに、モデルがいままでより良いものかどうかを判断していきます。 指標は Precision@k と Recall@k がよく用いられます。 レコメンドエンジンの精度を測る指標 Precision@k と Recall@k の定義の違いを考えると、異なる部分は分母のうち、正解データに絞るかどうかという点です。 専門書などでも触れられているのをみたことはありませんが、Precision@kだと、取れた正解データ(実際のユーザーの遷移データなどから取ってきます)の量が少ないと小さく見積もられてしまいます。データの量に応じて精度が変わってくるということが起こるわけです。 それに対して、Recall@kではその影響が抑えられています。なので、レコメンドエンジンのモデルの精度を測る時の指標はRecall@kがよく用いられるということかと思います。 まとめ 本稿では、歴史的によく用いられてきた協調フィルタリングから、ニューラルネットによるモデルのツールによる実装、またその最新モデルをカスタムした状況で必要になる評価基盤についてざっと振り返りました。この記事からさらに深い議論が起こると嬉しいです。
アバター
目次 導入 動画紹介1 動画紹介2 動画紹介3 動画紹介4 まとめ 導入 こんにちは。InsightEdgeのデータサイエンティストの小柳です。 本記事では前回の記事で紹介したPyMCの使い方を学ぶ一環として、PyMConの紹介をしようと思います。 突然ですが、PyMCに限らずモジュールの使い方を学ぶ際にはいくつか困難がありますよね。 PyMC固有の話とそうでない話を混ぜて挙げてみると 使う人があまり多くない。機械学習ならいざしらず、統計系の人は組織に少なくなりがち。したがって詳しい人が近くにおらずあまり人に聞けない。 公式ドキュメントを読んでもなんだかよくわからなかったり、なぜ例としているコードになったのかがわからないことがある。使用例のコードがない時もある。 教科書がない。あったとしても新しくて効率的な構文のキャッチアップができないことが多い。 これらを克服するためには忍耐強くデモコードを読み解いたり、実装を遡る必要があり大変です。 今回取り上げるベイズ統計用のMCMCサンプラーであるPyMCはコミュニティ作りを意識的にしており、PyMConウェブシリーズという名前でYouTubeに動画投稿をしています。 このシリーズの動画はPyMCの開発者や世界中で活躍するエンジニア、学校の先生たちがケーススタディー的に教えてくれていてすごくありがたいです。最新の機能のキャッチアップにもなります。 今回はこの動画シリーズの紹介をしようと思います。自分のやりたい分析に近いものがあったら見てみると新たな発見があるかもしれません。 PyMConは2020年位に始まったWebシリーズで、2020年に投稿されたものは'PyMCon 2020'とサムネに書いてあリますが、それ以降のやつは年度を切るのをやめてしまったみたいです。 全部紹介しているときりがないので、2023年に投稿されたものをいくつか紹介していきます。 動画紹介1 リンク: https://www.youtube.com/watch?v=0B3xbrGHPx0 テーマ:Automatic Probability スピーカー:Ricardo Vieira (PyMC Labs所属) Automatic Probabilityは自動微分のアナロジーです。 自動微分は偏微分を実行するルーティンであり、チェインルール、指数の微分ルール、積や商の微分ルール等を用いて機械的に微分を計算します。機械学習には必須で、最適化の勾配法やMCMCのNUTS、変分法に使われます。 一方自動確率は確率を自動で計算できるようにしたルーティンのセットで、列挙や、変数の交換などなどを行い確率を計算します。ベイズ統計には必須であり当然PyMCの内部でも使われている技術で、その紹介動画です。 内容はPyMCとそのバックエンドであるPyTensorを使った単純な分布の確率密度の計算や、変数変換時の確率密度計算、打ち切り分布の密度計算、条件付き確率密度の計算、混合分布の確率密度計算等です。 いくつか例示してみます。例えば確率密度の計算は以下の通りです。\ ->のところは出力だと思ってください。 import pymc as pm x = pm.Normal.dist() z = x + 5 def prob(rv, value): return pm.logp(rv, value).exp() prob(x,0.98146052).eval() -> array(0.24645622) prob(z,5.98146052).eval() -> array(0.24645622) このように、zの分布を指定していないのに変数変換ができています。 これは正規分布が平行移動しているだけなので難しくないですがそうでない場合も対応可能です。 例えば1対1変換で import pytensor.tensor as pt x = pm.Normal.dist() z = pt.exp(x) prob(x,-1.18149583).eval() -> array(0.1985122) prob(z,0.30681945).eval() -> array(0.64700006) と計算できます。ヤコビアンをかけてきちんと変数変換できているか確認してみましょう。 import numpy as np prob(z,0.30681945).eval()*np.exp(-1.18149583) -> 0.19851219823788108 うまくいっていますね。 次は多対1変換。実はこれもできます。 x = pm.Normal.dist(0,1) z = pt.abs(x) np.isclos( prob(x,0.3).eval(), (prob(z,0.3) /2).eval() ) -> True 打ち切り変換もできます。 rv = pm.Normal.dist(shape=(3,)) clipped_rv = pt.clip(rv, -1, 2) censored_rv = pm.Censored.dist(rv, lower=-1, upper=2) clipped_value = [-1, 0.5, 2] print( prob(clopped_rv, clipped_value).eval(), prob(censored_rv, clipped_value).eval(), sep="/n" ) ->[0.159 0.352 0.023] ->[0.159 0.352 0.023] 確率変数が2つ以上の時の確率密度や、条件付き確率密度も算出できます。 import pytensor import pytensor.tensor as pt from pymc.logprob.basic import conditional_logp x = pm.Normal.dist() y = pm.Normal.dist() z = x-y def conditional_prob(rvs_to_values: dict, fn=True): logps = conditional_logp(rvs_to_values).values() probs = [logp.exp() for logp in logps] if fn: values = list(rvs_to_values.values()) return pytensor.function(values, probs) else: return probs x_value = pt.scalar('x_value') y_value = pt.scalar('y_value') z_value = pt.scalar('z_value') prob_fn = conditional_prob({x: x_value, y:y_value}) print(prob_fn(x_value=0.5, y_value=0.9)) ->[array(0.35206533), array(0.26608525)] prob_fn = conditional_prob({x: x_value, z:z_value}) print(prob_fn(x_value=0.5, z_value=0.9+0.5)) ->[array(0.35206533), array(0.26608525)] prob_fn = conditional_prob({y: y_value, z:z_value}) print(prob_fn(y_value=0.9, z_value=0.5-0.9)) ->[array(0.26608525), array(0.35206533)] このような感じで、条件付き分布の確率密度が柔軟に計算できます。 直接使う方法はすぐには出てきませんが、PyMCとPyTensorの柔軟性を垣間見られて面白いかと思います。 動画紹介2 リンク: https://www.youtube.com/watch?v=b47wmTdcICE&t=1455s テーマ:Bayesian Causal Modeling スピーカー:Thomas Wiecki (PyMC Labs所属) 最近界隈で盛り上がっている因果推論系がテーマです。前半は実際にベイズを使った因果推論ですが、後半は最近出たPyMCシリーズのマーケティング用ツールPyMC-Marketingを使ったケーススタディで、ベイズとMMMを使って広告効果モデリングをした話です。 動画の詳細欄を見ればわかるのですが、スライドとコードが公開されているので詳細には立ち入らずちょっとした紹介程度にします。 前半はオーソドックスな因果推論の話で、扱う題材も広告効果です。 背景としてある商品の売上を上げるために、Google Adsでネット広告とTVCMを行った時に、本当にGoogle Adsに効果があるのかを知りたいという状況を考えます。 Google Adsを打った時と打っていない時での架空の売上のヒストグラムは以下の通りで、 引用: https://www.pymc-labs.com/blog-posts/causal-analysis-with-pymc-answering-what-if-with-the-new-do-operator/ t検定やベイズ的検定のどれを使ってもバッチリ差があると判定される結果です。 実はこのデータは人工的に作られたもので、その生成過程的には効果なしと出て欲しいものです。 さて、この状況に対し3つの変数とそれらの生成過程を以下のようにを考えます。 C:TVCM費用。連続値を取る。売上に影響を及ぼすし、相乗効果を狙うためGoogle Adsのオンオフにも影響を与える。 Z:Google Adsのオンオフ。0か1の二値を取る。売上に影響を与える。 Y:売上。連続値を取る。 この時のデータ生成過程を以下のように考えます。 このモデルに対し適当な事前分布をおいてやり、 の事後分布を見てやれば良いわけです。 今回の話は本来それで終わらせられるのですが、PyMCは5.8から因果推論系の機能を足したようでdo-operator等が紹介されています。本動画はそれを用いており、モデルの書き方は以下のようになります。ここまでは通常のモデリングと同じです。 with pm.Model() as model: # CからY,Zへの効果の事前分布 beta_c = pm.Normal('beta_c', shape=3) # ZからYへの効果の事前分布 beta_z = pm.Normal('beta_z', shape=2) # Yの観測ノイズ sigma_y = pm.HalfNormal('sigma_y') c = pm.Normal('c', mu=0, sigma=1, observed=tv_spend) z = pm.Bernoulli('z', p=pm.invlogit(beta_z[0] + beta_z[1]*c), observed=search_spend) y_mu = pm.Deterministic('y_mu', beta_c[0] + (beta_c[1] * z) + (beta_c[2] * c)) y = pm.Normal('y', mu=y_mu, sigma=sigma_y, observed=sales) idata = pm.sample() これに対して、do-operatorを用いてzをCから切り離して決定するモデルを定義しさらにそこからYをサンプリングするのは以下のコードになります。 from pymc_experimental.model_transform.conditioning import do model_z0 = do(model, {'z': 0}) model_z1 = do(model, {'z': 1}) with model_z0: idata_z0 = pm.sample_posterior_predictive(idata) with model_z1: idata_z1 = pm.sample_posterior_predictive(idata) そして、平均介入効果の計算も簡単です。 ATE = idata_z1.predictions - idata_z0.predictions このように非常に簡単にATEが計算できるようになっています。実際にこのデータでATEを算出すると、-0.1から0.1のあたりに分布しており、無事効果がないことがわかります。 今回挙げたような線形な関係性であればこれを使う必要はありませんが、非線形な効果が絡んでくるようなモデルを考える場合は便利になりそうです。 後半はMMMをベイズと組み合わせ分析のケーススタディになっています。実はここで紹介されているものはPyMC Labsが分析した事例で、PyMCon2020の動画に詳しい説明があるので興味があればご覧ください。 あまりMMMに詳しくないので、この事例では通常固定値とされるMMMのパラメータ時間とともに推移するようにモデリングしたようで、同様のモデルがPyMC-Marketingで使えるようです。 PyMC-Marketingは名前の通り広告効果分析に特化していて、ROAS(広告の費用対効果らしいです)や広告費を事後的にあの時いくらにしていたら的な分析、CLV(Customer Lifetime Value)推定等マーケティングで知りたくなりそうな分析は揃っているようです。 動画紹介3 リンク: https://www.youtube.com/watch?v=2dGrN8JGd_w テーマ:A Soccer-Factor-Model スピーカー:Maximilian Goebel (Università Bocconi所属) そもそもサッカーファクターモデルとは何でしょうか? 私は聞いたことがありませんでした。 これは金融業界で使われるファクターモデルというものから派生したものだそうです。 ファクターモデルとはリスク資産のリターンのモデルで、資産iのリターンを 1.その資産固有のリターン、2.すべての資産に影響を与える複数の要因の影響の線形結合、3.エラー項の3つの和として分解したもののようです。 サッカーファクターモデルよりは知名度があるようで、ファクターモデルでGoogle検索すると このページ を筆頭にいくつもヒットしますし、Wikipediaにも ページ があります。サッカーの方はヒットしません。 そのファクターモデルを使うことのご利益は2つあります。1つ目はファンドが市況の影響をどのように受けるかが推定できることです。そして2つ目はファンドのパフォーマンスから市況の影響を除去することでマネージャーがどれだけ優秀か=どれだけリターンを高められるかを推定できることです。 この2つ目のご利益を横展開して、チームスポーツにおいて個人の強さを推定してやろうというのが今回の動画のテーマです。 本動画ではサッカーをモチーフに各選手が試合でゴールを決める確率をモデリングします。 この が以下の確率を持つベルヌーイ分布に従うようなモデルを考えます。 これの各パラメータ、特に を見ることで、その選手のパフォーマンスから例えばチーム全体の調子や、チームのアシスト力のような要素を除去し本来のパフォーマンスを測ることができるのだそうです。 当然この手法の応用先は別にサッカーに限らず、チームスポーツだったり営業チームのパフォーマンスだったりと様々な例が考えられそうです。 モデルの良し悪しはROC、LogProb、RMSE等で測定していましたが、これは取り扱うテーマ次第でしょう。 分析に関してはそれほど難しい話ではないので省略します。 アイデアの面白さや応用先の広さ、ファクターをどうするか等を考えると分析者の力量が出そうなトピックでした。 動画紹介4 リンク: https://www.youtube.com/watch?v=ri5sJAdcYHk テーマ: Introduction to Hilbert Space GPs in PyMC スピーカー:Bill Engels(PyMC Labs所属) この動画のテーマはヒルベルト空間ガウス過程です。 私はそこまでガウス過程に詳しくないので正直このまとめはあまり自信がありません。 この動画ではそんなガウス過程初心者のために通常のガウス過程の特徴や長短所そしてその短所の乗り越え方としてのHSGP(Hilbert Space Gauss Process)を解説してくれています。といっても別に理論を詳細に扱うわけではありません。 ガウス過程の長所として 柔軟なモデリング。線形モデルもニューラルネットワークも可能 解釈可能。モデリング次第だが解釈可能なモデルも作れる。 不確実性の定量化。各地点の分散も計算可能 一方圧倒的な短所として 遅い。計算量が 。 が挙げられます。この短所に対して多くのアプローチが提案されており、その1つが今回の動画で紹介されているヒルベルト空間ガウス過程です。もちろんいつでも使えるわけではなく、 PPL(Probabilistic Programming Language:確率的プログラミング言語)を使う必要あり 入力変数次元は最大4 カーネル関数に制限あり 等の制約があります。このような特徴なので他のPPLでも実装できて、StanやBRMS、NumPyroで実施した例があるようです。 さて、PyMCでの実装です。まず通常のガウス過程をPyMCで実施する際には以下のようにします。 (注意:このコードはこのままでは動きません。詳細は https://github.com/bwengals/hsgp/blob/main/cherry_blossoms_hsgp.ipynb を参照ください。動画と同じコードがあります。) with pm.Model(coords=coords) as model: # Set prior on GP hyperparameters eta = pm.Exponential("eta", lam=0.25) ell_params = pm.find_constrained_prior( pm.InverseGamma, lower=10, upper=200, init_guess={"alpha": 2, "beta": 20}, ) ell = pm.InverseGamma("ell", **ell_params) # Specify covariance function and GP cov = eta**2 * pm.gp.cov.Matern52(1, ls=ell) gp = pm.gp.Latent(cov_func=cov) f = gp.prior("f", X=df["year"].values[:, None], dims="year") これをHSGPにするには gp = pm.gp.HSGP(m=[400], c=2.0, cov_func=cov) とするだけで、非常に簡単です。 PyMCを使ってHSGPを行うことにはいくつか長所があります。 インプットデータの次元を変えるのが簡単でpm.gp.HSGPの引数mを変えればよいだけ。 共分散関数を加法的に定義できる。covを2つ書いてHSGPの引数にcov_func=cov1+cov2をとるだけ。 好きなカーネル関数が書きやすい。よく使われる二乗指数カーネル、指数カーネル、Maternカーネルあたりはすでに実装されていますが、変わったものが使うことも可能。 基底と共分散を線形化した形で出力できる。これを使うことで予測時に高速に計算可能。 私があまりガウス過程に詳しくないのでこの機能の素晴らしさや使いやすさが伝えきれなかったかもしれませんが、ガウス過程の理解に役立てば幸いです。 まとめ 本記事ではPyMConの紹介をするための動画をいくつか紹介しました。各動画のテーマはバラエティに富んでいて、この記事を読むような方々なら恐らくどれかにはご興味いただけるかと思いますし、新しい知識を得るのに非常に役立ちそうな予感は感じられると思います。 PyMC界隈、特に日本語コミュニティはまだまだ活発になる余地は十分にあると思います。本記事がその一助になれば幸いです。 Insight EdgeではPyMCに限らず、様々な技術に興味を持っているデータサイエンティス・エンジニアを募集しています! カジュアル面談も行っていますので、興味がある方は以下サイトを御覧ください! recruit.insightedge.jp
アバター
こんにちは。Insight EdgeでDeveloperをしている熊田です。 昨今はインフラ環境を構築する際に、Infrastructure as Code(IaC)を検討することが多くなっているかと思います。 これまで私自身はIaCに触れてきませんでしたが、Terraformを使ってWebアプリを構築してみたいとは常々思っていました。 そんなときに、Streamlitフレームワークで開発したアプリをGCP上に構築する機会がありましたので、そのとき使用したコード等を本記事で紹介したいと思います。 目次 Streamlitについて インフラ/セキュリティ要件 構築してみた Terraformで構築する つまずきポイント まとめ/感想 Streamlitについて Streamlitをご存知でない方に向けての説明になりますが、StreamlitはPythonのWebアプリケーションフレームワークです。一番の特徴は、フロントエンドもPythonで書けることかと思います。 加えて、直感的にUI実装できるので学習コストが低く、幅広くチャートライブラリをサポートしています。そのような特徴から、データ分析結果をお手軽にダッシュボードで可視化したいときに便利です。 そういった利点があり、データサイエンティストを多く抱える弊社では、Streamlitを使ったアプリ開発が増えてきています。 以下はStreamlitを使ったサンプルコードです。 ### main.py ### import streamlit as st import pandas as pd import numpy as np # タイトル st.title( "Streamlit Sample" ) # サイドバー with st.sidebar: st.title( "[MENU]" ) option = st.sidebar.radio( "表示形式を選択してください" , ( "折れ線グラフ" , "棒グラフ" , "面グラフ" , "テーブル" )) # ランダムなデータを生成 chart_data = pd.DataFrame(np.random.randn( 20 , 3 ), columns=[ "a" , "b" , "c" ]) # チャートを表示 if option == "折れ線グラフ" : st.line_chart(chart_data) elif option == "棒グラフ" : st.bar_chart(chart_data) elif option == "面グラフ" : st.area_chart(chart_data) elif option == "テーブル" : st.table(chart_data) アプリ画面 インフラ/セキュリティ要件 Webアプリを構築するにあたってのインフラ要件を確認していきます。 Webアプリ開発の前段であるデータ分析にてGCPのサービスが使われており、Webアプリも同クラウドプラットフォーム上で完結させたかったため、今回はGCPで構築します。 StreamlitがWebSocket通信をするため、コンテナ実行環境はWebSocket通信をサポートしているCloud Runを利用することとしました。 セキュリティ要件としては、HTTPS通信、DDos対策、ユーザ認証を行う必要がありました。 加えて、今回のアプリは弊社開発メンバとエンドユーザの双方がアクセスするのですが、以下のような制約がありました。 弊社開発メンバ エンドユーザ Googleアカウントあり Googleアカウントなし VPNなし (固定IPなし) VPNあり (固定IPあり) そのため、弊社開発メンバとエンドユーザの認証方法を分けることにし、弊社開発メンバはSSO認証、 エンドユーザはEmail/PW認証&IP制限を設けて、セキュリティ要件を満たすようにしました。 セキュリティ対策では、GCPの以下サービスを利用して構築します。なお、本記事では各サービス機能の説明はしませんので、ご了承ください。 Load Balancer Cloud Armor Identity-Aware Proxy(IAP) Identity Platform 全体のインフラ構成図は以下の通りです。 構成図 構築してみた 実際に使用したコードをお見せして、つまづいたポイントなどを紹介したいと思います。 ### Dockerfile ### FROM --platform=linux/amd64 python:3.10 RUN pip install streamlit COPY src /app/src WORKDIR /app/src CMD ["streamlit", "run", "main.py"] ### docker-build.sh ### PROJECT_ID = < your-project-id > REGION = < your-region > PREFIX = < your-prefix > REPOSITORY = $REGION -docker.pkg.dev/ $PROJECT_ID / $PREFIX -repo IMAGE =streamlit-image TAG =latest APP = $PREFIX -app docker image build -t $IMAGE : $TAG . docker tag $IMAGE : $TAG $REPOSITORY / $IMAGE : $TAG docker push $REPOSITORY / $IMAGE : $TAG gcloud run deploy $APP \ --project $PROJECT_ID \ --image $REPOSITORY / $IMAGE : $TAG \ --platform managed \ --region $REGION \ --ingress internal-and-cloud-load-balancing \ --allow-unauthenticated \ --memory 2Gi \ --cpu 1 \ --max-instances 1 \ --timeout 900 \ --concurrency 80 \ --port 8501 Cloud Runをデプロイするために、Dockerfileとshellスクリプトを作成しています。 初期構築時はTerraformからshellスクリプトを実行します。初期構築後、Cloud Runをデプロイし直したい時はshellスクリプトのみを実行することを想定しています。 なお、外部ネットワークからCloud Runへ直接アクセスさせず、必ずLoad Balancerを経由させたいので、 gcloud run deploy のオプションに --ingress internal-and-cloud-load-balancing を指定しています。 Terraformで構築する Terraformを使って構築するにあたり、以下作業を行うことを前提としています。  1. Terraform実行用サービスアカウントの作成  2. ドメインの取得、DNSレコードの設定  3. Google Cloud APIsをコンソールから有効化  4. OAuth同意画面をコンソールから設定(こちらの記事の OAuth同意画面を設定する を参考にしました)  5. Identity Platformの設定(こちらの ユーザ作成 まで参照ください) 実行するTerraformコード(ver:1.4.7)は以下の通りです。 ### variables.tf ### variable "project_id" { description = "project id" type = string default = <your-project-id> } variable "project_number" { description = "project number" type = string default = <your-project-number> } variable "default_region" { description = "The default region for resources" default = <your-region> } variable "credentials_file" { description = "The path to the credentials file" default = <your-credentials-file> } variable "prefix" { description = "The prefix to use for all resources" default = "streamlit-app" } variable "oauth_domain" { description = "The domain to use for oauth" default = <your-oauth-domain> } variable "non_oauth_domain" { description = "The domain to use for non-oauth" default = <your-non-oauth-domain> } ### main.tf ### ## Provider ## provider "google" { credentials = file (var.credentials_file) project = var.project_id region = var.default_region } data "google_cloud_run_service" "default" { name = "$ { var.prefix } -app" location = var.default_region depends_on = [ google_artifact_registry_repository.my-repo ] } ## Artifact Registry ## resource "google_artifact_registry_repository" "my-repo" { location = var.default_region repository_id = "$ { var.prefix } -repo" description = "docker repository" format = "DOCKER" provisioner "local-exec" { command = "sh docker-build.sh" } } ## SSL Certificate ## resource "google_compute_managed_ssl_certificate" "default" { name = "$ { var.prefix } -ssl-certificate" managed { domains = [ var.oauth_domain, var.non_oauth_domain, ] } } ## Network Endpoint Group ## resource "google_compute_region_network_endpoint_group" "default" { name = "$ { var.prefix } -neg" region = var.default_region network_endpoint_type = "SERVERLESS" cloud_run { service = data.google_cloud_run_service.default.name } } ## Load Balancer Backend Service ## resource "google_compute_backend_service" "oauth" { name = "$ { var.prefix } -oauth-backend-service" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = google_iap_client.oauth_client.client_id oauth2_client_secret = google_iap_client.oauth_client.secret } depends_on = [ google_iap_client.oauth_client ] } resource "google_compute_backend_service" "non-oauth" { name = "$ { var.prefix } -non-oauth-backend-service" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = google_iap_client.non-oauth_client.client_id oauth2_client_secret = google_iap_client.non-oauth_client.secret } security_policy = google_compute_security_policy.non-oauth.id } ## Load Balancer ## resource "google_compute_url_map" "default" { name = "$ { var.prefix } -url-map" default_service = google_compute_backend_service.oauth.id host_rule { hosts = [ var.non_oauth_domain ] path_matcher = "non-oauth" } path_matcher { name = "non-oauth" default_service = google_compute_backend_service.non-oauth.id } } ## Load Balancer Proxy ## resource "google_compute_target_https_proxy" "default" { name = "$ { var.prefix } -https-proxy" url_map = google_compute_url_map.default.id ssl_certificates = [ google_compute_managed_ssl_certificate.default.id ] } ## Load Balancer Forwarding Rule ## resource "google_compute_global_forwarding_rule" "oauth" { name = "$ { var.prefix } -oauth-forwarding-rule" target = google_compute_target_https_proxy.default.id ip_address = google_compute_global_address.oauth.address port_range = "443-443" } resource "google_compute_global_forwarding_rule" "non-oauth" { name = "$ { var.prefix } -non-oauth-forwarding-rule" target = google_compute_target_https_proxy.default.id ip_address = google_compute_global_address.non-oauth.address port_range = "443-443" } ## IP Address ## resource "google_compute_global_address" "oauth" { name = "$ { var.prefix } -oauth-global-address" } resource "google_compute_global_address" "non-oauth" { name = "$ { var.prefix } -non-oauth-global-address" } ## IAP OAuth Client ## resource "google_iap_client" "oauth_client" { display_name = "IAP OAuth Client" brand = "projects/$ { var.project_number } /brands/$ { var.project_number } " } resource "google_iap_client" "non-oauth_client" { display_name = "IAP Non OAuth Client" brand = "projects/$ { var.project_number } /brands/$ { var.project_number } " } ## IAP IAM ## resource "google_iap_web_backend_service_iam_binding" "default" { web_backend_service = google_compute_backend_service.oauth.name role = "roles/iap.httpsResourceAccessor" members = [ "domain:<your-domain>" , "user:<your-user>" , ] } ## Cloud Armor ## resource "google_compute_security_policy" "non-oauth" { name = "$ { var.prefix } -non-oauth-security-policy" description = "non-oauth security policy" rule { action = "allow" priority = 0 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = [ "<your-ip>" ] } } description = "allow rule" } rule { action = "deny(403)" priority = 2147483647 match { versioned_expr = "SRC_IPS_V1" config { src_ip_ranges = [ "*" ] } } description = "default deny rule" } } 上記Terrafomコードですが、IAPのEmail/PW認証に関する設定が不十分な(いま現在、Terraformがサポートしていない)ため、 ここからはコンソール上で設定していきます。まずは、外部IDの承認をします。 外部ID承認 認証ページ設定 これにより、Email/PWを利用しての認証ページが自動作成されます。アクセスしてみると以下画面になります。 認証ページ なお、システム管理者のみにユーザ追加、削除させたいのでIdentity Platformの設定をコンソールから変更しています。 Identity Platform設定 手っ取り早く外部IDを利用した簡単な認証を作成できるのが良いですね。 Email以外にもMicrosoftアカウントやX(旧Twitter)アカウントなども選択できるので、いつか試してみたいです。 つまずきポイント Terrafom実行でつまずいたところは色々ありましたが、大きなところでいうと2点です。 まず1点目は、OAuth同意画面の設定です。前提④で実行した内容ですが、 当初は google_iap_brand を利用してTerraformで設定しようと考えていました。しかし、applyしようとすると Error 400: Support email is not allowed のエラーが出てしまいました。 調べてみても有効な解決策を見つけられませんでしたが、どうやらOAuth同意画面の設定はTerraformでの管理に向かないようです。 というのも、APIを介してのOAuth同意画面の変更、削除はできないとのことです。変更するにはコンソールから操作する必要があります。 であれば、Terraformで管理することは必須でありませんので、コンソール上から設定することにしました。 ※調査の際は、こちらの記事を参考にさせていただきました。 Terraform で IAP 設定する 2点目は、Terraform構築が無事完了しアクセスしてOAuth認証も成功して、 アプリのトップ画面を開こうとすると The IAP service account is not provisioned のエラーが出たところです。 公式ドキュメント によると、 IAPサービスアカウントを作成しないとCloud Runを起動できず、このエラーになるようです。 gcloud beta services identity create --service=iap.googleapis.com --project=<your-project-id> 上記を実行したところ無事アプリを開くことができました。以前にコンソールでIAPを有効化したときは実施していなかったので戸惑いましたが、 公式ドキュメントをよく読むと、APIを介してIAPを有効化した際は、上記コマンドでIAPサービスアカウントを作成する必要があるようです。 まとめ/感想 以上、Terraformを使ってGCP上にでセキュアなアプリを構築してみたときのお話でした。 認証機能を自前で実装となるとかなり大変なので、こういったIAPやAWS Cogniteのような認証サービスを利用できるのはとてもありがたいですね。 ただ、今回はIAP周りのIaC化がとてもややこしく手間どりました。触ってみて、IAP周りのTerraformおよびGCP APIは両方とも現在も開発中っぽく、ここの部分については無理にIaC化しなくても良さそうと感じましたが、必要作業について自分なりに整理できたのでやってみてよかったです。 社内に限らず、社外でもStreamlitを使ったアプリ開発が今後増えていくと思われますので、この記事が参考になれば幸いです。 参考 Cloud Run+Cloud SQL構成をTerraformで管理する GCP上でセキュアなCloud Runを構築する方法(Terraform事例有)
アバター
こんにちは!リードプロジェクトマネージャーの加藤です。つい先日までは残暑に四苦八苦しておりましたが、いつの間にか、金木犀の香りが漂う季節となり、とても過ごしやすい季節になりましたね。皆様いかがお過ごしでしょうか? さて、今回の記事では、タイトルにある通り、Insight EdgeでのPM/PL採用について言及してみたいと思います。背景としてはInsight Edgeが設立4年を経過し、組織拡充に向けてより採用を強化している点も理由の1つにあるのですが、私が入社して丁度1年が経過しようとしており、その1年過ごした実績を踏まえ、PM/PL採用の応募要項では伝えきれないリアルな求める人物像について語りたい点が理由としては大きいです。具体性を持って赤裸々に語れればと思いますので、最後までどうぞお付き合い頂ければ幸いです。 (当社の業務内容、及びそれを踏まえた私が考えるプロジェクトマネジメント業務そのものの要諦については 前回記事 もご参照ください) techblog.insightedge.jp 前回記事のおさらい 先ずは前回記事のおさらいからになりますが、当社業務の主軸となるのはDXを通じた企業価値向上、ないしは新規事業創出、業務オペレーションの変革等々と言った、所謂デジタルを介した企業/業務変革と称しても過言ではありません。その中でPM/PLとしての活動領域の中心となるのは企画/構想段階におけるマネジメント、及び開発段階での開発マネジメントとなる点は前回もお伝え致しました。具体的な業務プロセスについては前回触れておりますので、今回はそれぞれの具体的な情景から実体験も踏まえ、どのような人物像がPM/PLとして相応しいか(我こそは、と言う人に是非応募して欲しい!) を記載したいと思います。 多岐に亘るステークホルダーを主体的に巻き込み、業務推進可能な事 Insight Edgeで対応している各DX案件の何よりの特徴として、関係者が非常に多岐に亘る点が挙げられます。基本的にDX案件では、これまで取り扱っていない未知の事業領域に参入する、ないしはこれまで行ってきた業務を様々な事業戦略に基づき変革を試みる事が要求される為、該当案件のビジネスオーナーとなる事業部(事業会社)だけではなく、協業先の事業部(事業会社)、リスクや法務での関連部署、特定の技術提供を行うパートナー会社、実顧客等々、多種多様なステークホルダーと協業する必要があります。また、住友商事、及び住友商事グループ各社で取り扱っている事業領域の多寡にも起因し、A案件では金属業界、B案件ではエネルギー等、アサイン状況によっては全く異なる業界のDX案件の推進を複数任せられるケースもあります。この様に担当案件によって、全く異なる業界、且つ担当案件に如何に関わらず、非常に大勢の関係者との協業が求められる為、特定期間に集中して関係者を巻き込んで着実に案件推進を成功裡に収める必要があります。PM/PLとしては基本ではありながらも、関係者それぞれの思惑や意向が異なる中でプロジェクト奏功に向けて、各位のそれらを纏め上げる事に対して前向きに取り組める人物が相応しいと感じております。 リーダーシップを発揮し、如何なる困難にも屈せず立ち向かう姿勢 上記の関係者の多寡にも掛かる話ではありますが、関係者全員のデジタル素養、開発経験と言ったITリテラシーやプロジェクトマネジメントの知見も異なる点から、関係者毎に適切なレベル感での情報共有を行いながら、主体的にリードしていく気概も非常に重要です。再掲となりますが、基本的には前例や経験のない事業戦略立案がDX案件には内包されている為、各々が考える期待値の相違やどう進めていけばいいのか、ないしはリテラシーの濃淡によっては、そもそも一方が意図する事が適切に伝わらず、意思統率の時点でそれが困難になるケースも散見されます。更に言えば、前例がないからこそ、企画/構想としてはある程度煮詰まってきた段階で、ビジネス・技術何れかの要因で、これまで検討してきた方針では立ち行かなくなる可能性が生じるノックアウトファクターに直面し、推進が難航するケースもあり得ます。プロジェクトマネジメントの観点で言えば、共通言語としてのプロジェクト憲章やコミュニケーション定義書、用語集やコンティンジェンシープランを事前に策定し関係者合意を図る事が重要でありながらも、こうした状況に陥ったとしても諦めるのではなく、常にリーダーシップを発揮しながら、時にはプロジェクト外の有識者や社内メンバー等、凡ゆるネットワークを駆使し、問題解決に向けて愚直に歩みを進められる・進めていこうと言う姿勢こそが必須と私は考えます。 エンジニア・ユーザと適切にコミュニケーションを取りながら、前向きに前進出来る事 ここからは主に実開発のマネジメント経験を踏まえての記載となります。手前味噌な記載で恐縮ながら、入社して一番想像通りであり、実際に目の当たりにした際に感嘆とした点が、Insight Edgeに所属する各社員の専門性、及び技術力の高さです。実際のエンジニアリング能力もさることながら、技術領域に対する知見の深みは目を見張るものがあり、具体見当が進んでいない柔らかい状態で技術相談をしても、真摯、且つ即座に具体案の提示やそれに伴うリスク等の開示も行なって頂ける為、その点は本当に有難いです。一方で、開発案件を進めていく中では時には技術観点ではより高度な手法を取るべき所を、ビジネス観点ではコストや期間見合いで一旦は見送りたい、または折衷案の採択を行いたいケースが出てくる場合も散見されます。逆も然りで、ビジネス観点で実現したい事と実際に技術的に出来る限界値、または技術あり気ではなく、ビジネス観点での協力や歩み寄りによってのみ実現出来る要件等、現時点の技術で実現出来る範囲と実際にユーザが技術で実現したい点でギャップが生じるケースもあります。PM/PLとしては、各々の背景や意向を具に確認した上で、双方が納得する折衷案の検討や、そもそも両者が共通で掌握出来るレベル感に情報粒度を統一して開示する必要もあります。こうした事から、主体的に適切なコミュニケーションを適宜実施し、単なる情報の橋渡し役に留まらず、常に当事者意識を持って全体の納得感を得ながらプロジェクト推進を行える姿勢も求められます。 QCDを適切に調整・折衝可能である事 Insight Edgeで取り扱う開発案件の多くがPoC案件となります。商用開発前の実証実験の段階になる為、ユーザとしては極力コストを抑えながら、如何に早く出来上がったプロダクトを実際に触りながら検証を行えるか、を求められるケースが多々有ります。一方であまりにもコスト・納期に傾倒し過ぎると品質がおざなりになり、そもそも検証に耐えうるプロダクトにならなかった、または余計な追加コスト・期間が計上されてしまうリスクも出てきてしまう為、QCDの按分を見極め、適切な開発スコープ策定、及び体制とスケジュール検討をする点はPM/PLとして非常に腕の見せ所となる部分です。もう少し踏み込んで言えば、その後の開発リスクを勘案して企画/構想段階より、それらQCDの調整を実現案ベースにリスク事項を含め可視化し、適切に関係者間で合意形成を図れる事が非常に重要で有り、求められる要素だと考えております。 本気で技術の力で世界を”Re-Design”したいと思えるかどうか 最後の記載は具体経験とは異なり、採用過程で言うところの所謂、志望動機や熱意になります。私が入社を志した理由の一つに、様々な業界、産業分野の社会課題を技術の力でDXを通じて一つでも多く解決したいと考えていた点が有ります。この点については、入社前も入社後も変わらず、当社の同僚、及び住友商事/住友商事グループ各社における関係者全ての人も上記飽くなき情熱を持って、業務遂行に励んでいます。個人的には職種毎にある程度のスキル・経験・(会社の求める)パーソナリティの一致、と言うのは重要と考える一方で、一番大事なのは組織全体が追い求める全社ミッションや理念に心から賛同し、同じ志を持てるかどうか、だと考えています。 最後に 如何でしたでしょうか?今回はInsight Edgeで求めるPM/PL人物像の詳細について記載させて頂きました。冒頭でも触れた通り、Insight Edgeは設立4年を経過し、当社が抱えるミッション達成をより高度化・加速化させるべく、組織拡充を担い、採用を強化しております。PM/PL募集についてもこれまで以上に採用強化していく予定につき、上記具体情景を踏まえ、我こそは!と言う方には是非ともご応募頂きたく、何卒よろしくお願い致します!一緒に働ける事を楽しみにしております!!
アバター
こんにちは、Insight Edge(以下、IE)のData Scientistの石倉です。私はIEにジョインしてから約1年が経ったところで、振り返るとまだ1年かと思う時もあれば、もう1年かと思うこともある不思議な感覚です。最近は様々なプロジェクトに携わっており、ChatGPT (LLM)活用プロジェクトもその1つです。今回は、IE内のChatGPT活用の一部を紹介しようと思います! 目次 はじめに 従来のテキストマイニング ChatGPTを用いた分析 Streamlit for LLM まとめ 参考文献 はじめに 皆さん既にご存知の通り、ChatGPTをはじめとした大規模言語モデル(以降LLM)が最近注目を集めており、本Tech Blog内でも既にいくつかChatGPT関連の記事を出しています。 総合商社ビジネスにおける生成AI活用 - Insight Edge Tech Blog ChatGPT(LLM)のビジネス現場での活用に向けた技術的な課題と取り組み - Insight Edge Tech Blog Insight Edgeは住友商事グループ各社(国内外約900社ほど)のDXを推進するために設立された企業で、生成AIのビジネス活用もその一部です。本稿では、従来のテキストマイニングで行ってきた分析タスクをChatGPT(LLM)を用いてどのようなことができるか処理プロセスの比較も含めてお話しできればと思います。 従来のテキストマイニング テキストマイニングは、大量のテキストデータの中からパターンやトレンドを識別することを目的としており、以下が主要なプロセスとなります。 データ収集: データはウェブページ、社内文書、SNSの投稿、レビューなど、さまざまなソースから収集され、目的に応じて異なります。 前処理: 収集したテキストデータは、しばしばノイズや不要な情報が混じっているため、それをクリーニングする必要があります。このステップでは、トークン化、ストップワードの除去、ステミングや形態素解析、正規化などの処理が行われます。 特徴量の抽出: テキストから特徴を形成します。例として、TF-IDF(Term Frequency-Inverse Document Frequency)やWord2Vec、BERTなどの深層学習ベースの手法が挙げられます。 データ探索: 統計的な手法やビジュアル化を利用して、データ内のパターンやトレンドを探るためのステップです。 統計的分析: テキストデータの頻度分析や共起分析などを行い、どの単語やフレーズが多く使われているのか、どの単語が他の単語と一緒によく現れるのかを調査します。 可視化: ワードクラウドや共起ネットワークの可視化などを利用して、テキストデータの特性を直感的に理解する。 モデリング: データ探索で得られた知識を基に、特定のタスクを達成するためのモデルを構築するステップです。 テキスト分類: サポートベクトルマシン(SVM)、ランダムフォレスト、深層学習(CNN、RNN、Transformerベースのモデルなど)を用いて、テキストを定義されたカテゴリに分類します。例えば、感情分析やスパムメールの識別などがこのタスクに該当します。 クラスタリング: K-meansや階層的クラスタリング、トピックモデリングなどの手法を用いて、テキストを自然にグループ化します。これは、カテゴリが事前に定義されていない場合や新しいトピックを発見したいときに役立ちます。 関連性分析: コサイン類似度やJaccard係数などのメトリクスを用いて、テキスト間の距離や関連性を測定し、レコメンデーションや変数間の関連性の探索などに使用されます。 評価: 生成されたモデルのパフォーマンスを評価します。モデルの精度や再現性を確認し、必要に応じてモデリング段階に戻って改善を図ります。 前処理/特徴量抽出/モデル作成/評価などのステップが多く、モデル評価後でも、収集されたデータを処理にかける度にビジネス側のユーザーが分析の視点を持って都度出てきた結果を解釈する必要があります。 ChatGPTを用いた分析 上記で触れたテキストマイニングを行なってどの様な活用をしたいかは各企業/ユーザーによって異なりますが、世間を賑わせているChatGPT(LLM)を用いて、従来行われていたテキストマイニングの処理/解釈プロセスを大幅に削減できる可能性があります。本記事ではChatGPTを用いた分類と要約タスクを行なってどの様なことができるのかを紹介しようと思います。 イメージしやすいようにテーマとしてはとある旅館の口コミを分析することを想定します。 ※本例で取り扱うもの(口コミや分類カテゴリ)は全てChatGPTを用いて生成したもので、架空のものです。 分類タスク 以下の例では、口コミを与えられた分類カテゴリに分けています。従来のテキストマイニングでは前処理/特徴量の抽出/関連性分析などのプロセスによって与えられたカテゴリとの類似度を計測し、ある程度の分類は可能です。また、クラスタリングを行うことでテキストデータのグルーピングが可能です。しかし、関連性分析やクラスタリングなどで詳細なラベル付や解釈は難しく試行錯誤の時間を要します。そのような点で、以下の例では、ChatGPTを用いることにより階層構造のカテゴリにも十分に対応できていることが見て取れ、大幅な分析タスクの簡略化が見込めそうです。 分類タスク 分類結果 要約タスク 要約については、過去のTech blogにもある通り、Map ReduceやRefineなどの手法を用いて要約が可能です。しかし、一言で”要約”と言えど、以下の様に注意すべき点は多いと思います。 情報の抽出:本質的な情報を正確に抽出し、不要な情報を省く必要がある。 コンテキストの理解:コンテキストを理解し、本質的な情報を適切に伝える必要があります。コンテキストを見失うと、要約が誤解を招く恐れがあります。 長さの制約:情報を簡潔に伝えるために、一定の長さに制約されます。重要な情報を省略せずに、分かりやすく伝えるのは難しい場合があります。 対象者の認識:要約の対象者やその知識レベル、ニーズに応じて、要約の内容やスタイルを適切に調整する必要があります。 バイアスの排除:要約者の主観やバイアスが要約に影響を与えないように、客観性と公平性を保つ必要があります。 LLM関連論文の出稿が最近多く、幅広く追いかけることが難しくなっている今日この頃ですが、難しい要約タスクについてプロンプトエンジニアリングのみで質の向上を図った比較的実装も容易そうなものを見つけたのでご紹介しようと思います! 論文紹介 ~ From Sparse to Dense: GPT-4 Summarization with Chain of Density Prompting 本稿ではOpenAIの提供するGPT-4を用いて、「Chain of Density(CoD)プロンプト」と呼ぶ方法を提案し、要約の質の向上を図ったものになります。 既にGPT-4で要約タスクを試されたことある方はご存じかと思いますが、シンプルな”以下の文を要約してください。”といったプロンプトを投げるだけでは冗長な要約文が生成されることが多く、情報として十分な場合はあるものの、質的観点からはあまり良しとはされていません。 今回提案されたCoDプロンプトの手法では、最初に冗長な要約を生成したのち、文の長さを増やさずに欠けている重要な要素を反復的に加えていきます。CoDによって生成された要約は、標準的なプロンプトによって生成されたGPT-4の要約と比較して、より抽象的かつ、より情報統合されており、冒頭部分への偏りが少ないという結果が出ています。 Chain of Density prompt しかし、情報過多になりすぎると人間の好みとはかけ離れていくことについて言及されており、明瞭さと情報量のトレードオフの関係を対象者に応じて変更させて調整する必要があると考えられます。本手法のメリットはChain of Density stepにより明瞭さと情報量をコントロールできることを示唆していると思われ、開発側でプロンプトを試行錯誤しながら要約内容を都度確認し、ユーザーの求める要約の状態まで上手く生成できるように都度調整する手間が省けそうです。 本論文ではニュース記事のみを取り扱っているとのことでした。現状のままだと、入力する記事のトークン数が非常に大きくなりやすく、実装する上でボトルネックになる可能性があります。しかし、プロンプトエンジニアリングだけでここまでのことができるとは大変興味深いです。 CoD step Streamlit for LLM 去年にIE社内の勉強会でStreamlitを触り始めてから、色々と遊んでいたのですが、最近は案件でプロトタイプ提供のために使う機会も増えてきました。既にご存知の方も多いと思うので Streamlit の詳細はここでは割愛します。 触り始めてから今に至るまで次々と新しいバージョンがリリースされ、最近のLLMブームの影響を受けてか、 LLM用の機能 も新たに追加されています(まだ少々扱いづらい点はありますが早期に改善されていくと思います。)。 下図はStreamlitを用いた試作です。前節の分析にて一度分類タスクが完了すると、カテゴリ毎の件数などの可視化も容易になります。また、生成AIの得意とする要約などの要素も追加することで、ユーザー側での解釈性の大幅な向上が期待でき、従来のワードクラウドや共起ネットワークによる分析解釈をする必要がなくなりそうです。さらに、Retrieval-augmented generation (RAG)の機能も追加することでユーザー側でチャットボットとして対話をすることも可能になります。 streamlit まとめ 今回はChatGPTの活用例について紹介しましたが、従来のテキストマイニングのプロセスと比較するとかなり短縮でき、ユーザー側でも十分な解釈性の向上が期待できそうです。まさに生成AIブーム真っ只中にいる私たちですが、生成AIのできるタスクを複合的に組み合わせることで様々な便利サービスを作ることができそうです。そのためにも技術のキャッチアップを怠らず、日々注視していきたいと思います!(最近論文の出稿が多すぎてついていけるか不安です 笑) 参考文献 Griffin Adams, Alexander Fabbri, Faisal Ladhak, Eric Lehman, Noémie Elhadad "From Sparse to Dense: GPT-4 Summarization with Chain of Density Prompting"
アバター
Introduction こんにちは、データサイエンティストの善之です。 Insight Edgeでは社内のコミュニケーション活性化を目的として定期的にシャッフルランチを開催しています。 企画の全体像については以前ntさんに投稿いただいた 社員同士の距離を縮める!シャッフルランチ会開催レポート をご覧ください。 今回は、ntさんの記事で詳しく触れていなかった「グループ分けを最適化するアルゴリズム」の詳細をご紹介したいと思います。 目次 実現したいことと課題 要件の整理 アルゴリズムの概要 Pythonで実装 実行結果 引き継ぎのためにStreamlitでUIを作成 まとめ 実現したいことと課題 ランチ会の目的は以前の記事にも記載の通り、コミュニケーション活性化のために社員同士の接点を増加させることでした。 したがって、 普段あまり接点がない人どうしをランチ会でできる限り巡り合わせる ことが最大のミッションでした。 さらに、企画の要件から次の制約条件もありました。 各社員が3回参加(月に1回、3ヶ月間) 1グループあたり4-5名 参加者は31名 これらの制約を満たしながら、普段あまり接点がない人どうしの接点を最大化させる必要があったのですが、全て手作業で決めるのは至難の業です。 手作業でもある程度良い組み合わせは見つけられるかもしれませんが、労力がかかるうえに最適解よりも接点がない人どうしの組み合わせ数が少なかったり、 制約を一部満たせなかったりするかもしれません。 そこで、今回はPythonで遺伝的アルゴリズムを使って最適解を探索し、組み合わせを自動で決定するアルゴリズムを開発しました。 要件の整理 あらためて、今回の要件を整理したいと思います。 最大化する目的関数 普段あまり接点のない人どうしが同じグループになる回数 (ただし、重複も1回とカウント。たとえば普段あまり接点のない同一ペアが3回とも同じグループになっても1回とカウント) 制約条件 企画段階から決まっていた要件 各社員が3回参加(月に1回、3ヶ月間) 1グループあたり4-5名 参加者は31名 アルゴリズムを作成する過程で加えた方が良いと判断した制約 マネージャーどうしは同じグループにはならない(普段から接点が非常に多いので) メンバーの中身がほとんど同じチームは2回以上できない(今回はチームのうち3人以上が他の回と同じ組み合わせになるチームを除外) 3回とも同じ人と同じチームにはならない 今回はこれらの条件を満たすアルゴリズムを開発しました。 アルゴリズムの概要 基本的なアイデア まずは基本的なアイデアを紹介します。 今回は1-3月に毎月1回ずつランチ会を開催しましたので、はじめに全メンバー(31名)のリストを計3回分作ります(3×31の2次元配列になります)。 そして、月ごとにメンバー31名の順番をランダムに並べ替えておきます(①) 次にこのリストを左から順に5人→5人→5人→4人→4人→4人→4人に区切って分割します(②) これで月ごとに全31名を5人グループ×3と4人グループ×4に分割できました。 あとは、ランダムな並べ替えパターン全通りの中から制約条件を満たすパターンだけに絞り込み、 その中で普段あまり接点のない人どうしが同じグループになる回数が最大のものを抽出すれば完了になります。 ですが、今回はこのパターン数が膨大なので、計算に時間がかかり過ぎてしまいます。 実際に月ごとにメンバーの順番をランダムに並べ替えるとすると、全パターンが 31!×31!×31!=5.56e+101通りあり、とても計算できないです。 もう少しスマートに、ランダムな並べ替えではなくメンバーの組み合わせパターンのみを考えることで試行数は減らせますが それでも計算量は膨大です(1ヶ月分の組み合わせだけでも9.96e+19通りあります) そこで、今回は 遺伝的アルゴリズム を使って効率的に解を探索することにしました。 遺伝的アルゴリズム 遺伝的アルゴリズムとは、自然進化の原理をもとにした最適化技術であり、選択・交差・突然変異といったステップを通じて解の質を逐次的に向上させるアルゴリズムです。 (解の効率的な探索手法であって、必ずしも最適解が求まるとは限らないことに注意が必要です) アルゴリズムの概要を図にまとめました。 このアルゴリズムの手順は ① 初期個体をランダムにN個生成 ② N個の個体から、目的関数のスコアが良い個体Best2を選択 ③ ②で選定した2つの個体から、交叉と変異を用いて新しい個体をN個生成 ④ ③で生成されたN個の個体を新たに②のインプトットとして、②-③の手順をM回繰り返す(このMは世代数と呼ばれる) となります。 ただし、今回は最適化したい配列が1次元ではなく、3ヶ月分の3次元の配列ですので、このアルゴリズムをそのまま適用できません。 そこで今回は、遺伝的アルゴリズムをカスタマイズして実装しました。 実装した方法を図に示します。 基本的なアイデアは先ほどと同じですが、交叉と変異は月ごとのメンバー全員の配列ごとに行いました。 なお、②の個体の選択では、1-3月の合計スコアから最良の個体を判定しています。 以上が今回実装した遺伝的アルゴリズムになります。 制約条件の反映 さて、ここまでのアイデアを実装すれば - 各社員が3回参加(月に1回、3ヶ月間) - 1グループあたり4-5名 - 参加者は31名 これらの制約を満たしつつ、普段あまり接点のない人どうしが同じグループになる回数をできる限り最大化できそうです。 一方で、以下の制約については別途考える必要があります。 - マネージャーどうしは同じグループにはならない(普段から接点が非常に多いので) - メンバーの中身がほとんど同じチームは2回以上できない(今回はチームのうち3人以上が他の回と同じ組み合わせになるチームを除外) - 3回とも同じ人と同じチームにはならない これらの条件については、満たさないケースでは目的関数の値を大きく減らすことで、 極力これらの制約を満たさないものは選ばれないように工夫しました。 具体的には、制約を満たさないケースがある個体(たとえば、マネージャーどうしが同じグループになるケースがある個体)は、 条件を満たさなかったケースごとに目的関数のスコアを-100するようにしました。 Pythonで実装 では、ここまで説明してきたアルゴリズムをPythonで実装していきます。 1. 事前準備 まず事前準備として、組み合わせをExcelにしてまとめておきます。 図のような表形式で、普段あまり接点がなさそうな人どうしの組み合わせに◯をつけていきました (○はマネージャー陣につけてもらいました)。 なお、画像は本記事のために作成したダミーデータです。○は乱数をもとにつけています。 2. 必要なモジュールのimport ではPythonでの実装に入ります。まず必要なモジュールをimportしておきます。 今回は遺伝的アルゴリズムも自前で実装するため、pandas以外に外部ライブラリは用いません。 import itertools import random import sys from collections import Counter import pandas as pd 3. チーム分け用関数の作成 はじめに、メンバーの配列を5人チームと4人チームに分割する関数を作成します。 基本的なアイデア における「② 各月で左から順に5人グループと4人グループに分割」の部分です。 たとえば、 people = ["A", "B", "C", "D", "E", "F", "G", "H", "I"] の場合は [["A", "B", "C", "D", "E"], ["F", "G", "H", "I"]] のように、引数 people を4人と5人のグループに分割した配列が返却されます。 def divide_teams (people): n = len (people) result = [] # 4人チームの最大数 max_fours = n // 4 # 5人チームと4人チームの組み合わせを検討する valid_division = False for i in range (max_fours + 1 ): if (n - i * 4 ) % 5 == 0 : four_person_teams = i five_person_teams = (n - i * 4 ) // 5 valid_division = True break if not valid_division: raise ValueError (f "この人数({n}人)は4人チームと5人チームに分割できません" ) # 5人チームの分割 for _ in range (five_person_teams): team = people[: 5 ] result.append(team) people = people[ 5 :] # 4人チームの分割 for _ in range (four_person_teams): team = people[: 4 ] result.append(team) people = people[ 4 :] return result 4. 普段あまり接点がない人どうしをカウントする関数の作成 ここでは、生成されたグループのうち、普段あまり接点がない人どうしの組み合わせが何回発生しているかを計算します。 この値を最大化するのが、今回の目的になります。引数 groups は、 divide_teams 関数で生成された配列です。 また、 count_good_combinations の引数 good_pairs は1.事前準備で作成したExcelにおいて○がついていたメンバーの組み合わせを格納したタプルとなっています。 def make_unique_pairs_from_groups (groups): # 全グループでのメンバー同士のユニークな全組み合わせを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの順番を問わずに重複ペアを削除 unique_pairs = list ( set ( tuple ( sorted (pair)) for pair in pairs)) return unique_pairs def count_good_combinations (groups, good_pairs): # 新しい組み合わせ数を計算する関数 # 全体のペアをユニークに取得 unique_pairs = make_unique_pairs_from_groups(groups) # まだ話したことがない組み合わせの数を計算 count = 0 for pair in unique_pairs: if pair in good_pairs: count += 1 return count 5. 制約条件の追加 次に制約条件を追加していきます。 各制約条件を 満たさない ケースをカウントする関数になります。 まずは、マネージャーどうしが同じグループになっている件数をカウントする関数です。 引数 to_divide_members には、マネージャーの名前が入っている想定です。 def count_to_divide_combinations (groups, to_divide_members): # グループごとにペアを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの中身をsort sorted_pairs = list ( tuple ( sorted (pair)) for pair in pairs) # 分割したい人たちのペアを作成 to_divide_pairs = [ tuple ( sorted (pair)) for pair in itertools.combinations(to_divide_members, 2 ) ] # countする count = 0 for sorted_pair in sorted_pairs: if sorted_pair in to_divide_pairs: count += 1 return count 次に、メンバーの中身がほとんど同じチームの件数をカウントする関数です。 def count_overlap_groups_over_threshold (groups, threshold): # メンバーの重複がthreshold以上のグループをカウントする # たとえば、thresholdが3の場合、3人以上が被っているグループがいくつあるかをカウント count = 0 for i in range ( len (groups)): for j in range (i + 1 , len (groups)): # 重複要素を見つける overlap_set = set (groups[i]) & set (groups[j]) # threshold以上の重複がある場合、カウントを増やす if len (overlap_set) >= threshold: count += 1 return count 最後に、3回とも同じ人と同じチームになった件数をカウントする関数です。 def count_multi_pairs_over_threshold (groups, threshold): # グループごとにペアを作成 pairs = [] for group in groups: pairs += list (itertools.combinations(group, 2 )) # タプルの中身をsort sorted_pairs = list ( tuple ( sorted (pair)) for pair in pairs) # 重複をカウント # threshold以上同じグループになったペアをカウント counter = Counter(sorted_pairs) return sum ( 1 for count in counter.values() if count >= threshold) 以上の関数をもとに、これらのケースができる限り起こらないような目的関数を作成していきます。 6. 制約を加えた目的関数の設定 それでは、制約を加えた最終的な目的関数を定義します。遺伝的アルゴリズムでは、このスコアが最大になるように解を探索していきます。 制約条件を満たさない場合は、 penalty_score を乗算して1件につきスコアを-100する設計にしています。 これによって、これらの制約条件をできる限り満たしながら、普段あまり接点がない人どうしの組み合わせを最大化できます。 なお、後ほど引数で渡しますが、今回 multi_pair_threshold は3(3回とも同じ人と同じになることは避ける)、 overlap_threshold も3(3人以上同一メンバーが含まれるチームの生成は避ける)にしています。 def calculate_score ( groups, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, penalty_score= 100 , ): # 普段あまり接点がない人どうしの組み合わせをカウント good_count = count_good_combinations(groups, good_pairs) # 分割したい人たち(マネージャー陣)のペアをカウント to_divide_count = count_to_divide_combinations(groups, to_divide_members) # 一定回数以上同じペアをカウント(例: ランチ会3回中3回とも同じ人と同じ) multi_pair_count = count_multi_pairs_over_threshold( groups, multi_pair_threshold ) # ほとんど同じグループをカウント(例: メンバー4人中3人が、前回と同じ人たち) overlap_groups_count = count_overlap_groups_over_threshold( groups, overlap_threshold ) return ( good_count - to_divide_count * penalty_score - multi_pair_count * penalty_score - overlap_groups_count * penalty_score ) 7. 遺伝的アルゴリズムの実装 これで準備が整いましたので、遺伝的アルゴリズムを実装していきます。 まず、遺伝的アルゴリズムの計算に必要な要素となる関数を定義します。 def initialize_population (size, employee_list, lunch_session_times): # 初期の個体群をランダムに生成。「① 初期個体をランダムにN個生成」に相当。 return [ [ random.sample(employee_list, len (employee_list)) for i in range (lunch_session_times) ] for _ in range (size) ] def evaluate_solution ( solution, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, ): # 個体のスコアを評価する groups = [] for solution_per_session in solution: groups += divide_teams(solution_per_session) return calculate_score( groups, good_pairs, to_divide_members, multi_pair_threshold, overlap_threshold, ) def select_parents (population, scores): # ベストスコアの2個体を選別 # 「② N個の個体からスコアの良い個体を2個選択」に相当 parents = sorted ( zip (population, scores), key= lambda x: x[ 1 ], reverse= True ) return [parents[i][ 0 ] for i in range ( 2 )] def crossover (parent1, parent2): # 実施月ごとに交差を実施 child1 = [] child2 = [] for session_time in range ( len (parent1)): # 一点で交叉 idx = random.randint( 0 , len (parent1[session_time]) - 1 ) child1.append( parent1[session_time][:idx] + [ x for x in parent2[session_time] if x not in parent1[session_time][:idx] ] ) child2.append( parent2[session_time][:idx] + [ x for x in parent1[session_time] if x not in parent2[session_time][:idx] ] ) return child1, child2 def mutate (child, mutation_rate): # 一定の確率で2点間の位置をスワップ for session_time in range ( len (child)): for i in range ( len (child[session_time])): if random.random() < mutation_rate: swap_idx = random.randint( 0 , len (child[session_time]) - 1 ) child[session_time][i], child[session_time][swap_idx] = ( child[session_time][swap_idx], child[session_time][i], ) return child では、いよいよこれらの関数を用いて遺伝的アルゴリズムを実行しましょう。 # 設定値 population_size = 100 # 世代ごとに生成する個体数。 generations = 1000 # 変異と交叉を繰り返す回数 mutation_rate = 0.01 # 変異が発生する確率 lunch_session_times = 3 # ランチセッションの開催回数 overlap_threshold = 3 # N人以上重複するグループを避ける際のN # 実行 population = initialize_population( population_size, employee_list, lunch_session_times ) best_solution = None best_score = -sys.maxsize for generation in range (generations): scores = [ evaluate_solution( solution, good_pairs, to_divide_members, lunch_session_times, overlap_threshold, ) for solution in population ] if max (scores) > best_score: best_score = max (scores) best_solution = population[scores.index(best_score)] parents = select_parents(population, scores) new_population = [] for i in range (population_size // 2 ): child1, child2 = crossover(parents[ 0 ], parents[ 1 ]) new_population.append(mutate(child1, mutation_rate)) new_population.append(mutate(child2, mutation_rate)) population = new_population この計算によって、制約条件をできる限り満たしながら、普段あまり接点がない人どうしの組み合わせをできる限り最大化した結果が得られました。 それでは、次のセクションで実行結果をご紹介します。 実行結果 ここでは、本記事用のダミーデータでの実行結果を記載します。 マネージャーはAさん~Gさんの7名と想定しています。 実際に分割されたメンバーリスト まずは、遺伝的アルゴリズムで得られたグループを見てみましょう。 マネージャーは 太字 にしています 第1回(1月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Rさん Gさん Wさん Uさん Xさん グループ2 Tさん Kさん Lさん Vさん Eさん グループ3 Bさん Oさん Zさん Sさん Yさん グループ4 cさん eさん Pさん Cさん グループ5 Mさん Fさん Nさん aさん グループ6 Jさん Iさん Qさん Dさん グループ7 bさん dさん Hさん Aさん 第2回(2月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Vさん Kさん Gさん Sさん Hさん グループ2 Aさん Oさん Xさん Jさん Pさん グループ3 aさん Tさん Yさん Cさん Qさん グループ4 eさん bさん Bさん Mさん グループ5 Lさん Rさん Iさん Fさん グループ6 dさん Wさん Uさん Dさん グループ7 Nさん Zさん cさん Eさん 第3回(3月) グループ メンバー1 メンバー2 メンバー3 メンバー4 メンバー5 グループ1 Nさん Sさん Pさん Gさん Jさん グループ2 Aさん Kさん Zさん Mさん Qさん グループ3 Yさん Dさん Lさん Vさん Rさん グループ4 Uさん Tさん Bさん Hさん グループ5 aさん dさん Cさん eさん グループ6 Xさん Eさん Iさん bさん グループ7 Oさん Fさん cさん Wさん マネージャーが毎回7チームに分散しており、同じようなチームもなく、うまくチーム分けがなされているように見えます。 検算したところ、実際にこのチーム分けは、制約条件を全て満たしていました。 更に全194件の普段あまり接点がない人どうしの組み合わせのうち、118件の組み合わせを実現できています! 最適化の時間推移 では、最適化によってスコアがどのように推移したかを見てみましょう。 横軸は学習ステップ数(世代数)、縦軸は学習スコア(各世代のbest_score)を表しています。 序盤に大きくスコアが改善し(制約条件を満たさないケースが淘汰されています)、200ステップ以降はほとんどスコアに変動がありません。 今回のケースでは、200世代程度でも十分に良い解が導けていたということになります。 ランダム探索との比較 では、今回の学習で遺伝的アルゴリズムを利用せず、ランダムに探索した場合は結果がどうなるかを検証して結果の考察を終えたいと思います。 ここではランダムにメンバーをシャッフルした個体を評価し、最もスコアの高い個体を選択します。 今回は各世代の個体が100で、世代が1,000だったため、100×1,000=100,000通りをランダムに探索してみました。 その結果、遺伝的アルゴリズムが実現できた普段あまり接点のない人どうしの組み合わせは 118件 だったのに対し、ランダムに探索した場合は 77件 となりました (制約条件は全て満たせていました)。 遺伝的アルゴリズムの方が 35% も実現できた組み合わせ数が多く、解の効率的な探索に有効であったことがわかります。 引き継ぎのためにStreamlitでUIを作成 今回は私が実際にPythonコードを動作させて組み合わせを決めましたが、今後もランチ会を実施する際に非エンジニアでもこのアルゴリズムを動かせるように、StreamlitでUIを実装しました。 コードの詳細は省略しますが、簡単にUIをご紹介したいと思います。 まず、普段接点がなさそうな人の組み合わせに◯をつけたExcelファイルをアップロードします。 次に、マネージャーを選択します。選択肢はアップロードしたExcelファイルに含まれる人名が表示されます。 最後にランチ会の開催回数を入力して、計算実行ボタンを押すと最適化計算が開始されます。 計算中は学習が進む様子をビジュアルで確認できるようにしました。 計算が終わると、アルゴリズムが導き出した最適なグループのリストが表形式で表示されます。 (以降のグループは省略) このように簡易なUIにすることで、非エンジニアの方でも気軽にグループ分けを実施できるようになりました。 まとめ かなりボリュームのある記事になりましたが、最後までお読みいただきありがとうございます。 もし同じような課題を抱えている方がいれば、本記事を参考にしていただけると嬉しいです。 また、Insight Edgeはランチ会のグループ分けでも技術的なチャレンジが自由にできる、データサイエンティストの技術を存分に発揮できる環境です。 Insight Edgeに興味を持たれた方は、ぜひ 公式サイト の採用ページからカジュアル面談をお申し込みください!
アバター
こんにちは!Insight Edgeコンサルタントの山田です。最近体重増加が著しく、16項目の計測が可能なAnkerの体重計を購入したのですが、毎朝データを取り、日々の変化をグラフ化することでダイエットのモチベーションが維持できています。改めてデータの可視化の重要性を実感しているところです。 さて、この記事では例にもれず生成AIをテーマに、総合商社における生成AI活用についてまとめたいと思います。Insight Edgeは住友商事グループの内製エンジニア組織ですが、グループ全体のCoE組織である 「SC-AI Hub」 の中核を担っており、生成AI関連だけでも現在数十件のプロジェクトを推進しています。住友商事グループの生成AI活用に向けた取り組みを一部紹介しているので、是非ご覧ください。 生成AIのビジネス活用 総合商社が生成AI活用するインパクト ビジネスドメインの多様性 蓄積された豊富な情報 住友商事グループにおける活用事例 社内情報検索 問い合わせチャットボット VoC(Voice of Custmor)分析 経営の意思決定サポート まとめ、今後の展望 生成AIのビジネス活用 生成AI技術のアップデートのスピードはすさまじく、毎日どこかで新たな発表がされているような状況です。同時にビジネスシーンにおいても紙面で生成AIの文字を見ない日はないほど、生成AIの導入・活用が当たり前となっています。 私が現在担当するプロジェクトも、6~7割が生成AI関連となっており、実感としても業界業種問わず活用への機運が急速に上昇している印象があります。 8/4付の日本経済新聞の記事では、調査対象とした約100社のうち7割の企業が具体的な労働時間削減の効果を見込んでおり、社員の生産性向上に寄与すると期待されています。同時に、機密情報漏洩や著作権侵害など懸念される事項もあり、各社導入とルール整備を急速に進めているフェーズです。 生成AIで企業の7割時短 100社調査 - 日本経済新聞 総合商社が生成AI活用するインパクト 業界業種を問わず大きなインパクトをもたらしている生成AIですが、実際に住商グループの生成AI活用をリードする立場としてプロジェクトを進める中で、総合商社は活用ポテンシャルが特に大きい業態の一つと感じています。その理由は大きく以下2点です。 ビジネスドメインの多様性 ご存知の通り総合商社は、資源化学品、エネルギー、輸送機、インフラ、消費財、小売り、不動産、金融、IT、物流、ヘルスケア等、多岐にわたるビジネスを展開していますが、生成AIの実装という観点で見ると、それぞれのビジネスで共通なユースケースと業界特有のユースケースがあります。情報検索や高度なタスクの自動化など、共通ユースケースの実装が比較的先行しますが、業界特有のユースケースも、着眼点次第で活用方法は多岐に渡り、ブレイクスルーをもたらす可能性があります。多様なビジネスドメインにおいて同時並行で活用アイデアを探ることができるのは大きな強みと感じます。 蓄積された豊富な情報 上記に派生するポイントでもありますが、総合商社は多数のステークホルダーやマーケットから日々膨大な情報を収集しており、社内に長年のナレッジとして蓄積されています。これら情報を活用し、また時には業界を横断したデータ活用により、新しいビジネスアイデアを生み出すことができます。また、過去の情報からビジネスの成功要因や失敗要因を把握し、新しいビジネスの成功確度を高めることも期待できます。(後述の意思決定支援ツールの事例にも通じます) 住友商事グループにおける活用事例 特にビジネスドメインの多様性による強みは大きいと感じており、社内情報検索や問い合わせ対応など代表的なユースケースはもちろん、業界特有のペインポイントを生成AIで解決するようなユニークな活用方法があるはずで、そのアイデアやニーズにいち早くアクセスできる点は非常に強みと感じます。 一方で、ビジネス面・技術面の両輪での試行錯誤がきわめて重要となるこの領域において、ビジネスアイデアを高速で形にしながらトライ&エラーができる環境も同時に重要であり、この点はInsight Edgeが内製エンジニア組織として価値を発揮しているポイントでもあります。 以降では、ビジネス現場とInsight Edgeがともに試行錯誤しながら取り組む具体事例をいくつかご紹介します。 生成AIのユースケース分類 社内情報検索 LLM自身が持つ情報だけでなく、企業内のクローズドなデータを組み合わせて、企業や部署専用の情報検索ツールとして活用する方法で、企業利用における最も代表的なユースケースの一つです。RAG(Retrieval Augmented Generation)などが有名な手法です。あらかじめ参照したい社内外の情報をデータベース(ベクトルストア)に用意しておき、ユーザーからの質問に対してベクトルストアの中で類似度の高い文書を検索し、その文書を質問と合わせてプロンプトとして入力することで、既存のLLMだけでは回答できない質問にも精度高く回答できるようにする使い方です。 企業の規模が大きくなるほど、社則や業務ルールも膨大になり、かつ主管部署も異なることも多々あり、求める情報にたどり着くまでに時間を要することも少なくありません。本来は時間をかけるべきでない作業を削減し、より価値のある業務に時間を使えるようにすることは、生成AIを積極的に活用するべき最大の理由でしょう。 Insight Edgeでは社内情報検索ソリューションのβ版をリリースし、住友商事をはじめ、複数のグループ企業に導入するべく目下準備を進めています。 社内情報検索ソリューションの画面イメージ。住友商事の一部メンバーを対象にトライアル導入中。 問い合わせチャットボット 生成AIを活用したFAQチャットボットも、代表的なユースケースです。①の情報検索の仕組みと同様に、業務マニュアルや過去のQ&A履歴、それに対する主管部署の回答などをベクトルストアに用意しておくことで、従来のルールベースのAIチャットボットよりも回答精度の高いFAQシステムができることが期待できます。 このユースケースも、確認すべき業務ルールが多いほど、また業務遂行に際し社内ルールだけでなく、法令や業法など一般的なルール順守が求められる場合にも、一次回答を迅速に得るための強力な武器になりえます。(法令や業法など参照する場合は正確性が求められるため、ハルシネーションの理解には注意が必要で、ユーザーのリテラシーが問われます) 特に人事や総務、法務、情シスなど、コーポレート機能を担うメンバーの問い合わせ対応工数を大きく削減できる可能性があります。 VoC(Voice of Custmor)分析 VoC分析は、顧客の意見やフィードバックを収集、分析することで、顧客満足度向上や製品・サービス改善を目指す手法です。toCビジネスも数多く手掛ける商社では、VoC分析の重要性は非常に高く、ここにも生成AIは活用できます。 日々寄せられる大量のVoCをカテゴリごとに分類・要約することはもちろん、そこから示される顧客感情の示唆、ポジネガ分析、さらには改善に向けたアクション提案などが可能となります。 従来のAI技術でもある程度のVoC分析は可能でしたが、例えば感情分析において業界や企業ごとに判断基準が異なり、VoCの内容に対して、一概にポジティブかネガティブかの判断が難しい場合がありました。一方生成AIでは、プロンプトに判断基準の例を示すことで、業界や企業ごとに重視する観点を反映した分析が可能となります。専門知識や経験が乏しくても、筋のいい分析をするためのサポートツールになりえます。 経営の意思決定サポート 総合商社における生成AI活用の本命の一つが、経営判断における意思決定支援だと思います。総合商社はトレーディングから事業投資/事業経営へとビジネスを拡大してきましたが、社内には投資判断を行うための調査資料や分析レポート、意思決定にあたってのマネジメントの所感・コメント等が残されています。 これらを活用し、新規投資案件など意思決定が必要な場面において、過去に類似の案件がなかったか、その際にはどのような観点やリスクが議論され、結果としてどうなったかなどの情報を生成AIにより抽出・要約することで、意思決定において重要となる情報提供が可能となります。 決断までをAIに委ねる未来は恐らく来ることはないですが、意思決定における強力なサポートアイテムになるのではと期待しています。 Insight Edgeでも住友商事のリスクマネジメント部と連携し、意思決定支援ツール開発のプロジェクトを進めています。 意思決定支援ツールを活用した質問例 これら事例は、下記メディアでも取り上げて頂いているので、よければこちらもご一読ください。 5大商社はChatGPTをどう活用?伊藤忠は「同僚化」、住商は鋼管取引と投資判断に【プロンプト実例紹介】 | コピーですぐに使える!ChatGPT100選 職種別・業種別・部署別 | ダイヤモンド・オンライン まとめ、今後の展望 総合商社は過去100年にわたって、トレーディングから事業投資、事業経営と、常に時流を読み柔軟にその姿を変えてきました。そして現在においては、先端テクノロジーをいかにビジネスに適用し付加価値を創造できるかが強く求められています。 生成AIの登場という、これまでにないエポックメイキングな技術革新の中においても、常に市場を先取りし新しいビジネスを開拓し続ける住友商事グループの内製エンジニア企業として、Insight Edgeもワンチームとして生成AIの社会実装に貢献していきたいと思っています。
アバター
こんにちは!Insight Edgeの塩見です。 2023年7月11日(火)~13日(木)の3日間、東京ビッグサイトにて「AI World 2023」が初開催されました。本展示会では、ChatGPTを活用したソリューションが数多く展示されていたため、その内容をご紹介したいと思います。 AI Worldとは? ChatGPTとは? 各社のChatGPTソリューション紹介 株式会社AVILEN 「研修、アイディアソン、ソリューション開発」 株式会社ギブリー 「法人GAI・行政GAI」 パーソルプロセス&テクノロジー株式会社 「TIMO Meeting」 Allganize Japan株式会社「Alli」 まとめ AI Worldとは? AI Worldは、ビジネス変革と業務効率化をテーマとした大型展示会です。この展示会では、DX(デジタルトランスフォーメーション)、業務効率化、チャットボット、機械学習、画像・音声認識、自然言語処理、対話AIを始めとする最新のAIソリューションが一堂に展示されます。商談を求める企業にとっては、課題解決のための貴重なヒントを手に入れることができ、またAI技術の最先端を一目で確認することができる場となっています。  AI World 2023 さらに、この展示会はリアルな場所での展示の他、オンラインでのセミナー視聴や製品資料のダウンロードも可能です。これにより、遠方からの参加者や、展示会場に来ることができない方でも情報収集が容易になっています。 場所としては、東京ビッグサイト(西1・2ホール)を使用して開催され、展示製品数は同時開催された他展示会と合わせると、日本最大級とされる約870製品が出展されました。また、デジタル庁や総務省、東京都などの公的機関からの後援を受けており、その規模と重要性を物語っています。実際、来場者数は計22,295名であり、業界関係者やビジネスパーソンからの高い関心が伺えます。 ChatGPTとは? ChatGPTは、OpenAIによって開発された大規模な言語モデルを活用したAIチャットサービスです。ユーザーとの対話形式で情報提供や問い合わせ応答が可能であり、その高度な自然言語処理能力により、様々なビジネスやサービスに応用されています。ChatGPTは2022年末に公開され、その後、急速に利用者数が増加しました。 2023年2月、本展示会と類似内容の展示会であるDX EXPOが開催され、その場でも多くのAIソリューションが展示されていましたが、ChatGPTの公開から間もない為、ChatGPTに関連したソリューションを展示していた企業は1社程度しかありませんでした。 一方、本展示会では、10を超えるChatGPTソリューションが展示されており、多くの企業がChatGPT公開後に関連サービスの開発を進めていたことがわかります。実際、これらのソリューションは展示会見学者の注目を多く集めており、企業だけでなくユーザの期待値も高いことが想定できます。 各社のChatGPTソリューション紹介 ここからは、AI Worldにて展示されていたChatGPTソリューションを一部ご紹介します。各ソリューションの詳細が記載されたWebページへのURLリンクも併せて載せていますので、こちらもご参照ください。 株式会社AVILEN 「研修、アイディアソン、ソリューション開発」 株式会社AVILENでは、ChatGPTをビジネス活用する上で、各社の課題やフェーズに応じたソリューションを一気通貫で支援するサービスを提供していました。 具体的には、以下の支援サービスがあります。 ChatGPTビジネス研修 ChatGPTアイディアソン ChatGPTソリューション開発(Chat Mee, Chat Mee Pro) ビジネス研修では、「そもそもChatGPTで何ができるのかわからない」というユーザ向けに、ChatGPTの基礎知識をE-ラーニングで学習してもらいながら、ChatGPTを実際に体験するワークを実施します。 アイディアソンでは、「ChatGPTを自社でどのように活用できるかアイディアが欠けている」と感じるユーザ向けに、企画立案から実行まで、コンペ形式でAVILENがサポートします。 ソリューション開発では、「ChatGPT導入・開発に必要なノウハウやリソースが不足している」と感じるユーザ向けに、ChatGPTに精通したデータサイエンティストが課題の明確化から開発、保守まで一気通貫で支援します。 現在、ChatGPTの利用は日本国内でも増加していますが、まだChatGPTを利用したことがないユーザが大多数であることを考えると、ChatGPTの基礎教育・業務課題やユースケースの整理・ソリューション開発といった一連の流れをサポートするこのサービスは、非常に有益だと感じました。 株式会社AVILEN ChatGPT活用支援サービスの詳細はこちら 株式会社ギブリー 「法人GAI・行政GAI」 株式会社ギブリーでは、法人・行政でChatGPTを安全に使用できるGAIサービスを展開しています。ChatGPTを法人・行政内で使用する場合、機密情報が漏洩する可能性がありますが、これらのGAIサービスでは、情報が外部に漏洩するリスクをなくしつつ、ChatGPTを法人・行政内で使用することが可能です。 その他、NGワードの登録、ユーザの利用状況をモニタリングできるダッシュボード機能、実務における様々な利用シーンを想定したプロンプトレシピ機能等、ChatGPTを実務で活用する上での利便性の高い機能を多く揃えています。 セキュアな環境でChatGPTを活用する為のサービスは、直近続々とリリースされている印象がありますが、プロンプトレシピやダッシュボード機能など、ユーザに選んでもらう上では、プラスアルファの機能が重要になってくるのかなと思います。 株式会社ギブリー 法人GAIの詳細はこちら 株式会社ギブリー 行政GAIの詳細はこちら パーソルプロセス&テクノロジー株式会社 「TIMO Meeting」 TIMO Meetingは、会議プロセスのデジタル化を通じて、生産性向上を実現する国内初のミーティングマネジメントツールです。本ツールにもChatGPTが活用されており、「AIアシスタント」と「AIアドバイザリー」の2つの機能があります。 AIアシスタントでは、会議予定に添付された資料を読み込み、資料を元にして、会議の目的・背景・論点などを端的にまとめてくれます。これにより、会議参加者は事前にこの会議がどのようなものかを理解することができ、会議の効率性や効果の改善に繋がります。 AIアドバイザリーでは、会議予定の添付資料に関する質問をチャット形式で投げかけることで、資料内容を要約してもらう等、AIからの様々なアドバイスを受けることができます。 TIMO Meetingでは、自社独自ソリューションにChatGPTを組み込み、他社と差別化を計りつつソリューション価値を向上することが出来ている良い事例かと思います。 パーソルプロセス&テクノロジー株式会社 TIMO Meetingの詳細はこちら Allganize Japan株式会社「Alli」 Allganize Japan株式会社は、AIチャットボットのAlliを展示していました。就業規則などの社内資料を元に、ユーザの質問に対して、ChatGPTの自然な応答を元に回答をしてくれるようです。 Allganize Japan株式会社は、2023年2月に開催されたDX EXPOでも自社ソリューションの展示を行っていましたが、DX EXPOの中で唯一、ChatGPTを活用したソリューションの展示を実施していました。この時の展示スペースはとても小さかったのですが、今回は非常に大きなスペースでAlliの紹介をしていました。 スタッフの方にお話を伺ったところ、前回の展示会以降、ChatGPTに関する問い合わせが大きく増え、今回ブースをさらに拡大することになったようです。ChatGPTのサービスリリースは昨年末でしたので、わずか1~2ヶ月で展示会にソリューションを出展したことになります。この迅速な対応は非常に印象的であり、今回のAI Worldでも、多くのユーザから注目を集めていました。 Allganize Japan株式会社 Alliの詳細はこちら まとめ 近年、ChatGPTの活用範囲は日本国内でも拡大しており、多くの企業が各社独自のビジネスモデルにChatGPTを取り入れて、自社サービスの成長を促進しようとしています。 今回の展示会では、セキュアな環境でChatGPTを使用できるソリューションが数多く展示されていました。実際、このようなソリューションへのニーズは増加しているように感じます。 一方で、2023年8月にはOpenAIから法人向けのChatGPT Enterpriseが発表され、更なる付加価値を持つChatGPTソリューションの提供が開始されました。このような状況下、ChatGPTの単なる機能提供を行うソリューションでは、今後の差別化は難しいと感じられます。 パーソルプロセス&テクノロジー株式会社のTimo Meegingのように、自社独自ツールの価値を向上させるためにChatGPTを活用する、または株式会社AVILENのように、ChatGPTのユースケース整理やコンサルティングサービスを提供する、といった取り組みが重要であると感じました。これは、今回の展示会を通じて得た私の個人的な感想です。 今後もChatGPTの進化と、それを取り入れる企業の斬新なアイディアに注目していきたいと思います。
アバター