TECH PLAY

株式会社Insight Edge

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

174

init_mathjax = function() { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true } }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ], processEscapes: true, processEnvironments: true }, displayAlign: 'left', CommonHTML: { linebreaks: { automatic: true } } }); MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } } init_mathjax(); 目次 初めに 提案されたモデルの概要 モデルの問題設定 モデルの意義 提案されたモデル 変数変換後の提案されたモデル 着目している性質 劣モジュラ性 $L^\natural$凸性 証明に必要なLemma 1, Theorem 1 Lemma 1 Theorem 1 最適解が唯一つであることの証明 所感 参考文献 初めに  弊社の分析チームではライトニングトークを社内勉強会の1つとして取り入れており、本稿で解説する論文はその時に私が紹介したものです。勉強会では証明の話はせず、論文の概要をお話しました。 そこで今回は論文で提案されたモデルの最適解が唯一つであることの証明を簡単に解説します。  論文の証明では、まず最適解の候補を2つに絞り、モデルの性質に基づいて1つであることを導いています。着目している性質は$L^\natural$凸性と呼ばれる、極小値と最小値が一致する性質で、劣モジュラ性と関連します。提案されたモデルは$L^\natural$凸性を持つかは明らかでなく、変数を変換したモデルが持っております。本論文では変数変換したモデルの持つ劣モジュラ性により解の更なる絞り込みを行い、変数変換前の提案されたモデルに議論を移すことで証明しています。  以上の内容の解説にあたり、本稿の構成は以下としています。まず提案されたモデルの概要として問題設定やモデル説明をします。次に、本記事で着目している性質($L^\natural$凸性、劣モジュラ性)の定義を述べ、証明に必要なLemma 1やTheorem 1を簡単に説明します。そして最後に、最適解が唯一つであることの証明を見ていきたい思います。 提案されたモデルの概要 モデルの問題設定  問題設定は、著者らが実務で取り組んだ修理部品の在庫管理に由来しています。修理部品は装置の故障が起きなければ使われることはないため在庫は捌きにくいです。そのため長期間保管することになり、保管にかかる費用がかさんでしまいます。しかし、費用を抑えるために在庫量を減らすと、在庫切れを起こす可能性が高まります。万一在庫切れが生じた場合、修理部品を手配できるまでの期間、装置が止まってしまいます。それにより多大な損失が発生することから、在庫切れは避けなければなりません。そこで著者らは必要最小限の在庫量を保管できるよう、装置にセンサを取り付けて解析し、故障予測を行っています。故障予測では、修理部品の必要数と必要になる日にちを予測していますが、予測を外してしまう時もあります。日にちの予測を外すとは、故障する日にちを早め/遅めに予測してしまうことを指しており、保管費用などに影響を及ぼします。また必要数について、少なく予測した場合は在庫切れの可能性が高まり、多く予測した場合は保管費用の増大につながります。特に後者の場合、筆者らは過剰に抱えた在庫を追加料金を支払って発注元に返品し、在庫費用を抑える工夫をしています。本問題は、このような状況における修理部品の管理費用を最小にする発注計画を考えることです。但し、発注には一定のリードタイムはありますが、返品は即日対応であり、修理部品は装置の近くに建設した倉庫に保管しているため修理部品を届ける時間はかかりません。  よって以上をまとめると、本問題は需要予測を用いた在庫管理問題となります。具体的には、需要量や需要のタイミングといった需要予測の情報を援用し、発注量や返却量のタイミングと量を調整して管理費用を最小化する問題に取り組むものです。管理費用とは発注費用、返却費用、在庫の保管費用、そして在庫切れに伴う損失を含みます。また、問題を単純化するために修理物品と倉庫は1種類と仮定しています。    モデルの意義  モデルは、 前節で述べた設定の問題 を解くとともに、不確かであっても需要予測の情報を用いた方が在庫管理にかかる費用が抑えられることを確認するために用いられています。  論文によると、従来のモデルは需要情報が完全に正しいとしていることが多く、需要のタイミングや予測誤差が考慮されることはあまりなかったそうです。また、在庫を大量に抱えてしまった場合、従来は在庫を捌けるのを待つのみで、発注元への返却は考慮されていませんでした。需要のタイミングや予測誤差、発注元への返却を考慮している点が本モデルの新規性となります。  このような在庫最適化はある時刻の発注・返却が次以降の発注・返却に影響を及ぼすため、実行可能解の数が爆発的に増え、解析が難しくなります。最適解を唯一つ持つモデルであること(や本稿でほとんど言及しませんが、効率の良いアルゴリズムを構築できるモデルであること)は解析を簡単にし、運用や費用低減の評価を容易にします。 提案されたモデル 提案されているモデルは以下のようになります(モデルの表記は原文ママ)。 $$ \begin{align} &\begin{split} f_t(\mathbf{a}, \mathbf{z}) = \min_{(z_L, y) \in \mathcal{A}_x} {J_t({\mathbf{a}, \mathbf{z}, z_L, y}) }, \end{split} \\ &\begin{split} J_t({\mathbf{a}, \mathbf{z}, z_L, y}) &= cz_L + c_ry + L(a_{\tau_u}, \dots, a_{\tau_l}, x+z_0-y) \\ &+ E[ f_{t+1}(\mathbf{\bar{a} - \mathbf{\bar{R}}, \it{W}}, (x + z_0 - y - \sum_{\tau = \tau_l}^{\tau_u}R_{\tau} - D^{u} )^{+}, z_1, \ldots, z_L)], \end{split} \\ &\begin{split} \mathrm{where} \end{split} \\ &\begin{split} L(a_{\tau_u}, \dots, a_{\tau_l}, x+z_0-y) &= hE[(x + z_0-y-\sum_{\tau = \tau_l}^{\tau_u}R_{\tau} - D^{u} )^{+}] \\ &+ c_eE[(\sum_{\tau = \tau_l}^{\tau_u}R_{\tau} + D^{u}-x - z_0+y)^{+}]. \end{split} \\ \end{align} $$  $f_t$は時刻$t$から終端時刻までにかかる管理費用の最小値で、この値をとるような発注量と返却量の組み合わせである$(z_L, y) \in \mathbb{Z}_{+} \times \mathbb{Z}_{+}$を求めたいということになります。$z$の添字に使われている$L$はリードタイムを表しており、$z_L$は発注したものが時刻$t+L$に納品されることを示しています。発注や返却は時刻$t$のみで行うわけではないため、実際に求めたい値は各時刻における発注量と返却量になります。 時刻$t$以降の管理費用は、需要量と時刻$t$以前に発注していた物品の納品量に依存するため、それらを表す$\mathbf{a}, \mathbf{z}$の関数になります。$\mathbf{a}={a_{\tau_u}, \dots, a_{\tau_0}}$であり、時刻$t+\tau$における需要量を$a_{\tau}$としています。$\tau$は需要のリードタイムようなものであり、今回の問題設定において$\tau \in [\tau_l, \tau_u]$を満たす$\tau$について、$t+\tau$に需要は発生するとしています。それ以外のタイミングに需要は発生するかもしれませんが、その場合は予測に失敗したことを意味します。また$\mathbf{z}={x, z_{0}, \dots, z_{L-1}}$であり、$x$は時刻$t$における手持ち在庫量としています。  次にモデルの構成要素である$J_t$に着目します。$J_t$はその定義から、時刻$t$にかかる管理費用(最初3項)と、時刻$t+1$から終端時刻までの管理費用$\bar{f}_{t+1}$(最終項)に分けることができます。最初3項は順番に、発注にかかる費用($c$は発注単価)、返却にかかる費用($c_r$は返却単価)、そして在庫に関する費用になります。在庫に関する費用は$L$としてまとめられており、倉庫の保管費用($h$は保管単価)と在庫切れに関する損失($c_e$は損失単価)から成っています。  本稿では上記モデルの最適解が唯一つであることを見ていきます。 変数変換後の提案されたモデル  $v_l = x + \sum_{t=0}^l z_t$、$l = -1, \dots, L$とし、新たに変数$\mathbf{v} = (v_{-1}, v_0, \dots, v_l)$を導入します(変数の表記は原文ママ)。但し、$v_{-1} = x$となります。新しく導入した変数により例えば$z_L = v_L -v_{L-1}$のように記述できるため、 $\mathbf{z}$を$\mathbf{v}$で記述できるようになります。これにより$f_t(\mathbf{a}, \mathbf{z})$から$\bar{f_t}(\mathbf{a}, \mathbf{v})$が、$J_t({\mathbf{a}, \mathbf{z}, z_L, y})$から$\bar{J_t}({\mathbf{a}, \mathbf{v}, v_L, y})$が得られます。 着目している性質 劣モジュラ性  実効定義域が空でない関数$g:\mathbb{Z}^n \rightarrow \mathbb{Z} \cup {\infty}$ が以下を満たす時、$g$は劣モジュラであるといいます。 $$ \begin{align} g(p)+g(q)\ge g(p \vee q)+g(p \land q), \quad p, q \in \mathbb{Z}^n. \end{align} $$ 但し、$\vee, \land$はベクトルの要素ごとに最大値、最小値を出力する演算です。 $L^\natural$凸性  上で定義した関数$g$が以下に示す並進劣モジュラ性を満たす時、関数$g$は$L^\natural$凸であるといいます。 $$ \begin{align} g(p)+g(q)\ge g((p-\alpha \mathbb{1}) \vee q)+g(p \land q+\alpha \mathbb{1}), \quad p, q \in \mathbb{Z}^n, \alpha \in \mathbb{Z}_+. \end{align} $$ 但し、$\mathbb{1}$は全ての要素が$1$のベクトルです。  この性質は離散中点凸性と同値であり、また$g$が整凸性と劣モジュラ性の両方を持つことにも等しくなります。本論文では$L^\natural$凸の持つ劣モジュラ性を用いて、Lemma 1を含め種々のLemmaの証明がなされています。 証明に必要なLemma 1, Theorem 1 Lemma 1 For each $(\mathbf{a}, \mathbf{z}) \in \mathcal{U} \times \mathcal{Z}$ and $t = 1, \dots, T$, the optimal decisions are characterized by $z_L^{*}(a, z) \times y^{*}(a, z) = 0$. このLemmaは最適解の満たす性質を言及したもので、管理費用を最小にする発注量と返却量の積は0になると述べています。この主張をもう少し説明すると、発注量と返却量の両方が正であるときは、返却している分だけ余分に発注していることになり、その分を減らせると言っています。 Theorem 1 a) $\bar{J_t}({\mathbf{a}, \mathbf{v}, v_L, y})$ is $L^\natural-convex$ in $(\mathbf{v}, v_L, y) \in Q$ for each $\mathbf{a} \in \mathcal{U}$ and $t = 1,\dots,T$. b) $\bar{f_t}(\mathbf{a}, \mathbf{v})$ is $L^\natural-convex$ in $\mathbf{v} \in \mathcal{V}$ for each $\mathbf{a} \in \mathcal{U}$ and $t = 1,\dots,T$.  本稿で言及する主張のみ掲載しています。この主張は、提案されたモデルについては不明ですが、変数変換後のモデルは$L^\natural$凸性、つまり劣モジュラ性を持つと言っています。変数変換後のモデルを議論の対象にすると、任意の実行可能解の組に対して、劣モジュラ性が満たす不等式を適用できるため、最適解についての議論ができそうだと分かります。  Theorem 1の証明は$\bar{f}_{t}(\mathbf{a}, \mathbf{v})$の持つ再帰構造に着目した帰納法に基づいています。具体的には、$\bar{f}_{t+1}(\mathbf{a}, \mathbf{v})$が$L^\natural$凸性を持つと仮定し、$\bar{f}_{t}(\mathbf{a}, \mathbf{v})$の内部で再起的に記述される$\bar{f}_{t+1}$が$L^\natural$凸性を持つことと、それに対する期待値や最小化の操作が$L^\natural$凸性を保存することより$\bar{f}_{t}(\mathbf{a}, \mathbf{v})$も$L^\natural$凸性を持つことを導いています。  変数変換は、再帰的に記述されるモデル$\bar{f}_{t+1}$が$L^\natural$凸性を持つことを保証するために行なった操作になります。本稿では言及しませんが、 前に定義した関数$g(p)$ が$L^\natural$凸性を持つならば、$p-\alpha \mathbb{1} \ge \mathbf{0}$となる$\alpha$に対して$g(p-\alpha \mathbb{1})$は$L^\natural$凸性を持ちます(Lemma 2)。変数変換をしなければ、再起的に記述される$f_{t+1}$はイメージ$\mathbf{z}-y\mathbf{e}_1$についての関数となることから$L^\natural$凸性を持つとは限りませんが、$\bar{f}_{t+1}$はイメージ$\mathbf{v}-y\mathbb{1}$についての関数となることから保証されます。 最適解が唯一つであることの証明  Lemma 1より、$J_t$の最適解は「発注量=0」である$(0, y^{*})$または「返却量=0」である$(z_L^{*}(\mathbf{a}, \mathbf{z}), 0)$の2通りになります。両方が同時に満たされる場合は明らかに解が唯一つとなるため、そうならない場合を考えます。最適解が唯一つであることを証明するためには、両方が同時に最適解になり得ないことを証明すればよく、背理法を用います。  同時に最適解になり得ないことを導くために、Theorem 1より得られる変数変換後のモデル$\bar{J_t}$の劣モジュラ性に着目します。劣モジュラ性の式は、実行可能解の組とその組の最小・最大から得られる変数の合計4つを取り扱うことになります。ここで実行可能解の組を上手く選び、「発注量=0」の最適解$(v_{L-1}, y^{*})$と「返却量=0」の最適解$(v_{L-1} + z_L^{*}(\mathbf{a}, \mathbf{z}), 0)$の2つが得られるようにします。そのような実行可能解を劣モジュラ性が満たす式に代入し、以下の式を作ります。 $$ \begin{align} &\begin{split} \bar{J_t}({\mathbf{a}, \mathbf{v}, v_{L-1}, y^{*}}) - \bar{J_t}({\mathbf{a}, \mathbf{v}, v_{L-1}, 0}) \ge \\ \bar{J_t}({\mathbf{a}, \mathbf{v}, v_{L-1} + z_L^{*}(\mathbf{a}, \mathbf{z}), y^{*}}) - \bar{J_t}({\mathbf{a}, \mathbf{v}, v_{L-1} + z_L^{*}(\mathbf{a}, \mathbf{z}), 0}) \end{split} \end{align} $$  ここで代入している変数が最適解であることから、左辺は非正であり、右辺は非負となることから不等式は成立しなくなります。「発注量=0」と「返却量=0」のときは両辺が一致しますが、本節の冒頭で述べたようにその場合を除外しています。よってTheorem 1に反することから、$\bar{J_t}$の最適解は唯一つになります。  $\bar{J_t}$と$J_t$の実行可能解は1対1に対応していることから、$J_t$の最適解も唯一つであることが示されます。 所感  本稿では、不確かな需要情報をもとに在庫管理を行う提案モデルの最適解が一意に定まることの証明を簡単に解説しました。この証明では、変数変換後のモデルが持つ劣モジュラ性と背理法を用いており、比較的シンプルなものになっています。  提案されたモデルの式は、ある時刻の在庫に関する費用の算出にあたって未来の需要を考慮しています。個人的には、これにより在庫の保管費用を過小評価していると思われるため、改良の余地はあるように見えます。また、本モデルの有用性は発注費用や需要の頻度など問題の細かな設定に応じて変わるため、リグレットを使ってモデルを定式化すると面白いと感じています。解の導出は難しくなりますが、バウンドを求められれば、数値実験と合わせることで有用性の見通しが立ちやすくなるように思います。 参考文献 [1]. Topan, Engin, et al. "Using imperfect advance demand information in lost-sales inventory systems with the option of returning inventory." IISE Transactions 50.3 (2018): 246-264. [2]. Fujishige, Satoru, and Kazuo Murota. On the relationship between L-convex functions and submodular integrally convex functions. Kyoto University. Research Institute for Mathematical Sciences [RIMS], 1997. [3]. Kazuo Murota, Discrete Convex Analysis. Hausdorff Institute of Mathematics, Summer School, 2015.
アバター
こんにちは!Lead Engineerの筒井です。生成AIが話題ですね。弊社においても生成AI活用の取り組みを進めています。Insight Edgeでは生成AIの活用も含め、住友商事グループ各社の課題解決に取り組む仲間を募集しています!ご興味がある方はぜひ、お気軽に 公式サイトの採用ページ からご連絡ください! さて、ChatGPTを企業で活用する場合、主にAzure OpenAI Appsを通じて利用することになるということもあり、各案件のシステム構築に最近はAzureを活用し始めています。この記事では、Azure Container Apps上にAPIサーバーを構築し、APIキーでアクセス制限をかける方法を紹介します。 概要 今回は前提として、以下のようなケースを想定しています。 Azure上でコンテナをサクッと動かしたい コンテナは2種類あり、相互に通信する。うち1つはAPIサーバーであり、外部からWebアクセスする フロントエンド側を色々試したい事情があり、HTTPヘッダにAPIキーを入れるくらいで簡単にアクセス制御したい 認証機能をアプリ側で実装するのではなく、Azure側に委譲したい Container Appsには組み込みの認証機能があり、とても簡単に各種IdPと連携した認証機能を設定することができます。ただ、APIキーによる認証機能は含まれないため、前段にAzure API Managementを設置することで対応することにしました。また、Azure App Serviceを利用すると、同様の仕組みをもっと簡単に実現することができます。しかし、今回はWebサービスでないコンテナもあるため、Container Appsを採用しました。 というわけで、システム構成はシンプルですが、下図のようになりました。 構築方法 仮想ネットワークを作成する 上図の通り、各通信は仮想ネットワーク経由で行われます。そのため、仮想ネットワークとサブネットを作成し、サブネットにネットワークセキュリティグループを割り当てておきます。ネットワークセキュリティグループには、以下のリンク先を参考に必要なルールを追加します。インターネットから仮想ネットワーク宛のHTTPS通信を許可するルールを追加すれば最低限動作はします。 Container Apps上にコンテナーアプリを立ち上げる Container Apps上にコンテナーアプリを立ち上げます。 まずは、Container Apps環境を作成します。今回はAPI Managementから仮想ネットワーク経由でコンテナーアプリにアクセスさせるため、「自分の仮想ネットワークを使用する」を「はい」とし、仮想IPは「内部」を選びます。 コンテナーアプリ側では、イングレスを下図のように設定します。Container AppsなのでHTTP/TCPやターゲットポートを任意に指定できますね。実際には2種類のコンテナーアプリを作成しましたが、ここではAPIサーバーのみ紹介します。もう一方もやり方は同じです。 API Managementリソースを作成する 事前にAPI Managementに割り当てるパブリックIPアドレスを作成しておき、それからAPI Managementのリソースを作成します。下図のように設定することで、仮想ネットワークに内にリソースを作成し、割り当てたパブリックIPアドレスによりインターネットからアクセスできるようになります。 プライベートDNSゾーン リンク 仮想ネットワーク内でAPI Managementやコンテナーアプリがお互いに名前で通信できるようにするために、プライベートDNSゾーンを作成し、仮想ネットワークに割り当てます。 作成するプライベートDNSゾーンの「名前」には、コンテナーアプリの「アプリケーションURL」のドメイン部分を指定します。 作成が終わったら、先ほど作成した仮想ネットワークを選択して割り当てます。 さらに、レコードセットとして、コンテナーアプリの名前とIPアドレスを対応づけるAレコードを、コンテナーアプリごとに追加します。この時、IPアドレスはどのコンテナーアプリもすべてContainer Apps環境の静的IPアドレスを指定します。 API Managementにコンテナーアプリを割り当てる API Managementの「API」メニューから「Container App」を選択し、コンテナーアプリを割り当てます。さらに、「Subscription required」にチェックすることで、「サブスクリプション」メニューにあるキーをAPIキーとして利用できるようになります。 動作確認 以上で構築が完了しましたので、動作確認をしてみます。 まず、コンテナーアプリに割り当てられているURLは、名前解決もできずインターネットからアクセスすることができません。 次に、APIキーなしでAPI Mangementに割り当てられているURLにアクセスすると、以下のようにエラーが返ってきます。 最後に、APIキーを付与して同じURLにアクセスすると、正常にレスポンスが返ってきます。 まとめ 実際の利用時には、APIキーよりもContainer Appsに組み込みの認証機能を利用することが多いと思いますが、この記事ではAzureの機能を使いつつ、APIキーでアクセス制限をかける方法を紹介しました。なお、App Serviceでは同じことが簡単にできると記載しましたが、その場合はApp Service上にアプリを立ち上げた後、API Managementをそのアプリに紐づけると、ここで紹介したネットワークやDNSなどの設定を全て自動で終わらせてくれるという感じです。この記事が今回紹介したような構成を検討している方の参考になれば幸いです。
アバター
梅雨の時期でジメジメしてきましたね。なかなか外にでかけづらいので休日はティアキンに精を出したいと思っているエンジニアリングマネージャの猪子です。 ある程度経験を積んだエンジニアであれば、過去何回か案件や通常業務で利用する技術の選定をしたことがあるかと思います。 本記事ではInsight Edgeにおける技術・アーキテクチャの採用指針や実際に採用した技術について紹介したいと思います。 採用技術・アーキテクチャを決める軸 企業でメインとして利用する技術や案件で採用するアーキテクチャを決める為の基準は非常に重要で、判断を間違うと著しく開発効率が下がったり、更に悪い状況だとサービス継続が危ぶまれる場合もあります。 これまで、幾つかの技術を採用してきましたが、私が新規に技術を採用する際の観点は以下にまとめられるかと思います。 観点 内容 ビジネス目標との整合性 選定する技術は企業のビジネス戦略と一致しているか。例えば、高度なデータ分析を必要とする企業であれば、より高度な機能を持つデータレイク、データストアなどのツール / サービス選定が必要です。 非機能要求 機能だけでなく、アプリケーションのパフォーマンス要件、セキュリティ要件、信頼性要件等、非機能面での技術的な要求に対応できるかという点です 費用対効果 採用する技術のコストは、その技術が提供するビジネス価値に対して適切かどうか。直接的な調達コストだけでなく、メンテナンスやトレーニングのコストも考慮が必要です。 開発者との親和性 開発者との親和性も大切な要素です。親和性においてはスキル・経験があり、効率的に技術を利用できるか、という側面の他に、採用の観点を含め開発者がその技術利用したいというモチベーションがあるかという側面もあります 技術の成熟度とコミュニティのサポート 新しい技術を採用する際に技術がまだ成熟していない場合、成熟しているがEOLが近い場合、業界で利用している開発者が少ない場合は注意が必要です(特に言語・FW等)。又、コミュニティサポートや豊富な情報リソースがあるかも要確認です。 全てを完全に満たすものというのは難しいので(基本存在しない)、その時々の状況に応じた観点の優先度を決めつつ評価・評価していく事が必要です。 過去に自分が見た失敗事例でいうと、選定したプログラム言語としては"ビジネス目標との整合性"、"非機能要求"、"開発者との親和性"については合致していたものの、"技術の成熟度"という側面で使いこなせる人材が業界に少数なため、運用保守出来る人材を採用できずサービス継続のために泣く泣くプログラム言語を移行したというものがあります。。。 Insight Edgeの採用技術・アーキテクチャの方向性 Insight Edgeは元々少数のメンバでアプリ開発・データ分析両方の案件をスピード感を持って推進するという"ビジネス目標との整合性"、"開発者との親和性"や"技術的な成熟度"からPythonを選択していました。 フロントエンドについても同様の観点からReact / TypeScriptを採用しています。 ちなみにGitHubが発表している 2022年で最もGitHub上で利用されている言語 の1-4位は上からJavaScript、Python、Java、TypeScriptとなっています。(4つ中3つをInsight Edge内で利用) 元々PythonはML系のライブラリ・機能の優位性からデータサイエンティストを中心にそのシェアを伸ばしてきていると思いますが、近年、ML系技術がさらにコモディティ化しており、今後もWebエンジニアがML系技術を積極利用することは変わらないと思うのでPythonのシェアは高いまま維持し続けるだろうと予想しています。 これらある程度全社共通的な技術を利用しつつ、Insight Edgeでは今後の案件でさらなる開発効率化が見込めそうなものについては積極的に技術を取り入れるようにしています。(目安として、1つの案件で1つは技術的なチャレンジを取り入れるようにしています) このあたりのチャレンジングな技術要素は案件の主担当者のエンジニアに委ねています。 我々の主な目標はPoCの開発であり、PoCではスピードを出しつつ、検証に必要十分な品質を確保するのが重要な要素になります。如何にこれを満たすための最適な技術・アーキテクチャを選定するかが案件をリードするエンジニアの腕の見せどころであり、常日頃からエンジニアも業務の中で新規技術のキャッチアップを行う事が出来るような全社的な取り組みをしています。 採用技術・アーキテクチャ例 これまでInsight Edgeで採用した具体的な採用技術を幾つか紹介します。 業務共通 通常業務で利用しているツール・サービスについてはInsight Edge設立当初に既存メンバが慣れているもの、という観点で決めたものが多いです。 概ね一般的なものが多いですが、WrikeについてはWBS・ガントチャートのUIが心地よくて個人的に気に入って使ってます。 用途 ツール・サービス名 メッセージング Slack ドキュメント管理 Confluence プロジェクト管理 Wrike レポジトリ GitHub クラウドPlatform AWS / Google Cloud / Azure 業務でよく使うものについては、"費用対効果"も重要ですが、開発者がモチベーション高く気持ちよく仕事が出来るか("開発者との親和性")を重視しています! 開発編 Webアプリ Webアプリを素早く構築技術は多種多様であり、Insight Edgeでもスピード感を持ってPoC開発をするために各種技術を使い分けています。又、必要に応じ、仕様の複雑さ、技術的な制約を鑑みてノーコード・ローコードを活用し、出来るだけ"作らない"開発を目指しています。 その他、各種サービスモジュールも多分に活用しています 用途 ツール・サービス名 ノーコード・ローコード Bubble / Power Apps / AppSheet / GAS / GitHub Copilot etc 認証 Cognito(AWS) / IAP(GCP) CICD GitHub Actions ローカル開発環境 Docker Desktop その他 各種クラウドPlatformのサービス等 IoT IoT機器については要件によって各種センサ / 通信プロトコル / 電池等を使い分けるので案件毎で千差万別です。 通信プロトコル(主にLPWA)については、各種ありますが導入のし易さ(基地局設置不要)、通信コスト等の観点から "LoRaWAN", "Sigfox"を採用することが多いです。 データ分析編 モデル + 簡易IF データ分析系の案件では何らかのモデルを構築し、ユーザが利用するための簡易的なIF(画面 / API)を提供することが多いです。 元々はスクラッチで作ることもあったのですが、最近では Streamlit , Mercury 等を使い、JupyterLab等で記載されたプログラムをWebアプリ化することが多いです BI / DWH BIツールはコスト・機能・デスクトップアプリとしてのレスポンス性能で Looker Studio, Tableauを使い分け、MS Suiteとの統合が必要な場合、Power BIも利用しています。 DWHはBigQueryを基本にしつつ、最近はSnowflakeも採用をし始めています。パフォーマンスや、コスト効率、AWSしか使えないが、BigQueryの利用体験をしたい時に採用する想定です. その他 その他継続的にChatGPT、XR等最新の技術トレンドもキャッチアップし、業務・案件の中に取り入れています。 尚、Insight Edgeでは業務時間の10%を活用し、これらの技術トレンドのキャッチアップ等自己研鑽に取り組む"勉強会"の制度を設けています。 勉強会は業務に直結しないテーマもOKで、エンジニアだけではなく営業やコンサルタントの人も参加し、自由な発想や創発が生まれる場としています。 終わりに 如何だったでしょうか?Insight Edgeは、カイゼンのマインドを持ち日頃から最適な技術を調査・選定するのが好きなメンバが集まっている会社です。 技術を活用した効率化が好き!新規技術にも取り組んでいきたい!という方はカジュアルにお話させて頂ければと思いますので、お気軽に こちら からお申し込みください!
アバター
Insight EdgeのLead Engineerの日下です。 弊社ではちょっとしたWebアプリを作るときに、AWSを用いたサーバーレスアーキテクチャで フロントエンド CloudFront + S3 + SPA(React等) バックエンド API Gateway + Lambda という構成をしばしば使います。 今回は、この構成においてありがちなキャッシュによるバージョン不整合の対策について紹介します。 SPAにおけるキャッシュ問題 上記の構成は安価かつスケーラブルにSPAを運用できることが魅力ですが、 フロントエンドの静的ファイルに対してブラウザのキャッシュやCloudFrontのエッジキャッシュが働いてしまい、 アプリの更新がうまく反映されなかったり、フロントエンドとバックエンドのバージョン不整合の原因になることがあります。 jsやcssファイルにはハッシュ値が付与されるものの、呼び出し元であるindex.htmlにキャッシュが効いてしまうとjsやcssも古いファイルが参照されるため、 それによってキャッシュされたものが呼び出されたり、リンク切れが発生したりしてしまいます。 デプロイのたびに CloudFrontのキャッシュを削除(Invalidation)してもブラウザのキャッシュには対策できない ため ユーザー側でフルリロードしなければ最新化されずバージョン不整合などの原因になります。 アプリの処理としてバージョン整合性チェックを実装する方法もありますが、今回はお手軽な方法として、 index.htmlファイルにCache-Controlを設定してキャッシュを無効化する方法を紹介します。 S3のオブジェクトメタデータにキャッシュ動作を設定する CloudFront + S3 で配信するファイルは、S3のオブジェクトメタデータにCache-Control等を設定することでHTTPヘッダに反映できます。 マネジメントコンソールで確認・設定する場合、index.htmlのファイルの詳細画面にて プロパティ タブの メタデータ の項目に設定します。 S3のオブジェクトメタデータ設定 デプロイ時にAPIで設定 自動デプロイに組み込むためにAWS CLIでもやってみます。 CLIにはオブジェクトメタデータのみを編集するコマンドが無いため、以下のように aws s3api copy-object を使用してメタデータを編集します。 その際、Content-Typeなど他のメタデータも明示的に指定しないと失われてしまうため、 --content-type オプション等で指定しています。 # 既存のオブジェクトにメタデータを設定 aws s3api copy-object --bucket your-web-contents-bucket --key index.html --copy-source your-web-contents-bucket/index.html --metadata-directive REPLACE --cache-control "no-cache, no-store" --content-type "text/html" このコマンドは既存オブジェクトのメタデータを編集できますが、 aws s3 sync 等でファイルをアップロードした後にメタデータ編集を実行する手順だと、そのタイムラグの間にキャッシュ設定未反映のファイルがユーザに配信されてしまう可能性があります。 アップロードと同時にキャッシュ設定を反映させるには、先にindex.html以外を aws s3 sync で更新し、 最後にindex.htmlを aws s3 cp コマンドでメタデータ設定とともにアップロードすると良いでしょう。 # index.html以外をアップロード aws s3 sync ./build s3://your-web-contents-bucket --exclude index.html --delete # index.htmlをメタデータ設定とともにアップロード aws s3 cp ./build/index.html s3://your-web-contents-bucket/index.html --metadata-directive REPLACE --cache-control "no-cache, no-store" --content-type "text/html" CloudFront経由で配信されたindex.htmlのヘッダをブラウザの開発者ツールで確認してみます。 ちゃんと設定されていますね。 レスポンスヘッダ
アバター
こんにちは。Insight Edge開発チームのntです。前回の投稿 1 では、事業案件や技術的な話について紹介しましたが、今回はInsight Edgeの社風に焦点を当てます。 Insight Edgeのバリューの1つに「みんなでやる」という考え方があります。そのバリューを実現するために、チームワークを高めるための活動のひとつとして、弊社分析チームの善之さん( yoshiyuki555 )らと共に企画して開催したシャッフルランチ会です。 以下、その開催レポートです。 はじめに 仕事上のチームビルディングや新しい人間関係を築くために、飲み会や最近ではランチ会などの社内イベント企画が多いかと思います。ランチ会とは、社員同士でグループを組み、会社がランチ代を補助してランチに行くことができる制度です。 Insight Edgeは2019年に創業して以来、社員が順調に増えています。しかし、創業間もなくコロナ禍に見舞われ、安全第一の観点からテレワーク環境が増えました。成果につながるためには、フィジカルなコミュニケーションの機会を確保する必要があります。そのため、出社によるコミュニケーションの活性化が重要と考え、火曜日と金曜日を出社推奨日とし、その他の日でも出社が可能としています。一方で、同じ事業案件を担当している人や、何らかの用事がある人以外とは、会話が自然に発生することが少ないという課題がありました。 旧ランチ会企画とその課題 以前、コミュニケーション活性化や異なる組織ラインの交流促進のためにランチ会を導入しました。補助利用には条件があり、異なる組織ラインで4名~6名程度のグループを組む必要がありました。開催の流れは、Slackで「今日会社来ている人でランチ行きませんか?」と声をかけ、メンバーでランチに行く形式でした。1カ月間の運用後、次のような課題が浮上しました。 ランチ会を誘う人の心理的負荷が高い ランチ会を利用するメンバーに偏りが生じ、期待するほど活性化の効果が得られなかった この問題を解決するため、新しいランチ会企画を立ち上げました。以下では、企画の流れやアンケート結果について詳しく説明します。 ランチ会企画の流れ ランチ会企画の準備期間は1カ月で、実際にランチ会を3カ月間運用しました。最後にアンケートを実施し、振り返り評価しました。 自身の問題意識 私がこの問題に取り組む理由は、出社時に声かけや雑談をするものの、コミュニケーション範囲に限界を感じていたからです。接点数(=雑談した人数)をシステマチックに拡大するアイディアで効果が得られると考えました。マネージャーと週次の1 on 1でアイデアを提案し、別の組織ラインからのメンバーも参加し、運営チームを立ち上げました。 運営チーム結成・課題認識・打ち手検討(1週目) 運営チームは、開発チーム1名(私)、分析チーム1名(善之さん)、マネージャー1名の合計3名で結成されました。企画効果を定量的に振り返ることができ、形骸化しないよう配慮しました。以下の問題解決のため課題や要件を定義しました。 目的:コミュニケーション活性化 前提:コミュニケーション活性化は、社員同士の接点が増加している状態を意味する。ランチ会に参加することで、社員同士の新たな接点が生まれる。 手段:ランチ会企画の運営により、社員全員が偏りなくランチ会に参加し雑談できる環境を整え、接点数を効率的に最大化する。 企画の要件: 参加対象:社員全員 参加回数:各社員が3回参加(月に1回、3ヶ月間) 取りまとめ:各社員が1回ずつ取りまとめを担当し、負荷を分散させる。 グループ人数:1グループあたり4~5人 組み合わせ方法:最適化アルゴリズムを実装する。 開催期間・組み合わせ案:3ヶ月分の開催時期と組み合わせ案は事前に指定する。 課題認識が一致した後、次週から企画準備タスクに取り組み始めました。 ランチグループ及び参加者の組み合わせ方法(2~3週目) 最適化アルゴリズムを使って、社員同士のランチ会の組み合わせ案を作成しました。 まず、社員の組み合わせ表(接点の有無のマトリックス)を作成。各チームリーダーに協力を得て、接点があるペアには1、ない場合には0と記入。センシティブな情報なため社内公開は見送りました。 次に、最適化アルゴリズムを以下の要件に従って実装: 現状の接点組み合わせを入力 同じグループにマネージャーは1人まで 開催することでグループ内の社員同士の接点を増やす 上記を3回反復して全体の接点数を最大化 実装は善之さんが担当。出力結果をもとにGoogleスプレッドシートでランチグループ管理表を作成し、全員へ展開。調整や記録が容易になります。 社内決裁や、参加者にランチ会について周知するために用いる企画資料の作成も行いました。これで準備が整いました。 企画の承認(3週目) 社内決裁は運営マネージャーが取りまとめ、迅速に承認および経費予算の決裁を得ました。 シャッフルランチ会の運用(4週目~(3ヶ月間)) 運用フェーズでは以下を実施しました。 運営側: 企画案内・周知 月に1回各グループの推進を依頼 参加者側: 合計3回参加(取りまとめ役を最大1回担当) ランチ時間内に雑談や情報交換 他のチームが行ったお店を候補選定に利用 以上で、シャッフルランチ会が3ヶ月間進みました。 振り返りと評価 シャッフルランチ会の効果を調査するため、アンケートを実施しました。 アンケート設計 アンケートで得たい情報は以下です: 効果についての定量的評価 ランチ会の改善ポイント 他のアイディアがあるか Google Formsで1週間の記名式アンケートを行いました。 結果と今後の改善点 回答率は74%。結果サマリは以下の通りです。 質問 結果サマリ ランチ会で新たな接点が作れたか? 全員が新たな接点を作ることができた ランチ会で不安や抵抗感が減ったか? 全員不安や抵抗感が低減、最も多かった回答は「かなり減った」 新たに話せるようになった人数 目標と比べて若干少なかったものの、平均して複数名との接点が作れていた 気軽に話せる人数 全員一定数いるものの、まだ増加余地あり ランチ会の頻度、継続する場合の頻度 月1回がちょうど良い。継続するなら1ヶ月に1回前後が適切 改善点(自由記述) 実施負荷軽減、チーム編成 他の企画アイディア(自由記述) 交流機会増加、社員のプロフィール拡充 具体的な数値や考察はここでは省略しますが、ランチ会が新たな接点作りやコミュニケーションの不安減少に効果がありました。 まとめ ランチ会開催による検証結果は以下のとおりです。 アンケート調査によれば、チームビルディングや社内コミュニケーションが向上しました。 コミュニケーション改善を継続するために、ランチ会を引き続き実施することが望ましいです。 ランチ会を継続する際は月1回程度が理想的であり、実施負荷の軽減やチーム編成の改善も検討が必要です。 Insight Edgeの社内コミュニケーション向上取り組みが、同様の課題を持つ組織にとって参考になれば幸いです。 また、Insight Edgeは社員が自ら意見を述べ、成果向上のために柔軟に組織を変革できる環境です。この柔軟さを活かし、会社を共に盛り上げてくれるメンバを募集しています。 興味がある方はカジュアルにお話させて頂ければと思いますので、お気軽に 公式サイト の採用ページからお申し込みください! Google Sheets + Google Apps Script でローカル開発・本番デプロイ ↩
アバター
こんにちは! Lead Data Scientistの梶原(悠)です。 Insight Edgeには商社内の資源系ビジネス部門から市況・需要予測系の相談が多くよせられます。 しかし、この種の案件は予測モデリングの本質的な難易度とユーザーからの期待値のずれが大きく、なかなか有効な活用に繋がりにくい印象があります。 こうした現況の改善に向けて技術的な論点は色々あるのですが、今回は、市況が急変するなどしてデータの分布が大きく変わるケースの対処をテーマに、簡易なツール調査を行います。 目次 ドリフトとは ドリフトの検出ツール 検出デモ まとめ データドリフトとは データドリフトは、時間の経過によって特徴量の分布が変化してしまう現象です。 例えば、金属価格を予測する機械学習モデルを訓練し、継続的に運用するサービスを考えます。 このモデルの説明変数の分布は、イノベーションや地政学的なショックにより変化する可能性があります。 学習時点では現れなかったようなサンプルが頻出すると、予測の信頼性が悪化する可能性があります。 関連する概念として、コンセプトドリフトやラベルドリフトなどが挙げられます。 概念 継続的運用やオンライン学習の文脈 バッチ学習の文脈 説明変数と目的変数の関係の変化 コンセプトドリフト(Concept drift) コンセプトシフト(Concept shift) 目的変数の分布の変化 ラベルドリフト(Label drift) ラベルシフト(Label shift) 説明変数の分布の変化 データドリフト(Data drift) データセットシフト(Dataset shift) / 共変量シフト(Covariate shift) データドリフトの影響を緩和するために、定期的なデータの監視とモデルの再学習が有効です。 運用環境のデータと訓練データとの間で分布の差異を監視することで、ドリフトの早期検出が可能になります。 ドリフトの検出ツール オープンソースのドリフト検出ツールは多数存在します。 使える手法の豊富さや可視化機能の充実度合いなどによる差別化が見られます。 ツール名 ライセンス プログラミング言語 フレームワーク ドキュメンテーション Seldon Alibi-detect Apache 2.0 Python TensorFlow, PyTorch https://docs.seldon.io/projects/alibi-detect/en/latest/ Evidently Drift Detection Apache 2.0 Python scikit-learn https://docs.evidentlyai.com/ TorchDrift MIT Python PyTorch https://torchdrift.org/ Deequ Apache 2.0 Scala, Java Apache Spark https://github.com/awslabs/deequ 検出デモ Seldon Alibi-detectによるシンプルなドリフト検知デモを行います。 確率分布間の距離の指標であるMMD(Maximum Mean Discrepancy)を使ってドリフトを検知するalibi_detect.cd.MMDDriftOnlineクラスを使用します。 ある企業では翌日の金属価格の予測モデルを日次で運用しているとします。 さらにこのユーザーは、説明変数にガス・石油の燃料価格を用いているとします。 これらの変数のドリフトを捉えたいとします。 まず、セントルイス銀行のWebサイトからガスと石油のサンプルデータを取得してみます。 import pandas as pd import pandas_datareader.data as web import datetime tickers = { "PNGASJPUSDM" : "Gas" , "DCOILBRENTEU" : "Oil" } data_df = ( web.DataReader( tickers.keys(), "fred" , start=datetime.datetime( 2016 , 1 , 1 ), end=datetime.datetime( 2023 , 2 , 1 ) ) .interpolate() .dropna() .rename(columns=tickers) ) 可視化してみると、2020年前半の石油価格の落ち込みや2021年からのガスと石油の価格高騰が目立ちます。2020年の落ち込みはコロナショック、2021年からの高騰はウクライナ危機によるものと想像されます。 現在は2019年の1月だとします。過去のデータを参照期間として、ドリフトの検出器を作成します。 検出器の内部では、並べかえ検定によりドリフト検出の閾値が作成されます。 今回のケースでは、並べかえ検定による閾値は小さすぎて実用的でありません。 実際に使う場合は、参照期間のデータの時系列クロスバリデーションなどにより、自分で閾値を作り込む対処が考えられます。 from alibi_detect.cd import MMDDriftOnline # データを参照期間とテスト期間に分割する. now = pd.to_datetime( "2019/1/1" ) ref_df = data_df[data_df.index <= now] test_df = data_df[now < data_df.index] X_ref = ref_df.values.copy() X_test = test_df.values.copy() # ドリフト検出器を作成する. detector = MMDDriftOnline( X_ref, ert= 500 , window_size= 30 , backend= "pytorch" , verbose= True ) 未来のデータをテスト期間とし、検出器を走らせてMMDを算出します。 素朴な参考値として、参照期間で走らせたMMDも算出してみます。 # 参照期間とテスト期間のMMDを算出する. test_df[ "squared_mmd" ] = [detector.score(x) for x in X_test] ref_df[ "squared_mmd" ] = [detector.score(x) for x in X_ref] テスト期間のMMDは、2020年の4月や2021年の10月に参照期間におけるMMDの最大値を超えています。 これは、コロナショックやウクライナ危機による燃料価格の急変を検出していると想像されます。 まとめ この記事では、データドリフトの検出ツールのライトな調査を実施し、燃料価格のデータでMMDによるドリフト検出のデモを行いました。 かなり尻切れとんぼな内容になってしまいましたが、何らかの参考になりましたら幸いです。 学習時と運用時の間でデータの分布が変わってしまう問題周辺は、継続的に調査していきたいと思っています。
アバター
  はじめまして! Insight Edge で UI/UX デザイナーとして働いている佐藤と申します。 IT の業界におられる方はご承知の通り、UI デザインツールは様々存在し日々進化をしてきました。 Adobe XD、Sketch、Figma と様々、実務の中で導入し使用してきた中で、コンポーネント作成やプロトタイプ作成、リサイズやレスポンシブデザイン、そしてエンジニアへインスペクト連携する際などの効率性などの視点などから、現時点で最も優れているツールは、Figma が頭ひとつ抜きん出ている印象に感じております。 そこで、この記事では、Autolayout によるデザイン作成などを中心に、使い方や作り方の基礎をお話出来ればと思います。 AutoLayout は業務の効率化に繋がる AutoLayout を使いこなす上での基本的な機能 拡大縮小時に便利なフレーム設定 起点を設定する方法や設定の場所 要素の配置バリエーション バリアント作成をしてさらなる効率化 バリアント機能を使ってボタンの状態変化を簡単に作れる プロトタイプ作成時にも重宝 ChatGPT にも訊いてみた まとめ 参考記事 AutoLayout は業務の効率化に繋がる デザインがある程度決まった後に、或いはプロトタイプを作成する段階で、様々なデバイスでデザインの確認や検証を行うケースがあると思います。共通部品となる要素をコンポーネント化し、かつ中身を AutoLayout によって作成しておくと、部品のリサイズやプロトタイプ作成時において、デザイン業務の効率化や高速化を実現できます。 今回、記事用にサンプルの画面デザインを用意しました。 下記の画像の場合カード部分が共通コンポーネントです。 AutoLayout を使いこなす上での基本的な機能 拡大縮小時に便利なフレーム設定 Figma の インターフェースの項目に「フレーム」というものがあります。 ここで何を基準にして中身を構成するか設定できます。 上のアニメーションで見せたような、横に伸ばして要素を伸び縮みさせる方法はここのメニューを駆使して調整していきます。 上記にある GIF 画像のように、文字量に応じて縦の長さを可変させる場合は「コンテンツをハグ」を、なおかつ横に伸ばす時に本文部分をカード横サイズに合わせて追従させる場合は「コンテナに合わせて拡大」を選択します。固定幅はその名の通りです。これらを応用して組み合わせて、コンポーネントを作成しておけば、いくつも同じ要素が並ぶようなインターフェースを作る際に一気に展開や修正することができます。 起点を設定する方法や設定の場所 先程、見ていた「フレーム」メニューの真下に「オートレイアウト」のメニューがありますね。 ここも作成する上で大変重要で、よく触るメニューです。 「フレーム」&「オートレイアウト」で基本的にはおおよそのレイアウトは完成しますし、要素を揃えたりマージンを共通の数値にしたり、起点を設定したり、とても役立ちます。 余談ですが、私はこの Figma のインターフェース(9つのマトのようなもの)を初めて見た時に、何か操作できるものに直感的には正直見えなくて、気づいた時「え?これ触れるの?」って戸惑った記憶があります。気付くまで、苦戦した記憶があります…。 整列系のメニューが上部にもあるので、感覚で覚えようとしたら遠回りしました。 なので、初心者の方は是非ここを見落とさないようにすると大変良いと思います。 リリース初期から触ってる方や、詳しい方にとっては「そんなの普通に分かるわ!」と言われそうですけど、この仕組みに気付くまで、灯台下暗し状態でした。 気づいた時、これ楽々だな〜と感じたのを思い出します。 ここを理解しておけば、情報設計の構築 → デザイン案用にタタキ台や感覚で作った状態の絵 → 清書を進めながら要素の整列をし精緻化していく → プロダクトの対応デバイスに応じてデザインのリサイズやイメージの作成をする → エンジニアにインスペクト連携する、といった一連の UI デザイン作成の流れを素早く行うことが可能になります。正確な数値で作りたい時や起点をどこにするかの設定にも、一瞬で応えてくれるので便利です。 要素の配置バリエーション 詳細設定の使い方次第で様々な表現が可能になります。ほんの一例ですが画像で紹介しておきます。 バリアント作成をしてさらなる効率化 バリアント機能を使ってボタンの状態変化を簡単に作れる UIデザインを作成していく上で、上記に触れたレイアウトの他に、ボタンの状態の変化やプロトタイプを作ってイメージを確認する時などに役立つのが「バリアント」機能です。 こちらを準備しておくと下記の画像のように、同種のコンポーネントを一つのグループにして、レイヤー名を自動でプロパティ値に設定してくれるので、ボタンの状態変化を表現したりプロトタイプの作成時に便利です。 プロトタイプ作成時にも重宝 プロトタイプやモックを作る時にも非常に便利です。 プロトタイプパネルへ移動した後、対象のコンポーネントを選択して、インタラクションを追加し、挙動に応じてプロパティを変更するだけで、プロトタイプの作成時に、状態変化用に別ページを増やさなくても作ることができます。 ChatGPT にも訊いてみた 昨今もっぱら世の中を賑わせている ChatGPT。 せっかくなので GPT 先生にも、以下のように質問してみました。 解説上手すぎて完全に負けました…(途中で「使っ」で切れてるけど…) 今回は、可視化して説明することに重点を置いたので、自分の言葉で書き始めてまとめており、抜粋して使うこともしなかったのですが、今後は利活用していこうっと…(泣)と感じております。 まとめ 難易度としては、さほど高くない内容ですが、初級の方に向けて参考になればと思っています。 冒頭でも述べましたが、ここ数年のプロトタイピングツール戦国時代の中で、主に私は、並行してAdobe XD や Sketch そして Figma を導入し、実務の現場の中で覚えて参りました。 20年以上デザインの現場にいる者として、Adobe Illustrator や Photoshop のレガシー的なツールに慣れ親しんだ人間としては、ソフトの使い方の観点という意味で最初は難航したものの、実際に手を動かしてマスターするとそれぞれの利点に納得するものです。 そして、その中で「使える」ものはどんどん吸収していくべきだと考えています。 AutoLayout についてもその中の小さな手法に過ぎませんが、日々新しい事を覚えて、面白く感じる、便利に感じる、痒い所に手が届いている、なんて感じる日々を積み重ねる事が、何かを良い方向に変えていく原動力になっていくものと信じています。 大袈裟ですが、それが業務効率化や作成速度の向上に繋がるのであれば、他の新たなキャッチアップに時間を割くことも可能になりますし、時代の流れとして今後は使えるものを上手に使う、Figma の AutoLayout や先に述べた ChatGPT についてもそうですが、そういった「道具」をより上手く使って、効率的に仕事をこなしていくことが求められていくようになっていく気がしています。 本日現在だと、Figma に対応した GPT AI プラグインも出現してきています。便利なモノは積極的に利活用して、良き相棒が自分の側にいるような気分で楽しんで参考にしながら、進化するデザインツールや AI と向き合っていきたいと感じている今日この頃です。 参考記事 Figma auto layout playground (Community) https://www.figma.com/community/file/784448220678228461/Figma-Auto-Layout-playground/Figma-auto-layout-playground Figma tutorial (What’s new in Auto layout) https://www.youtube.com/watch?v=floQKLsWAy4 www.youtube.com  
アバター
こんにちは、開発エンジニアの熊田です。入社してから早くも2か月が経ちました。 今回は、AWSに触れる機会の少なかった私が、ある案件のアプリ基盤のリプレイス作業を担当した経験を振り返りながら、運用保守を外部委託するまでの話を書いていきます。 目次 AWSアカウントの分離 背景 - これまで社内共通AWSアカウントで運用していた 新規AWSアカウントを作成 アプリ基盤のリプレイス 課題 - ストレージ不足とデプロイ作業の属人化 対応 - Fargateの導入とドキュメント整備 運用保守の外部委託 ユーザ管理と最小権限 役割の明確化 振り返り ECSではなくLambdaの方がベストだった IaCと自動デプロイ利用による改善余地あり 終わりに AWSアカウントの分離 まず、リプレイス作業するにあたり、既存環境とは別に新環境用のAWSアカウントを用意しました。 AWS Organizationsを利用していましたので、アカウントは「組織の一部であるAWSアカウント」として作成しました。 背景 - これまで社内共通AWSアカウントで運用していた AWSアカウントを分離する背景には、Insight Edge設立初期の案件という特有の事情がありました。 技術検証用AWSアカウントが用意されており、開発メンバは各々そこでAWSサービスを利用して開発作業を行ってきました。 いまでも技術検証目的で利用するアカウントですが、AWS Organizationsを導入する以前ですと、技術検証用AWSアカウントのまま本番利用するものがありました。 今回のリプレイス対象がまさに上記に該当します。これまでは社内メンバが管理していたので特に支障はありませんでしたが、社員の保守工数を下げる目的で外部委託することにしました。 共通アカウント上にあることで問題となるのは、委託先に開示してはいけないシステムとしたいシステムの区別ができないことです。 これに対処するには、別途AWSアカウントを作成する必要がありました。 新規AWSアカウントを作成 そもそもAWS Organizationsは、用途ごとにAWSアカウントを分けるマルチアカウント運用をする際には必須といえるサービスです。 公式サイトによると、AWSアカウントを一元管理するサービスで、1つの管理アカウントと複数のメンバーアカウントで構成されます。 アカウントを組織単位 (OU) にグループ化し、各OUに異なるアクセス ポリシーをアタッチできます。 さらに、メンバーアカウントの一括請求にも対応しています。 「組織の一部であるAWSアカウント」としてアカウント作成する手順は 公式サイト に載っており、簡単に実施できます。 アプリ基盤のリプレイス さてさて、アカウントの準備ができました。 次に必要だったのは、旧環境の構成であるDocker on EC2から、Fargate&ECSへのリプレイスです。 EC2上でコンテナを動かす際に2つ課題が生じていましたので、それらに対応するためリプレイスを実施しました。 課題 - ストレージ不足とデプロイ作業の属人化 まずはストレージ不足による定期的なメンテナンス作業が必要であることがあげられます。サーバレスでない環境ではよく起こる問題です。 Linuxディストリビューションでのバックアップがストレージを逼迫させることがあり、サービスが停止することもありました。 もうひとつの課題は、デプロイ作業の属人化です。 最初にEC2上でコンテナを起動し、そのコンテナ上でソースファイルや学習モデルを更新していくため、初期のDockerイメージと乖離してしまっていました。 また、Dockerfileなども用意されていなかったため、現在使用しているDockerイメージが何をもとに作成されたのか、すぐには把握しきれなくなってしまいました。 最初に構築した人が作業する分にはこれまでの作業を把握しており大きな問題にはなりませんが、他の人が対応する場合はなかなか作業が進められない状況に陥っていました。 対応 - Fargateの導入とドキュメント整備 これら課題に対応するべく、Fargateを使うことにしました。 サーバレスであればストレージ不足に悩む必要もありませんし、諸々のメンテナンス作業からも解放されます。 また、もともとDockerコンテナを利用していたため、オーソドックスなFargate&ECS構成へリプレイスすることにしました。 構成図としては以下のような形になっています。 Application Load Balancer(ALB)の前にNetwork Load Balancer(NLB)を配置しています。 ALBのIPは動的に変わることを考慮して、この構成にしています。NLBにより静的IPを利用できるので、NLBのターゲットグループにALBを設定しています。 今回のアプリは、NLBのIPアドレスを利用してスプレッドシート(GAS)からAPIリクエストするため、上記のようにIPアドレスが変わらないよう対策しています。 構築する際は、こちらのブログを参考にさせていただきました。 Network Load BalancerのターゲットグループにApplication Load Balancerを設定する もうひとつの課題であるデプロイ作業の属人化を解消するために、ドキュメント整備にも力を入れています。 外部委託先の方が困らないようにするためにも、初見の人でもデプロイできる手順書を作成しました。 具体的には、ローカル端末でのイメージ作成、ECRにPush、ECSサービスの更新を実施する手順です。 今回のアプリは本番運用開始から数年経っており、頻繁にデプロイされるアプリでもなかったので、今回はコスト的な観点から自動デプロイは用意しませんでした。 余談ではありますが、ECS Execを利用できるように設定しています。 これにより、ローカル端末からFargateのコンテナへ直接アクセスできるようになります。 SSH接続が不要であり、エラー発生時のトラブルシューティングにも活用できる利点がありますので用意しました。 運用保守の外部委託 新環境の構築が終わりましたので、ようやく運用保守を外部委託する話に移ります。 なお、外部委託するにあたって、NDA(秘密保持契約)は必須ですので、委託先への情報開示は締結後に行います。 マネージャに対応してもらっていましたので、詳細については触れませんが用語の説明を記載しておきます。 NDA(秘密保持契約)とは 相手方に開示する自社の秘密情報について、契約締結時に予定している用途以外で使うことや、他人に開示することを禁止したい場合に締結する契約 (引用元:https://keiyaku-watch.jp/media/keiyakuruikei/himitsuhojikeiyaku/nda/) ユーザ管理と最小権限 システム面の話に戻ります。委託先のメンバが運用保守を行うためには、IAMユーザ、GitHubへのアクセス権限が必要になります。 IAMユーザについては、新しく作成したアカウントでIAMユーザを作成しました。 委託メンバは複数名いたので、グループを作成しポリシーをアタッチしました。 そして、グループにIAMユーザを追加します。 これにより、各メンバのアクセス権限をグループとして一括で管理できるようになります。 なお、グループにアタッチしたポリシーは最小権限に設定しています。 よく目にする用語だと思いますが、「最小特権の原則」に則っています。 セキュリティ強化を目的に推奨されてる原則で、障害や不正による被害を最小限に抑えられる効果があります。 GitHubについては、Organizationを利用しているので、outside collaboratorとして招待しました。 数あるリポジトリのうち一部しか参照してほしくないので、リポジトリごとに招待しています。 実際の作業は、GitHub管理者の方に実施いただきました。 役割の明確化 委託するにあたって、役割を明確にしました。当たり前な話ではありますが、仕事を進める上で役割・責任を明確にすることは重要です。 委託先には何を担っていただきたいか、一方、委託元であるInsight Edgeとしても何を担ったままなのか明確に定義しました。 そのうえで作業フローのイメージを共有することで、双方の仕事が進めやすくなるかと思います。 振り返り 今回の作業ではやらなかったことや、今後やってみたいことがありましたので、それらについてまとめていきます。 ECSではなくLambdaの方がベストだった? コンテナを実行する構成としてオーソドックスなFargate&ECS構成を採用しましたが、リクエストの量、リクエストの時間帯が限定的なことから、Lambda実行にした方が、コストメリットがあったように思われます。 ただし、新環境自体は高スペックではないので、コストの差は微々たるものでした。 それよりもlambdaはコールドスタート問題があり、処理時間が想定より長くなる可能性があったため、今回はUXを優先してLambda利用を見送りました。 今回は検証できませんでしたが、SnapStart等でLambdaのコールドスタートを短縮出来る可能性があるので今後検証してみたいです。 IaCと自動デプロイ利用による改善余地あり AWSサービスの構築はIaCを利用してコード化&自動構築する方法もありましたが、今回はスピード優先としてコンソール画面から環境構築しています。 また、GitHubワークフローなどを用意して、自動デプロイする手もありましたが、今回の案件は開発及びデプロイ頻度が極まれでした。 そのため前述でも触れたとおり、手順書、DockerFileがあれば十分対応できることもあり、コスト観点からGitHubワークフローの用意は行っていません。 今後、追加開発などが始まり、環境構築、デプロイ頻度が増える場合には、それぞれの自動化を検討したいと思います。 終わりに リプレイス作業と外部委託についての話は以上になります。読んでいただきありがとうございました。 Insight Edgeに入社して2か月経ちましたが、非常に充実した日々を過ごしています。弊社には、技術力の高い人たちが集まり、プロジェクトも挑戦的なものばかりです。 最初は私の慣れもかねてスタンダードな作業に取り組んでいました。そして、現在は新規サービスを開発する挑戦的なプロジェクトに関わっています。 前職では会計システム開発にずっと関わっており安定性重視の仕事をしてきましたが、いまは新しいことへ取り組む機会にあふれており非常にやりがいがあります。 いま参画しているプロジェクトでもたくさんの学びがありますので、次回はそのことについての記事を書いてみたいなと思います。
アバター
(with the Frontier Development Lab, SETI Institute, Triullium USA, NASA, and Google Cloud) Why go outside the company to build new skills New problems, new solutions, fresh perspective Insight Edge has a unique mission when compared to other DX-oriented startups: we target digital transformation among the overall Sumitomo Corporation group. That may sound like a limited purview -- after all, other Developer Experience and AI-themed startups may serve an entire market of industrial clients, as long as the project is mutually agreeable. While perhaps counterintuitive, our focused mission is actually a source of a wide variety of projects. Our collaborative relationship with our parent SC's DXC means that our projects can take a longer view, rather than fret over meeting strict quarterly profit quotas. We may incur short-term costs, as long as the longer-term mission and output are producing growth for SC and our global operating companies. You can think of us as building DX momentum for the group, rather than trying to maximize profits per project. With all of that being said, there are times when we, as data scientists and engineers at Insight Edge, could benefit from exploring problems outside the group. If all we ever look at are the obvious issues with which our partners are struggling, we could miss chances to be more proactive, inventive, and creative. Part of how we have accomplished this outside grounding is by having a diverse talent pool from the start. IE's technical staff includes those with doctoral degrees in chemistry, planetary science, neuroscience, and even astronomy (that's me). This gives our team a diverse and scientifically savvy background, but what's even better is the opportunity for us to reconnect with scientific-themed projects, or even better, novel challenges requiring a combination of professionals from ML and cloud engineering to physical science. In addition, our members often engage in external events like exhibitions, conferences, and workshops to encounter new issues which at least, on-the-surface, are different from what the SC group is currently struggling with (see the SC Group home page for an overview of our global activities). What is the Frontier Development Lab The Frontier Development Lab is a public-private partnership whose planning is year-round. However, the actual R&D takes place during an intense, adventurous (often challenging) 2-month agile development sprint. From Space to Sustainability FDL started in 2016, and originally focused on challenges combing space science domain knowledge with data science and machine learning practical expertise. Project themes have branched out from space exploration into sustainability-related projects, such as earth observation, disaster prevention and management, and renewable energy. Public-private partnership FDL's core government-side partner has been NASA, with the SETI institute acting as a liaison between NASA and private partners, such as NVIDIA, Google Cloud, and others. As of 2022, the US Department of Energy began sponsoring FDL challenge teams, broadening the scope beyond just projects with a clear space connection. Agile Development in Novel Situations The agile development aspect of FDL is what has always really inspired me. Because here you have a way of working that meshes well with what industry-employed developers and engineers often face: face-paced, incremental agile dev and problem-solving cycles. And at the same time, this is a skill that is also much-needed in academia, where projects can tend to stagnate or be siloed into the domain of a few key experts' labs. Astrobiology tools, and problems relevant to industrial data science From Frontier Science Toward Frontier Industries FDL has sometimes hosted challenges over the years that seem rather avant-guard. Probably at top of the list would be the "Astrobiology" team, which I was part of in 2018 and then again in 2022. You might think: "Wait, that has absolutely nothing to do with industry!" But, as a discipline, Astrobiology connects multiple domains and fundamental questions, including the challenge of understanding even the evolution of the most rudimentary systems of life and their theoretical constituent building blocks, to the future of our planet, and the search for signs of present or past life in our Solar System. The Need for Interdisciplinarity Personally speaking, working through the challenges of the Astrobiology project through FDL is analogous to the challenges I face in my industry job. At Insight Edge, we are constantly taking on new DX challenges from SC subsidiaries and business units from all sorts of domains, from manufacturing to energy grids --- and this has us always diving into new projects trying to learn how that domain handles their problems, how we can help, and how we can connect them to what we've learned from other domains, in completely different projects. This means the Insight Edge Engineers have to be somewhat interdisciplinary by nature. Astrobiology is likewise one of the most interdisciplinary research topics one can address. Even if you build a team, like ours at FDL, with two "Astriobiolgists" -- they will most likely have two very different perspectives and skillsets, e.g. geophysics and bioinformatics. FDL is as much about solving a problem we're given as it is redefining the problem based on our team's talents. Projects at Insight Edge are similar: given sometimes limited resources, we adapt what we can propose and how to accomplish it based on what talent we have available, on the actual situation on the ground. We have to build relationships with the folks actually working in and managing factories, for example. Likewise in FDL, in a very fast timeframe, we have to reach out to many different stakeholders, scientific collaborators, and data repositories in a very short timeframe, so that what we create at FDL is well-tuned to the actual scientific needs: we don't pretend to know everything about our challenge within our small team. Space Science and AI In recent years "AI" has become a kind of catch-all term for so many things. Sometimes the ways that FDL connects Space Science and AI can feel like Science Fiction. But we have more concrete reasons for looking to AI -- more specifically, autonomous spacecraft and robotics, and automated on-site sampling and data analysis techniques. Difficulty of pre-training models for unexpected environments Even the latest language models have difficulty being trained for all the different situations and contexts humans may throw at them. Now imagine you're training a model to explore new worlds: there is a huge risk that we can "overfit" such models to environments we know on Earth. Rather than simply pre-training a classifier on known living organisms and biomolecues, we needed to find a way to both remain open to yet unknown biomolecular structures, but still have something that coud perform inference rapidly and efficienty. Resource Limitations on Space Missions What most AI news these days is talking about is usually deep learning. Very large models of data, with millions, even billions of parameters, that can map inputs to outputs. You might have also heard how incredibly expensive these models are to train. Even so, actually using a trained model is often feasible enough that we can deploy them as web apps on cloud services and GPU servers and so on, such that tech-savvy users are bound to run across these tools on a daily basis (your iPhone auto-categorizing your photos, a web page for anime-izing your homework.) For reference, consider the computers onboard existing missions. The Mars Perserverance rover is equipped with only a single-core CPU, operating at only a few hundred MHz -- very conservative compared to even the standard environment of a Google Colab, or an NVIDIA GTX10 series budget gaming PC. Space hardware is heavily customized for specific environments, power constraints, and the need for redundancy in missions where hardware failure can mean the permamnent loss of a multi-million dollar mission. While it's true future missions will likely have more computing power than Perserverance, which launched in 1998, it is unlikely they will keep pace with the environments that deep-learning developers take for granted. Any comutational techniques developed now for future missions need to take such constraints into acount. Communication delays Space is big. Mission time is often limited. Especially when you consider specific windows of opportunity, like the time between dust storms on Mars, etc. While many calculational or simulational tools exist for assessing samples and deciding on the next steps of an experiment, two major limitations exist: 1) not all tools, but many of them can be very computationally expensive, and the hardware onboard space probes are often very conservative (and sometimes several years old by the time the mission starts!). 2) Mid-mission assessment, experiment planning, and re-planning, typically require human intervention -- meaning a round-trip transmission lag from your favorite planet or moon. Autonomous or at least semi-autonomous experiment guiding could save precious actual sampling time. Also, computationally expensive calculations and simulations could be "compressed" or emulated by a well-trained neural network, which could give us a balanced trade-off between accuracy, computational time, and hardware requirements. Life Creates Complexity: How do we search for it Agnostic detection approaches If we could assume life elsewhere in the universe resembled Earth life, tests for specific molecules and reactions common among all known organisms might apply. But how can we make such an assumption, knowing the vast diversity of environments out there? What our FDL project assumes instead is that life could take on molecular forms we may not recognize. Instead, we consider that life, in whatever form, is likely to assemble simple building blocks into more complex ones. A way of looking at this idea in more concrete terms, is to think in terms of molecular complexity: how likely is it that a sample of molecules detected on some celestial body formed without the involvement of life? Geological and atmospheric processes can definitely result in complex molecules, but statistically speaking, high abundances of increasingly complex molecules may be one of the most agnostic indications of the presence of living processes at work. For the technical discussion, I'll rely mostly on quotes from our workshop abstract , presented at the ML for Physical Sciences workshop , at the 2022 NeurIPS conference in New Orleans. By all means, please have a read, and look into the references therein! This was a very collaborative work, This paper was led by our team's ML-lead, Timothy (Timmy) Gebhard , and describes the work we carried-out over 8 weeks in summer 2022. Also on the core researcher team were Jaden ("J.J.") Hastings , who grounded our bioinformatics and space exploration goals; and Jian Gong , our primary geochemistry/geobiology domain expert, who also took the lead on our data strategy. We go into the details of three commonly cited metrics for defining molecular complexity. I'll quote the descriptions below, but the main point is that it's not an easy question to answer, so we opted to explore multiple metrics instead of favoring a particular one: another approach that tends to appear in industrial data science projects, whenever tackling a novel industry or task. Fundamentally, [molecular complexity, "MC"] measures are numeric features intrinsic to a molecule that represent an abstraction of its structure (or formation process) while also characterizing its information content. Intuitively, one may expect MC to increase with the molecular size, the multiplicity of bonds, or the presence of heteroatoms, while it should decrease with increasing symmetry (Randic, 2005). MC is usually not considered as an end in itself but used in a relative fashion to compare molecules or to characterize chemical reactions. Various definitions of MC have been proposed in the literature (see, e.g., the introduction of (Boettcher, 2016) for an overview), typically building on concepts from graph and information theory. In this work, we focus on the following three definitions: 1) Bertz complexity $C_T$ (Bertz, 1981): The first general index of molecular complexity. It combines concepts from graph and information theory and is defined as $C_T = C(\eta) + C(E)$, where $C(\eta)$ describes the bond structure and $C(E)$ the complexity due to heteroatoms. Calculating $C_T$ is fast and scales linearly with the molecule size. We compute $C_T$ using the BertzCT method from RDKit (The RDKit Team, Landrum et al.) 2) Böttcher complexity $C_m$ (Boettcher; 2016, 2017) An information-theoric measure that is based on the information content in the microenvironments of all atoms; it is additive and simple to calculate even for large molecules. Our computation of $C_m$ uses a freely available open source implementation (Boskovic et al, 2020). 3) Molecular Assembly index (MA) (Marshall et al., 2017): Also known as pathway complexity, the MA represents the minimum number of steps required to assemble a molecule from fundamental building blocks. MA is particularly well-suited to biosignature detection while being experimentally verifiable (Marshall, et al., 2021). It is at least as hard as NP-complete to compute (Liu, et al., 2021), requiring hundreds of CPU hours even for moderately sized molecules. We use a (currently non-public) implementation kindly provided by the authors of Marshall, et al. (2017). While we chose to look at multiple metrics, we also found correlation often exists between them. I mention this primarily because of how often this occurs in industrial data science too! Often in our projects, we receive data sources that at least at first glance, appear to be highly correlated. Only when we explore higher dimensional features can we see which data is more informative. We note that, to first order and at low molecular weights, these three measures are strongly correlated; the mass of the molecule acts as a confounder constraining the maximum complexity. When regressing out the mass, however, the correlation becomes less strong, and it becomes apparent that $C_T$, $C_M$, and MA each capture different aspects of the molecule. Inferring complexity with Machine Learning How to measure molecular complexity is not a settled matter (pun intended.) In our paper, we detail various approaches to train models on molecular complexity data, in hopes that we could eventually achieve a model that can -- at perhaps some accuracy costs -- very quickly infer scores that could otherwise take a very long time to calculate. Figure 1 illustrates where this capability might fit into a broader, automated analysis pipeline of a rover or probe exploring another celestial body. Dataset generation While the above talk of molecules and complexities and life and space might already sound overwhelming, the truth of this project -- not unlike almost every project I have worked on in industry as well -- is that dataset curation was the main obstacle. As often occurs in data science, potentially relevant but fragmented or not well-organized datasets are all around. But actually putting together that data needed, and having it in both a format ready for ML, as well as to be able to describe it easily to stakeholders, is most of the battle. The following text describes some of the technical particulars, but the main hurdle was acquiring mass-spectrometry data for all the molecules we wanted to explore. This is the practical crux of our challenge since mass-spectrometry is very likely the type of data that a future probe would actually sample. As no ready-made dataset for our task---inferring MC from MS data---exists, we created our own. For this, we queried a public database ( the NIST WebBook ), retrieving all molecules below 1000 Dalton for which an MS was available. These molecules were then appended with other basic chemical properties, including our three MC metrics. Empirical MS data on NIST were taken using electron ionization at a $m/z$-resolution of 1 (standard MS as opposed to higher resolution tandem MS that employs a variety of techniques). This is approximately comparable to the target resolution of 0.4-3 of the DraMS instrument onboard the Dragonfly mission (Grubisic, 2021). Our final dataset consists of 17,021 unique molecules with associated MS, randomly split into a training set with 12,000 molecules and an evaluation set with 5,021. [...] The major limitation of the dataset generation was the computation of MA, taking 65,000+ CPU hours over hundreds of compute-optimized nodes on Google Cloud in parallel. Prediction results Apologies in advance for the spoiler: we did not detect aliens by the end of the summer. We did achieve a model that shows promising performance at predicting various molecular complexity scores! The following quote gives some technical details, described visually in the figure thereafter. Unsurprisingly, all models outperform the naïve baseline (reducing the error by more than 50% in best case), and non-linear models perform better than the linear one. More interestingly, we find that there is a consistent trend across all models that MA is easier to predict than Böttcher complexity, which in turn is easier to predict than Bertz complexity (evidenced by respectively lower predicted errors). We speculate that this may have to do with the definition of the MA, which is, in a way, conceptually similar to the idea of mass spectrometry: The MA counts the number of steps to assemble a molecule from smaller pieces, while MS observers the patterns that emerge when a molecule is fragmented. Of course, what really matters is how this is put into use, highlighting another issue in industry: a lot of projects end in PoCs. Often preliminary results, even promising, may not translate to actual performance. This can be for a variety of reasons: the PoC dataset was not representative of production, the stakeholders have limited appetite for production implementation, or the PoC itself did not connect to a real commitment to production (i.e. its main goal was to convince a manager or administrator to pay attention, or allocate resources in a certain way towards long-term goals.) In our case, the project is designed to demonstrate that scientific and space exploration bottlenecks may be resolved with agile science-tech development, and machine learning approaches. The actual implementation depends on so many different factors, but we are extremely hopeful that this work has demonstrated the problem-solving potential of machine learning toward finding new neighbors in our solar system. FDL in 2023 FDL is an ongoing program, with the particular topics addressed varying each year. For the latest news, please see the FDL 2023 homepage ! You'll find the full summary of results for all FDL 2022 teams here . As for me, I'll continue to actively hunt for life beyond earth (with my Insight Edge vacation days); and continue to look for ways to both contribute my industry knowledge beyond Insight Edge -- but also for ways that are extremely distant (pun intended, as usual) topics and challenges can help inspire my work within Insight Edge, and the Sumitomo Corporation Group. References References appearing in the quotes above are listed here, but you can find the full reference list for our project in the workshop paper linked before. Here it is again for convenience! M. Randi´c, X. Guo, Plavši´c, and A. T. Balaban, “On the Complexity of Fullerenes and Nanotubes,” in Complexity in Chemistry, Biology, and Ecology, New York, NY, USA: Springer, pages 1–48. DOI: 10.1007/0-387-25871-x_1. T. Böttcher, “An Additive Definition of Molecular Complexity,” Journal of Chemical Information and Modeling, volume 56(3): 462–470, 2016. DOI: 10.1021/acs.jcim.5b00723. S. H. Bertz, “The first general index of molecular complexity,” Journal of the American Chemical Society, volume 103(12): 3599–3601, 1981. DOI: 10.1021/ja00402a071. The RDKit Team (G. Landrum et al.), RDKit: Open-source cheminformatics. Online Boskovic Research Group, bottchercomplexity, 2020. Online , Commit: a212f96. S. M. Marshall, C. Mathis, E. Carrick, G. Keenan, G. J. Cooper, H. Graham, M. Craven, P. S. Gromski, D. G. Moore, S. Walker, and L. Cronin, “Identifying molecules as biosignatures with assembly theory and mass spectrometry,” Nature Communications, volume 12(1), 2021. DOI: 10.1038/s41467-021-23258-x. S. M.Marshall, A. R. G. Murray, and L. Cronin, “A probabilistic framework for identifying biosignatures using Pathway Complexity,” Philosophical Transactions of the Royal Society A: Mathematical, Physical and Engineering Sciences, volume 375(2109): 20160342, 2017. DOI: 10.1098/rsta.2016.0342. Y. Liu, C. Mathis, M. D. Bajczyk, S. M. Marshall, L. Wilbraham, et al., “Exploring and mapping chemical space with molecular assembly trees,” Science Advances, volume 7(39), 2021. DOI: 10.1126/sciadv.abj2465. A.Grubisic, M. G. Trainer, X. Li, W. B. Brinckerhoff, F. H. van Amerom, et al., “Laser Desorption Mass Spectrometry at Saturn’s moon Titan,” International Journal of Mass Spectrometry, volume 470: 116707, 2021. DOI: 10.1016/j.ijms.2021.116707. Acknowledgements This work would not have been possible without the incredible support from our team's mentors: Atılım Günes Baydin, Kimberley Warren-Rhodes, Michael Phillips, G. Matthew Fricke, Nathalie Cabrol, and Scott Sandford; funding and support from Google Cloud, and our primary scientific stakeholder, the NASA Astrobiology Institute, represented by Mary Voytek who provided critical feedback on scientific milestones of the project.
アバター
はじめまして。Insight Edgeでリードプロジェクトマネージャーを務めている加藤です。簡単に自己紹介から始めさせて頂きますと、私は2022年にプロジェクトマネージャーとしてInsight Edgeに参画致しました。後述致しますが、参画後、現時点(執筆時点:2023年3月)でも、これまで経験出来なかった様な多種多様な業界/分野にて業務遂行出来ており、非常に刺激的な日々を送る事が出来ております。今回の記事ではInsight Edgeにおけるプロジェクトマネージャーの具体的な業務内容と、参画後、私なりに重要だと考えるスキルセットやマインドセットについて紹介させて頂きます。 対象となる業界/分野について プロジェクトマネージャーの業務内容について ①案件企画/構想段階での内容具体化に向けたマネジメントについて ②開発段階での開発マネジメントについて プロジェクトマネージャーに必要、且つ重要なスキル/マインドセットについて ①核となる普遍的な業務スキルを獲得/養う事 ②スタンスを取る事 ③当事者意識/主体性を持って業務遂行に臨む事 終わりに 対象となる業界/分野について 当社HP ( https://insightedge.jp/company/ )、及び冒頭でも触れた通り、当社は住友商事グループのデジタルトランスフォーメーション(以下、DXと記す)を加速する為の技術専門会社として設立された経緯から、非常に多岐に亘る業界/分野に活躍出来る機会があります。具体的に申し上げれば、金属、建設、インフラ、生活・不動産、農業、エネルギー等々が挙げられ、定量的な観点で言えば、全世界900社強と実際にコラボレーションし、DXの推進を強力、且つ主体的にリードしています。こうした状況下でのプロジェクトマネージャーの具体的な業務内容について、次セクションにて説明致します。 プロジェクトマネージャーの業務内容について プロジェクトマネージャーの具体的な業務内容について、上記の通り、業界/分野の切り口でも多彩で有り、置かれている状況/目指すべき方向性も千差万別である為に、私が対応した範囲でも数多くの業務があり、その中でも主要な業務を挙げると以下になります。 ① 案件企画/構想段階での内容具体化に向けたマネジメント ② 開発段階での開発マネジメント ③ 推進中案件におけるPMO、及び中立性、且つ専門性を発揮したアドバイザリ ④ ITDD、及び新規事業/技術採用における中長期目線、且つPM目線での評価 今回の記事ではこの中で①・②について詳細を記載致します。 ①案件企画/構想段階での内容具体化に向けたマネジメントについて 当社では住友商事グループのDX推進の加速を、というのは先程も触れた通りですが、実際に案件として相談されるステータスも様々異なります。こうした状況下から、実際に具体的な企画・構想に昇華すべく、現状把握は勿論の事、意思決定者等のキーマンや組織/関連ステークホルダーの整理、主要関係者を中心とした対象業務のヒアリング、ヒアリングを元にした課題設定や仮説となる有用な打ち手の提案を行います。提案内容についても内部の有識者や業界知見の高い専門家の支援も仰ぎつつ、プロジェクトマネジメント観点での実行可能性の精査や、具体的な計画立案、及び費用対効果を意識しながら有用な開発手法・陣容の整理を行います。また、単一の計画策定に留まらず、第二/第三案の立案等、不測の事態に備えつつ、最もフィージビリティが確保出来、実現速度もある程度担保可能な案にて関係者全体で合意形成を図る事を重視しながら行動します。こうした活動、及び関係者宛の説明や合意形成を経て、後述の実開発に進みます。 ②開発段階での開発マネジメントについて 実際の開発段階に進んだ案件については、基本的には事前合意済みの開発手法/陣容/計画に則って開発を推進する事になりますが、ここでは実証実験段階での開発と仮定して内容について説明致します。通常、製品として実際にサービス提供を行う場合は、機能要件、並びに非機能要件共に厳格に定義した上で開発推進すべきですが、実証実験段階ではそもそも利用者/シーンが限定的である事、また、開発対象の製品そのもののリッチさというよりかは、いち早く案件企画/構想段階で仮説定義した要件が業界/分野のゲームチェンジャーとなる有用な施策となり得るかどうかの検証を行いたいニーズも強い為、MVP開発としつつも、機能要件/非機能要件を何処まで精緻に今回開発スコープとするかについても十分に確認と協議が必要になる部分です。一方で開発手法の如何に関わらず、上記内容をコストコントロールも意識した上で、関係者に確認を行い、納期超過とならない様に開発計画に織り込み、品質についても有識者による成果物検証や第三者検証を交えながら推進を行う必要があるという意味では、実証実験段階と通常の製品開発マネジメントと異なる部分は無いです。 プロジェクトマネージャーに必要、且つ重要なスキル/マインドセットについて 最後に私がこれまで過ごした期間で感じた、プロジェクトマネージャーに求められるスキルセット/マインドセットについて説明致します。結論から申し上げると、以下の3点になります。 ①核となる普遍的な業務スキルを獲得/養う事 ②スタンスを取る事 ③当事者意識/主体性を持って業務遂行に臨む事 ここからは該当の各記載事項についての詳細を説明致します。 ①核となる普遍的な業務スキルを獲得/養う事 これまで記載した業務内容を分解すると、単純に列挙したとしても、 プロジェクトマネジメントスキル ITスキル 業界知見 問題解決能力 コミュニケーション能力 等々、幅広い業務スキルが必要となりますが、これらは一朝一夕で身に付けられる物ではなく、日々の業務遂行や泥臭い自己研鑽の繰り返しの積み上げで蓄積される物です。また、特にITスキルで言えば、日進月歩で技術革新が発生している昨今の状況を鑑みても、それらをキャッチアップするのは常にアンテナを張り続けるだけではなく、意欲的に学習を行う姿勢が重要となります。一方、それぞれで共通に言える事は、全て要諦と言える勘所、即ち原理原則に当たる部分が存在するという事です。例えば、プロジェクトマネジメントスキルで言えば、プロジェクトマネジメントにおける各知識領域下での要諦はそれぞれ一意(スケジュール管理で言えば、各タスク間の先行関係の可視化、及び適切な重要マイルストーンの設置も伴ったクリティカルチェーンの図示、不測事態発生時のリカバリ計画を考慮した計画策定等)に定まるものの、実際の管理手法といった方法論は多岐に亘る、といった具合です。最終的には全方位的なスキル獲得・習熟は必要になりますが、先ずは一つでも自分の中で武器となり、核となる業務スキルを身に付ける事、また、身に付ける内容については特定分野/ニッチな領域の知見といった細かな枝葉の部分ではなく、該当スキルの原理原則となり得る幹の領域を取得する事を推奨します。 ②スタンスを取る事 DX推進と一言で表現しても、その内容は一概には決まっていないものの、実際の案件内容は新規事業の創出や既存業務の抜本的な見直しといった、これまでの企業文化/活動を変革させる事が中心となるケースが多々有ります。それは言い換えれば、これまで誰も成し得た事がない、未知の領域に足を踏み込む事と同義になると共に、当然、明確な正解や道筋は存在しません。この「正解や道筋は存在しない」状況の中でも、その状況下で考え得る尤もらしい正解を仮説として、唱え続ける能力こそがスタンスを取るという事であり、非常に重要なポイントの一つです。正解が存在しない以上、何をどう主張したとしても時には反対意見や抵抗勢力と拮抗する場合も当然出てきます。しかし、見方を変えれば、この反対意見や抵抗勢力に対抗出来る対応案や対策を講じられれば、その仮説の確からしさはより確実な物になる上に、仮に当該見解の通りだったとしても、それはその見解を起点に新たにスタンスを取り直せば良い話とも言えます。大事なのは、スタンスを取り続ける事で目指すべき方向性を生み出し、軌道修正しながら、プロジェクトの正解を可視化して具現化していく事なのです。 ③当事者意識/主体性を持って業務遂行に臨む事 最後になりますが、これは純粋なマインドセットの話になるものの、ある意味では一番重要な要素と考えている事項になります。皆さんは自分自身の業務範囲/役割について、明確に回答可能でしょうか?若しくは、アサインされた案件や業務、タスクについて、何処までが自分自身の作業スコープかどうか正しく理解出来ていますでしょうか?改めて問いたい点は、回答可能でも理解出来ていた場合でも、何故自分自身の管轄外(と自身で定義した)の業務は未実施でも問題ないのか、という点です。勿論、これは、過剰な労働やワーカホリックとなる事を推奨する訳でもなければ、職責や役割の観点からあまりにも乖離した業務遂行を奨励する訳でも有りません(極端な例になりますが、入社早々で知見・経験が無い状態のまま特定会社への事業投資判断をする等)。伝えたいのは、自分自身で定義している管轄=壁は本当に壁なのかどうか、という事です。先程の「スタンスを取る事」で触れた通り、正解がない、未知の領域での業務遂行が基本となるDX推進において、方針転換が発生するケースは少なくはなく、突発的に発生するタスクや至急対応を求められる事案も散発します。こうした時に当事者意識/主体性を持ってリード出来る人間と、管轄を厳格に定義し、管轄外業務では遂行を断念する/リードを他者に依存する人間とでは、達成した結果が同じであったとしても、培われる経験値や知見の蓄積数、または得られる信頼感の多寡は異なるケースが多いと感じます。もし、可能であれば明日からでも、且つどんなに小さな事でも構いませんので、自分で定義した管轄を、壁を破ってみて、自分事として業務遂行の範囲を広げてみてはどうでしょうか?上記、自分自身の更なる成長という観点を除いたとしても、きっと素敵な結果が待っていると思います。 終わりに 如何でしたでしょうか?今回はInsight Edgeでのプロジェクトマネージャーの具体的な業務内容の紹介と私が大切にする具体的なスキル・マインドについて説明させて頂きました。Insight Edgeでは今回ご紹介したプロジェクトマネージャーの採用も意欲的に行なっており、今回の記事を通じて私達と是非とも働いてみたいと感じる方がいらっしゃればご応募の上、いつか一緒に私達の最大の目標である住友商事グループの更なるDXの加速を実現出来れば幸いです!
アバター
目次 導入 PyMCとは PyMCの最近の動向 コードリーディングの方針とスコープ メインコンテンツ Modelクラスとインスタンス化 with文 メタクラス 実装の確認 確率変数と分布クラスの管理 分布クラスの構造 ベータ分布クラス 分布クラス 観測された確率変数 サンプリング サンプリング手法選定 並列サンプリング NUTS実装 まとめ 導入 こんにちは。InsightEdgeのデータサイエンティストの小柳です。 本記事ではデータ分析の強い味方、MCMCサンプラーの実装を見てみようと思います。 今回取り上げるのは私が普段使っているPyMCというPython用のモジュールです。 実装を読む動機はいくつかあります。教科書で読むHMCの方法は美しいですが、自分で実装しろと言われるとどうすればよいかすぐにはわかりません。そのような不思議なものがどうやって実際に作られているのかはとても気になります。他にも、PyMCに限りませんがサンプリング前にエラーが起きたときに対処ができるようになりたいとか、わずか数行でサンプリングが動く便利さはどうやって実装されているのか解き明かしたいとか、 特殊な使い方をするときにはどこをどう触ればよいのか知りたいとか、ご利益はたくさんあります。また、PyMCはあまり日本語の文献が無いのでこの界隈を盛り上げたい、そんな動機もあります。 PyMCとは おそらくこの記事を読むような方には不要かと思いますが、軽くPyMCについての紹介をします。 PyMCとは簡単に言えばPython向けのベイズ推定用のサンプリングモジュールです。確率的プログラミング言語(PPL)とも呼ばれます。Python向けに限らず同等なものはいくつか存在しています。 Pyro, NumPyro, TensorFlowProbability, Stan(PyStan), WinBUGS, JAGSあたりでしょう。どれもMCMCサンプリングと変分ベイズ(変分推論、ADVI)、プラスアルファで確率的最適化ができるはずです。 PyMCの特徴は開発が盛んなこと、そして使用者が多いことです。以下はGoogle Trendsで調べた上記のPython向けPPLの人気です。 また、国別でいうと、日本ではPyStanがトップ、PyMCとNumPyroが同程度で2位という感じですが、その他のほぼ全ての国ではPyMCが主流といった趣です。 PyMCの最近の動向 2017年にver3系がリリースされたあたりからPyMCはPython向けのPPLの中でもメジャーなものであり続けています。ですが、近年(2022年あたり)の動向をキャッチアップしているものは少ない印象です。PyMCに関することをGoogle検索するとモジュールインポートの際に import pymc3 と書いている記事が多いのですが、これはそのver3系を使ったものです。最新のものをインポートする際にはpymcでOKです。 このver3のときまで、PyMCはTheanoというモジュールをバックエンドとして使っていましたが、Theanoの開発が2018年で終わってしまったようで[1]別のモジュールを使う方向に進んできました。当初はTensorFlowProbabilityを代替として開発を進めていましたが、その方針も結局2020に破棄[2]。ついにTheanoの後継としてAesaraをそしてそのさらに後ろでJAX等を使う方向になったようです。 紆余曲折ありましたが、ついに、2022年にver4.0がリリースされました[3]。その当時に書かれたイラストが以下です。 ですがver4.0はとても短命でした。開発目標の違いなどから、今はAesaraからフォークしたPyTensorを使う仕様になるに伴いver5系となり、現在も開発が進められています。 コードリーディングの方針とスコープ 今回はpymc v5.1.0を読んでいこうと思います。 また、読む範囲としてはPyMCが担当している範囲とします。従って、PyTensorやJAXが担当している範囲はスコープ外です。次回以降でやるかもしれませんが… 全てを理解しようとすると膨大になってしまうので、関係が薄い部分は大胆に削っていくことにします。 また、私のバックグラウンドはPythonを使ったデータ分析はできるけれどモジュール開発等まではできない人間です。なのでデータ分析では使わないようなPythonの使い方に重点を置いて読んでいくことにします。 方針としては、以下のような単純なモデルの挙動を追っていくことにします。 import pymc as pm trials = 10; successes = 5 with pm.Model() as coin_flip_model: p = pm.Beta("p", alpha=1, beta=1) obs = pm.Binomial("obs", p=p, n=trials, observed=successes, ) idata = pm.sample() 二項分布の事前分布にベータ分布をおいたモデルになります。非常に単純です。共役事前分布なんだからMCMCせず手計算で瞬殺だろと言いたくなるようなモデルですね。 変数名から推測するに10回投げたら5回表がでたようなコインの表がでる確率の事後分布を求めるような問題です。 PyMCのWebサイトのInteractive Demoから引用しています。 メインコンテンツ Modelクラスとインスタンス化 まずはmodel.py内のModelクラスの挙動から確認していきましょう。 with文 モデル作成時にwith文が使われています。with文の詳細はPythonの公式ドキュメント[5]に譲るのですが、端的に言えばModelクラスの__enter__()メソッドがwith文の冒頭で実行され、同__exit__()メソッドがwith文の最後に実行されます。従って、まずはModelクラスの__new__(), __init__(), __enter__(), __exit__()メソッドを確認していきます。が、その前に分かりづらいのがModelクラス定義時に指定されているmetaclassです。 メタクラス そもそも論になりますが、Pythonのクラス定義の際には class ExampleClass(ParentClass): A = 1 ... としますが、これはどのような処理がなされているのでしょうか? 実はこのとき行われているのは以下のコードと等価です。 X = type('ExampleClass', (ParentClass), {'A':1}) このようにしてtypeクラスのインスタンスを作成することになるのですが、メタクラスを指定するとtypeの代わりにメタクラスのインスタンスを作成することになります。当然、メタクラスの__new__(),__init__()が実行されることになります。 今回のModelクラスのメタクラスであるContextMetaクラスは__new__()でModelクラスの__enter__()と__exit__()を作成しています。以下が実際のContextMetaクラスの__new__(),__init__()です。 以後コードを貼るときは本質的に理解に不要なコメントやエラーハンドリング等は省略します。 model.py/ContextMeta class ContextMeta(type): """省略""" def __new__(cls, name, bases, dct, **kwargs): # pylint: disable=unused-argument """Add __enter__ and __exit__ methods to the class.""" def __enter__(self): self.__class__.context_class.get_contexts().append(self) # self._pytensor_config is set in Model.__new__ self._config_context = None if hasattr(self, "_pytensor_config"): self._config_context = pytensor.config.change_flags(**self._pytensor_config) self._config_context.__enter__() return self def __exit__(self, typ, value, traceback): # pylint: disable=unused-argument self.__class__.context_class.get_contexts().pop() # self._pytensor_config is set in Model.__new__ if self._config_context: self._config_context.__exit__(typ, value, traceback) dct[__enter__.__name__] = __enter__ dct[__exit__.__name__] = __exit__ def __init__( cls, name, bases, nmspc, context_class: Optional[Type] = None, **kwargs ): # pylint: disable=unused-argument """Add ``__enter__`` and ``__exit__`` methods to the new class automatically.""" if context_class is not None: cls._context_class = context_class super().__init__(name, bases, nmspc) そして、以下がModelクラスの__init__()までです。 model.py/Model class Model(WithMemoization, metaclass=ContextMeta): if TYPE_CHECKING: def __enter__(self: "Model") -> "Model": ... def __exit__(self: "Model", *exc: Any) -> bool: ... def __new__(cls, *args, **kwargs): # resolves the parent instance instance = super().__new__(cls) if kwargs.get("model") is not None: instance._parent = kwargs.get("model") else: instance._parent = cls.get_context(error_if_none=False) instance._pytensor_config = kwargs.get("pytensor_config", {}) return instance """省略""" def __init__( self, name="", coords=None, check_bounds=True, *, pytensor_config=None, model=None, ): del pytensor_config, model # used in __new__ self.name = self._validate_name(name) self.check_bounds = check_bounds if self.parent is not None: self.named_vars = treedict(parent=self.parent.named_vars) """省略""" else: self.named_vars = treedict() """省略""" self.add_coords(coords) from pymc.printing import str_for_model self.str_repr = types.MethodType(str_for_model, self) self._repr_latex_ = types.MethodType( functools.partial(str_for_model, formatting="latex"), self ) 実装の確認 これを踏まえてwith文のところまでの挙動を見てみましょう。 まずはModelクラスの定義時にContextMeta.__new__()が実行され、Model.__enter__()とModel.__exit__()が定義されます。次にContextMeta.__init__()が実行されます。 with文実行時にModelクラスのインスタンスが作成されます。この際にはあまり変なことは起きません。 その次に今作ったインスタンスの__enter__()メソッドが実行されます。ここでのポイントが同メソッドで行われる self.__class__.context_class.get_contexts().append(self) です。ここでModelクラスの「今対象にしている同クラスのリスト」の末尾に今作ったモデルインスタンスが追加されます。 こうなっているため後々with文内でModelインスタンスを意識することなくモデルを組み立てられるようになっています。 確率変数と分布クラスの管理 分布クラスの構造 次に p = pm.Beta("p", alpha=1, beta=1) を見ていきましょう。二項分布のパラメータの事前分布としてベータ分布を与えるところです。 プログラム的にはベータ分布クラスのインスタンスを作っているので、どのようなことが行われているかをみていきます。 ベータ分布クラス まずベータ分布に至るまでの継承とメタクラスの関係を見ていくと、 ベータ分布クラス→台が[0,1]の連続分布クラス→連続分布クラス→分布クラス→(メタクラス)分布メタクラス という関係になっています((子)→(親)の関係性)。 それらのうち、内容があるのはベータ分布クラス、分布クラス、分布メタクラスなので前2つを見ていきます。分布メタクラスの内容は省略します。 ベータ分布のコードが以下です。 distributions/continuous.py/Beta class Beta(UnitContinuous): """省略""" rv_op = pytensor.tensor.random.beta @classmethod def dist(cls, alpha=None, beta=None, mu=None, sigma=None, nu=None, *args, **kwargs): alpha, beta = cls.get_alpha_beta(alpha, beta, mu, sigma, nu) alpha = at.as_tensor_variable(floatX(alpha)) beta = at.as_tensor_variable(floatX(beta)) return super().dist([alpha, beta], **kwargs) def moment(rv, size, alpha, beta): """省略""" return mean @classmethod def get_alpha_beta(self, alpha=None, beta=None, mu=None, sigma=None, nu=None): """省略""" return alpha, beta def logp(value, alpha, beta): """省略""" return check_parameters( res, alpha > 0, beta > 0, msg="alpha > 0, beta > 0", ) def logcdf(value, alpha, beta): """省略""" return check_parameters( logcdf, alpha > 0, beta > 0, msg="alpha > 0, beta > 0", ) 平均、パラメータ、対数密度、累積分布関数の対数値を得るためのメソッドが定義されています。__new__()は後で扱う分布クラスのものを使います。 dist()メソッドも後で扱いますが、これは__new__()実行時に呼び出されます。ベータ分布に従うPyTensorの確率変数クラスにパラメータを与えてインスタンス(rv_out)を得るためのメソッドです。ということで次に分布クラスを見ていきます。 分布クラス 分布クラスには個別の分布のインスタンスを生成するときの__new__()メソッドと、個別の分布クラスのdist()メソッドを内部で呼び出すdist()メソッドが実装されています。__new__()メソッドがdist()メソッドを呼び出すことで確率変数インスタンスrv_outを作り、その後Model.register_rv()メソッドで先程作ったModelインスタンスに登録します。このとき確率変数に付けた名前や対数尤度を計算する時の変形等も登録します。 また、作った確率変数が観測値を持つか否かで処理を変えます。今回は直接観測される値では無いので先程のModelインスタンスのfree_RVsに登録されます。 分布クラスを見て実際にそうなっていることを確認しましょう。 distributions/distributions.py/Distribution class Distribution(metaclass=DistributionMeta): rv_op: [RandomVariable, SymbolicRandomVariable] = None rv_type: MetaType = None def __new__( cls, name: str, *args, rng=None, dims: Optional[Dims] = None, initval=None, observed=None, total_size=None, transform=UNSET, **kwargs, ) -> TensorVariable: try: from pymc.model import Model model = Model.get_context() except TypeError: """省略""" """省略""" rv_out = cls.dist(*args, **kwargs) rv_out = model.register_rv( rv_out, name, observed, total_size, dims=dims, transform=transform, initval=initval, ) # add in pretty-printing support rv_out.str_repr = types.MethodType(str_for_dist, rv_out) rv_out._repr_latex_ = types.MethodType( functools.partial(str_for_dist, formatting="latex"), rv_out ) rv_out.logp = _make_nice_attr_error("rv.logp(x)", "pm.logp(rv, x)") rv_out.logcdf = _make_nice_attr_error("rv.logcdf(x)", "pm.logcdf(rv, x)") rv_out.random = _make_nice_attr_error("rv.random()", "pm.draw(rv)") return rv_out @classmethod def dist( cls, dist_params, *, shape: Optional[Shape] = None, **kwargs, ) -> TensorVariable: """省略""" rv_out = cls.rv_op(*dist_params, size=create_size, **kwargs) rv_out.logp = _make_nice_attr_error("rv.logp(x)", "pm.logp(rv, x)") rv_out.logcdf = _make_nice_attr_error("rv.logcdf(x)", "pm.logcdf(rv, x)") rv_out.random = _make_nice_attr_error("rv.random()", "pm.draw(rv)") _add_future_warning_tag(rv_out) return rv_out 確かに、PyTensorの確率分布クラスにパラメータを与えてインスタンスを作りそれをモデルに登録する、というプロセスはこのような方式であればどの確率変数でも同じなのでこのような構成になるのは納得できます。 では次にモデルクラスのregister_rv()メソッドをみてみましょう、といってもモデルにいろいろ登録するだけですが。 model.py/Model class Model(WithMemoization, metaclass=ContextMeta): """省略""" def register_rv( self, rv_var, name, observed=None, total_size=None, dims=None, transform=UNSET, initval=None ): name = self.name_for(name) rv_var.name = name _add_future_warning_tag(rv_var) """省略""" if observed is None: if total_size is not None: raise ValueError("total_size can only be passed to observed RVs") self.free_RVs.append(rv_var) self.create_value_var(rv_var, transform) self.add_named_variable(rv_var, dims) self.set_initval(rv_var, initval) else: """省略""" rv_var = self.make_obs_var(rv_var, observed, dims, transform, total_size) return rv_var 観測された確率変数 次に追うコードは以下です。 obs = pm.Binomial("obs", p=p, n=trials, observed=successes, ) 先程事前分布にベータ分布をおいたパラメータを使った二項分布です。先程と同様に二項分布に至るまでの継承とメタクラスを見ていくと、 二項分布クラス→離散分布クラス→分布クラス→(メタクラス)分布メタクラス となっていて、構成はもちろんほぼ同じですし実行されることも概ね同じです。異なるところはインスタンス生成時のModel.register_rv()メソッド内の挙動です。引数observedに観測データを渡したことで変化し、Model.make_obs_var()メソッドが実行されます。さらにその中でModel.create_value_var()が実行され、モデルに登録されます。 Model.make_obs_var()メソッドを見てみましょう。 model.py/Model class Model(WithMemoization, metaclass=ContextMeta): """省略""" def make_obs_var( self, rv_var: TensorVariable, data: np.ndarray, dims, transform: Union[Any, None], total_size: Union[int, None], ) -> TensorVariable: name = rv_var.name data = convert_observed_data(data).astype(rv_var.dtype) if data.ndim != rv_var.ndim: raise ShapeError( "Dimensionality of data and RV don't match.", actual=data.ndim, expected=rv_var.ndim ) if pytensor.config.compute_test_value != "off": """省略""" mask = getattr(data, "mask", None) if mask is not None: """省略""" else: if sps.issparse(data): data = sparse.basic.as_sparse(data, name=name) else: data = at.as_tensor_variable(data, name=name) if total_size: from pymc.variational.minibatch_rv import create_minibatch_rv rv_var = create_minibatch_rv(rv_var, total_size) rv_var.name = name rv_var.tag.observations = data self.create_value_var(rv_var, transform=None, value_var=data) self.add_named_variable(rv_var, dims) self.observed_RVs.append(rv_var) return rv_var make_obs_var()の最後の処理は、observedがNoneのときのModel.register_rv()の最後の処理とほとんど同じです。極論すると異なる部分はrv_var.tag.observations = dataを与えているかどうかのようです。 サンプリング 最後の一行、サンプリングの部分です。 idata = pm.sample() サンプリング概観 最近のデータ分析環境であればまず間違いなくマルチコアCPUが使えるので、自動的に並列サンプリングが実行されます。 今回のコードを実行するとpymcで実装されたNUTSサンプリングが並列で走るので、そのケースがたどる部分を見ていこうと思います。 まずはsample()関数を見てみましょう。 sampling/mcmc.py/sample def sample( draws: int = 1000, *, tune: int = 1000, chains: Optional[int] = None, cores: Optional[int] = None, random_seed: RandomState = None, progressbar: bool = True, step=None, nuts_sampler: str = "pymc", initvals: Optional[Union[StartDict, Sequence[Optional[StartDict]]]] = None, init: str = "auto", jitter_max_retries: int = 10, n_init: int = 200_000, trace: Optional[TraceOrBackend] = None, discard_tuned_samples: bool = True, compute_convergence_checks: bool = True, keep_warning_stat: bool = False, return_inferencedata: bool = True, idata_kwargs: Optional[Dict[str, Any]] = None, callback=None, mp_ctx=None, model: Optional[Model] = None, **kwargs, ) -> Union[InferenceData, MultiTrace]: """省略""" model = modelcontext(model) if not model.free_RVs: """省略""" if cores is None: cores = min(4, _cpu_count()) if chains is None: chains = max(2, cores) """省略""" step = assign_step_methods(model, step, methods=pm.STEP_METHODS, step_kwargs=kwargs) if nuts_sampler != "pymc": """省略""" # Create trace backends for each chain run, traces = init_traces( backend=trace, chains=chains, expected_length=draws + tune, step=step, initial_point=ip, model=model, ) sample_args = { "draws": draws + tune, # FIXME: Why is tune added to draws? "step": step, "start": initial_points, "traces": traces, "chains": chains, "tune": tune, "progressbar": progressbar, "model": model, "cores": cores, "callback": callback, "discard_tuned_samples": discard_tuned_samples, } parallel_args = { "mp_ctx": mp_ctx, } sample_args.update(kwargs) """省略""" if parallel: _log.info(f"Multiprocess sampling ({chains} chains in {cores} jobs)") _print_step_hierarchy(step) try: _mp_sample(**sample_args, **parallel_args) except pickle.PickleError: """省略""" if not parallel: """省略""" return _sample_return( run=run, traces=traces, tune=tune, t_sampling=t_sampling, discard_tuned_samples=discard_tuned_samples, compute_convergence_checks=compute_convergence_checks, return_inferencedata=return_inferencedata, keep_warning_stat=keep_warning_stat, idata_kwargs=idata_kwargs or {}, model=model, ) コードからわかるように、ここではassign_step_methods()メソッドを使ってstepすなわちサンプリング手法を決定し、そのあと_mp_sample()に引数を入れて実行しています。実はassign_step_methods()はサンプリング手法を決定するだけの関数ではなく、各サンプリング手法クラスのインスタンスを作って返します。 ということで次はassign_step_methods()の中身を見ていきましょう。 サンプリング手法選定 この関数では、パラメータごとに各サンプリング手法(現在候補はNUTS, HMC, Metropolis,BinaryMetropolis, BinaryGibbsMetropolis, Slice, CategoricalGibbsMetropolis)から最適なものを選びます。各サンプリング手法はクラスとして存在しており、どの手法も.competence()という相性を測るメソッドが実装されています。このメソッドは0-3の点数を返し、大きい方がより良いとされています。NUTSを例に取ると、変数が対数尤度に対して勾配を持つ場合2点を返すようになっています。現状3点を返す手法はかなり限られているので、通常連続変数で勾配があればNUTSが選ばれるようです。 余談ですが3点を返すケースはとても少ないようです。例えば、ベルヌーイ分布に従うような確率変数に対しては、BinaryGibbsMetropolisサンプリングが理想的と判定されます。この辺なにか研究があるのでしょうか?気になるところではあります。 最後にinstantiate_steppersを実行してその返り値を返します。関数名の通り各サンプリング手法クラスのインスタンスを作り、それらを二個目の引数のリスト(steps)に足したものを返します。 sampling/mcmc.py/assign_step_methods def assign_step_methods(model, step=None, methods=None, step_kwargs=None): steps = [] assigned_vars = set() if methods is None: methods = pm.STEP_METHODS if step is not None: """省略""" # Use competence classmethods to select step methods for remaining # variables selected_steps = defaultdict(list) model_logp = model.logp() for var in model.value_vars: if var not in assigned_vars: # determine if a gradient can be computed has_gradient = var.dtype not in discrete_types if has_gradient: try: tg.grad(model_logp, var) except (NotImplementedError, tg.NullTypeGradError): has_gradient = False # select the best method rv_var = model.values_to_rvs[var] selected = max( methods, key=lambda method, var=rv_var, has_gradient=has_gradient: method._competence( var, has_gradient ), ) selected_steps[selected].append(var) return instantiate_steppers(model, steps, selected_steps, step_kwargs) 並列サンプリング 次にサンプリングの本丸、_mp_sample()メソッドをみていきます。 やっていることはParallelSamplerインスタンスを作ったあとにそこからイテレーション結果を引き出してtraceに格納しているだけです。 sampling/mcmc.py/_mp_sample def _mp_sample( *, draws: int, tune: int, step, chains: int, cores: int, random_seed: Sequence[RandomSeed], start: Sequence[PointType], progressbar: bool = True, traces: Sequence[IBaseTrace], model: Optional[Model] = None, callback: Optional[SamplingIteratorCallback] = None, mp_ctx=None, **kwargs, ) -> None: """省略""" import pymc.sampling.parallel as ps # We did draws += tune in pm.sample draws -= tune sampler = ps.ParallelSampler( draws=draws, tune=tune, chains=chains, cores=cores, seeds=random_seed, start_points=start, step_method=step, progressbar=progressbar, mp_ctx=mp_ctx, ) try: try: with sampler: for draw in sampler: strace = traces[draw.chain] strace.record(draw.point, draw.stats) log_warning_stats(draw.stats) if draw.is_last: strace.close() if callback is not None: callback(trace=strace, draw=draw) except ps.ParallelSamplingError as error: """省略""" except KeyboardInterrupt: pass finally: for strace in traces: strace.close() ここ自体はそんなに難しくありませんね。 では次にParallelSampelerクラスのインスタンス作成からイテレーション実行までを見ていきます。 初期化メソッドでまず並列処理の開始方式を決めています。mp_ctx関連の部分です。OSが変わってもきちんと動くようにうまくやっているという認識で大丈夫です。 次にチェーン数だけProcessAdapterインスタンスとそれに伴う子プロセスを作りリスト化した後、それら全てをself._inactiveというまだ動いていないプロセスに登録します。 イテレーション実行時には、ParallelSamplerの__iter__()メソッドが呼び出されます。__iter__()メソッドが呼び出されたときの一般的な挙動を詳細に述べると長くなるのでざっくりした説明が次のようになります。 for i in hoge: を実行するとhoge.__iter__()が実行され、最初のyieldのところまで実行して止まり、iにyeild以後を代入して返します。そしてループが回ってiの値を更新する際には、先程のyieldのところから再開し、次のyieldのところまで実行しiにそれを代入して返す、という挙動になります。 今回のコードの場合だと、最初にParallelSamplerの._make_active()メソッドを実行して各プロセスに対しproc.start()メソッドとproc.write_next()メソッドを実行します。proc.start()メソッドで'start'メッセージを受け取った子プロセス達は各々サンプリングを始め、'write_next'メッセージ=パイプに結果を書き込んで良いよ、のメッセージを受取るまで結果を保持して待ちます。親プロセス側でProcessAdapter.recv_draw()が実行されると、最も早く結果が用意できたプロセスのパイプからサンプリング結果を取得します。その後proc.write_next()メソッドを呼ぶことで、結果を返した子プロセスに対し次のステップのサンプリング結果を計算して良いぞとパイプ越しに命令することになります。 sampling/parallel.py/ParallelSampler class ParallelSampler: def __init__( self, *, draws: int, tune: int, chains: int, cores: int, seeds: Sequence["RandomSeed"], start_points: Sequence[Dict[str, np.ndarray]], step_method, progressbar: bool = True, mp_ctx=None, ): """ 省略""" if mp_ctx is None or isinstance(mp_ctx, str): """ 省略""" mp_ctx = multiprocessing.get_context(mp_ctx) step_method_pickled = None if mp_ctx.get_start_method() != "fork": step_method_pickled = cloudpickle.dumps(step_method, protocol=-1) self._samplers = [ ProcessAdapter( draws, tune, step_method, step_method_pickled, chain, seed, start, mp_ctx, ) for chain, seed, start in zip(range(chains), seeds, start_points) ] self._inactive = self._samplers.copy() self._finished: List[ProcessAdapter] = [] self._active: List[ProcessAdapter] = [] self._max_active = cores """ 省略""" def _make_active(self): while self._inactive and len(self._active) < self._max_active: proc = self._inactive.pop(0) proc.start() proc.write_next() self._active.append(proc) def __iter__(self): if not self._in_context: raise ValueError("Use ParallelSampler as context manager.") self._make_active() if self._active and self._progress: self._progress.update(self._total_draws) while self._active: draw = ProcessAdapter.recv_draw(self._active) proc, is_last, draw, tuning, stats = draw self._total_draws += 1 """ 省略""" # Already called for new proc in _make_active if not is_last: proc.write_next() yield Draw(proc.chain, is_last, draw, tuning, stats, point) """ 省略""" 実際に親子間でやりとりするコードを見てみましょう。主にサンプリングのところだけを抜き出しています。終了処理も省略しています。 親プロセス側で、子プロセスと直接やりとりをするのが以下のProcessAdapterクラスです。 sampling/paralell.py/ProcessAdapter class ProcessAdapter: """Control a Chain process from the main thread.""" def __init__( self, draws: int, tune: int, step_method, step_method_pickled, chain: int, seed, start: Dict[str, np.ndarray], mp_ctx, ): self.chain = chain process_name = "worker_chain_%s" % chain self._msg_pipe, remote_conn = multiprocessing.Pipe() self._shared_point = {} self._point = {} for name, shape, dtype in DictToArrayBijection.map(start).point_map_info: """ 省略""" self._readable = True self._num_samples = 0 if step_method_pickled is not None: step_method_send = step_method_pickled else: if mp_ctx.get_start_method() == "spawn": raise ValueError( "please provide a pre-pickled step method when multiprocessing start method is 'spawn'" ) step_method_send = step_method self._process = mp_ctx.Process( daemon=True, name=process_name, target=_run_process, args=( process_name, remote_conn, step_method_send, step_method_pickled is not None, self._shared_point, draws, tune, seed, ), ) self._process.start() # Close the remote pipe, so that we get notified if the other # end is closed. remote_conn.close() """省略""" def _send(self, msg, *args): try: self._msg_pipe.send((msg, *args)) except Exception: # try to receive an error message message = None try: message = self._msg_pipe.recv() except Exception: pass if message is not None and message[0] == "error": old_error = message[1] if old_error is not None: error = ParallelSamplingError( f"Chain {self.chain} failed with: {old_error}", self.chain ) else: error = RuntimeError(f"Chain {self.chain} failed.") raise error from old_error raise def start(self): self._send("start") def write_next(self): self._readable = False self._send("write_next") """ 省略""" @staticmethod def recv_draw(processes, timeout=3600): if not processes: raise ValueError("No processes.") pipes = [proc._msg_pipe for proc in processes] ready = multiprocessing.connection.wait(pipes) if not ready: raise multiprocessing.TimeoutError("No message from samplers.") idxs = {id(proc._msg_pipe): proc for proc in processes} proc = idxs[id(ready[0])] msg = ready[0].recv() if msg[0] == "error": """ 省略""" elif msg[0] == "writing_done": proc._readable = True proc._num_samples += 1 return (proc,) + msg[1:] else: raise ValueError("Sampler sent bad message.") """ 省略""" def _run_process(*args): _Process(*args).run() 一方で、子プロセスとして動かすのが以下の Processクラスです。 self. recv_msg()でパイプからメッセージを受け取る際には何か受け取れるまで待ち続けます。 これらを見ると大まかな挙動がわかるのではないかと思います。 sampling/parallel.py/_Process class _Process: def __init__( self, name: str, msg_pipe, step_method, step_method_is_pickled, shared_point, draws: int, tune: int, seed, ): self._msg_pipe = msg_pipe self._step_method = step_method self._step_method_is_pickled = step_method_is_pickled self._shared_point = shared_point self._seed = seed self._at_seed = seed + 1 self._draws = draws self._tune = tune def _unpickle_step_method(self): """ 省略""" def run(self): try: self._unpickle_step_method() self._point = self._make_numpy_refs() self._start_loop() except KeyboardInterrupt: pass except BaseException as e: e = ExceptionWithTraceback(e, e.__traceback__) self._msg_pipe.send(("error", e)) self._wait_for_abortion() finally: self._msg_pipe.close() """ 省略""" def _recv_msg(self): return self._msg_pipe.recv() def _start_loop(self): np.random.seed(self._seed) draw = 0 tuning = True msg = self._recv_msg() if msg[0] == "abort": raise KeyboardInterrupt() if msg[0] != "start": raise ValueError("Unexpected msg " + msg[0]) while True: if draw == self._tune: self._step_method.stop_tuning() tuning = False if draw < self._draws + self._tune: try: point, stats = self._step_method.step(self._point) except SamplingError as e: e = ExceptionWithTraceback(e, e.__traceback__) self._msg_pipe.send(("error", e)) else: return msg = self._recv_msg() if msg[0] == "abort": raise KeyboardInterrupt() elif msg[0] == "write_next": self._write_point(point) is_last = draw + 1 == self._draws + self._tune self._msg_pipe.send(("writing_done", is_last, draw, tuning, stats)) draw += 1 else: raise ValueError("Unknown message " + msg[0]) NUTS実装 これまでで概ね実装は追い終わり、PyMCが何を担当しているかがわかってきました。MCMCサンプリングを通じて、PyMCは確率変数とそれらの関係性をモデルとして管理することと、並列サンプリングをしていました。逆に、確率分布そのもの実装や対数尤度の計算はPyTensorにまかせていることもわかってきました。 若干オプショナルになりますが、PyMCによるNUTSの実装も読んでみることにします。オプショナルなのは単純な理由で、PyMCで実装されているNUTSはJAXを使ったNUTSよりも遅いため使う必要性があまり無いからです。実際、公式も最新バージョン(5.1.2)ではsample()関数の引数からNUTSサンプラーにnutpieを選ぶのが最速とアナウンスしています。ですが、今回のコードだとサンプラーを指定していないのでPyMCのものが動くのと、そしてNUTSはどう実装されているのかを確認したいという個人的欲求から見ていくことにします。NUTSやHMCサンプリングについては[6]で確認してください。 これから見ていくところは、 Processクラスの start_loop()メソッド内にある point, stats = self. step_method.step(self. point) を行ったときに呼び出される各サンプリングクラスの.step()とそれに関連する部分です。 まずはNUTSクラスの継承関係を見ていくと、 NUTS→BaseHMC→GradientSharedStep→ArrayStepShared→BlockedStep という関係になっています。 NUTS.step()を実行したときに実際に実行されるのはArrayStepSharedから継承した.step()メソッドですが、さらに詳細に見るとBaseHMC.astep()メソッドが呼ばれています。 その中では初速の生成、対数尤度からのエネルギーの計算、1ステップ分のサンプリング、(ステップサイズを自動更新するなら)ステップサイズの更新、発散状況の警告作成等を行います。統計に近いのでやっていることがわかりやすいです。 コードは以下のようになっています。 step_methods/hmc/base_hmc.py/BaseHMC class BaseHMC(GradientSharedStep): """省略""" def astep(self, q0: RaveledVars) -> tuple[RaveledVars, StatsType]: """Perform a single HMC iteration.""" perf_start = time.perf_counter() process_start = time.process_time() p0 = self.potential.random() p0 = RaveledVars(p0, q0.point_map_info) start = self.integrator.compute_state(q0, p0) warning: SamplerWarning | None = None if not np.isfinite(start.energy): """省略""" raise SamplingError(f"Bad initial energy: {warning}") adapt_step = self.tune and self.adapt_step_size step_size = self.step_adapt.current(adapt_step) self.step_size = step_size if self._step_rand is not None: step_size = self._step_rand(step_size) hmc_step = self._hamiltonian_step(start, p0.data, step_size) perf_end = time.perf_counter() process_end = time.process_time() self.step_adapt.update(hmc_step.accept_stat, adapt_step) self.potential.update(hmc_step.end.q, hmc_step.end.q_grad, self.tune) if hmc_step.divergence_info: info = hmc_step.divergence_info point = None point_dest = None info_store = None if self.tune: kind = WarningType.TUNING_DIVERGENCE else: kind = WarningType.DIVERGENCE self._num_divs_sample += 1 # We don't want to fill up all memory with divergence info if self._num_divs_sample < 100 and info.state is not None: point = DictToArrayBijection.rmap(info.state.q) if self._num_divs_sample < 100 and info.state_div is not None: point_dest = DictToArrayBijection.rmap(info.state_div.q) if self._num_divs_sample < 100: info_store = info warning = SamplerWarning( kind, info.message, "debug", self.iter_count, info.exec_info, divergence_point_source=point, divergence_point_dest=point_dest, divergence_info=info_store, ) self.iter_count += 1 stats: dict[str, Any] = { "tune": self.tune, "diverging": bool(hmc_step.divergence_info), "perf_counter_diff": perf_end - perf_start, "process_time_diff": process_end - process_start, "perf_counter_start": perf_start, "warning": warning, } stats.update(hmc_step.stats) stats.update(self.step_adapt.stats()) stats.update(self.potential.stats()) return hmc_step.end.q, [stats] 1ステップ分のサンプリングだけはNUTS特有ですが、ほかの部分はHMC系?のサンプリングで共通なのでこのようにbaseHMCクラスのような実装になっています。 NUTSのサンプリングと言えば、前後のどちらかに進むかをランダムに決めて1ステップそちらに時間積分で進み、また進む方向を決めて2ステップ進み、次は4ステップ…を繰り返しそれを木としてくっつけていく手法です。1サイクルごとに積分した軌跡がパラメータ空間中でUターンしていないことを確認していくことが名前の由来です。こちらの実装も見てみましょう。論文中の擬似コードをそのまま書き写したといった感じでこちらもわかりやすくなっています。 step_methods/hmc/nuts.py NUTS class NUTS(BaseHMC): """省略""" name = "nuts" default_blocked = True stats_dtypes_shapes = { "depth": (np.int64, []), """省略""" } def __init__(self, vars=None, max_treedepth=10, early_max_treedepth=8, **kwargs): """省略""" super().__init__(vars, **kwargs) self.max_treedepth = max_treedepth self.early_max_treedepth = early_max_treedepth self._reached_max_treedepth = 0 def _hamiltonian_step(self, start, p0, step_size): if self.tune and self.iter_count < 200: max_treedepth = self.early_max_treedepth else: max_treedepth = self.max_treedepth tree = _Tree(len(p0), self.integrator, start, step_size, self.Emax) reached_max_treedepth = False for _ in range(max_treedepth): direction = logbern(np.log(0.5)) * 2 - 1 divergence_info, turning = tree.extend(direction) if divergence_info or turning: break else: reached_max_treedepth = not self.tune stats = tree.stats() accept_stat = stats["mean_tree_accept"] stats["reached_max_treedepth"] = reached_max_treedepth return HMCStepData(tree.proposal, accept_stat, divergence_info, stats) そして以下がサンプリング経路を保持する Treeクラスです。 build_subtree()メソッドは一回実行されるごとに二回再帰的に実行されるため、本体のツリーに追加されるサブツリーがステップごとに倍々になっていきます。 step_methods/hmc/nuts.py _Tree class _Tree: def __init__( self, ndim: int, integrator: integration.CpuLeapfrogIntegrator, start: State, step_size: float, Emax: float, ): """省略""" def extend(self, direction): """省略""" if direction > 0: tree, diverging, turning = self._build_subtree( self.right, self.depth, floatX(np.asarray(self.step_size)) ) leftmost_begin, leftmost_end = self.left, self.right rightmost_begin, rightmost_end = tree.left, tree.right leftmost_p_sum = self.p_sum.copy() rightmost_p_sum = tree.p_sum self.right = tree.right else: """省略""" self.depth += 1 if diverging or turning: return diverging, turning size1, size2 = self.log_size, tree.log_size if logbern(size2 - size1): self.proposal = tree.proposal self.log_size = np.logaddexp(self.log_size, tree.log_size) self.p_sum[:] += tree.p_sum # Additional turning check only when tree depth > 0 to avoid redundant work if self.depth > 0: left, right = self.left, self.right p_sum = self.p_sum turning = (p_sum.dot(left.v) <= 0) or (p_sum.dot(right.v) <= 0) p_sum1 = leftmost_p_sum + rightmost_begin.p.data turning1 = (p_sum1.dot(leftmost_begin.v) <= 0) or (p_sum1.dot(rightmost_begin.v) <= 0) p_sum2 = leftmost_end.p.data + rightmost_p_sum turning2 = (p_sum2.dot(leftmost_end.v) <= 0) or (p_sum2.dot(rightmost_end.v) <= 0) turning = turning | turning1 | turning2 return diverging, turning def _single_step(self, left: State, epsilon: float): """Perform a leapfrog step and handle error cases.""" """省略""" return tree, divergence_info, False def _build_subtree(self, left, depth, epsilon): if depth == 0: return self._single_step(left, epsilon) tree1, diverging, turning = self._build_subtree(left, depth - 1, epsilon) if diverging or turning: return tree1, diverging, turning tree2, diverging, turning = self._build_subtree(tree1.right, depth - 1, epsilon) left, right = tree1.left, tree2.right if not (diverging or turning): p_sum = tree1.p_sum + tree2.p_sum turning = (p_sum.dot(left.v) <= 0) or (p_sum.dot(right.v) <= 0) # Additional U turn check only when depth > 1 to avoid redundant work. if depth - 1 > 0: p_sum1 = tree1.p_sum + tree2.left.p.data turning1 = (p_sum1.dot(tree1.left.v) <= 0) or (p_sum1.dot(tree2.left.v) <= 0) p_sum2 = tree1.right.p.data + tree2.p_sum turning2 = (p_sum2.dot(tree1.right.v) <= 0) or (p_sum2.dot(tree2.right.v) <= 0) turning = turning | turning1 | turning2 log_size = np.logaddexp(tree1.log_size, tree2.log_size) if logbern(tree2.log_size - log_size): proposal = tree2.proposal else: proposal = tree1.proposal else: p_sum = tree1.p_sum log_size = tree1.log_size proposal = tree1.proposal tree = Subtree(left, right, p_sum, proposal, log_size) return tree, diverging, turning def stats(self): self.mean_tree_accept = np.exp(self.log_accept_sum) / self.n_proposals return { """省略""" } まとめ モデル作成からサンプリングまでの挙動を見ることで、PyMCが実際に何をしているのかを明らかにしました。 PyMCが担当しているパートはおおまかに言うと PyTensorの確率変数をインスタンス化 モデルとして確率変数の関係を保持 サンプリングの並列実行 サンプリング手法の実装 でした。理解が深まる一助になれば幸いです。 [1]: https://pymc-devs.medium.com/theano-tensorflow-and-the-future-of-pymc-6c9987bb19d5 [2]: https://pymc-devs.medium.com/the-future-of-pymc3-or-theano-is-dead-long-live-theano-d8005f8a0e9b [3]: https://www.pymc.io/about/history.html [4]: https://www.pymc.io/blog/pytensor_announcement.html#pytensor_announcement [5]: https://docs.python.org/ja/3/reference/compound_stmts.html#with [6]: https://arxiv.org/pdf/1111.4246.pdf
アバター
こんにちは!Insight Edgeでコンサルタントとして働いている山田です。 この記事では、AI開発プロジェクトにおいてよく議論になる「知的財産権の帰属」について、 クライアントとの契約時に注意すべきポイントをまとめました。 本記事は経済産業省が策定している「AI・データの利用に関する契約ガイドライン」を参考に、 特にAIモデルの知的財産権に焦点を当てて解説しています。 もしご興味があれば、ガイドラインも併せてご覧頂けると、より理解が深まるかと思います。(全362ページの大作!) 「AI・データの利用に関する契約ガイドライン 1.1版」を策定しました (METI/経済産業省) 1.知的財産権とは? 知的財産権の種類 2. AI開発において発生する知的財産権 何に対して知的財産権が発生するのか 知的財産権の発生と帰属のデフォルトルール (参考)知的財産の発生有無と権利帰属のデフォルトルール 3. 知的財産権に関する契約の勘所 AI開発の知財帰属で、ユーザーとベンダーは対立しがち 知的財産権の利用条件の定め方 プログラムの部分はベンダー側に帰属させる方が良い 4. まとめ 参考記事・資料 1.知的財産権とは? 知的財産権の種類 知的財産権とは、様々な知的創造活動によって生み出されたものを、創作者の財産として保護するために存在する権利です。 知的財産権と一口にいっても、創作意欲の促進を目的とした「知的創造物についての権利」と、使用者の信用維持を目的とした「営業上の標識についての権利」の2つに大別されます。 特許庁「知的財産権について」より抜粋、一部改変 2. AI開発において発生する知的財産権 知的財産権には上記の通りいくつも種類がありますが、AI開発において重要となるのは以下3種類です。 特許権 技術的に新規性のある発明に対する権利。AI開発ではアルゴリズムや機械学習モデル、アーキテクチャ等に適用されます。 著作権 著作物や創作物に対する権利で、AI開発ではソースコードやアルゴリズム、データセット、データベース等が該当します。 営業秘密 企業独自の情報を企業秘密として保持する権利です。研究開発成果やノウハウ等が当てはまります。 何に対して知的財産権が発生するのか そもそも、どのようなデータや成果物が知的財産権の対象となるのでしょうか。 AI開発のフローに沿って考えると、検討する必要のある構成要素として以下の6つが考えられます。 ①生データ 加工や編集がされていない、元々の状態のデータ ②学習用データセット 前処理やクレンジングを行った、AIモデルが学習できる形式に整理されたデータの集合 ③学習用プログラム AIモデルが学習するために使用されるソフトウェアやアルゴリズム ④学習済みモデル 学習用データセットを用いて訓練されたAIモデルで、最適化されたパラメータが組み込まれた推論プログラム ⑤学習済みパラメータ AIモデルが学習用データセットを用いて訓練される過程で最適化された重みなどの値 ⑥ノウハウ AI開発プロセス全体における独自の技術や知識 知的財産権の発生の観点では、これらを 「データ」 、 「プログラム」 、 「ノウハウ」 の3つに大別することができます。 経産省「AI・データの利用に関する契約ガイドライン」より作成 知的財産権の発生と帰属のデフォルトルール 上記6つのどの要素に知的財産権が発生するか、発生する場合誰に帰属するかは、あらかじめ法的に定められています。 「営業秘密」は特定の要件(※1)を満たす場合はいずれも共通して発生するため、以下では各要素が「特許権」「著作権」に該当するかどうかを、それぞれ見ていきたいと思います。 (※1)秘密管理性、有用性、非公知性の三要件 最初に結論を以下の表にまとめました。 それぞれの根拠は後述しますが、少し細かくなるのでざっくり以下のようなイメージです。 データ 特許権も著作権も発生しないことが多い。利用権については契約で定める必要あり。 プログラム 基本的に特許権および著作権は発生し、作成者であるベンダーが保有することが一般的。利用権については契約で定めることでユーザーも柔軟な利活用が可能。 ノウハウ 特許権も著作権も発生しないことが多い。利用権については契約で定める必要あり。 経産省「AI・データの利用に関する契約ガイドライン」より作成 (参考)知的財産の発生有無と権利帰属のデフォルトルール ①生データ ユーザー側が提供する生データは、単なる事実や情報の集積(ログデータや気象データなど)であるケースが多く、その場合は知的財産には該当しません。ただし、生データに著作物性(写真、音声、映像、小説等)が認められる場合は、「著作権」が発生する可能性があります。 帰属については、生データが営業秘密にも該当しない場合は、知的財産が発生しないため、契約によって利用方法を個別に定める必要があります。 ②学習用データセット 生データを加工したとしても単なる情報の集積に過ぎないため、知的財産には該当しないケースが多いです。一方で、情報を体系的に整理し、データ抽出を容易にするような創作性を有する場合は、「データベースの著作物」に該当し、「著作権」が発生する可能性があります。 帰属については、ベンダーのノウハウのみを使用してデータセットを作った場合はベンダーが保有者となり、ユーザーとベンダーが共同で作成した場合は共同所有となることが多いです。 ③学習用プログラム 一般的な「プログラム」と同じく、ソースコード部分は著作物として「著作権」が発生し、アルゴリズム部分は特許法上の要件を満たせば「特許権」を有する可能性があります。 帰属については、著作権を持つのは著作者であり、特許権を持つのは発明者であるため、これら権利は基本的にベンダー側に帰属することが一般的です。 ④学習済みモデル 学習モデルのうち、「推論プログラム」は③の学習用プログラムと同じ整理になるため、ソースコード部分は著作権が、アルゴリズム部分は要件を満たせば特許権が発生します。「学習済みパラメータ」は⑤でも説明しますが、大量の数値データに過ぎず創作性等が認められないため、知的財産権の対象にはならない可能性が高いです。 帰属については、「推論プログラム」部分は、作成したベンダーに帰属することが基本です。 ⑤学習済みパラメータ 学習用プログラムにより自動的に生成される大量の数値データに過ぎないため、創作性が認められず、著作物も特許権も発生しない可能性が高いです。(一方、プログラムに準ずるものや著作性を持つと判断される見解もあり、定まっていないのが実情です) 帰属については、営業秘密にも該当しない場合は、知的財産権が発生しないため、契約によって利用方法を個別に定める必要があります。 ⑥ノウハウ AI開発には様々なノウハウが必要になりますが、ノウハウ自体は無形物であるため著作権の対象にはならないものの、発明の要件を満たす場合は特許権の対象になります。 帰属については、営業秘密にも発明にも該当しない場合は、知的財産が発生しないため、契約によって利用方法を個別に定める必要があります。 3. 知的財産権に関する契約の勘所 AI開発における「データ」、「プログラム」、「ノウハウ」について、知的財産に該当するかどうかは法的に定められており、一般的な解釈については理解できました。 一方で契約で争点になりやすいのが、 「知的財産権がユーザーとベンダーのどちらに帰属するか」 という点です。 先ほどのデフォルトルールのうち、法的に明確なのは「プログラム」で、基本的に作成者であるベンダーに知的財産権は帰属することになります。一方で、「データ」「ノウハウ」については、営業秘密や発明に該当しない場合は、原則ユーザー/ベンダー双方が自由に利用可能となってしまうため、契約によって利用条件を定める必要があります。 AI開発の知財帰属で、ユーザーとベンダーは対立しがち AI開発プロジェクトにおける権利帰属の議論の際、以下のようなやりとりがよく見られます。 双方が権利の帰属を主張し続ける限り、平行線のまま交渉に時間と労力だけを費やすことになります。ここで重要となるのが、 「権利帰属にこだわるのではなく、利用条件で実をとること」 です。 成果物をビジネスで利用する際、知的財産権を所有していなかったとしても、利用に制限がなく自由度が高ければ、実務上不都合が生じることは少ないです。そのため、個人的には権利の有無よりも、いかに自社にとって有利な利用条件とできるかの方が肝と考えます。 知的財産権の利用条件の定め方 利用条件の定め方に正解は無く、実態に応じて都度検討する必要がありますが、ユーザー/ベンダーがそれぞれ何を求めているかを相互によく理解した上で、利用条件を細かに設定することが重要です。 利用条件を定める上で考慮すべきポイントとしては、以下のようなものが考えられます。 利用目的 契約に規定された開発目的に限定するか否か 利用範囲 利用者がAIモデルをどの程度の範囲で使用できるか 利用期間 契約期間や終了条件の明示 第三者への利用許諾・譲渡可否 他社への提供や横展開を認めるか 利益配分 ライセンスフィー、プロフィットシェア プログラムの部分はベンダー側に帰属させる方が良い それぞれの知的財産権は、ユーザー、ベンダーいずれにも権利帰属させることは可能ですが、個人的にはAIモデルのプログラムに関する部分はベンダー側で保有する方がメリットが大きいと感じます。 例えば、AIモデルは技術発展のスピードが著しく、開発したモデルの陳腐化が早いことを踏まえると、ベンダー側に権利帰属させることで、アップデートや再学習を柔軟に行うことができます。ユーザー側には、ビジネス展開上不自由のない利用条件を付与することで双方の利益が最大化する可能性が高いです。 また、ユーザー側の懸念として、ベンダーが知的財産権を利用して競合他社へ横展開することを避けたい場合は、開発後一定期間の目的外利用や協業的利用をベンダーに禁止する等の契約とすることも可能です。 このように、権利帰属にこだわるのではなく、双方にとって不都合のない利用条件となるように、契約内容を協議することが重要になると思います。 ちなみにInsight Edgeでは、知的財産権の帰属は可能な限り自社に保有する契約にすることが多いです。 Insight Edgeの場合、ユーザーのほとんどは住友商事グループの事業会社ですが、対象とする業界や課題が多岐にわたるため、個別開発したAIモデルが他業界や事業会社に展開できるケースが少なくありません。 その際に、Insight Edgeにソリューションやノウハウを集積し、スピーディーに展開できるようにすることで、グループ全体の利益創出が期待できるためです。 4. まとめ AI開発における知的財産権にフォーカスし、契約においてユーザーとベンダーの間で論点となりやすいポイントをまとめました。 AIソリューションをビジネス展開する上で、知財戦略は重要な観点な一方で、AI技術の急速な普及により、AI技術の特性を当事者が理解しきれていないことや、権利関係・責任関係等の法律関係が不明確であるなど、契約に関するベストプラクティスが十分に確立されていないのが実情です。したがって、契約締結においてはビジネスの実態に踏まえた最適な内容となるよう、当事者間でしっかりと認識を合わせることが重要となります。 経産省が発表している「AI・データの利用に関する契約ガイドライン」では、具体事例も交えながら分かりやすくまとめられていますので、こちらも是非参考にして頂ければと思います。 参考記事・資料 AI・データの利用に関する契約ガイドライン | 経済産業省 「AI・データの利用に関する契約ガイドライン」に学ぶAI開発契約の8つのポイント | STORIA法律事務所 知的財産権について | 経済産業省 特許庁
アバター
はじめに  こんにちは!Insight Edgeでデータサイエンティストとして働いている五十嵐です!  最近花粉症が大変すぎて飲み薬に目薬に点鼻薬と毎日薬漬けです。鼻うがいも毎日してます!  今回は、AIの公平性について少し調べてみようかなと思い、調査内容を簡単にまとめます。本記事の内容は、基本的に、 A Survey on Bias and Fairness in Machine Learning (Mehrabi et al.) を参考にしています。本論文は、初稿が2019年8月ですが、何度か改修され、last revised が2022年1月となっております。被引用件数が2,000件を超えているので、この分野のsurvey論文としてはかなり有力なものではないでしょうか。  本記事は、様々な人にも興味を持って頂けるよう、技術的内容にはあまり触れずに紹介しようと思います!紹介論文は、34ページなので、今回紹介できる部分は極一部であることをご理解頂ければと思います。また、私が未熟で不勉強な部分も多いため、もし間違った解釈があった場合は優しくご指摘頂けますとありがたいです!! 「公平」とは  そもそも、「公平」とはなんなのでしょうか。AIの公平性についての話をする前に、そもそも「公平」の定義とはどのようなものなのか説明します。  様々な分野や考え方によって「公平」の定義は異なる為、一意に決めることは非常に難しいですが、本論文では、次のように説明されています。 “ absense of any prejudice or favoritism toward an individual or group based on their inherent or acquired characteristics ”.  直訳すると、「個人・グループに対して、先天的、または後天的な特徴によっていかなる偏見や好意がないこと」でしょうか。納得性が高い定義のように思います。 ※ 似たような意味で「公正」という言葉がありますが、「公平」と「公正」の正しい言葉の使い分けに自信があまりないので、本記事ではfairnessを公平、公平性と訳しております。 何故、AIの公平性が重要なのか  人間の判断に度々偏りが生じてしまうように、AIの判断も公平でなければ、我々人間と同じように差別的な判断をしてしまったり、偏った判断をしてしまう可能性があります。  近年は、AIシステムやアプリケーションが日常生活の中で広く使われるようになってきており、人生に大きく関わる分野でAI技術が使われるようになってきています。この為、以前より不公平なAIのもたらす影響が大きくなり得ると言えます。  例えば、不公平なAIシステムの例として頻出なものに、職業推薦システムがあります。同一条件であるにも拘らず、女性というだけで男性より低い評価になってしまう、つまり、性別の違いだけで推薦する職業やその収入が大きく違ってしまうという例は聞いたことがある人も多いのではないでしょうか。 不公平になる原因と不公平なAIが生む悪循環  不公平なAIシステムは一体どのような原因で生み出されてしまうのでしょうか。  それは、データやアルゴリズムに隠れた、あるいは無視されたバイアスです。(具体的なバイアス例については後述します。)  また、万が一、不公平なAIシステムが世の中で使われるとどうなるのでしょうか。  論文では、偏ったアルゴリズムの結果が、ユーザー体験に影響を与え、データ、アルゴリズム、ユーザーの間でフィードバックループが生じてしまい、既存の偏りを永続させ、さらに増幅させる可能性がある、と説明されています。 データ、アルゴリズム、ユーザーインタラクションのフィードバックループに配置されるバイアス定義の例 (A Survey on Bias and Fairness in Machine Learning (Mehrabi et al.))  学習データにバイアスがある場合、それを学習したアルゴリズムはそのバイアスを反映して予測をしてしまいます。また、データにバイアスがなくても、アルゴリズム自体が特定の設計上の仮定によりバイアスを有した挙動を示すことがあります。このようなバイアスを持つアルゴリズムの結果は、実世界のシステムに投入され、ユーザーの意思決定に影響を与え、よりバイアスのあるデータを生み出してしまいます。 バイアスの例  ここまで、不公平なAIシステムの原因としてデータやアルゴリズムに存在するバイアスであることを説明しました。  それでは、どのようなバイアスがあるのでしょうか。  ここでは、論文で紹介されていたバイアスについて、データに関するバイアスの一部を紹介します。 計測バイアス(reporting bias)  特定の特徴をどのように選択、利用、計測するかによって発生するバイアスです。 代表バイアス(representation bias)  データ収集プロセスにおいて、母集団からどのようにデータをサンプリングするかに起因するバイアスです。 (例) 下図に示すように、ImageNetの地理的な多様性の欠如は、西洋文化に対するバイアスに繋がるとされています。 Open ImagesとImageNetの画像データセットに含まれる、2文字のISOコードで表される各国の割合。両データセットとも、米国と英国が上位を占める. (No Classification without Representation: Assessing Geodiversity Issues in Open Data Sets for the Developing World(Shreya Shankar et al.)) 社会的バイアス(social bias)  他人の行動が我々の判断に影響を与える時に生じるバイアスです。 (例) 低い得点で何かを評価・レビューした場合に、他の人が高い評価をしていると、自分の評価が厳しすぎると考えて得点を変更してしまうことで生じるバイアスです。 歴史的バイアス(historical bias)  世の中に既に存在する偏りや社会技術的な問題であり、完璧なサンプリングと特徴選択を行えたとしても、データ生成プロセスから染み込んでくる可能性のあるバイアスです。 (例) 2018年においては、フォーチュン500のCEOのうち女性が5%しかいないことに起因し、CEOの画像検索結果が男性CEOに偏っていました。これは、現実を反映してものではありますが、検索アルゴリズムがこの現実を反映すべきかは検討が必要です。  他にも、様々なバイアスが紹介されていましたが、バイアスを全て紹介するのが本記事の目的ではないので、詳細を知りたい方は論文をご確認頂ければと思います。 どこからが問題なのか  これまで、AIシステムの原因となり得る様々なバイアスについて説明してきましたが、具体的にどこからが問題となるのでしょうか。バイアスではなく、正当な特徴であるかどうかの判断はどのようにすれば良いのか、この議題についても下記のような説明がされています。 差異が正当に説明可能かどうか  異なる集団間の待遇や結果の違いは、場合によってはある属性によって正当に説明されることがあります。このように、「差異が正当化され説明される状況では、それは問題にはならず、説明可能である」と記載されています。  この例として、平均して男性の方が女性より平均年収が高いという場合に、平均して女性の方が労働時間が短いという属性があれば、この男女差は説明可能であり、許容される、と説明されています。  個人的には、労働時間の男女差が何故生じているのかまで踏み込んで考えなければ扱いが難しい問題だとは思いますが、このようにある特徴量によって説明される場合は問題ないと見なされることが多いみたいです。  上記とは違い、どのような属性によっても正当に説明されない差異の場合は問題となります。 不公平なAIを作り出さないために、どうすれば良いのか  それでは、不公平なAIを作り出さないためにはどのようにすれば良いのでしょうか。  これまで紹介してきました、不公平なAIの原因となるバイアスは、大きくデータ由来のものと、アルゴリズム由来のものがあります。これらのバイアスを避けるために紹介されていた方法について、いくつか説明します。 データ由来のバイアス対策 データ由来のバイアスを避けるために、下記の内容が重要であると紹介されています。 「全てのデータセットは、データ管理者によってなされたいくつかの設計上の決定の結果である」ことを理解する. 扱っているデータの生成プロセスを正しく・詳細に理解する. 因果モデルや因果グラフの利用を検討する. それぞれについて説明していきます。 1. 「全てのデータセットは、データ管理者によってなされたいくつかの設計上の決定の結果である」ことを理解する  解析に用いるデータには、データ管理者が存在します。その管理者には何か目的がありデータを作成・収集しています。また、その管理者にもコントロールすることが難しい因子がデータそのもの、または、データ収集環境に存在する可能性があります。このことを正しく理解することで、データに存在するバイアスの調査にも取り掛れますし、後に説明するデータ生成プロセスの理解に時間を掛けることにも繋がります。  本論文では、対策として、データセット作成、特性、動機、偏りを報告するデータシートの作成をルール化する、というようなデータ利用時の良い習慣を提唱するアプローチもいくつか紹介されています。 2. 扱っているデータの生成プロセスを正しく・詳細に理解する  データを扱う際、そのデータだけを見ていても取得できる情報は限られています。データ背景、データ生成プロセスなどを詳細に理解しなければ正しくデータを理解することは非常に難しいです。この対策として、例えば、データドメインを調査し、データ生成プロセスを詳細に理解することが挙げられます。データ生成プロセスを正しく理解することで、前処理によってバイアスを取り除くなどの対処ができる場合があります。また、学習過程においては、目的関数に変更を加えたり、制約を課すなどしてバイアスを取り除ける場合があります。 3. 因果モデルや因果グラフの利用を検討する  2.と同様で、データを扱う際、そのデータだけを見ていても取得できる情報は限られています。この対策として、因果モデルや因果グラフの利用が多数提案されています。因果グラフはデータだけではなく、その背景や生成プロセスなど、交絡因子に関する因果関係を表現することができます。 アルゴリズム由来のバイアス対策  アルゴリズムもバイアスを持つことがあります。その中の一つ、帰納バイアスについて紹介します。  帰納バイアスとは、簡単にいうと「そのアルゴリズムが前提としている仮定により発生するバイアス」です。具体例として、ViT(Vision Transformer)とCNN(Convolutional Neural Network)の学習データ量と精度の関係についての議論で知っている人も多いのではないでしょうか。CNNは「画像データは近傍の(局所的な)情報が重要である」という仮定を持つ、帰納バイアスのあるモデルです。これに対して、ViTは強い仮定をおいていないため、強い帰納バイアスを持ちません。CNNが比較的少ないデータ数の場合は、ViTより高い精度が出やすいのはこの帰納バイアスが上手く機能している為と考えられています。逆に、帰納バイアスの弱いViTは十分なデータセットを用いた場合にはCNNよりも高い精度を誇ります。このように、アルゴリズムの持つバイアスによって使い所が変わりますし、データ構造やその目的によって適切なアルゴリズムを選択することが理想的だと言えます。また、帰納バイアスなど、使用するアルゴリズムの性質を正しく理解していないとその使い所や解釈を間違えてしまうため、アルゴリズムの持つバイアスを理解することは非常に重要です。  他にアルゴリズムの持つ仮定として、データや残差の分布が正規分布を仮定するなど様々ありますが、正しく利用し正しく解釈するには、いずれもデータとアルゴリズムの理解が必要です。 最後に  論文の一部を簡単に紹介してきましたが、正直かなり難しい問題であることを再認識できました。論文を読んだだけでも、そのタイトルにある” Bias and Fairness in Machine Learning ”という議題がいかに難しいかが実感できます。また、バイアスについてもご紹介した以外に様々あり、世の中からあらゆるバイアスを完全に無くすことはかなり難しいことも実感します。しかし、本記事で説明したように、データの生成プロセスを意識したり、使用するアルゴリズムの性質を理解することで、AIシステムにバイアスが入り込む可能性を低くする取り組みができます。 また、対策についても、本記事でご紹介できた以外にも様々なアイデアや取り組みが数多くあるということはご理解頂ければと思います。  今回紹介できた部分は極一部ですが、この記事が誰かがAIの公平性について考える一助になってくれれば幸いです。
アバター
こんにちは、花粉症がキツくなってきましたエンジニアリングマネージャの猪子です! 2023/3/1にTech Meetup Eventを弊社、 FastLabel株式会社 、 株式会社ヘッドウォータース の3社で合同開催しました。 テーマは 先端テクノロジー活用によるDX実現を目指す開発組織の中身に潜入 です。 FastLabelのCEO 上田 英介さん司会のもと、弊社 CTO 福井が冒頭挨拶をさせて頂きました。 その後、FastLabel VPoE 植野 晃司さん、ヘッドウォータース コネクテッドテクノロジー部 部長 西川 貴弘さん、私がスピーカーとしてLightning Talkを行い、発表後は30分ほどフリートークセッションが行われました。 スピーカーの皆様 イベントは一般公開はしなかったのですが、当日の参加者は50名を超え、社内のセミナー用slackチャンネルも盛り上がりを見せていました :) 当日の発表 LT① 継続カイゼン!トライ&エラーから学んだコミュニケーションと環境づくり 株式会社Insight Edge Engineering Manager 猪子 徹 speakerdeck.com LT② 急成長を続けるAI×SaaSスタートアップで求められるエンジニアスキル FastLabel株式会社 VPoE 植野 晃司 speakerdeck.com LT③ ”新しいことに挑戦したい組織”が考えるべきこと ~”やりたい”だけでは実現しない~ 株式会社ヘッドウォータース コネクテッドテクノロジー部 部長 西川 貴弘 speakerdeck.com 振り返って 夕方の遅い時間にも関わらず各社から多くの参加者が集まり、盛り上がりを見せていました! 内容としてもコミュニケーション、キャリア/スキル、組織としてのチャレンジ等、各社のフェーズは異なれど何れも今後の成長のヒントになる内容だったと思います。 何より未だセミナー登壇などもオンラインで聴講者の顔が見えずに配信される中、他社のエンジニアの方々と顔を見せ合いながら交流出来たのが個人的に良い刺激でした! 今後も社内外含め定期的に情報発信を続けていきます :) 終わりに Insight Edgeはデータサイエンティスト、エンジニア、UI/UXデザイナ、プロジェクトマネージャ、コンサルタント等、会社を共に盛り上げてくれるメンバを募集しています。 興味がある方はカジュアルにお話させて頂ければと思いますので、お気軽に こちら からお申し込みください!
アバター
init_mathjax = function() { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true } }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], displayMath: [ ['$$','$$'], ["\\[","\\]"] ], processEscapes: true, processEnvironments: true }, displayAlign: 'center', CommonHTML: { linebreaks: { automatic: true } } }); MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } } init_mathjax(); $\def\R{{\mathbb R}}$ $\def\N{{\mathbb N}}$ $\def\B{\{0, 1\}}$ Insight Edgeのデータサイエンティストのki_ieです。今日は最近勉強した数理最適化関連の技術を紹介します。 はじめに 本記事では、混合整数計画(MIP)問題に対する列生成法の発展的な使い方を紹介します。列生成法は巨大な線形計画(LP)問題を解くための厳密解法です。列生成法の有名な応用先として、多数の子問題と少数の子問題をまたぐ制約とに分解できるMIP問題のヒューリスティック解法があります。本記事では、このヒューリスティック解法を典型的なユースケースから少し拡張して利用する(具体的には、子問題に連続変数が登場するような場合でも利用できるように拡張する)方法を考えます。 本記事の構成は以下の通りです。 列生成法 : 列生成法について簡単に説明します。 列生成法のMIP問題への応用 - 典型的なケース : 列生成法のMIP問題への典型的な応用例を紹介します。 列生成法のMIP問題への応用 - 発展編 : 子問題に連続変数が登場するようなMIP問題での列生成法を紹介します。 プログラム実行例 : 簡単なサンプルプログラムを添付します。 列生成法 列生成法とは、大規模なLP問題を解くための技法です。この章では、列生成法の手続きと、その基本的な性質について簡単に説明します。 対象とするLP問題 次のLP問題 $(P)$ を考えます。 主問題 $(P)$ 目的関数: max. $c^\top x$ 制約: $Ax \leq b$, $x \geq 0$ 後ほど解析に利用するので、ここで $(P)$ の双対問題 $(D)$ も定義しておきましょう。 双対問題 $(D)$ 目的関数: min. $b^\top y$ 制約: $A^\top y \geq c, y \geq 0$ 通常、LP問題はLPソルバーで高速に解くことができるのため求解に苦労することはないですが、 Aの列数があまりにも多く、Aの列を事前にすべて列挙しつくすことができない という場合状況が変わります。 このような場合に用いられるのが列生成法です。 列生成法の手順 列生成法では「ほとんどの $A$ の列(と対応する変数)を無視して問題を解いても、最適解は得られるだろう」と山を張って、必要最小限の $A$ の列を列挙することで、効率的に $(P)$を解こうとします。 具体的な手順を見てみましょう。 列生成法では、最初に$A$ の列の一部 $A'$ を列挙し、 $(P)$ の実行可能な部分問題$(P')$ を作ります。ここで、$x'$ は $(P)$ において $A'$ の列に対応する変数のベクトルで、$c'$ は同様に対応する目的関数の重みです。( $(P')$ が実行可能になるような $A'$ の列挙は簡単にできると想定します。) 部分問題 $(P')$ 目的関数: max. $c'^\top x'$ 制約: $A' x' \leq b$, $x' \geq 0$ ここで $(P')$ の最適解を $x'_*$ とします。$x'_*$ は $(P)$ の実行可能解ですが、これがもし $(P)$ の最適解であればうれしいですね!また、もしそうでなければ、適当な列を $A'$ に加えて部分問題の最適解の目的関数値を改善することにしましょう。 最適性の確認 まず $x'_*$ の $(P)$ での最適性を確認しましょう。解析のために、 $(P')$ の双対問題 $(D')$ を次のように定義し、この最適解を $y'_*$ とします。 双対問題 $(D')$ 目的関数: min. $b^\top y$ 制約: $A'^\top y \geq c', y \geq 0$ このとき、 $x'_*, y'_*$ の性質は以下のように整理できます。 $x'_*$ は $(P)$ の実行可能解 $x'_*$ の $(P)$ での目的関数値と、$y'_*$ の $(D)$ での目的関数値は一致する。 (というのも、 $x'_*$ の $(P)$ での目的関数値 = $x'_*$ の $(P')$ での目的関数値 = $y'_*$ の $(D')$ での目的関数値 = $y'_*$ の $(D)$ での目的関数値、であるから) ここに「3. $y'_*$ は $(D)$ の実行可能解」が加われば主実行可能解と双対実行可能解の目的関数値が一致していることになり、$x'_*$ は $(P)$ の最適解であることがわかりハッピーエンドです。 $y'_*$ が $(D)$ の実行可能解であることを確認するためには、 $A^\top y \geq c$ を確認すればOKです。 ここで $A=[A' | A''], c=[c' | c'']$ と分解してこの条件を再度確認すると、 $\begin{bmatrix}A'^\top \\ A''^\top \end{bmatrix} y \geq \begin{bmatrix}c' \\ c'' \end{bmatrix}$ です。 $A'^\top y \geq c'$ は $y'_*$ が $(D')$ の実行可能解であることから担保されているので、下段の $A''^\top y \geq c''$ が確認できればOKです。 何らかの方法で $A''^\top y \geq c''$ が証明できれば最適性の証明は終わりです。一般には、 $A$ の列をすべて列挙することが現実的でないという仮定の下ではこれを愚直に証明することは難しいでしょう。しかし、すこし唐突ですが 何らかの効率の良い方法により $c'' - A''^\top y$ の最大値を求めることできれば (← まだ腑に落ちないで大丈夫です!)、最適性の証明が可能となります。つまり、 $c'' - A''^\top y$ の最大値が $0$ 以下の場合、$x'_*$ が$(P)$ の最適解です。 列生成法は、$c'' - A''^\top y$ の最大値を求められるような特殊な状況で用いられるアルゴリズムなのです。 列の生成 $c'' - A''^\top y$ の最大値が $0$ より大の場合はどうでしょうか。この場合、列生成法のアルゴリズムはこの最大値を与える列を $A'$ に加えて$(P'), (D')$ を解き、また最適性の確認に戻ります。 $A$ の列数は有限なのでこの手続きはいずれ停止します。実際には、$A$ のごく一部の列を加えるだけでこの手続きが終了することが期待されます。 最小限の列生成法の解説はこの通りですが、 Aの列数があまりにも多く、Aの列を事前にすべて列挙しつくすことができない 何らかの効率の良い方法により $c'' - A''^\top y$ の最大値を求めることができる という条件を満たす具体的なケースが想像しづらく、そんなケースが実在するのか怪しく見えてしまいますね。これらの条件が実際に満たされるケースは列生成法のMIP問題への応用を見ると理解しやすいので、次の章で考えましょう。 列生成法のMIP問題への応用 - 典型的なケース 問題の定義 列生成法のMIP問題への応用を見ていきましょう。次の問題を考えます。 多数のタンクを部屋に収納したいと考えている。タンクにはそれぞれサイズが定まっている。 部屋は、同じサイズのものが複数与えられている。 与えられた部屋に収納可能な範囲で「タンクのサイズの総和」を最大化したい。 これは、数理最適化問題としては次のように記述されます。 タンク配置問題1 $(P_{1})$ パラメタ $I$: タンクの集合。 $|I|=m$ $J = \{0, 1, \cdots, n-1 \}$: 部屋の集合。 $|J|=n$ $s_i \in \N_{>0} \ (i \in I)$: タンクのサイズ。 $S \in \N_{>0} $ : 各部屋の容量 (すべての部屋で共通とする) 変数 $x_{ij} \in \B \ (i \in I, j \in J)$ : タンク $i$ を部屋 $j$ に設置するか否か 目的関数 すべての部屋に置かれたタンクのサイズの和を最大化したい max $\sum_{i \in I, j \in J} s_i x_{ij}$ 制約 タンクは一度しか使えない $\sum_{j \in J} x_{ij} \leq 1 \ (i \in I)$ 部屋に置けるタンクのサイズの和には上限がある $\sum_{i \in I} s_i x_{ij} \leq S \ (j \in J)$ $(P_{1})$ は、変数、制約の数という意味では効率よく表現されたMIP問題で「$A$ の列数があまりにも多く、$A$ の列を事前にすべて列挙しつくすことができない」ような問題ではありません。このままMIPソルバーに問題なく入力が可能です。しかし、このような多数の子問題の解を張り合わせて全体の解とするようなMIP問題では、上手に変形して列生成法を利用することで効率的に(理論保証のない)実行可能解を得られるケースがあります。 問題の変形 列生成法を利用できる形に問題を変形しましょう。まず、すべての部屋 $j \in J$ について、その部屋に入れられるタンクの組合せ全ての集合 $Q(j)$ を定義します。この集合は、問題規模に応じて指数的に大きくなります。これらの組合せ $q \in Q(j), j \in J$ について、部屋 $j$ の組合せ $q$ におけるタンクサイズの和 $s'_{jq}$ とします。また部屋 $j$ の組合せ $q$ にタンク $i$ が含まれるかどうかを $\xi_{ijq}$ とします。これらを問題のパラメタだと思うと、部屋 $j$ で組合せ $q$ を採用するか否かの0/1変数 $X_{jq}$ を用いて、$(P_{1})$ は次のように変形できます。 タンク配置問題1 - 変形版 $(MP_{1})$ パラメタ $I$: タンクの集合。 $|I|=m$ $J = \{0, 1, ..., n-1 \}$: 部屋の集合。 $|J|=n$ $Q(j) \ ( j\in J) $ : 部屋 $j$ に入れられるタンクの組合せの集合 $s'_{jq} \ (j \in J, q \in Q(j))$ : 部屋 $j$ の組合せ $q$ におけるタンクサイズ和 $\xi_{ijq} \ (j \in J, q \in Q(j))$ : 部屋 $j$ の組合せ $q$ にタンク $i$ が含まれるか。 変数 $X_{jq} \in \B \ (j \in J, q \in Q(j))$ : 部屋 $j$ で組合せ $q$ を採用するか 目的関数 すべての部屋に置かれたタンクのサイズの和を最大化したい max $\sum_{j \in J, q \in Q(j)} s'_{jq} X_{jq}$ 制約 タンクは一度しか使えない $\sum_{j \in J, q \in Q(j)} X_{jq} \xi_{ijq} \leq 1 \ (i \in I)$ 組合せ選択 $\sum_{q \in Q(j)} X_{jq} \leq 1 \ (j \in J)$ 列生成法の利用 問題 $(MP_{1})$ には $\sum_{j \in J} |Q(j)|$ 個の変数・列が登場するため、 問題全体を記述しきってからこの問題をソルバーに解かせるというのは現実的ではありませんが、 列生成法を用いて有用であろう少数の変数・列を列挙をすることで、良い実行可能界が算出できます 列生成法を利用した $(MP_{1})$ の解き方は次のような手順です。背景には、$(MP'_{1})$ に登場する変数・列をだけを考慮しても、問題 $(MP_{1})$ の良い実行可能解が得られるだろうという期待があります。 列生成法を利用した $(MP_{1})$ の解き方: $(MP_{1})$ の線形緩和問題 $(MLP_{1})$ の最適解を求める。ここで列生成法を利用する。 $(MP_{1})$ のうち前段の列生成法中に利用した変数・列だけに注目しが部分問題 $(MP'_{1})$ を定義し、これをMIPソルバーで解く。 では列生成法を実行するために必要だった「$c'' - A''^\top y$ の最大値を求める」という部分はどのように実装すれば良いのでしょうか。 $(MLP_{1})$ の部分問題 $(MLP'_{1})$ において $j \in J$ について列挙されている列の集合が $Q'(j) \ (Q'(j) \subseteq Q(j))$ であるとしましょう。このとき、 「$c'' - A''^\top y$ の最大値を求める」というのは、以下の最適化問題を解くことにほかなりません。 ここで $\pi_i (i \in I), \mu_j (j \in J)$ はそれぞれ制約1, 2 に対応する$(MLP'_{1})$ における双対最適解の値です。 子問題1 $(SP_{1})$ 変数 $j \in J$ $q \in Q(j)$ 目的関数 max. $s'_{jq} - \sum_{i \in I} \xi_{ijq} \pi_i - \mu_j$ この問題は、$j$ を固定して $s'_{jq}, \xi_{ijq}$ の定義に立ち返ると次の整数計画(IP)問題として記述できます。(補足: $(SP_{1})$ の定義では、本来は変数を $j \in J, q \in Q(j) \setminus Q'(j)$ ととるのが $A''$ の定義と一貫した方法です。しかし子問題の最適化結果が既知の列を返すことは列生成の終了判定条件が満たされる時だけなので、$j \in J, q \in Q(j)$ で代用して問題ありません。またMIPで記述する際に $Q(j) \setminus Q'(j)$ の取り扱いが難くなるので、このような変数の取り方を採用しています。) 子問題1' $(SP'_{1}(j))$ 変数 $x_{i} \in \B \ (i \in I)$ : タンク $i$ を組合せに入れるか否か。 目的関数 max $\sum_{i \in I} s_i x_{i} - \sum_{i \in I} \pi_i - \mu_j$ 制約 タンクは一度しか使えない $\sum_{j \in J} x_{i} \leq 1 \ (i \in I)$ 部屋に置けるタンクのサイズの和には上限がある $\sum_{i \in I} s_i x_{i} \leq S$ $(SP'_{1}(j))$ はナップサック問題なので、($S$ が大きすぎなければ) 高速に解くことが可能です。これを利用して、列生成法の「$c'' - A''^\top y$ の最大値を求める」という手続きを実行できます。 問題 $(MP_{1})$ に対する列生成法を利用したヒューリスティックは、まとめると以下のような流れになります。 $(MP_{1})$ の線形緩和問題 $(MLP_{1})$ の最適解を求める。ここで列生成法を利用する。 初期解生成: 各部屋 $j$ に「一つもタンクを含まない組合せ」のみからなる集合を $Q'(j)$ として与えればOK。一つもタンクを置かないというのは実行可能解であるため。 列の生成: $(SP'_{1}(j))$ を利用して終了判定および列の追加を繰り返す。 $(MLP_{1})$ の記述に利用した変数・列だけを利用して、問題 $(MP_{1})$ の部分問題 $(MP'_{1})$ をMIPソルバーで解く。 この問題に限らず、子問題が切り出せるMIP問題では、上記の流れのヒューリスティック解法が利用できます。具体的な問題によって精度の良し悪しがあったり、初期解生成に一工夫を加えたりという手間が必要なこともありますが、実装や個別の問題における数学的な整理がそこまで難しくないため、列生成法は子問題に分解可能な難しいMIP問題を解くためのツールとして頻繁に用いられています。 列生成法のMIP問題への応用 - 発展編 ここまで、子問題がIP問題であるようなMIP問題に列生成法が有効であることを確認しました。 では、子問題の構造を持ちながら子問題が連続変数を含むMIP問題になっているケースに列生成法を適用するとき、どのような注意・変更が必要なのでしょうか。実際の問題を題材に考えていきましょう。 問題の定義 前述の問題を拡張した次の問題を考えます。 多数のタンクを部屋に収納し、それらのタンクに水を入れて保管したいと考えている。 タンクにはそれぞれ自重とサイズが与えられている。 部屋は、同じサイズのものが $n$ 個与えられている。 与えられた部屋に収納可能な範囲で「 タンクに入れる水の量 」を最大化したい。 部屋は1次元的に等間隔で並んでおり、その座標は $0, 1, 2, \cdots, n-1$ である。 各部屋の重さは収納したタンクと水の重さの合計であり、建物の構造上、全体の重心が両端の部屋の中央(座標で$(n-1)/2$)にある必要がある。 これは、数理最適化問題としては次のように記述されます。 タンク配置問題2 $(P_{2})$ パラメタ - $I$: タンクの集合。 $|I|=m$ - $J = \{0, 1, \cdots, n-1 \}$: 部屋の集合。 $|J|=n$ - $s_i \in \N_{>0} \ (i \in I)$: タンクのサイズ。 - $w_i \in \R_{>0} \ (i \in I)$: タンクの重さ。 - $S \in \N_{>0} $ : 各部屋の容量 (すべての部屋で共通とする) 変数 $x_{ij} \in \B \ (i \in I, j \in J)$ : タンク $i$ を部屋 $j$ に設置するか否か $y_{j} \geq 0 \ (j \in J)$ : 部屋 $j$ のタンクに合計どれだけ水を入れるか。 目的関数 タンクに注ぐ水の量の和を最大化したい max $\sum_{j \in J} y_{j}$ 制約 タンクは一度しか使えない $\sum_{j \in J} x_{ij} \leq 1 \ (i \in I)$ 部屋に置けるタンクのサイズの和には上限がある $\sum_{i \in I} s_i x_{ij} \leq S \ (j \in J)$ 部屋のタンクに入れる水の量の上限は、置かれたタンクのサイズの和によって決まる $y_{j} \leq \sum_{i \in I} s_i x_{ij} \ (j \in J)$ 重心が中央にあること $\sum_{j \in J} W_j (j-c) = 0$ ただし $c = (n-1)/2 $, $W_j = y_j + \sum_{i \in I} w_i x_{ij}$ 問題の変形 この問題も子問題の構造を持ちます。列生成法の適用を考えるため、子問題を列挙するような形に変形してみましょう。 タンク配置問題2 - 変形版1 $(MP_{2.1})$ パラメタ $I$: タンクの集合。 $|I|=m$ $J = \{0, 1, ..., n-1 \}$: 部屋の集合。 $|J|=n$ $Q(j) \ ( j\in J) $ : 部屋 $j$ に入れられるタンクと水量の組合せの集合 $s'_{jq} \ (j \in J, q \in Q(j))$ : 部屋 $j$ の組合せ $q$ におけるタンクサイズ和 $w'_{jq} \ (j \in J, q \in Q(j))$ : 同、タンクの重さ。 $\xi_{ijq} \ (j \in J, q \in Q(j))$ : 部屋 $j$ の組合せ $q$ にタンク $i$ が含まれるか。 $\lambda_{jq} \ (j \in J, q \in Q(j))$ : 部屋 $j$ の組合せ $q$ で、タンクにどれだけの水をいれるか。 変数 $X_{jq} \in \B \ (j \in J, q \in Q(j))$ : 部屋 $j$ で組合せ $q$ を採用するか 目的関数 タンクに入った水の量の最大化 max $\sum_{j \in J, q \in Q(j)} \lambda_{jq} X_{jq}$ 制約 タンクは一度しか使えない $\sum_{j \in J, q \in Q(j)} X_{jq} \xi_{ijq} \leq 1 \ (i \in I)$ 組合せ選択 $\sum_{q \in Q(j)} X_{jq} \leq 1 \ (j \in J)$ 重心が中央にあること $\sum_{j \in J} W_j (j-c) = 0$ ただし $c = (n-1)/2 $, $W_j = \sum_{q \in Q(j)} (w'_{jq} + \lambda_{jq}) X_{jq}$ $(MP_{2.1})$ 正しい変形になっていますが、$Q(j)$ が無限集合になっている点が気になります。また列生成法でそのごく一部 $Q'(j) \ (j \in J)$ を列挙した後、本来連続変数で自由に(計算コスト低く)最適化できる $y_j$ が $\lambda_{jq}$ で(いくつかの飛び飛びの値に)固定されてしまっているのも気になりますね。今回は制約3.のように等式制約が入っているので、これは特に都合が悪いです。 そこで、これらの課題を回避するために次のような定式化を考えます。 タンク配置問題2 - 変形版2 $(MP_{2.2})$ パラメタ $I$: タンクの集合。 $|I|=m$ $J = \{0, 1, ..., n-1 \}$: 部屋の集合。 $|J|=n$ $R(j) \ ( j\in J) $ : 部屋 $j$ に入れられるタンクの組合せの集合 $s'_{jr} \ (j \in J, r \in R(j))$ : 部屋 $j$ の組合せ $r$ におけるタンクサイズ和 $w'_{jr} \ (j \in J, r \in R(j))$ : 同、タンクの重さ。 $\xi_{ijr} \ (j \in J, r \in R(j))$ : 部屋 $j$ の組合せ $r$ にタンク $i$ が含まれるか。 変数 $X_{jr} \in \B \ (j \in J, r \in R(j))$ : 部屋 $j$ で組合せ $r$ を採用するか $Y_{jr} \in [0, 1] \ (j \in J, r \in R(j))$ : 部屋 $j$ で組合せ $r$ を採用したときに、何割の水を入れるか。 目的関数 タンクに入った水の量の最大化 max $\sum_{j \in J, r \in R(j)} s'_{jr} Y_{jr}$ 制約 タンクは一度しか使えない $\sum_{j \in J, r \in R(j)} X_{jr} \xi_{ijr} \leq 1 \ (i \in I)$ 組合せ選択 $\sum_{r \in R(j)} X_{jr} \leq 1 \ (j \in J)$ Yの制限 $Y_{jr} \leq X_{jr} \ (j \in J, r \in R(j))$ 重心が中央にあること $\sum_{j \in J} W_j (j-c) = 0$ ただし $c = (n-1)/2 $, $W_j = \sum_{r \in R(j)} w'_{jr} X_{jr} + s'_{jr} Y_{jr}$ この定式化では $R(j)$ は有限集合です。また、これの部分集合 $R'(j) \ (j \in J)$ を列挙した後でも、連続変数の自由度は $Y_{jr}$ の形で保たれています。しかし今度は $R(j)$ の一要素 $r$ に対して、2つの列($X_{ir}$, $Y_{jr}$)が対応してしまい、素朴に列生成を行うことができません。 しかし実は $(MP_{2.2})$ の定式化を用いると、列生成的なヒューリスティックを組み立てることができます。 列生成法の利用 $(MP_{2.2})$ を用いたヒューリスティックの手順は以下の通りです: $(MP_{2.2})$ の線形緩和問題 $(MLP_{2.2})$ の最適解を求める。ここで列生成法を利用する。 初期解生成 : $R'(j)$ : 各部屋 $j$ に「一つもタンクを含まない組合せ」のみからなる集合を $R'(j)$ として与えればOK。 終了判定と列生成 $R(j) \ (j \in J)$ の部分集合 $R'(j)$ が列挙されているとする。これに対応する$(MLP_{2.2})$ の部分問題 $(MLP'_{2.2})$ を解く。 子問題 $(SP'_{2})$ を利用して終了判定および列の追加を繰り返す。 最適性が証明された場合、 2. に移行 最適性が証明されなかった場合 $R'(j)$ にタンクの組合せを追加する。 部分MIP問題の求解 $(MP_{2.2})$ の$Q'(j)$に対応する部分問題 $(MP'_{2.2})$ をMIPソルバーで解く ここで、子問題 $(SP'_{2})$ は典型的なケースで利用した論法をそのまま利用して、以下の最適化問題で定義します。ここで $\pi_i (i \in I), \mu_j (j \in J), \gamma$ はそれぞれ制約1, 2, 3に対応する $(MLP'_{2.1})$ の双対最適解における値です。 子問題2' $(SP'_{2}(j))$ 変数 $x_{i} \in \B \ (i \in I)$ : タンク $i$ を組合せに入れるか否か。 $y$ : どれだけの水を入れるか。 目的関数 max $\sum_{i \in I} y_{i} - \sum_{i \in I} \pi_i - \mu_j - (w_i x_i + y) (j-c) \gamma$ 制約 タンクは一度しか使えない $\sum_{j \in J} x_{i} \leq 1 \ (i \in I)$ 部屋に置けるタンクのサイズの和には上限がある $\sum_{i \in I} s_i x_{i} \leq S$ 有限回で手続きが終了するのか、$j$ について $(SP'_{2}(j))$ の目的関数値が 0 以下となったときに $(MLP_{2.2})$ が厳密に解けているのか、という部分の確認は少し複雑なので省略します。 いずれも、 $(MP_{2.1})$ の $Q(j)$ の要素を子問題において連続変数が(整数変数を固定したときの)端点になっている解に絞った問題 $(MPV_{2})$ を考え、前述の手続きを $(MPV_{2})$ の線形緩和問題 $(MLPV_{2})$ (これは $(MLP_{2.2})$ と等価になります) に対する列生成法の手続きとして解釈することで、証明ができます。 この手続きの「終了判定と列生成」は、$Q'(j)$ に列を追加する際に「同じ整数変数値の列全て」を追加することで、有限回 (最大でも $\sum_{j\in J}|Q(j)|$回) で停止するようになっています。また、部分MIP問題の求解のステップでは $(MP_{2.2})$ のモデルを用いて子問題の連続変数を自由にコントロールして、等式制約に問題なく対処できる手続きになっています。 ここまで、子問題に連続変数が登場するようなMIP問題でも列生成法を使ったヒューリスティック解法の設計ができる場合がある、というご紹介でした。 最後に $(P_2)$ に対するナイーブな解法・列生成法的な解法のサンプルプログラムと実行結果を紹介して、この記事の結びとします。 プログラム実行例 準備 import numpy as np import pulp # MIPソルバーCBC へのラッパー https://github.com/coin-or/pulp import itertools from dataclasses import dataclass @ dataclass class Params : # (P_2) の問題パラメタをまとめるためのクラス。s や w の値はランダム生成するようにする。 # 初期化に利用するメタパラメタ m: int = 5 n: int = 10 S: int = 8 # ランダムシード rand_seed: int = 0 # 初期化後に生成するパラメタ I: range = None J: range = None s: np.ndarray = None w: np.ndarray = None def __post_init__ (self): np.random.seed(self.rand_seed) self.I = range (self.m) self.J = range (self.n) self.s = np.random.randint( 1 , self.S, size=(self.m,)) self.w = ( 1 / 2 ) * self.s def rename_constraint (name: str , c: pulp.LpConstraint) -> pulp.LpConstraint: # pulp の制約に名前をつけてそのまま返す。デバッグ用。 c.setName(name) return c ナイーブな方法 def generate_problem (params: Params): prob = pulp.LpProblem(sense=pulp.LpMaximize) ## 変数定義 x = { (i, j): pulp.LpVariable(name=f 'x_{i}_{j}' , cat=pulp.LpBinary) for i in params.I for j in params.J } y = { j: pulp.LpVariable(name=f 'y_{j}' ,lowBound= 0 , upBound= None , cat=pulp.LpContinuous) for j in params.J } ## 目的関数 max_obj = pulp.lpSum(y.values()) ## 補助的な線形関数 # 線形関数を補助的に定義 部屋内合計タンクサイズ = { j: pulp.lpSum(params.s[i] * x[i, j] for i in params.I) for j in params.J } ## 制約 # タンクは一度しか使えない タンク利用_constr = { i: pulp.lpSum(x[i, j] for j in params.J) <= 1 for i in params.I } # 部屋に置けるタンクのサイズの和には上限がある 部屋内合計タンクサイズ_上限_constr = { j: 部屋内合計タンクサイズ[j] <= params.S for j in params.J } # 部屋のタンクに入れる水の量の上限は、置かれたタンクのサイズの和によって決まる 部屋内水量_上限_constr = { j: y[j] <= 部屋内合計タンクサイズ[j] for j in params.J } # 部屋の重心が中心にあること c = (params.n- 1 )/ 2 W = {j: pulp.lpSum(params.w[i] * x[i, j] for i in params.I) + y[j] for j in params.J} 重心_constr = (pulp.lpSum( W[j] * (j-c) for j in params.J) == 0 ) # 目的関数・制約を最適化問題に追加 prob.setObjective(max_obj) for c in itertools.chain( タンク利用_constr.values(), 部屋内合計タンクサイズ_上限_constr.values(), 部屋内水量_上限_constr.values(), [重心_constr] ): prob.addConstraint(c) return prob, x, y my_params = Params(m= 100 , n= 30 , S= 20 , rand_seed= 0 ) # パラメタ生成 my_prob, *_ = generate_problem(my_params) # 問題生成 # optimization_status = my_prob.solve(pulp.PULP_CBC_CMD(timeLimit=10, msg=1)) # 求解 optimization_status = my_prob.solve(pulp.PULP_CBC_CMD(timeLimit= 10 , msg= 0 )) # (ブログ整形用にmsg=0に設定) 結果は次の通り。目的関数値562.88の実行可能解が得られているが、上界値(Upper bound)は600とまだ最適化の余地がある。 Welcome to the CBC MILP Solver Version: 2.10.3 Build Date: Dec 15 2019 (中略) Result - Stopped on time limit Objective value: 562.87931034 Upper bound: 600.000 Gap: -0.06 Enumerated nodes: 8030 Total iterations: 84038 Time (CPU seconds): 9.93 Time (Wallclock seconds): 10.00 Option for printingOptions changed from normal to all Total time (CPU seconds): 9.94 (Wallclock seconds): 10.01 列生成法的な方法 def solve_colgen (params: Params, time_limit= 10 , max_loop= 10 , eps= 0.0001 ): # タンクの組合せとその情報 ## 変数定義 X = [[pulp.LpVariable(name=f 'X_{j}_{0}' , cat=pulp.LpBinary)] for j in params.J] # 0/1 離散変数。 X[j][r] は j についてパターン r のタンクの置き方を採用することを意味。 Y = [[pulp.LpVariable(name=f 'Y_{j}_{0}' , cat=pulp.LpContinuous, lowBound= 0.0 , upBound= 1.0 )] for j in params.J] # 0-1 連続変数。 Y[j][r] は j についてパターン r のタンクに、容量に対してどれだけの割合の(0~1)水を入れるか。 ## 生成された列(変数)のデータ ss = [[ 0.0 ] for j in params.J] # ss[j][r] は部屋 j のパターン r のタンクの置き方における、水の容量。 ww = [[ 0.0 ] for j in params.J] # ww[j][r] は (同) タンク自体の重さ xi = {(i,j):[ 0.0 ] for j in params.J for i in params.I} # xi[i,j][r] は (同) において、タンク i を利用するか否か (0/1) def solve_master_prob (): master_prob = pulp.LpProblem(sense=pulp.LpMaximize) # 目的関数 max_obj = pulp.lpSum([ ss[j][r] * Y[j][r] for j in params.J for r in range ( len (X[j])) ]) # 制約 ## パターンの選択 パターン選択_constr = { j: rename_constraint( f 'パターン選択_constr_{j}' , pulp.lpSum(X[j][r] for r in range ( len (X[j]))) <= 1 ) for j in params.J } ## Yの制限 Yの制限_constr = { (j, r): rename_constraint( f 'Yの制限_constr_{j}_{r}' , Y[j][r] <= X[j][r] ) for j in params.J for r in range ( len (X[j])) } ## タンクは一度しか使えない タンク利用_constr = { i: rename_constraint( f 'タンク利用_constr_{i}' , pulp.lpSum( xi[i,j][r] * X[j][r] for j in params.J for r in range ( len (X[j])) ) <= 1 ) for i in params.I } ## 部屋の重心が中心にあること c = (params.n- 1 )/ 2 W = { j: pulp.lpSum(ww[j][r] * X[j][r] + ss[j][r] * Y[j][r] for r in range ( len (X[j]))) for j in params.J } 重心_constr = rename_constraint( '重心_constr' , (pulp.lpSum( W[j] * (j-c) for j in params.J) == 0 ) ) # 目的関数・制約を最適化問題に追加 master_prob.setObjective(max_obj) for c in itertools.chain( パターン選択_constr.values(), Yの制限_constr.values(), タンク利用_constr.values(), [重心_constr], ): master_prob.addConstraint(c) master_prob.solve(pulp.PULP_CBC_CMD(mip= 0 , msg= 0 )) パターン選択_constr_dual = {r: v.pi for r,v in パターン選択_constr.items()} Yの制限_constr_dual = {r: v.pi for r,v in Yの制限_constr.items()} タンク利用_constr_dual = {r: v.pi for r,v in タンク利用_constr.items()} 重心_constr_dual = 重心_constr.pi return master_prob, パターン選択_constr_dual, Yの制限_constr_dual, タンク利用_constr_dual, 重心_constr_dual, # 子問題の変数定義 x = { (i, j): pulp.LpVariable(name=f 'x_{i}_{j}' , cat=pulp.LpBinary) for i in params.I for j in params.J } y = { j: pulp.LpVariable(name=f 'y_{j}' ,lowBound= 0 , upBound= None , cat=pulp.LpContinuous) for j in params.J } def solve_sub_prob (パターン選択_constr_dual, Yの制限_constr_dual, タンク利用_constr_dual, 重心_constr_dual): # 陽に部屋ごとに問題を分離して解くことが自然だが、 # 問題が明らかに分解可能な形なので、MIPソルバーが自動的に分解して求解することを期待して全ての j について # まとめた問題をソルバーに投げている。 sub_prob = pulp.LpProblem(sense=pulp.LpMaximize, name= 'Sub' ) # 目的関数 c = (params.n- 1 )/ 2 W = { j: pulp.lpSum(params.w[i] * x[i, j] for i in params.I) + y[j] for j in params.J } max_obj_部屋別 = { j: ( y[j] # 元の目的関数 - pulp.lpSum( タンク利用_constr_dual[i] * x[i,j] for i in params.I ) # タンク利用制約の双対変数由来 - 重心_constr_dual * W[j] * (j-c) # 重心制約の双対変数由来 - パターン選択_constr_dual[j] # パターン選択制約の双対変数由来 TODO これでいいのか? ) for j in params.J } max_obj = pulp.lpSum(max_obj_部屋別.values()) # 線形関数を補助的に定義 部屋内合計タンクサイズ = { j: pulp.lpSum(params.s[i] * x[i, j] for i in params.I) for j in params.J } # 部屋に置けるタンクのサイズの和には上限がある 部屋内合計タンクサイズ_上限_constr = { j: rename_constraint(f '部屋内合計タンクサイズ_上限_constr_{j}' , 部屋内合計タンクサイズ[j] <= params.S) for j in params.J } # 部屋のタンクに入れる水の量の上限は、置かれたタンクのサイズの和によって決まる 部屋内水量_上限_constr = { j: rename_constraint(f '部屋内水量_上限_constr_{j}' , y[j] == 部屋内合計タンクサイズ[j]) # j: rename_constraint(f'部屋内水量_上限_constr_{j}', y[j] <= 部屋内合計タンクサイズ[j]) # TODO DEV for j in params.J } # 目的関数・制約を最適化問題に追加 sub_prob.setObjective(max_obj) for c in itertools.chain( 部屋内合計タンクサイズ_上限_constr.values(), 部屋内水量_上限_constr.values(), ): sub_prob.addConstraint(c) sub_prob.solve(pulp.PULP_CBC_CMD(timeLimit=time_limit, msg= 0 )) # マスター問題の update for j in params.J: if max_obj_部屋別[j].value() > eps: r_ub = len (X[j]) X[j].append( pulp.LpVariable(name=f 'X_{j}_{r_ub}' , cat=pulp.LpBinary) ) Y[j].append( pulp.LpVariable(name=f 'Y_{j}_{r_ub}' , cat=pulp.LpContinuous, lowBound= 0.0 , upBound= 1.0 ) ) ss[j].append(部屋内合計タンクサイズ[j].value()) ww[j].append(pulp.lpSum(params.w[i] * x[i, j] for i in params.I).value()) for i in params.I: xi[i,j].append(x[i,j].value()) return sub_prob, max_obj_部屋別 def get_naive_problem_variable_values (): _x = { (i, j): pulp.LpAffineExpression() for i in params.I for j in params.J } for i, j, in itertools.product(params.I, params.J): for r in range ( len (X[j])): _x[i, j] += (xi[i,j][r] * X[j][r]) _y = { j: pulp.LpAffineExpression() for j in params.J } for j in params.J: for r in range ( len (X[j])): _y[j] += (ss[j][r] * Y[j][r]) _x_vals = {k: v.value() for k, v in _x.items()} _y_vals = {k: v.value() for k, v in _y.items()} return _x_vals, _y_vals, _x, _y for _i in range (max_loop): print (f ' \n iter: {_i} ----------------' ) # master_prob, タンク利用_constr_dual, 重心_constr_dual = solve_master_prob() master_prob, パターン選択_constr_dual, Yの制限_constr_dual, タンク利用_constr_dual, 重心_constr_dual = solve_master_prob() print (f ' MASTER_LP_STATUS:' ) print (f ' Result: {pulp.LpStatus[master_prob.status]}' ) print (f ' MasterLP_Obj: {master_prob.objective.value()}' ) # print(f' Duals:') # print(f' パターン選択_constr_dual {list(パターン選択_constr_dual.values())}') # # print(f' {Yの制限_constr_dual=}') # print(f' タンク利用_constr_dual {list(タンク利用_constr_dual.values())}') # print(f' 重心_constr_dual {重心_constr_dual}') # print('***') # print(master_prob) # for j in params.J: # for r in range(len(X[j])): # if X[j][r].value() > 0.0: # print(f'X[{j}][{r}] = {X[j][r].value()}') # if Y[j][r].value() > 0.0: # print(f'Y[{j}][{r}] = {Y[j][r].value()}') sub_prob, max_obj_部屋別 = solve_sub_prob(パターン選択_constr_dual, Yの制限_constr_dual, タンク利用_constr_dual, 重心_constr_dual) SubprobObj_max = max (e.value() for e in max_obj_部屋別.values()) print (f ' SUBPROB_MIP_STATUS:' ) print (f ' Result: {pulp.LpStatus[sub_prob.status]}' ) print (f ' SubprobObj: {SubprobObj_max}' ) # for j in params.J: # print(f' {j} : {max_obj_部屋別[j].value()}') # print('***') # print(sub_prob) # for i in params.I: # for j in params.J: # if x[i,j].value() > 0.0: # print(f'x[{i},{j}] = {x[i,j].value()}') # for j in params.J: # if y[j].value() > 0.0: # print(f'y[{j}] = {y[j].value()}') # 終了判定 if SubprobObj_max <= eps: break # 生成された列を使ってMIP部分問題を解き、最終的な結果とする master_prob, パターン選択_constr_dual, Yの制限_constr_dual, タンク利用_constr_dual, 重心_constr_dual = solve_master_prob() master_prob.solve(pulp.PULP_CBC_CMD(timeLimit=time_limit, msg= 0 )) print (f 'MASTER_MIP_STATUS:' ) print (f ' MasterMIP_Obj: {master_prob.objective.value()}' ) # 解の検証 x_vals, y_vals, *_ = get_naive_problem_variable_values() prob_for_validation, x_v, y_v = generate_problem(params) # ナイーブなモデルの変数を固定して、valid となるかを確認。 for k, v in x_v.items(): v.setInitialValue(x_vals[k]) for k, v in y_v.items(): v.setInitialValue(y_vals[k]) print (f ' ValidationMIP_Obj: {prob_for_validation.objective.value()}' ) print (f ' ValidationMIP_Valid: {prob_for_validation.valid(eps=eps)}' ) solve_colgen(my_params, time_limit= 5 , max_loop= 100 ) iter: 0 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: None SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 1 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 199.36507944 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 29.206349135000004 iter: 2 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 260.0 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 3 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 327.69230796 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 4 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 408.96969695999996 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 23.0303030435 iter: 5 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 456.8902041020001 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 6 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 483.91531642000007 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 23.099094506 iter: 7 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 502.4378701800001 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 8 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 532.7289676199999 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.905528775 iter: 9 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 535.1733378319999 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 10 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 566.835042106 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 11 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 589.4684529115998 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 20.0 iter: 12 ---------------- MASTER_LP_STATUS: Result: Optimal MasterLP_Obj: 600.0000004290002 SUBPROB_MIP_STATUS: Result: Optimal SubprobObj: 5.149999989795262e-07 MASTER_MIP_STATUS: MasterMIP_Obj: 578.9655172 ValidationMIP_Obj: 578.9655172 ValidationMIP_Valid: True 比較 ナイーブな方法での目的関数値が562.9、列生成法的なアプローチでは579.0ということで、一例だけの比較に意味はないですが少しだけ良い結果が得られています。 参考文献 A generic view of Dantzig–Wolfe decomposition in mixed integer programming (François Vanderbeck, Martin W.P. Savelsbergh) はじめての列生成法 (宮本裕一郎) 最適化法 (田村明久・村松正和)
アバター
Introduction こんにちは、データサイエンティストの善之です。 本記事では私が日頃使っている、データの誤りを見逃さないためのデータ探索チェックリストをご紹介します。 データを分析するにあたり、良質なデータを用いることが非常に重要です。誤ったデータを分析しても誤った結果しか出てきません。 しかし、実際に業務で扱うデータは誤ったデータが含まれていることが多くあります。 特にデータをクライアントから受領する場合には、クライアントのデータ抽出過程にミスがあったり、 そもそもクライアントのデータベースに格納されているデータが何かしらの誤りを含んでいたりします。 そのような誤りにプロジェクトの途中で気づくと、それまでの分析が全てやり直しになってしまうケースも考えられます。 したがって、プロジェクトの初期の時点でデータの誤りを見抜いておくことが大事になってきます。 とはいえ、なんとなくデータを探索するだけではデータの誤りを見落としてしまう可能性が高いです。 そこで私がオススメするのは、確認すべき項目をチェックリストにして、データ分析を始める前に漏れなくチェックする方法です。 本記事では、私が日頃使っているチェックリストをご紹介したいと思います。 目次 全データカラム共通 欠損がないか 数値データカラム ドメイン知識の範囲と異なるデータがないか ヒストグラムで不自然な値がないか カテゴリデータカラム ドメイン知識と異なるデータが含まれていないか 出現頻度でドメイン知識と異なる傾向を示す変数がないか データカラム間の関係 相関がドメイン知識と一致しているか 前提条件であるデータどうしの関係式が成立しているか 時間で集計 明らかに他と異なる傾向がないか ドメイン知識と異なる挙動をしていないか 空間で集計 他の場所と明らかに異なる傾向を示す場所がないか ドメイン知識と異なる挙動をしている場所がないか 関連データの活用 データ結合ができるか 別途提供されている集計済データや一般公開されているデータと値が一致するか まとめ 全データカラム共通 まずは、データに含まれるカラム1つ1つを確認していきます。 後ほどカテゴリデータと数値データそれぞれに固有の確認事項をご紹介しますが、 その前にどちらのデータにも共通する確認事項として、欠損の確認をご紹介します。 欠損がないか 基本的な内容ですが、欠損がないかを確認します。もし欠損データがある場合は、次にその項目の生データを確認していきます。 生データを確認すると、欠損があっても問題がなかったと判明するかもしれません。そのような場合はスルーしても大丈夫です。(売上が0の商品データは購買顧客に関する詳細情報が欠損しているなど) それ以外の場合は、念のためデータ提供元に原因を問い合わせます。原因が判明すれば欠損を無くせる場合もあります。 数値データカラム 次に、数値データカラムの確認事項をご紹介していきます。 ドメイン知識の範囲と異なるデータがないか まずは数値データを持つカラム1つ1つについて最大値と最小値を確認し、ドメイン知識の範囲内に入っているかを確認します。 気温のデータに300が含まれるなどが、ドメイン知識から外れている例です。データが誤っているか、単位が摂氏ではない可能性があるので、データ提供元に確認します。 (実際に過去に私が扱った案件でこのような例がありました。その際は単位が摂氏ではなくケルビンでした) 他にも、湿度が100%を超えている、日射量がマイナスの値、などもドメイン知識から考えて不自然なデータの例として挙げられます。 ヒストグラムで不自然な値がないか 続いて、ヒストグラムを描いてみます。 そのうえで、極端に大きい/小さいデータがないか確認します。ヒストグラムの分布から大きく外れているようなデータです。 例として、商品の価格データの分布を考えます。 図のように、他の価格に比べ極端に価格の高い商品があれば、念のため生データを確認します。 本当に高価な商品であれば問題ないですが、データが誤っている可能性もあります。 また、極端に大きくなくても、分布の中で特異的な点があればそのデータを確認します。 たとえば、商品価格中で30,000円~31,000円のデータが極端に頻度が大きいとします。 その場合、何か特殊なケースにおいて一律30,000円などの同じ数値を入力している可能性も考えられます。 カテゴリデータカラム ここでは、カテゴリデータカラムの確認事項をご紹介します。 ドメイン知識と異なるデータが含まれていないか まずはユニークな項目を全て目視確認します。(項目数が多くチェックしきれない場合は、一部だけでも見てみます) その際に想定していないデータがないかを確認します。 たとえば、曜日データに「月火水木木土日」の7種類以外のデータがないかなどです。(例えば、同じ土曜日でも表記揺れで「土曜」と「土曜日」に分かれているかもしれないです) 出現頻度でドメイン知識と異なる傾向を示す変数がないか データに含まれる各カテゴリについて出現頻度のヒストグラムを描き、事前に把握しているドメイン知識と乖離がないかを確認します。 たとえば、データカラムに曜日がある場合を考えます。毎日取得しているデータであれば各曜日が均等に現れるはずです。 ここで分布が均等でないならば、データ取得に何らかの不備がある可能性があります。 データカラム間の関係 ここまではデータカラムの1つ1つを確認してきましたが、ここではデータカラムどうしの関係性を確認します。 相関がドメイン知識と一致しているか ドメイン知識的に、相関があると分かっている項目については2軸でプロットして相関を確認します。 例えば、小売のとある店舗の全商品の売上合計個数と売上高はおよそ比例関係にあるはずです。 この傾向から極端に外れている点があれば、生データを確認した方がよいでしょう。 生データを確認のうえ、外れている理由が推察できないようであればデータ提供元に問い合わせます。 前提条件であるデータどうしの関係式が成立しているか データどうしに前提条件の関係式がある場合は、その関係式が成立しているか確認します。 たとえば、「利益=売上-原価」などです。ここが崩れている場合、いずれかのデータが誤っている可能性が高いです。 他にも、「仕入れ数=売上数+廃棄数」などもデータどうしの関係式の例として挙げられます。 時間で集計 ここでは、データを集計してから確認する手法を紹介します。 まずは、時間で集計する手法です。 明らかに他と異なる傾向がないか 時間情報が含まれるデータの場合は、数値を時系列でプロットします。 たとえば、商品の売上高を日付ごとにプロットした際、ある日付だけ売上が極端に少ないとします。 もし完全にゼロの場合は、その日が休日だった可能性もあります。しかしゼロではなく少ない場合は、データの取得が不完全であった可能性があります。 ドメイン知識と異なる挙動をしていないか プロットでのもう1つの確認事項として、ドメイン知識と異なる挙動をしていないかも確認します。 たとえばある設備を毎年少しずつ増設していると事前に把握していたとします。 にも関わらず、時系列でプロットした際に途中で急に設備数が増えている箇所があれば要注意です。 その年にたまたま多く増設された可能性もありますが、その時点を境にデータの取得方法が変わっている可能性もあります。 空間で集計 他の場所と明らかに異なる傾向を示す場所がないか 位置情報を持つデータの場合は、数値をヒートマップで地理的空間にプロットしてみます。 ここでは例として電力消費量を考えます。 電力消費量を地図上にヒートマップでプロットした際に、周辺には消費電力があるのに、ある領域だけ電力消費が極端に少ないとします。 山間部であれば問題ないのですが、もし都市部にも関わらずそのようなことが起きている場合は、その地域のデータの一部抽出漏れが疑われます。 ドメイン知識と異なる挙動をしている場所がないか 先ほどと同じく、プロット上でドメイン知識と異なる挙動をしている場所がないか確認します。 こちらも電力消費量を例に考えます。 たとえば海の上で電力消費がある、山間部の電力消費が都市部よりも多い、などの場合はデータの位置情報がずれている可能性が高いです。 関連データの活用 データ結合ができるか データセットは1種類のみ提供されるわけではなく、複数種類のデータセットをもとに必要なデータを抽出して、最終的な分析用データを作成していく場合が多いかと思います。 そのようなデータを扱う場合は、まずデータ同士の結合が問題なく可能かを確認します。 結合できない項目があれば、データが漏れている可能性が高いです。 別途提供されている集計済データや一般公開されているデータと値が一致するか 可能な場合のみですが、念のため別途提供されている集計済データや一般公開されているデータと値が一致するかを確認します。 例えば、商品売上の商品ごとの生データとともに、全商品について売上を合算した後のサマリーデータも同時に受領している場合を考えます。 この場合、自分自身で商品売上の生データを合計した結果と、提供されたサマリーデータを比較します。 意外とずれる場合も多いかと思います。(生データの方に重複があるなどが考えられます) また、一般公開されているデータとの照合も必要に応じて行います。 例えば、商品売上データの合計と公開されている有価証券報告書に記載の売上高に乖離がないかを確認する方法も考えられます。 まとめ いかがだったでしょうか。 今回ご紹介した項目を調査すれば、データの誤りにあらかじめ気付ける可能性が高まるかと思います。 参考にしていただけると嬉しいです。
アバター
こんにちは!Insight Edgeでコンサルタントとして働いている塩見です。 2月7日から2月9日の3日間、東京ビッグサイトにて、日本国内のDXソリューションが集う国内最大規模の展示会「第3回 DX EXPO」が開催されました。今回、私がこちらの展示会に参加してきましたので、会場の様子や注目を集めていたDXソリューションをご紹介したいと思います。 今後、DX EXPOへの参加を検討している方の参考になれば幸いです。 DX EXPOとは? 展示会場(3つの専門エリア) バックオフィスDX展 マーケティング・営業DX展 店舗・施設DX展 DXセミナー 注目を集めていたDXソリューション ナビ搭載業務自動化RPA_PKシリーズ(株式会社キーエンス) CLOVA OCR(LINE株式会社) ChatGPT×AIチャットボットAlli(Allganize, Inc.) まとめ DX EXPOとは? DX EXPOとは、業務効率化・働き方改革・経営基盤強化を実現するためのDXソリューションが一堂に集う、日本最大級の展示会です。後援にデジタル庁・総務省・東京都・大阪府等が入っています。 今回で開催が3回目となったDX EXPOですが、本開催は「経営支援 EXPO」と「ニューノーマルワークスタイル EXPO」との同時開催となっており、東京ビッグサイトにて、多くのDXソリューションの展示が行われました。  図1. DX EXPO 開催会場(東京ビッグサイト) DXソリューションは約750種類出展されており、それらの多くを展示会場で実際に触って体験することが出来ます。また、富士通やサッポロホールディングス等、様々な業界トップ企業が講演するDXセミナーも無料で受講することが可能です。 展示会場(3つの専門エリア) 私は開場初日、展示会に参加してきました。午前中はあまり来場者がおらず、お昼過ぎから人が増えてきて、夕方にはかなり混雑していたように思います。 図2. 開場直後の展示会場内部の様子。まだ来場者は殆どいません。展示側も慌ただしく準備をしていました。 混雑時の混み具合については、DX EXPOの公式ホームページが参考になると思いますので、気になる方はこちらをご確認いただくと良いかもしれません。 https://www.dx-expo.jp/spring/report 展示会場には、展示ブースの他、2箇所のセミナー会場とテレワークラウンジが設置されています。テレワークラウンジではWi-Fiが使える為、リモートワークを行う多くの方が使用されていました。何度か覗きましたが、常に満席状態だった為、使用する際は早めに席をとっておいた方が良さそうです。 さて、今回のDX EXPOでは、展示会場が3つの専門エリアに分けられています。それぞれのエリアにて、各テーマに特化したソリューションを体験することが出来ます。ここからは各エリアの様子を簡単にご紹介します。 バックオフィスDX展 まず、こちらのエリアでは、バックオフィス業務に関連するDXソリューションの展示が行われていました。具体的には、 人事・労務システム DX人材育成・採用・研修 PRA・チャットボット といったソリューションが紹介されていました。 本エリアの広さは会場の3〜4割を占めるぐらいに広く、大規模なブースエリアを設けている会社が多くありました。また、ブースに訪れる来場者数も他エリアと比べて、多かった印象があります。 マーケティング・営業DX展 次に、こちらのエリアでは、マーケティングや営業等の業務に関連するDXソリューションの展示が行われました。具体的には、 SNSマーケティング MAツール データ分析・BIツール といったソリューションが紹介されていました。 本エリアもかなり広く、バックオフィスDX展と同等規模の広さでした。バックオフィス展と比べると、中小規模のブースが多く、来場者数は若干少なかったと思います。 店舗・施設DX展 最後に、こちらのエリアでは、店舗や施設運営等の業務に関連するDXソリューションの展示が行われました。具体的には、 店舗運営ツール キャッシュレス決済 CX向上ツール といったソリューションが紹介されていました。 本エリアだけ、かなり狭かったです。出展数も10に満たないぐらい少なかったと思います。店舗や施設運営に関するDXソリューション自体、数が少なく、展示する企業があまりいなかったのかもしれません。 DXセミナー 本展示会では、大手企業が開催するDXセミナーを無償で受講することが出来ました。セミナー会場は2箇所あり、大きい方の会場では500人近い座席スペースが用意されていました。 図3. 大きい方のセミナー会場。セミナー中は撮影不可とのことだったので、セミナー開始前の様子です。 図4. セミナー登壇者。大手企業の役員の方々がDXに関連する内容を話してくれます。 私は2月7日に実施された、富士通・福田さんとサッポロホールディングス・佐藤さんのセミナーを受講させていただきました。両セミナーともに、座席スペースが満席となり、追加のパイプ椅子が用意されるなど、非常に人気だったようです。 福田さんは、富士通が取り組むDX「フジトラ」の内容を話されました。日本でDXが進まない課題を経路依存性(過去の経緯や歴史によって決められた仕組みや出来事にしばられる現象)として捉え、それを解決する為の富士通の活動内容を熱く説明されていました。 また、佐藤さんはサッポログループ全体で進めている「グループ全社員DX人材化」の具体的な取り組みに関するお話をされました。約6,000人規模のグループ社員に対し、段階的に教育を行い、実際にプロジェクト経験を積ませている等、勉強になる点が多かったです。 他のセミナーも聞きたかったのですが、今回は都合が合わず、残念ながら諦めました。なお、セミナー参加には事前の申し込みが必要ですが、座席に余りがあれば当日参加も可能とのこと。立ち見している人もいたので、急遽見たいセミナーがあった場合、結構融通は効くのかもしれません。 注目を集めていたDXソリューション 今回の展示会では750以上のDXソリューションが展示されていました。その中でも、多くの人の注目を集めていたソリューションをいくつかご紹介します。私が見たタイミングで人が多かっただけかもしれないので、あくまでご参考までに捉えていただけますと幸いです。 ナビ搭載業務自動化RPA_PKシリーズ(株式会社キーエンス) 図5. 株式会社キーエンスのブース 人が少ない初日午前でも人だかりが出来ていたのが、キーエンスのブースでした。何を展示しているんだろうと、ふと覗いたら、RPA製品であるPKシリーズと呼ばれる商品を紹介していました。本当に何でも作っていますね。 ブースにいた方にお話を伺ったところ、こちらの商品は事業開始して1年ほどらしく、従来のRPA以上に直感的な操作で簡単に作業シナリオが作成できるようです。また、Excelのデータを使った作業自動化も得意で、これら自動処理を安定的に実行できる点も、他RPAより優れた点とのことでした。 PKシリーズの詳細はこちら。 https://www.keyence.co.jp/ss/products/software/rk/006/ CLOVA OCR(LINE株式会社) 図6. LINE株式会社のブース LINEのブースも人が多かったです。セミナー会場近くだったということもあると思いますが、LINEがOCRサービスを展開していることを知らない方が多くいたのかもしれません。 ブースでは、OCRの認識精度を競う世界的なコンペで世界No.1を獲得している点や、様々な画像・PDFデータを認識可能な点を社員の方がアピールされていました。 CLOVA OCRの詳細はこちら。 https://clova.line.me/clova-ocr/ ChatGPT×AIチャットボットAlli(Allganize, Inc.) 図7. Allganize, Inc. のブース AIベースのナレッジマネジメント事業を展開するAllganize(オルガナイズ)。こちらのブースは展示会の中でも一番小さいサイズのものでしたが、その中でも人が多く集まっていました。 ブースでは、主にチャットボットに関連するサービスの展示が行われていました。チャットボットの展示は複数のブースで行われていましたが、Allganizeの展示では、ChatGPTと自社チャットボットサービスの連携をアピールしていました。ChatGPTの訴求だけで集客力が強まったと思うのですが、ChatGPTが流行って間もない中、こういった展示会までに用意できるスピード感はすごいと思いました。 ブースにいた方に話を伺ったところ、実はChatGPTを活用した機能の商用リリースはされておらず、機能の詳細もお伺いできませんでした。今後ChatGPTと連携した機能をリリースしていく予定とのこと。具体的にどんなサービスが展開されるのか楽しみですね。 AIチャットボットAlliの詳細はこちら。 https://alli.allganize.ai/ まとめ 本記事では、2月に東京ビッグサイトで開催された第3回 DX EXPOについてご紹介しました。想像していた以上に人が多く、また展示側も積極的に自社アピールをしていて、かなり賑やかでした。 今回の展示会では、RPAやダッシュボード系のソリューションが多く展示されており、来場者が多く集まっていたブースも、これらのソリューションを扱っているところでした。日本のDXトレンドとしては、業務自動化や現状の可視化といったニーズが、売り手側・買い手側ともに強いんだなと感じました。 一方で、これらのソリューションは部分的な業務改善には寄与しやすいと思うのですが、企業全体の変革といった大きな改善には繋がりにくいようにも思います。富士通・福田さんがセミナーで話していた通り、日本国内でDX推進を進める上では、経路依存性の課題を解決するような取り組みを進めた方が良いのかもしれません。 最後に、今年はあと2回、DX EXPOが開催される予定です。3月7日〜9日に大阪ATCホールで、7月11日〜13日に東京ビッグサイトで展示会が開かれます。無料で参加することが出来ますので、DXに興味がある方は、ご参加いただくと良いかと思います。
アバター
 こんにちは!Insight EdgeのData Scientistの石倉です。私は以前、地球物理学を専攻していて偏微分方程式を扱っていたのですが、最近NeurIPSやその他学会などで見られるPhysics-informed neural networks(以下、PINNs)の"Physics"に思わずアンテナが反応してしまい、色々と文献を調査してみました。そこで今回は、簡単に解析解の分かる微分方程式を用いてPINNsをTensorflowで実装してみたのでご紹介したいと思います! Physics-informed neural networks とは 減衰振動 微分方程式 Finite difference method Data-driven neural networks Physics-informed neural networks まとめ FD vs. NeuralNetwork DDNNs vs. PINNs 論文紹介 感想 参考文献 参考:実装コード Physics-informed neural networks とは  科学や工学における多くの現象は偏微分方程式(以下、PDE)で記述され、複雑な現象を再現するために数値計算法を用いて様々な分野でシミュレーションが行われています。解析解を直接得られる一部の微分方程式を除いて、多くの方程式は有限差分法や有限要素法などの数値計算法を用いて近似的に解く必要があり、連続関数を離散化してグリッドに分割し膨大な数の繰り返し計算が必要となります。近年ディープラーニング技術を用いて、PDEを解く有効な手段としてPINNsが注目されており、MLの有名どころの学会に留まらず物理学系の学会などでも論文が多数見受けられ研究が活発に行われています。  PINNsの目的は一言で表すと、観測対象のPDEの解を観測データと物理モデル両者を考慮して近似的に求めることです。PINNs内のNeural Network(以下、NN)の構造はMLPで構成されており、NNのもつUniversal Approximation Theoremでは、”有限個のユニットを持つ一層の隠れ層で構成されるfeed-forward networkは、任意の連続関数を近似することが出来る”ということが保証されています。また、Activation functionがSquashing functionの時はMLPにおいてもこの定理が保証されています。PINNsはこの定理の性質を用いてPDEの近似関数を求めていくことになります。  PINNsの火付け役となった論文はRaissi et al.(2019)で、流体力学で用いられるNavier–Stokes equationに対してPINNsを適用した論文となっています。本稿ではいきなりNavier-Stokesのような非線形偏微分方程式を扱うと難易度が高いので、解析解の分かる簡単なマス-バネ-ダンパー系の減衰振動の微分方程式を扱うことにします。また、数値計算法とPINNsを比較したいと思います。 減衰振動 微分方程式  下図のようにマスの質量を , バネ定数を , ダンパーの減衰係数を とするとマス-バネ-ダンパー系の運動方程式は式 のように記述できます。また初期条件については、時刻 の時に , 速度 とした時の解析解は式 のようになります。 初期条件: 解析解: マスーバネーダンパー系 Finite difference method  今回は、マス-バネ-ダンパー系の運動方程式、式 を数値計算法の1つである有限差分法を用いてシミュレーションを作成しました。運動方程式には時刻 による微分項が含まれるので、テーラー展開の一次精度(オイラー法)を用いて離散化を行います。式 中の は十分に小さいものとします。 ここで、簡単に を と変化させて、シミュレーション結果と解析解と比較したものが下図になります。 が大きくなると誤差が増大して、 の時には発散している様子がわかります。これはテーラー展開を行なって1次精度で近似して2次以降の項を打ち切ったことによる誤差が に応じて大きくなっているためです。連続関数を離散化している以上、数値計算による誤差は避けられませんが、誤差を無視できる程度まで を十分に小さくしようとすると、トレードオフで計算量は増大します。 時間間隔を変えた時の数値計算誤差の様子 Data-driven neural networks  Raissi et al. (2019)の論文内でもData driven neural networks(以下、DDNNs)についての記載があったので、本稿でも簡単に記載します。Data drivenなので文字通り、データのみを用いて観測データと予測データの誤差を最小化するようにNNを学習していくものです。NNの構造はMLPです。下のGIF画像を見ての通り、当然ながらデータの存在しない区間では近似は不可能ですが、データの観測されている区間においては非常によく近似できていることがわかります。 DDNNsの学習の様子 (GIF画像) Physics-informed neural networks  本題のPINNsです。NNの構造はDDNNsと同じですがLossの構成が異なり、PINNsでは観測データと予測データの誤差、境界条件誤差、微分方程式誤差で構成されます(今回は境界条件誤差はありません。)。この微分方程式誤差がPhysics-informedと呼ばれる所以ですが、NNを学習する際に用いられるバックプロパゲーションを用いて勾配を計算することが可能(自動微分)なので、この機能を用いて方程式中の微分項を学習できます。以下に、Tensorflowの例を記していますが、シミュレーターを作成することと比較すると、実装が非常に簡単というのもPINNsの特徴かと思います。今回は時間微分項しかないので、シミュレーションも簡単ですが、空間微分を含む微分方程式を扱うとシミュレーターの実装は複雑になります。 def train_step (self, t_data, x_data, t_pinn, c, k): with tf.GradientTape() as tape_total: tape_total.watch(self._model.trainable_variables) x_pred = self._model(t_data) loss1 = self._loss_fn(x_pred, x_data) #観測データ誤差 loss1 = tf.cast(loss1, dtype=tf.float32) with tf.GradientTape() as tape2: tape2.watch(t_pinn) with tf.GradientTape() as tape1: tape1.watch(t_pinn) x_pred_pinn = self._model(t_pinn) dx_dt = tape1.gradient(x_pred_pinn, t_pinn) #1階微分 dx_dt2 = tape2.gradient(dx_dt, t_pinn) #2階微分 dx_dt = tf.cast(dx_dt, dtype=tf.float32) dx_dt2 = tf.cast(dx_dt2, dtype=tf.float32) x_pred_pinn = tf.cast(x_pred_pinn, dtype=tf.float32) loss_physics = dx_dt2 + c * dx_dt + k * x_pred_pinn loss2 = 5.0e-4 * self._loss_fn(loss_physics, tf.zeros_like(loss_physics)) loss2 = tf.cast(loss2, dtype=tf.float32) loss = loss1 + loss2 # Lossの合算 self._optimizer.minimize(loss, self._model.trainable_variables, tape=tape_total) self._loss_values.append(loss) return self 今回、あえて試験的に観測データをスパースに5点ランダムに選び、観測データLossに組み込みました。また、Physics lossの学習データポイントはxで示しており、今回は1.0sまでの区間において学習することとしました。勿論この学習区間は任意に設定可能ですが、学習区間を広く取りすぎると収束までにより多くの時間を要したり、扱う微分方程式によっては上手く学習できないこともあることが論文などでも報告されています。 観測データとPINNsの学習領域 下のGIF画像に学習の過程を記していますが、観測データがある区間だけでなく観測データの欠損区間及び観測されていない未来の区間でも微分方程式誤差の最小化に伴って、上手く学習できている様子が見て取れるかと思います。 PINNsの学習の様子 (GIF画像) まとめ  減衰振動の微分方程式を用いてシミュレーション/DDNNs/PINNsを作ってみたので、それぞれの精度について検証してみたいと思います。精度検証については、解析解との平均絶対値誤差で評価しています。 FD (dt=0.005) FD (dt=0.001) FD (dt=5.0e-5) DDNNs (データ区間に限る) PINNs 0.130 0.0203 9.63e-4 3.32e-3 9.64e-4 FD vs. NeuralNetwork  上記表の結果からDDNNsやPINNsほどの精度をシミュレーションで追求しようとすると時間間隔をかなり小さく取る必要があり、その分計算回数をかなり多く要します。今回は簡単な例を用いたシミュレーションなので計算負荷についての議論は難しいです。しかし一般的に、今回の様な陽的数値解法でも大きなモデルをシミュレーションする場合、時間間隔を小さく設計するとかなりの計算負荷がかかることを予想されます。また、陰的数値解法では陽的解法に比べて数値計算的に安定しやすく、時間間隔を比較的粗く設計しても精度が落ちにくいという点がありますが、時間間隔毎に逆行列を計算する必要があるので同様に計算負荷が大きいという問題もあります。PINNsについては、複雑な微分方程式の近似関数や学習領域を広範囲に設計すると、その分レイヤー数やユニット数を増やす必要があり収束に時間を要すると予想されるので、一概にどちらの手法が計算コストを低くできるかというのは難しいと感じています。  PINNsのメリットとして、観測現象の微分方程式を離散化していないので、シミュレーションの様に空間グリッド間隔や時間間隔に縛られず、任意の空間及び時間の観測データを制約条件として利用できる点、上手く収束すれば学習領域内においては任意の空間及び時間の挙動を把握できる点が挙げられると考えています。  PINNsのデメリットとして、解析解の分からないような複雑な現象を学習する際の収束をどのように判断するかが非常に難しいと感じています。今回の例で試験的に観測データを極端に少なくして試行しましたが、学習が上手くいかなかった際は振幅誤差だけでなく、位相ズレも見られました。 DDNNs vs. PINNs  DDNNsは、物理学的な要素は何も含まれていないので、良い近似関数を求めるためには多分に観測データが必要になるという点が挙げられます。一方、PINNsではPhysics lossが組み込まれているので、DDNNsに比べて観測データ点が少なくとも学習が可能である点、観測データの無い未来の挙動も学習可能である点は興味深いと感じています。 論文紹介  様々な分野においてPINNsの適用が見られますが、現時点で扱う微分方程式によってそれぞれ適切なOptimizerやActivation function、Lossの設計(L1 or L2 norm)、学習手法が異なり、画一的に万能な手法はなさそうです。先述の火付け役となった論文Raissi et al.(2019)と同様にKrishnapriyan et al.(2021)では流体力学系の偏微分方程式に対して様々な検証を行なっています。例えば、移流方程式においては通常のやり方では時間方向の学習を上手くできないので、Curriculum learningという簡単な偏微分方程式から学習を始めて徐々に目的の偏微分方程式へと近づけていく手法を取ったり、時間方向の学習領域をある程度区切って学習を行なう手法を用いたりと様々な工夫を行なっています。また一般的にNNの学習でOptimizerはAdamを用いられることが多いですが、この論文ではAdamは機能せずL-BFGSを用いたようです。Activation functionはhyperbolic tangentを使っています。 Krishnapriyan et al.(2021)より引用  Moseley et al.(2020)では、波動方程式に対して適用を行なっており、従来のシミュレーションでよく用いられるFDとDDNNs、PINNsの比較などを行なっています。この論文では、OptimizerにはAdamを、LossはL1 norm、Activation functionはsoftplus、学習方法としては、初期はDDNNsで学習を進めていき、途中からPhysics lossを組み込むといった手法を取っています。Activation functionについては連続関数を近似することが目的なので、PINNsではReluのような微分がステップ関数となるようなものはあまり用いられていない印象です。 Moseley et al.(2020)より引用  Huang et al.(2022)はPINNsの調査論文で、参考文献が200本以上掲載されており、体系立ててまとめられているので、PINNsの基礎から最近のPINNsの動向まで幅広く知りたい方にはおすすめかと思います。 感想  今回実際に作ってみた感想として、純粋に実装のみの観点からお話しすると、TensorflowやPytorchのおかげでNNの構築、自動微分による勾配計算が非常に容易である点は大きく、空間微分と時間微分を含む微分方程式のシミュレーターを作成することに比べると作業量もかなり少ないと感じました。また、実際に試行してみてPINNsの面白さを実感できた点はよかったです。末尾に実装コードを掲載していますのでご参考まで。論文紹介でも述べたとおり、現状様々な学習手法が試されており、解析解や従来手法との比較の論文が多い印象ですが、今後さらに研究が進んでいくことを期待して引き続き動向を追っていきたいと思います! 参考文献 A. Krishnapriyan, A. Gholami, S. Zhe, R. Kirby and M. Mahoney, "Characterizing possible failure modes in physics-informed neural networks" 35th Conference on Neural Information Processing Systems (NeurIPS 2021). B. Moseley, A. Markham and T. Nissen-Meyer, "Solving the wave equation with physics-informed deep learning" M. Raissi, P. Perdikaris, and G. E. Karniadakis, “Physics-informed neural networks: A deep learning framework for solving forward and inverse problems involving nonlinear partial differential equations,” Journal of Computational Physics, vol. 378, pp. 686–707, 2019. S. Huang, W. Feng, C. Tang, Z. He, C. Yu, and J. Lv, "Partial Differential Equations Meet Deep Neural Networks: A Survey" 参考:実装コード 動作確認済みですが、グラフ描画については省略しています。以下ご参考まで。 import numpy as np import tensorflow as tf import matplotlib.pyplot as plt def FDM (init_x, init_v, init_t, gamma, omega ,dt, T): ''' init_x :マスの初期位置 init_v :マスの初期速度 init_t :マスの初期時刻 gamma :ダンパーの減衰係数 / (2.0 * マスの質量) -> マス質量は1.0と仮定 omega :周波数 ''' # parameter x = init_x v = init_v t = init_t g, w0 = gamma, omega num_iter = int (T/dt) alpha = np.arctan(- 1 *g/np.sqrt(w0** 2 - g** 2 )) a = np.sqrt(w0** 2 * x** 2 / (w0** 2 - g** 2 )) # data array t_array = [] x_array = [] v_array = [] x_analytical_array = [] diff_array = [] # time step loop for i in range (num_iter): fx = v fv = - 1 *w0** 2 * x - 2 *g * v x = x + dt * fx v = v + dt * fv t = t + dt x_a = a * np.exp(- 1 *g * t) * np.cos(np.sqrt(w0** 2 - g** 2 ) * t + alpha) diff = x_a - x t_array.append(t) x_array.append(x) x_analytical_array.append(x_a) v_array.append(v) diff_array.append(diff) return t_array, x_array, v_array, x_analytical_array, diff_array def analytical_solution (g, w0, t): ''' g :ダンパーの減衰係数 / (2.0 * マスの質量) -> マス質量は1.0と仮定 w0 :周波数 t :tf.linespace ''' assert g <= w0 w = np.sqrt(w0** 2 -g** 2 ) phi = np.arctan(-g/w) A = 1 /( 2 *np.cos(phi)) cos = tf.math.cos(phi+w*t) sin = tf.math.sin(phi+w*t) exp = tf.math.exp(-g*t) x = exp* 2 *A*cos return x def MLP (n_input, n_output, n_neuron, n_layer, act_fn= 'tanh' ): tf.random.set_seed( 1234 ) model = tf.keras.Sequential([ tf.keras.layers.Dense( units=n_neuron, activation=act_fn, kernel_initializer=tf.keras.initializers.GlorotNormal(), input_shape=(n_input,), name= 'H1' ) ]) for i in range (n_layer- 1 ): model.add( tf.keras.layers.Dense( units=n_neuron, activation=act_fn, kernel_initializer=tf.keras.initializers.GlorotNormal(), name= 'H{}' .format( str (i+ 2 )) )) model.add( tf.keras.layers.Dense( units=n_output, name= 'output' )) return model class EarlyStopping : def __init__ (self, patience= 10 , verbose= 0 ): ''' Parameters: patience(int): 監視するエポック数(デフォルトは10) verbose(int): 早期終了の出力フラグ 出力(1),出力しない(0) ''' self.epoch = 0 # 監視中のエポック数のカウンターを初期 self.pre_loss = float ( 'inf' ) # 比較対象の損失を無限大'inf'で初期化 self.patience = patience # 監視対象のエポック数をパラメーターで初期化 self.verbose = verbose # 早期終了メッセージの出力フラグをパラメーターで初期化 def __call__ (self, current_loss): ''' Parameters: current_loss(float): 1エポック終了後の検証データの損失 Return: True:監視回数の上限までに前エポックの損失を超えた場合 False:監視回数の上限までに前エポックの損失を超えない場合 ''' if self.pre_loss < current_loss: # 前エポックの損失より大きくなった場合 self.epoch += 1 # カウンターを1増やす if self.epoch > self.patience: # 監視回数の上限に達した場合 if self.verbose: # 早期終了のフラグが1の場合 print ( 'early stopping' ) return True # 学習を終了するTrueを返す else : # 前エポックの損失以下の場合 self.epoch = 0 # カウンターを0に戻す self.pre_loss = current_loss # 損失の値を更新す return False class DataDrivenNNs (): def __init__ (self, n_input, n_output, n_neuron, n_layer, epochs, act_fn= 'tanh' ): ''' n_input : インプット数 n_output : アウトプット数 n_neuron : 隠れ層のユニット数 n_layer : 隠れ層の層数 act_fn : 活性化関数 epochs : エポック数 ''' self.n_input = n_input self.n_output = n_output self.n_neuron = n_neuron self.n_layer = n_layer self.epochs = epochs self.act_fn = act_fn def build (self, optimizer, loss_fn, early_stopping): self._model = MLP(self.n_input, self.n_output, self.n_neuron, self.n_layer, self.act_fn) self._optimizer = optimizer self._loss_fn = loss_fn self._early_stopping = early_stopping return self def train_step (self, t_data, x_data): with tf.GradientTape() as tape: x_pred = self._model(t_data) loss = self._loss_fn(x_pred,x_data) self._gradients = tape.gradient(loss,self._model.trainable_variables) self._optimizer.apply_gradients( zip (self._gradients, self._model.trainable_variables)) self._loss_values.append(loss) return self def train (self, t, x, t_data, x_data): self._loss_values = [] for i in range (self.epochs): self.train_step(t_data, x_data) if self._early_stopping(self._loss_values[- 1 ]): break class PhysicsInformedNNs (): def __init__ (self, n_input, n_output, n_neuron, n_layer, epochs, act_fn= 'tanh' ): ''' n_input : インプット数 n_output : アウトプット数 n_neuron : 隠れ層のユニット数 n_layer : 隠れ層の層数 act_fn : 活性化関数 epochs : エポック数 ''' self.n_input = n_input self.n_output = n_output self.n_neuron = n_neuron self.n_layer = n_layer self.epochs = epochs self.act_fn = act_fn def build (self, optimizer, loss_fn, early_stopping): self._model = MLP(self.n_input, self.n_output, self.n_neuron, self.n_layer, self.act_fn) self._optimizer = optimizer self._loss_fn = loss_fn self._early_stopping = early_stopping return self def train_step (self, t_data, x_data, t_pinn, c, k): with tf.GradientTape() as tape_total: tape_total.watch(self._model.trainable_variables) x_pred = self._model(t_data) loss1 = self._loss_fn(x_pred, x_data) loss1 = tf.cast(loss1, dtype=tf.float32) with tf.GradientTape() as tape2: tape2.watch(t_pinn) with tf.GradientTape() as tape1: tape1.watch(t_pinn) x_pred_pinn = self._model(t_pinn) dx_dt = tape1.gradient(x_pred_pinn, t_pinn) dx_dt2 = tape2.gradient(dx_dt, t_pinn) dx_dt = tf.cast(dx_dt, dtype=tf.float32) dx_dt2 = tf.cast(dx_dt2, dtype=tf.float32) x_pred_pinn = tf.cast(x_pred_pinn, dtype=tf.float32) loss_physics = dx_dt2 + c * dx_dt + k * x_pred_pinn loss2 = 5.0e-4 * self._loss_fn(loss_physics, tf.zeros_like(loss_physics)) loss2 = tf.cast(loss2, dtype=tf.float32) loss = loss1 + loss2 self._optimizer.minimize(loss, self._model.trainable_variables, tape=tape_total) self._loss_values.append(loss) return self def train (self, t, x, t_data, x_data, t_pinn, c, k): self._loss_values = [] for i in range (self.epochs): self.train_step(t_data, x_data, t_pinn, c, k) if self._early_stopping(self._loss_values[- 1 ]): break if __name__ == "__main__" : ################## Finite difference method ################## _,_,_,_,diff_00005 = FDM( 1.0 , 0.0 , 0.0 , 2.0 , 20.0 , 0.00005 , 1.0 ) gt_001,gx_001,_,gx_a,diff_001 = FDM( 1.0 , 0.0 , 0.0 , 2.0 , 20.0 , 0.001 , 1.0 ) gt_005,gx_005,_,_,diff_005 = FDM( 1.0 , 0.0 , 0.0 , 2.0 , 20.0 , 0.005 , 1.0 ) gt_01,gx_01,_,_,diff_015 = FDM( 1.0 , 0.0 , 0.0 , 2.0 , 20.0 , 0.015 , 1.0 ) ################ Data-driven neural networks ################ g, w0 = 2 , 20 c, k = 2 *g, w0** 2 t = tf.linspace( 0 , 1 , 500 ) t = tf.reshape(t,[- 1 , 1 ]) x = analytical_solution(g, w0, t) x = tf.reshape(x,[- 1 , 1 ]) # Data points datapoint_list = [i for i in range ( 0 , 300 , 20 )] t_data = tf.gather(t, datapoint_list) x_data = tf.gather(x, datapoint_list) DDNNs = DataDrivenNNs( 1 , 1 , 32 , 4 , 5000 ) optimizer = tf.keras.optimizers.Adam(learning_rate= 1e-3 ) loss_fn = tf.keras.losses.MeanSquaredError() early_stopping = EarlyStopping(patience= 200 ,verbose= 1 ) DDNNs.build(optimizer, loss_fn, early_stopping) DDNNs.train(t,x,t_data,x_data) ############## Physics-informed neural networks ############## t_pinn = tf.linspace( 0 , 1 , 30 ) t_pinn = tf.reshape(t_pinn,[- 1 , 1 ]) # Random data points random_list = [ 0 , 35 , 50 , 110 , 300 ] t_data = tf.gather(t, random_list) x_data = tf.gather(x, random_list) PINNs = PhysicsInformedNNs( 1 , 1 , 32 , 4 , 50000 ) optimizer = tf.keras.optimizers.Adam(learning_rate= 1e-3 ) loss_fn = tf.keras.losses.MeanSquaredError() early_stopping = EarlyStopping(patience= 200 ,verbose= 1 ) PINNs.build(optimizer, loss_fn, early_stopping) PINNs.train(t, x, t_data, x_data, t_pinn, c, k)
アバター
こんにちは。Lead Data Scientistの森です。 最近家で線香を焚くのにハマっています。 線香というと、お寺で焚いているような白檀や沈香の香りを思い浮かべますが、おしゃれでポップな線香のジャンルがあります。 例えば、パリのフランス紅茶専門店である、マリアージュフレール(MARIAGE FRÉRES)も、線香のラインナップを出しています。 いわゆるお寺の香りではなく、柑橘系や甘い残り香を楽しむことができます。 自分はルームフレグランスのような用途で使っています。 マリアージュフレールの "THÉ SOUS LES NUAGES"(朝霧のお茶) を焚きながら、「これがパリの香りかー(※)」と思いながら仕事をしています。 ※マリアージュフレールの線香の製造元は京都。 さて、今回はヘッドレスCMSの 「Newt(ニュート)」 というサービスを提供する、 Newt株式会社のCTO目黒さん(以下写真)にインタビューした記事となります。 創業2年目のスタートアップで、日々試行錯誤しながらサービス開発をしている生々しい話を教えてくれました。協力してくれてありがとう! 目次 堅調にやってきた2022年 資金調達して信頼感が高まった エンジニア・デザイナー中心の組織を作りたい マーケットは厳しいがヘッドレスCMSは伸びている Newtは使いやすさにこだわる ユーザーにとっての"驚き最小"を目指したい プロダクト開発はデザイナーと二人三脚で取り組む 広く学びながら突き進んでいく 可能な限り「大きく解く」 自分のエンジニア能力が低かったら自分の品質で止まっちゃう 終わりに 堅調にやってきた2022年 はじめに近況を聞きますが、2022年を改めて振り返るとどうでしたか? この1年を振り返ると、2022年3月にサービスを正式リリースして、7月に有料プランのリリースと資金調達、12月にはFormApp機能をリリースしました。 ユーザー数も順調に増えていて、1700アカウントを超えています。(2023年1月現在) 爆発的にユーザー数が伸びているわけではないけど、それなりに堅調にやってきたのかなと思ってます。 ◆資金調達して信頼感が高まった 資金調達おめでとう! 何か変わったこととかありましたか? 自分たちの給料が出せるようになりました笑 それは冗談として、はじめて資金調達して一番変わった実感があるのは、会社の信用が上がったことです。 会社のWEBサイトを作るためのサービスを提供しているので、いつクローズするかわからないサービスだと使ってもらうことに抵抗感があって、 リリース当初は、ちょっと使ってはみるけど、それで終わりということが多かったです。 今は徐々に信頼感が高まっていることで、本番サイトにも使ってもらえるようになってきています。 直接の営業活動はしていないけど、Twitterでの評価とか問い合わせ内容を見ると、信頼性が上がっているのは感じてます。 ◆エンジニア・デザイナー中心の組織を作りたい 今後、どういう領域に投資していく予定ですか? 資金調達の際に考えていたのは、サービスの機能開発やコンテンツ拡充のための採用と、マーケティングです。 まだWEBサイトに広告を出したくらいで、そこまでお金の使い方は変わってません。 採用は、エンジニアとデザイナーを募集しています。 会社のHP で募集を出しているのと、Wantedlyを使っています。 ただ、それだけだとあんまり応募が来ないので笑、 TwitterでDMしたりYOUTRUSTでスカウト打ってみる、とかの施策も考えています。 Insight Edgeでは、 ビズリーチの公募記事 とかを出したりしています。 将来的にはどれくらいの組織の規模感を目指していますか? 創業当初から話しているのは、サービスが大きくなっても、エンジニア・デザイナー中心の少人数で運営していくようなイメージでいます。 たとえば、 Ghost という記事を書いたりするプラットフォームサービスは50人くらいで運営していて、似たような将来像を持っています。 ◆マーケットは厳しいがヘッドレスCMSは伸びている ヘッドレスCMSの業界をほとんど知らないんだけど、どういう市況感ですか? 2022年5、6月くらいから、体感としてマーケット全体の状況が悪くなっているのを感じています。 特にグロース系の新興企業の株価下落とともに、スタートアップへの投資も絞られているという話をよく聞いてます。 ヘッドレスCMS業界に絞ると、国内では microCMS というシェアが大きなサービスが1社あって、2021年に資金調達していました。 海外では何社かサービスが出てきていて、ちょうど2022年に Storyblok や Contentstack が数十Mドル規模の大きめの資金調達をしていました。 海外もそこまでマーケットの状況はよくないと思うけど、しっかり資金調達できている分野だと思います。 Newtは使いやすさにこだわる ありがとうございます。ヘッドレスCMSは、これから伸びてくるフェーズなんですね。 自分は素人なので、どのヘッドレスCMSも全部一緒に見えちゃうんですが、 国内でも海外でも複数のヘッドレスCMSサービスが出てきている中で、ユーザーとしてはどういうところを比較すればいいですか? 基本は似ていて、コンテンツ管理してAPIで取得するという部分は一緒です。その上で、各社違いを出しています。 例えば、機能的にAPIの取得方法がREST APIではなくてGraphQLだったり。 おそらく世界で最も有名な Contentful は、エンタープライズ用途で機能が充実しています。 ◆ユーザーにとっての"驚き最小"を目指したい 競合が複数社いる中で、 Newt はどういう部分で差別化しようと思ってますか? 元々ヘッドレスCMSはエンジニア向けのプロダクトですが、Newtは非エンジニアでも使いやすいサービスを目指しています。 機能的には、複数のサイトを作るときにスペースが分断しないようになっているのと、 開発メンバーが増えた場合でも、ワンスペースで開発を進められるのが推しているポイントです。 12月にリリースしたForm Appも、別々のサービスを契約せずにNewt上のワンスペースで完結する、という思想で機能をリリースしました。 なるほど。各サービスの違いは使い勝手とか機能の差になっちゃうんですね。 業界特化とか、そういった観点で差別化しているようなヘッドレスCMSってありますか? たとえば、 Shopify みたいな、ECに特化したやり方とかはあるのかもしれないです。 でも、そういう業界特化のポジションを見つけることができているヘッドレスCMSはまだあんまりないのかも。 機能だけの勝負になっちゃうと、機能開発に投資した者勝ちみたいな印象で、 資金が限られている後発のスタートアップだと厳しいイメージなんですが、その辺はどう考えてますか? エンタープライズ向けのCMSは、機能が充実している代わりに使いづらくて、非エンジニアにとってはペインポイントになっています。 CMS自体はエンジニアが使うツールなんですが、実際に記事を書いたりコンテンツを作るのは非エンジニアなので、 非エンジニアにとって使いやすいことは大事だし、ニーズがあると思っています。 Newtとしては、特に、ユーザーにとっての「 驚き最小 」を意識しています。 素人目線だと、独自の機能を作って他社と差を出していくのかなと思ったんですが、 それは逆で、むしろ「驚き最小」なんだ。 例えば、他社サービスと同じことを実現するのに特殊な仕様になっていると、 それが使いやすかったらイノベーションなのかもしれないですが、実際にはそういうことはほとんど起こらないので、 サービスとしてのコンテキストを大事にしつつ、基本はセオリー通りに作ることを意識しています。 ◆プロダクト開発はデザイナーと二人三脚で取り組む そのサービス開発の思想は、どこから生まれてきたんですか? 前職で上場を経験していると思うけど、その成功体験を基にしているとか? うーん、そうではないですね。 おそらく、弊社デザイナーはサービスの使いやすさについて突き詰めて考える人なので、二人三脚で一緒に取り組んでいる中で影響を受けているのではないかという気がします。 非エンジニアが使いやすいサービスというのは、具体的にどういうことを考えているんですか? 単に見映えとかグラフィカルな部分だけではなくて、 エンジニア目線だと、機能を充実させてタブやボタンが増えていく、ということになりがちですが、 その気持ちをグッと堪えて、機能毎に優先度を付けて重要な機能に絞って表示し、重要でない機能は隠す、みたいなイメージです。 他にも、ヘッドレスCMSの競合だけでなく、 Notion など、 非エンジニアのユーザーもうまく獲得できているサービスを参考にして、デザイナーと日々議論を重ねています。 なるほど。機能を削ってしまうと不便が出るので、機能を削るのではなくて強弱をつけるということですか。 デザイナーがプロダクト開発に深く関わっているというのは、すごく新鮮なんですが、 Newtのプロダクト開発におけるデザイナーの役割についてもう少し教えてください。 弊社のデザイナーはWeb制作と、プロダクト開発(プロダクトデザイン)の経験を持っています。 特に、ヘッドレスCMSのサービスを考える上では、Web制作の実務知識も必要で、 デザイナーなんですが、通常PdMが担当するような機能の取捨選択を決める役割も担っています。 一般的にデザイナーと聞くと、Web制作とかコンテンツを作っているデザイナーのイメージが強かったんですが、 サービスのデザインについてもデザイナーという職種に分類されるんですね。 同じデザイナーという職種でも、必要なスキルセットが全く違うので不思議ですね。 そうですね。 プロダクト開発に強いデザイナーは、あんまり数が多くないのかもしれません。 Newtでも、プロダクトデザイナーとコミュニケーションデザイナーで職種を分けて募集しています。 広く学びながら突き進んでいく ◆可能な限り「大きく解く」 ここから、実務寄りの話も聞かせてください。 エンジニアがまだ1人2人しかいない中で、サービス全体を開発するっていうのは、 全てを作らなきゃいけないのは想像はできるけど、実際やってみてどうですか? やっぱり広いです。元々前職ではフロントエンドもバックエンドも書いていて、それは一緒なんですが、 さらにその下のレイヤーのSREやセキュリティ、サポートやSDKもやらなくちゃいけません。 やったことない分野に関しては、都度都度勉強しながら作っています。しかも、当然間違えられません。 めんどくさくもあるけど、新鮮だから面白くもあります。 最近伸びているベンチャーとかが、スタートアップのCTO経験者を積極的に採用している理由がわかった気がします。 スタートアップでCTOをやると、必然的に実務の中でフルスタックなことをやるので、説得力がありますね。 実際に開発を進める上で、特に気をつけてることはありますか? 可能な限り「大きく解く」 ことを意識してます。 対象機能に絞って工数をかけずに開発や修正ができるのは大事だけど、それが積み重なると全体が直しにくくなるシステムになるイメージがあり、 そもそも全体の処理が変にならないように設計することが理想です。 例えばデータの管理だと、データの入稿・保存・取得の機能があります。 データ取得のリクエストがくるときに、この機能だけを考えるのは簡単ですが、前の段階の処理との不整合が起きないように、全体のデータの流れを踏まえた上で仕様を決定します。 こうすると、軽微な機能開発なのに想定よりも規模の大きな開発になってしまうことはありますが、2倍の工数で10倍ぐらい良いシステムになれば、最終的にはコスパがよいのかなと思っています。 機能をモジュールに分けて開発する、みたいなことはよく言われていますが、そこからさらに発展してるということですか? 機能ごとのアップデートはすぐにできるけど、そもそも機能の分け方がよくない、みたいな話なのかもしれません。 そういう場合に、機能の分け方から作り直すイメージです。 ◆自分のエンジニア能力が低かったら自分の品質で止まっちゃう プロダクトマネージャーのようなマネージャーが気にすることと、CTOとして気にすることって違いはありますか? あんまり違いはないんじゃないかと思います。 ただ、なんだかんだ会社の成長とか戦略を考える部分は、CTOの方が多い気がします。 経営とか戦略って、普段どうやって勉強してるんですか? オンライン学習が好きなので、MOOC(Massive Open Online Course)とか、オンラインで受講できて王道そうな講座を受けるって感じです。 Business Strategy from Wharton: Competitive Advantage とか。プロダクトの戦略に限らず、一般的なビジネス戦略の考え方から勉強しています。 プロダクトの戦略については、 Stanfordのオンライン講座 を受講したりとかですね。 エンジニアと違わないと言いつつ、なんだかんだCTOとして色々やってるんですね。色々やってる中で、今一番足りないと思ってるスキルはどういうところですか? あー、経営面はCEOがいるから自分は役割としてはあくまでもサブなので、やっぱりエンジニアとしての能力を伸ばしていきたいと思ってます。 もう少し人が増えてきたら、チームの開発方針を決めたりとかコードレビューとか、自分以外の人もある程度のクォリティで開発を進められるようにしないといけないと思います。 まだ少人数の会社のCTOなので、 自分のエンジニア能力が低かったら自分の品質で止まっちゃう から、頑張らないといけない。 なるほど。最後になりますが、キャリアの中で一度スタートアップで上場を経験していて成功体験なのかなと思うんですが、何か今に生きていることってありますか? エンジニアの責任と裁量の広さ、プロダクトファーストの思想、年齢や立場を気にせずフラットにディスカッションするスタイルは前の会社で衝撃を受けた部分で、このあたりは今も生きています。 終わりに 対談は以上です。 目黒さん、忙しい中時間を取ってもらってありがとうございました。今度お礼をさせてください。 一番心に残ったのが、「 自分のエンジニア能力が低かったら自分の品質で止まっちゃう 」って言葉です。 マネジメントスタイルを端的に表現してるとともに、かっこいいなと思いました。 自分は漫画のキングダムが好きなんですが、組織の中で将軍が一番強いっていうのは憧れます。 BigTechやイーロン・マスクをはじめとして、マネジメントがちゃんと技術的にも強いっていうスタイルにはすごく共感します。 エンジニアの実務って結構泥臭くて視野が狭くなりがちで、ビジョナリーとテッキーを両立させるのがすごく難しいと思いますが、2023年も応援しています!
アバター
こんにちは!小林和樹と申します。Insight Edgeに参画して約4ヶ月が経ちました。 現在、Insight Edgeでソフトウェア品質向上のためのシステム基盤の選定・構築を担当しています。 その中で、静的解析を共通機能として取り入れるため、そのためのツール調査を実施しています。 特に、セキュリティを高め将来的な負債を減らすこと、開発者に負担をかけずに品質確保の仕組みを導入することを目的としました。 様々な静的解析ツールがある中で脆弱性検出の機能がある、GitHub Code Scanning、SonarQube/SonarCloudを導入対象として比較し、共通機能としてどれが適しているか調査しました。 目次 静的解析について 静的解析ツールについて GitHub Code Scanning SonarQube SonarCloud 比較観点 比較手法 結果 脆弱性検知 対象言語の種類 セットアップの難易度 実行時間 使いやすさ/見やすさ 価格 まとめ reference 静的解析について 静的解析をGoogle検索などで調べると、「コードを実行せずに行う検証」と出てきます。 静的解析をすることで、脆弱性・Code Smell・複雑度・バグなどをチェックすることが目的となっています。 静的解析ツールについて 静的解析は基本的にツールを用いて行われます。 静的解析ツールは様々ありますが、それぞれ機能が異なり、目的に応じて使い分けが必要です。 例えば、静的解析ツールの中でも有名なESlintはソースコードがコーディングルールに則っているかを解析し、修正したり指摘したりするツールであり、Prettierはソースコードを整形するFormatterとしての役割を持つツールです。 これらのツールは利用している方も多いのではないでしょうか? 今回紹介するGitHub Code Scanning、SonarQube、SonarCloudは脆弱性検知を備えているメジャーなツールであり、それぞれ特徴的なツールとなっています。 GitHub Code Scanning GitHub Code Scanningは、特に脆弱性検知に優れており、名前の通りGitHubの1機能です。 GitHub Code Scanningによってソースコードのスキャンが実行されるとPRコメントなどによって結果が表示されます。 脆弱性が見つかった場合、× が表示されます(図1)。 図1 右のDetailsのリンクをクリックすることでどのような脆弱性が見つかったか一覧を表示できます(図2)。 図2 脆弱性の内容によってCritical・High・Medium・Lowの4段階のレベルがあり、High以上は必ず目を通すべきとされています。 導入は非常に簡単でGitHub Actionsによって実行でき、YAMLファイルもほぼ自動で生成してくれます。 導入方法については、本記事では紹介致しませんので私が導入した際に、参考にしました こちら の記事などをご参照ください。 GitHub Code Scanningは、publicリポジトリであれば無料で利用できますが、privateリポジトリで利用するにはEnterpriseアカウントにGitHub Advanced Securityオプションをつける必要があります。 GitHub Advanced Securityオプションを追加することにより、以下3つのセキュリティ機能をリポジトリに追加できます。 Code scanning Secret scanning アクセストークンなど本来Gitに上げるべきではない内容を検出し通知する Dependabot 依存ライブラリのバージョンアップデートや脆弱性情報を通知する こちらの機能もpublicでは無料で利用できる なお、GitHub Code Scanningの脆弱性検知にはCodeQLを利用しているため、ローカルでCodeQLを実行することで同じ結果を得ることができます。 CodeQLの導入方法は こちら をご参照ください。 SonarQube SonarQubeは総合的に品質管理をするためのオープンソースプラットフォームです。 そのため利用するには、自分でサーバを立てる必要がありますが基本的に無料で利用できます。 現在ではDockerを用いることが多いそうです。(参考記事は こちら ) 検査を実行すると図3のような概要ページが表示されます。 コーディングエラーや脆弱性、テストのカバレッジ率など様々な情報を確認できます。アルファベットによって深刻度が一目で分かります。 図3 各項目をクリックすると、種類ごとに検出された課題の一覧が表示されます(図4)。 図4 脆弱性の深刻度は順番に、Blocker・Critical・Major・Minor・Infoの5段階に分かれており、3段階目のMajor以上の数が概要ページの評価に影響します。 深刻度が高いほど、アプリケーションの動作に影響を与える可能性が大きく、Major以上の脆弱性は修正が推奨されています。 SonarCloud SonarCloudは簡単に言えば、SonarQubeのCloud版となっています。基本的な機能はSonarQubeと変わりませんが、導入は非常に簡単です。(参考記事は こちら ) Cloud版であるということで、セットアップや保守という面では、SonarQubeより優れています。 比較観点 今回の調査では、主に脆弱性検出についてメインで取り上げていますが、総合的に判断するため、以下の観点で比較しました。 脆弱性検知 検出結果にどのような違いがあるか 検知した脆弱性に関してどれぐらいの情報量があるか 対象言語の種類 JavaScript・TypeScript・Pythonなど社内で用いられる言語に対応しているか 対応言語の総数はいくらか セットアップ・保守の難易度 セットアップ・保守にどれぐらいの時間を要するか 実行時間 テストの実行を始めてから、結果が出るまでどれぐらいの時間がかかるか 使いやすさ/見やすさ 業務の中で使いやすいのはどちらか 脆弱性についての結果は分かりやすくなっているか 価格 プランごとの価格はいくらか 比較手法 今回は同じソースコードに対して、Github Code ScanningやSonarQubeを用いた場合それぞれどのような結果となるかを調査しました。 そのため、OSSを利用し検証します。 今回比較対象とするソースコードはこちらです。 Life Restart 結果 まずは、GitHub Code Scanningの結果です。 すべてのレベルを合わせて21件の結果が表示されました。(図2参照) High以上の脆弱性は20件であり、 内訳は以下の通りでした。 Inefficient regular expression(3件) 正規表現が非効率であり、DOS攻撃にも繋がる可能性がある Incorrect suffix check(8件) IndexOfやLastIndexOfの使い方に問題がある Useless regular-expression character escape エスケープ処理の使い方に問題がある(6件) Incomplete multi-character sanitization(2件) 文字列の置換メソッドを直接使用してエスケープ処理を行うと、エラーが発生しやすい Insecure randomness(1件) Math.random()は正確なランダムではなくIDやパスワードなどの生成に使うべきではない 続いてSonarQubeを用いた場合を確認します。 Major以上の脆弱性は1件見つかり、Medium以上のSecurity Hotspotsは45件見つかりました(図4参照)。 脆弱性の内容は、 Verify the origin of the received message Window.postMessage() の脆弱性を悪用される可能性がある(1件) Security Hotspotsは以下の内訳でした。 Math.random()の利用(30件) 正規表現によるDoSの脅威(15件) 脆弱性として検出されたものは、GitHub Code ScanningとSonarQubeでそれぞれ被らないという結果となりました。 正規表現の問題、 Math.random()については、SonarQubeのSecurity HotSpotとして検出されていますが、Code Scanningでは脆弱性で検出されています。 Math.random()はプログラム上でたくさん用いているものの、GitHub Code ScanningではIDの生成に使っている部分にのみ警告を出しており、セキュリティに大きく関わる部分に絞って検出しているように見えました。 このように、GitHub Code Scanningによって発見された脆弱性については検出理由に納得のいくものが多く感じました。 続いて、それぞれのエラーに対する詳細な情報を見てみます。 GitHub Code Scanningは図5のように、エラー箇所と修正方針のRecommendation、修正例が表示されています。 図5 ユーザは、エラー箇所を確認しRecommendationを見て修正すべきと判断した場合、修正例を見てコードの修正するといった使い方が考えられます。 SonarQubeでは図6のように、どこで見つかった脆弱性か、どんな内容の脆弱性なのかが分けて表示されています。 図6 SonarQubeでは、NoncompliantCodeExampleとCompliantSolutionが対比して書かれているため、CompliantSolutionを確認して修正方針を立てるといった使い方になると考えられます。 では、ツールの使用感や様々な情報から先ほどの観点で比較します。 脆弱性検知 脆弱性検知に関しては、GitHub Code Scanningをおすすめします。 どちらも違った内容の脆弱性を検出しましたが、GitHub Code Scanningの方が、的確にセキュリティ上修正が必要な部分を抜き出して検出していると感じました。 そして、何よりどのように修正すべきかのレコメンドやサンプルがあり、修正にすぐに着手できる点もポイントが高いです。 また、検出できる脆弱性もGitHub Code Scanningの方が多くなっています。 今回はJavaScriptに関して比較しました。 GitHub Code Scanning 全203種類の脆弱性検知が可能 https://codeql.github.com/codeql-query-help/javascript/ SonarQube/SonarCloud 31種類の脆弱性+62種類のSecurity Hotspots https://rules.sonarsource.com/javascript/RSPEC-6096 対象言語の種類 種類の数に関してはSonarQubeに軍配があがりましたが、当社においては、Python・JavaScript・TypeScriptの利用が圧倒的であり、どちらの製品に関しても対象となっています。 使う言語によってどちらを使うべきか考える必要があります。 SonarQubeのカバー範囲に驚かされた結果となりました。 GitHub Code Scanning 10言語 https://codeql.Github.com/docs/codeql-overview/supported-languages-and-frameworks/ SonarQube/SonarCloud プランによって最大29言語 https://www.sonarqube.org/features/multi-languages/ セットアップの難易度 セットアップのしやすさについては、GitHub Code Scanningをおすすめします。 導入には、複雑な工程など必要なくGitHub上だけで完結できる点が素晴らしいです。対して、SonarQubeは自分でDockerやAWSなどを用いてサーバを構築する必要があります。 慣れていない人にとってはセットアップに戸惑う可能性があり、アップデートも非常に大変です。(実際私は、アップデートにかなり苦戦しました。) しかし、SonarCloudを用いる場合は、Code Scanningと同じぐらいの手間で導入が可能となっています。 GitHub Code Scanning GitHub Actionsを組み込むのみ。YAMLファイルも自動で生成される SonarQube 自分でサーバをインストールし、起動する必要がある。また、SonarQube側でプロジェクトをそれぞれ立ち上げる必要がある SonarCloud SonarCloudとGitHubを連携しGitHub Actionsを作成する 実行時間 実行時間については、SonarQubeに軍配が上がりました。 上記のソースコードに対する実行では、GitHub Code Scanningは約5分、SonarCloudが4分ほどかかったのに対して、SonarQubeは1分ほどで解析が終了しました。 使いやすさ/見やすさ 使いやすさ/見やすさに関しては、GitHub Code Scanningをおすすめします。 GitHub Code ScanningはPRコメントなどGitHubのCI上で全て完結できるので、手間がかなり少なくなっています。 SonarQubeは脆弱性のみならず、さまざまな観点からコードの静的解析を行なってくれます。また、それぞれの解析結果について視覚的にわかりやすく改善ポイントが見えるので、ソースコードのクオリティが一目で分かりやすい点が非常に良いです。 どちらのツールもそれぞれ特徴は異なりますが、十分扱いやすいツールだと言えます。 しかし、今回については脆弱性検出の観点で見ると、脆弱性に関して必要な情報が整理されている点を踏まえてGitHub Code Scanningの方が使いやすいと思います。 GitHub Code Scanning GitHubのUI上で詳細まで確認できる点が非常に便利 問題点や修正方法が簡潔に提示される SonarQube プログラムのどんな部分に問題があるか可視化されており非常に分かりやすい 価格 価格については、利用用途によって大きく異なるのでプランを紹介します。 業務上publicで開発することは考えづらいので、privateなシステム開発を考えると、SonarQubeのCommunityプランが一番コストを抑えることができます。 また、GitHub Code ScanningとSonarCloudを比較します。 仮に5000行ほどのソースコードを持つリポジトリが5つあり、開発者が10名とするとSonarCloudは10万行まで €10/月ほどなので、GitHub Advanced Securityよりも安く済みます。 (GitHub Advanced Securityの価格についてはGitHub社へお問い合わせください。) GitHub Code Scanning publicでの利用は無料 GitHub Advanced Securityのライセンスを購入すればprivateでも利用可能。(価格は要問合せ) SonarQube Communityプランでは基本無料 その他プランは行数によって変化する https://www.sonarsource.com/plans-and-pricing/ SonarCloud publicでの利用は無料 こちらも行数によって価格は変化する まとめ 以上の結果から以下のように、星取表をまとめてみました。 ◎・・・文句なし ◯・・・十分導入できる △・・・懸念あり 脆弱性検知 対象言語 セットアップ・保守難易度 実行時間 使いやすさ/見やすさ 価格 GitHub Code Scanning ◎ ◯ ◯ ◯ ◯ △ SonarQube ◯ ◎ △ ◎ ◯ ◎ SonarCloud ◯ ◎ ◯ ◯ ◯ ◯ 今回の検証では以上の観点のうち、脆弱性検知能力と、セットアップ・保守の負荷の低さを重視しました。 脆弱性検知とセットアップ・保守難易度をどちらも考慮するとGitHub Code Scanningを利用したい...という思いが強いのですが、価格が少しネックになってきます。 また、当社は保守運用まで実施しているプロジェクトはまだ少ないです。 その点を考慮すると、価格も安く、セットアップ・保守難易度も低く十分な静的解析機能を持つSonarCloudが適しているのではないかというのが結論です。 今回の検証では、SonarCloudを選択しましたが、GitHub Code Scanningの脆弱性検知能力は非常に強力です。 規模やユースケースによってこれらの選択は大きく変わると思うのでぜひ今回の記事を検討の参考にしてください。 reference SonarQube でソースコードの静的解析とレビューを自動化してみる(前編) GitHub Code Scanning を試してみた | 脆弱性の自動検出 CodeQL で遊ぶ ~ ローカル環境で試す『静的アプリケーション・セキュリティ・テスト』 ~ 【SonarQube】Docker Compose でサクッと試してみる sonarcloud と GitHub をつなげて静的コード解析を手に入れる Sonar CodeQL documentation Life Restart
アバター