フォルシアのブログ - TECH PLAY

TECH PLAY

フォルシア

フォルシア の技術ブログ

250

この記事は Competitive Programming (1) Advent Calendar 2019 23日目の記事です。 旅行プラットフォーム事業部の大沢です。 競技プログラミングを2年前に始めて以来、週末のAtCoderコンテストにはほとんど欠かさず出ています。 私は昨年末に青色コーダーになり、実力をどうにかキープしています。まだ時間はかかってでも強くなりたい気持ちがあります。 この記事の気持ち 二分探索についての教材は世の中に多くあり、良質な記事も多い反面、「半開区間」などの考え方が難しく混乱するという意見も耳にしています。また、実際に書いてみると意外とバグりやすいことでも有名で、私もよくハマってしまうことがありました。「半開区間」という言葉を使わず、私なりにわかりやすいと思う理解と、バグりにくい書き方を記事にしてみました。 メインターゲットの読者は、以下のいずれかを想定しています。 二分探索って何だろう?という方 二分探索の概要をなんとなく知っている方 二分探索を学んだことがあるが、理解がちょっと怪しい方 二分探索を実際に書いたことがあるけれど、よく細部をバグらせてしまう方 二分探索をあらためて直感的に理解したい方 特に、細部をバグらせないような、直感的でおすすめな理解の仕方を紹介したいと思います。 二分探索とは 二分探索は一言で言うと「 境目を見つける 」アルゴリズムです。 探索範囲の1か所に境目があって、「 境目の左側が全てある条件を満たし、右側が全てその条件を満たさない 」ことがわかっているときに、その境目を高速に見つけることができます。 もちろん条件を満たす満たさないは 左右逆でも使えます 。 境目というのは何でもよいです。読みかけの本のここまで読んだ/読んでないのページの境目とか、背の順に並んだ児童の中で身長が100cm未満/以上の境目とか、納期に間に合う/間に合わないのタスク量の境目とか、 全社員を満足させるために足りる/足りないのピノの箱数の境目 、とか・・・。 とにかく「 1か所の境目 」の両側で判定結果が二分されていることが重要です。判定結果が「1か所の境目」で二分されない条件では基本使えないと思ってください(使えないことは無いですがこの記事では扱いません)。 実装は後ほど解説しますが、まずは長さ10の配列を使って、二分探索の動き方を見ていきましょう! 以下の例では左側が条件を満たす側だとします(逆の場合は後述します)。 図1 図1で、黒い枠線は要素数10の配列だとします。上の緑文字がindexです。 青い領域は条件を満たすことが確定した領域、赤い領域は条件を満たさないことが確定した領域です。 ok , ng の2つの変数を用意し、探索範囲の外になるような値を設定する ※1 ok=-1 , ng=10 と置く ok と ng の平均を求める。 4 となる is_ok(4) == True となり条件を満たすので ok = 4 とする ※2 ok と ng の平均を求める。 7 となる is_ok(7) == False となり条件を満たさないので ng = 7 とする ok と ng の平均を求める。 5 となる is_ok(5) == True となり条件を満たすので ok = 5 とする ok と ng の平均を求める。 6 となる is_ok(6) == False となり条件を満たさないので ng = 6 とする ok と ng の差が1になったので処理を終了する ※1 ok,ngの初期値について 探索範囲の思いっきり外側でもよいし、探索範囲の内側でもよい。 大事なのは ok は確実に条件を満たすゾーン、 ng は確実に条件を満たさないゾーンに含まれていること。これが間違っていると正常に動作しません。 ※2 判定関数 is_ok(i) はindexが i のときに条件を満たすなら True 、そうでなければ False を返します。配列外の i が引数で来たときにも、満たす側なら True 、満たさない側なら False と返すものとします。 今回の例では単純に、 def is_ok(i): return i のような実装がされていると思ってください(ここでは配列の中身すら無視されていますが・・・)。 理解のポイントとしては、 ok は常に条件を満たすことが確定したゾーンの一番右側にいる ng は常に条件を満たさないことが確定したゾーンの一番左側にいる 最終的に ok と ng は密着する(差が1になる)ことがわかれば完璧です! この挙動をするコードをPythonで書くと次のようになります。 ok = -1 ng = 10 while ng-ok > 1: mid = (ok+ng) // 2 # 平均(小数切り捨て) if is_ok(mid): ok = mid else: ng = mid print(ok,ng) # "5 6" が出力される それでは、5で最終結果として得られた ok , ng の値は何を示すでしょうか? ok・・・条件を満たすなかで最大のindex ng・・・条件を満たさないなかで最小のindex この理解でほとんど問題ありません(注意すべき点は後述します)。 実際 ok として得られた 5 は、 is_ok(i) を満たす i のうち最大の整数です。 左側がngの場合 また、左側がngとして実装した場合、コードは例えばこのようになります。 def is_ok(i): return i > 5 #大きい側がTrue ok,ng = 10,-1 # さっきと逆なので注意 while ok-ng > 1: # さっきと逆なので注意。abs(ok-ng)のように汎用的に書く流派もある mid = (ok+ng) // 2 # 平均(小数切り捨て) if is_ok(mid): ok = mid else: ng = mid print(ok,ng) # "6 5" が出力される そして、 ng・・・条件を満たさないなかで最大のindex ok・・・条件を満たすなかで最小のindex となります。 ここでよく混乱しがちなのが、二分探索で得られた2つのポインタのうち、最終的にどちらを使えばよいのか?という問題です。 大体の場合、 ok を使えばOK ところで、このような問題文をよく見ませんか? 条件を満たすなかで最大の〇〇を求めよ 条件を満たすなかで最小の〇〇を求めよ このような問題文が出てきたときは、二分探索を使えるケースが少なくないです。 そして、 条件を満たすなかで最大の〇〇 → ok を左側として実装し、最終的に ok を使う 条件を満たすなかで最小の〇〇 → ok を右側として実装し、最終的に ok を使う なんと、 ok として得られた値をそのまま使えばよいのです! 「条件を満たす側」「条件を満たさない側」と分けてきたのは、このためです。 一般的な二分探索では2つのポインタを high , low みたいな名前で管理することが多いと思うのですが、 ok , ng とすることで、何を扱っているのかがわかりやすくなり、何かと嬉しいことが多いです。 この ok , ng で管理する方式は私が考えたのではなく、いわゆる「 めぐる式二分探索 」として知られています。 配列外参照には注意 配列の要素すべてが条件を満たさない場合、 ok = -1 となり、 ok が配列外を指します。 同様に、要素すべてが条件を満たす場合、 ng = 10 となり、 ng が配列外を指します。 左を ng とするケースではこれの逆で、 ng = -1 や ok = 10 の状態が生じます。 私がよくやるのは、下のような関数を作って判定します。 def is_ok(i): if i = N: return False return 有効なiに対する判定 引数の i が配列外など、有効な範囲にないときの処理を忘れないようにしましょう。 また、向きにも注意で、 ok を返す側の異常値のときに True 、 ng を返す側の異常値のときに False を返してください。 「個数を求める」場合などもちょっと注意 条件を満たすものがいくつあるか? などという問題に対してはちょっとだけ注意が必要です。 結論を書くと、 条件を満たす側が左側なら → ng が答え 条件を満たす側が右側なら → N - ok が答え になります(が、これは覚える必要はありません)。 あくまで ok が指すのはギリギリ条件を満たすボーダーの入力(ここではindex)です。 indexから個数を求める必要があるということは頭の片隅に置いてください(これが頭から抜けていると、二分探索の実装がバグっているのか?と錯覚して焦りがちです)。 なぜこのように求まるかはぜひ考えてみてください。次節のような図を描けば、感覚的にも理解できるかと思います。 最終的なok,ngについて視覚的な理解 ところで、要素数 10 の配列を二分探索した場合、得られる結果は 11 通りあります。 要素数 N なら N+1 通りです。 図2 上の図に書かれた 0 ~ 10 の青い数字は、要素の境目に番号を振ったものです。 このように 0 始まりで番号を振った場合、二分探索の結果のうち、 ok , ng の大きい方に一致します。 たとえば ok=5, ng=6 ならば 6 という境目 ok=3, ng=2 ならば 3 という境目 が求められたことを示します。 これがイメージできていれば、最終的に二分探索によって何が得られているのかが確実に理解できているはずです!図で捉えればもう怖いものはないですね! 二分探索の強力さ 探索範囲が N 要素の場合、 log_2(N) 回程度の比較回数でよいです。 例であげたような 10 要素ぐらいの場合では効果は薄いのですが(むしろ二分探索を使わない方が実装が単純な分よい)、 探索範囲の規模が大きくなるほど効果が強い です! 例えば 100000 (=10^5)要素 → 17回程度 5000兆 (=5*10^15)要素 → 53回程度 の比較回数で求まりかなり強力です! 図1(再掲) 図1を改めて見ていただくと、未確定のゾーン(白い部分)が 1回の処理でおよそ半分 にしぼり込まれていく様子がわかると思います。 先ほど5000兆要素と書きましたが、実際にこれだけの長さの配列がメモリに乗ることは現実にはないと思います。 実は、 探索対象は配列でなくてもよい のです。 is_ok(x) 関数の結果の、 True / False が切り替わる 境目が1か所以下 (=単調性がある)ならば、二分探索が使えます。 探索対象が浮動小数をとる場合 お気づきの方もいるかと思いますが、探索対象が配列でなくてもよいということは、 is_ok(x) の引数 x が 整数以外を取ってもよい ということです。 実際、境目となる浮動小数の値を二分探索で調べたいケースもあります。 while ng-ok > 1: でループさせるような先ほどの実装では期待した動きになりません。 このようなときは、何も考えずに 100 回程度ループするのが定石となっているようです。 (一般的なdouble型の精度より遥かに余裕がある回数なので、お好みで調整してください。) for i in range(100): mid = (ok+ng) / 2 # 平均(浮動小数) if is_ok(mid): ok = mid else: ng = mid このケースでは ok と ng は同じ値に収束してくるので、どちらを使う?のように考える必要はないですね。 bisectモジュールの使えるところと使えないところ ※これはPythonista向けの話題です。 Pythonには、ソートされた配列の中に、ある値が入るべき境界を二分探索で見つけたいとき、標準モジュールに bisect.bisect_left() や bisect.bisect_right() といった関数があります(C++ だと std::lower_bound() や std::upper_bound() に相当する気がします)。 非常に便利ですので、これらについて最後に簡単に紹介します。 まず使用例です。 from bisect import bisect_left, bisect_right arr = [1,3,5,5,5,6,7] # 昇順にソートされている必要がある l = bisect_left(arr, 5) # 5が入るべき境目のうち最も左側の境目を返す r = bisect_right(arr, 5) # 5が入るべき境目のうち最も右側の境目を返す print(l,r) # "2 5" が出力される このように、昇順ソートされた配列の境目を見つけるタイプの問題であれば、 bisect モジュールを呼び出せば自分で実装する必要はありません。私も時々これのお世話になっています。 ちなみにこれの返り値の正体は、 図2で書かれている青い数字 に相当するものが返ってきます。 これがわかっているだけでもライバルに差がつきます! また、配列の中身が数値でなくても bisect は利用できます。文字列やタプルの場合も辞書順比較をしてくれます。Comparableな要素の配列で、正しく昇順ソートされていればよしなにやってくれます。 では逆に使えないケースはどんなときかというと、 探索範囲が配列ではないとき です。 関数に入力される x のうち、条件を満たす/満たさない x のボーダーを見つけたい場合は、自前実装するしかありません。 x について条件を満たすかどうかの判定が 配列の値以外 によるのであればこれに該当します。 (配列でなく関数の結果を探索する場合、二分法と呼ぶのが正しい気がしていますが、競プロの文脈では区別されないことが多いです。この記事でも「二分探索」に統一して呼んでいます。) 降順ソートされた配列に対しては、自前実装で対応してもよいのですが、私は反転した配列に bisect を使い、それの結果を反転することが多いです。 配列の反転には O(N) かかりますが、入力の時点で O(N) かかっているはずなので問題になることはおそらくありません。 まとめ okは条件を満たすことが確定したゾーン、ngは満たさないことが確定したゾーン どちら側が条件を満たすのか、には要注意 okとngの2変数で未確定のゾーンをはさみながら絞り込んでいく 最終的には ok を使おう ただしそのまま使えないケースもあるので、よく見極めて ok と ng の境目を求めているのだということを理解しよう 図でイメージできれば、もう間違えない! 水色になってぜひフォルシアへ! フォルシアでは 2021年度新卒採用 を行っています。 近年 AtCoderJobs からの応募・入社が増えてきています。 強い技術を持ちながらビジネスに活かしたいWebエンジニアの方、水色以上になってAtCoderJobs経由で応募いただくと書類選考が免除されます! 仕様を実装に落とし込むのが早く、計算量の感覚も身についていると、とても素敵です! 競プロerの皆さん、ぜひ一緒に働きましょう!
FORCIA アドベントカレンダー2019  21日目の記事です。 エンジニアの島本です。私は入浴・朝晩のストレッチなど日常的に体をほぐしているのですが、日々腰痛に悩まされていました。 しかし、骨盤を後傾させて「反り腰」を改善するとよい、という後輩から聞いたアドバイスを実践したところ、腰痛・モモ裏の張り・肩こりのすべてがやわらいでびっくりしています(後輩は整体師から教わったことを共有してくれました)! 体の仕組みから原因を特定し、原因に合わせた対応をすることで改善する。これはアルゴリズムや仕様を理解して、最適な設計・実装をするというプログラマの日常と全く一緒ですね。 さて、オライリーからも ヘルシープログラマ~プログラミングを楽しく続けるための健康Hack~ という本が出ているように、プログラマにとって重要な健康をHackする様々な方法が世の中で紹介されていますが、それらを実践できている人は少ないのではないでしょうか。 しかし、仕事でハイパフォーマンスを維持するのに健康は欠かせませんよね。 私は現在、社内最大の売上を担うプロジェクトのエンジニアリーダーという重要な役割を担う傍ら、週末にはtoC向けのサービス作りをしており、常に高いパフォーマンスを維持できるよう日ごろから健康に気を使っています。今回はそんな私が、様々な文献から仕入れ、手を抜きつつ実践してきた健康Hackをご紹介します。 眠気に打ち勝つ 睡眠の質向上 眠気をなくす最善の方法は「眠くなくなるまで寝る」ですが、これを実践するのはなかなか難しいですよね。 睡眠時間確保の次のアプローチは睡眠の質向上です。寝具にこだわることなども大事ですが、私が実践しているのは「入眠の1時間半前にお風呂に入る」、これだけです。 皮膚体温と深部体温(体の内部の体温)の関係上、このタイミングで入眠することで睡眠の質が向上します。 (参考: スタンフォード式 最高の睡眠 ) これを実践したところ、明らかに寝つきが良くなったと実感できました。元々入浴の習慣があったため導入へのハードルはほぼ0でした。 シャワー派の方は熱いシャワーを浴びた1時間後を目安にベットに入るのがよいそうです。 NO MORE !目覚めのコーヒー 世界一美味しい飲み物は「コーヒー」ですよね。カフェインによる覚醒作用は高いパフォーマンスを発揮するのにも役立つため、毎日何杯も飲みたくなってしまいます。 ただし、コーヒーの覚醒作用に頼ってしまうと人間が本来持っている覚醒力が減少してしまうため、朝のコーヒーは完全に脳が覚醒してから飲むのがよいそうです。 また、夕方以降のコーヒーも睡眠の質を下げるので控えた方がよいです。私も寝起きのコーヒーをやめて、昼食後から夕方にかけて1〜3杯程度飲むようにしたところ、午前中に頭がぼーっとすることが減りました。 胃を酷使しない 健康やダイエットに興味がある方は、ファスティング(断食)という言葉を耳にしたことがあるかと思います。 私は職場の先輩のファスティング体験を聞き興味を持ったのですが、ファスティング中の激しいスポーツは危険ということもあって断念しました(新宿1部リーグのサッカーチームに所属しており週1,2回プレーしています)。 ファスティングの目的は、「人間の体の真の機能を取り戻す」ことです。 胃腸は約7~8時間でものを消化するので、もし3食のスパンが8時間より短ければ、胃腸は不眠不休ということになります。 胃を休ませることによって体の機能を取り戻し、感覚を鋭敏にすることがファスティングの目的なので、要は胃を休めればよいということです。 私は、 朝食を抜く 夜は固形物を減らしスムージーやプロテインでお腹を満たす という生活に変えたところ、体が軽くなるのを実感できました。また便通も改善しました。 しかし、朝食を抜くと空腹との戦いが始まります。空腹を紛らわすには水を飲み塩をなめるのがよいそうです。 それでも空腹に勝てないときは、我慢せずに早めに昼食をとるなりナッツを食べるなりしています。 完璧を目指してストレスを感じるより、ズボラにでも続けることを優先しています。 炭水化物の支配から抜け出そう 現代人は炭水化物に支配されていると言っても過言でありません。コンビニやお店のどこでも食べられるし美味しいので、食事の中心が炭水化物になっている人がほとんどかと思います。 ですが、炭水化物の摂取しすぎは体に悪影響です。農業が誕生したことにより、人類は大量のデンプンの摂取が可能になり(現代人の主要栄養素は米、小麦、トウモロコシ、じゃがいも)、糖になるこれらデンプンの大量摂取は現代のあらゆる病気の原因となっています。 (参考: GO WILD 野生の体を取り戻せ! 科学が教えるトレイルラン、低炭水化物食、マインドフルネス ) ただし、炭水化物そのものが悪いのではなく、炭水化物の取りすぎや多様性のない食事が問題なのです。 そのため、小腹が空いたときはパンやおにぎりを食べるのでなくナッツを食べるようにするなど、炭水化物を惰性で食べないようにするのが重要です。 私は、 朝食:抜き 昼食:好きなもの 夕食:炭水化物少なめ+スムージーやプロテイン というくらいズボラに実践し、サッカーがあるときには炭水化物中心の食事でカーボローディングしています。 食べる楽しみを忘れないのも大切です。 戦略をもって散歩しよう 健康に大切なのは睡眠と運動ですよね。私は日常的に運動する時間を作るために、通勤時間に散歩を組み込むようにしています。 日常に取り入れやすい「1駅手前で降りて歩く」方法は非常にシンプルであると理解しつつも、朝の時間がもったいなどの理由で実践に移せない人も多いのではないでしょうか。そこで、散歩の間の時間をもったいと思わないような戦略を持ち込むのがオススメです。 私はこちらを実践しています。 思考するかネタを用意しておく 歩きながら本を読む(聞く) Appleの共同創業者、故スティーブ・ジョブズは何か重要な話をするときや考えをまとめる際にはとにかく公園や道路など、あちこちをよく散歩していたというエピソードがあります。このように、なかなか考えがまとまらないときに、ふらっと外を歩いていると良いアイデアが突然浮かんでくることがあるのは万国共通の体験ではないでしょうか。 朝の散歩の前に思考すべきネタを用意することで、散歩しながら仕事を進めることができます。 また、 Audible などのオーディオブックは、非常に便利なサービスですが利用するにはお金がかかります。しかし、スマホのKindleの読み上げ機能を使うことで無料でオーディオブック化することができます。 1駅歩く分少し早く起きるだけで、通勤時間が散歩と思考と読書の時間に変わり、人生がより豊かになるのでぜひお試し下さい。 Don't think! train. トレーニングは思考停止状態で始めよう 日々の散歩だけでなく強度のあるトレーニングもしたいですよね。ただ、ジムに通ったり家の周辺を走ったりするのはなかなか続かないのではないでしょうか。 そこでおすすめなのが Nike Training Club です。 パーソナルトレーニングアプリで、メニューは自宅で5分程度でできる簡単ものから、屋外やジムで器具を使って行う本格的なものまで幅広く用意されています。 何よりよいのが音声サポートです。「それでは始めます」と、どんどんメニューをこなさなければいけない厳しさと「もう少しです。頑張りましょう!」と励ましてくれる優しさがあります。 一人で黙々とトレーニングを続けるには意思が必要ですが、自宅で言われるがままに体を動かすだけでよいので「スタートボタンを押す」ことさえできれば思考停止状態で続けることができます。 番外編 〜オフィスで飲む1杯のコーヒーにこだわろう〜 最近はコンビニで美味しくてコスパの良いコーヒーが飲めますが、さすがに毎日だと飽きてしまいますよね。 オフィスで自分好みの豆で淹れた美味しいコーヒーが飲みたいと思っている人は多いのではないでしょうか? 私もその一人であり、今は家で挽いた豆を kintoのカフェプレスマグ を使ってフレンチプレスで飲んでいます。 これまでにオフィスでドリップすることや、家で淹れたコーヒーを魔法瓶に入れ持参するという方法を試してきましたが、最終的には淹れたてのコーヒーの香りが楽しめることと手軽さのバランスが最もよいこの方法に落ち着きました。 私がこれまで試した中で、良かった道具を紹介します。 一人用コーヒーメーカー ハンドドリップよりもお手軽で、 コンパクトで広いスペースを必要としません メタルのフィルターのためペーパーフィルターの購入が不要であり、オイルがカットされずコーヒー本来の持つ味と香りが楽しめます ナポリ式コーヒー コンロにセットするだけで蒸らしから抽出までできるため、お手軽かつ本格的なコーヒーが飲めます タイガーの夢重力 コンパクトでびっくりするくらい軽いです しっかり保温もできるため家で淹れたコーヒーをオフィスに持参するのに最適です 個人的にはハンドドリップコーヒーが一番好きで、家でドリップしたコーヒーをオフィスに持参していた時期もありましたが、朝の10分を節約したいという気持ちや淹れたてのコーヒーをその場で飲めない切なさから、オフィスで淹れるようになりました。 美味しいコーヒーをオフィスで飲みたいと思っている方の参考になれば幸いです。 さいごに 以上、私がズボラに実践してきた健康Hackを紹介しました。健康に気を付けて、ハイパフォーマンスで仕事していきましょう!
FORCIAアドベントカレンダー2019  20日目の記事です。 こんにちは。アドベントカレンダー20日目を担当します、旅行プラットフォーム事業部の高橋です。19新卒として今年度フォルシアに入社し、現在はwebアプリの保守・運用・開発を行っています。 また、今年の10月に発足した 技術広報チーム にもjoinし、このアドベントカレンダーの企画と運営もしています。 さて、技術広報としてフォルシアの技術をどの切り口からアピールしていこうかと思案している折、このような記事を見かけました。 Slackはただのコミュニケーションツールじゃない、企業の技術を映す鏡だ ふむふむなるほど、確かにフォルシアでもSlackはもはや欠かせないツールとなっていますし、その機能の拡張性はエンジニアの心をくすぐるものがあります。 この記事を読みながら、 弊社のSlack活用も悪くない線をいっているのでは・・・? と手前味噌ながらに思いましたので、私からフォルシアのSlack活用術についてご紹介します。 活用のまとめ 体系的なチャンネル名 外部サービスとの連携 社内の交流を加速させる仕組み チャンネルのプレフィックス フォルシアのSlackの社内チャンネルには数百もの数があり、私が参加しているチャンネルも相当の数があります。 フォルシアではチャンネル名の前に次のようなプレフィックスを付けることで、チャンネルを分類・整理しています。 プレフィックス 対象 例 00 全社共通 00_all 01 組織 01_sales 02 顧客 02_forcia 03 社内プロジェクト 03_appring 10 情報共有 10_forcia_cube 11 サークル 11_tennis xx その他 xx_fresh2019 zzz 個人 zzz_takahashi フォルシアにSlackが導入されたのは2016/01頃から2016/03にかけてです。この期間に社内のIT管理部門を中心にプレフィクスの整理をし、Slackの使用感に応じて、柔軟に拡張・整備がされていきました。 このルールにのっとれば、社員それぞれがチャンネルを新しく作成することができます。 プレフィックスのルールを決めておくことで検索がしやすい、並び順が整然として見やすい、などのメリットがあります。 この中で特徴的なのは、 zzz から始まる個人チャンネル(通称 「ずずず」チャンネル )でしょうか。社員個人の考えていること、最近あったこと、おいしかったランチのお店など、個性あふれるつぶやきがされます。 また新人研修の際には、新入社員の個人チャンネルに先輩社員がjoinして、新入社員が作業の様子などをそのチャンネルにつぶやくことで 進捗の管理に利用 されることもあります。入社間もない頃は、多くの社員が参加しているチャンネルだとなんとなく発言するのに緊張してしまいますが、 zzz は自分のためのチャンネルなので 比較的低いハードルで発言できる のがよいですね。 システム監視 フォルシアは旅行業界を中心として、様々な企業のwebアプリケーションを開発・運用しています。 それらのシステムが問題なく稼働できるよう、細心の注意を払って運用を行ってはいるものの、時として種々の要因によりシステムに問題が生じる場合があります。 システムの状況監視は社内サーバーから常時行っていますが、システムに問題が生じた場合の 通知手段の一つとしてSlackが用いられます 。 システム監視関連の通知は #00_system_report というチャンネルに通知されます。 システムに異常を検知した場合は、該当のサーバー、対応手順書のリンク、エラーの内容などがまとめられた通知がされ、対応者はこの投稿に対してスレッド形式で現在の状況をコメントしていく運用がなされています。こうすることで対応者以外も現在の状況を知ることができ、 連携や協力が行いやすく なっています。 さらに対応が完了した際は、このスレッドに「 対応完了 」と書き込むことで、障害報告レポートが自動的に起票されるようにもなっています。 外部サービスとの連携 Slackの大きな魅力として、 APPの導入による機能の拡張 や、 豊富なAPIを活用した外部サービスとの連携 があるかと思います。 フォルシアでもいくつかのサービスと連携して、開発が円滑になるような仕組みが整備されています。 GitLabとの連携 こちらの記事でも紹介した通り、フォルシアではソースコードの管理に GitLab を利用しています。 GitLab CI/CD 導入の手引き GitLabのリポジトリはチーム単位、プロジェクト単位などで作られますが、リポジトリに新たに動きがある(push、コメント、マージリクエストの提出など)と、対応する #_チーム名_dev などと名付けられたSlackチャンネルに通知が送られるような運用が行われています。いちいちGitLabのサイトに行かなくてもSlack上で確認できるので、開発やレビューの手間を削減することができています。 そのほかにも、現在提出されているマージリクエストを 毎朝Slackに投稿してくれるbot なども社内で自作されました。 私が所属するチームではこのbotの導入により、気づかれずにレビューされないまま放置されるマージリクエストを減らすことができました。 GitLab CIによるテストの成功/失敗も、もちろんSlackに通知されます。 esaとの連携 フォルシアでは社内の文書を共有するツールとして、 esa というサービスを利用しています。 esaに新しく投稿があると、 # xx_doc チャンネルに記事のタイトルや内容の一部が通知されます。このチャンネルを流し見しておくことで、他のチームの状況だったり誰かの投稿した技術記事などを見逃すことなく知ることができます。 社内交流の促進 ここまで紹介してきたものは業務改善、開発速度の向上などに役立つ機能の話でしたが、次に Slackを用いた社内交流の促進 という観点でいくつか紹介したいと思います。 シャッフルランチ 今年から始まり、社内では毎月の恒例行事として定着してきたシャッフルランチ。この開催にもSlackが用いられています。詳しくは以下の記事をご覧ください。 普段関わりのない社員同士をマッチング シャッフルランチはじめました~企画編~ GoogleカレンダーとSlackからの情報で「グループ分け」 シャッフルランチはじめました~テクノロジー編~ シャッフルランチのコンセプトは、「 普段関わりの薄い人とランチに行く 」ことです。ではどのようにして関わりの薄さを判定するのか、そこで目を付けたのがSlackのチャンネルです。 プレフィックスの説明でも述べたように、Slackのチャンネルは様々な単位で作成されます。AさんとBさんが 共通して参加しているチャンネルが多ければ 、その二人は 関わりが強く 、反対に 共通のチャンネルが少なければ関わりが薄い と考えられるだろう、と仮定してシャッフルランチbotの実装が行われました。 私も毎回参加していますが、確かに業務で関わりの薄い社員と同じグループになることが多く、よくできた仕組みだなと驚いています。 最近では強化学習によりさらにアルゴリズムが強化されたようなので、興味がある方はぜひこちらもご覧ください。 AIで解く最適化問題 ~今日から使える深層強化学習~ イケメンスタンプ #zzz_ikemen チャンネルは、 イケメン というスタンプが押された投稿が共有されるチャンネルです。 このチャンネルに参加していると、自分が参加していないチャンネルでの「イケメン」な投稿も知ることができます(例:新しく案件受注しました!、この日のオンコール当番代わりますよー、など)。 さらに毎月一回、その月のイケメンスタンプを押された数が集計され、 獲得数が上位の人には表彰 が行われます。 こちらの仕組みには「 ホメルくん 」を使わせていただいています。 スタンプ一つで気軽に称えあえる、良い文化だなと思います。 イケメンスタンプ以外にも、用途に合わせて数多くのスタンプが自作され、現在ではスタンプの総数が 1483種類 にもなりました! 複雑な感情もスタンプを使うと表現できることが多いので、他のSNSなどを利用している際に「今Slackのあのスタンプが使えたら・・・!」ともどかしくなることが結構あります(笑)。 さいごに フォルシアのSlack活用術はいかがでしたでしょうか。 「こんな機能が欲しい!」と思った時、発想力と実装力次第で機能を拡張できるのがSlackの良いところかと思います。 開発環境の向上、社内の交流の促進など、Slackは大きな役割を担う存在となっているので、今後も継続して改善していきたいと思います。
これは、 AWS Advent Calendar 2019 の19日目の記事です。 旅行プラットフォーム事業部 WEBエンジニアの西山です。 フォルシアでは少数精鋭でプロジェクトに取り組むことが多く、フルスタックエンジニアとしてフロントからサーバサイドまではもちろん、手を挙げればインフラチームと協力してインフラ関連の業務に当たることができます。 また、昨今では社内外のプロジェクトでAWSを始めとしたクラウドサービスを利用することが増えてきました。 この記事では、WEBエンジニアの私がスキマ時間を利用した AWS認定ソリューションアーキテクト-アソシエイト の勉強法、および学習過程で感じたことを紹介します。 始める前の状態 インフラ関連知識 サーバ構築自動化や、アプリケーションデプロイパイプライン作成などの業務経験あり AWS関連: EC2, S3など基本的なサービスに触れた経験はあり 勉強方法 1. サンプル問題に取り組み、学習を始める前の自分の理解度を知る AWS公式サンプル問題 に触れ、実際にどのような問題が出題されるのかの感覚と、学習開始前の自分の理解度を測りました。 2. 学習 スキマ時間で学習できること、インプットとアウトプットをバランス良くできること、辞書的に使える読み物で網羅的に把握することを念頭に下記を利用しました。 Udemy | これだけでOK! AWS 認定ソリューションアーキテクト - アソシエイト試験突破講座(初心者向け21時間完全コース) 多くのブログなどで紹介されていますが、とてもおすすめです AWSハンズオンメインの動画です。スマホアプリでは動画をダウンロードしてオフライン環境での再生も可能なので通勤や散歩、寝る前などスキマ時間で学習できます セールで購入すればかなり安価で購入することができます 講義、ハンズオン、小テスト、模擬テストとバランス良くコンテンツが用意されており、ハンズオン以外はすべてスマホで完結できます 書籍 動画で学んだことを書き込むノート代わりとして利用しました 基本的には上記動画で一通り押さえているので、ざっと流し読みする程度でも内容は入ってきます 書店で中身を確認して自分に合いそうなものをえらびました 3. AWS公式模擬試験 有料ですが、本試験と同じ形式なので、受験前に慣れておくために受験しました。 感じたこと 関連サービスに触れるなかでソフトウェアを学び直す機会になった AWSには多くのソフトウェアがサービスとして組み込まれています。 そのため資格勉強を通して、ソフトウェアについて知識の整理ができました。 例えば下記などが挙げられます。 ECS, EKSを通してdockerやk8sなどのコンテナ周りの基本的な知識や、コンテナオーケストレーションツールの利点を把握 RDS, DynamoDBなどを通してデータベースの種類ごとの特徴、利点の把握 他にも、AWSと直接の関係はないもののBatchを通してLinuxが用意しているcrontabの仕組みや、S3のバージョン管理からgitのバージョン管理の仕組みを学び直す事ができました。 顧客サービスをシステム構成観点でより深く考えられるようになった 今回学習したソリューションアーキテクトアソシエイトでは 顧客の要件に基づき、アーキテクチャ設計原則に沿ってソリューションを定義できること が求められます。 はじめはインフラ/ネットワーク設計に関する知識のみが求められるのかと思っていましたが、実際には AWSが提唱するアーキテクトのベストプラクティス を下記の観点で学ぶものでした。 運用上の優秀性 セキュリティ 信頼性 パフォーマンス効率 コスト最適化 例えば、運用上の優秀性においては、保守・運用フェイズでの登場人物を想定し、各登場人物に適切な権限を付与することでスムーズな運用を実現するにはどうしたらよいかを検討します。 通常、WEBエンジニアはどのようにサービスに機能を追加していくかにフォーカスを当てて考える事が多いですが、AWS学習を通して上記の観点を学ぶ上で、サービス開発を一歩引いた広い観点で捉えることができ、よりお客様のサービスに寄り添えるエンジニアになれると感じました。 さいごに 社内インフラエンジニアに聞いたところ、数年前までは学習コンテンツも充実しておらず、AWSの公式ドキュメントをひたすら読み込んで資格対策をしていたそうです。 今は学習教材も充実しており、かなり少ないコストで学習および資格取得ができるようになっていると実感しました。 インフラエンジニアではないけどAWSの資格とってみようかな、と思っている方の背中を少しでも押すことができたら幸いです!
FORCIAアドベントカレンダー2019 18日目の記事です。 こんにちは。アドベントカレンダー18日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務ではJavaScript やPython などでプログラムを書くことが多いですが、今回はあえて、普段使用していない関数型プログラミング言語Haskell に触れてみつつ、以前から興味があったメタプログラミングを実際にやってみようと思います。 Haskell にはメタプログラミングを行うためのTemplate Haskellという言語拡張があり、これを使えば簡単にメタプログラミングができるのではないか?という期待を胸に手を出し
FORCIAアドベントカレンダー2019  18日目の記事です。 こんにちは。アドベントカレンダー18日目の記事を担当させて頂きます、エンジニアの澤田です。 普段の業務ではJavaScript やPython などでプログラムを書くことが多いですが、今回はあえて、普段使用していない関数型プログラミング言語Haskell に触れてみつつ、以前から興味があったメタプログラミングを実際にやってみようと思います。 Haskell にはメタプログラミングを行うための Template Haskell という言語拡張があり、これを使えば簡単にメタプログラミングができるのではないか?という期待を胸に手を出してみました。 では、早速始めていきましょう! なお、GHC はバージョン8.0.2を、Template Haskell はバージョン 2.11.1.0を使用しています。 メタプログラミングとは メタプログラミングとは、プログラムを生成するプログラムを書くことで、マクロやテンプレートメタプログラミングによって行われることが多いようです。 このプログラム生成は、プログラムが処理されるどの過程で行われるのでしょうか?少し見てみましょう。 プログラムのコードは言語処理系によって、基本的に以下の段階を経て実行プログラムに変換される(コンパイル型)か直接実行されます(インタプリタ型)。 字句解析: 文字列を字句(トークン)列に変換する 構文解析: 字句列の文法を解析して抽象構文木(AST: Abstract Syntax Tree)に変換する 意味解析: 抽象構文木に対して意味的な解析を行い中間コードに変換する 最適化: 中間コードを計算量・メモリ使用量などの観点から効率化する コード生成: オブジェクトプログラム(アセンブリ言語、機械語)を生成する 上記のどの段階に対してメタプログラミングを行えるかは、言語ごとの拡張機能によって異なっていて、C のプリプロセッサマクロは「字句解析」の段階で変換され、Lisp マクロは「構文解析」で、D テンプレートは「意味解析」で変換されます。 Template Haskell は、上記の「構文解析」で変換されるマクロで、抽象構文木を組み替えたり合成したりすることができます。 抽象構文木を直接操作できるなんてワクワクしませんか? では、やっていきましょう! Template Haskell で抽象構文木を眺めてみる Template Haskell には、クォート式(Quotation)という特別な括弧で囲われた式などを、抽象構文木に変換して出力する機能があります。 この式の抽象構文木はどうなっているのかな?と思ったら簡単に確認できるわけです。すごいですね。 見てみましょう。 まず、 -XTemplateHaskell オプションを付けて ghci を起動します。 そして Language.Haskell.TH モジュールを読み込みます。 $ ghci -XTemplateHaskell Prelude> :module + Language.Haskell.TH Prelude Language.Haskell.TH> 1 + 2 の抽象構文木を見てみます。 Prelude Language.Haskell.TH> runQ [e| 1 + 2 |] InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2))) おお、出ましたね!図にするとこんな感じです。 変数への束縛の場合はどうなるでしょうか。 Prelude Language.Haskell.TH> :{ Prelude Language.Haskell.TH| runQ [d| Prelude Language.Haskell.TH| x = 1 Prelude Language.Haskell.TH| y = 2 Prelude Language.Haskell.TH| |] Prelude Language.Haskell.TH| :} [ValD (VarP x_1) (NormalB (LitE (IntegerL 1))) [],ValD (VarP y_0) (NormalB (LitE (IntegerL 2))) []] こちらも図にしてみます。 面白いですね! 今度はこの抽象構文木を元の式などに戻してみましょう。 抽象構文木を元の式などに戻してみる 抽象構文木を元の式などに戻すには、基本的にppr関数に抽象構文木を渡すだけでOKですが、 GHC.Num.+ などは、そのままでは名前として解釈してくれないので、先頭にシングルクォートを付けて、 '(GHC.Num.+) などのように書き直す必要があります。 それでは先の 1つ目の例を出力してみましょう。 Prelude Language . Haskell . TH > ppr ( InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE ' ( GHC . Num .+)) ( Just ( LitE ( IntegerL 2 )))) 1 GHC . Num .+ 2 おお、元に戻りました! 続いて先の2つ目の例です。2つ目の例では、新しい変数 x と y を導入していますので、名前として解釈させてもそんな変数は無い、と怒られてしまいます。 そんなときは mkName "x" などのように記述して名前を作る必要があります。 それでは先の2つ目の例も出力してみましょう。 Prelude Language.Haskell.TH> ppr [ValD (VarP (mkName "x")) (NormalB (LitE (IntegerL 1))) [],ValD (VarP (mkName "y")) (NormalB (LitE (IntegerL 2))) []] x = 1 y = 2 できました! 次はいよいよ抽象構文木を書き換えてみましょう。 抽象構文木を書き換えてみる これまで、式などを抽象構文木にしたり、抽象構文木から式などを復元する方法を見てきました。 この相互の行き来ができるなら、抽象構文木を直接書き換えることもできなくはなさそうです。 しかし 1 + 2 のような単純な式でも、抽象構文木は、 InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 2))) のように複雑な記述になってしまいます。 これを間違えずに書き換えるのはかなり大変そうですよね・・・。 そこで、先ほど出てきたクォート式(Quotation)を使います。このクォート式には全部で 4種類あります。 (各クォートの型のところに書かれている Q は Quotation Monad の略です ) 式クォート(Expression quotations) 構文: [| ... |] または [e| ... |] 型: Q Exp 宣言クォート(Declaration quotations) 構文: [d| ... |] 型: Q [Dec] 型クォート(Type quotations) 構文: [t| ... |] 型: Q Type パタンクォート(Pattern quotations) 構文: [p| ... |] 型: Q Pat そして、上記のクォート式を接合(Splice)する、 $( ... ) という特別な括弧があります。 この接合用の括弧を使うと、 Q Exp 型などの抽象構文木を通常のHaskellのコードに埋め込むことができます。 1 + 2 の 2 の部分だけ抽象構文木で記述した 3 に書き換えて接合してみます。 Prelude Language.Haskell.TH> runQ [| 1 + $(return (LitE (IntegerL 3))) |] InfixE (Just (LitE (IntegerL 1))) (VarE GHC.Num.+) (Just (LitE (IntegerL 3))) 接合して抽象構文木を書き換えることができました!! そしてクォート式を使わない場合、以下のように書かないといけません。 Prelude Language . Haskell . TH > runQ ( return ( InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE ' ( GHC . Num .+)) ( Just ( LitE ( IntegerL 3 ))))) InfixE ( Just ( LitE ( IntegerL 1 ))) ( VarE GHC . Num .+) ( Just ( LitE ( IntegerL 3 ))) これはしんどいですね・・・。 さいごに この記事を書くにあたって、メタプログラミングについて勉強して、実際の業務に役に立つ何かを見つけ出そうと意気込んでいましたが、抽象構文木を実際に見るだけでも面白くなってしまい、通常のコードと抽象構文木を行ったり来たりするのに終始してしまいました・・・。 「これが具体的に何の役に立つのか?」と聞かれると困ってしまいますが、抽象構文木を操作するという新鮮な体験をすることができました :) 皆さんも新しいプログラミング言語に興味を持った際には、新しいプログラミング手法も試してみてはいかがでしょうか。
FORCIAアドベントカレンダー2019 17日目の記事です。 検索プラットフォーム事業部エンジニアの相澤です。 普段はPostgreSQLで複数の旅行会社のデータをまとめるような処理を取り扱っています。 弊社の得意な分野はまさに旅行系の「複雑かつ膨大な」在庫・料金などのデータ処理なのですが、これを高速に扱えるのであれば、他の部分に目が行くのがエンジニアのサガ。 そこで、様々な会社から入稿される施設データの中で特に厄介なものである、「フリーテキスト入力」をなんとか綺麗にできないかと考えました。 前がたり 旅行会社が持つ情報というのは、「電話番号」「緯度経度」「郵便番号」「住所」「禁煙
FORCIAアドベントカレンダー2019  17日目の記事です。 検索プラットフォーム事業部エンジニアの相澤です。 普段はPostgreSQLで複数の旅行会社のデータをまとめるような処理を取り扱っています。 弊社の得意な分野はまさに旅行系の「複雑かつ膨大な」在庫・料金などのデータ処理なのですが、これを高速に扱えるのであれば、他の部分に目が行くのがエンジニアのサガ。 そこで、様々な会社から入稿される施設データの中で特に厄介なものである、「フリーテキスト入力」をなんとか綺麗にできないかと考えました。 前がたり 旅行会社が持つ情報というのは、「電話番号」「緯度経度」「郵便番号」「住所」「禁煙・喫煙/露天風呂/インターネット環境/WiFi etcの有無」「バリアフリー/幼児/ペットetcの対応状況」というものになっているのですが、電話番号・郵便番号・緯度経度は数字の全角半角の表記ゆれがある程度でデータ管理がしやすいのに対し、施設名・住所は大抵の場合、入力する人が入力欄に書き込んだ通りにデータ入稿されるため、表記ゆれや意図しないデータが入るなど非常に管理しにくいのです。 フリーテキスト入力なものでも「説明」「補足」「口コミ」といったデータは、 最低限の処理を施したらwebサイトに掲載できるのですが(なお、HTML制御文字が混ざっていたり、HTMLが入力されていたりする場合があります。口コミや宣伝文句に<strong>とか<big>とか混ぜないでください・・・。そういったものは取り除く必要があります)、住所に関しては違います。 別々の会社から入稿されたこのデータとあのデータは、同じ施設のデータなのか、はたまた同名の異なる施設のデータなのか。住所は施設(建物)の同一性を保証する重要なデータとなります(なお建物名は表記ゆれが大きく、A棟B棟,離れ別館などがあるので案外当てになりません)。 そこで今回は フリーテキストで入力された住所を、人が同じ住所か違う住所か判断できるレベルまでPostgreSQLの文字列置換を使って正規化することに取り組んでみたいと思います。 今回は住所に関する専門的な知識は使わず、一般的な技術の範囲内で取り組んでいきます。 やっていくぞ! 目標 与えられたデータを、同じ住所のものは同じ形に正規化する関数を作成します! 準備 テーブル locationList を下記のように定義します。 論理名 物理名 型 フリーテキスト入力住所カラム address_original test 空の正規化住所カラム address_normalized text 以下のようにして置換を行い、正規化します。 CREATE OR REPLACE FUNCTION normalize(original text) RETURNS text AS $funcbody$ DECLARE result text; BEGIN result := original; -- この辺に置換処理を書く RETURN result; END; $funcbody$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; UPDATE locationList SET address_normalized = normalize(address_original); step1 :注意文言や制御文字の削除 (regexp_replace を用いた削除) 住所欄には、住所以外の文言が入力されている場合が多々あります。 例: 『(駐車場は裏にあります)』『※〇〇駅徒歩5分 』『〇〇市(旧:☓☓町)』  そのほか電話番号、セールストークetc・・・ こういうもののほとんどはカッコや記号の後ろに書いてあるので、これを目印に消しましょう。 使う関数は正規表現を扱える regexp_replace ですね!以下のようになります。 丁寧にやるならカッコの対応をきちんと種類ごとにしてもいいですが、 現実的に入力されるカッコの種類は一定ではなく、シンタックスも必ずしも一致しません。 カッコは最短マッチで消した後、残っているものも消してしまいます。 -- カッコの中身の削除 result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《).*?(\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《|\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); -- 注意文言の削除 result := regexp_replace( result ,E'(※|*|◎|~|~|★|■|◆|●|☆|□|◇|○|●|(TEL)|℡|〒).*$' ,E'' ,'g' ); HTML制御文字は & から始まり、 ; で終わりますが、入力システム側で大文字に変換されたりして、無効化されている場合もあります。大文字小文字両方に対応し、制御文字を取り除きます。 -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); step2:かな・アルファベット・数字表記ゆれ対応 (translateを用いた置換) 住所にはたくさんの"かな"が含まれますが、ひらがな・カタカナ・濁点・半濁点は意外と一致しませんので正規化します。translateという関数は、一対一対応で文字を置換してくれますので、これを使いましょう。 ちなみに濁点や半濁点は単体でも入力できますし、半角カナでは独立するので、気をつけて置換する必要があります。 -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); -- 濁点半濁点対応 result := translate( result ,'ゔヴがぎぐげごガギグゲゴざじずぜぞザジズゼゾだぢづでどダヂヅデドばびぶべぼバビブベボぱぴぷぺぽパピプペポ゚゙゛゜' ,'ううかきくけこかきくけこさしすせそさしすせそたちつてとたちつてとはひふへほはひふへほはひふへほはひふへほ' ); -- 歴史的仮名遣い対応 result := translate( result ,'ゐゑヰヱ' ,'いえいえ' ); -- 小文字を大文字に result := translate( result ,'ぁぃぅぇぉァィゥェォァィゥェォヵヶっッッゃゅょャュョャュョ' ,'あいうえおあいうえおあいうえおかけつつつやゆよやゆよやゆよ' ); -- 半角カナを全角カナに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- 全角カナをひらがなに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); step3:スペース・ハイフンの置換(unicodeを用いた置換) さて、基本的な正規化を終えるにはあと一歩ですがこれが面倒なのです。 スペース・ハイフンには実はものすごい種類があります(今回は対応しませんがチルダも厄介です)。エンジニアでもなければ全角ハイフン・半角ハイフンなどは区別しないのかもしれません。 基本的な記号は置換してしまいましょう。 全角スペース・半角スペース・タブ → 今回は 消します 全角ハイフン・半角ハイフンetc.. → 半角ハイフンに統一しましょう(全角ハイフン系の記号にすると漢数字のイチと見分けが付きにくので) ここで E'(全角スペース|半角スペース|タブ)' のようなパターンの書き方もできますが、これだと一目で見て何をやっているのかよくわからなくなってしまいますね。 スペースはまだマシですが、ハイフンなどは何が対応できていて何に対応できていないのかがわからなくなってしまいます。 ここはUnicodeを使用して置換をコントロールします。具体的には以下のようにします。 --スペースの削除 result := regexp_replace( result ,E'(\\u0020|\\u00A0|\\u0009)' ,E'' ,'g' ); -- ハイフンの統一 result := regexp_replace( result ,E'(\\u002D|\\uFF0D|\\u2212|\\u2015|\\u2010|\\u2011|\\u2012|\\u2013|\\u2014|\\uFF70|\\u4E00)' ,E'\u002D' ,'g' ); ハイフンとして長音記号を扱うかどうかは、かなり難しいところです。 というのも、 ディズニーランド のように何故かハイフンと長音を混同した入力がみられるためです(制御文字などに対しても思うのですが、素朴な疑問としてどうやって打っているのでしょうか・・・)。私は長音記号はハイフンとしませんでした。 ちなみに、「一丁目3一7」のようにハイフンの代わりに漢数字のイチが入力される場合もあり、そちらも正規化を諦めました(普通に不正データでは・・・)。 step4:住所ゆれの対応(捕捉変数を使う) 住所にはいくつかの"どちらもあっている"パターンがあります。 1. 〇〇県(〇〇郡)〇〇町 の 郡はあってもなくても良いものです。 〇〇県 + (1文字以上) + 郡 + (1文字以上)市町村 というパターンに関して"郡"を抜いてしまえば良さそうです。 100%の精度ではないですが、ある程度はカバーできそうです。 PostgreSQLの正規表現には(先読み|後読み)(肯定|否定)はありませんのでキャプチャー(捕捉変数)を用いて置換します。 -- 都道府県 + 郡 の無視 result := regexp_replace( result ,E'^(北海道|青森県|岩手県|宮城県|秋田県|山形県|福島県|茨城県|栃木県|群馬県|埼玉県|千葉県|東京都|神奈川県|新潟県|富山県|石川県|福井県|山梨県|長野県|岐阜県|静岡県|愛知県|三重県|滋賀県|京都府|大阪府|兵庫県|奈良県|和歌山県|鳥取県|島根県|岡山県|広島県|山口県|徳島県|香川県|愛媛県|高知県|福岡県|佐賀県|長崎県|熊本県|大分県|宮崎県|鹿児島県|沖縄県)(.+?郡)(.+(市|町|村))' ,E'\\1\\3\\4' ); 行政区画の中に"字"(あざ)、大字(おおあざ)が入ってくる場合があります。私の祖父の家もそういった行政区画だったのですが、住んでいる本人たちも正式にはどう書くべきか知らないようでした。 市町村名などに"字"が含まれる場合に誤って消してしまうかもしれませんが、それで混同してしまうような市町村はなさそうだったので、私はこれを無視します。 -- 字・大字は判断に使えないので無視する result := regexp_replace( result ,E'大?字' ,E'' ,'g' ); また、京都には「〇〇通り」「〇〇上ル」「下る」「入る」といった地名があるようですが、この送り仮名はあったりなかったりひらがなだったりカタカナだったりします。送り仮名は消してしまいましょう。 -- 入る・上る・下るの[る]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(入|上|下)る' ,E'\\1' ,'g' ); -- 通りの[り]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(通)り' ,E'\\1' ,'g' ); step5:番地以下の正規化(以下は新しい技術性はありません) 番地以下のカテゴリーには区切るハイフンだけではなく、丁・丁目・番・番地・番街・番町・号・ハイフンなどのバリエーションがあるので、これらをすべてハイフンで統一してしまいましょう。 余計なことはしないように、数字の後ろの場合にのみ置換をかけます。 result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); この処理には問題があります。「四ノ宮(→変換されて「四の宮」)」のような地名は、巻き込まれて「四-宮」になってしまいます。 予め、漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナのノに戻します。 また、北海道では「条」が「丁目」のように使われています。 京都やその他の地域でこのようなことはないので、北海道の「条」だけ変換をかけると良さそうですね。 -- 番地の正規化 -- 漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナに戻す result := regexp_replace( result ,E'(一|二|三|四|五|六|七|八|九|十|百|千)(の)([^0-9])' ,E'\\1ノ\\3' ,'g' ); result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); -- 北海道は「条」をハイフンとして扱う result := regexp_replace( result ,E'^(北海道.*)(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(条)' ,E'\\1\\2\u002D' ,'g' ); step6:説明や建物名を除く 住所に混入された説明や建物名を完全に除くのは難しいです。できることがあるとすれば、「最初に登場する算用数字・ハイフン群の後ろは、説明か何かと判断する」という方法です。 ただし現実には「2条河原」のように地名も算用数字で入稿される場合があるので、それは少々大雑把すぎるやり方です。 -- 不要な説明や建物名を除く result := regexp_replace( result ,E'^(.*?)((0|1|2|3|4|5|6|7|8|9|\\u002D)+)(.*)$' ,E'\\1\\2' ); result := regexp_replace( result ,E'\\u002D$' ,E'' ,'g' ); 結果 これらの処理を行うことで、私の扱っているデータからは句読点や記号は綺麗サッパリ消すことができました。 しかし、今回作った関数ではうまく扱えていないパターンもありますのでご紹介します。 建物の名前の扱い 記号やそのほか様々なものが入ってきており、住所に建物の名前が入っているパターンでは、建物の名前をきれいにすることができませんでした。 今回は建物自体が同じかどうかを判断するものだったので、無視しましたが、 テナント等を判別する場合難しい課題になりそうです。 千葉県浦安なのに住所が「東京都浦安~」となっているもの 千葉県の「東京」ディズニーラントだけではなく、千葉・埼玉にこのパターンが結構ありました(データ提供サイトのご都合かもしれません)。 郵便番号などその他のデータをもとに上書きしてしまったほうが良いかもしれません。 旧字の統一 旧字体が正式な住所の場合、新字と旧字が混ざってしまうパターンがあります。 そういったパターンすべてを洗い出してtranslateすれば良いのですが、洗い出しができませんでした。 漢数字・ローマ数字 漢数字やローマ数字を算用数字に置換することができませんでした。 うまいやり方があるのでしょうか・・・ 京都の住所の一部 「 〇〇番地〇〇通り〇〇丁目 」のように、丁目以下にも細かい情報が入ってきて対応しきれていません。 ハイフンと漢数字のイチと長音記号 「星のリゾート」のような記載や「三丁目一2(ハイフンではなく漢数字のイチ)」のようなパターンへの対応ができませんでした。 住所の真ん中に説明がガンガン入ってくるパターン 顔文字 取り除ききれない部分がありました。 上記に関しては私では解消しきれませんでしたが、住所や日本語の知識をつければ対応できるものもありそうです! 完成品 CREATE OR REPLACE FUNCTION normalize(original text) RETURNS text AS $funcbody$ DECLARE result text; BEGIN result := original; --スペースの削除 result := regexp_replace( result ,E'(\\u0020|\\u00A0|\\u0009)' ,E'' ,'g' ); -- ハイフンの統一 result := regexp_replace( result ,E'(\\u002D|\\uFF0D|\\u2212|\\u2015|\\u2010|\\u2011|\\u2012|\\u2013|\\u2014|\\uFF70|\\u4E00)' ,E'\u002D' ,'g' ); -- カッコの中身の削除 result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《).*?(\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); result := regexp_replace( result ,E'(\\(|(|\\[|「|【|『|〈|《|\\)|)|\\]|」|】|』|〉|》)' ,E'' ,'g' ); -- 注意文言の削除 result := regexp_replace( result ,E'(※|*|◎|~|~|★|■|◆|●|☆|□|◇|○|●|(TEL)|℡|〒).*$' ,E'' ,'g' ); -- 制御文字の削除 result := regexp_replace( result ,E'(&|&).+?(;|;)' ,E'' ,'g' ); -- 濁点半濁点対応 result := translate( result ,'ゔヴがぎぐげごガギグゲゴざじずぜぞザジズゼゾだぢづでどダヂヅデドばびぶべぼバビブベボぱぴぷぺぽパピプペポ゚゙゛゜' ,'ううかきくけこかきくけこさしすせそさしすせそたちつてとたちつてとはひふへほはひふへほはひふへほはひふへほ' ); -- 歴史的仮名遣い対応 result := translate( result ,'ゐゑヰヱ' ,'いえいえ' ); -- 小文字を大文字に result := translate( result ,'ぁぃぅぇぉァィゥェォァィゥェォヵヶっッッゃゅょャュョャュョ' ,'あいうえおあいうえおあいうえおかけつつつやゆよやゆよやゆよ' ); -- 半角カナを全角カナに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- 全角カナをひらがなに result := translate( result ,'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン' ,'あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわおん' ); -- アルファベットの正規化 result := translate( result ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate( result ,'abcdefghijklmnopqrstuvwxyz' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate( result ,'abcdefghijklmnopqrstuvwxyz' ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ' ); result := translate(result ,'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ' ,'ABGDEZHQIKLMNXOPRSTUFCYW' ); result := translate(result ,'αβγδεζηθικλμνξοπρστυφχψω' ,'ABGDEZHQIKLMNXOPRSTUFCYW' ); -- 数字の正規化 result := translate(result ,'0123456789ⅰⅠⅱⅡⅲⅢ' ,'0123456789121233' ); -- 都道府県 + 郡 の無視 result := regexp_replace( result ,E'^(北海道|青森県|岩手県|宮城県|秋田県|山形県|福島県|茨城県|栃木県|群馬県|埼玉県|千葉県|東京都|神奈川県|新潟県|富山県|石川県|福井県|山梨県|長野県|岐阜県|静岡県|愛知県|三重県|滋賀県|京都府|大阪府|兵庫県|奈良県|和歌山県|鳥取県|島根県|岡山県|広島県|山口県|徳島県|香川県|愛媛県|高知県|福岡県|佐賀県|長崎県|熊本県|大分県|宮崎県|鹿児島県|沖縄県)(.+?郡)(.+(市|町|村))' ,E'\\1\\3\\4' ); -- 字・大字は判断に使えないので無視する result := regexp_replace( result ,E'大?字' ,E'' ,'g' ); -- 入る・上る・下るの[る]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(入|上|下)る' ,E'\\1' ,'g' ); -- 通りの[り]はあったりなかったりカタカナだったりするので消す result := regexp_replace( result ,E'(通)り' ,E'\\1' ,'g' ); -- 番地の正規化 -- 漢数字 + ノ + 数字以外("四ノ宮"など) は特別に扱うためにカタカナに戻す result := regexp_replace( result ,E'(一|二|三|四|五|六|七|八|九|十|百|千)(の)([^0-9])' ,E'\\1ノ\\3' ,'g' ); result := regexp_replace( result ,E'(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(丁目?(の|\\u002D)?|番(地|町|街)?(の|\\u002D)?|の|号)' ,E'\\1\u002D' ,'g' ); -- 北海道は「条」をハイフンとして扱う result := regexp_replace( result ,E'^(北海道.*)(0|1|2|3|4|5|6|7|8|9|〇|一|二|三|四|五|六|七|八|九|十|百|千)(条)' ,E'\\1\\2\u002D' ,'g' ); -- 不要な説明や建物名を除く result := regexp_replace( result ,E'^(.*?)((0|1|2|3|4|5|6|7|8|9|\\u002D)+)(.*)$' ,E'\\1\\2' ); result := regexp_replace( result ,E'\\u002D$' ,E'' ,'g' ); RETURN result; END; $funcbody$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; 最後に フォルシアではデータクレンジングを専門にしているエンジニアもいますが、今回は私の勉強も兼ねていたので、自力で試行錯誤しました。 正規表現はパズルみたいな面白さもあり、仕様には知識だけではなく経験も必要なので、適切な課題を設定して取り組むのは面白いですね。
FORCIAアドベントカレンダー2019  16日目の記事です。 初めまして!今年の4月にキャリア入社しました、営業の佐塚と申します。 去年の3月までは、福井県の放送局でアナウンサーとして働いていました。 なぜまったく違うITの世界に飛び込んだのかは、涙なしには語れない壮大な物語があるのですが(嘘です)、 話すと半日はかかってしまうので(嘘です)、こちらでは簡単なご紹介だけとさせていただきます。 さて、みなさんは「四角」ってお好きですか? 私は中でも平行四辺形が大好きで、辺ABを1秒に2cmずつ移動する点Pとは懇意にしていました。 間違えました。「四角」ではなく「死角」ですね。 相手のに入れたときはうれしいですが、相手に入られたときはヒヤッとしますよね。 今日は「四角」でも「死角」でもなく、「資格」について、IT分野の資格「ITパスポート」 通称iパスを受けてきましたので、その時の体験をご紹介できればと思います。 ITパスポートとは ITパスポートを主催する情報処理推進機構のwebサイトでは、このように紹介されています。 iパスは、ITを利活用するすべての社会人・これから社会人となる学生が備えておくべきITに関する基礎的な知識が証明できる国家試験です。 そうです、泣く子も黙る国家資格なのです。エッヘン! 建築士や公認会計士、調理師、美容師といったその道のプロフェッショナルともいえる国家資格が、実はITの分野にもあるんですね。 と言っても、難しく考えたり、構えたりする必要は全くありません。上の説明にもある通り、iパスは「ITに関する基礎的な知識が証明できる国家試験」ですから、IT分野の「基本のキ」とも言える問題が出題されます。初心者でも大丈夫。むしろ、初心者こそ、文系こそ、非エンジニアこそウェルカムな試験なのです。 私の受験理由 そんなITパスポートを、私が受けようと思った理由は「ITの分野に関する知識を少しでもつけたかったから」です。 全く違う業界からITの業界に飛び込んだので、少しでも早く、ITに関する知識をつけたいと思っていました。資格取得だけがその手段ではありませんが、目標を設定して、そこに向けて勉強していくというのは、私の性分には合っていたかなと思います。知識を習得したという客観的な証明にもなりますよね。 それと、フォルシアに「資格取得支援制度」があったことも大きかったです。 フォルシアでは、業務に関わる資格を取得できた場合、受験料と、勉強に使用した書籍代を補助してもらえる制度があります。個人のスキルアップを会社として後押ししてもらえるというのは、とても励みになります。金銭的にも助かりました。 申し込んでから試験当日まで ITパスポートは、全国の会場で、比較的頻繁に試験が行われているので、試験日程は選びやすかったです。私は1か月ほど前から勉強を始めて、2週間程度で参考書を1冊一通り読んだら、残りの2週間は、ひたすら過去問題集を解くという勉強方法を取りました。 入門的な試験なので、難しい計算をしたり、プログラミング言語を深く理解していないと解けないような問題はありませんが、例えばハードウェアに関することからデータベースに関すること、マネジメント、法務、経営戦略など、広い分野から問題が出ます。 特定の分野の得点が低いと合格できないシステムのため、全分野覚えるべきことをきちんと覚える必要があります。私にとってはこれが大変だったので、とにかく過去問を解いて、間違えた問題を2日後ぐらいにまた解いて......と繰り返すことで覚えていきました。 試験当日 iパスは、IT系の資格らしく、CBT形式と呼ばれる、試験会場のコンピュータ上で問題に解答する形式です。ペーパーテストに慣れていると少し違和感があるかもしれませんが、無駄が少なく、また終了時に、即座に自分の点数がコンピュータ上に表示されるため、どのくらいできたのかをすぐに知ることもできます。 学生の頃のテストでは、シャーペンの音が響いていましたが、iパスではマウスの「カチカチ」という音が会場に響いていたのが面白く心地よかったです(会場にはヘッドホンも置いてあったので、気になる方はそれを付けることもできます)。 受験してみて やはり「受けてよかった」という気持ちが強いです(無事合格しました!)。 というのも、IT分野の国家試験には、他にも様々な種類があり、私は現在、「基本情報技術者」の取得を目指して、次の勉強を始めているのですが、iパスで学んだことが基礎となって、より詳しい内容を学ぶことができていると感じます。 基本情報技術者は、ぐっと学ぶ内容が多くなり、名前についている「基本」の文字に「本当か!?」と言いたいぐらい、非エンジニアの私にとっては難しく感じています。それでも、この勉強を「面白い」とも感じられているのは、iパス受験時に、基本的なことをきちんと学習できたからに他なりません。 例えば、コンピュータが「2進数」を用いて、実際どのようにして四則演算を行っているかや、そのほかの処理にどのように応用しているかということは、iパスで基本を学び、基本情報技術者の勉強で「なるほどそういうことだったのか!」とさらに理解を深めることに繋がっていて、楽しさが広がっていくように感じています。 これからIT系の企業への就職を目指している学生のみなさん、転職でIT分野に飛び込もうとしている社会人のみなさん、そして、私と同じようにIT分野初心者の駆け出し営業の方に、少しでも参考になれば嬉しいです。
FORCIAアドベントカレンダー2019 14日目の記事です。 2019新卒入社の東川です。この記事ではシャッフルランチという社内交流企画で現れた最適化問題に対して、強化学習を適用した事例についてご紹介します。 ! 2019年12月時点の情報です (※記事が古いため、リンク先が失われている場合があります) シャッフルランチとは フォルシアで行っているシャッフルランチとは業務上の関わりの薄い社員同士のコミュニケーション促進のために月一回開催している社内企画であり、Slackの特定のチャンネルにjoinした参加希望者を自動的に3-4人のグループに分けて、それらのグループでランチに行くと
FORCIAアドベントカレンダー2019  14日目の記事です。 2019新卒入社の東川です。この記事ではシャッフルランチという社内交流企画で現れた最適化問題に対して、強化学習を適用した事例についてご紹介します。 シャッフルランチとは フォルシアで行っているシャッフルランチとは業務上の関わりの薄い社員同士のコミュニケーション促進のために月一回開催している社内企画であり、Slackの特定のチャンネルにjoinした参加希望者を自動的に3-4人のグループに分けて、それらのグループでランチに行くというものです。シャッフルランチの費用は会社が負担しています。 (開催の背景については こちらの記事 、技術的側面については こちらの記事 をご覧ください) 幸い社内でも好評であり、毎月60人強の社員が継続して参加しています。 フォルシアのシャッフルランチの特徴は、グループ分けが完全にランダムではなく、 参加者間の会社での関わりができるだけ小さくなるように グループ分けされているという点です。これにはできるだけ普段の業務で関わりのない人と交流してほしいという運営側の意図があります。参加者間の「会社での関わり」の指標としては「Slackの共通で加入しているチャンネルの数」を使用しており、これを 親密度 と呼ぶことにします。会社での関わりが薄い2人の社員は共通で加入しているSlackチャンネルの数も少ないと考えられるためです。 組み合わせ最適化としてのシャッフルランチ シャッフルランチを最適化問題として定義するには何らかの指標が必要です。 グループ内のコスト をグループに属する2者間の親密度の合計とし、 グループ分けのコスト を、全グループのグループ内のコストの総和として定義します。このコストが最小化されるとき、会社での関わりができるだけ小さくなるようなグループ分けになっているはずです。 わかりやすい例として、人を3人ずつのグループへ分割して考えましょう(図1)。 まず、2者間の関係性を点数化し、図1(a)のような図を作成します。続いて、図1(b)のような3人ずつのグループ分けを考えます。この時「グループ内のコスト」はそれぞれ5+3+5(=13), 8+1+5(=14)であり、「グループ分けのコスト」は13+14=27です。図1(c)の「グループ分けのコスト」は70であり、図1(b)のグループ分けは図1(c)のグループ分けに比べて、参加者間の会社での関わりが小さいグループ分けになっているということができます。 図1: 6人組を3人+3人のグループに分割する場合。数字は2者間の共通のチャンネルの数を表します。 図(b)の「グループ分けのコスト」は27で、図(c)の「グループ分けのコスト」は70です。 シャッフルランチの組み分けにおいて会社での関わりができるだけ小さい組み分けを探し出すことは重要であり、実際アンケートを見ると「普段関わりのない人と話す機会ができてよかった」という声がたくさんあります。一方で、上記の設定にしたがって「グループ分けのコスト」を最小にするグループ分けを求めるのは、以下の2つの観点から難しい問題です。 考えうる組み合わせが膨大である 上の6人を分ける問題ではグループ分けの数は 6 ! / ( 3 ! × 3 ! ) = 20 通りで全探索が可能です。一方、この組み合わせの数は全体の人数とともに爆発的に増えます。例えば、60人を4人ずつのグループに分ける場合の数は 60 ! / ( 4 15 × 15 ! ) ≒ 6 × 10 60 通りあり、これを網羅的に調べるのは現実的ではありません。 グループ分けのコスト を最小化するのは難しい ある一つのグループ分けに対して グループ内のコスト を最小化するのは簡単ですが、一つのグループ分けを最小に使用すると往々にして他のグループのコストが上がってしまうことがあります。 グループ内のコストの最小化と全体のコストの最小化はトレードオフの関係にあり、ちょうど良いところを定量的に決めるのは難しい問題です。 上記2つの問題に対して、流行りの強化学習が適用できるのではないかと思い、実際に試してみました(あと私自身、強化学習に触れてみたかったという理由もあります)。 以下では、そのインストール方法とサンプルコードを中心に説明します。深層学習や強化学習のライブラリやフレームワークを使用するのは私にとって今回が初めてでしたが、そのインストールの簡便さとコーディングのインターフェースの簡単さには感銘を受けました。 この記事が、組み合わせ最適化問題をできるだけ手軽に解きたい、深層強化学習のライブラリに触れてみたいという方の一助になれば幸いです。 強化学習とは?強化学習に必要な各種フレームワークとライブラリのインストール 強化学習とは近年流行している機械学習の一つの分野であり、GoogleのAlpha Goでも使用されている技術として一躍注目を集めました。機械学習には大まかに教師あり学習、教師なし学習、強化学習の3つの分野があります。 教師あり学習:多数の教師データを基に入出力の関係を学習するものです。画像分類などに使用されます。 教師なし学習:多数のデータを基にデータのカテゴライズを学習するものです。クラスタリングなどに使用されます。 強化学習:多数の試行錯誤を基に最適な戦略を選び出し、「価値」を最適化するものです。 教師なし学習や教師あり学習では多数のデータが必要ですが、強化学習では必要ありません。多数回の試行錯誤を通してデータが作成され、それを基に自律的に学習が進むからです。 強化学習の最適戦略の探索において、AIの核となる技術である深層学習が使用されており、しばしば深層強化学習と呼ばれることもあります。以下では深層強化学習の中でもQ学習、とそこで用いられる手法であるDQN(Deep Q Network)にフォーカスして解説します。 強化学習の問題設定 強化学習では、 エージェント とエージェントが制御する対象である 環境 を考えます。強化学習で重要になる概念が選択 action 、状態 state 、観測データ observation 、報酬 reward です(図2)。 エージェントは各ステップで何らかのactionを選択し環境のstateが変化します。環境は変化後のstateに対してobservationを計算し、observationとrewardを返却します。エージェントは多数の試行錯誤からactionとobservation, rewardの関係性を学習し、rewardを最大化するような最適戦略を導き出します。 今回の例では環境はグループ分けの全体であり、actionは例えば2つのグループの人を交換するという操作、observationは各グループの点数などです。rewardは現在のグループ分けのコストに-1をかけたものであり、rewardの最大化はグループ分けのコストの最小化と同値です。 一点注意するべき点はrewardを最大化する戦略に収束させるためには、取りうるactionの値(action_space)とstateの取りうる値の空間(observation_space)を適切に選ばなければいけないしないということです。人間の学習プロセスがそうであるように、可能なactionが著しく少なかったり、stateから抽出された値がrewardの最大化にあまり関係のないものだったりする場合、強化学習を通して上手い最適戦略が得られない場合もあります。 図2: 強化学習の基本概念。エージェントはactionに対するstateの挙動を見て、試行錯誤しながらrewardを最大化します。 例は今回のグループ分けの最適化において使用されたものを表します。 今回は強化学習ライブラリとしてkeras-rl+gymを使用しました。強化学習のフレームワークに触れるのは私にとって初めての経験でしたが、離散最適化問題のソルバーとして強化学習を用いることのメリットとして以下のような点を感じました。 ライブラリの導入コストが低く、必要となるコーディングも少ない フレームワークにしたがって、直感的に書くことができる デバッグツールと可視化ツールが豊富で開発がしやすい tensorflow, keras, keras-rl, gymのインストール keras とはPythonで書かれたニューラルネットワークライブラリであり、その使いやすさから深層学習の研究と実践で広く使用されています。今回はバックエンドとして tensorflow を使用するので、それもインストールします。インストールはpipを使って以下のコマンドで出来ます。 pip install tensorflow pip install keras 深層強化学習をするために、さらに keras-rl と OpenAI gym をインストールします。keras-rlとはkerasを利用してDQNなどの深層強化学習を実装できるライブラリであり、gymは強化学習に必要な一連の変数を定義するために必要なライブラリです。下で見るように上述のaction, stateなどの枠組みはgymのクラスを継承することで作成できます。 git clone https://github.com/matthiasplappert/keras-rl.git pip install ./keras-rl pip install gym 強化学習による最適なグループ分けの探索 問題設定と強化学習の環境との対応 n m 人を n g 人ずつのグループに分けることを考えます。すべてのメンバーにインデックス i ( = 1 , 2 , . . . n m ) を貼ります。各グループ分けを表現するために長さ n m の配列 G を考え、 i 番目の人がグループ G [ i ] に属しているとします。 例えば、 G [ 3 ] = 1 であれば 3 番目のメンバーはグループ 1 に所属しているという形です(図3(a))。 i とメンバー j の親密度を R [ i , j ] として、これを行列要素に持つような n m × n m 行列 R を定義し、これを親密度行列と呼ぶことにします(図3(b))。この時、グループ分け G に対して g 番目のグループのコストとグループ分け全体のコストはそれぞれ図3(c), (d)のように表現されます。 図3: グループ分け G と親密度行列 R 、コスト C の説明。 χ は定義関数で χ ( t r u e ) = 1 , χ ( f a l s e ) = 0 です。 今回の問題と強化学習の変数との対応は下表の通りです。stateはグループ分け G 、rewardは − C です。actionは G から新しいグループ分け G ′ を生成する操作であり、例えばメンバーの入れ替えなどであり、observationは例えば現在のグループからメンバーの入れ替えをした時の点数の変化などをとります。 state グループ分け G (図3(a))、例: [ 0 , 2 , 1 , 2 , 0 , 1 ] ( 6人を3グループに分ける場合) action 新しいグループ分け G ′ を作る操作、例: メンバー 2 と 3 の入れ替え G = [ 0 , 1 , 2 , 2 , 0 , 1 ] ⇒ G ′ = [ 0 , 2 , 1 , 2 , 0 , 1 ] observation 例: 各グループの点数 reward グループ分けのコストに − 1 をかけたもの ( − C ) (図3(d)) 表1: 強化学習の変数state, action, observation, rewardと今回の問題との対応 学習環境の定義 最適化の環境を表すクラス(下では GroupingEnv としています)はgymのクラスcore.Envを継承して作成します。 class GroupingEnv(gym.core.Env): # クラスの定義 core.Envを継承するには5つのメソッドstep, reset, render, close, seedと3つのプロパティaction_space, observation_space, reward_spaceを実装する必要があります。 step : 各ステップで実行される関数です。actionを引数に取り、observation, reward, is_done(終了条件を満たしたかどうか), infoを出力します。今回のケースでは、actionの値にしたがってメンバーを入れ替える⇒メソッドget_observationで入れ替え後の状態に対して観測データを得る⇒メソッドget_rewardで報酬を計算する⇒終了条件を満たしているかを判定する、の4ステップを入れました。 # 各ステップで実行される操作 def step(self, action): self.time += 1 # step1: actionに戻づいてグループ分けを更新する self.grouping = self.exchange_member(self.grouping, action).copy() # step2: 観測データ(observation)の計算 observation = self.get_observation() # step3: 報酬(reward)の計算 reward = self.get_reward(self.grouping) # step4: 終了時刻を満たしているかの判定 done = self.check_is_done() info = {} return observation, reward, done, info infoは必要なければ空ディレクトリにします。 reset : 状態の初期化をする関数です。出力はstateから得られる観測データobservationとします。今回は以下のようにランダムなグループ分けに初期化するようにしました。 def reset(self): # 変数の初期化。ランダムなグループ分けに初期化する self.time = 0 self.grouping = self.get_random_grouping() return self.get_observation() # ランダムなグループ分けを得るための関数 def get_random_grouping(self): grouping = list(range(self.n_group)) * int(self.n_member / self.n_group) random.shuffle(grouping) return grouping render , close , seed : render, close, seedはそれぞれ、環境の可視化、終了時の処理、乱数の固定を表す関数です。特に必要なければ、passとします。 def render(self, mode): # 画面への描画 pass action_space : action_spaceとはactionの取りうる値の空間を指します。actionとして2人のメンバーの入れ替えを採用しているため、メンバー n m に対して、actionとしては n m × ( n m − 1 ) / 2 通りの選択肢あります。これだとメンバー数の増加とともに学習のコストが増大してしまうため、今回は「可能なactionの集合に対してactionにしたがって組み替えたときのコストを計算し、コストの小さい10通りのうち何番目を選ぶか」という設定にしました(かなり経験的な方法に寄せています)。 self.n_action = 10 self.action_space = gym.spaces.Discrete(self.n_action) # actionの取りうる値 ここで gym.spaces.Discrete(N) は N 個の離散値の空間を表します。 observation_space : observation_spaceとは観測データの取りうる値の空間を表します。今回の場合、上記のコストが小さくなるような10通りのメンバーの入れ替えのコストを観測データとしました。 self.observation_space = gym.spaces.Box(low=-10, high=10, shape=(self.n_action,)) # 観測データの取りうる値 必要に応じてstateからobservationを計算するための関数 get_observation 、rewardを計算するための関数 get_reward 、終了条件を満たしているかを判定する関数 check_is_done を定義します。 最後に学習の終了条件を定義します。今回は終了条件は単純に「ステップ数の上限(20回)を超えたら終了する」としました。 # 終了条件を判定する関数 def check_is_done(self): # 最大数に達したら終了する return self.time == self.max_step 出来上がったコード全体は以下の通りです。 import gym.spaces import copy import numpy as np import random class GroupingEnv(gym.core.Env): metadata = {'render.modes': ['human', 'rgb_array']} def __init__(self, relationship_matrix, n_group, n_member): self.relationship_matrix = relationship_matrix # 2者間の関係性を表す行列 self.n_member = n_member # 全体のメンバー数 self.n_group = n_group # グループの数 self.grouping = list(range(self.n_group)) * int(self.n_member / self.n_group) # グループ分け self.group_id_list = list(range(n_group)) # groupのidのリスト self.n_action = 10 self.action_space = gym.spaces.Discrete(self.n_action) # actionの取りうる値 self.observation_space = gym.spaces.Box(low=-10, high=10, shape=(self.n_action,)) # 観測データの取りうる値 self.time = 0 # ステップ self.max_step = 20 # ステップの最大数 self.pair_list = self.get_pair_list() self.candidate_list = [] # 各ステップで実行される操作 def step(self, action): self.time += 1 # step1: actionに戻づいてグループ分けを更新する self.grouping = self.exchange_member_action(self.grouping, action).copy() # step2: 観測データ(observation)の計算 observation = self.get_observation() # step3: 報酬(reward)の計算 reward = self.get_reward(self.grouping) # step4: 終了時刻を満たしているかの判定 done = self.check_is_done() info = {} return observation, reward, done, info def reset(self): # 変数の初期化。ランダムなグループ分けに初期化する self.time = 0 self.grouping = self.get_random_grouping() return self.get_observation() def render(self, mode): # 画面への描画 pass def close(self): # 終了時の処理 pass def seed(self): # 乱数の固定 pass # 報酬を計算する関数 def get_reward(self, grouping): return -1 * self.total_grouping_cost(grouping) # 報酬 = グループ分けgroupingのコスト*(-1) # 観測データを計算する関数 def get_observation(self): grouping = self.grouping.copy() candidate_list = [] for pair in self.pair_list: id1, id2 = pair cost = self.get_reward(self.exchange_member_pair(grouping, id1, id2)) candidate_list.append([id1, id2, cost]) self.candidate_list = sorted(candidate_list, key=lambda x:-x[2])[0:self.n_action] return [cand[2] for cand in self.candidate_list] # 終了条件を判定する関数 def check_is_done(self): # 最大数に達したら終了する return self.time == self.max_step # actionの値に応じて、2人のグループを交換する関数 def exchange_member_action(self, grouping, action): new_grouping = grouping.copy() id1, id2, cost = self.candidate_list[action] new_grouping[id1] = grouping[id2] # id1のメンバーとid2のメンバーを交換 new_grouping[id2] = grouping[id1] return new_grouping # id1, id2のメンバーを交換する関数 def exchange_member_pair(self, grouping, id1, id2): new_grouping = grouping.copy() new_grouping[id1] = grouping[id2] new_grouping[id2] = grouping[id1] return new_grouping # ランダムなグループ分けを得るための関数 def get_random_grouping(self): grouping = list(range(self.n_group)) * int(self.n_member / self.n_group) random.shuffle(grouping) return grouping # グループ分けのコストを計算する関数 def total_grouping_cost(self, grouping): return sum([self.group_cost(grouping, group_id) for group_id in self.group_id_list]) # グループgroup_idのコストの計算 def group_cost(self, grouping, group_id): group = [i for i, _x in enumerate(grouping) if _x == group_id] n_pair = len(group) * (len(group) - 1) return self.relationship_matrix[np.ix_(group, group)].flatten().sum() / n_pair # 可能なペアの列挙 def get_pair_list(self): pair_list = [] for id1 in list(range(self.n_member)): for id2 in list(range(self.n_member)): if id1 定義された環境を使って学習環境を定義します。例えば20人を4つのグループに分割する問題を考えるときは以下のようにします(親密度行列 R は本来Slackから取得される情報を使って定義されるべきですが、今回は簡単のため乱数を要素とする行列としました)。 # 組み分けの環境の定義 n_member = 20 n_group = 4 n_action = 10 # 親密度行列の定義 # NOTE: ランダム行列で代用 relationship_matrix = np.array([random.random() for _ in range(n_member ** 2)]).reshape(n_member, n_member) env = GroupingEnv(relationship_matrix, n_group, n_member) ニューラルネットワークの定義と訓練 続いて、この環境に最適な行動を求めるためのモデルをニューラルネットワークで求めます。 from keras.models import Sequential from keras.layers import Dense, Activation, Flatten from keras.optimizers import Adam from rl.agents.dqn import DQNAgent from rl.policy import BoltzmannQPolicy from rl.memory import SequentialMemory # ニューラルネットワークの構造を定義 model = Sequential() model.add(Flatten(input_shape=(1,) + env.observation_space.shape)) model.add(Dense(128)) model.add(Activation('relu')) model.add(Dense(n_action)) model.add(Activation('linear')) print(model.summary()) # モデルの定義をコンソールに出力 # モデルのコンパイル memory = SequentialMemory(limit=50000, window_length=1) policy = BoltzmannQPolicy(tau=1.) dqn = DQNAgent(model=model, nb_actions=n_action, memory=memory, nb_steps_warmup=50, target_model_update=1e-2, policy=policy) dqn.compile(Adam(lr=1e-3), metrics=['mae']) ニューラルネットワークの中間層は1個としました。 モデルの訓練は以下の通りです。 # 訓練 history = dqn.fit(env, nb_steps=5000, visualize=False, verbose=2, nb_max_episode_steps=300) 訓練中は以下のようなレコードが出て、rewardやmean_qの推移が表示されます。 mean_qは学習の進み具合を表すパラメータであり、これの収束は学習が終了したことを表します。 200/5000: episode: 1, duration: 2.922s, episode steps: 200, steps per second: 68, episode reward: -298.082, mean reward: -1.490 [-1.593, -1.376], mean action: 31.190 [0.000, 63.000], mean observation: 0.547 [0.000, 1.000], loss: 0.052949, mae: 1.083208, mean_q: -0.088404 400/5000: episode: 2, duration: 1.784s, episode steps: 200, steps per second: 112, episode reward: -298.902, mean reward: -1.495 [-1.593, -1.376], mean action: 31.120 [0.000, 63.000], mean observation: 0.547 [0.000, 1.000], loss: 0.011003, mae: 2.727983, mean_q: -2.161458 600/5000: episode: 3, duration: 1.781s, episode steps: 200, steps per second: 112, episode reward: -298.719, mean reward: -1.494 [-1.590, -1.388], mean action: 32.490 [0.000, 63.000], mean observation: 0.547 [0.000, 1.000], loss: 0.048550, mae: 4.750234, mean_q: -3.986667 最後に評価をします。ログを記録するために、 rl.callbacks.Callback を拡張したクラス EpisodeLogger を作成し、 dqn.test のコールバックとして渡します。このように簡単に学習過程のログが作れることもkeras-rlの魅力の一つです。 import rl.callbacks # ログを記録するためのクラスの定義 class EpisodeLogger(rl.callbacks.Callback): def __init__(self): self.rewards = {} def on_episode_begin(self, episode, logs): self.rewards[episode] = [] def on_step_end(self, step, logs): episode = logs['episode'] self.rewards[episode].append(logs['reward']) episode_logger = EpisodeLogger() nb_episodes = 100 dqn.test(env, nb_episodes=nb_episodes, visualize=False, callbacks=[episode_logger]) テスト中は以下のようなレコードが出て、各エピソードの終了時点でのrewardが出力されます。 Testing for 100 episodes ... Episode 1: reward: -60.495, steps: 20 Episode 2: reward: -61.666, steps: 20 Episode 3: reward: -59.772, steps: 20 上では100個のランダムなグループ分けを初期値として、最適化をしています。ステップごとのrewardの平均は図4左のように推移し、ステップとともにrewardが増加(すなわちグループ分けのコスト C が減少)していることがわかります。 random_sampling = [env.get_reward(env.get_random_grouping()) for _ in list(range(10000))] import matplotlib.pyplot as plt plt.plot(mean_reward_list, label='N={}'.format(nb_episodes)) plt.xlabel('step', fontsize=18) plt.ylabel('mean reward', fontsize=18) plt.legend(loc='upper right', fontsize=18) plt.show() # plt.savefig('mean_award.png') 図4右は各エピソード終了時点でのrewardの頻度分布です。青のヒストグラムはN=10000でランダムにグループ分けを生成したときの頻度分布であり、強化学習を用いた最適化の方法のほうが系統的に良い結果を与えていることがわかります。 図4: (左)ステップ数に対するrewardの推移(100回平均)。 (右)エピソード終了時点でのrewardの値のランダムサンプリングとの比較 最後に この記事ではシャッフルランチで出てきた離散最適化問題を例にして、強化学習により離散最適化問題の解法について紹介しました。今回実装してみて、組み合わせ最適化問題のソルバーに強化学習のフレームワークを使用することにはいくつかのメリットがあると感じました。 手軽。ライブラリをインストールして、動くものを作るまでそこまで時間がかからない action, reward, step, resetなどライブラリで使用されている用語が直感的で実装しやすい 組み合わせ最適化問題には汎用的な解法がなく難しいところがありますが、強化学習のライブラリであるkeras-rl + gymを組み合わせることで、お手軽にそこそこの精度のものが開発できるのではないかと感じました。各ステップでの実行プロセスやリセット時の操作、ログ出力などスクラッチで書くとなかなか大変ですが、ライブラリの使用でそのコストを大幅に下げることができます。実際、上の程度の量であれば、ライブラリのインストールから動くものを作るまで半日程度の時間があれば十分でした。 一方で以下のような難しさも感じました。 observationとして与える特徴量やactionとして取りうる選択肢を決めるのが難しい。うまいobservation, actionの組み合わせを与えないと全く学習してくれない 変更できるパラメータがネットワークの構造や訓練の各種パラメータなど多岐にわたり、チューニングが難しい 特に一点目については試行錯誤が必要であり、苦労しました。AIだからうまく学習してくれるかなと思って、observationとして次元が大きな特徴量を加工せずにそのまま入れたり、actionの選択肢の数を大きくしてみたりしたのですがほとんど学習が進みませんでした。 実際に、上ではobservationやactionとして取りうる選択はほとんど経験的な方法を採用してしまっており強化学習の良さを殺してしまっていると感じました。また、今回実装したものが深層強化学習を使用しないアルゴリズムよりも良いものであるかどうかは非自明です。 何をどう学習させるか、AIを使うのは簡単ですが、その「気持ち」と「ふるまい」を理解して使いこなすのは難しいものだと感じました。
FORCIAアドベントカレンダー2019  13日目の記事です。 旅行プラットフォーム事業部の小海です。 まもなく創業20周年となるフォルシアですが、フォルシアの技術・検索プラットフォーム Spook は日々進化を続けています。 膨大で複雑なデータに合わせて、最適な検索を実現するための「技術基盤」であるSpookには様々な技術が使われていますが、昨年から今年にかけて新しいWebアプリケーションフレームワークを開発・導入しました。 この記事では、その新しいWebアプリケーションフレームワークの技術的な側面ではなく、検討・開発・導入に至るまでの道のりを紹介します。 Webアプリケーションフレームワークとは? 「Webアプリケーションフレームワーク」とは、ブラウザなどで動作するWebアプリケーションを効率的に開発するためのフレームワークです。案件によってサイトやページ構成などは異なりますが、「URLの解釈」「htmlを作成」「データベース接続」など基本的に必須の共通機能が多くあります。「Webアプリケーションフレームワーク」を使用することで、これら様々な共通する機能を1から作る必要がなくなります。 世の中には「Ruby on Rails」「Django」「Angular」「Vue」などがあり 、多くのWebエンジニアがそれらを利用してWebアプリを開発しています。 背景 フォルシアには、2004年頃と2013年頃にそれぞれ作られた社内製フレームワークが2つあります。これらのフレームワークはとても深く考えて作られており、今まで多くのサイトを生み出してきました。 しかし、急速に成長し続けるWeb業界の新しい技術や考え方・デバイスに追随できていない部分が少なからずあり、新しい言語や技術が世の中に出るたび、社内の新フレームワークを求める機運が次第に高まってきました。 きっかけ そんな中、エンジニアたちのディスカッションで新フレームワークの話があがり、新フレームワーク検討が本格化してきました。新フレームワークを開発する場合に何が必要かを洗い出した結果、下記の検討が必要となりました。 言語はどうするのか 世の中にあるものか社内製を開発するか 言語やコミュニティの将来性・信頼性 実行速度 学習コスト 世の中に存在するフレームワークのメリット・デメリットや、社内製フレームワーク開発のメリット・デメリットを話し合う中で、実際にハッカソン形式で様々な言語やフレームワークを使ってみようということになりました。 ハッカソン ハッカソンの企画は当時新卒2年目だったエンジニアが主体となって2018年3月に行われました。オンライン参加を含めると参加者はなんと20名!有志のみの参加だったのですが、フォルシアのエンジニアの半数近くが参加となり、エンジニア一丸となって様々なフレームワークを使いながらお題のWebアプリを作成しました。 それぞれが使いたい言語やフレームワークを持ち寄った結果、使用された言語は8言語・フレームワークは15種類となりました。ハッカソンの最後に会議室でお寿司を食べつつ、参加者同士が作成したアプリを見ながら言語やフレームワークのメリット・デメリットを話し合いました。 当時新卒6年目だった私は言語はTypeScript、フレームワークは Hapi を使用して参加しました。先輩や後輩たちの実装・発表が素晴らしく、とても勉強になったのを強く覚えています。 新フレームワーク導入・開発チームの発足 ハッカソンで実際に使ってみた後、様々な言語やフレームワークのメリット・デメリットを洗い出し、2018年4月に新フレームワーク導入・開発チームが発足しました。ハッカソンを主催した新卒3年目のエンジニアがリーダーとなり、そのほかに同じく新卒3年目エンジニアが2人、私の4人がメンバーとなりました。 その後キャリア入社のエンジニアなど強力なメンバーを加え、様々な課題に立ち向かいながらSpookへの新フレームワーク導入・開発を進めることとなりますが、開発秘話・技術的な内容などはまた別の機会にゆっくりお話しできればと思います。 チームの合言葉は「やっていくぞ」。どんな課題にも前向きに取り組んでいく気持ちを表した言葉です。 新フレームワークをリリース 新フレームワークの導入検討・開発をチームで進めていく中で、新しい技術・考え方などを取り入れるポジティブな対応だけではなく、既存の監視ツールへの対応など多くの課題が出てきました。それらをチームで乗り越え、今年の5月にようやく初版をリリースすることができました。 別の記事でも紹介されている「 社内のエンジニア向け勉強会 "devゼミ" 」などで新フレームワークのハンズオンなどをしながら社内への告知を行いました。 その後、社内の新規案件などに採用され、片手で数えきれないほどのアプリで使われ始めています。様々な案件で使用されながら随時フィードバックをもらいつつ、チーム一丸となってフレームワークの改修を進めています。 今後のSpookを支える技術の導入・開発に携われていることに感動と感謝を感じつつ、これからも前向きに「やっていくぞ」。 おわりに 冒頭に記載した通り、Spookは日々進化しています。そしてこれからも進化を続けていきます。社外の方にフォルシアやSpookの話をするとき、エンジニアの業務内容として「Spookを使う仕事なの?作る仕事なの?」という質問をいただくことが多くあります。 フォルシアのエンジニアは明確に「使うひと」「作るひと」を分けず、技術部全体で一丸となってSpookをより良いものにしていくために日々努力を続けています。 そこには若手やベテランなどの垣根は存在せず、それぞれが得意分野を持ち寄りながら議論を重ねて改善をしていく文化があります。 フォルシアではエンジニアを募集しています!興味を持たれた方は こちら からお問い合わせ頂ければと思います。
FORCIAアドベントカレンダー2019  11日目の記事です。 旅行プラットフォーム事業部の佐藤です。 7月に、エンジニアの教育活動の一環として行っている" devゼミ "をご紹介しましたが、開始から8ヶ月目となる今でも受講者が減ることなく継続的に開催が続き、今日までに23回ものゼミが行われました。これまでに受講したことのあるエンジニアは社内全エンジニアの約7割、講師を務めたエンジニアは約4割と、当初の想定を超えるものとなりました。また、これまでに行った講師・受講者向けのアンケートにおいても非常に高い満足度を得ています。 そこで今回は、満足度の高い状態で継続的にdevゼミを開催してきて得た気づきをご紹介します。 これまでに開催されたゼミ過去10回のタイトル一覧 dockerさわってみよっかー! 外部講師によるISUCON勉強会 旅行商材ウルトラクイズ SQLの計算量を意識してみよう 社内の新フレームワークで始めるReact+Redux PostgreSQL index事始め&複合indexを使ってみよう GitLab CI/CDであそぼ フォルシアワークフローツールをソースコードレベルで理解する 社内ライブラリを使ってみよう作ってみよう サーバ構成・スペックの見積もり方法~入門編~ 講義で扱う対象・進め方がイメージできるようなタイトルにしています。 継続的な開催をするために 下記のdevゼミの目的を達成し、組織に根付かせるためには継続的な開催がカギとなります。そこで、ここでは継続的な開催をする上で重要だと気づいた点について述べようと思います。 エンジニア全体の技術力の底上げ 学ぶ習慣づくり 教える習慣づくり 講師の満足度をあげよ! ゼミは講師のおかげで成り立っていると言っても過言ではありません。講義を「やってみたい」と思ってもらえること、さらに講義実施後に「やってよかった」「勉強になった」と言ってもらえるようにすることが最も重要です。 講師がゼミで扱う内容に迷いが不安がある場合は、抱え込まないように運営チームと一緒に話し合い、明確にしていきます。 また、初めて講師を務めるエンジニアに対しては、運営チームが各回に参加して、気づいたことを元に進め方について適宜フォローします。 さらに、これまで講師をしたエンジニアから特に好評なのが、ゼミ開催後に参加者から集めたフィードバックです。「フィードバックをもらえること自体が嬉しい」「参加者の率直な意見をもらえてありがたい」「思っているよりも受講者は前向きだということがわかった」といった声が挙がっています。 リアクションをするという点では同様に、フィードバックだけではなく講義中にもたくさんリアクションを行うことが大切だと思います。実際に、社内のメンバー同士だからこそできる面白いツッコミやガヤが入ることで、講師・受講者ともに楽しんでいる様子もしばしば見受けられました。 受講者の満足度をあげよ! 講師だけではなく受講者が「参加してよかった」と感じて再び参加してくれることも、もちろん大切です。前回 devゼミをご紹介した記事 でも記載していますが、当初の企画の狙い通り「0ベースから、実際に使うところまでできるのが良い」という感想が多く寄せられています。 また、一度講師をやったことがあるエンジニアとしては、講義の進め方自体も大変勉強になります。 例えば、先日 西山さん が行った「Prometheusで自分の開発環境をモニタリングしてみよう」という講義では、ハンズオンの際にdockerのイメージを受講者に配布して、そのイメージを用いて作成したコンテナを用いて各自課題に取り組みました。そのため、各受講者の開発環境に依存することなくスムーズにハンズオンが進められていて、今後のゼミ開催にあたって大変勉強になりました。 エンジニアの関心を聞き逃すな! 継続開催するにあたり、ゼミで扱うテーマも継続的に選出していかなければなりません。そのためには、社内で「〇〇って新入社員向けの講義のテーマとしては扱っていないけど、うちのエンジニアは知っておいたほうがいいかもね」や「〇〇の良さを他のエンジニアにも知ってもらいたいなあ」といった声を(特にslackのメッセージなどで)聞いた際には、運営チーム内ですぐに共有します。 また、各エンジニアが今どのようなことを行っているのかということも、休憩スペースで話しかけたりランチに一緒に行ったり、社内のドキュメントを読んだりしてキャッチアップすることを心がけています。各エンジニアが取り組んでいることは思っている以上に多様で、またそれらが他のエンジニアにも知られていないことって多いなと思います。 たまに刺激を入れてみよ! 今年の9月には運営チームメンバーのエンジニアのご友人を招いて、 ISUCON に向けた講義を行っていただきました。ISUCONに特化した内容から社内では使われていない便利なツールやノウハウまで教えていただき、さらにゼミ終了後の懇親会では、講師の方が所属する会社の中身の様子や取り組みなどをざっくばらんにお話しいただきました。私自身も、一エンジニアとして非常に良い刺激をもらえました。 今後の課題 一方で、今後の開催に当たり下記のような課題も残されています。 テーマが実業務に関わることに偏りがちである 参加者が新卒入社のエンジニアに偏りがちである どうしても業務優先になってしまい、なかなか参加できない人がいる 参加できなかった人へのフォローが足りない 内容の検討や資料の準備など講師の負担が大きめである etc. いずれも継続性を考えた場合、うまく仕組みを作っていくことが必要かと思います。来年のdevゼミ運営チームの課題ですね。 おわりに devゼミを通して気づいたことは、まず向学心の高いエンジニアが社内に多いということです。回数を重ねても受講者が減らないことや、「この講義、やりたいです!」と運営チームが声を掛けずとも積極的に手を上げて講師を引き受けてくださるエンジニアが複数いることも驚きました。 また、上述のようにフィードバックはモチベーション維持に非常に重要だということがわかりました。これは勉強会やエンジニアに限った話ではなく、普段の仕事においても同様で、互いに小さなことでもフィードバックや何らかのリアクションを送り合うことの大切さを改めて気づきました(仕事だけでなく、日常生活でも活かせそうですね)。 私自身devゼミ運営チームとしてほぼ毎回ゼミに参加しているため、実は運営チームメンバーが一番勉強させてもらっているのではないかとも思います。また講師や受講者とのやりとりを通して、普段の業務で関わる機会が少ないエンジニアの興味や強み、人となりを知ることはとても楽しいです。さらに、受講者や運営チームだけでなく、講師が「講義中も楽しかった」と言ってくれたり、エンジニアのボトムアップというdevゼミのコンセプトに共感してくれる社員が多いことが、何より嬉しいです。 エンジニア勉強会の企画を考えている方々に、この記事が少しでも参考になれば幸いです!
FORCIAアドベントカレンダー2019 10日目の記事です。 検索プラットフォーム事業部の澁谷です。 皆さん、システムコールって意識していますか? 昔からあるデバック方法の一つですが、最近の開発で「システムコール」を意識することも少なくなっている気がします。今回はシステムコールのデバックコマンド [strace ] の紹介がてら、postgresql で実行したSQLの挙動を眺めてみます。 システムコールとは? システムコールとは、コンピュータ上で実行中のプログラムが、オペレーティングシステム(OS)のカーネルの特権的な機能を呼び出す仕組み。ネットワークを利用した通信、ファイルへの入出力、新しいプロセスの生成、プロセス間通信などは、システムコールを使用することで実現されます。 なお、CPU・メモリ上の計算であればシステムコールは発生しません。 どうやってトレースする? linux (ubuntu ) の strace コマンドを使用し、トレースしたいプロセスのPIDを事前もしくは実行中に調べ、strace コマンド の [p]オプションへ指定すると確認することができます。 例えば、apache の親プロセスをトレースすると以下のようになります。 select,read.fstat,write,waitid などがシステムコールになります。 $ sudo strace -p 1339 strace: Process 1339 attached select(48, [3 5 6 7 8 9 11 12 13 15 18 19 20 22 23 26 27 29 32 34 35 37 39 41 43 44 45 47], [], [7 8 9 11 12 15 19], NULL) = 1 (in [23]) read(23, "[20194:20194:1128/134821.397022:"..., 8192) = 117 read(23, 0x55e2ab413285, 8075) = -1 EAGAIN (Resource temporarily unavailable) fstat(14, {st_mode=S_IFREG|0640, st_size=5046, ...}) = 0 write(14, "[20194:20194:1128/134821.397022:"..., 117) = 117 read(3, 0x7ffc0e3bf577, 1) = -1 EAGAIN (Resource temporarily unavailable) waitid(P_ALL, 0, {}, WNOHANG|WEXITED|WSTOPPED|WCONTINUED, NULL) = 0 select(48, [3 5 6 7 8 9 11 12 13 15 18 19 20 22 23 26 27 29 32 34 35 37 39 41 43 44 45 47], [], [7 8 9 11 12 15 19], NULL^Cstrace: Process 1339 detached <detached ...> 事前準備 検証用データの作成 検索用情報を持つテーブル foo と検索情報に紐づくテキスト情報を持つ bar テーブルを作成。内部結合を行った際の処理と、select句内でサブクエリを記述し取得した際の処理をそれぞれトレースします。 用意するデータのイメージは以下の通り 検索用情報テーブル (foo) ID1 ID2 ID3 bit1 bit2 bit3 1 3 100 true true true 2 5 300 true true false ... ... ... ... ... ... 10000 17 500 false false true テキスト情報テーブル (bar) ID1 text1 1 ふなっしー 2 くまもん ... ... 10000 素敵坊主 今回の検証用SQLで得られる結果(id2 - bit3 は返却対象から外しています) ID1 ID2 ID3 bit1 bit2 bit3 text1 1 3 100 true true true ふなっしー 2 5 300 true true false くまもん ... ... ... ... ... ... ... 10000 17 500 false false true 素敵坊主 -- 検索用情報テーブル CREATE TABLE foo ( id1 int4, id2 int4, id3 int4, id4 int4, bit1 bit(1), bit2 bit(1), bit3 bit(1), bit4 bit(1) ); insert into foo select n as id1, n as id2, n as id3, n as id4, '1'::bit as bit1, '1'::bit as bit2, '1'::bit as bit3, '1'::bit as bit4 from ( SELECT GENERATE_SERIES(1, 10000) as n )x; -- テキスト情報テーブル CREATE TABLE bar ( id1 int4, text1 text ); insert into bar select n as id1, 'hoge'::text as text1 from ( SELECT GENERATE_SERIES(1, 10000) as n )x strace で プロセスをトレースする準備 straceコマンドでSQL実行中のプロセスをトレースするため、SQLを実行するアプリケーションpgadmin からSQLを実行しPIDを確認します。 $ ps -ax | grep postgres | grep SELECT 28495 ? Rs 10:34 postgres: forcia forcia 10.0.2.2(64164) SELECT ここでは PID 28495が得られたので、そのプロセスを追跡していくこととします。 同一の処理結果となる 2つのSQLを比較 SQL1.内部結合で取得 select b.text1 from foo a inner join bar b using(id1); -- explain analyze "Hash Join (cost=319.00..506.12 rows=5184 width=32) (actual time=4.277..9.833 rows=10000 loops=1)" " Hash Cond: (b.id1 = a.id1)" " -> Seq Scan on bar b (cost=0.00..115.84 rows=5184 width=36) (actual time=0.013..1.626 rows=10000 loops=1)" " -> Hash (cost=194.00..194.00 rows=10000 width=4) (actual time=4.253..4.253 rows=10000 loops=1)" " Buckets: 16384 Batches: 1 Memory Usage: 480kB" " -> Seq Scan on foo a (cost=0.00..194.00 rows=10000 width=4) (actual time=0.005..2.116 rows=10000 loops=1)" "Planning time: 0.086 ms" "Execution time: 10.485 ms" SQL2.セレクト句にサブクエリを記述 select (select text1 from bar b where b.id1 = a.id1) as text1 from foo a; -- explain analyze "Seq Scan on foo a (cost=0.00..1890194.00 rows=10000 width=32) (actual time=1.801..19942.947 rows=10000 loops=1)" " SubPlan 1" " -> Seq Scan on bar b (cost=0.00..189.00 rows=1 width=5) (actual time=1.015..1.990 rows=1 loops=10000)" " Filter: (id1 = a.id1)" " Rows Removed by Filter: 9999" "Planning time: 0.078 ms" "Execution time: 19946.171 ms" SQL1,SQL2それぞれの処理速度(Execution time)を見ると、SQL1の10.485ms に対して、SQL2は19946.171 msで、SQL2はSQL1よりも約2000倍遅い。 SQL1、SQL2の実行プランを比較すると、bar テーブル の処理はどちらもシーケンシャルスキャンですが、rows/loops の処理方法に違いがあることがわかります。 SQL1: Seq Scan on bar b (cost=0.00..115.84 rows=5184 width=36) (actual time=0.013..1.626 rows=10000 loops=1 ) SQL2: Seq Scan on bar b (cost=0.00..189.00 rows=1 width=5) (actual time=1.015..1.990 rows=1 loops=10000 ) この時のSQL1,SQL2のシステムコールを眺めてみよう SQL1 のシステムコールの統計情報 $ strace -p 28495 -c % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 3 write 0.00 0.000000 0 10 lseek 0.00 0.000000 0 7 brk 0.00 0.000000 0 4 sendto 0.00 0.000000 0 6 3 recvfrom 0.00 0.000000 0 3 epoll_wait ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 33 3 total SQL2 のシステムコールの統計情報 $ strace -p 28495 -c % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 3 write 0.00 0.000000 0 10009 lseek 0.00 0.000000 0 4 sendto 0.00 0.000000 0 6 3 recvfrom 0.00 0.000000 0 3 epoll_wait ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 10025 3 total 上記より、SQL1のlseekが10回呼ばれているのに対して、SQL2の lseek は10009回呼ばれていることがわかります。 lseek はファイルの読み書きを行うためにポインタを移動する際に呼ばれるシステムコール。 先のexplain analyzeの結果を合わせてみると、SQL1 SQL2で実行したシーケンシャルスキャンの処理の違いは以下のように推測することができます。 SQL1: foo テーブルに対し、bar テーブルを1回のHDDアクセスで10000レコード取得。それを1回のループで処理。 SQL2:foo テーブルに対し、bar テーブルを1レコードずつHDDへアクセスしてデータを取得。それを10000回繰り返し処理。 つまり、SQL2の様に select 句でサブクエリを記述するとそのレコード毎に評価されることがシステムコールからも読み取れることがわかりました。 SQL2のシステムコールを減らしてみる 冒頭のシステムコールの説明で「CPU・メモリ上の計算であればシステムコールは発生しない」と書きました。 先のSQL2では、10000レコードの処理を行う際に1レコードずつファイルディスクリプタへアクセスをしていたが、メモリに乗っていればシステムコールは発生しないはず。検索情報テーブル(foo)とテキスト情報テーブル(bar)が結合するカラムにそれぞれインデックスを作成し試してみます。 create index _idx_foo on foo (id1); create index _idx_bar on bar (id1); SQL3 (SQL1 with index) "Hash Join (cost=289.00..620.50 rows=10000 width=5) (actual time=5.295..10.963 rows=10000 loops=1)" " Hash Cond: (a.id1 = b.id1)" " -> Seq Scan on foo a (cost=0.00..194.00 rows=10000 width=4) (actual time=0.008..1.429 rows=10000 loops=1)" " -> Hash (cost=164.00..164.00 rows=10000 width=9) (actual time=5.272..5.272 rows=10000 loops=1)" " Buckets: 16384 Batches: 1 Memory Usage: 558kB" " -> Seq Scan on bar b (cost=0.00..164.00 rows=10000 width=9) (actual time=0.007..2.447 rows=10000 loops=1)" "Planning time: 0.196 ms" "Execution time: 11.648 ms" SQL4 (SQL2 with index) "Seq Scan on foo a (cost=0.00..83219.00 rows=10000 width=32) (actual time=0.022..61.605 rows=10000 loops=1)" " SubPlan 1" " -> Index Scan using _idx_bar on bar b (cost=0.29..8.30 rows=1 width=5) (actual time=0.004..0.005 rows=1 loops=10000)" " Index Cond: (id1 = a.id1)" "Planning time: 0.068 ms" "Execution time: 63.099 ms" 実行プランを Explain analze で SQL4(SQL2 with index) の処理速度を確認すると 63.099 ms と、SQL2の 19946.171 ms から大幅に改善したのがわかります。では、この時のシステムコールを確認してみましょう。 SQL3 (SQL1 with index)のシステムコールの統計情報 $ strace -p 28495 -c % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 3 write 0.00 0.000000 0 12 lseek 0.00 0.000000 0 4 sendto 0.00 0.000000 0 6 3 recvfrom 0.00 0.000000 0 3 epoll_wait ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 28 3 total SQL4 (SQL2 with index)のシステムコールの統計情報 $ strace -p 28495 -c % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 3 write 0.00 0.000000 0 11 lseek 0.00 0.000000 0 4 sendto 0.00 0.000000 0 6 3 recvfrom 0.00 0.000000 0 3 epoll_wait ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 27 3 total SQL4のシステムコールがSQL1と同等レベルまで改善しているのがわかります。 SQL2が遅かった原因 is 何? 処理速度が遅かったSQL2にインデックスを張ったら性能が改善しました。ある意味当然の結果ではありますが、ここではシステムコールの発生が抑えられていることが確認できています。 SQL2のボトルネックを考えた時、シーケンシャルスキャンのロジックがそもそも遅いのか、それとも、ファイルディスクリプタへのアクセスが頻発することによるI/Oのオーバーヘッドが遅いのか、どちらなのでしょうか。 例えば、後者がボトルネックであれば、HDDをSSDに変えればアプリケーションの修正ではなくH/Wのレイヤーで性能を改善することができる、という検討もできます。 これを切り分けるために、SQL2の実行プランかつシステムコールの発生を抑えるSQLを組み立てて確認してみます。 SQLの準備 ここでは with句を使用して試してみます。with句で作成したデータは同一セッション内でメモリ内保存され後続処理で再利用する特性があり、この特性を生かして、予めメモリ内で展開したデータを元にSQL2と同等の処理が実行するSQL5を作成してみます。 SQL5 (with句使用) with baz as ( select id1, text1 from bar ) ,fug as ( select id1 from foo ) select (select text1 from baz b where b.id1 = a.id1) as text1 from fug a; -- explain analyze "CTE Scan on fug a (cost=339.00..2250539.00 rows=10000 width=32) (actual time=4.304..36537.863 rows=10000 loops=1)" " CTE baz" " -> Seq Scan on bar (cost=0.00..155.00 rows=10000 width=9) (actual time=0.015..1.356 rows=10000 loops=1)" " CTE fug" " -> Seq Scan on foo (cost=0.00..184.00 rows=10000 width=4) (actual time=0.020..13.423 rows=10000 loops=1)" " SubPlan 3" " -> CTE Scan on baz b (cost=0.00..225.00 rows=50 width=32) (actual time=1.833..3.648 rows=1 loops=10000)" " Filter: (id1 = a.id1)" " Rows Removed by Filter: 9999" "Planning time: 0.078 ms" "Execution time: 36543.400 ms" SQL5 のシステムコールの統計情報 strace -p 28495 -c % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 0.00 0.000000 0 3 write 0.00 0.000000 0 10 lseek 0.00 0.000000 0 3 brk 0.00 0.000000 0 4 sendto 0.00 0.000000 0 6 3 recvfrom 0.00 0.000000 0 3 epoll_wait ------ ----------- ----------- --------- --------- ---------------- 100.00 0.000000 29 3 total システムコールの回数だけ見ると、SQL1、SQL3、SQL4と遜色ないレベルにありますが、SQL2のexplain analyzeの結果と比較すると処理速度が悪化しています。 このことから、このSQLにおけるボトルネックはファイルディスクリプタへのアクセスが頻発していることではなく、実行プランがシーケンシャルスキャンになっており、CPUの演算処理で時間がかかっていることだと判断できます。 例えば、SQL2がボトルネックになった場合、SSDを導入しても改善効果は乏しい、ということが推測できると思います。 CTE Scan on fug a (cost=339.00..2250539.00 rows=10000 width=32) (**actual time=4.304..36537.863** rows=10000 loops=1) さいごに SQLを通してシステムトレースを眺めてみましが、ファイル・ネットソケットのopen/closeが適切に行われていない、ブラックボックス化されたライブラリ動作が遅かった、などを調査する手法としてシステムコールをトレースするのは有効だったりします。 触ったことが無い方、調査等で困った時にふと思い出して頂ければ幸いです。
FORCIAアドベントカレンダー2019 9日目の記事です。 旅行プラットフォーム事業部エンジニア2年目の籏野です。 フォルシアでは常に2名のエンジニアがオンコール対応を行えるように体制を整えています。 ほとんどのエンジニアが持ち回りで担当するのですが、入社後誰もがすぐにオンコールとしての対応を行うことはできません。 そこで、フォルシアに入社したエンジニアに対して、オンコール対応のためのトレーニングを実施しています。 昨年、私はこのトレーニング用の環境を整備しました。 今回はこの環境整備の話をしつつ、実際にどのようなトレーニングを行っているのかを紹介したいと思います。 トレーニング環境整備 フォルシアにはオンコール対応のトレーニング環境は元々存在していたのですが、構成情報などがあまり管理されていない状態でした。これではトレーニング中に何か起こった時に元に戻すのが大変です。 そこで今回、Ansibleを用いてトレーニング環境の構成情報を管理するようにしました。 フォルシアでは近年Ansibleを用いて各環境を構築しており、必要なミドルウェアやツール群を入れるための設定が多く用意されています。 これらを利用することによって、当時1年目の私でもまっさらな状態のサーバー上にトレーニング用の環境を構築することができました。また、Ansibleを用いたことで、私以外の社員でもコマンド一つでトレーニング環境を構築し直すことができます。 フォルシアが導入したAnsibleの話は12日に公開予定のアドベントカレンダーの記事でも紹介する予定ですので そちら もご覧ください。 トレーニングの様子 オンコールトレーニングは社内で構築されたトレーニング用環境で行われており、ここでトレーナーが様々なアラートを発生させます。 アラート発生時にはSlackに通知が流れるようになっています。 これは本番環境でのアラート時も同様で、トレーナー/トレーニーが本番環境さながらの状態でやり取り・対応を行っていきます。 全ての対応を終え、最後にスレッド内で「対応完了」とつぶやくことでトレーニングが完了します。 このつぶやきに反応して、社内botが「おつかれ」のスタンプと社内チケットの自動起票を行ってくれます。 ※本番環境ではこのチケットを元に月次のアラート発生件数の確認や恒久対応の必要なチケットの選別を行います。 アラートと言ってもその内容はさまざまであり、初めのうちはその原因調査や対応内容の判断に時間がかかってしまうことも多くあります。 しかし、このようなトレーニングを繰り返し行うことで、実際のオンコール対応時に落ち着いて対応を行うことができるようになるのです。 最後に 今回紹介した「オンコールトレーニング」はフォルシアに入社した際に受ける研修の一つです。 このトレーニング以外にもフォルシアでは様々な研修を用意しており、(特に新卒入社社員のような)経験の少ないエンジニアも安心して業務に取り組めるような環境を用意しています。 少しでもフォルシアに興味のある方はぜひ こちら へ!
この記事はCI/CD Advent Calendar 2019 8日目の記事です。 こんにちは。8日目のアドベントカレンダー記事を書かせていただきます、エンジニアの山門です! 現在は旅行プラットフォーム事業部で大手旅行会社のシステム開発を担当しています。 突然ですが皆さん、CI/CD しているでしょうか? 社内ではレポジトリ管理にGitLabを使用しているのですが、これまでGitLab CI/CD は使用していませんでした。 ここ最近になり、GitLab CI/CD の魅力に気づいた社内の各所で使われ始め、私自身も使い始めてみたので、そもそもどうやって使うんだ?という導入部分の紹介ができ
この記事は CI/CD Advent Calendar 2019  8日目の記事です。 こんにちは。8日目のアドベントカレンダー記事を書かせていただきます、エンジニアの山門です! 現在は旅行プラットフォーム事業部で大手旅行会社のシステム開発を担当しています。 突然ですが皆さん、CI/CD しているでしょうか? 社内ではレポジトリ管理にGitLabを使用しているのですが、これまでGitLab CI/CD は使用していませんでした。 ここ最近になり、GitLab CI/CD の魅力に気づいた社内の各所で使われ始め、私自身も使い始めてみたので、そもそもどうやって使うんだ?という導入部分の紹介ができればと思います。 そもそも CI/CD って? ここ最近 CI/CD というワードをよく耳にするようになりましたが、実際どういったものなのでしょう。 概要 参照元 ざっくり以下のような役割を果たしており、開発段階の効率をあげるプロセスがCIで、運用段階の効率をあげるプロセスがCDのようなイメージです。 CI(Continuous Integration): 継続的インテグレーション ビルド 単体テスト 結合テスト CD(Continuous Delivery): 継続的デプロイ ソースコードレビュー ステージング環境へのデプロイ 本番環境へのデプロイ GitLab CI/CDはこのどちらの機能も有している便利ツールなのです。 今回はCIにフォーカスして、その導入を実践していただければと思っています。 GitLab CI/CDとは GitLab CI/CDは GitLabの機能の一機能で、アプリケーションのビルドや単体テストのオペレーションを自動化し、品質維持を手助けしてくれるツールです。 GitLab CI/CDで動作する各jobはプロジェクトに関連しているため、プロジェクトの特定のブランチ更新や、mergeをトリガーにしてjobを呼び出します。 こんな感じのイメージ 参照元 レポジトリにpushした後にGitLabがjobをキックし、テスト等々実行してくれるものだと思っておいていただけると良いかと思います。 メリット フォルシアでは以下のような点が良いと感じ、GitLab CI/CDの利用を開始しました。 社内のレポジトリ管理にGitLabを使っているため、GitLab上でbuildやtestが完結し、使うツールをまとめることができる レポジトリと結びついているので、branchやMRとの連携が容易 設定を .gitlab-ci.yml というファイルでコード管理することができる GitLab Runner GitLab CI/CDを利用するにあたってGitLab Runnerの存在が不可欠です。GitLab RunnerはGitLabがmerge等のトリガーを検知した際に、実際にjobを実行し、結果をGitLabに返してくれるツールです。中身はGoで書かれており、GNU/Linux, macOS, Windowsといった様々な環境で動作可能なのが特徴です。 また、Executorというjobの実行形式を変えることで、各人の環境に合わせた選択をすることが可能です。 Runnerの種類 RunnerにはShered RunnersとSpecific Runnersの2種類の利用方法があります。 Shared Runners 複数のプロジェクトのjob実行を共有のRunnerで処理する方式 現在実行されているjobの数が最も少ないプロジェクトから処理が実行される Specific Runners 特定のプロジェクトのjobのみを実行する方式 基本的に実行リクエストが来た順に処理が実行される Executorの種類 Runnerはjobを実行するプラットフォームに応じてExecutorとよばれるjobの実行形式を選択します。 主なExecutorとして、以下のようなものがあり、フォルシアでは推奨でもあるDocker Executorを利用しています。 Shell Executor Runnerが導入されているサーバー上で、build,test等を実行 Docker Executor Docker APIを通してDocker Engineと接続することによりコンテナから各build,test等を実行 Virtual Box Executor VM上のSSHを経由してbuild,test等を実行 SSH Executor RunnerからSSH接続可能なサーバに対して、コマンドをSSH経由で送信してbuild,test等を実行 Kubernetes Executor Kubernetes API経由でクラスタ上のPodを作成してbuild,test等を実行 準備 Runnerのinstall フォルシアではオンプレでGitLabを利用しており、そこに新しくCI/CDの実行環境を用意しました。 ここでは事前準備としてRunnerのinstallをします。 ここでは、以下の前提の上で話を進めます。 GitLab が乗っているサーバがある -- A Runnerを動かすサーバ(Dockerがinstallされている)がある -- B A,Bがネットワーク的につながっている RunnerのContainerを実行 今回はRunnerをDocker imageでinstallしていきます。 こちら の公式ドキュメントが参考になるかと思います。 まずはRunnerのContainerを起動。 docker run -d --name gitlab-runner --restart always \ -v /srv/gitlab-runner/config:/etc/gitlab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ gitlab/gitlab-runner:latest Runnerの登録 次にRunnerの登録を行うのですが、登録にはあらかじめurlとtokenが必要になるので、各自取得する必要があります。登録するRunnerの種類によって以下の流れでurlとtokenを知ることができます。 Specific Runners登録の場合 各自追加したいレポジトリの CI/CD -> Ruuners Shared Runners 登録の場合 管理者権限で Admin Area -> Runners Specific Runners登録の場合のurlとtokenの記載場所例 モザイク箇所にurlとtokenが記載されています 情報が揃った方は実際に登録してみましょう。下記コマンドを打つと、対話ベースでRunnerの登録が行えます。 docker run --rm -t -i -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register Runtime platform arch=amd64 os=linux pid=6 revision=577f813d version=12.5.0 Running in system-mode. Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/): <取得したGitLab url> Please enter the gitlab-ci token for this runner: <取得したtoken> Please enter the gitlab-ci description for this runner: [3b2bf8a2a755]: <このrunnerを表す名称(gitlab-runner01)> Please enter the gitlab-ci tags for this runner (comma separated): <カンマ区切りでタグを設定(docker,sample)> Registering runner... succeeded runner=snZhRtXu Please enter the executor: docker-ssh, ssh, docker-ssh+machine, kubernetes, docker, parallels, shell, virtualbox, docker+machine, custom: <使用するexecutorを入力(docker)> Please enter the default Docker image (e.g. ruby:2.6): <使用するexecutorを入力(alpine:latest)> Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 問題なく完了すれば、無事Runnerの登録完了です! 登録したレポジトリの CI/CD -> Ruuners を見ると、登録したRunnerが以下のように画面に表示されているはずなので、早速動かしていきましょう! 早速始めてみる 社内ではShared Runnerを使用しているため、以下Shared Runnersのケースで進めます。 .gitlab-ci.yml の違いはtagsの設定くらいなので、Specific Runnersの方は、 公式 を参考にtagsの設定を適宜入れていただければと思います。 まずは自身がいじれるレポジトリでissueなどを使って適当にbranchを作成します。 その後、レポジトリルートに .gitlab-ci.yml を作成し、以下コードを追加してみましょう。 image: node test1: script: - npm install - npm run test (testがない方は echo "this is test1" のようにechoするだけでも問題ありません) 追加したらadd&commit&pushでレポジトリに反映しましょう。 自分のレポジトリの CI/CD -> Pipelines を見てみると、以下のようにgit上でtestが実行されるはずです。 注意 submodule周りで怒られているとなった場合、以下記述をimageプロパティと同じ高さに記載するとうまくいくかもしれません variables: GIT_SUBMODULE_STRATEGY: recursive GitLab CI/CDはデフォルトではsubmoduleを引っ張ってこないため、対象のレポジトリがsubmoduleを持っている場合に必要になるプロパティです 設定 内容 none(デフォルト) submoduleは引っ張ってこない normal トップレベルのsubmoduleのみ引っ張ってくる recursive submoduleの中にあるsubmoduleといった入れ子構造も引っ張ってくる 参考 やったこと 今書いていただいたのは、ミニマムでGitLab CI/CDを利用する方法です。 GitLab CI/CDは .gitlab-ci.yml という設定ファイルを、レポジトリルートに配置することで実行することができます。 image 使用するDocker イメージを指定 今回はnpmを使う関係でnodeのイメージを利用しています test1 job名 job名は .gitlab-ci.yml の中で 一意 になっている必要があります script Runner上で実行されるスクリプトやコマンドを指定 scriptはjobの中で 必須 stage, pipelineを追加してみる お次はstageという考えを導入し、pipelineを組んでみましょう。 先ほどの .gitlab-ci.yml に少し記述を追加します。 image: node stages: - build - test build: stage: build script: - echo "this is build stage" test1: stage: test script: - npm install - npm run test ここでは新たにstagesパラメータを導入し、build,testの2つのステージを追加します。 また、jobにstageパラメータを追加し、どのjobがどのstageに属するかを指定しています。 stages ビルド、テスト、デプロイといった大きな塊を表す定義の一覧 stage 各jobをどのステージに割り当てるかを指定 同じステージのjobは並列で実行 される 基本的に 前のステージのjobが全て成功しないと、次のステージのjobは実行されない この状態で更新してpushしてみると、GitLab上でpipelineが自動的に組まれます。簡単ですね。これにより、ビルドが成功したらテストをやってデプロイまで、といった流れを簡単に組むことができます。 注意 1jobに対して1コンテナが立ち上がる ので、各jobは独立していることに注意しましょう ES6やTSのプロジェクトで、build jobで npm run build したからといって、testのjobでbuildせずにtestを走らせると失敗してしまいます! jobの並列実行 引き続き今度は、同じstageで複数のjobを設定し、jobを並列で実行してみましょう。 image: node stages: - build - test build: stage: build script: - echo "this is build stage" test1: stage: test script: - npm install - npm run test test2: stage: test script: - echo "test1 and test2 are run simultaneously" そろそろ皆さん慣れてきたかもしれませんが、testステージにtest2というjobを追加しました。この状態でpushすると、buildステージが成功した後に、testステージのjobが並列で実行されるはずです。 もう少しやってみる 最低限の導入という意味ではここまでで十分なのですが、せっかくなので以下2点も合わせてやってみましょう。 branchによる実行jobの変更 各jobの前後への処理の追加 image: node stages: - build - test before_script: - npm install after_script: - echo "this is executed after each job" build: stage: build script: - echo "this is build stage" only: - branches except: - master test1: stage: test script: - echo "this is test1" - npm run test only: - branches except: - master test2: stage: test script: - echo "this is test2" only: - branches except: - master test3: stage: test before_script: - echo "before_script is overwritten" script: - echo "this is test3" only: - master いきなり増えましたが、追加したパラメータの説明を以下に記載します。 before_script scriptの前に行うタスクを定義 環境の設定や、npm installなどの、どのjobでも実行されるようなタスクを記載 after_script scriptの後に行うタスクを定義 only 指定したブランチ、およびタグの更新があった場合のみjobを実行 except 指定した以外のブランチ、およびタグの更新があった場合のみjobを実行 onlyパラメータと併用することが多い やりたいことが増えてくると、jobの数が増加し、pipeline実行にかかる時間が長くなってきてしまうのですが、こういったパラメータを利用することで、時間の短縮や、必要な場面でのみjobを実行することができるようになります。 パラメータはネストが深い方で上書きする(グローバル宣言したものよりローカル宣言したものが強い)ようになっており、test3では、グローバルに宣言されている before_script を上書きし、 npm install が実行されないようになっています。 補足: 特定のjobだけ別のimageを使いたいという意図で、jobの中にimage指定をして上書きすることはよくあります この状態でpushするとbuild,test1,test2が実行され、masterにmergeされた際はtest3だけが実行されます。 パラメータはこの他にもたくさんあり、例えば以下のようなパラメータを使用することもできます。 設定 内容 記述例 variables job内で利用される変数定義 variables:       GIT_SUBMODULE_STRATEGY: recursive tags 実行するRunerのタグを指定 tags: docker allow_failure 失敗することを許可するか否かを指定 allow_failure: true when 特定の条件にマッチした場合のみjobを実行 when: on_failure retry 失敗したときのretry回数を設定 retry: 2 公式のページ がかなり丁寧に書かれているので、困ったらそちらを見ると良いと思います。 MRの作成 .gitlab-ci.yml を導入することでMRでも恩恵を受けることができます。 実際にMRを作成し、画面を見てみると、これまでなかったpipelineの表示が出てくるため、pipelineが成功してmergeという運用が新たにできるようになります。 対象のMRのpipelineが実行中の場合、mergeボタンが「Merge when pipeline succeeds」という表示になり、レビュアーがOKと判断し、このボタンを押しておけば、pipeline成功時に自動でmergeをしてくれるようになります。 buildや単体テストによるチェックはCIに任せて開発効率をじゃんじゃん上げていきましょう! (ちょうどいい画像が用意できなかったため公式から引っ張ってきました) 参考までに実際に今回のMRをmergeしたときに走るpipelineです。 設定した通りmasterではtest3のみ実行されています。 終わりに 駆け足でしたが、ざっとGitLab CI/CDについて理解・実践していただき、導入コストの低さと利便性を感じていただけたのではないかと思います!これを機に導入を検討されてみてはいかがでしょうか。 フォルシアでは技術の知見を共有しあうdevゼミという試みがあり、本記事もそこでの発表を元に、まとめ直したものをご紹介しております(devゼミについての紹介は こちら をご覧ください)。 GitLab CI/CD は 公式ドキュメント がかなり丁寧に書かれており、そちらを見れば何かしらヒントがあるので、導入の際はぜひ読んでいただければと思います。 1push,1MR単位でCIを実施して、ソースコードの品質維持・向上を担ってくれているので、実際に開発者、レビュワーが実装に集中して業務に取り組めるようになったと感じています。 GitLab CI/CD に限らず、今後CI/CDが当たり前のものになっていくといいですね。
FORCIAアドベントカレンダー2019 7日目の記事です。 検索プラットフォーム事業部エンジニアの小孫です。 旅行会社のサイトで主に列車の検索画面を担当しています。最近注目の鉄道ニュースといえば、来年5月から運行される「WEST EXPRESS 銀河」ですよね。通常の指定席料金で乗れる夜行特急ということで、旅の選択肢がまた一つ増えますね! さて、アドベントカレンダー7日目は、鉄道ではなく「船」の話をします。突然ですが、ギリシャ語で「水先案内人」を何というかご存知でしょうか。 そうです。κυβερνήτηςですね。 すみません。Kubernetesです。クバネティスです(発音はたぶん)。 このところ本番環境での利用が増加しているこのKubernetesについて、フォルシアでの導入の検討過程についてお話します。 Kubernetesの技術的な話というよりは、フォルシアでの新しい技術への取り組みについて、Kubernetesを一例としてお伝えしようと思います。 個人的きっかけ 新人の頃に同期がSlackで共有した記事に、「いまどきdockerを使ってない会社なんてすぐに辞めるべき」みたいなことが書いてありました。当時フォルシアの業務ではdockerを使っていなかったので、これは辞めるべきなんか!?と焦りました(嘘です。全然焦ってないです)。 しばらくしてKubernetesというものを耳にしました。いま流行りの「コンテナオーケストレーションシステム」らしい。よくわからん。 そもそもどう発音するのが正解?くーべるねてぃす?きゅばねてぃす? というわけで本を買って少し勉強してみました。 フォルシアでの導入を考える その後デブサミに行く機会があったので、Kubernetes関係のセッションを探してみました。すると結構ある!早速登録して話を聞きに行きました。 会場で「Kubernetes触ったことあるよ〜って人?」は3割くらい、「本番運用してるよ〜って人?」は数%で、思っていたよりも少ない。日本での普及はこれからだが、世界的には本番運用も珍しい技術ではない、とのことでした。 さて、このイベントで特に知りたかったのは、フォルシアのサービスでもKubernetesを導入できるのか、する意味があるのか、という点でした。ちょうど登録したセッションの中に、長年運用してきたアプリをマイクロサービス化してKubernetesに移行中、というものがありました。 お、参考になりそう!と思い、セッション後に登壇者に話を聞きに行ってみました。 が、フォルシアのようにオンプレで運用している顧客サービスが多い場合には、Kubernetes移行はコスパが悪いと思う、という話でした。 うーん、まあたしかに。 登壇者の会社でも、Kubernetes移行したのはクラウド運用の自社サービスだけということでした。 というわけで、どうしたもんかなあ、と思っているうちに月日は流れていきました。 季節は巡り 社内で絶賛開発中の案件で、開発環境にdockerを使っているという話が出てきました。コンテナ化=マイクロサービス化というイメージが強く、アプリのフレームワークから考え直さないといけないかなと思っていましたが、そう難しく考える必要はありませんでした。 1プロセス1コンテナの原則に従いミドルウェアごとにコンテナを作って、docker-composeで管理する、という構成でした(これでフォルシアを辞める理由がなくなった!)。 さらに、ログ分析のためにオンプレサーバでKubernetesの利用を始めた!という事例も出てきました(独力でKubernetesを運用し始めた凄腕エンジニアのHさん、恐るべし)。 機運高まる! このように社内でコンテナ化、Kubernetes利用の機運が高まってきた中、フォルシアのサービスでコンテナをどう活用していくか、という内容でエンジニアでディスカッションを行いました。 開発環境でdockerを使うのは、特に大規模アプリの開発で便利 本番環境でのKubernetesの利用は、運用やリリースのコストが下がり、顧客にとってもフォルシアにとってもメリットがある Spook の性質上、データの永続化が不要なので、コンテナとの相性が良い 顧客サービスでKubernetesをいきなり導入するのは現実的ではないので、自社サービスから段階的に実績を作っていきたい という感じで、今後の方向性をはっきりさせることができました。 そしてこのディスカッションを受けて、早速Spookの簡単なアプリをGKEに乗せてみました! (実際にはアドベントカレンダーまでに、ということでがんばりました。アドベントカレンダー駆動開発ですね) まだまだKubernetesを触り始めたばかりですが、来年のアドベントカレンダーでは実際に本番環境に導入した話をしたいです。 最後に 今回のKubernetesのように、フォルシアでは新しい技術に関するディスカッションや、実際にその技術を使っているエンジニアによるゼミ形式の講義を毎週社内で行っています。新しい技術に触れながらも、地に足を付けて開発に取り組む環境が整ってきたと感じています。 そんなフォルシアに興味を持った方は、ぜひ こちら まで!
JavaScript 2 Advent Calendar 2019 6日目の記事です。 こんにちは。2019年新卒入社エンジニアの中曽です。 私は経済学部卒で、入社してから本格的にプログラミングに触れ始めました。 現在の業務ではJavaScriptというプログラミング言語を主に使用しており、この記事では、経済学部卒という背景と合わせて経済学の問題をJavaScriptで解いてみます。 フォルシアには経済学部卒のエンジニアも少なからず在籍し活躍しており、この記事を読んで経済学部の方もプログラミングをやってみたいと思う機会になれば幸いです。もちろん経済学部ではない方でも、経済学に興味を持ってもらうきっかけになると嬉しいです。 ゲーム理論 初めに、何の問題を解くかを決定する必要がありますね。 例えば、ミクロ経済学には消費者理論・生産者理論であったり、マクロ経済学には貨幣市場・労働市場であったりテーマにできそうなものは色々あります。 その数ある問題の中で、日常生活でも関わってくる場面があり興味を持ってもらいやすいと思うので、ゲーム理論を取り上げたいと思います。 簡単に説明すると、ゲーム理論とは、複数の利害関係を持つプレイヤーがいる状況で、それぞれの利得を考えて合理的に意思決定を行う理論のことです。 先程日常生活でも関わってくる場面があると述べましたが、例えば野球での打者と投手の駆け引きもゲーム理論のフレームワークを使って表現することができます。 囚人のジレンマ 次に、ゲーム理論の思考方法に関して有名な例題を出して説明します。 ある犯罪によって2人の容疑者が逮捕され、それぞれが 意思疎通のできない別々の部屋 で尋問を受けている。 この時、2人がとる行動は「自白する」or「自白しない」の2択で、それぞれの行動の結果、下記の判決がくだされることが予め分かっているものとします。 片方が「自白する」、もう一方は「自白しない」を選択した場合 「自白する」を選択した方は無罪(懲役0年) 「自白しない」を選択した方は懲役10年 両方が「自白しない」を選択した場合は、両方が懲役1年 両方が「自白する」を選択した場合は、両方が懲役5年 これを標準型ゲームとして表現し、図示すると、下の表のようになります。 「自白する」をA、「自白しない」をBとし、1文字目にプレイヤー1の選択が来るようにしています。例えばAB(0, -10)は、プレイヤー1が「(A)自白する」、プレイヤー2が「(B)自白しない」場合となります。 プレイヤー1 \ 2 (A)自白する (B)自白しない (A)自白する AA (-5, -5) AB (0, -10) (B)自白しない BA (-10, 0) BB (-1, -1) 上の図を説明すると... プレイヤー2が「(A)自白する」と考えると、プレイヤー1の利得は 「(A)自白する」を選択した場合→ -5 「(B)自白しない」を選択した場合→ -10 →「(A)自白する」を選択した方が利得が大きくなる プレイヤー2が「(B)自白しない」と考えると、プレイヤー1の利得は 「(A)自白する」を選択した場合→ 0 「(B)自白しない」を選択した場合→ -1 →「(A)自白する」を選択した方が利得が大きくなる プレイヤー1はプレイヤー2の選択によらず、常に「(A)自白する」を選択したほうが利得が大きくなります。プレイヤー2の目線で考えた場合も同じような結果になります。 それぞれのプレイヤーは個人の利益を優先して考えた場合、相手がどちらの選択をする場合で考えても「(A)自白する」を選択した方が得になります。 選択したものに 〇 を付けてみます。 プレイヤー1 \ 2 (A)自白する (B)自白しない (A)自白する AA ( 〇 -5, 〇 -5) AB ( 〇 0, -10) (B)自白しない BA (-10, 〇 0) BB (-1, -1) その結果、AA(-5, -5)がお互いに合理的な判断をした結果となります。これは ナッシュ均衡 になっています。どちらにも 〇 が付いていますね。 ナッシュ均衡をもう少し説明すると、ナッシュ均衡とは「全てのプレイヤーが本人以外の行動を所与として、全てのプレイヤーが互いに最適な戦略を取っている状態」となります。ナッシュ均衡から自分だけが戦略を変更しても利得を上げることができない状態となります。 ちなみに、全体としてはBB(-1, -1)が最適なのにも関わらず、お互いに合理的な判断をした結果AA(-5, -5)がナッシュ均衡となります。これが 囚人のジレンマ と呼ばれる所以です。 実際に解いてみる 下記の利得表で表現される標準型ゲームのナッシュ均衡を求めるプログラムを作成したいと思います。 プレイヤー1 \ 2 A B A AA (a, b) AB (c, d) B BA (e, f) BB (g, h) プログラムとして表現する際に、利得表を下記のデータ構造で表現し、関数を作成するにあたり、下のような入力と出力を設定します。 入力 const result = { "AA": {player1: a, player2: b}, "AB": {player1: c, player2: d}, "BA": {player1: e, player2: f}, "BB": {player1: g, player2: h} }  1つ目の戦略をA、2つ目の戦略をBとしており、player1の戦略を1文字目に表しています。  "AA"等の下位のオブジェクトは、player1,2のそれぞれの利得を示しています。 出力 ナッシュ均衡を、AA, AB, BA,BB が含まれる配列の形で返す。   ex) ["AA"] 実装 それでは、コードを書いていきます。   // ナッシュ均衡を求める関数 const calcNash = (obj) => { // 相手のそれぞれの選択に対する最適な選択を決める const p1Strategy1 = getMaxOption(obj, "player1", ["AA", "BA"]); const p1Strategy2 = getMaxOption(obj, "player1", ["AB", "BB"]); const p2Strategy1 = getMaxOption(obj, "player2", ["AA", "AB"]); const p2Strategy2 = getMaxOption(obj, "player2", ["BA", "BB"]); // 2人のプレイヤーが同じ選択をする場合、ナッシュ均衡となる const nash = [p1Strategy1, p1Strategy2].filter(elm => { return elm === p2Strategy1 || elm === p2Strategy2; }); return nash; }; // 利得が最大になる選択を決定する関数 const getMaxOption = (obj, player, array) => { // 適当に初期値を設定(利得の最小値より小さくする必要がある) let maxProfit = -100; let maxOption = ""; array.forEach(elm => { Object.keys(obj).forEach(key => { if (key === elm && obj[key][player] > maxProfit) { maxProfit = obj[key][player]; maxOption = key; } }); }); return maxOption; }; const result = { "AA": {player1: -5, player2: -5}, "AB": {player1: 0, player2: -10}, "BA": {player1: -10, player2: 0}, "BB": {player1: -1, player2: -1} }; console.log(calcNash(result)); // ['AA'] コードの説明 書いたコードについて説明します。 ナッシュ均衡を求める関数(calcNash)とは別に、利得が最大になる選択を決定する関数(getMaxOption)を分けて作成しました。 getMaxOption関数で相手のそれぞれの選択に対する最適反応を求めた後、calcNash関数でナッシュ均衡を求めるという流れになっています。 getMaxOption関数での処理 array.forEach...引数で指定された配列に対して繰り返し処理を行います。 ex) ["AA", "BB"] Object.keys(obj).forEach...引数で指定されたオブジェクトに対して繰り返し処理を行います。 if文での処理...配列の要素とオブジェクトのkeyが合致している、かつ指定されたプレイヤーの利得が、以前選択した利得より大きくなる場合に、maxProfitとmaxOptionを更新します。 calcNash関数での処理 p1Starategy1等などの定数...getMaxOption関数を呼び、相手のそれぞれの選択に対する最適な選択を決めます。 nash定数...filter関数を使用してプレイヤー1に対して繰り返し処理をすることで、両プレイヤーが同じ選択をした場合にのみ返り値を渡す配列に結果を抜き出します。 囚人のジレンマのケースで考えると、 プレイヤー1が選択をする際、p1Strategy1で"AA"、p1Strategy2で"AB"が選ばれます。 プレイヤー2が選択をする際、p2Strategy1で"AA"、p2Strategy1で"BA"が選ばれます。 よって、両方が"AA"の選択を行うため、"AA"がナッシュ均衡として返されるという処理になっています。 確率を含めたゲーム理論 しかし、上記では対応できないケースもあります。 例えば、次のようなケースです。 流行の先端を行くリーダーと、リーダーの真似をしようとするフォロワーという2人のプレイヤーがいる。 リーダーにとってはフォロワーと異なる服を着ると利得が高いが、フォロワーにとってはリーダーと同じ色を着ると利得が高い。 リーダー \ フォロワー (A)赤色の服 q (B)青色の服 1 - q (A)赤色の服 p AA (-1, 〇 1) AB ( 〇 1, -1) (B)青色の服 1 - p BA ( 〇 1, -1) BB (-1, 〇 -1) ナッシュ均衡を求めようとしても、お互いの選択が一致するものが無いため求めることができません。 この場合、複数の戦略を確率的に混ぜる「混合戦略」というものを行うということになり、期待される利得を最大化するようにそれぞれの選択確率(どちらの服を選ぶかどうかを決める確率)を求め、その確率が均衡点となります。 これに対して、先のコードで求められるのは、「純粋戦略」のナッシュ均衡といわれ、「純粋戦略」とは与えられた状況に対して、毎回同じ行為のみを行う種類の戦略のことを指します。 例えば、上記の例の場合でリーダーが赤色の服を選ぶ確率を p 、フォロワーが赤色の服を選ぶ確率を q とすると、リーダーの期待される利得はフォロワーの選択確率によって変わるので、 赤色の服を選んだ場合: u(赤色の服) = AA(-1) × q + AB(1) × (1-q) = -2q + 1 青色の服を選んだ場合: u(青色の服) = BA(1) × q + BB(-1) × (1-q) = 2q - 1 となります。( u(x) は x を選んだときに期待される利得とします。) u(赤色の服) > u(青色の服)となるのは、 -2q + 1 > 2q - 1 すなわち q のとき u(赤色の服) -2q + 1 すなわち q > 1/2 のとき u(赤色の服) = u(青色の服)となるのは、 -2q + 1 = 2q - 1 すなわち q = 1/2 のときです。 これをまとめると、 q のとき、 p = 1 (「(A)赤色の服」を選ぶ) q = 1/2 のとき、 0 q > 1/2 のとき、 p = 0 (「(B)青色の服」を選ぶ) となります。 同様にすると、フォロワーについては、 p のとき、 q = 0 (「(B)青色の服」を選ぶ) p = 1/2 のとき、 0 p > 1/2 のとき、 q = 1 (「(A)赤色の服」を選ぶ) となります。 これを基に、それぞれのプレイヤーが相手の選択確率に対する反応を図示した曲線(最適反応曲線と呼ばれる)のグラフの交点が、混合戦略におけるナッシュ均衡となります。 それでは、コードを書いていきます。 引数は純粋戦略と同様な形にし、出力は次のように設定します。 出力 配列の中に、ナッシュ均衡となる確率のペアを配列として入れて返す。   ex) [[0.5, 0.5]] // ナッシュ均衡を求める関数 const calcNash = (obj) => { // 相手のそれぞれの選択に対する最適な選択を決める const p1Strategy1 = getMaxOption(obj, "player1", ["AA", "BA"]); const p1Strategy2 = getMaxOption(obj, "player1", ["AB", "BB"]); const p2Strategy1 = getMaxOption(obj, "player2", ["AA", "AB"]); const p2Strategy2 = getMaxOption(obj, "player2", ["BA", "BB"]); let nash = []; const p1Prob = calcProbability(obj, "player2"); const p2Prob = calcProbability(obj, "player1"); nash.push([p1Prob, p2Prob]); if (p1Strategy1 === p2Strategy1 ) { nash.push([1,1]) } if (p1Strategy2 === p2Strategy2) { nash.push([0,0]) } return nash; }; const getMaxOption = (obj, player, array) => { // 適当に初期値を設定(利得の最小値より小さくする必要がある) let maxProfit = -100; let maxOption = ""; array.forEach(elm => { Object.keys(obj).forEach(key => { if (key === elm && obj[key][player] > maxProfit) { maxProfit = obj[key][player]; maxOption = key; } }); }); return maxOption; }; // 戦略を選択する確率を算出する関数 const calcProbability = (obj, player) => { const prob = player === "player1" ? (obj["BB"][player] - obj["AB"][player]) / (obj["AA"][player] - obj["AB"][player] - obj["BA"][player] + obj["BB"][player]) : (obj["BB"][player] - obj["BA"][player]) / (obj["AA"][player] - obj["BA"][player] - obj["AB"][player] + obj["BB"][player]); return prob; }; const result = { "AA": {player1: -1, player2: 1}, "AB": {player1: 1, player2: -1}, "BA": {player1: 1, player2: -1}, "BB": {player1: -1, player2: 1} }; console.log(calcNash(result)); // [[0.5, 0.5]] 処理として加えたのは、calcNash関数において、calcProbability関数を呼び出し確率を求めるという部分です。 calcProbability関数での処理 ここの確率は、各プレイヤーがAを選んだ時とBを選んだ時のそれぞれの期待利得が合致する確率を求めます。例えばプレイヤー1の期待利得は、プレイヤー2の確率によって左右されるので、p2Probの引数に、"player1"が入ってこの関数は呼び出されます。 また、確率を求める式は、例えば -2q + 1 = 2q - 1 といった式を予め移項して処理するようにしています。 calcNash関数での処理 ナッシュ均衡時の確率p1Prob1,p2Prob(p1Prob: プレイヤー1がAを選択する確率、p2Prob: プレイヤー2がAを選択する確率)の組み合わせを返します。 if文を追加したのは、混合戦略において上で求めた確率(グラフ上の交点になる)の他に、下の図のように(0, 0), (1, 1)で交点を持つ場合があるからです。確率を用いる混合戦略の中でも、各プレイヤーがそれぞれ相手がAという行動をするならば自分もAを行い、相手がBという行動をするならば自分もBを行った方が利得が高い場合です。 これは純粋戦略における場合と同じ要領で求めることができるので、先に使用したgetMaxOption関数を再度利用しています。下の問題がその例です。 男女の争い 男(プレイヤー1)\ 女(プレイヤー2) (A) 野球を見る q (B)買い物に行く 1 - q (A)野球を見る p AA (4, 1) AB (0, 0) (B)買い物に行く 1-p BA (0, 0) BB (1, 4) 相手がどちらを選択するかは絶対的には決定されないが、もし相手がを野球を見るなら一緒に野球を見た方が利得が大きく、買い物に行くなら買い物に行った方がお互い利得が大きいといった内容ですね。 関数の実行結果は下記のようになります。 console.log(calcNash(result)); // [ [ 0.8, 0.2 ], [ 1, 1 ], [ 0, 0 ] ] まとめ この記事では経済学の分野からゲーム理論を取り上げ、JavaScriptを使って、標準型ゲームの混合戦略も含めたナッシュ均衡を求めました。 この記事でゲーム理論について興味が出てきたという方がいれば、もっと調べてもらえると嬉しいです。 一方、ゲーム理論は分かるけどもプログラミングに関してはあまり経験がないという方も、この機会に触れてみるのも面白いと思います。 フォルシアでは、需要量を予測し、供給量に応じて最適な販売価格を提示するダイナミックプライシングを扱っています。計量経済学的な面白さもある分野なので、興味のある方はぜひ 話を聞きに来てみてください 。
FORCIAアドベントカレンダー2019 5日目の記事です。 検索プラットフォーム事業部の田中です。 フォルシアでは、最新の技術をプロダクトに取り込むということにも果敢に挑戦していますが、一方でレガシーコードの改善や日々の運用の改善にも力を入れています。 今回は、過去にAPIテストを自動化するための Frisby.js のバージョンが0.85と古くなっていたため、今更ですが最新の2.xに置き換えた話をします。 Frisby.js とは APIのブラックボックステストを自動化するためのフレームワークで、JavaScriptの定番のテストフレームワークのJestベースで書かれています(2.0の途中でJasmineからJestに変更になりました)。APIテストフレームワークのデファクトになりつつある人気のツールです。 最初のコミットは2011年と長く愛されています。ながらくv0.85からリリースがありませんでしたが、1系をとばして2017年の7月にv2.0がリリースされました。 v0.x からv2.x へ 変更点については こちら にもありますが、独自のラッパーを書くのではなく、Jest(またはJasmine)の作法に則る形でテストを書けるようになりました(it -> expect で書けるようになった)。 より詳細な、変更点や思想は メインコミッターのブログ の記事を読むと良いかと思います。 v0.x のコード var frisby = require('frisby'); frisby.create('should get user Joe Schmoe') .get(testHost + '/users/1') .expectStatus(200) .expectJSON({ id: 1, email: 'joe.schmoe@example.com' }) .toss(); v2.x のコード var frisby = require('frisby'); it('should get user Joe Schmoe', function() { return frisby.get(testHost + '/users/1') .expect('status', 200) .expect('json', { id: 1, email: 'joe.schmoe@example.com' }); }); 基本的にはJestの文法に従って、frisby->create->get->expectXX->toss を frisby->get->expect('XX') と変えていくだけです。 型のチェックは、JavaScript の型を指定していた箇所が Joi のオブジェクトを指定するように変わっています。 これにより、さらに細かいバリデーションができるようになっています。 v0.x expectJSONTypes('*', { hoge: String }) v2.x expect('jsonTypes', '*', { hoge: Joi.string() }) また v2.x から expectJSON(...) の代替として expet('json', ...) と expect('jsonStrict', ...) がありますが、json は指定したものが一致していれば良い jsonStrict は完全に一致している必要があります。 移行時に実際に困ったところ 基本的には、v0.x と v2.x は同等のものが用意されており置き換えていくだけですが、いくつか考慮が必要だった点があるので記載します。 配列同士の比較が厳密な比較になってしまう 例えば下記のようなレスポンスがあったとします。 [ { hoge: 1, fuga: 2 } ] v0.x では下記でテストをパスしていました。 expectJSON( '*', [ { hoge: 1 } ] ) v2.x では下記はFailになってしまいます。 expect( 'json' '*', [ { hoge: 1 } ] ) 下記のように指定する必要があります。 expectJSON( '*.0', { hoge: 1 } ) expectJSON でできていた関数の評価ができない v0.x では expectJSON の中で下記のように評価関数を実行することができました。 expectJSON( { message: function(val) { expect(val).toBe('hoge'); } } ) v2.xではこれができなくなっています。そこでかなり力技ですが、下記のように再帰的に評価するようなカスタムマッチャーを作ればこれを実現できます。 const getPathVal = (response, prefix, results) => { results = results || []; for (let key in response){ const path = prefix ? prefix + '.' + key : key; if (typeof response[key] === 'object') { if(Array.isArray(response[key])) { response[key].forEach((item, i) => { getPathVal(item, path + '.' + i, results); }); } else { getPathVal(response[key], path, results); } } else { results.push({path: path, val: response[key]}); } } return results; }; beforeAll(() => { frisby.addExpectHandler('jsonf', (response, path, expected) => { frisby.utils.withPath(path, response.json, (jsonChunk) => { getPathVal(expected).forEach((item) => { frisby.utils.withPath(item.path ,jsonChunk, (actual) => { // valがfunctionの場合はmatcherが設定されているのでそれを評価する if(typeof item.val === 'function') { item.val(actual); } else { expect(actual).toBe(item.val); } }); }); }); }); }); frisby.get(host).expect( 'jsonf', '*', { message: function(val) { expect(val).toBe('hoge'); } } ); ちなみにこのカスタムマッチャーを定義しておくと、配列同士の比較が厳密な比較になってしまう問題も解決できます。 フォームデータ形式のpostリクエストの指定の仕方が変わった v0.x ではPOSTでリクエストを送る場合は引数にオブジェクトを指定すれば、よしなに body を application/x-www-form-urlencoded の形式に変換して送ってくれていたのですが、v2.0 では下記のようにする必要があります(formDataで指定もできますがここでは割愛します)。 const body = { hoge: 1, fuga: 2 }; const frisby = require('frisby'); const qs = require('qs'); frisby.setup({ request: { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } ).post(host, qs.stringify(body)).; さいごに すごく地味な作業ですが、こういったツール類も新しいものに移行していくと、新機能を使えるなど色々と恩恵を受けられます。最初からバージョンアップを見越して、テストコードにロジックを書き過ぎないようにしたり、正解データをコードとは切り離して持っておいたりすることなどが重要です。ちょっと重い腰を上げてバージョンアップしていきましょう。