TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

981

こんにちは。品質管理部エンジニアリングチームの遠藤です。 私の所属している品質管理部では、業務の一環として、ZOZOSUIT計測精度の向上のために新しいアプリがリリースされる度に精度のチェックを行っております。 常に計測、比較、検証などを行っており、特に計測は実際にZOZOSUITを着用し、計測、結果の記録を行っているのでとても時間がかかる作業です。 計測プロセス自体は毎回(大体)同じなので、その点をエンジニアリング的に解決できないか試してみました。だってエンジニアリングチームなんですもの! どうやって計測するか 最初大まかに考えた構成は、ディスプレイに写真を表示して、その写真を適度なタイミングで撮影、計測、計測値の取得という流れです。 効率のため並列で動くようにして、だれでも気軽に動かせる画面を用意する、こんな感じでしょうか。 実現には様々な技術を組み合わせる必要があるかと思いますが 今回はその中で音声認識についてスポットを当ててみようと思います。 音声認識について ZOZOSUITの計測は主に音声案内が使われています。まずそこをどうすればいいか考えました。なら音声認識すればいいのでは? という単純な理由で音声認識を調べると、Google Speech APIというホットなサービスがありました。有料ですが初回利用時に300ドル分のトライアルを無料でもらえたのでそちらを利用させていただくことにしました。 単純な音声認識をしてみる(python3) とりあえず、Googleのサンプルを実行してみました。 音声ファイルはZOZOTOWNアプリの計測時の「もう一度数字を読み上げます。数字を押してください 9」というおなじみの音声をMacBook Proの内蔵マイクで録音したものを使ってみました。 import io import os # Imports the Google Cloud client library from google.cloud import speech from google.cloud.speech import enums from google.cloud.speech import types # Instantiates a client client = speech.SpeechClient() # The name of the audio file to transcribe file_name = os.path.join( os.path.dirname(__file__), 'resources' , 'audio.wav' ) # Loads the audio into memory with io. open (file_name, 'rb' ) as audio_file: content = audio_file.read() audio = types.RecognitionAudio(content=content) config = types.RecognitionConfig( encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16, # sample_rate_hertz=16000, # language_code='en-US') sample_rate_hertz= 44100 , language_code= 'ja-JP' ) # Detects speech in the audio file response = client.recognize(config, audio) for result in response.results: print ( 'Transcript: {}' . format (result.alternatives[ 0 ].transcript)) Googleのサンプルそのままですが。 sample_rate_hertz= 44100 language_code= 'ja-JP' 'audio.raw' → 'audio.wav' の部分のみ音声ファイルに合わせて変更しています。 それでは実際に実行してみましょう。 ラジオしてくださいって! どうやらうまく認識できていないようです。 いろいろ記事をみてみると、Google Speech APIの認識率はとても高いようですので、音声ファイルに何らかの原因がありそうです。 音声ファイルを聞いてみて気になった点としては 最初に何秒か無音部分がある 環境を気にせず録音したためエコーがかったように聞こえる 音声のボリュームが小さい というのがありました。 その点を注意し試しに別の音声を取り込んでみると、 今度はうまくいきました。 音声認識の手順がわかったところで今度は音声の取り込みを試します。 静かな部屋の中、一台の端末を行うなら内臓マイクで録音するのも良いかと思いますが、複数端末を行いたいのでAndroid端末のイヤホンジャックから音声を取り込むことにします。 音声デバイスのインデックスはこちらのコードで確認できます。 import pyaudio audio = pyaudio.PyAudio() for i in range (audio.get_device_count()): dic = audio.get_device_info_by_index(i) print (dic[ 'index' ], dic[ 'name' ]) 0 Built-in Microphone 1 Built-in Output 2 DisplayPort 3 HDMI Androidのヘッドフォン端子から音声を入力するために マイク端子を増設するケーブルをUSBに接続するとこのように表示されます。 0 Built-in Microphone 1 Built-in Output 2 DisplayPort 3 HDMI 4 USB PnP Sound Device 4の USB PnP Sound Device を指定して録音を行えばケーブルからの音声の取り込みで動作させられそうです。 実際このようなシステムを構築し、実験してみました。 実際にやってみて アプリの流れに沿って音声認識でデバイスを制御してみると、いろいろと問題があることを把握できました。 言葉の区切りごとでAPIにリクエストすれば高い精度で認識してくれるのですが、言葉の途中でリクエストしてしまうとそこで聞き取った音声がうまく認識されませんでした(当たり前ですね)。 また、タイミングよくリクエストしても、APIからの返答までに次の音声案内に入ってしまうという問題もありました。 対策として 音声ボリュームを確認しながら無音が何秒かあった場合は言葉の区切りと見なしてみたり、 リクエスト中に別スレッドが録音を担当するように手分けしてみました。 それでも100%に近いかたちで音声を認識させることは難しかったです。 また録音した音声を実際に聞いてみるとなぜか音声にノイズが含まれていることもわかりました(試せていないのですが、もしかしたらオーディオ変換ケーブルの相性があるかもしれません)。 結論 単純に音声認識をすることは問題なく行えたのですが、実際にそれを組み込み、意図した動作させることにはまだまだ課題がありそうです。 感想 一通り構築してみて、うまくいかなかった点はありましたがいろいろ学ぶことはとても多かったです。 音声認識については意図した動作をさせることができませんでしたが 「それは失敗ではなくて、その方法ではうまくいかないことがわかったんだから成功なんだ」 ってエジソン先生も言ってることですし、試してみて問題があれば、ひとつひとつ解決していく方法を探していきたいと思います。 参考サイト Cloud Speech-to-Text API クイックスタートクライアント ライブラリの使用 https://cloud.google.com/speech-to-text/docs/quickstart-client-libraries?hl=ja#client-libraries-install-python 最後に ZOZOテクノロジーズでは、一緒にサービスを作成、サポートしてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/starttoday-tech/projects
ZOZO研究所でインターンをしている松井です。本記事では、cross-domain画像検索とdeep metric learningの概要と、cross-domain画像検索で良い精度を達成するためのテクニックを取り上げます。 metric learningの概要 metric learning とは、データ間の関係を表す計量(距離や類似度など)を学習する手法です。 画像分類や、画像検索などに応用できます。 意味の近いデータの特徴量どうしは近く、意味の異なるデータの特徴量どうしは遠くなるような計量を学習 します。 意味の近いデータのペアを positive pair 、意味の異なるデータのペアを negative pair と呼びます。 deep learningが出てくる前の代表的な手法として、マハラノビス距離の共分散行列を学習させる手法があります。 :データの特徴量 :パラメータ(共分散行列) この を、positive pairならば小さく、negative pairならば大きくする損失関数で学習をします。 前述したマハラノビス距離の式において と分解すると、以下のように表すこともできます。 :ユークリッド距離 つまり、マハラノビス距離の共分散行列を学習することは、 ユークリッド距離においてpositive pairならば近く、negative pairならば遠くなるような特徴量 を抽出する行列 を学習 することとも捉えられます。 をかけることは線形写像ですが、その代わりに非線形写像をdeep learningで学習する手法が、 deep metric learning です(以下、DMLと略します)。 :非線形写像 :パラメータ metric learningの応用 metric learningの代表的な応用である、 fine-grained image classification と cross-domain画像検索 を紹介します。 fine-granined image classification fine-grained image classification とは、以下の入出力を持つタスクです。 入力:画像 出力:画像に映る物体のクラス fine-grained image classificationでは車や鳥といった大雑把なクラスを予測するのではなく、 車種や鳥の種類など、より細かいクラス を予測します。 クラス数が多く、クラスあたりの画像数が少ない 状況です。 metric learningを用いて解くとき、positive pairとnegative pairを以下のように定義します。 positive pair:同じクラスの画像ペア negative pair:異なるクラスの画像ペア そして、学習した距離を用いたk-nearest neighborで分類問題を解きます。 つまり、分類したいデータに最も近いk個のデータのクラスを見て分類します。 metric learningでは、学習時の入力が画像1枚ではなく画像のペアなので、1クラスあたりの学習データを大幅に増やすことができます。 そのため、クラスあたりの画像数が少ないfine-grained image classificationにmetric learningは適しています。 また、手法の評価方法は予測クラスが合っていれば正解として、accuracyを用いることが多いです。 cross-domain画像検索 cross-domain画像検索 とは、以下の入出力を持つタスクです。 入力:クエリ画像 出力:クエリ画像に対応する、ドメインの異なる画像 今回は、ファッションの画像を扱います。入出力は具体的に以下になります。 入力:スナップ写真中の検索したいアイテム領域(以下street queryとする) 出力:そのアイテムの商品画像(以下shop imageとする) metric learningを用いて解くとき、positive pairとnegative pairを以下のように定義します。 positive pair:同じ商品のstreet queryとshop imageのペア negative pair:異なる商品のstreet queryとshop imageのペア 商品IDをクラスとみなすと、クラスあたりの画像が少ない状況はfine-grained image classificationと同じです。 ですから、同様の理由でcross-domain画像検索にもmetric learningを用いることができます。 fine-grained image classificationと異なる点は以下の3が挙げられます。 ペア間で画像のドメインが異なる クラス(商品ID)あたりの画像数が1枚だけで、より少ない 同じ商品なのに似ていないペア(hard positive pair)や、異なる商品なのに似ているペア(hard negative)がいくつか存在する 手法の評価方法は、検索結果の上位k個にstreet queryと同じ商品が含まれていれば正解として、top-k accuracyを用いることが多いです。 metric learningの研究では、モデルの評価は主にfine-grained image classificationのタスクで行われています。 cross-domain画像検索を用いた評価は、主流ではありません。 DMLの手法を構成する項目 DMLの手法はいくつかの項目で構成されます。 例えば、計量として類似度と距離どちらを用いるのかや、どのような損失関数で距離を小さく・大きくするかなどがあります。 この章では以下の4つの各項目において、どのような選択肢があるのかを紹介します。 サンプリング ネットワークの構造 計量 損失関数 サンプリング metric learningの文脈におけるサンプリング方法とは、入力データの作り方です。 代表的なものとして、 contrastive sampling 、 triplet sampling があります。 contrastive samplingのバッチ内のサンプルは以下になります。 :positive pair :negative pair :pairの起点となるサンプル(以下、anchorとする) 例:street query :anchorに対して、positiveな関係にあるサンプル(以下、positiveとする) 例:同じ商品のshop image :anchorに対して、negativeな関係にあるサンプル(以下、negativeとする) 例:異なる商品のshop image triplet samplingのバッチ内のサンプルは以下になります。 $$ (a, p, n) $$ また、 N-pair sampling が以下の論文で提案されました。 (K. Sohn. 2016) Improved Deep Metric Learning with Multi-class N-pair Loss Objective. N-pair samplingのバッチ内のサンプルは以下になります。 $$ (a_i, p_i, \left\{n_j\right\}_{j=1, j \neq i}^B) $$ :anchorとpositiveの添字 :バッチサイズ バッチ内の他の商品のpositiveをすべてnegativeとして扱います。 contrastive samplingやtriplet samplingに比べ、1サンプルにつき複数のnegativeを考慮できます。 metric learningでは、学習が進むに連れて、十分離れたnegativeが増えてきます。 1サンプルにつき1つのnegativeしか考慮しないと、十分離れたnegativeをサンプルしてしまった場合、重みが更新されません。 そのため、contrastiveやtripletよりN-pairのほうが学習の効率が良いとされています。 ネットワークの構造 基本的な構造は、ImageNetでpre-trainしたCNNの先にFC(全結合)層を1層つけた形になります。 構造の項目として、今回は以下を考えました。 CNNをstreetとshopで分けるか FCをstreetとshopで分けるか 上の2つの項目はcross-domain画像検索のときのみ考えます。 理由は、ドメインごとに異なる特徴抽出器を学習させたほうが精度を上げられる可能性があるためです。 計量 代表的な計量として以下の3つが挙げられます。 類似度 コサイン類似度 内積 距離 ユークリッド距離 ※正しくは、類似度を計量と呼びません。距離と類似度の総称が無いため、この記事では便宜上、まとめて計量と呼ぶことにします。 損失関数 この記事では、効率の良いN-pair samplingについてのみ扱っていくので、N-pair samplingのときの損失関数について取り上げます。 今回は、以下の2つの損失関数を取り上げます。 hinge loss SCE(softmax cross-entropy)loss hinge loss hinge lossは以下の式で表されます。 $$ L_{U} = \frac{1}{B} ( \sum_{i=1}^B{ \sum_{j=1 , j \neq i}^B{ \max( 0, d_p^{(i)} - d_n^{(i,j)} + m ) } }) $$ $$ d_p^{(i)} = d(x_{U}^{(i)}, x_{V}^{(i)}) $$ $$ d_n^{(i, j)} = d(x_{U}^{(i)}, x_{V}^{(j)}) $$ :ミニバッチサイズ :マージン :anchor、positiveの添字 :negativeの添字 :positive pair間のユークリッド距離 :negative pair間のユークリッド距離 :ネットワークで抽出した画像特徴量 :ドメイン hinge lossは、 を より小さくしようし、 という式を満たすよう学習させます。 以下の図のように、positiveとの距離+マージンの外にnegativeを追いやるよう学習させます。 なお、計量が類似度ならば符号を逆にします。 また、上の式はドメイン の画像特徴量をanchorとしていますが、 と を入れ替えた との和を最終的な損失関数とします。 $$ L = L_{U} + L_{V} $$ SCE loss SCE lossは以下の式で表されます。 $$ L = - \frac{1}{B} \sum_{i=1}^B \sum_{k=1}^B y_{(i,k)} \log{ P_i(k) } $$ 確率 を1に、それ以外の確率は0にしたい場合、 の項だけが残るよう、 を以下のように定義します。 $$ y_{(i,k)} = \begin{cases} 1 & (k=k_1) \\ 0 & (k \neq k_1) \end{cases} $$ 類似度を用いて確率を表すとSCE lossは以下のように表せます。 $$ L = - \frac{1}{B} \sum_{i=1}^B \sum_{k=1}^B y_{(i,k)} \log{ \frac{ \exp{s^{(i,k)}} }{ \sum_{j=1}^B \exp{s^{(i,j)}} } } $$ :サンプル とサンプル の類似度 今、positive pairの類似度 を大きくし、negative pairの類似度 は小さくしたいです。 すなわち、 にして、それ以外の確率は0にしたいので、 の項だけ残します。 $$ L = - \frac{1}{B} \sum_{i=1}^B \log{ \frac{ \exp{s^{(i,i)}} }{ \sum_{j=1}^B \exp{s^{(i,j)}} } } $$ $$ = \frac{1}{B} \sum_{i=1}^B \left\{ - s^{(i,i)} + \log{ \sum_{j=1}^B \exp{s^{(i,j)}} } \right\} $$ 上の式は、類似度を使ったhinge lossと似ています。 ただ、negative sample ついてhinge lossでは平均をとっていますが、SCE lossではLogSumExpをとっています。 hinge lossではバッチ内のnegative pairが離れていくに連れlossが小さくなります。 しかし、 SCEではバッチ内に1つでも十分離れていないnegativeが存在すればlossは大きいまま です。 よって、以下の論文ではSCE lossのときの学習効率のほうが良いとされています。 (K. Sohn. 2016) Improved Deep Metric Learning with Multi-class N-pair Loss Objective. fine-grained image classificationで有効な慣習がcross-domain画像検索では有効とは限らない 今回、cross-domain画像検索を試作しました。 そのとき、fine-grained image classificationでは有効である慣習の1つが、cross-domain画像検索では有効でないことがわかりました。 試作の条件 以下の条件で試作を作りました。 サンプリング:N-pair sampling ネットワークの構造:CNN, FC共有 計量:コサイン類似度 損失関数:hinge loss マージン:1.0 CNNはInception V3を用い、ImageNetでpre-trainした重みを採用しました。 CNNを、出力が2048次元の特徴抽出器としました。 CNNの重みはmetric learningでfine-tuningしました。 FCの出力は512次元にしました。 データセットの各サンプルは以下の形をしています。 $$ (I_{strt}, I_{shop}) $$ :WEARのスナップ画像からSSDで検出したアイテム領域 :それに対応するZOZOの商品画像 trainデータでは60,000サンプルを使用しました。 最適化手法はAdamを用いました。Adamのハイパーパラメータは としました。 バッチサイズは30としました。 1,000 iteration学習させました。 評価方法 評価指標は Acc.@k/N(Top-k Accuracy against N items) を用いました。 学習したネットワークで抽出したstreet queryとshop imageの特徴量でk近傍探索を行います。 つまり、N枚のshop imageから、あるstreet queryと似ている画像を検索します。 そして、検索結果の上位k個の中に同じ商品のshop imageが含まれれば正解=1とします。 含まれなければ、不正解=0とします。 N枚のstreet queryに対し、上記の操作を繰り返し、平均を取った値がAcc.@k/Nです。 値が高いほど、良いです。 通常、Nの方は明記されませんが、以下の論文ではNが大きくなるに連れてAcc.@kの値は減少する結果が出ています。 ですから、結果の比較をしやすくするため、あえてNを明記しました。 (J. Huang, R. S. Feris, Q. Chen, & S. Yan. 2015) DARN | Cross-domain Image Retrieval with a Dual Attribute-aware Ranking Network. 今回は、Acc.@20/1000を用いました。 以降、簡単のため、この指標を単に精度と呼ぶことにします。 評価に用いるtestデータはtrainデータとは異なる1,000サンプルを用いました。 結果 HingeCosM1.0:試作 ImageNet:ImageNetで学習したCNNにより抽出した特徴量を用いた方法 試作は、ImageNetよりも精度が低かったです。 精度改善のため、hinge lossがちゃんと機能しているかを調べました。 バッチ内のanchor・positive・negative間のコサイン類似度が学習によってどう変化するかをプロットしました。 anc_pos:positive pair間のコサイン類似度の平均 anc_neg:negative pair間のコサイン類似度の平均 pos_neg_strt:street query間のコサイン類似度の平均 pos_neg_shop:shop image間のコサイン類似度の平均 学習が進むにつれ、anc_posとその他との差が開いていったことがわかります。 すなわち、同じ商品は近くに、異なる商品は遠くに埋め込まれたと考えられます。 この図だけ見ると、hinge lossは意図した通りに機能していたようです。 ここで、学習による精度の変化も見てみます。 初めは、一気に精度が上がり、そこから徐々に落ちていったことがわかります。 また、検索結果も見てみます。 query item 正解のshop image ImageNet HingeCosM1.0 各手法の検索結果の上位20個のshop image(左から右、上から下の順) ImageNetの結果ほうがstreet queryに近い柄のアイテムが検索されました。 次に、testデータの、各shop queryに対する1,000 shop imagesのコサイン類似度のヒストグラムを見ました。 横軸がコサイン類似度で、縦軸は頻度です。 類似度が1か-0.3付近に集中していることがわかります。 つまり、どちらかといえば似ているshop imageの類似度はほぼ1になっていると考えられます。 この現象の原因は、学習データにhard positive pair(同じ商品だが似ていないペア)が多く含まれるためだと考えられます。 hard positive pairの類似度を上げようとすると、学習していないnegative pairのうち、hard positive pairより似ているものの類似度はより高くなる可能性があります。 つまり、同じ商品なのに似ていないペアの類似度を上げようとすると、より似ている異なる商品との類似度も上げてしまう可能性があるということです。 そのため、あまり似ていない商品が含まれる結果になったと考えられます。 すなわち、マージン=1.0だと、hard positive pairに引っ張られて過学習を起こしやすいと考えられます。 そこでマージンを小さくすることで、どちらかといえば似ている商品の類似度を程よい値(0.5など)にでき、精度があがると考えました。 実験 マージンを小さくすると精度があがるかを検証するための実験を行いました。 比較する条件は以下の3つです。 HingeCosM1.0:マージン=1.0(先程の試作) HingeCosM0.5:マージン=0.5 HingeCosM0.1:マージン=0.1 結果 マージン=0.5のときが最も良い精度となりました。 query item 正解のshop image ImageNet HingeCosM1.0 HingeCosM0.5 HingeCosM0.1 各手法の検索結果の上位20個のshop image(左から右、上から下の順) 検索結果も、マージン=0.5のときの柄が最もshop queryに似ていました。 なお、shop queryの色と検索結果のshop imageの 色が異なるのは、同じ商品でも異なる色のペアがデータセットに含まれていたため だと考えられます。 そのようなペアをデータセットから除けば、検索結果に色も反映できる と思われます。 次に、testデータのコサイン類似度のヒストグラムも見てみます。 HingeCosM1.0 HingeCosM0.5 HingeCosM0.1 マージン=1.0にくらべ、マージン=0.5のときの類似度は-0.25から1にかけてなだらかに分布してるのがわかります。 マージン=0.1のときはそもそも類似度の差が開いていないため、精度が悪かったと考えられます。 結論 既存のmetric learningの研究では、マージンは大きいほうが良いという慣習がありました。 しかし、既存の研究では、fine-grained image classificationで評価することがほとんどです。 今回、マージンの大きさによるcross-domain画像検索の精度を計測したことで、cross-domain画像検索においてマージンを大きくすることは有効でないことがわかりました。 マージンを大きくするとhard positiveに引っ張られてしまい、hard positiveが多く含まれるcross-domain画像検索では精度が落ちることがわかりました。 また、マージン以外にも、既存の研究で有効とされている慣習がcross-domain画像検索では有効とは限らない可能性があると考えられます。 Cross-domain画像検索で有効なテクニックの探索 さらなる改善のため、マージン以外の項目も同時に探索しました。 探索範囲 以下の項目の組合せを探索しました。 CNNの構造 共有するか 分けるか CNNの重み ImageNetの重みで固定するか fine-tuingするか FCの構造 共有するか 分けるか 計量 類似度 コサイン類似度 内積 距離 ユークリッド距離 損失関数 hinge loss SCE loss(計量が類似度のときのみ) マージン(hinge lossのときのみ) 1.0, 0.7, 0.5, 0.3, 0.1(計量がコサイン類似度のとき) 625, 437, 312, 187, 62(計量が内積のとき) 12250, 3240, 1440, 490, 40(計量がユークリッド距離のとき) FCを分ける場合、重みの初期値を共有にしないと精度が一向に上がりませんでした。 初期値を共有にすることで学習が安定したので、今回はFCを分けるとき、初期値は共有しました。 ユークリッド距離とSCE lossを同時に用いるなどの妥当でない組合せを除き、230条件を探索しました。 1000 iterationだけ学習させ、10 iterationごとに精度を見て3回連続で落ちた場合、学習を打ち切りました。 評価には最後のiterationの重みを用いました。 上記の条件以外は、試作の条件と同じです。 マージンの探索範囲は、コサイン類似度のときのマージンの範囲を各計量に応じて変換しました。 学習初期の画像特徴量ベクトルのノルムが約25だったので以下の式で変換しました。 内積のときは以下の変換を用いました。 $$ m_{dot} := ||x||^2m_{cos} = 625 m_{cos} $$ :内積のときのマージン :画像特徴量ベクトルのノルム :コサイン類似度のときのマージン 内積は、コサイン類似度に両ベクトルのノルム( )をかけたものなので、マージンも 倍しました。 ユークリッド距離のときは以下の変換を用いました。 $$ m_{euc} := \sqrt{ 2 (1-\sqrt{1-m_{cos}^2}) }||x|| $$ $$ = 25\sqrt{ 2 (1-\sqrt{1-m_{cos}^2}) } $$ :ユークリッド距離のときのマージン また、実装の都合上、距離ではなく距離の二乗を採用したため、マージンも二乗した を用いました。 ユークリッド距離のときのマージンの変換の導出は、付録1で説明します。 結果 探索範囲の内、最良の条件は以下で、Acc.@20/1000=48.4%に達しました。 CNNの構造:共有する CNNの重み:fine-tuingする FCの構造:分ける 計量:内積 損失関数:hinge loss マージン:187 なお、349 iterationで打ち切られていたため1000 iterationまで学習させたところ、Acc.@20/1000=57.1%に達しました。 以降、上の条件をHingeDotM187SpFcと呼ぶことにします。 下の図からわかるように、前の章で最も良かったHingeCosM0.5に比べて大幅に精度が良くなりました。 検索結果は以下になります。 query item 正解のshop image HingeCosM0.5 HingeDotM187SpFc 各手法の検索結果の上位20個のshop image(左から右、上から下の順) HingeDotM187SpFcの方が、street queryの柄により近い結果となりました。 また、各項目による精度の違いを見ていきます。 下記の表では計量とCNNの重みの条件以外は、CNNとFCともに共有、Hinge lossで固定したときの精度です。 条件 計量 CNNの重み Acc.@20/1000 HingeCosM0.5 コサイン類似度 fine-tuning 0.283 HingeCosM0.5Fx コサイン類似度 固定 0.152 条件 計量 CNNの重み Acc.@20/1000 HingeDotM187 内積 fine-tuning 0.469 HingeDotM187Fx 内積 固定 0.132 条件 計量 CNNの重み Acc.@20/1000 HingeEucM324 ユークリッド距離 fine-tuning 0.442 HingeEucM324Fx ユークリッド距離 固定 0.136 Mの後の数字はマージンです。 どの計量においても CNNをfine-tuningしたほうが良かった です。 また、計量に関して 内積とユークリッド距離が、コサイン類似度より大幅に良かった です。 マージンを変えたときや、CNNやFCをそれぞれ分けたときにおいても同様の結果が得られました。 また、画像特徴量のノルムの、学習による変化を見てみます。 まず、以下がHingeEucM324のとき。 次に、以下がHingeDotM187のとき。 norm_strtがstreet query、norm_shopがshop image、の画像特徴量のノルムを表します。 どちらも 学習につれ特徴量のノルムが大きくなりました。 次に、CNNを分けたときと共有したときの精度の学習による変化を見ます。 SpCnnと書かれたオレンジの線がCNNを分けた条件で、青い方がCNNを共有した条件です。 CNNを共有したほうが、少ないiterationで良い精度に達しました。 他の計量やマージンを用いたときや、FCを分けときも同様の結果が得られました。 次に、FCを分けたときと共有したときの精度の学習による変化を見ます。 SpFcと書かれたオレンジの線がFCを分けた条件で、青い方がFCを共有した条件です。 FCを分けても、あまり大差はありませんでした。 他の計量やマージンを用いたときや、CNNを分けときも同様の結果が得られました。 次に、損失関数による精度の違いを見てみます。 条件 計量 損失関数 Acc.@20/1000 HingeCosM0.5 コサイン類似度 hinge loss 0.283 SceCos コサイン類似度 SCE loss 0.168 条件 計量 損失関数 Acc.@20/1000 HingeDotM1870 内積 hinge loss 0.469 SceDot 内積 SCE loss 0.049 どちらの計量においても、 損失関数はSCE lossよりhinge lossのほうが良かった です。 考察 CNNの重みを固定すると、FC1層のパラメータによる表現力が足りなかったため、精度が落ちたと考えられます。 コサイン類似度に比べ、内積とユークリッド距離の方が大幅によかった理由は、ノルムを大きくすることでマージンによる悪影響を小さくできたためと考えられます。 ノルムを大きくするとマージンによる影響を小さくできたことについては、付録2で説明します。 fine-grained classificationにおいては、hinge lossよりSCE lossの方が良い結果を出しています。 一方、cross-domain画像検索では、マージンを大きくすることに加え、SCE lossを用いることも有効でないことがわかりました。 まとめ 既存のmetric learningの慣習が必ずしもcross-domain画像検索では有効とは限らないことがわかりました。 また、cross-domain画像検索においてDMLを用いる場合、以下のテクニックが有効だとわかりました。 少ないiterationで良い精度に達したければ、CNNは共有にする CNNはImageNetの重みからfine-tuningする 計量は内積を用いる 損失関数はSCE lossでなくhinge lossを用いる マージンは大きくしすぎない 付録1:ユークリッド距離のときのマージンの変換の導出 コサイン類似度では、以下のようにpositiveとnegativeの類似度を、最悪でもマージン分だけ差をつけたいです。 $$ \cos{\theta_p} = \cos{\theta_n} + m_{cos} $$ :positiveの角度 :negativeの角度 また、positiveとnegativeの離したい角度を とすると、 と表せます。 このときpositiveとnegativeの距離は、 の成す弦と一致するので、以下で表せます。 $$ m_{euc} = 2 ||x||\sin{\frac{\theta}{2}} $$ $$ =\sqrt{2(1-\cos{\theta})}||x|| $$ 今、 、 としたいので以下のようになります。 $$ \cos{\theta} $$ $$ =\cos{(\theta_n-\theta_p)} $$ $$ =\cos{\theta_n}\cos{\theta_p} + \sin{\theta_n}\sin{\theta_p} $$ $$ =\sqrt{1-m_{cos}^2} $$ よって、以下の変換を用いました。 $$ m_{euc} := \sqrt{ 2 (1-\sqrt{1-m_{cos}^2}) }||x|| $$ 付録2:ノルムを大きくするとマージンの悪影響を減らせる理由 まず、内積のときの場合について説明します。 ノルムが大きくなるにつれ内積も大きくなるので、マージンが相対的に小さくなります。 よって、コサイン類似度に比べ、マージンの影響を受けづらかったのだと考えられます。 また、ユークリッド距離の二乗を展開すると、内積のときと本質的に同じことがわかります。 ユークリッド距離を用いたhinge lossは以下を小さくしたいです。 $$ d_p^2 - d_n^2 $$ :positive pairのユークリッド距離 :negative pairのユークリッド距離 距離を展開します。 $$ d_p^2 - d_n^2 $$ $$ = ||x_a-x_p||^2 - ||x_a-x_n||^2 $$ $$ = - 2x_a^Tx_p + 2x_a^Tx_n + ||x_p||^2 - ||x_n||^2 $$ :anchorの画像特徴量 :positiveの画像特徴量 :negativeの画像特徴量 今、positiveとnegativeのドメインは同じなので、それぞれのノルムは同じとみなすと以下を小さくしたいということになります。 $$ - x_a^Tx_p + x_a^Tx_n $$ つまり、positive pairの内積は大きく、negative pairの内積は小さくしたいということになります。 よって、ユークリッド距離を用いたmetric learningは内積を用いた場合と本質的には同じだと考えられます。 ですから、内積のときと同様の理由で、マージンの影響を受けづらかったのだと考えられます。 最後に 研究所ではcross-domain画像検索以外にも「似合う」ということについて多角的に研究を進めています。機械学習に限らず、専門性を活かしてファッションを分析できる環境を提供しています。ファッションに関する研究テーマに挑戦したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。 www.wantedly.com 参考サイト、文献 Metric learning/similarly learningに関する資料集 - めも 距離計量学習とカーネル学習について - a lonely miner Similarity Learning with (or without) Convolutional Neural Network - Moitreya Chatterjee, Yunan Luo ronekko/deep_metric_learning: Deep metric learning methods implemented in Chainer
こんにちは、最近気になっている哺乳類は オリンギート な、開発部の塩崎です。 私の所属しているMarketingAutomationチームではRealtimeMarketingシステムの開発運用を行っております。 このシステムはZOZOTOWNのユーザーに対してメールやLINEなどのコミュニケーションチャンネルを使い情報の配信を行うものです。 メルマガの配信数や開封数などの数値は自動的に集計され、BIツールであるRedashによってモニタリングされています。 このRedashは社内PCによってホスティングされていましたが、運用面で辛い部分が多々あったためパブリッククラウドに移行しました。 移行先のクラウドはawsを選択し、RedashをホスティングするためのサービスはECS/Fargateを選択しました。 この記事ではawsに構築した環境や、移行作業などを紹介します。 移行前のRedash 移行前のRedashの問題点 現行のRedashの紹介 技術選定 構成を詳細に説明 RDS(PostgreSQL) + ElastiCache(Redis) ECS/Fargate Datadog Fargateの利点欠点 利点 EC2の管理から解放される コンテナ数のスケーリングをするときにEC2のスケーリングをしなくても良い AutoNamingによるサービスディスカバリは便利 欠点 コンテナのデプロイが遅い 同一タスク中の複数のプロセスが同じポートで待受できない ログドライバーがawslogsしか選択できない previllegeモードでDockerを動かすことができない まとめ 移行前のRedash 移行前のRedashがどのような環境でホスティングされていたのかを説明します。 Windowsの物理マシンがあり、そのなかにコンテナ管理ツールの Portainer が入っています。 RedashのwebサーバーやRedashを動作させるためのミドルウェアなどはすべてDockerコンテナの中で動いており、その管理をPortainerに任せています。 Redashのデータソースは主に2種類あり、それはBigQueryとPuredataです。 BigQuery はGoogle製の完全にマネージドなDWHです。 BigQueryとの接続にはRedashがデフォルトで用意しているインテグレーション機能を使用しています。 一方、PuredataはIBM製のDWHアプライアンスです。 こちらはRedashとのインテグレーションが用意されていません。 そのため、我々のチームでPuredataと接続するためのゲートウェイを作成し、Redashとの接続に使用しています。 Puredataに接続するためのインタフェースとしてIBMからJDBCドライバーが提供されています。 しかし、RedashはPythonで実装されており、JDBCドライバーを直接読み込むことができません。 何らかの仕組みでPythonとJavaの変換が必要です。 ですので、 Py4J を使いPythonからJavaのメソッドを呼ぶことでRedashからPuredataに接続しています。 参考: RedashとBigQueryのインテグレーション機能 移行前のRedashの問題点 上記で説明したような構成のRedashにはいくつかの問題がありました。 主には物理マシンの管理が面倒という内容です。 物理マシンが落ちた時の復旧が手動 何らかの原因で物理マシンが落ちたときには、物理マシンが置いてある場所まで行き、手動で起動をするという運用が必要になってしまっていました。 ビルの法定停電の時にマシンの電源を落とす必要がある ビルの法定停電の時には停電前に電源を落とし、停電から復旧した後に正常に動作するかどうかを確認する運用が必要になっていました。 現行のRedashの紹介 次にawsに構築したRedashの紹介をします。 Redashの動作に必要なミドルウェアはPostgerSQLとRedisです。 これらはawsのフルマネージドサービスであるRelational Database Service(RDS)とElastiCacheでホスティングすることによって、管理コストを下げました。 RedashとPuredataGatewayのDockerイメージはElastic Container Registry(ECR)で管理されており、ECSはここからdocker pullを行います。 DockerhubでホスティングされているRedash公式のDockerイメージを使わない理由は、Puredataに対応させるために独自のパッチを当てているためです。 次にコンテナ間通信について説明します。 RedashとPuredataGatewayの間はPy4JによるTCP通信をさせる必要があります。 しかしPuredataGatewayコンテナのIPアドレスは起動するごとに変化するので、Redashのコンテナの中にIPアドレスをハードコーディングすることはできません。 Redash workerコンテナがPuredataGatewayコンテナのIPアドレスを取得する仕組みが必要です。 NLBを使うことはその1つの解決策ですが、今回は最近東京リージョンでも使えるようになった Route53 Auto Naming で実現しました。 これはコンテナの起動・終了のタイミングでDNSのAレコードの値が自動的に増減する仕組みです。 Redash側からPuredataGatewayに接続するときにはこのAレコードを参照すれば、現在アクティブなPuredataGatewayのIPアドレスを得ることができます。 NLBではなくAutoNamingを利用した主な理由はロードバランサーを入れないほうが全体の構成がシンプルになるためです。 これらのインフラ構成はすべてCloudFormationで管理されています。 これ以降でインフラ構成を説明するときにはCloudFormationのテンプレートファイルを使って説明します。 また、これらのサービスの監視は Datadog で行っています。 RDSとElastiCacheは aws integration でメトリクスの収集を行っています。 そして、コンテナのメトリクスの収集はDatadog Agentのサイドカーコンテナを配置することで行っています。 FargateとDatadogの連携に関する設定はこの下で詳しく説明します。 技術選定 今回のRedashホスティングにFargateを選定した理由を説明します。 まず、Redashを動作させるためのDockerイメージが公式から提供されているので、それを使うことを考えました。 その場合、何かしらの仕組みでコンテナのオーケストレーションを行う必要があります。 マーケティングオートメーションチームは既にawsでのサービスの開発・運用実績がありましたので、パブリッククラウドとしてawsを選択しました。 awsでDockerのオーケストレーションをしてくれるマネージドサービスはECSとEKSがあります。 EKSはDocker界隈で最近ますます存在感を増しているkubernatesのマネージドサービスです。 ですが、コントロールプレーンのみがマネージドであり、ワーカーノードの管理は自分たちで行う必要があります。 今回は可能な限りインフラの管理をawsに任せたいという思いがあったため、ECS/Fargateを選択しました。 構成を詳細に説明 ここからは今回構築したRedash環境の詳細をCloudFormationのテンプレートファイルを交えながら説明していきます。 RDS(PostgreSQL) + ElastiCache(Redis) まずは、Redashが必要としているミドルウェアであるPostgreSQLとRedisを構築します。 どちらともawsによるマネージドサービスが提供されているので、RDSとElastiCacheを使うことにしました。 今回は高いSLOが求められないので、値段を優先して一番安いインスタンスを使ったシングルAZ構成で構築しました。 たとえシングルAZ構成でも、 OSのセキュリティパッチやデータベースのバックアップはaws側がやってくれます。 そのため、Redash公式のdocker-composeを使った構成よりも管理コストが低いです。 また、可用性を高めるためこれをマルチAZ構成にすることも比較的容易です。 Resources : RDSDBParameterGroupForRedash : Type : 'AWS::RDS::DBParameterGroup' Properties : Description : 'rds parameter group for redash' Family : 'postgres9.6' RDSDBSubnetGroupNameForRedash : Type : "AWS::RDS::DBSubnetGroup" Properties : DBSubnetGroupDescription : 'rds subnet group for redash' DBSubnetGroupName : redash-rds-subnet-group SubnetIds : - !Ref EC2SubnetApplicationPrivateAZ1 # 予め定義しておく - !Ref EC2SubnetApplicationPrivateAZ2 # 予め定義しておく EC2SecurityGroupRDSRedash : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-rds-security-group GroupDescription : 'rds security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupECSRedash # ECSタスクに紐づけているセキュリティグループ FromPort : 5432 ToPort : 5432 IpProtocol : 'tcp' RDSForRedash : Type : 'AWS::RDS::DBInstance' Properties : Engine : 'postgres' EngineVersion : '9.6' AutoMinorVersionUpgrade : true DBInstanceClass : 'db.t2.micro' DBParameterGroupName : !Ref RDSDBParameterGroupForRedash BackupRetentionPeriod : 7 # バックアップの保持期間 StorageType : 'gp2' AllocatedStorage : 20 # ストレージのサイズ(GB) MultiAZ : false AvailabilityZone : !Ref AZ1 # 予め定義しておく PubliclyAccessible : false DBSubnetGroupName : !Ref RDSDBSubnetGroupNameForRedash VPCSecurityGroups : - !Ref EC2SecurityGroupRDSRedash MasterUsername : !Ref RDSForRedashMasterUsername MasterUserPassword : !Ref RDSForRedashMasterUserPassword PreferredBackupWindow : '15:00-17:00' # UTC PreferredMaintenanceWindow : 'Sat:17:00-Sat:19:00' # UTC ## Redis ElastiCacheSubnetGroupForRedash : Type : 'AWS::ElastiCache::SubnetGroup' Properties : CacheSubnetGroupName : redash-redis-subnet-group Description : 'redis subnet group for redash' SubnetIds : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 EC2SecurityGroupRedashRedis : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-redis-security-group GroupDescription : 'redis security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupECSRedash FromPort : 6379 ToPort : 6379 IpProtocol : 'tcp' ElastiCacheParameterGroupForRedash : Type : 'AWS::ElastiCache::ParameterGroup' Properties : CacheParameterGroupFamily : 'redis4.0' Description : 'Redash Redis' ElasticacheClusterForRedash : Type : 'AWS::ElastiCache::CacheCluster' Properties : ClusterName : 'redash-redis' Engine : 'redis' EngineVersion : '4.0.10' AutoMinorVersionUpgrade : true CacheNodeType : 'cache.t2.micro' NumCacheNodes : 1 CacheParameterGroupName : !Ref ElastiCacheParameterGroupForRedash PreferredAvailabilityZone : !Ref AZ1 CacheSubnetGroupName : !Ref ElastiCacheSubnetGroupForRedash VpcSecurityGroupIds : - !GetAtt EC2SecurityGroupRedashRedis.GroupId Port : 6379 ECS/Fargate 次にECS /FargateでRedashをホスティングする部分を説明します。 今回は以下のようにサービスを分割しました。 Redash Server Redash Worker Puredata Gateway PuredataGatewayはPy4JでPythonとJavaの間を取り持つコンテナなので、分離されているのは自然です。 一方でRedash系の2つが分離されているのは違和感ある人がいるかもしてないので、理由を説明します。 Redash ServerはRedashのweb UIをホスティングしているwebサーバーで、このサービスが直接クエリの実行することはありません。 クエリの実行を担うのはRedash Workerの方です。 これらの間は非同期ジョブライブラリの Celery によってクエリのやり取りが行われています。 なお、Celeryによって作られるジョブキューの実体はRedisのリストです。 そしてクエリの実行結果はPostgreSQLに書き込まれ、それがRedash Serverによって読み出されグラフィカルに表示されます。 クエリの個数が多くなった時にRedash Workerのスケーリングが必要です。 ですので、Redash ServerとRedash Workerの分離を行いました。 今回はECSサービスを3つ作りましたが、まずはRedash ServerとRedash Workerのサービス定義を説明します。 特に注意が必要なポイントは以下の2点で、それぞれ後で説明します。 Redashの待受ポートが5100なこと puredata-gateway.redash.internalというドメインでRedash WorkerがPuredata Gatewayに接続すること Resources : ## ECS ECSClusterForRedash : Type : 'AWS::ECS::Cluster' Properties : ClusterName : redash IAMRoleForRedashTaskExecution : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : 'ecs-tasks.amazonaws.com' Action : 'sts:AssumeRole' ManagedPolicyArns : - 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy' IAMServiceLinkedRoleForECSRedashService : Type : 'AWS::IAM::ServiceLinkedRole' Properties : AWSServiceName : 'ecs.amazonaws.com' Description : 'Role to enable Amazon ECS to manage your cluster.' ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : Family : redash-server RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:last Name : 'redash-server' Command : - 'server' Environment : - Name : 'PYTHONUNBUFFERED' Value : '0' - Name : 'REDASH_LOG_LEVEL' Value : 'INFO' - Name : 'REDASH_REDIS_URL' Value : !Sub redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 - Name : 'REDASH_DATABASE_URL' Value : !Sub postgresql://redash:${RDSRedashUserPassword}@${RDSForRedash.Endpoint.Address}/redash # Redash用のRDSユーザーを予め作っておく - Name : 'REDASH_HOST' Value : '' - Name : 'REDASH_WEB_PORT' # Redashが待ち受けているポート番号 デフォルトの5000ではない理由は後述 Value : '5100' - Name : 'REDASH_ALLOW_SCRIPTS_IN_USER_INPUT' Value : 'true' - Name : 'REDASH_DATE_FORMAT' Value : 'YY/MM/DD' - Name : 'REDASH_ADDITIONAL_QUERY_RUNNERS' # Redashに新しいquery runnerを登録するため Value : 'redash.query_runner.puredata' - Name : 'REDASH_JAVA_GATEWAY_HOST' # このドメインはRoute53のAutoNamingで提供される Value : puredata-gateway.redash.internal Cpu : 1024 Memory : 2048 PortMappings : - ContainerPort : 5100 HostPort : 5100 Protocol : 'tcp' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash # 予め作っておく awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'server' ECSTaskDefinitionWorker : Type : 'AWS::ECS::TaskDefinition' Properties : Family : redash-worker RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:${RedashGithubSHA1} Name : 'redash-worker' Command : - 'scheduler' Environment : - Name : 'PYTHONUNBUFFERED' Value : '0' - Name : 'REDASH_LOG_LEVEL' Value : 'DEBUG' - Name : 'REDASH_REDIS_URL' Value : !Sub redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 - Name : 'REDASH_DATABASE_URL' Value : !Sub postgresql://redash:${RDSRedashUserPassword}@${RDSForRedash.Endpoint.Address}/redash - Name : 'QUEUES' Value : 'queries,scheduled_queries,celery' - Name : 'WORKERS_COUNT' Value : '10' - Name : 'REDASH_ADDITIONAL_QUERY_RUNNERS' Value : 'redash.query_runner.puredata' - Name : 'REDASH_JAVA_GATEWAY_HOST' Value : !Sub puredata-gateway.redash.internal Cpu : 1024 Memory : 2048 LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'worker' EC2SecurityGroupECSRedash : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : redash-ecs-security-group GroupDescription : 'ecs security group for redash' VpcId : !Ref EC2VPC SecurityGroupIngress : - SourceSecurityGroupId : !Ref EC2SecurityGroupALBExternalRedash FromPort : 5100 ToPort : 5100 IpProtocol : 'tcp' ECSServiceRedash : Type : 'AWS::ECS::Service' DependsOn : - IAMServiceLinkedRoleForECSRedashService - ECSServicePuredataGateway Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 1 LaunchType : 'FARGATE' LoadBalancers : - ContainerName : 'redash-server' ContainerPort : 5100 TargetGroupArn : !Ref ElasticLoadBalancingV2TargetGroupExternalRedash NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSRedash Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionApplication ECSServiceRedashWorker : Type : 'AWS::ECS::Service' DependsOn : - IAMServiceLinkedRoleForECSRedashService - ECSServicePuredataGateway Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 3 LaunchType : 'FARGATE' NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSRedash Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionWorker 次にPuredata Gatewayのサービス定義を示します。 基本的にはRedashのサービス定義と変わりませんが、ServiceDiscoveryに関する設定が追加されているのが特徴です。 前述したRedashサービスから接続をしていたpuredata-gatewat.redash.internalのサービスディスカバリはここで行われます。 まず AWS::ServiceDiscovery::PrivateDnsNamespace リソースによって、 puredata-gateway.redash.internal というHostedZoneがRoute53に作られます。 そして AWS::ServiceDiscovery::Service リソースによって、そのHostedZoneの中にマルチバリューAレコードが作られます。 最後に AWS::ECS::Service リソースのServiceRegistriesプロパティを指定します。 これによって、タスクの起動終了のタイミングでAレコードの値が自動的に書き換わります。 https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html Resources : ServiceDiscoveryPrivateDnsNamespaceForPuredataGateway : Type : "AWS::ServiceDiscovery::PrivateDnsNamespace" Properties : Vpc : !Ref EC2VPC Name : puredata-gateway.redash.internal ServiceDiscoveryServiceForPuredataGateway : Type : "AWS::ServiceDiscovery::Service" Properties : Name : puredata-gateway DnsConfig : NamespaceId : !Ref ServiceDiscoveryPrivateDnsNamespaceForPuredataGateway DnsRecords : - Type : 'A' TTL : 60 HealthCheckCustomConfig : FailureThreshold : 2 ECSTaskDefinitionPuredataGateway : Type : 'AWS::ECS::TaskDefinition' Properties : Family : puredata-gateway RequiresCompatibilities : - 'FARGATE' Cpu : 1024 Memory : 2048 NetworkMode : 'awsvpc' ExecutionRoleArn : !GetAtt IAMRoleForRedashTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForPuredataGateway} Name : 'puredata-gateway' Command : [ 'command' , 'to' , 'start' , 'puregata gateway' ] Cpu : 1024 Memory : 2048 PortMappings : - ContainerPort : 25333 HostPort : 25333 Protocol : 'tcp' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'puredata-gateway' EC2SecurityGroupECSPuredataGateway : Type : 'AWS::EC2::SecurityGroup' Properties : GroupName : puredata-gateway-ecs-security-group GroupDescription : 'ecs security group for puredata gateway' VpcId : !Ref EC2VPC SecurityGroupIngress : - CidrIp : !Ref ApplicationPrivateCidrBlockAZ1 FromPort : 25333 ToPort : 25333 IpProtocol : 'tcp' - CidrIp : !Ref ApplicationPrivateCidrBlockAZ2 FromPort : 25333 ToPort : 25333 IpProtocol : 'tcp' ECSServicePuredataGateway : Type : 'AWS::ECS::Service' DependsOn : IAMServiceLinkedRoleForECSRedashService Properties : Cluster : !Ref ECSClusterForRedash DesiredCount : 1 LaunchType : 'FARGATE' ServiceRegistries : - RegistryArn : !GetAtt ServiceDiscoveryServiceForPuredataGateway.Arn NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSPuredataGateway Subnets : - !Ref EC2SubnetApplicationPrivateAZ1 - !Ref EC2SubnetApplicationPrivateAZ2 PlatformVersion : '1.2.0' TaskDefinition : !Ref ECSTaskDefinitionPuredataGateway Redashをawsに構築するにあたって、これ以外にもALB、ACM、Route53なども使用しましたが、記事が長くなってしまうのでここでは割愛します。 Datadog これらのコンテナのメトリクスの監視にはDatadogを使用しています。 Datadogでメトリクスを収集するためには、Datadogのコンテナをサイドカーコンテナとして配置します。 例えば、Redash Serverのコンテナのメトリクスを収集したい場合は、以下のようにコンテナを配置します。 環境変数ECS_FARGATEを true に指定するだけで、そのタスクに含まれるすべてのコンテナのメトリクスを収集してくれます。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryForRedash}:last Name : 'redash-server' - Image : datadog/agent:latest Name : 'datadog' Cpu : 256 Memory : 512 Environment : - Name : 'DD_API_KEY' Value : !Ref DatadogAPIKey - Name : 'ECS_FARGATE' Value : 'true' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRedash awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'redash-datadog' https://www.datadoghq.com/blog/monitor-aws-fargate/ Fargateの利点欠点 FargateでRedashを構築する最中に感じた、Fargateの利点欠点などをまとめます。 利点 EC2の管理から解放される まず真っ先に思い浮かぶFargateの利点はEC2を管理する必要がないということです。 ECS on EC2の構築をするためにはEC2インスタンスを立ち上げ、そこにECSエージェントをインストールする必要があります。 Fargateを使うことによって、そのような一手間をかけること無く、いきなりコンテナをデプロイできるので便利です。 コンテナ数のスケーリングをするときにEC2のスケーリングをしなくても良い 前述したことと少し関連しますが、EC2の管理から解放されたことでスケーリングの時に考慮する必要のある要素が減りました。 スケーリングをするときにはサービスのタスク数だけを変化させればよく、従来のECS on EC2で考える必要のあったEC2インスタンスのスケーリングは不要です。 AutoNamingによるサービスディスカバリは便利 システムをマイクロサービス化するときにサービスディスカバリの仕組みはほぼ必須です。 Route53のAutoNamingを使うとDNSレベルでのサービスディスカバリができます。 なお、これはFargateの利点というよりもECSの利点です。 欠点 コンテナのデプロイが遅い Fargateを使った時にはコンテナのデプロイには数分かかってしまいます。 そのため、高速なスケールアウトを期待してFargateを使うとがっかりすることがあるかと思います。 同一タスク中の複数のプロセスが同じポートで待受できない Fargateでは同一タスクに属するコンテナ間の通信をさせるためにはlocalhostを使います。 そのため、同一タスクに属する複数のプロセスが同じポート番号で待受しようとすると、後から待受した方が失敗します。 実際にRedashとDatadog Agentの両方が5000番ポートでlistenをしていたため、Redashのサーバーが立ち上がらないという現象が発生しました。 そのためRedashにパッチを当て、5100番ポートで待ち受けるように修正しました。 CloudFormationのテンプレートで5100番ポートを使ってRedashの待受をしている理由はこれです(伏線回収) ログドライバーがawslogsしか選択できない ECS on EC2の場合はjournaldやfluentdにログを流すことができますが、ECS/Fargateではawslogsドライバーのみがサポートされます。 awslogsドライバーが収集したログはCloudWatchLogsに送られます。 そのため、すでに別のログ収集基盤がある場合には、CloudWatchLogsからそちらにログを流す必要があります。 https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_LogConfiguration.html previllegeモードでDockerを動かすことができない これは些細な欠点ですが、privilegeモードでDockerを動かすことができないので、例えばDocker in Dockerのような構成にすることはできません。 まとめ 社内PCでホスティングされていたRedashをawsに移すことによって、Redashの動作に必要なコンポーネントをすべてフルマネージドにできました。 さらにこの設定はすべてCloudFormationで管理されています。 そのため、他のチームがawsにRedashを構築したくなったときにはテンプレートファイルを渡すことで同等の構成を簡単に作ることが出来るようになりました。 弊社ではZOZOTOWNの売上を支えているMarketingAutomationに興味がある方、クラウドを活用して既存の運用を改善することに興味がある方を募集しています。 興味のある方はぜひ以下のリンクからご応募ください。 www.wantedly.com
こんにちは。ZOZO研究所 福岡の光瀬です。Pythonを書かれている皆様は、普段どのように開発をすすめていますか? pipとvenv/virtualenvによるこれまでのデファクトの組み合わせだけではなく、最近は Pipenv を使用している開発者も増えてきたのではないでしょうか。 日々の検証や開発を効率よく進めるにあたって、依存関係を適切かつ楽に管理するのはとても重要だと感じていて、ここ半年ほどPipenvを利用しています。 今回は、その中でsetup.pyやrequirements.txtそしてPipfileの住み分け・運用について考えたことをまとめてみました。 TL;DR Pipenvが使えることで、確かに楽になった部分はあるのかなと思っています。 一方で、既存のツールとの兼ね合いがまだ微妙な部分もあります。 その上で、以下の運用がベターなのかなと考えました。 Pipenvのみで完結させようとしない(setup.pyの install_requires を併用する) setup.pyの install_requires には、直接の依存関係をゆるく記述し pipenv install -e でPipenv管理下に置く 開発・テストに必要な依存関係をPipfileの dev-packages へ記述する 依存関係のロックが必要であればrequirements.txtではなくPipfile.lockを使用する Pipenvとは何か Pipenv は、一言でいえば pipとvirtualenvをラップして、依存関係を  Pipfile およびPipfile.lockで管理するツール です。 いわゆるRubyにおけるbundlerやNode.jsにおけるnpmあるいはyarnのような立ち位置ですね。 今やごく当たり前になっているであろう開発フローをPythonにおいても実現できます。 例えば、以下に列挙する機能が実装されています。 Pipfileによる依存関係の管理 Pipfileによる開発用の依存関係の管理 Pipfile.lockによる依存関係のロック virtualenv環境の自動構築 Pipfileで指定された python_version と現在のバージョンとのチェック Pipfileで指定された python_version の処理系の自動インストール(pyenvが利用可能な時のみ) ..。など より詳しくは 公式ドキュメント をどうぞ。なお、Pipenvには 日本語ドキュメント もあります。また、 作者のKenneth Reitz氏のPyCon 2018におけるスライド もオススメです。 Pipenv以前の話 pipおよびvenv/virtualenvによる開発フロー Pythonを書かれている皆様は、普段どのように開発をすすめられていますでしょうか? Pipenv以前、私は以下のようなフローで開発していました。 使用したいバージョンの処理系をインストールする venv/virtualenvでプロジェクト用の環境を切る 「使用したいライブラリ」をsetup.pyの install_requires に列挙して pip install -e . でインストールする テスト時に利用する依存関係はsetup.pyの test_requires に列挙する pip freeze でrequirements.txtを吐きだしておく(ライブラリではない、依存関係の終端であるプリケーションの場合) 一見、特に問題がないように見えますが、 開発時に必要な依存関係の管理方法が明確ではない のが1つの問題なのかなと思います。 例えば、静的型チェッカーである mypy を使って、編集しながら型チェックをしていきたい場合があります。 この場合、mypyはどこに依存関係として記述するのが適当でしょうか? 実際にアプリケーション・ライブラリが利用される際には、通常不要なツールです。 そのため、少なくとも install_requires に書くのは適当でなさそうです。 一方、テスト時にのみ使用できれば良いということではないので test_requires に書くのも違うように思えます。 開発時に必要な依存関係の管理に対するワークアラウンド 開発時に必要な依存関係の管理については、明確な答えはないものの、いくつかのワークアラウンドで対処されてきたようです。 setup.pyの extras_require を利用してオプショナルな依存関係として扱う requirements.txtに加えて、開発用に別途requirements-dev.txtを用意して pip install -r requirements-dev.txt する いずれの場合も、 pip freeze した際に開発用の依存関係がrequirements.txtへ混入します。 requirements.txtを依存関係のロックとして扱いたい場合に、余計なパッケージが含まれるのはあまり嬉しくありません。 Pipenvが解決した問題 開発・テスト用の依存関係の管理 Pipfileには、開発・テスト用の依存関係が記述・記録される専用の dev-packages というテーブルがあります。 コマンドラインからは、 pipenv install --dev {package_name} でインストール・Pipfileへ記録できます。 Pipfile.lockには開発用の依存関係も記録されますが、 pipenv install 時に --dev を付与した場合のみインストールされます。 pipやvirtualenvなど各種ツールの隠蔽 環境構築について、pipやvirtualenvそしてpyenvを直接さわらずとも環境がつくれるようになっています。 既存ツールをラップしながら、他の言語で提供されているツールと同等の機能を提供している点は、地味ながらもひとつの利点です。 少なくとも優れたパッケージ管理ツールを備えた他の言語のユーザーがPythonを触る際の驚きは減るはずです。 Pipenvが解決できていない問題 依存関係上の終端になるようなアプリケーションであれば、setup.pyを用意せずPipenvのみで開発を進められるように思います。 ただし、pipはPipfileを読まないため、ライブラリの場合には、Pipenvのみでは完結しません。 直接pipからインストールされるライブラリが大半であると思われる現状、ライブラリの開発においてはsetup.pyを書かざるをえなくなっています。 結局どのように運用するのが楽なのか Pipenvの公式の見解が Pipfile vs Setup.py に書かれています。 他のコードから参照される「ライブラリ」であればsetup.pyでまず管理すべきで、どこからも参照されない「アプリケーション」であればPipfileで依存関係を管理するのが良いという話ですね。 この公式の見解を踏まえつつ、ライブラリ・アプリケーションの区別をせずに同じフローへ落とすのが楽なのかなと考えました。 pipからインストール可能にするため、 install_requires を使用する 不要になったパッケージを排除しやすくするため、 pipenv install -e でプロジェクトのパッケージをインストールし、依存関係はPipenv管理下に置く 開発・テスト時にのみ必要なパッケージはsetup.pyに入っている必要はないので、Pipfileに書く 依存関係のロックが必要でない場合は、 pipenv install をする際に --skip-lock するのが適当かと考えています。 もちろん、ライブラリの開発の場合は、setup.pyだけで最低限管理できます。 とはいえほとんどの場合、開発時にのみ使用するツールが入ってきますし、どのみちvenv/virtualenv環境もつくります。 そのため、初めからPipenv管理下に置いてしまった方が楽な印象です。 まとめ:Pipenvは依存関係の管理を楽にしたのか 依存関係の管理については、方法にブレが減ったという意味では楽になったと感じます。 加えて、pipとvirtualenvのラッパーとして振る舞うので、特に意識しなくとも他プロジェクトの環境と分離して管理できるのも良い点です。 一方で、pipはPipfileおよびPipfile.lockを参照できないため、「便利ではあるけど既存ツールを完全に置き換えたものではない」というのが現状のように思います。 基本はsetup.pyを書くという前提で、Pipfileを併用していこうと考えています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
こんにちは。バックエンドエンジニアの田島( @katsuyan121 )です。 弊社ではデータマートをBigQuery上に構築しています。データマートはデータベース全体のデータのうち、必要なデータだけを使いやすい形にしたデータベースです。データマート作成のためのSQLクエリは日々更新や追加があり、BigQueryのコンソールから自由にデータマートを作ってしまうと管理が大変になってしまいます。 そこで、データマートをすべてGitHub上でバージョン管理し、運用の効率化をしました。また、差分更新の導入や依存関係のあるデータマートへの対応などのデータマート構築に必要な機能を作成しました。 弊社のデータ基盤をざっくり紹介します。まずデータはBigQueryへ集約し、Digdagを用いてデータ基盤を構築しています。以下がその概要図です。S3などの分散ストレージや各種DBからデータをBigQueryへ同期し、BigQuery内部でデータマートを構築します。本ブログではこのうちのデータマート構築について紹介します。 BigQueryでのデータマートの実現方法 データマートをBigQuery上に構築していると紹介しましたがその実態は以下の2つです。 BigQueryのView BigQueryのTable 計算コストが小さいマートに関しては View を用いることでデータの鮮度を保つようにします。逆に計算コストの高いマートは事前に集計などの計算をし Table として保持することで、参照時にコストの高い計算しなくて済むようにしています。 データマート構築の問題点 冒頭でもいくつか紹介しましたがデータマートの構築には以下のような問題が存在します。 データマートは日々更新や追加がされる 現在弊社のデータマートの Table View の数は100を超えており、今でも日々追加されている データマートをBigQueryのWebコンソールから自由に作ってしまうと管理が大変 誰が作ったのかわからない 一時的に作ったデータマートなどがどこから参照されているかわからない 溜まってしまったデータマートがどこから参照されているかわからないので不用意に消せない またテーブルを用いてデータマートを構築する場合以下のことが問題となります。 更新に時間がかかる データマートがどういったクエリにより作られたかわからない 定期的にテーブルを更新する必要がある データマートの Table View が他の Table View を参照している場合、更新する順番を間違えるとデータに不整合が生じる そこで、これらの問題を解決するためのシステムを作成しました。 データマートのクエリをGitHubで管理する BigQueryのViewやTableはBigQueryのWebコンソール画面から簡単に作ることができます。しかしそれでは誰が作ったマートなのかなのかが簡単には分からないなどの運用の問題が生じてしまうと紹介しました。そこでSQLファイルを作成し、それらをAPIからBigQueryに反映をします。実際にはRubyの Google Cloud SDK を利用して反映を行っています。 また、SQLファイルのファイル名を View または Table の名前にすることで、実際のマートとSQLファイルとの対応関係が簡単にわかります。以下がその概要です。 Before After マート作成方法 BigQueryのコンソールから手動で作成 SQLファイルをGit管理しAPI経由で作成 以下に View Table をデータマートへ反映する方法を示しますが、全てのマートをSELECT文のSQLファイルとして表現できます。 BigQueryにSQLを反映する仕組みを作ったことで、すべてのマートをSQLファイルとしてGit管理できるようになりました。 Viewのマート反映 View のマートに関しては作成したSQLをそのままBigQueryに反映します。 新規作成の場合 require ' google/cloud/bigquery ' sql = File .read( ' テーブル名.sql ' ) bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) dataset = bq_clent.dataset( ' データセット名 ' ) dataset.create_view( ' ビューの名前 ' , sql, standard_sql : true ) 更新の場合 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) dataset = bq_clent.dataset( ' データセット名 ' ) view = dataset.table( ' テーブル名) view.set_query(sql, standard_sql: true) テーブルのマート反映 テーブルのマートについてはBigQueryの Destination Table という機能を使うことで、SELECT文の結果をそのままテーブルに反映できます。 以下のようなコードを実行することでBigQueryにSELECT文の結果がテーブルとして作成されます。この処理は、BigQueryによってアトミックにテーブルが作成されます。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) job = bq_client.query_job(sql, standart_sql : true , table : ' テーブル名 ' , write : ' truncate ' , create : ' needed ' ) job.wait_until_done! 差分更新 計算コストの高いマートは事前に集計などの計算をし Table として保持すると紹介しました。しかし集計するデータが大きすぎる場合、毎回全量のデータを集計・更新すると時間がかかりすぎてしまいます。 例えば日付ごとに集計するテーブルの場合、毎日全量のデータを集計し直していると最新の日付以外の集計処理は前日と同じ計算をしてしまうことになり集計処理が増えて行きます。そこで差分更新をすることでこの問題を解決します。 本システムでは差分更新にappendとoverwriteの2種類を行っています。 append appendは集計した結果を既存のテーブルに対してinsertします。 以下のようなテンプレートに対しSQLファイルの中身をそのまま埋め込むことで、「テーブルのマート反映までの流れ」で紹介した処理と同じようにBigQueryに反映できます。 SELECT * FROM `既存のテーブル` union all ( <%= sql %> ) overwrite 一部集計した結果を既存のマートに上書きしなければならない場合appendだけでは対応できません。overwriteの処理ではそのようなケースに対応するため pk などのユニークキーを利用して集計結果をマージします。 こちらもappendのときと同じように以下のようなテンプレートに対してSQLを埋め込みBigQueryに反映します。 SELECT * except(priority, row_number) FROM ( SELECT *, row_number() over (partition by <%= pk %> order by priority) as row_number FROM ( SELECT *, 1 AS priority FROM ( <%= sql %> ) union all SELECT *, 2 FROM `既存のテーブル` ) ) WHERE row_number = 1 以上のように差分更新する対象のテーブルは統一した方法で差分更新が行われるようになりました。 append更新の改善 最初は以上のようにappendとoverwriteの処理を通常の更新処理と同じように行っていました。しかし運用しているうちに SELECT * FROM により全件のデータを毎回取得していることが以下の2点で問題になりました。 処理に時間がかかる BigQueryはクエリの使用量に対して課金されるためお金がかかってしまう そこでBigQueryの append の機能を利用するように変更しました。この、 append によるデータの追記もBigQueryによりアトミックに行われることが保証されています。以下のコードで、それを実現しています。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) sql = File .read( ' テーブル名.sql ' ) job = bq_client.query_job(sql, standard_sql : true , table : ' テーブル名 ' , write : ' append ' , create : ' needed ' ) job.wait_until_done! これにより、 SELECT * FROM の処理をなくすことに成功しました。ただし、overwriteは以上の解決策は適用できないため上で紹介した通りに今も更新を行っており、課題となっています。 冪等性の担保 差分更新の仕組みを紹介しましたが、差分更新の処理があると何かの問題で処理を再実行した場合データが重複するといったデータの整合性に対する問題が生じてしまいます。 例えば、既存のテーブルに対して append の処理をします。その後再実行したい場合、もう一度 append の処理を行うと append により追加されたデータが重複してしまいます。 そこで、以下のようにすることで冪等性を担保し、何回同じ処理をしても同じ結果になるよう工夫しました。 また、 overwrite でも同じように処理を行います。 マート更新時に、マートと同じデータを保持したバックアップを作成します。そして、差分更新を行う場合はバックアップに対して差分更新を行い、その結果を実際のマートに反映します。 2018-01-06 にマートを更新すると以下のような処理になります。 これをコードにしたものが以下になります。 require ' google/cloud/bigquery ' bq_client = Google :: Cloud :: Bigquery .new( project : ' project ' , credentials : ' path ' ) dataset = bq_clent.dataset( ' データセット名 ' ) table = dataset.table( ' mart_table ' , skip_lookup : true ) backup_table_yesterday = dataset.table( ' mart_table_20180105 ' , skip_lookup : true ) backup_table_today = dataset.table( ' mart_table_20180106 ' , skip_lookup : true ) backup_table.copy_job(destination_table, create : ' needed ' , write : ' truncate ' ) # バックアップテーブルを実際のテーブルにコピー job = bq_client.query_job(sql, standard_sql : true , table : table, write : ' append ' , create : ' needed ' , &job_configuration) job.wait_until_done! raise " Fail BigQuery job: #{ job.error }" if job.failed? table.copy_job( ' バックアップテーブル.sql ' , create : ' needed ' , write : ' truncate ' ) # 更新されたテーブルをバックアップ 以上のように差分更新をすることで、何回リトライしてもデータの整合性が保たれるうようになりました。 マートの依存関係 マートが他のマートを参照している場合、更新する順番を間違えるとデータに不整合が生じると紹介しました。例えば existing_table1 existing_table2 のテーブルが存在するとし、以下のような4つのマート構築を考えます。その場合、「table3の前にtable1」「table4の前にtable1とtable3」が更新されている必要があります。 table1.sql SELECT * FROM `project.dataset.existing_table1`; table2.sql SELECT * FROM `project.dataset.xisting_table2`; table3.sql SELECT * FROM `project.dataset.table2`; table4.sql SELECT * FROM `project.dataset.table1` UNION ALL SELECT * FROM `project.dataset.table4`; そこで以下のようなグラフを作成し、実行順を確認します。このグラフは、SQLの FROM または JOIN の後ろのテーブル名とSQLファイル名を利用します。FROMの後ろに書かれているテーブルはSQLファイル名のマートよりも前に実行しなければなりません。そのため「FROMの後ろのテーブル名」->「SQLファイルのファイル名」というグラフを作成します。 例えば上の4つのSQLからマートのグラフを作成すると以下のようになります。 以下のようなコードでグラフを生成します。 graph = {} sql_files = [table1.sql, table2.sql, table3.sql, table4.sql] sql_files.each do | sql_file | sql_file_table_name = sql_file.split( ' . ' ).first sql = File .open(sql_file, ' rt:UTF-8 ' ) { | file | file.read.chomp } related_tables = sql.scan( /(?: FROM | JOIN )[\s \n]+ ` (.+?) ` /i ) graph[sql_file_table_name] = related_tables.map do | str | _, table = str.first.split( ' . ' ) next if table == sql_file_table_name # 自己参照は除外 table end .compact end この結果に対してマート以外のテーブルを削除すると、以下のような結果になります。 [ { ' table3 ' => [ ' table2 ' ]}, { ' table4 ' => [ ' table1 ' , ' table3 ' ]} ] 並列実行 以上のグラフを利用することで、マート作成のクエリの実行の順番を担保したまま並列化できます。並列実行を簡易化するために以上のようなグラフを、以下のようなテーブルのリストのリストに変換します。各テーブルのリストは先頭から順番に実行でき、テーブルのリストの中身はそれぞれ並列実行することが可能になります。 [[table1, table2], [table3], [table4]] この変換は、まず親ノードがないテーブル一覧をリストに追加します。そして追加したテーブルをグラフから削除し、もう一度親ノードがないテーブル一覧をリストに追加します。これを繰り返すことで、以上のようなリストが生成されます。 以下のようなコードでグラフからリストを生成します。 def sort_tables (graph) graph = graph.dup result = [] until graph.empty? nodes = graph.keys.select { | node | graph[node].empty? } result << nodes raise ' cyclic path detected! ' if nodes.empty? nodes.each { | node | graph.delete(node) } graph.transform_values! { | v | v - nodes } end return result end このリストを利用し、最初に「table1, table2」続いて「table3」最後に「table4」を実行します。 並列実行はDigdagで実現しており、以下のようなconfigファイルになっています。 +set_refresh_views: call> set_refresh_views # REFRESH_TABLESESにテーブルのリストのリストを格納 +update_views: for_each>: REFRESH_TABLES: ${REFRESH_TABLESES} _parallel: false _do: for_each>: REFRESH_TABLE: ${REFRESH_TABLES} _parallel: true # ここをtrueにすることで並列実行を実現 _do: _retry: 3 call> refresh # 実際にマートを更新する処理 以上のように実行する順番を考える必要があった所を、システムにより自動解決することに成功しました。さらに、並列実行をすることで更新スピードの短縮にも成功しました。 このシステムを作ってどうなったか 最後に本システムによりデータマートの運用に関してどのようになったか紹介します。 GitHubでマート管理できるようになり日々追加されるマートの管理を統一したルールで管理できるようになった 差分更新や並列実行を導入したことで、データマートの更新の時間短縮に成功した データマートのすべてのクエリをGitHubで参照できるようになった システムを定期的に実行することでテーブルの更新が自動化された データマート同士の参照が自動解決されるようになった 以上のように冒頭に挙げた問題を解決することに成功しました。またその他に以下のようなメリットが見られました。 冪等性の担保をしたことで、マート更新の再実行を気軽にできるようになった マートのSQLをGitHubで管理することによりコードレビューが活発になった GitHub上でレビューができるため誰でも気軽にマートを追加できるようになった 以上のようにデータ基盤のコード管理とマート管理をGIthubに統一、システム化することで様々なメリットが得られました。 最後に 本文でも紹介しましたが、本システムにはまだまだ問題が残されています。弊社では一緒にデータ基盤を作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください! www.wantedly.com
こんにちは! 好きなスシローは 五反田店 なバックエンドエンジニアのりほやん( @rllllho ) です。 9/6,7,8に開催された builderscon tokyo 2018 へ参加しました。 カンファレンスで印象に残ったセッションをいくつかご紹介します。 buildersconとは 公式サイト にはbuildersconについて下記のように説明されています。 buildersconは「知らなかった、を聞く」をテーマとした技術を愛する全てのギーク達のお祭りです。buildersconではトークに関して技術的な制約はありません、特定のプログラミング言語や技術スタックによるくくりも設けません。 必要なのは技術者達に刺激を与えワクワクさせてくれるアイデアのみです。 あなたが実装したクレイジーなハックを見せて下さい。あなたの好きな言語のディープな知識をシェアしてください。あなたの直面した様々な問題と、それをどう解決したかを教えてください。未来技術のような未知の領域について教えてください。 記述の通り、プログラミング言語に特化したカンファレンスではなく包括的に様々な技術を扱っているカンファレンスです。 印象に残ったセッション 知らなかった時に困るWebサービスのセキュリティ対策 speakerdeck.com tnmtさんによる カラーミーショップで実際に発生したセキュリティインシデント の実例を元に、どのような攻撃が仕掛けられたのか、どのような対処・再発防止を行ったかについてのお話でした。 上記インシデントの検知は、不正なファイルが置かれたことを検知したのではなく不正なファイルが実行されたことにより発生した負荷アラートがきっかけで発覚したそうです。 またデータベース関連のログが不足しており、データベースへアクセスされた痕跡はあったものの何件抜かれたかなどがわからない状況だったそうです。 インシデントを受けセキュリティ新体制の見直し、WAF、OSコマンドの実行ログ、クエリログを残すなどの対応を行なっているそうです。 webアプリケーションに携わっている身として、セキュリティの問題は絶対避けては通れない問題です。 実際に起こってしまったインシデントに対しての対応や経験など実体験を聞くことができ、とても勉強になりました。 「Webとは何か?」あるいは「WebをWebたらしめるものは何か?」 www.youtube.com Jxckさんによるwebのこれまでの歴史、いまのwebの役割とは何なのか、これからのwebはどうなっていくのかというお話でした。 ティム・バーナーズ=リーが構想した最初のwebはHypermedia Systemとしてのwebでした。 JavaScriptやiframeなどを使用できるようになりwebによってできることが増え、webはHypemedia Systemからアプリケーションシステムへと役割が変わっていきました。 さらに現在ではwebからデバイスに接続できるようにもなり、Operation System化しているとのことでした。 webはセキュリティモデルと共に進化しているという話が印象的でした。 Ajaxの登場によりJavaScriptがブラウザの意図しない情報を通信できるようになったため、セキュリティモデルとしてoriginが導入され、このoriginというセキュリティモデルがあるおかげでAjaxを正当化できwebのアプリケーション化が進んだそうです。 そのためOS化しようとしている現在のwebのセキュリティモデルであるPermissionの必要性についてお話しされていました。 セキュリティモデルをどのように変化させて策定するかがwebの進化にとても重要であるという話がとても面白かったっです。 ソーシャルゲームが高負荷に陥っているとき、何が起こっているのか speakerdeck.com takihitoさんによるゲームの開発や運用の体制についてのお話でした。 実際にゲームを運用する上で発生した高負荷による障害とその対応について4つ紹介されました。 アプリケーションサーバーに対する高負荷検証は行なってはいたが、ログサーバーが盲点となっておりボトルネックになってしまった話 リリース後にサーバーの負荷が異常に高くなっており確認したところ、Redisのkeysコマンドがアプリケーションで使用されており高負荷になってしまっていた話 大規模な流入施策を行うために検証を行なってはいたが、アプリ申請後に重たいリクエストをアプリから頻繁に叩かれていることに気づきAPIを軽量に研ぎ澄ました話 ガチャによる高負荷によりDBが詰まり緊急メンテナンス、DBのチューニングやアプリケーションコードの修正を行なっても収まらず最終的にはテーブルの設計変更を行った話 ゲームならではの急激な高負荷についてのお話はとても興味深かったです。 高負荷対策のお話の中で、レプリケーション遅延なども考慮しなければいけないためアプリケーションのスケールアップよりDB関連のスケールアップの方が難しいということをおっしゃられていてDB設計の重要さを再確認しました。 RDB THE Right Way 〜壮大なるRDBリファクタリング物語〜 speakerdeck.com soudaiさんによるRDBのリファクタリングについてのお話でした。 アンケートフォームの設計を例に、RDB設計のアンチパターンをご紹介されていてとてもわかりやすかったです。 DBのテーブル設計を行う際に初めからデータの設計やデータストア選定などをしてしまいがちですが、最初にデータ設計をせずにモデリングを正しく行うことがとても重要だそうです。 またテーブルの責務はクラスの責務に似ており、テーブルのスコープを小さくすることを意識した方がよいとのことでした。 データベースの問題はすぐに顕在化するものではなくリリース後忘れた頃に顕在化するという話にはとても共感しました。 プロジェクトに途中から入った際にDB構成を理解する方法として、チームで『テーブル一覧を眺める会』を開催するとよいそうです。 中には誰も知らないテーブルが出てきたりするそうです。さっそくチームでやってみようと思います。 解決した問題の大きさがエンジニアの価値、大きな問題に立ち向かおう。という言葉がとても印象的でした。 自分もエンジニアとして、大きな問題に技術で立ち向かっていきたいです。 ブログサービスのHTTPS化を支えたAWSで作るピタゴラスイッチ speakerdeck.com aerealさんによる、独自ドメインで運用されているはてなブログのHTTPS証明書を配信・発行する仕組みについてのお話でした。 証明書配信に関しては、はてなブログでは万単位の独自ドメインがありリクエストごとに証明書を取得・使用するため低レイテンシであることが求められます。 そのためmemcacheに証明書をキャッシュしデータストアであるDynamoDBへの問い合わせを減らすことで、レイテンシを悪化させずに証明書の配信を行なっているそうです。 証明書発行に関しては、発行リクエストが失敗し続けるとAPIの上限回数を超えてしまうため、失敗した場合に対象から外したり外部API通信のエラーを適切に処理する必要があります。 AWSのStep Functions(SFn)というワークフローサービスとLambdaを組み合わせた仕組みになっているそうです。 DynamoDBへ保存する際に設定するTTLが切れたタイミングでトリガーが発火し証明書を更新するLambda関数が呼び出され、証明書の自動更新を行なっているとのことでした。 データ自身が更新タイミングを持つことにより、更新するLambdaが更新対象を抽出する必要がなくなります。 Lambdaの責任が証明書を更新することだけに切り分けられ、関数がシンプルになるということでした。 ワーフクローエンジンのみがバッチの状態・順番を知っており、関数やクラスは状態を保存せず入力に対し処理を実行し出力するというシンプルな形にすることでバッチの複雑性を排除したそうです。 まとめ DB設計の話や高負荷の話など、私が仕事で使っている身近な技術のお話を聞くことができとても勉強になり、たくさんの『知らなかったことを、聞く』ことができました。 カンファレンスでたくさんの知見を得たので、業務に還元していきたいと思います。 エンジニアが好きな技術や作ったもの、経験したことを熱く喋るお話はとても面白かったです! 運営の方・ご登壇の方々、素敵なカンファレンスをありがとうございました! スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
こんにちは。品質管理部エンジニアリングチームの高橋です。 今回は品質管理部として初のTECH BLOG投稿ということもあり、 「品質 / Quality」について掘り下げてみたいと思います。 「品質」の意味 「品質」という言葉の語源は古代ギリシャにまで遡ります。 「万学の祖」と称されるアリストテレスは、物質(Substance)を「量的側面」と「質的側面」に分けて定義しました。 その際に量的な側面を表現する言葉として英語の質量(Quantitiy)に相当するギリシャ語を用いました。 そして対立概念である質的な側面を表現する言葉として、英語のQualityの語源となったラテン語のQualitasを用いたと言われています。 月日は流れ現在、ISO 9000シリーズにて「品質」は「本来備わっている特性の集まりが、要求事項を満たす程度」と述べられています。※ISO 9000シリーズとは、国際標準化機構が定めた品質マネジメントシステムに関する規格の総称です。 『プログラミングの心理学』や『一般システム思考入門』を著した、ソフトウェア開発の人類学者であるジェラルド・ワインバーグは、「品質は誰かにとっての価値である」と自著内で述べています。 またソフトウェアエンジニアリングとしてのソフトウェア「品質」は以下のように定義されています。 (以下、 https://en.wikipedia.org/wiki/Software_quality より引用) In the context of software engineering, software quality refers to two related but distinct notions that exist wherever quality is defined in a business context: →ソフトウェアエンジニアリングにおいて、 ソフトウェア品質 とは、2つの関連しているが、異なる概念が定義される。 ・Software functional quality reflects how well it complies with or conforms to a given design, based on functional requirements or specifications. That attribute can also be described as the fitness for purpose of a piece of software or how it compares to competitors in the marketplace as a worthwhile product. It is the degree to which the correct software was produced. →ソフトウェアの 機能品質 は、機能要件または仕様に基づいて、与えられた設計に準拠しているかを指す。その特性はソフトウェアの目的に対する適合度、もしくは市場において競合ソフトと比べてより価値があると判断するための手段とも言うことができ、どれだけ正確にソフトウェアが作られたかの度合いである。 ・Software structural quality refers to how it meets non-functional requirements that support the delivery of the functional requirements, such as robustness or maintainability. It has a lot more to do with the degree to which the software works as needed. →ソフトウェアの構造的品質とは、堅牢性や保守性などの機能要件の提供をサポートする 非機能要件 を指す。ソフトウェアが必要に応じて動くための数多くの要件がある。 「品質」が良いとは 品質の良さに関して、「立場」から考えてみます。 もし自分が経営者の立場であったならば、最優先するべきは売上げです。 その為、会社として利益の大きいもの=品質が良いと考えられます。 では経営者ではなく研究者の立場だったらどうでしょう。 研究はそもそも莫大なお金がかかるものであり、研究者が研究中にコストやコストパフォーマンスを優先することはないでしょう。 おそらく研究者の方は、世界に比類ない発見や革新的なものを素晴らしいものと考えるはずです。 経営者 →最大限の利益が出るもの 研究者 →最新のテクノロジーが盛り込まれた革新的なもの 開発者 →要求された機能が実装された、整然としたコードで作成されたもの テスター→仕様書通りに動作する不具合のないもの このように、「品質」の基準は、判断する人の立場によって決定されます。 今度は品質の良さに関して、「物・サービス」から考えてみます。 例えば1万分の1の確率で何らかの不具合が発生する物・サービスの存在を仮定します。 そのサービスが「本の製本」だとしたら。製本ですので、1万冊に1冊の割合で落丁が発生します。 しかし出荷段階で弾いたり、無償で返品対応を行えば、そこまで大きな問題にはならないはずです。 ではそのサービスが「医療機器」だとしたら。 1万台に1台の割合で使用途中に動作が停止してしまうペースメーカーが出荷されたとしたらどうでしょう。 おそらく社会的な問題になるのではないでしょうか。 医療機器に限らず、人の命に関わる物・サービスに関しては、たとえ1万分の1であっても不具合は許されません。 このように、「品質」の基準は、物・サービスの目的によっても変わってきます。 上記のように様々な「品質」の価値基準を一概に定めることは不可能である為、必然的にどこに基準を置くかが重要になってきます。 では品質管理部の一員としての私は、「品質」の基準をどこに定めればよいのでしょうか。 ここでもう1度ジェラルド・ワインバーグの言葉を反芻してみます。 「品質は 誰か にとっての価値である」 この「誰か」とは、「 ユーザー/顧客 」と定義するべきでしょう。 すなわち、「品質」の価値基準は「ユーザー」です。 自社の提供する物・サービスを利用するユーザーの皆さんが、どうすれば便利で・安全に・楽しくサービスを利用できるか考えるのが品質管理部の使命ではないでしょうか。 「品質管理」とは 前項で品質の基準はユーザーに置くべきと述べました。自ずと「品質管理」の指針も見えてくるのではないでしょうか。 ただここで1つ留意しておきたいのが、 品質管理の成功 = ユーザーの満足 品質管理の失敗 = ユーザーの不満足 であるとは必ずしも言えないということです。 1980年代に狩野紀昭によって提唱された、狩野モデルという品質に関するモデルが存在します。 狩野は品質を「当たり前品質」「一元的品質」「魅力的品質」「無関心品質」「逆品質」の5つに分類しました。 そしてその品質がどの品質要素に該当するかによって、顧客満足度に与える効果が異なると提唱しました。 たとえ不具合が残っていたとしても、狩野モデルでいう「無関心品質」に該当する品質の不具合であればユーザー満足度に大きな影響は与えません。 逆に他の「一元的品質」や「魅力的品質」の品質の完成度が高ければ、ユーザーは十分に満足することでしょう。 ただどちらにせよ、品質の基準 = ユーザー の公式は基本的にゆるがないと思っています。 よって、「品質管理」= ユーザーが満足できるサービスが提供されるように導くこと だと私は考えています。 企画→開発→テストの全ての工程において、ユーザーを最優先に考える。そしてそれを継続することではじめて、「品質」が向上していくと思います。 「お客様は神様」という言葉が、接客を伴う業務の際に周知されたりしますよね。 多少オーバーな表現の気もしますが、ユーザーが直接目に見えないソフトウェア開発においても、お客様を第一に考えて業務にあたる姿勢が何よりも大切ではないでしょうか。 おわりに 今回は品質管理部の初投稿ということで、「品質」に関して記事を書かせて頂きました。 記事中でも述べたように、「品質」や「品質管理」に答えはないと思っています。 みなさんの考える「品質」とはなんでしょうか? ぜひ考えをお聞かせ下さい。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.starttoday-tech.com 参考文献 / クレジット 参考文献 https://en.wikipedia.org/wiki/Software_quality https://ja.wikipedia.org/wiki/ISO_9000 https://ja.wikipedia.org/wiki/%E7%8B%A9%E9%87%8E%E3%83%A2%E3%83%87%E3%83%AB クレジット https://www.flaticon.com/free-icon/check-list_1102560#term=check&page=2&position=54 https://www.flaticon.com/free-icon/medal_1067629#term=quality&page=3&position=22
Kotlin Fest2018参加レポート 福岡研究所の渡辺(しかじろう @shikajiro)です。Kotlinのおっきなイベントが東京で開催されるということで福岡から飛んで✈いきました。 福岡でもFukuoka.ktという名前で過去に2回ほどイベントを主催しており、KotlinFest主催の太郎さんに登壇していただいたこともありました。僕自身3か月ほどKotlinから離れてましたが、直近の技術情報などをフォローできたらいいなと思い参加しました。 KotlinFestとは 2018/08/25(土)に開催された有志による日本最大のKotlinイベントです。 kotlin.connpass.com 「Kotlinを愛でる」をビジョンに、Kotlinに関する知見の共有と、Kotlinファンの交流の場を提供する技術カンファレンスです。 とあるように、Kotlinを愛するKotlinユーザーグループのメンバーにより運営されています。 参加者プレゼント すごい作りのしっかりしたトートバッグをもらいました! まじ嬉しい! 企業ブース イベントはトークセッションだけでなく企業ブースの参加もありました。CyberAgentさんによるKotlinPuzzlersが人気で、「ぱっと見では間違えてしまいそうなKotlinコード」がどのように動くのかを当てる内容です。 (ぱっと見でわからないコードってそもそも問題だよね) 僕も挑戦しましたがまんまと間違えたので早々に去りました。 Yahooさんのブースではくじ引き(外れました)とモブプロをやっていました。群衆(モブ)プログラミング(プロ)は皆に見せながらプログラミングすることで、外野と「これはこう書くよね」とコミュニケーションを取ることでコードの質を上げていくものです。 オープニング オープニングセッションでは主催の長澤太郎さん(以下太郎さん)によるKotlinの歴史やこれからのお話がありました。 Kotlinを愛でる が今回のテーマ。Kotlin in Actionの第二部のタイトルが元ネタだそうです。 Kotlinの歴史を振り返ると、発表から1.0まで5年ほど時間がかかりましたが、それからは怒涛の勢いでリリースされています。AndroidやSpring公式になることで利用ユーザーもどんどん増えていっています。 2011/7 kotlin発表 2016/2 1.0リリース 1.1コルーチン 1.2マルチプラットフォーム 2016/5 gradle 2017/4 native 2017/5 Android公式言語に 2017/9 Spring Framework5公式に 1.3コルーチンとインラインクラス 2013/7にはJapan Kotlin User Group(JKUG)も発足しており、勉強会、Slack、読本など精力的に活動しています。 サンフランシスコで開催されたKotlinConf 2017は1200人もの参加があり、世界中の開発者からのKotlinへの期待の高さが伺えます。 プレゼンでは以下のようにAndroidばかりではなく、サーバー用途でKotlinを使ってもらおうとしているようです。 Server 30% Android 24% Native 18% 次回のKotlinConf 2018は2018/10にアムステルダムで開催。 3days、61speakerになり、規模が一気に拡大しています。 (僕も行きたいと思ってたけどSold Outでした) Ktor game graphQL GCP など、Kotlinを色んな所で色んな使い方をする発表がたくさんあるようです(行きたかった)。 話は戻り、KotlinFestは多くの協賛企業に支えられており、積極的にKotlinを採用していっているようです。スポンサーは募集してから即日で埋まったみたいです。 企業名 導入内容 BIZREACH Server CA Android LINE Android, Server, Clova M3 Android, Server mercari Android mixi Android sansan Android YAHOO Android, Server dmm ほか 藤原さんからはKotlinの現状のまとめのお話でした。 Kotlinの特徴は以下の通り。 マルチプラットフォーム 型言語 関数型、オブジェクト指向 OSS そして、Kotlinには哲学があるとのこと。 実用主義 Javaの考え方のまま。学習が容易。 完結 読みやすい。ボイラープレートは少ない 安全 静的型付け、NULL安全、スマートキャスト 相互運用性 Javaと仲良し 現在の日本語本は代表的なところで以下があり、入門と中級をカバーしています。 Kotlin in Action 赤べこ、黒べこ みんなでKotlinを愛でましょう ということで、KotlinFestはスタートしました。 お昼ごはん オープニングセッションが終わったらいきなりランチタイムです。KontributorであるshirajiさんがTwitterで ぼっち飯を回避する ランチの相手を募集していたので、ご一緒させていただきました。 明日Kotlin Festでランチ誰か一緒に行かないですか?サーバサイドKotlinの話したい方、Kontributeしたい方、弊社興味ある方ぜひ!!!自分含めて四人くらいで! (自腹でお願いします。。。) — shiraji (@shiraj_i) 2018年8月24日 美味しいお肉を皆で食べました。美味しかったです。 Kotlinで改善するAndroidアプリの品質 Android界の著名人であるあんざいゆきさんに拠るアプリ品質のお話です。 JavaのソースをKotlinに移行するとして、移行の難しさ、書き直しコスト、リグレッションなどの問題が出てくるけど、「Kotlin化にはそれを上回る効果あるのか?」を考える内容です。 まず、 品質 について、[オブジェクト指向入門~原則・コンセプト~]( https://www.amazon.co.jp/dp/4798111112 ) に「品質とは何か」がまとめられているので、この発表ではこれを基準とされていました。 外的品質要因 ユーザーが認識 スピード、つかいやすさ 要因 正確さ 頑丈さ 拡張性 再利用性 内的品質要因 それ以外 モジュール性、読みやすさ JavaからKotlinにソースを書き直しても、アプリの使いやすさは変わりません。しかし、内的品質要因は外的品質要因に影響するので、アプリを使いやすくするときに内的品質を向上していると良い影響が出ると説明されています。 Javaの実装パターンをまとめた名著である Effective Java とKotlinの実装をいくつか説明していくことで、Kotlin化の正当性を説明されています。 builder pattern コンストラクタの引数を増やすのではなく、Builder Patternを使うようにするのがJavaでのテクニックでした。 ※このサンプルソースは僕が考えたものです。 コンストラクタの場合 第1引数と第2引数が同じStringのため、2つを間違えてしまう可能性があります。これがbuilder patternを使うと分かりやすくなります。 User user = new User( "shikajiro" , "fukuoka-city" , Gender.MAN); builder patternの場合。 User user = User.builder() .name( "shikajiro" ) .gender(Gender.MAN) .address( "fukuoka-city" ) .build(); nameと"shikajiro"が対になるので分かりやすいですね。 builder patternの欠点は、このための実装をUserクラスにする手間がかかることです。 Kotlinだとこのbuilder patternは名前付き引数でできます。 val user = User(name= "shikajiro" , gender=Gender.MAN, address= "fukuoka-city" ) ただし、Kotlinの名前付き引数は1つの引数に複数個指定とかできないので、場合によってはbuilderの方が良い時もあるとのことでした。 など、他に数パターン、Effective Javaに書いてある問題がKotlinでは解消される事の説明がありました。 これはつまり、Kotlin化を進めることでコードの内的品質が向上し、結果的にはユーザーの使いやすさに繋がるということです。 最後に 「明瞭で、正しく、再利用可能で、頑丈で、柔軟性があり、保守可能なプログラムが書ける」 と締めくくられていました。 How to Test Server-side Kotlin エムスリー株式会社の鈴木さん、前原さんのお二方に拠るテスト実装の実体験です。 Androidアプリはテスト実装の経験があるのですが、サーバーはあまり触ってなかったので聞かせていただきました。 基本的に標準である、JUnit4, Mockito, AssertJを選択したとのことです。テストライブラリはたくさん出てきているそうですが、まずは標準であることの信頼を選んだようです(変なところハマると辛いですもんね…)。 選択するライブラリが沢山あるので、開発チームに合うものを選定するのが大事になりそうです。 歴史的な経緯 によりDBが肥大化しているため、実装と切り離す必要があり、 いきなり全てをテストするのは困難なので、まずはアプリケーション層を主にテストしたそうです。 コンストラクタが肥大化したクラスのデータ作成が大変辛くなったので、IDEからテストコードを自動生成するものを作ったそうです。 Kotlinでもテストちゃんと書ける! ということで、サーバー実装をする際は積極的に使っていきたいです。 Kotlin linter DMM釘宮さんによるLinter開発のお話です。 KotlinにはいくつかLinterがあるので、技術選定の材料にしてほしいので壇上に上がってくださったようです。 Linterは現在有名なものが3種類。 ktlint detekt android-lint ktlint linterをカスタマイズするには AST を知る必要がある。 ASTはツリー構造。PsiViewerを使う事でASTをビューできるので、これを使いながら進めると良いとのこと。 (なんとなくAPI responseのパーサーを実装したときを思い出しました) カスタムルールのformaterも作れるけど、実装する必要がある(lintルールとformatterが乖離するのはとても辛そう) detekt ktlintより高機能だが、フォーマット機能は途中からなくなったようです。カスタムルールでは作れるらしいです。 Kotlinコルーチンを理解しよう 株式会社Lang-8八木さんによるコルーチンの解説です。 コルーチンの歴史を紐解くと、1963年にメルヴィンコンウェイがcobolコンパイラでコルーチンの概念を出したそうです。 コルーチンとは 一時停止可能な計算インスタンス であり、スレッドに似ているけど、スレッドに束縛されないのが特徴です。継続状況を持つプログラムが容易に記述できます。Future Promiseみたいに値を返すことも可能です。 プレゼンでは、コルーチンがどのように実現しているかをKotlinから生成されたバイトコードを紐解いて探求していきます。 そこから、コルーチンはステートマシンで表現されていることがわかります。 詳しい説明はプレゼン資料を見ていただくとして、ここではサンプルソースを以下にまとめます。 var rootJob: Job = Job() val job = launch(UI, parent = rootJob) { repeat( 3 ) { try { //直列 val hoge = async {} async { hoge.await() }.await() //並列 async {}.await() async {}.await() return @launch } catch (e: CancellationException ) { } catch (e: Exception ) { if (souldRetry(e)) { return @repeat } } } } job.cancel() rootJob.cancel() 他にも以下のライブラリがコルーチンに対応しているので、Androidでも使わない理由はないようです。 Retrofit EventBus, Channel gistに転がっているらしい android-coroutines onActivityResultをsuspend How to Kontribute v4 JP ランチをご一緒したUBieの磯貝さんによる、KotlinプロジェクトへのContributeの仕方の解説です。 磯貝さんはOSSであるKotlinにコントリビュートした日本人として有名になりました。 photos.google.com KotlinはKotlinで書かれているため、Kotlinを愛している方にはもってこいです。Kotlin Pluginコントリビュータが多いので、決して難しいものばかりではないようです。 Kontribute手順は発表資料に委ねたいと思います。 僕自身、磯貝さんのブログに触発され、DroidKaigi2018のAndroidアプリへのContributeを決意しました。結構エグいIssueを辛うじてfixさせることができ、大きな自信へと繋がりました。 この記事を見ているみなさんも、もしContributeに興味がありましたら磯貝さんの記事をご一読ください! クロージング Kotlin Fest 2019は現時点では未定とのことです。もしやるなら次回はBigゲストを招待したいとのことでした。 懇親会 さぁ、ついに本番が始まりました。懇親会にも多くの参加者、発表者の方が参加し、Kotlin愛を語り合っていました。 スタッフの皆さん、本当にお疲れ様でした。 こうして、しかじろうのKotlinFestは幕を下ろしました(この後Android老人会の皆さんと二次会に行きました)。 引用 Top画像は https://kotlin.connpass.com/event/91666/ より引用させていただきました さいごに スタートトゥデイテクノロジーズではKotlinを使ってアプリ・サーバーを開発するエンジニアをこれでもかというほど募集しております。 www.wantedly.com www.wantedly.com www.wantedly.com 福岡研究所でも機械学習などを使ってファッションを科学する研究者を募集しております。 www.wantedly.com
こんにちは、新事業創造部の遠藤です。現在WEARの開発を行っています。 最近はWEARのコーディネート一覧やユーザー一覧など、リスト画面にバナー型の広告を実装をしました。 リストにデータを挿入する実装は簡単なように思えますが、種類の違うデータを扱う場合には、考慮するべきポイントがいくつかあります。 本記事ではリストに広告を表示することを例に、種類の違うデータをリストに挿入する際のデータの持ち方・実装ついて紹介したいと思います。 仕様 リスト画面にバナー型の広告を表示するにあたっての仕様は以下のとおりです。 広告を取得できない場合はリストをつめる スクロールして戻っても同じ広告が表示されている 広告のインプレッションは表示時のみとなるように制御する スクロールに合わせて遅延なく広告の表示をする 上記のような元々表示していたデータと異なる仕様を扱うため、実装が複雑になります。 バナー型広告について リストにデータを挿入する実装を紹介する前に、表示しているバナー型広告について軽く触れたいと思います。 今回はGoogle Mobile Ads SDKが提供しているバナー型広告(DFP)をリストに表示しています。 このバナー型広告は広告取得リクエストをするとdelegateメソッドを通じて表示するバナーのオブジェクトが取得できます。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { let banner : DFPBannerView ! override func viewDidLoad () { super .viewDidLoad() // バナー広告のインスタンスを生成してリクエスト banner = DFPBannerView() banner.adUnitID = "xxxxxxxxxxxxxxxxx" banner.delegate = self banner.request(DFPRequest()) } func adViewDidReceiveAd (_ bannerView : GADBannerView ) { // 広告取得成功のdelegate } func adView (_ bannerView : GADBannerView , didFailToReceiveAdWithError error : GADRequestError ) { // 広告取得失敗のdelegate } } また、広告が表示されなくても広告取得が成功したタイミングでインプレッションの計測がされるようになっています。 インプレッションを手動でカウントする仕組みはあるのですが、第三者ネットワーク広告には使用できないです。 実装について データの持ち方について リストに種類の違うデータを挿入する際に肝となるのはデータの持ち方だと思います。 データ保持の方法はいろいろあると思いますが、今回は以下の考慮するポイントをもとにデータの持ち方について検討しました。 考慮するポイント   1. 広告が取得できない場合の対応 2. cellの再利用 3. インプレッション計測を正しく行う 1. 広告が取得できない場合の対応 広告を取得できなかった場合にリストをつめる簡単な方法は、cellの高さを0にすることだと思います。 しかし、UICollectionViewで minimumLineSpacing を使用して実装している場合は気をつけないといけません。 高さを0にすることでリストがつまったように見えますが、cell自体は残っています。 高さを0にしたcellの minimumLineSpacing と前のcellの minimumLineSpacing が合わさりコンテンツ間のスペースが広く見えてしまいます。 なので、広告が取得できなかった場合には高さを0にする方法ではなくデータでの制御が必要になります。 2. cellの再利用 UITableViewやUICollectionViewはcellが再利用されます。 cellを表示するタイミングで広告のリクエストを行うと、スクロールするたびに違う広告が表示されてしまいます。 この問題を解決するために、バナーのViewを保持する必要があります。 3. インプレッション計測を正しく行う スクロールに合わせて遅延なく広告を表示するために、広告の先読み処理を行うと思います。 しかし今回導入したバナー型広告は、広告を取得したタイミングでインプレッション計測が行われます。 たくさんの広告を先読みしてしまうと、広告を表示していないのに、インプレッション数が増えてしまいます。 なので、表示する直前に1件ずつ広告取得のリクエストを行う必要があります。 上記の考慮するポイントをから、以下の3種類のリストデータを持つことにしました。 リスト画面で表示するコンテンツのみのリストデータ コンテンツ、AD、ADを入れる位置を確保するためのスタブのリストデータ ADの位置を確保するためのデータです。 コンテンツと取得できた広告だけのリストデータ 表示に使用するためのデータです。 UICollectionViewのdataSource、delegateで使用します。 ADを入れるための位置を確保したデータと表示に使用するデータを分けることにしました。 これにより実際に取得できた広告のみデータとして扱われることになるので、広告の取得が失敗したときにcellの高さを0にするという対応をしなくてよくなります。 また、広告の表示位置をスタブを使用して確保するようにしました。 そうすることで広告を挿入するたびに、挿入する位置の計算をしなくてよくなります。 リストのデータについてですが、スクロールして戻っても同じ広告を表示する仕様を実現するために、表示するバナーのViewをデータとして保持することにしました。 実装 データの持ち方が決まったので、リストに種類の違うデータを挿入する実装について大まかに説明していきます。 今回はわかりやすいように、リストに3件ずつ広告を入れる仕様で説明したいと思います。 1. コンテンツのリストデータに広告のスタブを挿入する リスト画面に表示するコンテンツが取得できたら、広告のスタブを挿入していきます。 複数の型を扱うためにenumの配列で実装しています。 class ViewController : UICollectionViewController { enum CellType { case contents(Contents) case ad(DFPBannerView) case adStub } private let adInterval : Int = 3 // 3件ずつ広告を表示する private var contentsList : [Contents] = [] // コンテンツのみのリスト private var dataList : [CellType] = [] // ADスタブが入ったリスト // APIリクエスト完了後に行う処理 // contentsListにはAPIから取得できたデータが追加されている private func insertAdStub () { // データを3件ずつに分割する let chunks = stride(from : 0 , to : contentsList.count , by : adInterval ).map { contentsList[ $0 ..< Swift.min( $0 + adInterval, contentsList.count)].map { CellType.contents( $0 ) } } // 分割したデータにAdStubを挿入していく dataList = chunks.map { ( $0 .count == splitSize) ? $0 + [CellType.adStub] : $0 }.flatMap { $0 } } } 2. 広告の挿入 広告の取得リクエストを行い、実際に表示するバナーのViewが取得できたら、AdStubと置き換えていきます。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { private var ads : [DFPBannerView] = [] private var dataList : [CellType] = [] private var ad : DFPBannerView ? private var latestAdIndex : Int = 0 override func viewDidLoad () { super .viewDidLoad() requestAd() } fileprivate func requestAd () { let bannerView = DFPBannerView() bannerView.adUnitID = "" bannerView.rootViewController = self bannerView.delegate = self bannerView.load(DFPRequest()) ad = bannerView } private func insertAd (bannerView : DFPBannerView ) { for index in latestAdIndex ..< dataList.count { if case .adStub = dataSorce[index] { dataList[index] = .ad(adView) latestAdIndex = index return } } } // MARK: - GADBannerViewDelegate func adViewDidReceiveAd (_ bannerView : GADBannerView ) { guard let bannerView = bannerView as ? DFPBannerView else { return } ads.removeFirst() insertAd(adView : adView ) } } 3. 表示 実際に表示するデータはAdStubを抜いたデータを使用します。 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { private func displayData () -> [CellType] { return dataList.filter { if case .adStub = $0 { return false } else { return true } } } // MARK: - UICollectionViewDataSource override func collectionView (_ collectionView : UICollectionView , numberOfItemsInSection section : Int ) -> Int { return displayData().count } override func collectionView (_ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell { if let contents = displayData()[indexPath : indexPath ] { ・・・ } else if case .ad = displayData()[indexPath.item] { ・・・ } else { fatalError( "Unexpected Display Data" ) } } } 4. 広告は1件ずつリクエストを行う インプレッション計測を正しく行うため、広告を表示する直前に広告取得のリクエストをするようにしました。 しかし表示する直前にリクエストを行うと、広告を表示したいタイミングに広告を取得できないことがあります。 広告が取得できていないと、スクロールして広告を表示する位置にきても広告を表示できません。 なので遅延なく広告を表示するために、直前に広告取得を行うのではなく、少し早いタイミングで広告の取得を行うようにしました。 今回は以下のタイミングで広告の取得リクエストを行っています。 初回表示(viewDidLoad) 広告が表示 広告の取得失敗 import GoogleMobileAds class ViewController : UICollectionViewController , GADBannerViewDelegate { override func viewDidLoad () { super .viewDidLoad() requestAd() } fileprivate func requestAd () { let bannerView = DFPBannerView() bannerView.adUnitID = "" bannerView.rootViewController = self bannerView.delegate = self bannerView.load(DFPRequest()) ad = bannerView } // MARK: - UICollectionViewDataSource override func collectionView (_ collectionView : UICollectionView , cellForItemAt indexPath : IndexPath ) -> UICollectionViewCell { if let contents = displayData()[indexPath : indexPath ] { ・・・ } else if case .ad = displayData()[indexPath.item] { requestAd() } else { fatalError( "Unexpected Display Data" ) } } // MARK: - GADBannerViewDelegate func adView (_ bannerView : GADBannerView , didFailToReceiveAdWithError error : GADRequestError ) { requestAd() } } 以上が今回バナー型広告をリストに挿入する実装についての説明です。 まとめ リスト画面に種類の違うデータを挿入する実装についての紹介でした。 広告など種類の違うデータをリスト画面に挿入して表示する際の参考になれば幸いです。 スタートトゥデイテクノロジーズではiOSエンジニアを募集しています。少しでも興味がある方は、ぜひ一度オフィスにお越しください。 下記からのエントリーもお待ちしています。 www.wantedly.com
こんにちは。スタートトゥデイ研究所の真木です。 8月5日から8月8日にかけて開催されたMIRU 2018という学会に行ってきました。また、5月下旬から約2か月間にわたって実施されてきた「MIRU若手プログラム」という 若手研究者同士 の交流プログラムにも参加してきたので、今回はその報告をします。 MIRUとは MIRUは正式名称を「画像の認識・理解シンポジウム」といい、21回目となる今年は札幌で開催されました。画像に関する研究の基礎から応用までがスコープに含まれ、この分野の学会としては 国内最大規模 です。今年は 700人 以上の参加があったようです。 通常のポスター発表とオーラル発表に加え、画像認識のトップカンファレンスであるCVPRで採択された論文の招待講演、4件のチュートリアル講演、海外のトップ研究者を招聘した特別講演、異分野の研究者が集って将来展望について議論する特別企画など盛りだくさんの内容でした。 どの発表も素晴らしかったのですが、特に高橋さん(オムロン)によるGANのチュートリアルは、数式による説明と図・動画による説明のバランスが絶妙で大変素晴らしく、勉強になりました。 チュートリアル講演の一部は以下で発表資料が公開されています。 画像認識分野における深層学習〜CNN, RNNからマルチタスク学習まで〜,山下隆義(中部大学) MIRU MIRUわかるGAN,高橋智洋(オムロン株式) スタートトゥデイと画像認識 スタートトゥデイ研究所では、ファッションに関わる基礎・応用研究を行なっており、研究の道具として画像認識をよく利用しています。例えば、商品の画像からカテゴリを自動的に認識したり、アイテム画像の組み合わせからオシャレに見えるコーディネートを推薦する研究などを行なっています。 そこで画像認識の知見をさらに深め、この分野の研究者と交流するため、スタートトゥデイ研究所はMIRU2018にゴールドスポンサーとして協賛し、企業ブースで研究を紹介しました。 大変うれしいことに、多くの方々が私たちの研究に興味を持ってくださいました。ポスターまで足を運んでくださった皆さま、ありがとうございました。 こちらは期間中展示していたポスターです。 若手プログラム MIRU若手プログラムは画像認識に興味を持つ若手研究者の交流企画で、学生を中心に大学の研究者、企業の研究者など40名以上の若手研究者が集まりました。企画内容は、「異分野サーベイ」や「若手の未来を議論する」などの真面目な企画だけでなく、自己紹介LTや写真の「インスタ映え」を競うエンタメ系の企画もあり、本会議と並んでこちらも盛りだくさんでした。 メインの企画である異分野サーベイでは、参加者が画像認識以外の7つの分野から1つを選び、その分野のサーベイを行います。これは、異分野で使われている手法から着想を得て画像認識に持ち込んだり、逆に画像認識で使われている手法をその他の分野に持ち込むことでブレークスルーを起こすことを意図したものです。サーベイ分野は(1)ロボティクス(2)自然言語処理(3)HCI(4)心理学(5)データマイニング(6)音声(7)生体の7つで、わたしはデータマイニングのグループに参加しました。 各グループには若手P実行員が1人ずつオブザーバとして参加し、議論をファシリテートしてくれました。 サーベイを始めるに際し、まず5月中旬に実行委員の米谷さん(東大)と片岡さん(産総研)からそれぞれサーベイ論文の書き方やグループサーベイの方法について以下の資料が提供されました。これを読むだけでもかなり勉強になるのでおすすめです。特に、サーベイは単に新しい論文をまとめるのではなく、分野に対する 新しい視点 を読者に与えることが重要であるという米谷さんの指摘には蒙を啓かれました。 サーベイ論文の書き方,米谷竜(東京大学) https://www.dropbox.com/s/vvqxs698en01uf2/PRMU180518_yonetani_small.pdf グループサーベイ方,片岡裕雄(産業総合研究所) http://hirokatsukataoka.net/temp/presen/180518PRMU_Kataoka.pdf これを受け、各グループの参加者たちはそれぞれ遠隔地にいながらもSlack、Git、Skypeなどのツールを駆使して、約2か月半に渡ってサーベイに取り組んできました。わたしにとっては、自分の研究分野以外の分野をサーベイすること、チームで協力しながらサーベイをすること、その両方が初めての体験でした。研究者として成長していく上で、とても貴重な機会だったと思います。 サーベイの成果はMIRU期間中にポスターでMIRU来場者に向けて発表し、さらに本会議翌日に口頭発表を行いました。他のグループの発表を聞くことで多様な分野の最新動向を把握することができ、とても勉強になりました。全チームの発表資料が下記のURLにて公開されているので、ぜひご覧ください。 MIRU若手プログラム 異分野サーベイ 発表資料 https://sites.google.com/view/miru2018sapporo/wakate_top/各チームの発表資料 若手プログラム通じてたくさんの学生や他企業の方々と仲を深めることができ、またデータマイニングに関する知見を深めることができ、非常に有意義でした。若手P参加者のみなさん、実行委員の皆さん、本当にありがとうございました! 最後に スタートトゥデイ研究所はまだできたばかりですが、ファッションという未開拓な分野を科学的な方法論で探求し、その成果を社会に還元するべく日々研究を行なっています。研究成果は、原則として会社内に留めることなく論文として公開していきます。 私たちと一緒にファッションの研究に挑戦しませんか?スタートトゥデイ研究所では豊富なデータ資産と潤沢な予算のもと研究を行うことができ、国内・国外を問わず業務として学会に参加できます。もちろん、論文を書くだけでなく研究成果を実際のサービスへ実装することでたくさんのユーザーに貢献することもできます。また、MIRU若手Pのような機会への参加を応援してくれる自由な社風があります。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com また、私たちの持つデータ資産を活用した共同研究の募集も行なっております。画像認識・機械学習はもちろん、社会科学や認知科学などファッションに関わる幅広い分野で研究を行いたいと考えていますので、ご興味のある方はぜひ以下のリンクからご連絡ください! https://www.starttoday-tech.com/contact/ www.starttoday-tech.com
こんにちは。新事業創造部インフラチームの内山(@k4ri474)です。 弊社が運営する IQON というサービスでは、長らくMySQLのバージョン5.6.27を利用していました。これは2018年9月にEOLを迎えるため、RDSの方針として強制アップグレードがアナウンスされています。 MySQLを継続する選択肢もありましたが、Auroraの運用知見が溜まっていたということもあり、これをキッカケにMySQLからのAurora移行を実施しました。 新事業創造部ではほぼ全てのAWSリソースをCloudFormationを使って宣言しているため、例に漏れずAuroraもテンプレートへ落とし込むことにしました。 ただ、CloudFormationだけで完結しない作業があったので、今回はCloudFormationでの宣言とコンソールからの手作業を織り交ぜるという対応を取っています。 執筆時点の公式ドキュメントではカバーできていない需要を満たすべく、 CloudFormationを使ったAmazon Auroraへの移行 の手順を皆さんに共有したいと思います。 利用したサービス AWS CloudFormation AWS CloudFormation はAWSのほぼ全てのリソースをテキストベースで管理し構築できるサービスです。 1 構築にはYAMLもしくはJSON形式で記述された テンプレート というテキストファイルを利用します。設計のタイミングで、どのリソースをどういったプロパティで作成するかをコントロールできます。 CloudFormationのテンプレートを使い回すことで、数クリックでサービス環境一式を複製できます。 また、ネットワークの詳細をテキストで表現するため、設計のノウハウをチーム内で共有できたり、他チームに提供する資料としてそのまま使えるというメリットもありますね。 Amazon Aurora Amazon Aurora はAWSの Amazon RDS というサービスで利用できる、MySQL・PostgreSQLと互換性のあるリレーショナルデータベースです。 特徴を何点か抜粋してみました。 パッチの自動適用 最大16TBまでオートスケールされるストレージ リードレプリカによる読み取りスループットの容易なスケール 読み込みエンドポイントによるリードレプリカの抽象化 僕のおすすめポイントは、読み込みエンドポイントがRDS版のロードバランサ感覚で使え、裏側のインスタンスの台数をクライアント側で管理する必要がなくなったことです。 2 Auroraへの移行 Auroraへの移行は、大雑把に言うとMySQLをAuroraで複製し、アプリケーションの接続DBをAuroraへ変更することで実現できます。 複製を作る過程でAuroraクラスタの呼称が変わるのでここで補足しておきます。 MySQLから非同期的にデータをコピー(レプリケーション)してきている段階のAuroraクラスタをAuroraリードレプリカといい、レプリケーションを止めて独立した状態のAuroraクラスタをスタンドアロンAuroraクラスタと呼びます。 今回はスタンドアロンAuroraクラスタをどうやってCloudFormationで作成するかにフォーカスして説明を行います。 AWS公式ドキュメント で示されている手法で移行を行うと、以下のような手順になります。 可能な限りCloudFormationで宣言したい部署の方針に沿って、この手順をテンプレートで最大限表現しようと思います。 CloudFormationでは次の作業ができることを期待し、検証を開始しました。 Auroraリードレプリカの作成とスタンドアロンAuroraクラスタへの昇格がCloudFormationのテンプレートで表現できれば、手作業を介さずに移行できます。 ただ、現実はこうでした。 結論として、リードレプリカの昇格はCloudFormationで実装できませんでした。 CloudFormationでは、Auroraクラスタ作成において Auroraリードレプリカの作成 か クラスタのスナップショットからクラスタを復元 することしかできません。 そのため、Auroraリードレプリカを昇格する作業は手作業となってしまいます。 ただ、普通に手作業で実施すると テンプレートの記述と実際のリソースに相違が発生 してしまいます。 これを解消するため、以下の手順で移行を実施しました。 Auroraリードレプリカの作成をテンプレートで宣言 コンソールにてAuroraリードレプリカを昇格 コンソールにてスタンドアロンAuroraクラスタのスナップショットを取得 手順1のテンプレートを再利用して手順3のスナップショットから新しいスタンドアロンAuroraクラスタを作成 この手順を踏むと、テンプレートの記述と実際のリソースが一致します。 実際に移行を行うにあたって、まずはスケジュールを説明します。 スケジュール 今回の作業ではメンテナンス前に実施する作業と、メンテナンス中に実施する作業が存在します。 メンテナンス前:Auroraリードレプリカの作成(手順1) メンテナンス中:Auroraリードレプリカの昇格以降、全ての手順(手順2・3・4) Auroraリードレプリカの昇格を実施するとレプリケーションが切れ、MySQL Masterとの差分が発生します。そのため、トランザクションが無くなってから手順2以降を実施する必要があります。 それでは、本題の各手順の内容を説明していきます。 1. Auroraリードレプリカの作成 RDS for MySQLからの移行に当たって、まずはMySQLからレプリケーションを受けるAuroraリードレプリカを作成する必要があります。 この作業はCloudFormationで宣言することが可能で、テンプレートで表すと以下のようになります。 # Auroraクラスタを作成 MyRDSDBCluster : Type : "AWS::RDS::DBCluster" Properties : # (各種必須プロパティを省略) Engine : 'aurora' EngineVersion : '5.6.10a' ReplicationSourceIdentifier : 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' # Auroraインスタンス1台目を作成 MyRDSDBInstanceApplicationFirst : Type : "AWS::RDS::DBInstance" Properties : # (各種必須プロパティを省略) DBClusterIdentifier : !Ref MyRDSDBCluster Engine : 'aurora' # Auroraインスタンス2台目を作成 MyRDSDBInstanceApplicationSecond : Type : "AWS::RDS::DBInstance" Properties : # (各種必須プロパティを省略) DBClusterIdentifier : !Ref MyRDSDBCluster Engine : 'aurora' ポイントは以下の2点です。 クラスタ側でReplicationSourceIdentifierというプロパティを記述し、レプリケーション元となるMySQLインスタンスのARNを指定する インスタンス側ではDBClusterIdentifierというプロパティを記述し、所属するクラスタを指定する これでMySQL Masterからレプリケーションを受けているAuroraクラスタができます。 2. Auroraリードレプリカの昇格 次に実施するのはAuroraリードレプリカをスタンドアロンのAuroraクラスタに昇格させる作業です。 この手順をなんとかCloudFormationで実装しようとしてテンプレートをぽちぽち弄ってみたのですが、実現出来ませんでした。 3 よって昇格は Aurora リードレプリカの昇格(AWS マネジメントコンソール) に記載された手順で実施しました。 先述の通り、この時点でテンプレートではAuroraリードレプリカ、実際はスタンドアロンAuroraクラスタが構築されており、差異があります。 これを解消するため、3・4の手順を踏んでテンプレートを実態に即したものへ修正します。 3. スナップショットの取得 最終的な スナップショットからの復元 の前準備として必要なのがこの手順3です。 この作業もテンプレートでは表現できないため、 DB スナップショットの作成(AWS マネジメントコンソール) に記述された手順で実施しました。 4. スナップショットからのクラスタ復元 最後に、テンプレートを使って手順3で取得したスナップショットからAuroraクラスタとAuroraインスタンスを復元します。 この手順は、手順1で作成したテンプレートを1行変更するだけで実現できます。 テンプレートに加える修正を以下にdiffで示します。 < ReplicationSourceIdentifier: 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' --- > # ReplicationSourceIdentifier: 'arn:aws:rds:<AZ>:<AccountId>:db:<SourceInstanceName>' > SnapshotIdentifier: arn:aws:rds:<AZ>:<AccountId>:cluster-snapshot:<SnapShotName> ReplicationSourceIdentifierの代わりにSnapshotIdentifierが有効になり、スナップショットを元にクラスタを作成するという宣言に代わります。 このテンプレートを反映すると、レプリカとして宣言していたAuroraクラスタとインスタンスが削除され、スナップショットから復元されるクラスタ・インスタンスで置換されます。 古いクラスタ・インスタンスを削除する手間が省けました。 DBClusterIdentifierで指定したAuroraクラスタが別のものになることで、インスタンスも再構築されています。 以上でCloudFormationを使って、 MySQLのAuroraレプリカが昇格したスタンドアロンAuroraクラスタ を構築できました。 後はアプリケーションの接続DBをこのスタンドアロンAuroraクラスタへ変更することで、無事移行が完了となりますね。 まとめ 今回の4つの手順を経て作成されたテンプレートは、サービスインした実際のAWSリソースを正しく表現したものとなりました。 メンテナンス中にAuroraクラスタを置換する必要があり、しかも手作業も発生するという困った状況でしたが、今回の方針で進める大きな決め手となったのは 事前に検証とリハーサルを繰り返して作業時間を正確に見積もれた ことです。本番での不確定要素を最小限に出来たことで踏み切れました。 テンプレートで表現できたため、今後はメンテナンスウィンドウやセキュリティグループなどの変更をコードベースでレビューを挟んで実施できますね。 さいごに スタートトゥデイテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.starttoday-tech.com/recruit/ サービスに追加された新機能がCloudFormationで実装されるまでにはそれなりにラグがありますが、そことはうまく付き合っていきます。 ↩ コネクションを受けているリードレプリカをクラスターからいきなり抜くとRDSはよしなに再接続処理をやってくれるんだろうか?という疑問はあります。スケールアウトは気軽にできますが・・ ↩ AuroraクラスタのReplicationSourceIdentifierを外してレプリケーション設定を削除することが有効だと思ったのですが、クラスタが削除されて新規にまっさらなクラスタが作成されるだけでした。 ↩
こんにちは! スタートトゥデイテクノロジーズ新事業創造部の塩崎です。 2018年7月24日〜26日にかけてサンフランシスコでGoogle Cloud Next '18が開催されました。 このイベントに新事業創造部の塩崎、今村、そして代表取締役CIOの金山の3名で参加してきました。 この記事では多数あった講演の中で特に印象に残ったものをいくつか紹介いたします。 講演 Building A Petabyte Scale Warehouse in BigQuery How to Do Predictive Analytics in BigQuery A Modern Data Pipeline in Action Real-Time Stream Analytics with Google Cloud Dataflow: Common Use Cases and Patterns Securing Access to GCP resources Better Practices for Cloud IAM 感想 まとめ 講演 Building A Petabyte Scale Warehouse in BigQuery DataWareHouse(DWH)のBigQuery移行をするために行ったことを各社さんが発表するセッションでした。 特にSpotifyさんの発表では移行のための具体的なtipsを4つ紹介されていました。 その4つの内訳は以下のものとのことです。 Administration オンデマンド料金ではなく、定額料金を使う ビジネス的に重要なジョブ用にスロットを予約する Education ジョブのパフォーマンス監視のために、ジョブの並列度を監視 Dremelのアーキテクチャ、特に伝統的なRDBとの違い列指向であることの理解 Integration BigQueryのAPIを使用し、自社ツールを開発 GCPサービス間でのデータ移動が簡単なので、他のGCPサービスと統合 Partnership BigQueryのサポートチームに機能のリクエストを出す また、Oathさんは各部署で独立してDWHを持っている(データサイロ)ために、部署横断的なデータ活用が難しいという課題があったそうです。 BigQuery移行のために、当時使用されていたHiveのデータをBigQueryにコピーし、BIツールであるLookerに繋ぎこむことから始めたそうです。 現在ではGCP製品をフル活用したデータ分析基盤になっているそうです。 TwitterさんはMySQL、Vertica、HDFS等に保存されたデータをBigQueryにコピーする際、一旦Apache Avro形式に変換をしているそうでした。 Avroに変換してからBigQueryにインポートすることによって高速化されるようです。 海外のBigQuery導入事例を聞くとデータ量が本当に桁違いで驚愕することが多かったです。 発表のタイトルにもあるPB(ペタバイト)という単語が発表中に頻繁に飛び交っていました。 BigQueryは大量のデータに対してもスケールするという宣伝文句はよく目にしますが、具体的な数値を見るとBigQueryにDWHを構築する時の安心感が増します。 How to Do Predictive Analytics in BigQuery BigQueryの新機能であるBigQuery MLに関する発表でした。 BigQueryに格納されているデータに対して機械学習でモデルを構築できるそうです。 データ分析を行う人にとっていつも使い慣れているSQLに似た構文で機械学習を行うことができるようになります。 お手軽に試してみたい人向けの機能としては、ハイパーパラメーターの自動決定やデータを学習用とテスト用に自動的に分けてくれる機能があるそうです。 また、アドバンストな機能として、学習率の設定やデータを学習用とテスト用に分ける時の方法の設定を自分で変更できる機能もあるそうです。 現在は線形回帰と二項ロジスティック回帰のモデルがサポートされているそうですが、使用できるモデルはこれから拡充されるようです。 対象としているユーザー層はAutoMLやCloudML APIを使う層とTensorFlowやCloudML Engineを使う層の中間層らしいです。 発表の後半ではHearst Newspapersさんでの導入事例の紹介がありました。 オンラインで提供している新聞購読サービスの解約予測モデルの構築をBigQuery MLを使って行ったそうです。 利用ログやデモグラ情報などから、そのユーザーが解約するかどうかを二項ロジスティック回帰で予測するモデルだそうです。 このモデルを構築することによって、どの属性がユーザーの解約に寄与するのかを可視化することが出来たそうです。 プロトタイプの構築はわずか1日で行うことができ、本番投入もわずか2スプリントで行うことができたそうです。 BigQueryにすべてのログやマスタデータが集約されている環境では非常にパワフルな機能であると感じました。 大量のデータに対する機械学習をシンプルなSQL記法で行うことができるため、アナリストが自分で予測モデルをつくることの敷居が下がるかと思います。 まだモデルの種類は2種類しかありませんが、将来的に増えていくことを強く期待しています。 A Modern Data Pipeline in Action モダンなデータパイプラインを構築するためのパターンの紹介でした。 なお、データパイプラインとはデータに対して以下の一連の処理を行うためのシステムのことです。 Ingest → Transform → Store → Analyze → Visualize 発表中では以下のパターンが紹介されていました。 一度取得したデータに対して、再度処理をかける時のパターン プログラムのバグやビジネスロジックの変更に備えるためのパターンです。 PubSubからのデータをDataFlowでBigQueryに入れるための流れとは別に、GCSにAvro形式でデータを保存する流れを作ります。 もしもの時にはこのデータをDataFlowで再度処理してBigQueryに入れ直すことができます。 データのバックフィルをする時のパターン CloudComposer(マネージドAirflow)を使い過去分のデータをBigQueryに入れるのがベストプラクティスらしいです。 また、データパイプラインのアーキテクチャを考える上で重要なことも紹介されていました。 Decoupling DBやログストリームなどのデータを生成する部分をデータを消費する部分をPubSubを使って分離させる。 Simple, Elastic, low-cost マネージドサービスの活用によって、スケーリングとロードバランスに関する問題を考えなくても良いようにする。 Low Latency ETL処理の負荷を可能な限り小さくして、低レイテンシでBigQueryにデータを入れる。 Stream and Batch DataFlowを使うとストリーム処理、バッチ処理の両方を同一のソースコードで処理可能になる。 Easy re-processing DataFlowによる変換処理前のデータをGCSに保存することによって、容易に再処理をできるようにする。 High-Level DataFlowならば簡単な記述で大きな処理を行わせることができる。 Portability DataFlowはApache Beamでプログラミングできるため、ロックインされない。 バッチ処理とストリーム処理を同じソースコードで実現できることにDataFlowの実力を感じました。 現在DigdagとEmbulkを活用してETLを構築していますが、CloudComposerやDataFlowを使用したパターンとの比較も真面目に検討する必要性を感じました。 Real-Time Stream Analytics with Google Cloud Dataflow: Common Use Cases and Patterns ストリーミングデータをリアルタイム分析をするためのパターンの紹介でした。 ストリーミングデータの特徴として、途切れることがないこと、予期せぬディレイが発生することなどが紹介されていました。 また、そのためにWindowingやWaterMarkなどのバッチでデータを処理するときには考えなくてもよい概念も紹介されていました。 後半ではCloudDataFlowを使った時の頻出パターンの紹介がなされていました。 Exactly-onceの実現 PubSubはAtLeastOnceの取り出ししかサポートしないために、DataFlow側でExactly-onceを実現するための方法です。 各々のイベントのAttributeにイベントID(乱数)を埋め込み、Dataflow側でwithIdAttributeすることによってExactly-onceが実現されます。 壊れたデータの扱い PubSubにはリトライ機能があるため、壊れたデータがやってくるとそのデータを無限に処理しようとしてしまいます。 そのため、予期せぬ例外をtry-catchで拾い、それをdead letter用のTopicにpublishすることでこの問題に対処できるそうです。 また、壊れたデータを後から解析するためBigQueryに永続化をしたり、アラートを上げるためのTopicにpublishするなどの派生パターンもあるそうです。 スキーマ変更への対応 イベントにフィールドが追加された時に対処するためのパターンです。 BigQueryのテーブルに対するスキーマ変更はダウンタイムなしで実行できるため、フィールド追加を検知した時にテーブルの列追加を行います。 また、前述したリトライ機能によって、テーブルへの挿入に失敗したイベントの取得もできます。 リアルタイムにデータの非正規化をする BigTableにもデータを入れ、DataFlow上でそのデータとJOINすることによってリアルタイムでデータの非正規化を行います。 @SetupでBigTableへのコネクションを確立し、@Teardownでコネクションを閉じるようにすると良いそうです。 ストリーミングデータを処理するにあたって、データの到着が遅延するという特性はバッチ処理にはない概念です。 ですが、DataFlowを活用することによって、これらの問題に対処できる可能性も同時に感じました。 Securing Access to GCP resources VPCの新機能であるVPC Service Controlについての発表でした。 VPCの中にセキュリティ境界を作ることができ、そこに出入りする通信のルールを定めることができるそうです。 GCSバケットに対するアクセスを接続元IPによって制限し、専用線で接続されたオンプレ環境のみからのアクセスに限定するという使用例が紹介されていました。 オペミスによってIAMの設定をミスしてしまった時や、内部の人が悪意を持ってアクセス権を変えてしまった時の防波堤として機能するようです。 たとえIAMでアクセスが許可されていてもVPC Service Controlの設定が優先されるそうです。 この機能はまだアルファ版なので使うためには申請が必要ですが、早くも利用してみたい気持ちが高まります。 データ流出事件のレポートを読んでいると、その原因の多くがアクセス権設定のオペミスであることに驚かされます。 人間は誰しもオペミスをするので、このような仕組みで2重3重に防御することでデータ流出の危険性を低減できるのではと期待できます。 Better Practices for Cloud IAM Cloud IAMの説明とベストプラクティスの紹介でした。 発表の前半では、Cloud IAMの説明として、以下のようなことが紹介されていました。 プロジェクトは信頼境界を引くためのもの Resourceは階層構造を持っており上位階層の設定は下位階層に引き継がれる 後半では、具体的なベストプラクティスの紹介がされていました。 主に以下のようなことが紹介されていました。 ロールを割り振る対象はユーザーではなく、グループにする Google Groupで作成したグループに対して権限を割り振ることで新しい人が入った時の運用負荷が下がる。 可能な限り小さな権限を与える Primitive Roleはプロジェクト全体に対する権限を与えてしまうので、Predefined RoleかCustom Roleかを使うようにする。 追跡するための情報を残す 監査ログをGCSかBigQueryに保存する。 組織の構造をCloudIAMの階層構造に反映させる 組織全体の設定はOrganization Policyで管理し部署ごとの設定はFolderで管理する。 そして、サービス毎、環境(production、development、test)毎にプロジェクトを分ける。 プロジェクトは信頼境界を引くためのものということが目から鱗な発表でした。 今までは開発用のリソースと本番用のリソースを同一プロジェクトに入れていたため、アクセス権の設定に頭を悩ませることが多かったです。 あの時にプロジェクトを分けるという発想に至っていれば…という後悔も同時に感じました。 「とりあえず使って見よう!」という状態では後回しにされがちなIAMの設定を体系的に知ることができ、今後のGCP運用のための大きな知見でした。 感想 Google Cloud Next'18に参加することによって今まで知らなかったGCPの機能を知ることができました。 今まではAWSをメインのクラウドとして使用し、GCPはBigQueryを使うのみであったため、GCP全体を本格的に学習することはありませんでした。 しかし、いざGCPについて学習するとどのサービスもとても奥深く、自分の理解不足を強く実感しました。 また、今回はセッションを聞くことを中心に時間を過ごしましたが、Googleの人と直接話すことができる場にもっとを顔を出せばよかったと感じました。 そして、英語でのコミュニケーション力不足も強く痛感しました。 発表の最中に上手く聞き取れなかった部分を後で字幕付き動画で確認するととても平易な英語なことが分かり、落ち込むことが多かったです。 これからの弊社の大きな目標に海外展開がありますので、英語の継続的な勉強の必要性を実感しました。 まとめ 今回のGoogle Cloud Next'18は会社の出張という形で参加しました。 参加期間はすべて業務扱いとなり、交通費、宿泊費の他、出張手当も会社からサポートされました。 国外のカンファレンスへの参加のサポートはエンジニアとして非常にありがたいことであると感じました。 今回の出張で得た知見を日頃のサービス開発に生かしていきたいと思います。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/starttoday-tech/projects
こんにちは。新事業創造部の荒井です。 今回はiOSアプリの多言語対応について紹介します。 はじめに 私は今までいくつかのiOSアプリを運営してきましたが、どのアプリも日本語のみのサポートでした。現在関わっているWEARでは、すでに多言語対応が進められており、良い機会ですので個人的に知見がなかった多言語対応について調査をしました。今回は基本となる文字列の翻訳について触れていきたいと思います。 多言語対応 幅広いユーザーにサービスを使用してもらうには、言語と地域が非常に重要になってきます。日付、通貨、長さの単位など、言語、地域、文化によって異なることが多く、アプリケーションを世界的に出すには地域や文化に対応していく必要があります。現在WEARでは4つの言語対応を進めており、設定言語に応じて、アプリケーションをその言語に翻訳しています。対応している言語は以下の通りです。 日本語 簡体字中国語 繁体字中国語 英語 多言語対応をすることで、日本人以外の方にも使用していただく機会が増え、ユーザー増加が見込めます。 Localizationの手順 それでは設定言語によってアプリケーション内の文字列を複数の言語に変更する方法を紹介します。大まかに以下の流れになります。 PROJECTのLocalizationsに言語の追加をする Localizable.stringsを用意する NSLocalizedStringを使用し言語の切り替えをする 1. Localizationに追加 ローカライズの設定は PROJECT -> Info -> Localizations で行います。 はじめにBase Internatioalizationを有効にします。 Base Internationalizationを有効にする PROJECT -> Info -> Use Base Internationalization にチェックを付けます。これによりアプリケーションで使う文字列を.storyboardと.xibから分離します。こちらはXcode 5以降ではデフォルトで有効になっています。それ以前のバージョンから作成されているプロジェクトは公式の「 Base Internationalizationを有効にする 」が参考になるかと思います。 言語を追加する 次に PROJECT -> Info -> Localizations の「 + 」を押して言語を追加します。例では英語をBaseにしているので日本語を追加しています。 追加する言語を選ぶと、対応するファイルと File Types を選択する画面が表示されます。表示する文字列のみを対応する場合は Localizable Strings を選択してください。 2. Localizable.stringsの作成 翻訳したい文字列を定義しておくLocalizable.stringsを作成します。 メニューの Fille -> New -> File から Strings File を選択します。 作成したLocalizable.Stringsを選択し「 Localize... 」から言語を選択すると選択した言語がローカライズされます。 Base にチェックをつけることで Localizable.strings(Japanese) と Localizable.strings(Base) が作成されます。 作成したLocalizable.stringsを編集していきます。 ここではアプリケーションでよく使う文字列をサンプルとして定義しました。 Localizable.strings(Japanese) "Yes" = "はい" ; "No" = "いいえ" ; "Cancel" = "キャンセル" ; "Confirm" = "確認" ; "Search" = "検索" ; "Else" = "その他" ; "Edit" = "編集" ; "Done" = "完了" ; "Friend" = "友達" ; "Save" = "保存" ; "Title" = "タイトル" ; "Decide" = "決定" ; Localizable.strings(Base) "Yes" = "Yes" ; "No" = "No" ; "Cancel" = "Cancel" ; "Confirm" = "Confirm" ; "Search" = "Search" ; "Else" = "Other" ; "Edit" = "Edit" ; "Done" = "Done" ; "Friend" = "Friends" ; "Save" = "Save" ; "Title" = "Title" ; "Decide" = "Enter" ; 3. NSLocalizedString ここまでで準備は完了です。NSLocalizedStringを使用し言語の切り替えを実装します。 override func viewDidLoad () { super .viewDidLoad() let yes = NSLocalizedString( "Yes" , comment : "" ) let no = NSLocalizedString( "No" , comment : "" ) let cancel = NSLocalizedString( "Cancel" , comment : "" ) let confirm = NSLocalizedString( "Confirm" , comment : "" ) let search = NSLocalizedString( "Search" , comment : "" ) print(yes) print(no) print(cancel) print(confirm) print(search) } このコードを実行すると以下のように出力されます。 Yes No Cancel Confirm Search 言語を日本語に切り替えることによって文字列が日本語になるか確認します。 Edit Scheme の Application Language を変更すると簡単に確認ができます。 実行結果は以下のようになります。 はい いいえ キャンセル 確認 検索 Application LanguageでJapanese選択すると日本語に切り替わることが確認できました。 このようにアプリケーションで必要な文字列を定義して使用していきます。 翻訳のフロー 多言語対応されたアプリケーションを運用するには普段と違った工程が必要となります。 1つの言語のみサポートするサービスとの大きな違いは機能追加ごとに翻訳のプロセスが入ることです。 翻訳サービスを使用したり、社内に翻訳を担当するチームがあったりと様々だと思いますが、 現在のWEARはチーム内で完結しています。 開発段階で翻訳が必要なリストを作成し、各言語を母国語とするメンバーが翻訳を担当しています。 Localizable.stringsをエンジニアが管理する方法もありますが、Apple社が案内している翻訳の手順にXLIFF(XML Localization Interchange File FormatXLIFF)を使用した運用があります。 画像出展: インターナショナライゼーションとローカリゼーションのガイド XcodeからXLIFFファイルにエクスポートし、翻訳者がXLIFFを編集し翻訳完了後にXLIFFファイルをインポートをします。Androidなど複数プラットフォームで多言語対応を進める際に有用です。 まとめ 今回はiOSアプリの多言語化について紹介しました。今回は文字列だけでしたが、実際には日付・価格などの表示やレイアウト対応など考慮するべきことは多々あります。より多くのユーザーにサービスを使って頂けるよう日々改善を続けています。少しでも興味がある方は、ぜひ一度オフィスにお越しください。 下記からのエントリーもお待ちしています。 www.wantedly.com 参考文献 https://developer.apple.com/jp/internationalization/
(Icon Credit *1 ) こんにちは。スタートトゥデイ研究所の後藤です。 今回は、集合を入力として扱うネットワークモデルの紹介をしたいと思います。機械学習の多くのモデルは、固定長の入出力や順序のある可変長の入出力を扱うように設計されます。画像データやテーブルデータは各サンプルの入出力の次元を合わせて学習しますし、自然言語処理のコーパスや時系列データは入出力の順序を保持して利用します。 その一方で、可変長で順序のない集合データを扱うモデルの研究は最近になって取り組み始められたばかりです。我々が研究しているファッションの領域において、入力データを集合として扱いたくなる状況がたびたびあるため、理解を深めておきたい問題設定です。 コーディネートをアイテムの集合とみなす コーディネートに使われたアイテムの例 コーディネートをアイテムの組み合わせとして捉えた場合、1つのコーディネートはアイテムを要素とする集合であると見なすことができそうです。ここでは、1つのコーディネート内に同じアイテムは1度しか登場しないことと、アイテムには順序がないことを前提としています。後ほど触れますが、アイテムのカテゴリによる順序を設定して系列データとして扱う研究もあります(Han et al. 2017, Nakamura & Goto 2018)。 コーディネートデータを扱う過去の取り組み コーディネートデータの学習をするための過去の取り組みを紹介します。 Siamese Networks 集合データから2つのアイテムを取り出して固定長の入力として扱う例です。Veit et al. 2015では、ファッションスタイルの違いを反映した空間を学習する試みで、この手法を取っています。集合全体を評価することは一旦諦めて、入力を固定長にしてしまうことで、従来の枠組みを用いてモデルを構築できます。 (Veit et al. 2015, Figure 2) このタスクでは、ペアが集合全体の性質を持っている場合は良いですが、集合全体として初めて成り立つ性質を持っているようなタスクには使えません。1つ1つのアイテムからはスタイルは見えてこないが、トータルコーディネートを見て初めてスタイルが明らかになるような例を学習することは難しそうです。 Bidirectional LSTM コーディネートに使われるアイテムの数は可変長なので、系列データとみなして扱った例です。Han et al. 2017では、アイテムのカテゴリにより入力の順序を決め、コーディネートを系列データとしてBidirectional LSTMを学習しています。 (Han et al. 2017, Figure 2) この手法ではカテゴリによる入力の順序を定義しているため、同じコーディネートの評価をする場合でも入力するデータの順序を変えると、出力が変わると言う性質があります。論文中では、インプットするカテゴリの順番を入れ替えて学習すると、得られるモデルのクオリティ自体が変わることも指摘されています。入力する順序によって組み合わせの評価値が変わる性質は、タスクによっては不都合なこともあります。 集合データを直接扱う方法 Zaheer et al. 2017では集合データをストレートに扱う方法を提案しています。この論文では、集合を入力とする際に満たされるべき性質を持った、必要十分なネットワーク構造を示しています。ここでは、permutation invariantとpermutation equivariantについて紹介します。実装に関しては、著者自身のコードがgithubにあるため理解の助けになるかと思います。 github.com Permutation invariant Invariant modelのアーキテクチャ (Zaheer et al. 2017, Figure 5) permutation invariantのモデルを模式的に表したのが上図です。 集合データを入力とし、スカラーを出力する問題です。集合に対して1つの値を返すため、入力データの順序を変えても出力が同じになります。この性質をpermutation invariantと呼びます。例えば、コーディネートに点数をつける、といったタスクが当てはまると考えられます。 関数 がpermutation invariantであるとは、任意の並べ替え に対して、 が成り立つことです。 論文では、集合の各要素の特徴を関数\( \phi(x)\)により独立に抽出し、それを足し上げたものを非線形関数\( \rho\)に渡すことで、permutation invariantな関数が実現できることを証明しています。各要素の特徴 \( \phi(x) \)の和は、任意の並べ替え \( \pi \)に対して変わらないので、\( f(X) = \rho (\sum_{x \in X} \phi(x)) \)が入力する順序を入れ替えても出力は変わらないことが直感的にわかるかと思います。 活用例 コーディネートデータの活用事例として、オートエンコーダーによるスタイルの抽出があります。この実験ではコーディネートを固定長のベクトルで表現する方法として、concat型とreduce型の方法を試しています。このうちのreduce型の手法は先ほど述べたpermutation invariantの条件を満たしています。この手法はコーディネートデータからスタイルを抽出するというタスクにおいて優れた性能を発揮することがわかっています。 Permutation equivariant 例えば、集合の中から仲間はずれを検知する、コーディネートデータの中からスタイルが一致しないアイテムを発見するなどの問題設定の場合、集合の入力に対して要素数分の出力が必要になります。さらに各入力要素と出力の各要素は対応関係を持っています。このような対称性をpermutation equivariantと呼び、以下のように表現されます。 ] モデルパラメータを\( \Theta\)を持つモデルを と表現した際、\( \Theta\)が を満たす変換であればpermutation equivarinatであることが示されています。さらにこの定式化の拡張として、 も、permutation equivariantを満たしています。 集合方向のmaxpoolingは入力データの順序に対して不変なので、これ自体はpermutation invariantです。 Equivariant modelのアーキテクチャ (Zaheer et al. 2017, Figure 7) このようなネットワークは、pytorchで表現すると以下のように記述することができます。PermutationEquivariantクラスが\(\Theta\)に対応する部分です。集合を扱うという新しいことができるようになっているのですが、とてもシンプルです。実際に入力の順序を入れ替えると対応した出力の順序も入れ替わることが確認できます。 import torch import torch.nn as nn class PermutationEquivariant (nn.Module): def __init__ (self, in_dim, out_dim): super (PermutationEquivariant, self).__init__() self.Gamma = nn.Linear(in_dim, out_dim) self.Lambda = nn.Linear(in_dim, out_dim, bias= False ) def forward (self, x): xm, _ = x. max ( 1 , keepdim= True ) xm = self.Lambda(xm) x = self.Gamma(x) x = x - xm return x class DeepSets (nn.Module): def __init__ (self, x_dim, d_dim): super (DeepSets, self).__init__() self.x_dim = x_dim self.d_dim = d_dim self.phi = nn.Sequential( PermutationEquivariant(self.x_dim, self.d_dim), nn.ELU(inplace= True ), ) self.rho = nn.Sequential( nn.Dropout(p= 0.5 ), nn.Linear(self.d_dim, self.d_dim), nn.ELU(inplace= True ), nn.Dropout(p= 0.5 ), nn.Linear(self.d_dim, 10 ), ) print (self) def forward (self, x): phi_output = self.phi(x) sum_output = phi_output.mean( 1 ) rho_output = self.rho(sum_output) return rho_output まとめ 今回は、集合を入力として扱うネットワークの表現のうち、permutation invariantとpermutation equivariantの2つの場合について紹介しました。入力に集合を扱えるようになることで、IQONやWEARのコーディネートデータを活用しやすくなります。例えば、コーディネートの採点やスタイルの抽出、スタイル不一致のアイテムの発見など、様々な応用例が考えられるでしょう。 さいごに 研究所ではアイテムの集合以外にも「似合う」ということについて多角的に研究を進めています。機械学習に限らず、専門性を活かしてファッションを分析できる環境を提供しています。ファッションに関する研究テーマに挑戦したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。 www.wantedly.com 参考 Han, X., Wu, Z., Jiang, Y., Davis, L. Learning Fashion Compatibility with Bidirectional LSTMs. In ACM Multimedia, 2017. Nakamura, T., Goto, R. Outfit Generation and Style Extraction via Bidirectional LSTM and Autoencoder. In KDD workshop 2018. Veit, A., Kovacs, B., Bell, S., McAuley, J., Bala, K., Belongie, S. Learning Visual Clothing Style with Heterogeneous Dyadic Co-occurrences. In ICCV, 2015. Zaheer, M., Kottur, S., Ravanbakhsh, S., Poczos, B., Salakhutdinov, R., Smola, A. Deep Sets. In NIPS, 2017.
こんにちは。新事業創造部インフラチームの光野(kotatsu360)です。 先日、VASILY時代 1 から長らく使われていたCapistranoによるデプロイを見直し、CodePipeline+CodeDeployによるデプロイフローを導入しました。 CodeDeployはEC2 AutoScalingとよく統合されており、この新しいデプロイフローによって最新のアプリケーションコードをどう反映するかという悩みから開放されました。この記事ではそのフローについて設計と運用を交えつつ紹介します。 AWS CodePipeline / AWS CodeDeploy CodePipeline AWS CodePipeline はアプリケーションのCI/CDパイプラインを作るためのサービスです。 Source 、 Build 、 Test 、 Deploy の4ステージに対して1つ以上のアクションを割り当てることで、任意のパイプラインを構築します。 設定項目が多く初めは戸惑いますが、全てのステップを使う必要はありません。 実際、今回構築したパイプラインでも Source と Deploy のみを使っています。 Build や Test に相当する部分は既存のCIで事足りるためです。 CodeDeploy AWS CodeDeploy はアプリケーションの自動デププロイを管理するためのサービスです。 エージェント式になっており、Amazon EC2だけでなくオンプレミスに対してもデプロイが可能です 2 。 デプロイの具体的な作業は、実行可能な形式で置いておきます。 # appspec.yml # CodeDeployに対してデプロイ処理を指定するファイル。リポジトリルートに置かれる version : 0.0 os : linux files : - source : / # zipに含まれるファイル全部を destination : /tmp/sample # sample以下に展開 hooks : ApplicationStop : - location : codedeploy/stop-application.sh timeout : 30 # Install: # CodeDeployが最新のコードをデプロイターゲットに配布する AfterInstall : - location : codedeploy/start-application.sh 上のappspec.ymlでは「既存プロセスをkillする」「新しいプロセスを起動する」といった内容を .sh で表現しています。これをエージェントが順次実行します。デプロイ先で実行可能であれば、表現方法は自由です。 デプロイフロー 新しいデプロイフローは次のようになりました。 画像の左下がスタートです。 アプリケーションコードがGitHub上で特定のブランチにマージされる CircleCI によるテストの後、zipで圧縮してS3に保存 S3はオブジェクトの更新をCodePipelineに通知 CodePipelineはCodeDeployを呼び出す CodeDeployは事前に定められたAutoScalingグループに対してデプロイを実行 詳細について触れていきます。 CodePipelineのパイプライン戦略 CodePipelineのデプロイパイプラインは、リポジトリ単位で分離します。 複数の役割を持っているリポジトリについては、 Deploy フェーズで分岐させ、それぞれをCodeDeployが実行します。 3 具体的には、APIのようにロードバランサにアタッチする必要のあるAutoScalingグループと、非同期処理を担当するAutoScalingグループで分離しています。 CodeDeploy + OpsWorksとの連携 CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、新規追加されたインスタンスに対して自動でデプロイを実行します 4 。便利な一方、新規に起動したインスタンスの構成管理について考慮する必要があります。 私が管理するいくつかのサービスでは、構成管理を全てOpsWorksに集約しています 5 。これはAutoScalingグループに所属するインスタンスであっても例外ではありません 6 。 何のフォローも行わない場合、構成管理中にCodeDeployのデプロイ処理だけが先行し、デプロイが失敗します。デプロイに失敗した新規インスタンスはAutoScaling側で失敗とみなされ破棄されるため、延々と起動・失敗・破棄を繰り返してしまいます。 これを避けるため、CodeDeployよるデプロイ処理中にOpsWorks側のステータスを確認する処理を含めています。 version : 0.0 os : linux files : - source : / # zipに含まれるファイル全部を destination : /tmp/sample # sample以下に展開 hooks : ApplicationStop : - location : codedeploy/stop-application.sh timeout : 30 # この処理を追加 BeforeInstall : - location : codedeploy/setup-wait.sh timeout : 900 AfterInstall : - location : codedeploy/start-application.sh #!/bin/bash set -e OPSWORKS_INSTANCE_ID = $( grep ' OpsWorks Instance ID ' /etc/motd | cut -d: -f2 | tr -d ' ' ) /usr/local/bin/aws opsworks --region us-east-1 wait instance-online --instance-ids $OPSWORKS_INSTANCE_ID 素朴なシェルスクリプトですが、この処理を挟むことで構成管理を待った上でデプロイを実行する事ができます。 インプレースデプロイかBlue/Greenデプロイか CodeDeployのデプロイターゲットとしてAutoScalingグループを指定すると、デプロイの方法として2パターンを選択できます。 インプレースデプロイ:既存のインスタンスに対してデプロイを行う Blue/Greenデプロイ:新規のインスタンスを作成しデプロイを行う 本件では前者のインプレースデプロイを採用しています。後者の長所は障害時の高速なロールバックかと思いますが、前述の構成管理と合わせて検討すると、revertコミット+インプレースデプロイが十分に高速かつシンプルという判断です 7 。 なお、インプレースデプロイかつデプロイターゲットがロードバランサに所属する場合、CodeDeploy側がデプロイの前後で適切にアタッチ・デタッチをしてくれます。サービスインしたままデプロイが行われるということはありません。 CodeDeployを採用してハマった事 完成した後のデプロイフローはとても安定しています。しかし、構築中にいくつかハマった部分もありました。 複数のロードバランサに所属する場合のフォロー CodeDeployは、デプロイターゲットがロードバランサ(ALBならターゲットグループ)に所属している場合、適切にアタッチ・デタッチしてくれます。しかし複数のロードバランサまでは面倒を見てくれません(2018年7月時点)。 そのため、何らかの理由で複数のロードバランサに所属するAutoScaingグループであれば、CodeDeployに渡す処理中でフォローしてやる必要があります。 #!/bin/bash set -e readonly INSTANCE_ID = $( curl -s http:// 169 . 254 . 169 . 254 /latest/meta-data/instance-id ) readonly TARGET_GROUP_INTERNAL_API = ' xxxxx ' /usr/local/bin/aws --region ap-northeast-1 elbv2 register-targets --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id = ${INSTANCE_ID} /usr/local/bin/aws --region ap-northeast-1 elbv2 wait target-in-service --target-group-arn ${TARGET_GROUP_INTERNAL_API} --targets Id = ${INSTANCE_ID} CodeDeployのBlockTraffic/AllowTraffic CodeDeployのデプロイは幾つかのステップがAWS側に予約されています。ロードバランサとのインテグレーションもその予約されたステップで行われるのですが、なぜか2〜3分も待たされる事があります。 どうやらCodeDeployの挙動はロードバランサのヘルスチェック設定に依存しているようです 8 。 BlockTraffic/AllowTrafficにかかる時間は次のとおりです。 Interval: 30sec 、 Healthy threshold: 10 ならそれぞれ300秒程度 Interval: 5sec 、 Healthy threshold: 2 ならそれぞれ10秒程度 運用のポリシーが許す範囲で短くしておくことをおすすめします。 CodeDeployを採用して良かったこと AutoScalingとの統合以外にもコスト面で効いた部分がありました。 集約率の向上 これはアプリケーションレベルでの Graceful Restart にこだわる必要が無くなったため得た恩恵です。 当初はAutoScalingによる柔軟なリソース配分を重視して始めた施策でしたが、改めて考えると一台あたりのパフォーマンスも上げることが可能でした。 本件で紹介しているデプロイフローを採用しているアプリケーションは Rails + unicorn + nginx という、Railsでよくみる鉄板構成です。 旧デプロイフローではunicornに対してCapistranoでUSR2シグナルによる Graceful Restart をしていました。 Graceful Restart の問題は、古いプロセスを残したまま新しいプロセスを作るため瞬間的にメモリ消費量が倍になることです。そのため、デプロイを安全に終了するためには、1インスタンス毎のメモリ消費量を40%程度に管理しておく必要があります。 一方、CodeDeployによるデプロイであれば一時的にサービスアウトしつつデプロイが進行します。そのため、既存のプロセスを一旦完全にkillできます。これによりデプロイ中という瞬間的な状態を気にすること無く集約率を検討できるようになりました。 まとめ 本記事では、CodePipeline+CodeDeployによる新しいデプロイフローについて紹介しました。 AutoScalingとの統合や集約率の向上といった恩恵はもちろんですが、デプロイを小さなステップに分割して整理できたため、保守性という意味でも向上したように感じています。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 https://www.starttoday-tech.com/recruit/ 株式会社スタートトゥデイテクノロジーズはスタートトゥデイ工務店 + VASILY + カラクルの3社を統合し発足されました ↩ AWS Lambdaに対するデプロイも可能ですが、本記事では扱いません ↩ 画像中でCodeBuildを呼び出していますが、これはパイプライン設計中に後からタスクを任せようと思った名残です。結局、既存のCircleCIで十分と気づき何もしないステージとしてそのままになっています。本記事でも触れません。 ↩ ライフサイクルフック の仕組みを用いています。 ↩ CloudFormationとOpsWorksでインフラを育てる ↩ UserData、OpsWorks、Lambdaを組み合わせ、常に新鮮なSpotFleetインスタンスでサービスを運用する (引用はSpotFleetに関する話題だが、SpotFleetでないAutoScalingでも同様) ↩ ゴールデンイメージによる構成管理時間の短縮を検討しなかったわけではありませんが、既存のOpsWorksによるフローを変更することのデメリットが遥かに大きく、早々にアプローチから外れました。 ↩ AWS Developer Forums: BlockTraffic/AllowTraffic durations ↩
(Icon Credit *1 ) こんにちは。PB開発部インフラチームの @inductor です。最近はすっかり インフラ勉強会 というオンライン勉強会の運営が趣味になっています。 今回はLambda@EdgeというAWSのサービスを使って、CloudFrontへのアクセスを「細かいルール」を設定して振り分けてみたいと思います。 Lambda@Edgeについてもう詳しく知っているよ! という方は、次のセクションはスキップしてもらって構いません。もしよく知らないという方は、一緒に勉強してみましょう! AWS Lambda@Edgeとは Lambda@Edge は以下の2つのサービスから成り立ちます。 AWS Lambda Amazon CloudFront CloudFrontのエッジロケーションにおいて、Lambdaで定義した任意のコードを実行できるというのがLambda@Edgeで、具体的には以下のような恩恵を得ることができます。 エッジロケーション(地域)に応じて表示させるコンテンツを変えたい User-Agentなどに応じて取得するコンテンツを変えたい アクセス元のIPアドレスによって表示させるコンテンツを変えたい 開発環境に、アクセス元IPが自社でない場合のみBasic認証を導入したい 実際に叩かれるURLと、S3などから実際に取得する資源のURIを変更したい この他にも、CloudFrontから取得できるデータに応じて様々な対応を柔軟に行えるのが特徴です。 AWS Lambdaのおさらい AWS Lambda は、任意のプログラムをサーバを用意することなく実行できるFaaS(Function as a Service)のサービスです。主に、バッチ処理やログの処理、サーバレスアーキテクチャにおけるバックエンドの処理などで利用されます。 現在Lambdaで提供されている実行環境には、Node.js、Java、C#、Go、Pythonがあり、それぞれ好きな言語を選んで実行させることができます。 詳しくは、 AWSの公式ページ や、 クラスメソッドさんなどの記事 をご覧いただければと思います。 Amazon CloudFrontのおさらい Amazon CloudFront は、AWSのCDNサービスです。世界中に存在する「エッジロケーション」と呼ばれるAmazonの各拠点に対して、S3に保存した静的コンテンツ(HTML、CSS、JSなど)をキャッシュさせておくことができます。 キャッシュのルールを細かく設定できる他、SSL証明書を ACM などから投入しつつHTTP/2の有効化やTLSのバージョン指定などのエンドポイントとしても機能的に利用できるなど、開発者にとって面倒な設定も簡単にできるという利点もあります。 改めてLambda@Edgeとは CloudFrontのエッジロケーションにおいて、Lambdaを使って細かいルールを自由に設定できるサービスです。 CloudFrontでは大きく分けて以下の4つのデータの流れがあり、それぞれ、1つのリクエスト(レスポンス)に対して一意のLambdaのコードを割り当てることができます(割り当てなくても動作はします)。 ※引用元スライド: https://www.slideshare.net/AmazonWebServicesJapan/aws-blackbelt-online-seminar-2017-amazon-cloudfront-aws-lambdaedge Lambda@Edge導入前の注意事項と事前準備 注意事項 Lambda@Edgeにて2018年7月現在で対応しているのはNode.js(6.10と8.10)のみです Lambda関数はバージニアリージョンで作成する必要があります Lambda関数が実行されたときに吐き出されるログが保管されるリージョンは、ユーザーから 最も近いエッジロケーション になります 例えばバージニアリージョンでコードを上げていても、ベトナムのユーザーがアクセスした場合、LambdaのCloudWatchログはムンバイなどのリージョンに表示されます CloudFrontの仕様上、特定のHTTPヘッダについて書き換えられないなどの制限があります 詳しくは 公式のドキュメント へどうぞ 必要な事前準備 Lambda@Edgeを適用するためには以下の準備が必要となります。 CloudFrontとS3の準備 Lambda@Edge用のIAMロールの作成 これらについては今回は省略します。それぞれ、該当リンクなどを参考に準備をしてみてください。 まず、IPアドレスに応じて必要な振り分け先を変えるという対応をしてみましょう。 サンプルとして、以下のようなコードを作成し、Lambda@Edgeの設定をしていきます(IPアドレスはダミーのため、各自試したいものに変更しましょう)。 'use strict' const permitIp = [ '172.0.0.1' , //IPアドレスは各自設定すること! '172.0.0.2' , ] ; exports.handler = ( event , context, callback) => { const request = event .Records [ 0 ] .cf.request; const httpVersion = request.httpVersion; const clientIp = request.clientIp; const isPermittedIp = permitIp.includes(clientIp); if (isPermittedIp) { // 許可されているIPであればそのまま次の処理へ callback( null , request); } else { // 許可されていないIPに対しては許可されていない旨のメッセージを返す const body = '<!DOCTYPE html> \n ' + '<html> \n ' + '<head><title>Hello From Lambda@Edge</title></head> \n ' + '<body> \n ' + 'Your IP address is not permitted to access! \n ' + '</body> \n ' + '</html>' /* Generate HTTP response */ const customResponse = { status : '200' , statusDescription: 'HTTP OK' , httpVersion: httpVersion, body: body, headers: { 'cache-control' : [{ key: 'Cache-Control' , value: 'max-age=100' }] , 'content-type' : [{ key: 'Content-Type' , value: 'text/html; charset=utf-8' }] , } , } ; callback( null , customResponse); } } ※特定の条件下で何もせず次の処理に渡したい場合は、上記コードのように callbackでrequestをそのまま返してあげる と良いです。 Lambdaの設定 Lambdaに対して上記コードを仕込んでいきます。 まず Lambdaのコンソール にアクセスし、関数を新しく作成します。 ※このとき、かならずバージニアリージョンを選びましょう! 続いて、以下スクリーンショットのように関数名に適当な名前を付け、ロールに作成済みのLambda@Edge用ロールを紐づけます。 関数が作成されますので、「関数コード」の欄に該当コードを貼り付け、画面上部、右上の「保存」ボタンをクリックします。 「アクション」から新しいバージョンを発行するを選択し、「発行」をクリックします。 バージョンが1になったことを確認したら、「Designer」よりトリガーの追加→CloudFrontを選択します。 「トリガーの設定」から、ディストリビューションにCloudFrontのDistribution IDを入力し、イベントトリガーにはビューアーリクエストを追加します。 上記が確認できたら「追加」をクリックし、再度、画面右上の保存ボタンをクリックします。 CloudFrontのコンソール画面 からBehaviorsタブを選択し、「Path Pattern」にて「Default(*)」または自分の選択したいパスを選択し、「Edit」をクリックします。 最下部のLambda Function Associationsにバージョン番号含めLambda関数が紐付いていることを確認します。 最後に、該当のCloudFrontのURLをブラウザで確認します! 該当しないIPアドレスからのアクセスに対して、以下のようなレスポンスが返ってくれば問題ないです。 以上、長くなってしまいましたが、Lambda@Edgeを使ったIPアドレスフィルタの実装手順でした。 さいごに Lambda@Edgeでは、CloudFrontだけでは今までできなかった細かいルールの振り分けが実現でき、より柔軟な使い方が可能になっています!本ブログでも、役に立った便利な使い方を定期的に更新していこうと思います。 みなさまも、この機会に是非導入を検討してみてはいかがでしょうか! スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://www.starttoday-tech.com/recruit/
スタートトゥデイ研究所リサーチャーの中村です。 今回は、コーディネートからスタイルを自動抽出する技術に関するアイデアの紹介です。こちらは、 企業研究所による研究発表カンファレンス (CCSE2018) でも同様の内容で発表させていただきました。 そのときに使用した資料はこちらです。 ファッションのスタイルについて 服は組み合わせによって見た目の印象が変化します。例えば同じスラックスを履いたとしても、トップスがYシャツのときとTシャツのときでは印象が異なるはずです。ファッションではこの現象をスタイルやテイストといった言葉で表現します。 スタイルはコーディネートを考える際の指針となります。頻出するスタイルにはエイリアスが設定されており、印象を伝える際に利用されます。例えば以下のようなものです。 ところが多くの人にとって、このスタイルという概念はとても厄介です。服の組み合わせは無限に存在し、組み合わせを印象へマッピングできるようになるためには高度な専門知識が必要になるからです。 しかし服をおしゃれに着る際にはスタイルは最も重要な指針のひとつなので、この背後にある法則を解析し、機械で再現することには意義があります。 スタイルを解釈するためのフレームワーク 日本ファッションスタイリスト協会 という団体がStyling Mapというフレームワークを提供しています。 ファッションに関係する物 *1 と人 *2 を、それぞれの個性に応じて4つのテイストに分類するための考え方です。 4つのテイストとはアクアテイスト(Aq)、ブライトテイスト(Br)、クリスタルテイスト(Cr)、アーステイスト(Ea)で、各テイストについて与える印象と属するオブジェクトの有する特徴が定義されています。 ( https://stylist-kyokai.jp/stylingmap ) Styling Mapでは、スタイルを先述の4つのテイストの混合で表現することを許容しています。 4値判別でなくあえて曖昧さを含む表現を用いることで、表現能力の高いフレームワークとなっています。このアイデアは非常に有用だと思います。 解きたい問題 Styling Mapはファッションの専門家が経験に基づいて組み上げたフレームワークであり、実績を上げています。 この知識を再現してECに導入できれば、推薦を高度化すると同時に説得力をもたせることも可能です。 今回はコーディネートを対象に、背後に存在するスタイルの抽出とコーディネートの分類に取り組みます。 なお、Styling Mapでは4つのテイストに分類していましたが、本記事では基底となるスタイルをデータから求めます。 スタイルの定式化 Styling Mapからアイデアを拝借し、コーディネートのスタイルを混合表現で記述することを考えます。 基底のスタイルベクトル と混合比 を与えたとき、コーディネートのベクトル をそれらの線形結合で表現します。 例えば、基底スタイルを先述の4種類のテイストとしたとき、あるコーディネートは以下のように表現できます。 今回の問題では と は未知であり、これらをデータから求めることに挑戦します。 モデル 3層の砂時計型ニューラルネットワークを使います。入力はコーディネートベクトル 、出力は復元したコーディネートベクトル 、中間層はスタイル混合比 に対応します。 スタイル混合比の獲得 スタイル混合比 の各要素は対応する基底スタイルの重みに相当します。コーディネートベクトル を入力とし、全結合層にfeedしてこれを求めます。 比であることを維持するため、softmax関数を適用します。 コーディネートベクトルの復元 とモデルパラメータ の内積によりコーディネートベクトルの復元を行います。 ここで、 の各行は基底スタイルのベクトルであると仮定すると、この操作は定式化の章で述べたスタイルの表現に対応します。 学習 2種類のloss関数を定義します。はじめに復元時の誤差として、 と の類似度をhinge関数で評価します。 ここで、負例 はミニバッチから選択します。2点の類似度を返す関数 はコサイン類似度を使います。 続いて の正則化です。 の各行はスタイルの基底であると仮定しているため、各行は独立であることが望まれます。 これを式にすると、以下の様になるかと思います。 ここで、 は の各行を正規化した行列です。 コーディネートベクトルの抽出 コーディネートベクトル はCNNを用いて抽出します。入力のコーディネートデータはアイテム画像の集合であるため、それぞれの画像をResNet50にfeedして得られた特徴量を平均したベクトルをコーディネートベクトルとします。 データ IQONのコーディネートデータセットを使います。データセットの詳細は以前のブログでも触れているのでそちらをご参照ください。 参考: コーディネートの自動生成 特徴は、1つの標本が画像の集合であり、標本ごとに画像枚数が異なるということです。 結果 の次元数を8に設定し、学習させました。得られた基底スタイルの9近傍を表示します。 各スタイルの1行が、1つのコーディネートに対応します。 やはりファッションに詳しくないと分類に成功したか否かの判断に困りますが、少なくとも色調の違いはかなりはっきりと現れているかと思います。 本家Styling Mapでも説明できそうなクラスタも見受けられます。例えば、#5はアクアテイスト *3 とかなり似ていることが確認できます。 スタイルを混合表現にしたことで、基底スタイルを混ぜ合わせたようなスタイルも表現できるようになりました。 ここに示した例は、混合した基底スタイルの特徴が現れているかと思います。 まとめ 今回はコーディネートからのスタイル抽出に挑戦してました。 スタイルに混合表現を導入し、データから基底スタイルと混合比を教師無しで抽出しました。 poolingの方法の高度化やattentionの導入など、まだまだ改良の余地はあるかと思います。 一方で、コーディネートの特徴量さえうまく作れれば、スタイル抽出はもっとシンプルな手法でも達成できる気がしました。LDAやICAなどの手法は相性が良さそうです。 さいごに 研究所ではスタイルの抽出も含め「似合う」ということについて多角的に研究を始めています。 機械学習に限らず、専門性を活かしてファッションを解析したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。 www.wantedly.com *1 : 服やアクセサリーなど *2 : 骨格や性格など *3 : 本家が定義しているアクアテイストの特徴をご確認ください
こんにちは。スタートトゥデイテクノロジーズ新事業創造部の id:takanamito です。 今日はVASILY時代から活用されているOpenAPI(Swagger)の定義からRubyのクラスを自動生成するgemを作ったので、その紹介をしようと思います。 Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている 弊社ではVASILY時代からSwaggerの導入が進んでいましたが、徐々に「Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている」といった問題が発生しはじめていました。 その問題を解決するために今回つくったのがこのgemです。 github.com 例えばこんなOpenAPI Specification 3.0のYAMLの定義から schemas : user : type : object properties : username : type : string uuid : type : string repository : type : object properties : slug : type : string owner : $ref : '#/components/schemas/user' pullrequest : type : object properties : id : type : integer title : type : string repository : $ref : '#/components/schemas/repository' author : $ref : '#/components/schemas/user' こんなクラスが自動生成できます。 # cliで生成 
$ openapi2ruby generate ./path/to/link-example.yaml --out ./ $ ls . pullrequest_serializer.rb repository_serializer.rb user_serializer.rb class PullrequestSerializer < ActiveModel :: Serializer attributes :id , :title , :repository , :author def repository RepositorySerializer .new(object.repository) end def author UserSerializer .new(object.user) end def id type_check( :id , [ Integer ]) object.id end def title type_check( :title , [ String ]) object.title end private def type_check (name, types) raise " Field type is invalid. #{ name }" unless types.include?(object.send(name).class) end end 開発の経緯 先述の「Swaggerの定義と実際のAPIが返すレスポンスの内容がズレている」問題 APIの改修時に必ずSwagger定義も更新した上でアプリケーションを書き換えるという運用が人の手によって行われていたため、当然起こりうる事象だったのですが 実際にコードを読んでみるとController内で単にHashのオブジェクトを to_json してレスポンスデータを生成している処理が散見されました。 そのためAPIに型の概念を持ち込んでSwagger上の定義とレスポンスを一致させる方法を考え始めました。 実際にはOpenAPIのschema定義から ActiveModel::Serializer のクラスを自動生成しています。 調査 「コードの自動生成」という用途においては swagger-codegen が有名だったので、まずは今回やりたいRubyクラスの自動生成ができないか調査してみました。 デフォルトでは ruby , sinatra , rails5 のコードジェネレータが用意されており、以下のようにDockerを使ってコードの自動生成ができます。 # 通常のコードジェネレート $ ./run- in -docker.sh generate -i modules/swagger-codegen/src/ test /resources/2_0/petstore.yaml -l ruby -o /path/to/output # 独自テンプレートでコードジェネレート $ ./run- in -docker.sh generate -i modules/swagger-codegen/src/ test /resources/2_0/petstore.yaml -l ruby -t path/to/template_dir -o /path/to/output 実際にコードジェネレートすることはできますが、以下の点が気になりました。 テンプレートにmustache記法を使うことを強制される 欲しいのはschema定義された数ファイルだけなのに不要なファイルが大量に生成されてしまう 回避しようと 新しい対応言語を定義 してみたがjarを実行するなどの手順が必要 たまたまやる気があったので、シンプルにOpenAPI Specificaton 3.0のschema定義からRubyのクラスだけを生成するgemを作ることにしました。 (後から知ったんですが --ignore-file-override と .swagger-codegen-ignore を使えば指定したテンプレートのみ使ってコードジェネレートできるようです。) 導入の利点 現状、Rubyアプリケーションにおいてスキーマ定義どおりにレスポンスが返っているか検証するにはテストを書くか もしくはGraphQLなどのスキーマと実装が密接に紐付いている仕組みを採用することになると思います。 しかしテストを書くか否かは実装者に依存してしまいますし、既存APIをRESTからGraphQLに置き換えるのは工数的にもなかなか選べないことが多いはずです。 そういう状況において「スキーマから自動生成したシリアライザーでレスポンスの型を保証できる」今回のようなアプローチは有用かと思います。 スキーマファーストで開発をしていても、スキーマを更新した後アプリケーションにその変更を反映することを忘れてしまうと同じ問題が起こってしまいますが 今回のgemはcliを提供しているので「シリアライザーのファイルをgit管理下から外し、CIでテストやデプロイ時に最新のスキーマから自動生成して配置する」といったことも可能です。 「人が忘れてたことによってOpenAPIのスキーマ定義と実際のレスポンスがズレる」といった問題が防げるところに導入の利点があると考えています。 サンプル このgemを使いつつ、Twitterのような簡単なRails APIを作ってみます。 まずはGemfileに以下を追加 gem 'active_model_serializers' 登場するモデルは User , Profile , Tweet の3種類です。 class User < ApplicationRecord has_one :profile has_many :tweets end class Profile < ApplicationRecord belongs_to :user end class Tweet < ApplicationRecord belongs_to :user end スキーマはこんな感じ。 create_table " profiles " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.bigint " user_id " t.string " description " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false t.index [ " user_id " ], name : " index_profiles_on_user_id " end create_table " tweets " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.bigint " user_id " t.string " tweet_text " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false t.index [ " user_id " ], name : " index_tweets_on_user_id " end create_table " users " , options : " ENGINE=InnoDB DEFAULT CHARSET=utf8 " , force : :cascade do | t | t.string " name " t.datetime " created_at " , null : false t.datetime " updated_at " , null : false end seeds.rbでサンプルデータも。 User .create( name : ' takanamito ' ) Profile .create( user : User .first, description : ' プロフィールです ' ) Tweet .create( user : User .first, tweet_text : ' 我が問いに空言人が焼かれ死ぬ ' ) Tweet .create( user : User .first, tweet_text : ' オレは太刀の間合い(半径4m)までで十分...!!(つーか これが限界) ' ) Tweet .create( user : User .first, tweet_text : ' 私の垂直跳びベストは16m80cm!!! ' ) 以下のようなユーザー情報を返すAPIをOpenAPIで定義します。 schemas : user : type : object properties : name : type : string profile : $ref : '#/components/schemas/profile' tweets : type : array items : $ref : '#/components/schemas/tweet' profile : type : object properties : description : type : string tweet : type : object properties : tweet_text : type : string ActiveModel::Serializerを使う前提でControllerを書いてゆきます。 class UsersController < ApplicationController def show @user = User .find(params[ :id ]) render json : @user end end ここまで用意すればあとはgemでシリアライザを自動生成するだけ。 $ openapi2ruby generate ./path/to/openapi.yaml --out ./app/serializers/ Railsを立ち上げて、ブラウザでアクセスしてみると... シリアライザを通して生成したjsonが返せています。 現状の問題点 開発を始めたばかりということもあり、いくつかの問題を抱えています。 OpenAPI上の各schemaのpropertyがActiveRecordのassociatonなのかわからない 上記の問題の解決のためにassociationであっても、シリアライザのattributes定義を使っているため循環参照による無限ループに陥る場合がある まず1点目 例えば上記ユースケース内で紹介しているモデルはすべて ActiveRecordのassociation として関係性が明示されていますが OpenAPIの定義からはその関係性がassociationなのか、単なるクラスのメンバ変数としてアクセスするのかを知るすべはありません。 そのためシリアライザ内で has_one , has_many の定義は使用しておらず 全て attributes として定義し $ref で参照しているクラスのシリアライザで初期化した値を返すメソッドを定義しています。 これによりassociationかどうかを意識せずシリアライザを扱うことができるようになりました。 ( --template オプションにより自作のテンプレートを使うこともできます。 参照: Use original template ) しかし2点目 associationでの対応を諦めたことにより has_one <-> belongs_to な関係性のモデルのシリアライザ生成時した場合、循環参照が生まれてしまいました。 例えば Profile モデルは belongs_to :user な関係にありますが これをschema定義上のProfileのpropertyとして定義してしまうと実行時にお互いのシリアライザを呼び合ってしまい循環参照から抜けられない状態になります。 # profileのschemaにuserへの参照を追加 profile : type : object properties : user : $ref : '#/components/schemas/user' description : type : string class UserSerializer < ActiveModel :: Serializer attributes :name , :profile , :tweets # Profileをシリアライズ def profile ProfileSerializer .new(object.profile) end # ..略.. end class ProfileSerializer < ActiveModel :: Serializer attributes :user , :description # Userをシリアライズしてるので循環参照を引き起こす def user UserSerializer .new(object.user) end # ..略.. end ActiveRecordのassociationが前提であれば Controllerで includeオプション を渡すことによりこの問題は回避可能です。 しかし先述の通りこのgemではschemaのpropertyがassociationなのか判定できず 全てのpropertyをシリアライザのattributesとして実装しているため、今回の問題を引き起こしてしまいます。 ※回避する方法をご存知の方がいればこっそり教えていただけると幸いです。 おわりに 開発の経緯からサンプル実装までご紹介させていただきました。 実際に現場のアプリケーションで導入を検討しているので、これからProduction環境にのせるにあたってgemの改修をしていく予定です。 また弊社では「レスポンスに型をもたせる」という文脈でGraphQL, gRPCなどの技術の採用について普段から議論しています。 RESTにとらわれずAPIを開発したい方はぜひ下のリンクから応募して、ぜひ一度オフィスに来ていただければと思います。
こんにちは、最近のマイブームは マヌルネコ動画 な新事業創造部バックエンドエンジニアの塩崎です。 今回のテックブログでは、以前にDigdagを紹介した記事の続編として、DigdagをHA構成にするためのTipsなどを紹介します。 Digdagとは Digdagはワークフローエンジンと呼ばれるソフトウェアです。 複数個のタスク間の依存関係からなるワークフローを定義し、そのワークフローの実行及び管理を行います。 この説明だけですと、何が便利なのかいまいちピンとこない方が多いかと思います。 ですが、かゆいところに手が届く便利ソフトウェアです。 具体的なかゆいところの紹介は以前にDigdagを紹介した記事の前半部分に書かれています。 Digdagを使用したことのない方はこちらを読んでから本記事を読み進めると理解しやすいかと思います。 tech.starttoday-tech.com さて、前回の記事ではDigdagを使うメリットの1つとしてHA構成を紹介しましたが、それを実現するための具体的な設定などについては紹介していませんでした。 今回はDigdagでHA構成を実現するために必要な構成要素や設定ファイルなどを紹介します。 HA構成にするための知識 DigdagをHA構成にするためには digdag server コマンドで起動しているプロセスが担っているいくつかの役割を理解することから始めると分かりやすいです。 以下でそれぞれのコンポーネントの説明をします。 API server このコンポーネントはHTTPサーバーとして動作し、以下の機能を提供します。 タスクの状態をブラウザから確認できる機能 digdag push などのdigdagのクライアントモードのコマンドを受け付けるためのREST API これはdigdag serverを起動した時に必ず有効化されます。 Agent Agentは実際にタスクの実行を行う部分です。タスクキューからタスクを取り出し、実行をします。 信用できない環境で動かすことも考慮されているため、この処理を行うスレッドはワークフロー情報が格納されているDBと直接通信をしません。 Workflow executor Workflow executorはワークフローの状態を監視し、次に実行するべきタスクをタスクキューにプッシュします。 Schedule executor Schedule executorはスケジュール実行が設定されているワークフローを監視し、設定された時刻になったらワークフローを実行状態にします。 一部の機能の無効化 これらの機能の一部は digdag server 起動時にコマンドにオプションを渡すことによって無効化できます。 指定するオプション API server Agent Workflow executor Schedule executor なし ○ ○ ○ ○ --disable-executor-loop ○ ○ ☓ ☓ --disable-local-agent ○ ☓ ○ ○ --disable-executor-loop --disable-local-agent ○ ☓ ☓ ☓ なお、これらのより詳細な説明はDigdag公式ドキュメントの中の以下の部分にかかれています。 Internal architecture 構成 HA構成のときの典型的なシステム構成図を以下に示します。 上で紹介した4つの機能のすべてがいずれかのサーバーで有効になっています。 以下ではそれぞれの構成要素について説明します。 PostgreSQL ワークフロー定義やタスクキューなどはPostgreSQLに保存されます。 システム全体をHA構成にする場合には当然ここもHA構成にする必要があります。 今回構築したシステムではAmazon AuroraのPostgreSQL互換モードをMultiAZ構成にすることによって、HA構成としました。 API Server API Serverはそれ専用のインスタンスを用意し、ロードバランサーの後段に2台を配置しました。 これらのサーバー上でのタスクの実行を止めるために、 --disable-executor-loop --disable-local-agent のオプションを指定しています。 また、ここではタスクの実行を行わないため、小さなEC2インスタンスを用いています。 Agent + Workflow executor + Schedule executor Agent、Workflow executor、Schedule executorの機能は同一のサーバーに載せています。 digdag server を起動する時に、 --disable-* オプションを指定せずに起動しています。 タスクの実行はこれらのサーバーのみで行われます。 実際には、これらもAPI Serverとしての機能も有していますが、HTTPリクエストを受けることはありません。 タスクの実行ログの場所について digdag serverを1つのサーバーだけで運用する場合、タスクの実行ログ(タスクが標準出力に書き出した内容)をサーバー上のローカルストレージに保存することが多いかと思います。 しかし、上で紹介したような構成を取る場合、全てのサーバーが読み書きできる場所に実行ログを配置する必要があります。 その1つの方法は、NFSなどの方法を使いネットワーク内の全てのDigdagで同じディレクトリを共有する事です。 また、Digdagはaws S3に実行ログを保存することも出来るので、今回はこちらを採用しました。 以下のような設定ファイルを全てのDigdag serverに読み込ませることによって、実行ログがS3に保存されます。 direct_downloadオプションはログをS3から直接ダウンロードするか否かを指定するためのオプションです。 log-server.type=s3 log-server.s3.bucket=<バケット名> log-server.s3.path=<ログを配置する場所のパス> log-server.s3.direct_download=false # S3から直にダウンロードする場合はtrueにする HA構成にしたときにハマったこと 構成例の次に、HA構成にすることによって発生した問題とその対処法を紹介します。 ローカルストレージのファイルの扱い DigdagをHA構成すると一般的にサーバー台数が2台以上になるため、1つのワークフローに属するタスクたちが複数台のサーバーで実行されることがあります。 そのため、タスク間でのファイルの受け渡しにサーバーのローカルストレージを使用すると、ファイルが見つからずエラーになることがあります。 +task1: sh>: echo 'hoge' > /tmp/hoge.txt +task2: sh>: cat /tmp/hoge.txt # ファイルが見つからない場合がある この問題を解消するためには、サーバーのローカルストレージを介したデータの受け渡しをなくす必要があります。 ローカルストレージの代わりにS3やDBなどの全サーバーから参照することのできるストレージを使うことによって、問題に対処をしました。 PostgreSQLのコネクション数 DigdagがPostgreSQLと接続する時に使用しているコネクションが多すぎると、Digdagの起動時にPostgreSQLとの接続を確立できないことがありました。 これはPosgreSQL側が受けることのできるコネクション数の上限に達すると発生する現象です。 コネクション数のデフォルトはCPUコアの数*32とかなり多めになっているので、以下のようにして、コネクション数を絞って問題に対処しました。 database.maximumPoolSize=32 サーバーが落ちた時の挙動 HA構成をとっているときにサーバーが突然死したときの挙動がどうなるのかの検証を行いました。 以下のようなタスクを実行し、sleep 10を実行している最中にDigdagプロセスを kill -9 で落としてみました。 timezone: UTC +task1: sh>: "echo task1 start && sleep 10 && echo task1 end" kill -9 を行ったdigdagプロセスのログは以下のようになり、task1の実行途中でサーバーが突然死をしたような挙動になっています。 2018-06-15 13:50:24 +0900: Digdag v0.9.25 2018-06-15 13:50:25 +0900 [INFO] (main): secret encryption engine: disabled 2018-06-15 13:50:25 +0900 [INFO] (main): XNIO version 3.3.6.Final 2018-06-15 13:50:25 +0900 [INFO] (main): XNIO NIO Implementation Version 3.3.6.Final 2018-06-15 13:50:25 +0900 [INFO] (main): Starting server on 0.0.0.0:65434 2018-06-15 13:50:25 +0900 [INFO] (main): Bound on 0:0:0:0:0:0:0:0:65434 (api) 2018-06-15 13:50:26 +0900 [INFO] (0042@[0:ha_sample]+sample^failure-alert): type: notify 2018-06-15 13:50:34 +0900 [INFO] (0042@[0:ha_sample]+sample+task1): sh>: echo task1 start && sleep 10 && echo task1 end task1 start # ここでdigdagプロセスに対してkill -9を行う。プロセスが突然死ぬので、これ以降のログは無い。 このワークフローの実行状況をweb UIで確認すると、タスクが未だ実行中という状態になっています。 そして、約5分後になると別のdigdagプロセスでタスクが実行され、ワークフローの実行が成功しました。 2018-06-15 13:55:37 +0900 [INFO] (0042@[0:ha_sample]+sample+task1): sh>: echo task1 start && sleep 10 && echo task1 end task1 start task1 end Agentはキューからタスクを取り出すときにqueued_task_locksテーブルの lock_expire_time に現在時刻から5分後のUNIX timeを書き込みます。 Agentは1分毎にこの値を現在時刻の5分後に設定するため、Agentが生きている限りは 現在時刻 < lock_expire_time になります。 一方、Agentが死んだ場合には lock_expire_time の更新がストップするため、約5分後になると現在時刻 > lock_expire_time という状態になります。 この状態のタスクの存在を他のAgentが検知することによって、突然死したAgentが担当していたタスクを他のAgentが代わりに実行するという機能が実現されています。 この動作はタスクのリトライ機能とは関係ないため、リトライ回数を設定していないタスクに対しても行われます。 このような内部実装になっているため、Digdagのワークルフローを書くときには可能な限り各タスクを冪等にするべきです。 オートスケーリングとの組み合わせ HA構成を組んだことによってオートスケーリングによるスケールアウトが簡単に行えるようになったので、その紹介もします。 構成図中でタスクを実行する役割を持っているEC2インスタンスたちに対してAutoScalingGroupの設定を行い、サーバーの起動時にDigdagが自動的に起動するよう設定します。 そして以下のようにすることによって、効率よく複数個のタスクを実行できます。 なお、Agentのサーバー台数をゼロにしてしまうとその後にサーバー台数を増やすためのリクエストを実行することすらできなくなりますので、注意が必要です。 +scale_out: sh>: autoscaling.sh 10 +load_tables: for_each>: table: ["table1", "table2", "table3", ... "table100"] _parallel: true _do: call>: load_table.dig +scale_in: sh>: autoscaling.sh 1 #!/bin/bash REGION='<region>' AUTO_SCALING_GROUP_NAME='<auto scaling group name>' aws --region $REGION autoscaling set-desired-capacity --auto-scaling-group-name $AUTO_SCALING_GROUP_NAME --desired-capacity $1 まとめ 今回はDigdagをHA構成で構築する方法を紹介しました。 ワークフロー管理ツールはDigdag以外にもAirflowやLuigiなどいろいろあります。 それら他のツールと比べるとDigdagはHA構成を簡単に組むことが出来るように最初から考えられている印象を受けます。 バッチをスケジュール実行するサーバーは多くのシステムでSPOFになりやすいので、HA構成にすることによって可用性をより高めましょう。 スタートトゥディテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください!
こんにちは、バックエンドエンジニアの田島( @katsuyan121 )です。 5/31〜6/2にかけて仙台で開催されたRubyKaigi2018に、スタートトゥデイテクノロジーズから5人が参加しました。 今年のRubyKaigiは3日間で50を超える講演があり、参加者も1000人を超える大変大規模なカンファレンスでした。たくさんの講演の中で、スタートトゥデイテクノロジーズのエンジニアが興味を持ったものを、この記事でいくつか紹介します。 また今回スタートトゥデイテクノロジーズはスポンサーブースを出展したので、そこで得られたことを共有します。 セッション Proverbs(Matz) Rubyのお父さんであり、弊社スタートトゥデイテクノロジーズの技術顧問でもある Matz さんの発表です。 今回の発表では3つの諺を例にお話を展開されました。 名は体を表す 「名は体を表す」という通り、名前は重要であるというお話でした。特にソフトウェアには実態がないため、名前付けが重要になるとのことでした。 名前には2種類あり、1つは振る舞いに対する名前でもう1つがプロジェクトに対する名前です。 振る舞いに対する名前付けが難しいという場面はよく訪れます。しかし名前付けが難しいということはその概念を十分に理解していないことであるとのことでした。 プロジェクト名という側面では、Rubyのような求心力のある名前が好ましいとの話でした。また、今の時代だとgooglabilityが非常に大切であるとも語られていました。それを解決するのためにTensorFlowのように単語を組み合わせたり、Jupyterのようにスペルをいじるのは良い手段であるとのことでした。 時は金(価値)なり 時は金(価値)なりが示すように、時間を有効活用することが非常に大切であるとの話でした。開発における時間には開発時間と実行時間という2つの側面があります。Rubyでは主に開発時間という部分に重きをおいてきました。開発時間を短くすることで人件費の節約など直接的に費用削減につながるということでした。 また、実行時間についてもRubyにJITを導入するなど改善を常に行っています。実行時間を短縮することでサーバの台数が減るなど直接的に費用を削減できると例をあげて説明していました。 塞翁が馬 塞翁が馬のように、良いこと悪いことに一喜一憂するなというお話でした。例えば、静的型が流行っているからと言ってRubyには絶対に静的型は導入しないとのことでした。長い目で見た時に安易に静的型を導入しないほうが有利になるとMatzさんは考えているそうです。 コスチュームスポンサー 今回のRubyKaigiではMatzさんには事前にZOZOSUITを着ていただき、測定結果を元にMatzさんピッタリサイズのジーンズを履いていただきました。 またkeynoteの最後でコスチュームスポンサーとしてスタートトゥデイテクノロジーズを紹介していただきました! Improve Ruby coding style rules and Lint(Koichi ITO) RuboCopのコミッタである Koichi ITO さんによる、RuboCopのStyleとLintについての発表でした。 Styleはコーディングスタイルのことを表します。コーディングスタイルは様々な文化から形成され、会社やプロジェクトによって異なったルールが存在します。RuboCopのデフォルトのStyleは Ruby Style Guide に従って作成されているそうです。Lintは潜在的なバグを指摘するもので、コーディングスタイルとは違い言語仕様的に適切でない箇所を指摘します。 Styleについては、会社やプロジェクトによってコーディングスタイルは違うものなので、積極的にデフォルト設定からカスタマイズしてほしいとのことでした。 また、Koichi ITOさん自身がRuboCopのルールをどのような経緯で新しく追加したかについてもお話されていました。会社のコードレビューを受けて気づいたルールや、RailsのカスタムコップからRuboCopに採用したルールもあるとのことでした。 発表を通して、自分たちが使ってよかったものを他の人にも広めていこうというKoichi ITOさんの意思がとても伝わってきました。 Architecture of hanami applications(Anton Davydov) Anton Davydov さんによる、hanamiを長期的にメンテナンスしていくうえで採用したアーキテクチャを振り返るという発表でした。 ファットモデルなコードから段階的にビジネスロジックを切り離していく方法としてhanamiに採用されている手法の説明がありました。具体的にはInteractor(Service object), Repositoryや、Event Sourcingなどが紹介されていました。 また、Event SourcingのPoC実装としてhanami-eventsというgemについての紹介もありました。 実際にInteractorやRepositoryを入れた意図やhanamiでの実装例が示されていました。 様々なデザインパターン紹介の最後に『必要かわからないときは使わないでおく』ということを書かれていた事は、エンジニアとしてすごく共感できました。 ビジネスロジックの切り分けはhanamiだけでなく、webアプリケーション開発をしている人全体の問題でもあります。良さそうなパターンは実際に検証して自分たちのプロジェクトに取り入れていこうと思います。 bancor: Token economy made with Ruby(Yuta Kurotaki) Yuta Kurotaki さんによるBancor Protocolというスマートトークンを発行するためのプロトコルをRubyで実装したという発表でした。ちなみにBancorはバンコールと読みます。 ブロックチェーン関連の実装はPythonやGo・JavaScriptで実装されているものが多いですが、Rubyで実装されているものがあまりなく実装してみたということでした。 実際のRailsアプリケーションに組み込んでbancor protocolを利用したデモでは、流動的に価格が決定されているところを見ることができました。 Ruby実装が少ないブロックチェーンを実装するという試みは、新しいものに挑戦していくという部分ですごく共感を得られました。 参考: bancorのリポジトリ Hijacking Ruby Syntax in Ruby(joker1007/Satoshi "moris" Tagomori) Hijacking Ruby Syntax in Ruby from SATOSHI TAGOMORI joker1007 さん・ Satoshi "moris" Tagomori さんによるメタプログラミングを最大限に活用しようという発表でした。メタプログラミングというRubyの黒魔術を最大限に活用して、Rubyの文法が変わったのかと錯覚するレベルの機能が示されていました。 肝になる機能は Binding#local_variable_set と Tracepoint で、これらの機能を利用していたるところにフックを仕込みます。そうすることで、いたるところの変数を書き換え邪悪な機能を実現しているとのことでした。 Javaでおなじみのfinal、override、abstractをRubyでも実現し、なおかつこれらはクラス定義時点でエラーを出せるという点に感動しました。 その他にPythonでおなじみのwithや、Goでおなじみのdeferなどと同等な機能をRubyのメタプロだけで実現している例が示されました。 Rubyはメタプロ文化があるとはいえ、ここまでのことができたのかという驚きがありました。 TTY - Ruby alchemist’s secret potion(Piotr Murach) Piotr Murach さんによるTTYというCLI作成用のライブラリを作ったという発表でした。 最初に以下のようにwebのアーキテクチャと比較している点が興味深かったです。 cli - router command - controller stdin - request stdout - response template - view TTYには複数のプラグインがあり、プラグインを導入することでCLIをリッチにできます。 実際にデモではTTYを利用したCLIが示されており、ギュインギュイン動いていて感動しました。 TTYは簡単にリッチなCLIツールを作るにはすごくいいライブラリだと思います。 またアーキテクチャも上に示した通りwebエンジニアにとってすごくわかりやすい構成となっていて、複雑なCLIツールを作るのにも向いているライブラリであると感じました。 参考: TTYリポジトリ Firmware programming with mruby/c(Hitoshi HASUMI) Hitoshi HASUMI さんによる、mruby/cを使って日本酒を醸す(かもす)というお話でした。 日本酒の製造にあたっては麹(こうじ)や醪(もろみ)の温度管理が必要で、そのために温度センサーのデータを収集するためのファームウェアをmruby/cで作ったそうです。 こういう用途ではラズパイの使用も考えられます。しかし冬でも35度である麹室での運用を考えた時に、低消費電力かつ低発熱なことを重視しARM Cortex-M3が搭載されたPSoc5LPマイコンにしたそうです。 このシステムは2018年1月から実際に稼働しているそうです。 mrubyとmruby/cはどちらとも軽量Rubyですが、mruby/cの方がより少ないメモリでも動くようにできているとのことでした。 大きな違いとしてmrubyはRTOS(RealTimeOS)の上で動くことが前提ですが、mruby/cはRTOSなしでその下に直接ハードウェアがあるという構成になっている点です。 そのためにHAL(ハードウェア抽象化レイヤー)があり、ソースコードの中のその部分を読むと面白いのではとのことでした。 開発中に困ったこととしては、デバッガでのステップ実行ができないのでprintデバッグしかできなかったとのことでした。また、 Array#each が未実装なので while と break で代替しなければならないという点でした。このようなことから、mruby/cはまだまだ成長の余地があって楽しみです。 また発表されていた十旭日は、東京では以下の酒販店で取り扱われているそうです。 生原酒と火入れ:新宿の三伊井上酒店 主に生原酒:笹塚のマルセウ本間商店 主に火入れ:中目黒の出口屋、練馬の大塚屋 定番品種:横浜君嶋屋の銀座店、恵比寿店 十旭日のなかでもmruby/cで醸されたお酒は29BY(平成29酒造年度の醸造)のものだそうです。 https://twitter.com/hasumikin/status/1006899085570334727 Ruby code from the stratosphere - SIAF, Sonic Pi, Petal(Kenichi Kanai) Kenichi Kanai さんによる、mrubyを使ったlivecodingの発表でした。 ここでいうlivecodingとは、「コンピュータの言語であるプログラムコードを直接操作することで、さまざまな音や映像をリアルタイムに生成する即興演奏の方法」とのことでした。 Petalというプログラミング言語(実際はRubyのDSL)を使うことで、シンプルな記法で多様な音を出すことができるそうです。 気象観測用の気球にラズパイとセンサーを載せて、上空でセンサーから取得したデータをもとにPetalのコードを生成します。そこから音を生成することによって、成層圏からの音楽を地上に届けるということを札幌国際芸術祭でやったそうです。 最初は気球に搭載したラズパイで音を生成してそれを地上に無線で届けるということをしていたそうです。しかしそれだとノイズが多くのってしまったので、2回目からは音の生成を地上でやるという方法で問題解決をしたらしいです。 発表の最後ではこの時に取得したセンサーデータのログから音楽を生成し、会場のみんなで成層圏からの音楽を楽しみました。 参考: スライド スライド Petalリポジトリ Deep Learning Programming on Ruby(Kenta Murata/Yusaku Hatanaka) Kenta Murata さん・ Yusaku Hatanaka さんによるRubyでDeep Learningをできるようにする試みについての発表でした。 このセッションは2つ分かれており、前半では「mxnet.rb」のについての発表。後半は「Red Chainer」の発表となっていました。 1つめのmxnet.rbのお話は、MXnetのRubyバインディングを開発したというお話でした。MXnetはDeepLearning用のライブラリで複数言語のバインディングが存在するが、Rubyには対応していなかったとのことで開発を初めたようです。 MXnet自体はKerasのbackendにも利用されています。TensorFlowでのバックグラウンドと比較して良いパフォーマンスであったことが確認されており、有用なライブラリであるとのことでした。 ONNXにも対応しており、一度MXnetで作ったモデルを他のONNXに対応したDeepLearningライブラリで利用できるそうです。また逆に他のライブラリで作成したモデルをMXnetで利用できるとのことでした。 mxnet.rbは絶賛開発中で、プルリクをお待ちしておりますとのことでした。 2つ目のRed ChainerはChainerをRuby実装として実現するというお話でした。こちらも絶賛開発中とのことで、まだPythonチックなRubyを書かないと行けない部分があったり、CPUにのみしか対応していないとのことでした。しかし、今後はもっとRubyらしく記述できるようにし、後述するCUMOを利用することで近々GPUに対応予定とのことでした。 Red ChainerはRed Data Toolsというコミュニティに属しています。Red Data Toolsは理念が素敵で誰でも気軽に参加できそうなコミュニティだと感じました。 このように、RubyでDeepLearningをするという試みが活発になってきていることが分かりました。Rubyで楽しくDeep Learningできるのはすごく楽しみです。 参考: Yusaku Hatanakaさん振り返りブログ mxnet.rbリポジトリ redchainerリポジトリ Fast Numerical Computing and Deep Learning in Ruby with Cumo(Naotoshi Seo) Naotoshi Seo さんによる、NumoというRubyの計算用ライブラリをCUDAに対応させようという試みの発表でした。 Numo自体はRubyの計算用ライブラリですが、CPUにのみ対応しています。そこで、Numo互換のCUDA対応ライブラリであるCUMOを作成したそうです。 CPUを前提とした実装をGPUに対応した時、GPUによる並列計算どのように計算させるのかということがわかりやすくまとまっていました。CUDAプログラミングをしたことがない人でも理解できるような内容となっていました。 実際にCUMOを利用する場合、ソースコード中のNumoをCumoに全置換するだけでGPUに対応できるのは感動しました。 またこのプロジェクトはRuby Associationに採択されたプロジェクトだそうです。個人的にやるプロジェクトでもなにかしらの締切が設けられることで、それがマイルストーンになるということも語られていました。 参考: 発表者振り返りブログ CUMOリジトリ NUMOリポジトリ スポンサーブース アンケート 1日目 お題 初日のお題は、「ブロック開始の中括弧とブロックパラメーターの間にスペースを開けるか」というものでした。 このお題の理由としては、技術顧問のMatzさんとの月イチのミーティングでRubCopの話になったことが切っ掛けです。RuboCopのデフォルトではスペースを開けるようになっています。しかし、Matzさんはスペースを開けたくないとのことだったのでこのお題を提示しました。 結果 考察 結果はスペースを開ける派が多数となりました。スペースを開けるという理由として多く挙げられたのはやはりRuboCopに指摘されるからというのが多かったです。また、中括弧でなく do end で記述する場合は do とブロックパラメータの間にスペースを開けるから統一するためとの解答もいただきました。 スペースを開けない派の意見としては、ArrayやHashを作るときにはカッコのあとにスペースを開けないからそちらと統一するためなどの意見をいただきました。 「Improve Ruby coding style rules and Lint」で示されていたように、styleは文化によって違います。そのためプロジェクト内で記法が統一されていれば、どちらでも良いと考えました。 2日目 お題 2日目のお題としては、「クラスメソッドの定義をするときに self.クラスメソッド名 と書くか、 class << self で特異クラスをオープンするのか」というものでした。このお題の意図としてはまたもRuboCopでは後者を使うように示されますが、前者のほうがクラスメソッドとしてわかりやすいのではないか? という意見が社内であったことからこのお題を提示しました。 結果 考察 結果は class << self で特異クラスをオープンするほうが多数となりました。特に多かった意見としては、クラスメソッドが1つの場合は self.クラスメソッド名 、2つ以上の場合は class << self を使うというものでした。 また、 class << self を使う理由としてはprivateクラスメソッドがこっちの書き方でしか書けないから、grepする時に探しやすいからというものがありました。 self.クラスメソッド名 を使う理由としてはパット見てクラスメソッドかどうかがわかりやすいからという意見が多かったです。 このアンケートは、一日目よりも迷う人が多かった様子でした。このケースはプロジェクトで柔軟に対応していくのがいいと考えます。 3日目 お題 最終日のお題は「文字列の配列を定義するときに、%記法を使うか」というものでした。このお題に関してもRuboCopでは前者が示されます。後者のほうが文字列の配列としてシンプルでわかりやすいという意見があったことからこのお題を提示しました。 結果 考察 結果は%記法を使うという方が多数となりました。%記法を使う理由としては、単純にタイプ数が減る。文字列以外が配列の中に入らないなどが挙げられました。ここでもRuboCopに指摘されるからという意見もありました。 %記法を使わない理由としては、%の次に何を書けばいいか忘れてしまう。Ruby以外の言語から来た人でもわかりやすい。文字列の配列であることが直感的にわかりやすということが挙げられました。 これについても、それぞれの利点をちゃんと知った上であれば好みのスタイルで書けばいいと考えました。 gem お題 最終日には好きなgemのアンケートを取りました。 結果 考察 今回の好きなgemアンケートでは pry や byebug など手元で動かすgemが多く挙げられました。 実際に自分が助けられたり便利だなって感じたgemを好きだと感じるのかなと思います。 知らないgemも多く挙げられすごく勉強になりました。ぜひアンケート結果から知らないgemを調べてみてください。 まとめ やはりRubyKaigiは日本一のテックカンファレンスだなと改めて実感しました。Rubyコミッタ級のエンジニアがゴロゴロ転がっているカンファレンスは他にはないです。また来年のRubyKaigiが楽しみです! 今回初めてスポンサーブースを出展させていただきました。ブースを出展することによってRubyのお話をたくさんの方とできました。Rubyコミッタの方たちも立ち寄って頂き気軽に話すことができたのはすごく勉強になりました。 また、今回のRubyKaigi参加に関する費用はすべて会社が負担してくれました。さらに出張手当をもらえたうえに、休日出勤分の振休まで取得させてくれました。 来年のRubyKaigiは福岡で開催される予定です。一緒にいこう! という方がいましたら、ぜひ以下のリンクからご応募ください。 https://www.wantedly.com/companies/starttoday-tech/projects おまけ RubyKaigi参加メンバー 弊社技術顧問であるMatzさんとの記念撮影 仙台は美味しいものが多すぎてお腹がいっぱい 牛タン からの牛タン からの牛タン うに 海鮮丼 東北大生名物さわき RubyKaigi最終日の次の日に猫島こと田代島にお出かけ いざ猫島 ねこにちょっかい 猫になるリーダー 猫神社 ねこー