TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは。 最近愛猫にトイレの出待ちをされるようになった、品質管理部エンジニアリングチームの高橋です。 品質管理部ではアプリの自動テストを主に担当しております。 本記事はAI(Artificial Intelligence, 人工知能)を活用したテスト自動化の奮闘記となっております。 内容的にはお世辞にも先進的と言えるものではありませんが、是非あたたかい目で見て頂けると幸いです。 AI時代におけるソフトウェアテスティング 言うまでもなく、今やAIは身近な存在となっています。 ソフトウェアテスティング業界も例外ではなく、既にAIを用いたテストツールやサービスが公開されている状況です。そしてその流れは今後更に大きくなり、AIによる自動テストが当たり前の時代がやってくると予想されます。 私達もその波に乗り遅れないよう、自動テストにAIを導入することを目標に掲げ、AIの調査・検討を開始しました。 現在、品質管理部では、モバイルアプリの回帰テストを全て自動で行なっています。 iOS/Androidそれぞれのテストフレームワークを用いて、対象アプリ毎にテストスクリプトを実装し、日々メンテナンスを行なっております。 スクリプト形式の自動化の為、コーディング通りにテストが行われる単純なものではありますが、リリース前の速やかな動作確認&リグレッションテストに一役買っています。 そしてここにAIの力が加われば、伝統的なスクリプト形式のテスト自動化の枠を超えることができるのではと考えました。 画像認識AIの活用 現在のスクリプト形式自動テストの悩ましい点の1つに、「 人間の認知・判断 」が必要な項目は自動化を行うことができないということが挙げられます。 例えば、「キッズ」で絞り込んだ際に、「キッズの画像」が表示されていることを確認するテストです。このような人間にしか判断できない項目はテスターが手動で確認する必要がありました。しかし既にディープラーニングによる画像認識の精度は十分に人間に追いついています。よってまずはAIの得意分野である、画像認識を利用した自動テストを実装することに決定しました。 また現在のスクリプト形式のコードはアプリのUIヒエラルキーを1階層ずつ辿っていくような書き方であり、自然とコードが長くなってしまいがちです。 WEARのUIヒエラルキー 画像認識を用いて「この画像を探してタップする」という書き方で統一できれば、1つ1つのテストコードがよりシンプルになり、メンテナンスがしやすくなる可能性もあります。 WEAR-画像認識 WEAR画面上の投稿コーディネートを認識させる AIのライブラリはTensorFlowを利用してみることにしました。 オープンソース/ライブラリが豊富/知識さえつけば自由度が高いという魅力があるらしく、公式チュートリアルも充実している為、こちらを採用することにしました。 https://www.tensorflow.org/ 本来AIが物体認識を行うには機械学習でモデルを学習させるプロセスが必要になりますが、TensorFlowは学習済みのライブラリを提供しています。WEARアプリは"人間"が写っているコーディネート画像が表示されている為、今回は物体認識の学習済モデルを利用します。 1.TensorFlow実行環境の構築 まずTensorFlow Object Detection APIのインストールが必要ですが、ここでは詳細手順は省略します。下記の手順通りに実行すれば、問題なくインストールできると思います。 https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/installation.md 次に、学習済のTensorFlow Object Detectionモデルをダウンロードしておく必要があります。今回は下記の物体検出モデルを使用しています。 http://download.tensorflow.org/models/object_detection/ssd_inception_v2_coco_2017_11_17.tar.gz その他の物体検出モデルは、次のURLからダウンロードできます。 https://github.com/tensorflow/models 2.モデルダウンロード、メモリへロード ここからは、実際のテストコードとなります。 WebCameraに写っている物を認識するサンプルコードを参考に致しました。 https://github.com/tensorflow/models/tree/master/research/object_detection # ダウンロード&保存済みのモデルとラベル・マップのパス設定 PATH_TO_CKPT = 'ssd_inception_v2_coco_2017_11_17' + '/frozen_inference_graph.pb' PATH_TO_LABELS = os.path.join( 'data' , 'mscoco_label_map.pbtxt' ) # モデルをメモリへロード / 下記、TensorFlowチュートリアルより detection_graph = tf.Graph() with detection_graph.as_default(): od_graph_def = tf.GraphDef() with tf.gfile.GFile(PATH_TO_CKPT, 'rb' ) as fid: serialized_graph = fid.read() od_graph_def.ParseFromString(serialized_graph) tf.import_graph_def(od_graph_def, name= '' ) 3.ラベル、マップをロード # 下記、TensorFlowチュートリアルより NUM_CLASSES = 90 label_map = label_map_util.load_labelmap(PATH_TO_LABELS) categories = label_map_util.convert_label_map_to_categories( label_map, max_num_classes=NUM_CLASSES, use_display_name= True ) category_index = label_map_util.create_category_index(categories) 4.テスト対象のAndroid WEARアプリを起動 # am startコマンドでMainActivity起動 subprocess.Popen( "adb shell am start -n com.starttoday.android.wear/.main.MainActivity" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() 5.画像認識画面取得 # adbコマンドでscreenshot実行 TEST_IMAGE_PATH = <ホストPC保存先+ファイル名> subprocess.Popen( "adb shell screencap -p /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb pull /sdcard/image.jpg TEST_IMAGE_PATH" , stdout=subprocess.PIPE).wait() subprocess.Popen( "adb shell rm /sdcard/image.jpg" , stdout=subprocess.PIPE).wait() 6.物体検出処理開始 # 分析対象画像の配列化 image = Image. open (TEST_IMAGE_PATH) image_np = load_image_into_numpy_array(image) image_np_expanded = np.expand_dims(image_np, axis= 0 ) # 1つの画像に対する物体検出 # run_inference_for_single_image()は、Tensorflow公式Tutorial提供関数 # 画像より検知した物体データがoutput_dictに保存 output_dict = run_inference_for_single_image(image_np, detection_graph) # run_inference_for_single_imageの処理内容は下記の通りである def run_inference_for_single_image (image, graph): with graph.as_default(): with tf.Session() as sess: # Get handles to input and output tensors ops = tf.get_default_graph().get_operations() all_tensor_names = {output.name for op in ops for output in op.outputs} tensor_dict = {} for key in [ 'num_detections' , 'detection_boxes' , 'detection_scores' , 'detection_classes' , 'detection_masks' ]: tensor_name = key + ':0' if tensor_name in all_tensor_names: tensor_dict[key] = tf.get_default_graph().get_tensor_by_name( tensor_name) if 'detection_masks' in tensor_dict: # The following processing is only for single image detection_boxes = tf.squeeze(tensor_dict[ 'detection_boxes' ], [ 0 ]) detection_masks = tf.squeeze(tensor_dict[ 'detection_masks' ], [ 0 ]) # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size. real_num_detection = tf.cast(tensor_dict[ 'num_detections' ][ 0 ], tf.int32) detection_boxes = tf. slice (detection_boxes, [ 0 , 0 ], [real_num_detection, - 1 ]) detection_masks = tf. slice (detection_masks, [ 0 , 0 , 0 ], [real_num_detection, - 1 , - 1 ]) detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks( detection_masks, detection_boxes, image.shape[ 0 ], image.shape[ 1 ]) detection_masks_reframed = tf.cast( tf.greater(detection_masks_reframed, 0.5 ), tf.uint8) # Follow the convention by adding back the batch dimension tensor_dict[ 'detection_masks' ] = tf.expand_dims( detection_masks_reframed, 0 ) image_tensor = tf.get_default_graph().get_tensor_by_name( 'image_tensor:0' ) # Run inference output_dict = sess.run(tensor_dict, feed_dict={image_tensor: np.expand_dims(image, 0 )}) # all outputs are float32 numpy arrays, so convert types as appropriate output_dict[ 'num_detections' ] = int (output_dict[ 'num_detections' ][ 0 ]) output_dict[ 'detection_classes' ] = output_dict[ 'detection_classes' ][ 0 ].astype(np.uint8) output_dict[ 'detection_boxes' ] = output_dict[ 'detection_boxes' ][ 0 ] output_dict[ 'detection_scores' ] = output_dict[ 'detection_scores' ][ 0 ] if 'detection_masks' in output_dict: output_dict[ 'detection_masks' ] = output_dict[ 'detection_masks' ][ 0 ] return output_dict 7.物体検出結果の解釈 output_dictの出力結果の項目は次のようになっており、検出物体の詳細情報が保存されています。 - class 認識結果 - prediction 検出精度 - boundingbox 検出された座標位置 8.検出結果の判定と画面操作コード実行 # output_dictに保存している検出結果の判定処理を開始 for i in range ( len (output_dict[ 'detection_boxes' ])): # class_name = 検出物体名(クラス名、例:person, car, bag, cellphone,,,) # accuracy = 検出物体の精度(例:"person"クラスである確率がXX%) class_name = category_index[output_dict[ 'detection_classes' ][i]][ 'name' ] accuracy = output_dict[ 'detection_scores' ][i] # 物体検出の精度が70%未満であれば何もしない if accuracy < 0.7 : break # 検出物体クラスが'人(person)'の場合のみ処理実行 if class_name != 'person' : break else : # 分析画像の横縦長さ:adbコマンドで検出物体のxy座標計算時に利用 width = image_np.shape[ 1 ] # Number of columns height = image_np.shape[ 0 ] # number of rows # 該当投稿の座標を指定しタップ実行 # コマンドの例:adb shell input touchscreen tap x座標 y座標 print ( "詳細画面へ移動(クリック)" ) xPosition = int (width * output_dict[ 'detection_boxes' ][i][ 1 ])+(( int (width * output_dict[ 'detection_boxes' ][i][ 3 ])- int (width * output_dict[ 'detection_boxes' ][i][ 1 ]))/ 2 ) yPosition = int (height * output_dict[ 'detection_boxes' ][i][ 0 ])+(( int (height * output_dict[ 'detection_boxes' ][i][ 2 ])- int (height * output_dict[ 'detection_boxes' ][i][ 0 ]))/ 2 ) subprocess.Popen([ 'adb' , 'shell' , 'input' , 'touchscreen' , 'tap' , str (xPosition), str (yPosition)], stdout=subprocess.PIPE).wait() # 投稿をクリック後、正常に画面遷移が行われ、コーデ詳細画面に移動しているかの確認 # WEARアプリのコーディネート詳細画面Activity名:DetailSnapActivity out = check_output([ 'adb' , 'shell' , 'dumpsys' , 'activity' , '|' , 'grep' , 'mResumedActivity' ]) if b 'DetailSnapActivity' in out: print ( "OK! 詳細画面への正常遷移" ) print ( "前の画面へ移動(キーイベントで移動、KEYCODE_BACK=4)" ) subprocess.Popen( "adb shell input keyevent 4" , shell= True , stdin=subprocess.PIPE, stdout=subprocess.PIPE).wait() time.sleep( 2 ) else : print ( "NG! 詳細画面への遷移失敗" ) 実行すると…しっかりコーディネート画像をタップしてくれました! 画像認識 コーディネート画像タップ 今後の展望 今回は学習済モデルを使用した為、予想よりも短い時間で実装までたどり着くことができました(もちろん四苦八苦しましたが)。 このチャレンジによって少しずつTestingにおける画像認識AIの活用性が見えてきた気がします。 今後は以下のような人依存のテストパターンもAIを利用して行ってみたいと考えています。 ・投稿コーディネートが性別と一致しているか ・投稿コーディネートが本当にファッション関連投稿なのか ・投稿コーディネートが該当タグ(帽子、ブランド名等)に一致しているか また将来的には、弊社アプリ向けにモデルをトレーニングしてテストを実行してみたいと思います。 以上が、AI-assistedの第一歩としてTensorFlowの学習済モデルを使用してみた体験談です。 AI門外漢/文系出身の自分としては、機械学習に関する知識が全く足りていないことを痛感しました。理想が実現するのは近い将来ではなさそうですが、日々精進していきたいと思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
アバター
こんにちは。iOS担当の荒井です。 今回はiOSチームで構築しているCI/CDのWorkflowについて紹介します。 CI/CD環境 はじめに、ZOZOテクノロジーズのiOSチームがどのようなCI/CD環境を整えているかを簡単に説明します。ZOZOテクノロジーズではZOZOTOWNやWEAR、プライベートブランドZOZOなど様々なサービスを運営しています。プロダクトによりCI/CD環境は違うのですが、多くのプロダクトでBitrise + DeployGateという構成をとっています。今回お話するWorkflowもBitriseを例に紹介していきます。 Bitrise 多くのCIサービスが存在していますが、以下の理由でBitriseを選定しています。 導入時、利用目的に対して価格面が見合っていた Xcodeバージョンアップへの対応速度 日本市場に注力 日本市場に注力していくという話もあり、今後サポートに期待が持てます。 最近では 日本語の記事 も掲載されています。 DeployGate TestFlightやFabricを使っている方も多いと思いますが、弊社のiOSチームはDeployGateを選択しています。元々CI/CDの環境が整っていないプロダクトが多かったため、環境構築を進めるにあたり、会社としてAndroidチームで運用実績があるDeployGateにしました。プロダクトをまたいでテストを行うチームが存在するため、ツールは統一しようといった理由があります。 Workflow それではWorkflowの話に移ります。 WorkflowはStepと呼ばれる定義済みスクリプトをまとめたものです。例えば「Carthageコマンドを実行する」には「Carthage」というStepを設定します。BitriseではGUIで簡単にSetpを追加し目的に応じたWorkflowを組むことができます。今回は以下のようなフローを例に、自動化する際どのようなWorkflowを組んでいるかを紹介します。 エンジニアがGitHubにコードをPush GitHubへのPushをWebhook Workflowの実行 DeployGateへのアップロード Slackへ完了通知 よくある「エンジニアがコードを書いてGitHubにPushしたら、テストが実行され、アーカイブしてQAに配布する。終わったらSlackに通知する」というものです。実運用ではPushのたびに配布をしていたら頻度が高いため、PullRequestの生成タイミングなど、目的に応じてトリガーを設定しています。 上記のようなフローを構築する場合、Bitrise上で以下のようなWorkflowが考えられます。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード 証明書のインストール Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 テストの実行 アーカイブ DeployGateへのアップロード Slack通知 しかし弊社ではこれらのStepのほとんどをfastlaneに任せています。 fastlane fastlaneはアプリの様々な作業を簡単に解決してくれるツールです。冒頭のフローを実装するのに必要となる「ビルドに必要な証明書類の管理」「DeployGateへのアップロード」などはすべてfastlaneで行なっています。fastlaneを使用すると以下のようなWorkflowになります。 SSH keyの登録 リポジトリのクローン ビルドキャッシュのダウンロード Carthageコマンドの実行 キャッシュの更新 スクリプトの実行 fastlaneの実行 fastlane以外のStepスクリプトを使用しない理由は、CIサービスの移行を容易にするためです。他のCIサービスに魅力的な機能が出たり、何かしらの理由でCIサービスを変更しなくてはいけない場合でもfastlaneに処理を寄せておけば移行がスムーズになります。Bitrise上ではCode Signingの設定もせず、出来ることは可能な限りfastlaneにしています。サンプルのフローを例にSlackへ通知するまでのfastlaneの実装を紹介します。 証明書のインストール iOSに関わる証明書などのファイルは暗号化されGitHubで管理しています。fastlaneではアクションという形で様々な機能が提供されており、証明書の管理はfastlaneの match というアクションを使って実装しています。今回は詳しく触れませんが、証明書の管理はGitHubにしておくと何かと便利です。まだすべてのプロダクトで使用しているわけではないので現在対応を進めています。 テスト テストには scan というアクションを使用します。 fastlaneの処理はFastfileという設定ファイルに記述して実装していきます。 lane :test do scan( scheme : " SCHEME-NAME " , clean : true , skip_slack : false ) end FastfileはRubyで記述していくことになりますが、ドキュメントも充実しており、Rubyに馴染みのないiOSエンジニアでも記述に困らないと思います。 Swiftでの記述 もサポートがあるため、こちらを試しても良いかもしれません。 アーカイブ アプリケーションのビルドサンプルです。ビルドには gym というアクションを使用しています。 private_lane :build do match # 証明書のダウンロード gym( scheme : " SCHEME-NAME " , configuration : " InHouse " , clean : true , export_method : " enterprise " ) end DeployGateへのアップロード DeployGateへのアップロードもfastlaneです。私たちはDeployGateを使用していますが、TestflightやFabricなどのアクションも揃っています。 private_lane :deploy_gate do | options | target = options[ :release ] ? " release " : " development " deploygate( api_token : ENV [ " DEPLOYGATE_API_TOKEN " ], user : " DEPLOYGATE-USER " , ipa : " ./ #{ target } .ipa " , message : last_git_commit[ :message ] ) end Slack通知 全員が業務でSlackを使用しているため、何かの処理が終わった場合にはSlackで通知するように組んでいます。プロダクトに影響しない処理はサービス間で同じ設定を使うこともあります。 手元のテストでSlack通知が行われないように、CI上のみで通知をすることが多いです。 before_all do ENV [ " SLACK_URL " ] = " WEBHOOKURL " end after_all do | lane | if is_ci? slack( message : " Test Successfully " ) end end error do | lane, exception | if is_ci? slack( message : exception.message, success : false ) end end Bitriseに設定するlane Bitriseに設定するlaneを作成します。 lane :deliver_qa do build deploy_gate( release : false ) end 今回はビルドとデプロイのみにしていますが、 他にもビルドバージョンやGitタグなどもfastlaneで自動化できます。 fastlane運用をしてみての利点 すでにCircleCIからBitriseに移行したサービスがありますが、基本的にfastlaneで書いてあったため移行はとてもスムーズでした。CI上の細かな設定を確認せずとも、fastlaneの設定ファイルであるFastfileを確認すれば処理の流れが把握出来るのも利点だと思います。また、fastlaneによるコードでの記述を行なっていれば、プロダクトをまたいで設定ファイルを簡単に共有できるメリットもあります。CI上でGUIによるWorkflow構築は簡単ですが、コードで記述していくメリットも十分あるように感じています。 まとめ 今回はiOSチームで構築しているWorkflowについて紹介しました。CI/CD環境は随時改善を進めており、どのチームも自動化を目指しています。まだまだ改善の余地があり今後も注力していきたいと思います。CI/CDに興味があり、自動化が好きな方下記よりご応募お待ちしております。 www.wantedly.com
アバター
ZOZO研究所の後藤です。本記事ではGoogle Cloud TPUを使った計量学習の高速化の事例を紹介します。 はじめに 深層学習を用いたプロダクトを開発・運用する上で、モデルの学習にかかる膨大な時間はボトルネックの1つです。 ファッションにおける深層学習を用いた画像認識技術にも同じことが言えます。 今回はファッションの分野において定番のタスクであるStreet2shopの課題設定に対し、Google Cloud TPUを用いて計量学習の高速化を試みます。 Street2shopは、スナップ画像から商品部分を切り出す物体検出のパートと、切り出した画像と類似した商品を検索するクロスドメイン画像検索のパートに分けられます。 今回の取り組みでは、後者のパートで利用する画像間の距離を測るためのモデルの学習の高速化を行います。 目次 はじめに 目次 Google Cloud TPUとは タスク Street2shop 計量学習 学習データ 学習 プロファイリング 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 学習結果 計算にかかる料金 モデルの活用 Shop2shop トップス アウター ドレス ボトムス スカート シューズ Street2shop スナップ1 スナップ2 まとめ 最後に Google Cloud TPUとは Tensor Processing Unit(以下、TPU)は、機械学習で頻繁に行われる行列演算を高速に処理することを目的にGoogleが開発したハードウェアです。 CPUやGPUは様々なソフトウェアやアプリケーションに対応するための汎用プロセッサとして設計されている一方で、TPUは汎用性を犠牲にして行列演算に特化した設計がなされています。 そのため、行列演算を多用する機械学習の計算にTPUを用いることによって、モデルの学習や推論にかかる時間を大幅に短縮することが可能になります。 TPUがなぜ高速に演算できるのかを理解するには、公式のブログ記事が詳しいです。 cloudplatform-jp.googleblog.com 速いことは良いことです。 学習速度が速いと、学習が収束したモデルをより短時間で獲得できるようになります。 また、限られた時間の中での試行錯誤の回数を増やすことができるため、ハイパーパラメータチューニングが捗り、最終的に得られるモデルがより良いものになる可能性が高くなります。Cloud TPUはTPUをクラウド上のインスタンスとして利用できるようにしたサービスです。TPUの強力な演算能力を時間単位で誰でも利用できます。 注意点として、TPUは設計上、得意な演算が限られるため、あらゆるモデルを高速化できるわけではありません。 適切な計算環境の選び方に関しては、以下の記事が参考になります。 cloud.google.com 例えば、以下の4つの項目に当てはまる場合、TPUによる高速化が達成可能かもしれません。 ほとんどの計算が行列計算である 学習ループにカスタムTensorFlow operationsが含まれない 学習に数週間から数ヶ月かかる 大きなバッチサイズで学習する 一方で、TPUを使うのに向いていない場合もあります。 頻繁なブランチングや要素ごとの積が多いプログラム スパースなメモリアクセス 高精度演算 カスタムTensorFlow operationを含むネットワーク タスク Street2shop Street2shopはストリートで撮影されたスナップ写真から、商品部分を検出して、ショップの商品と対応づけるタスクです。 ECサイトの商品検索の文脈においてこのタスクを応用すると、言葉では表現しにくい直感的な検索を画像を使って行うことができます。 弊社では過去に、この問題設定を解決するシステムの解説をブログに書いています。 techblog.zozo.com 計量学習 写り方の異なる同一商品の画像や、見た目が似ている商品を検索するには、画像同士の距離や類似度が適切に測れる空間が必要です。画像をそのような空間に埋め込むモデルを獲得するタスクが計量学習です。今回は損失関数にN-pair loss、ベースとなるネットワークにResNet-50、最適化の手法にAdam、空間の次元数は256を採用しています。問題設定の詳細な解説は過去のテックブログを参考にしてください。 techblog.zozo.com 実装のフレームワークにはTensorFlowを用います。 TensorFlowのTPUEstimator APIを利用して、CPU、GPU、TPU全てのチップで動作する汎用的なコードが得られます。 Googleが用意しているリファレンス実装を元に、自前のタスクに関する部分を書き換えることでTPUを使って動作するコードを得られます。 cloud.google.com 学習データ 今回の計量学習のために、商品画像と着用時の画像のペアを120万組ほど用意しました。 ストリートで取られたスナップ画像に対して商品検出器をかけ、商品部分を切り出しておきます。今回対象としたカテゴリはトップス、アウター、ドレス、パンツ、スカート、シューズの6種類で、女性ファッションのみに絞りました。 学習 プロファイリング Cloud TPUによる学習を高速化するためには、速度のボトルネックとなっている部分を特定して、適宜チューニングしていくという作業が必要になります。その際、cloud_tpu_profilerというツールを利用すると、ボトルネックの特定が容易になります。この辺りの作業は、一度Brennan Saetaさんによる実演を見ておくと理解がスムーズかと思います。 www.youtube.com cloud.google.com cloud_tpu_profilerを実行すると、TensorBoardでプロファイリングの結果が閲覧できるようになります。 サマリー画面は以下のようなものです。 この画面の3段目「Recommendation for Next Steps」に、学習のパフォーマンスを上げるためのアドバイスが書かれています。 summary TPUの学習速度に対してデータのインプットが追いついていない場合は、TPUに待ち時間が発生しますし、その逆も考えられます。プロファイラの各項目を深掘りすることで、どこが計算のボトルネックになっているのかを特定できます。 各オペレーションとメモリ使用量の関係 各オペレーションとTPUリソースの関係 今回の学習の場合、リファレンス実装をそのまま利用することにより高いパフォーマンスでCloud TPU動作させることができたため、特別なチューニングは行なっていません。 TPUはフィードフォワードとバックプロパゲーションが高速であり、データの読み込みがボトルネックになることがあります。 例えば、データの読み込み速度と学習速度のバランスをみてバッチサイズを変えるというチューニングが考えられます。 今回は、1バッチあたり1024枚と大きなミニバッチサイズを設定します。 学習結果 TPUにはコアが8つあるため、シングルGPUとの単純な比較は不公平ですが、現状の計算環境とTPUを使った場合の差分をはっきりさせるために敢えて比較します。 以下の表は、同じタスクを5日間ほど回した際の各評価値です。 学習の速度の観点では、P100で学習した場合、1秒あたり20組の画像を学習するのに対し、TPU v2では1100枚の画像を学習できました。 Cloud TPUを使うことにより、これまでに比べて55倍の学習の高速化が達成できたと言えるでしょう。 精度の観点では、同じ期間内にイテレーションをより多く回せたため、TPUで学習したモデルのAccuracyがGPUで学習したモデルのものより高い値になっています。 Unit Batch Size Top1 Accuracy (batch size 64) Top5 Accuracy (batch size 64) Train Loss Val Loss Elapsed Time / 1000 step N image / sec Nvidia P100 64 0.6511 0.9085 1.784 1.355 ~ 54 min ~ 19.75 image Cloud TPU v2 1024 0.8070 0.9573 1.155 0.7812 ~ 15.5 min ~ 1100 image 計算にかかる料金 2019年1月31日の時点でCloud TPU v2の単位時間あたりの料金は$4.50 USDです。 cloud.google.com 一方で、NVIDIA P100のGPUの料金は単位時間あたり1GPUあたり$1.46 USDです。 cloud.google.com 具体的な料金はCPUの数やメモリ、ディスクの容量にもよりますが、料金はせいぜい数倍の差です。その一方で、55倍の速さで学習ができたと考えると、Cloud TPUを使った方が最終的にかかる料金が低く抑えられるかもしれません。 モデルの活用 今回学習したモデルはインプットとして、「スナップ画像から商品部分を切り出した画像」と「商品画像」の両方を受け付け、256次元の数値をアウトプットします。 Shop2shopという単なる類似画像検索と、Street2shopという問題設定の両方を定性的に評価してみます。 評価時の入力と検索対象には、学習には用いていないバリデーション用データセットを利用しています。 Shop2shop 左側の赤枠で囲んだ画像を空間に埋め込み、ユークリッド距離が近い順に商品を選び出すという検索を行なっています。商品画像同士の類似度を直接学習したわけではありませんが、多くの場合で、形、色、柄、素材の観点で似ている商品が検索できています。以下、カテゴリ別の検索結果になります。 トップス 色、形はもちろんのこと、中央に写っているキャラクターも一致している ネック部分や袖の形の一致、花の刺繍部分の特徴も捉えることができている shop2shop_tops アウター 丈、色、カテゴリーが近いものを検索できている スカジャンの入力画像のポーズが独特だが、正面を向いたものが検索できている shop2shop_outer ドレス 丈、色、形、ネックの一致が見られる シースルーの素材感や柄の特徴も捉えられている shop2shop_dress ボトムス 色、形、柄の一致が見られる 例には載せていないが、ダメージジーンズのダメージ具合やダメージ位置の特徴の一致も検索できる shop2shop_bottoms スカート 丈、素材の一致が見られる デニムスカートのボタンの位置の特徴を押さえることが出来ている shop2shop_skirt シューズ 色、形、丈、素材感の一致が見られる 検索結果に着用画像と商品単体画像が混じり、その両方をある程度公平に評価できていることがわかる shop2shop_shoes Street2shop ストリートの環境で撮影された画像の商品部分を切り出して入力に使うタスクです。 自然光のもとで撮られたり、影が発生したりと複雑なシチュエーションが多くShop2shopよりも難しいタスクです。 スナップ1 トップスの文字部分の特徴を捉えている フォントの形も似ているものが選ばれている パンツは形が異なる商品も含まれる一方で、色、幅、丈が一致する商品も含まれる street2shop_1 スナップ2 ロゴが含まれたTシャツを検索することができる スカートのボタンのポイントは押さえているが、丈や形の面で間違えるパターンがある シューズはつま先とヒール部分を押さえられている street2shop_2 まとめ Cloud TPUを使うことで今回の計量学習に関しては、単体のGPU(Tesla P100)を使って学習した場合に比べて、55倍の学習速度の改善ができました。 55倍の速度改善は、1週間かかっていたモデルの学習が3時間で終わることを意味します。 Cloud TPUによる高速化が可能かどうかの見極めは必要になりますが、時間短縮のためにTensorFlow + Cloud TPUを使うことは一考に値するでしょう。 最後に 弊社では次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できる機械学習エンジニアを募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
アバター
こんにちは! ZOZOテクノロジーズ開発部の塩崎です。 この記事ではCloudFormationにDBのマスタパスワードなどの秘密情報を渡す3つの方法を説明いたします。 前提 我々のチームではAWSインフラリソースのプロビジョニングにCloudFormationを使用しています。 CloudFormationのテンプレートファイルはGitHubでバージョン管理されており、スタックに対するチェンジセットの作成をCircleCIから行っています。 このあたりの詳細は以下の記事に書かれているため、詳細はそちらをごらんください。 techblog.zozo.com 課題 このような方法でCloudFormationテンプレートを管理していましたが、それに伴う課題が生まれました。 DBのマスタパスワードなどの情報をどのようにして渡すかということです。 テンプレート内で使用するためのパラメーターは以下のような形式でテンプレートの中に埋め込み・参照できます。 Parameters : VPCCidrBlock : # パラメーターの定義 Type : String Default : '10.0.0.0/16' Resources : EC2VPC : # VPCを作成 Type : 'AWS::EC2::VPC' Properties : CidrBlock : !Ref VPCCidrBlock # こんな感じで参照。他の箇所からも!Refで参照できる。 しかし、この方法をそのまま使うと秘密情報をテンプレートにそのまま埋め込む必要が出てしまいます。 ちょうど以下のテンプレートのような形になります。 Parameters : RDSMasterUserPassword Type : String Default : 'Very_$ecret_Data' Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword これではGitHubにアクセスできる人から秘密情報が丸見えです。 可能ならばこれらの情報はGitHubにコミットしたくありません。 さらにCircleCIなどの外部SaaSなどに秘密情報(もしくはどこかから秘密情報を取り出すことができる権限情報)を渡すことも避けたいです。 一方でCloudFormation側の事情を考えると、CloudFormationは何らかの方法で平文の秘密情報を知っておく必要があります。 CloudFormationが内部的にAWSのAPIを呼び出してリソースを作成するときにはこの情報が必要なためです。 これらの要件をまとめると、以下の図で示すようなCloudFormationに秘密情報を渡す「何らかの仕組み」が必要です。 解決策 上の図の「何らかの方法」に対応する解決策を3つ紹介いたします。 UsePreviousValue 最初に紹介するのは、CloudFormationにパラメーターを渡す時にUsePreviousValueをTrueにする方法です。 この方法で以前テンプレートに渡した値を引き継げます。 ですので、そのパラメーターを2回目以降使う時はテンプレートの中に値を埋め込む必要がなくなります。 Parameters : RDSMasterUserPassword Type : String Default : '' # ここは空でOK Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : !Ref RDSMasterUserPassword チェンジセットを作成する際には以下のようなJSONを作成し、awsコマンドを叩くことで以前の値を再利用することが出来ます。 [ { " ParameterKey ": " RDSMasterUserPassword ", " UsePreviousValue ": true } ] aws cloudformation create-change-set --stack-name =< stack name > --parameters = parameters.json 参考: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-change-set.html カンの良い方ならば既にお気づきかもしれませんが、この方法には最初にパラメーターをセットするときにはどうするのかという問題があります。 最初にパラメーターをセットするためにはCloudFormationのテンプレートをAWSマネジメントコンソールもしくは手元のターミナルから反映する必要があります。 せっかくCircleCIを使ったCI/CDを構築しているのに、この部分だけが手動反映なのは残念な気持ちになります。 また、parameters.jsonを作成する必要があるのも少々面倒です。 Systems ManagerのSecureStringを使用する 次に紹介する方法はSystems ManagerのSecureStringを使用する方法です。 Systems ManagerはEC2インスタンスやオンプレのインスタンスを管理するためのものです。 その中に文字列を暗号化して保存するためのストレージがあるので、それを活用します。 Systems Managerに保存した文字列はDynamic Referenceという方法でCloudFormationから参照できます。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:ssm-secure:rds-master-user-password:1}}' 最終行がDynamic Referenceでパラメーターを参照している部分です。 Dynamic Referenceの書式は以下に示すように : で区切られた4つのセクションに分かれています。 {{resolve:ssm-secure:parameter-name:version}} 前半の2つはSystems ManagerのSecureStringを使うことを指定しています。 後半の2つで保存されたパラメーターのキーとバージョンを指定しています。 最新のバージョンを指定するということはできず、必ず数字でバージョンを指定する必要があります。 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-ssm-secure-strings パラメーターをセットするためには以下のようにawsコマンドを叩くか、もしくはAWSマネジメントコンソールで行います。 aws ssm put-parameter --name rds-master-user-password --value ' Very-$ecret-Data ' --type SecureString Systems Managerがこの値を保存するときにはKey Management System(KMS)を用いた暗号化がなされます。 暗号化に使う鍵はデフォルトキーだけでなく、 --key-id パラメーターを使ってユーザーキーを指定することも可能です。 この方法を使うことでCloudFormationにパラメーターを渡すことが出来ますが、注意点もあります。 適用できるリソースの種類に制限があるということです。 現時点ではIAMユーザーのパスワードやRDSのマスタパスワードなどの11種類のリソースに対してのみこの方法を適用できます。 そのため、任意の箇所へ秘密情報を埋め込むということは出来ません。 たとえば、ECSで動いているアプリケーションに対して環境変数を通して秘密情報を渡すことを考えると以下のようなテンプレートになるかと思います。 AWS::ECS::TaskDefinitionリソースはSecureStringに対応していないため、SSM SecureStringを使った方法は適用できません。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : 'Very-$ecret-Data' # ここには{{resolve:ssm-secure:rds-master-user-password:1}}と書けない Secrets Manager 最後に紹介する方法はSecrets Managerを使って秘密情報を渡す方法です。 この方法はSystem ManagerのSecureStringを使用する方法に似ていますが、少々異なる面もあります。 Secrets Managerを使った方法もDynamic Referenceを使用してパラメーターの参照を行うため、書式がかなり似ています。 Resources : RDSDBCluster : Type : 'AWS::RDS::DBCluster' Properties : MasterUserPassword : '{{resolve:secretsmanager:rds:SecretString:password}}' こちらの方法のDynamic Referenceの書式は以下に示すように : で区切られた5つのセクションに分かれています。 secret-id と json-key の2つを指定して秘密情報の取り出しを行います。 Secrets Managerのそれは連想配列型であるため、これら2つの情報を使って秘密情報を指定します。 {{resolve:secretsmanager:secret-id:SecretString:json-key}} 秘密情報をセットするためには以下のコマンドを使用します。 この例ではRDSのパスワードだけを設定していますが、RDSのユーザーやホスト名などの関連する情報を設定することも出来ます。 また、Secrets ManagerでもKMSのユーザーキーを用いた暗号化を行うことが出来ます。 aws secretsmanager put-secret-value --secret-id rds --secret-string ' {"password":"Very-$seret-Data"} ' Systems Managetとは異なり、Secrets Managerを使ったDynamic Referenceはどのリソースに対しても使用することが出来ます。 このような利便性がある一方で、うっかりと秘密情報を公開してしまう危険性も同時に持っているため注意が必要です。 Resources : ECSTaskDefinitionApplication : Type : 'AWS::ECS::TaskDefinition' Properties : ContainerDefinitions : Environment : - Name : 'DB_PASSWORD' Value : '{{resolve:secretsmanager:rds:SecretString:password}}' 参考: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager 3つの手法の比較 上で紹介した3つの方法の比較です。 UsePreviousValue SSM Secure String Secrets Manager CloudFormationのチェンジセット作成を自動で行えるか X O O パラメーターが暗号化されて保存されるか X O O KMSのユーザーキーを利用できるか X O O 任意のCloudFormationリソースに対して使用できるか O X O 他のAWSサービスとのインテグレート X O O この表を見てみると、Secrets Managerを使うのが現状のベストだと思います。 他のサービスとのインテグレートについて 今回紹介した方法はCloudFormationとSSM、CloudFormationとSecrets Managerという組み合わせでの使い勝手を見るという観点でした。 一方でSSMやSecrets ManagerはCloudFormation以外のサービスと組み合わせて使うことも出来ます。 その観点から見ると、現時点ではSSMのほうが他のサービスとのインテグレートへの対応が早いかと思います。 例えばElastic Container Service(ECS)とのインテグレートを考えます。 ECSでタスクを起動する時に秘密情報を環境変数にセットしてから起動したいとします。 2019年2月現在ではこのようなことが出来るのはSSM SecureStringのみです。 Secrets Managerの方が後発のサービスであることを考えると致し方ない気もしますが、SecretsManagerのこれからに期待したいです。 2/5追記 いつの間にかSecrets ManagerからECSに秘密情報が渡せるようになっていました。 気づいたらどんどん便利になっていくaws!! Secrets Managerの方が後発のサービスなので対応時期に少々の差(2018年11月と2019年1月)が出てしまっていますが、将来的にはこの差が縮まっていくことを期待しています。 よいまとめありがとうございます! 実は1月末の時点で ECS から Secrets Manager の値取れるようになってるので、ブログ記事にも反映して欲しいです〜 — ポジティブな Tori (@toricls) 2019年2月5日 参考: https://aws.amazon.com/jp/about-aws/whats-new/2018/11/aws-launches-secrets-support-for-amazon-elastic-container-servic/ https://docs.aws.amazon.com/AmazonECS/latest/developerguide/specifying-sensitive-data.html#secrets-create-secret まとめ CloudFormationに秘密情報を渡す3つの方法をお伝えしました。 現時点ではSecrets Managerを使うのがベストだと思います。 我々Marketing AutomationチームではAWSのインフラ構成をCloudFormationで管理することによって、Infrastructure as Codeを実現しています。 最近ではGCPのインフラも増えてきたことからTerraformの使用も視野に入れて日々の開発運用を行っております。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
アバター
前書き こんにちは、スマートファクトリー向け制御ソフトウェア開発チームの高石( @ksk_taka )です。 本記事では、アパレル業界や製造業界など、CADを取り扱う業界で広く使われているdxfファイルを 一括で画像ファイルに変換する 手法について記載します。 dxfファイルとは そもそも dxfファイルとは何ぞや? という方のために簡単に説明をします。 dxfファイルは CAD間を仲介する中間ファイル として使うことを目的としたファイルです。 例えば機械設計をする際に3D-CADが利用されますが、よく使われる3D-CADソフトとして、以下のものがあります。 CATIA SolidWorks Creo Parametric これらのそれぞれのソフトで作られる図面データは 別々のファイル形式(拡張子) を持っており、基本的に互換性がありません。 アパレル業界で用いられるCADソフトも同様で、以下の様な異なるCADソフトにて生成される図面データには互換性がありません。 クレアコンポ AGMS このままでは一方のCADソフトで生成した図面データを、別のCADソフトを持った人が開こうとすると「開かない!」という状況になってしまいます。 そんなの困る! ということで、必要となるのが 中間ファイル という存在です。 dxfファイルは恐らく 現在最も広く使われている であろう中間ファイルで、 使用するCADソフトに関わらず開くことができる図面データ になります。 dxfファイルを用いることで、異なるCADソフトを利用している人同士でも、図形データのやり取りが可能になります。 ※但し、「各社独自のCADオブジェクト」などはdxfでは再現できません。やり取りの際には注意が必要です。 dxfファイルを画像化する目的 上記のように便利なdxfファイルですが、万能なわけではありません。 例えば以下のような状況では、もっと汎用的なデータ形式の方が望ましいと言えます。 CADソフトを持たない人に図面データを渡したい場合 Web上や社内のドキュメントファイルなどに図面(絵柄)を貼付したい場合 そのような状況に対応するため、dxfファイルに含まれる図面データを一括で画像化するソフトをGo言語で実装しました。 dxfファイルの構造 dxfファイルはテキスト形式のファイルです。その為、テキストエディタで簡単に内容を確認できます。 ファイル内のテキストは2行で1組となっており、1行目は グループコード 、2行目はグループコードに応じて 文字列 、 数値 などが入ります。 dxfファイルの例を以下に記載します。 0 SECTION //SECTION開始 2 BLOCKS //BLOCKS SECTION開始 0 BLOCK //1個目のBLOCKの開始 8 1 //1個目のBLOCKの階層 2 1_BlockName //1個目のBLOCKの名前 10 0 //1個目のBLOCKのX座標 20 0 //1個目のBLOCKのY座標 0 POLYLINE //1個目のBLOCK内の初めのENTITYデータ(POLYLINE:連続した頂点で描画される図形) ・ ・ ・ 0 LINE //1個目のBLOCK内の2個目のENTITYデータ(LINE:2点を結ぶ線分で描画される図形) ・ ・ ・ 0 ENDBLK //1個目のBLOCK項目の終了 0 BLOCK //2個目のBLOCK項目の開始 ・ ・ ・ 0 ENDBLK //n個目のBLOCK項目の終了 0 ENDSEC //BLOCKS SECTION終了 0 EOF //FILEの終了 今回はひとまず、使用頻度の高い POLYLINE と LINE の2つの図形を描画する機能を実装します。 実装方法 ここからは具体的な実装について記載していきます。 今回、ソフトウェアを実装する上で肝となるのは以下の機能です。 dxfファイルを1行ずつ読み取る機能 読み取ったデータを構造体に格納する機能 構造体の格納データに応じて画像を描画する機能 1つずつ見ていきましょう。 事前準備 まず、事前準備から。 目標の機能を実装するにあたり、Go言語のパッケージは以下のものを使用します。 import ( "bufio" "image" "io/ioutil" "os" "path/filepath" "regexp" "runtime" "strconv" "sync" "time" "github.com/llgcode/draw2d/draw2dimg" "image/color" "fmt" "golang.org/x/text/encoding/japanese" "golang.org/x/text/transform" ) 更に、読み取ったdxfファイルから各要素を格納していくための構造体を準備しておきましょう。 // Section is a top level group. type Section struct { Blocks []Block Entities []Entity Headers [][] string Tables [][] string } // Block is a second level group in Section. type Block struct { Name string LayerName string BlockType string X float64 Y float64 Entity []Entity } // Entity is a third level group in Section or Entities. type Entity struct { TYPE string Name string FULL [][] string } dxfファイルを1行ずつ読み取る まずはdxfファイルを1行ずつ読み取り、Sliceデータとして返す関数を実装します。 func getFileStream(inputpath string , fileName string ) (data [][][] string ) { var row [][] string var scangroup [][][] string input, err := os.Open(filepath.Join(inputpath, fileName)) if err != nil { // Openエラー処理 panic (err) } defer input.Close() scangroup = nil //dxfファイル ロード開始 sc := bufio.NewScanner(transform.NewReader(input, japanese.ShiftJIS.NewDecoder())) for i := 0 ; sc.Scan(); i++ { if err := sc.Err(); err != nil { // エラー処理 break } if i != 0 && sc.Text() == " 0" { scangroup = append (scangroup, row) //区切り文字で塊を作る row = [][] string {} //塊を作ったら初期化 } //2行で1要素分なので判別しやすいようにまとめてSlice化 gkey := sc.Text() sc.Scan() gvalue := sc.Text() row = append (row, [] string {gkey, gvalue}) } scangroup = append (scangroup, row) row = [][] string {} return scangroup } ファイルパス と ファイル名 の文字列が与えられると、該当ファイルを1行ずつ読み込む関数です。 上でお伝えした通り、dxfファイルは2行で1組となっているため、 1組分をまとめてSlice にしています。 データを構造体に格納する 続いて、読み取ったデータを各種構造体に格納する処理を実装していきましょう。 func makeSection(data [][][] string ) (sec Section) { var isBlocks bool var isEntities bool var blk Block var ent Entity var copyData [][][] string var copyGroup [][] string var copyRows [] string for _, g := range data { for _, r := range g { copyRows = make ([] string , 2 ) copy (copyRows, r) copyGroup = append (copyGroup, copyRows) } copyData = append (copyData, copyGroup) copyGroup = [][] string {} } sec = Section{} for _, group := range copyData { if group[ 0 ][ 0 ] == " 0" { switch group[ 0 ][ 1 ] { case "SECTION" : //SECTIONの処理 for _, rows := range group { if rows[ 0 ] == " 2" { switch rows[ 1 ] { case "BLOCKS" : isBlocks = true isEntities = false case "ENTITIES" : isBlocks = false isEntities = true case "HEADER" : isBlocks = false isEntities = false sec.Headers = group case "TABLES" : isBlocks = false isEntities = false sec.Tables = group default : } } } case "VERTEX" , "LINE" , "POLYLINE" , "SEQEND" , "TEXT" : //図形データ ent = Entity{} ent.TYPE = group[ 0 ][ 1 ] ent.FULL = group if isBlocks { blk.Entity = append (blk.Entity, ent) } else if isEntities { sec.Entities = append (sec.Entities, ent) } case "BLOCK" : //画像は1BLOCKにつき1枚 blk = Block{} //BLOCKの処理 for _, rows := range group { switch rows[ 0 ] { case " 8" : blk.LayerName = rows[ 1 ] case " 2" : blk.Name = rows[ 1 ] //file名に使用 case " 70" : blk.BlockType = rows[ 1 ] case " 10" : blk.X, _ = strconv.ParseFloat(rows[ 1 ], 64 ) case " 20" : blk.Y, _ = strconv.ParseFloat(rows[ 1 ], 64 ) default : } } case "ENDBLK" : //BLOCKの終わりを示す sec.Blocks = append (sec.Blocks, blk) case "EOF" : //fileの終わりを示す default : } } } return sec } 先ほど作成したデータを入力として、各構造体にデータを格納していくコードです。 最終的に全てのSECTIONをまとめたものがSliceで返されます。 画像化する 続いて、構造体に格納されたデータを用いて画像を描画する機能を実装します。 func exportPNGperBlk(sec Section, dir string ) { const PrefixSEQEND = - 1 var vertexX, vertexY float64 var lstartX, lendX, lstartY, lendY float64 var maxX, maxY, minX, minY float64 var exportFlag bool var vertexs [][] float64 var lines [][] float64 var name string var i int exportFlag = false for _, bl := range sec.Blocks { //file名にはディレクトリ名とブロック名を使用する name = dir + "-" + bl.Name exportFlag = true for _, en := range bl.Entity { if en.TYPE == "SEQEND" { //線の切れ目 vertexs = append (vertexs, [] float64 {PrefixSEQEND, PrefixSEQEND}) continue } if en.TYPE == "VERTEX" { //POLYLINEで頂点を繋ぐ線分を描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //頂点のX座標 vertexX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexX > maxX { maxX = vertexX } if vertexX < minX { maxX = vertexX } case " 20" : //頂点のY座標 vertexY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if vertexY > maxY { maxY = vertexY } if vertexY < minY { minY = vertexY } } } //"vertexs"に、頂点のXY座標を格納 vertexs = append (vertexs, [] float64 {vertexX, vertexY}) } if en.TYPE == "LINE" { //LINEで線分描画する際に使用 for _, rows := range en.FULL { switch rows[ 0 ] { case " 10" : //開始地点のX座標 lstartX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartX > maxX { maxX = lstartX } if lstartX < minX { minX = lstartX } case " 11" : //開始地点のY座標 lendX, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendX > maxX { maxX = lendX } if lendX < minX { minX = lendX } case " 20" : //終了地点のX座標 lstartY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lstartY > maxY { maxY = lstartY } if lstartY < minY { minY = lstartY } case " 21" : //終了地点のY座標 lendY, _ = strconv.ParseFloat(rows[ 1 ], 64 ) if lendY > maxY { maxY = lendY } if lendY < minY { minY = lendY } } } //"lines"に、開始-終了地点のXY座標を格納 lines = append (lines, [] float64 {lstartX, lstartY, lendX, lendY}) } } if exportFlag { //画像サイズがPartsごとに異なるので毎回準備する img := image.NewRGBA(image.Rect( int (minX), int (minY), int (maxX+ 1 ), int (maxY+ 1 ))) gc := draw2dimg.NewGraphicContext(img) gc.SetFillColor(color.White) gc.SetStrokeColor(color.RGBA{ 0 , 0 , 255 , 255 }) gc.Fill() //POLYLINE描画 for _, vertex := range vertexs {} i++ fmt.Println( len (vertexs), vertex, i) if vertex[ 0 ] == PrefixSEQEND && vertex[ 1 ] == PrefixSEQEND { if i < len (vertexs) { //線の切れ目では描画をせず座標移動だけ実施 gc.MoveTo(vertexs[i][ 0 ], maxY-vertexs[i][ 1 ]) } continue } //線を描画。画像とdxfでY座標の方向が反転する点に注意 gc.LineTo(vertex[ 0 ], maxY-vertex[ 1 ]) } //LINE描画 for _, line := range lines { gc.MoveTo(line[ 0 ], maxY-line[ 1 ]) gc.LineTo(line[ 2 ], maxY-line[ 3 ]) } gc.Stroke() gc.Close() //出力フォルダを作成する outputpath, err := filepath.Abs(filepath.Join( "." , "file" , "output" , dir)) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //出力フォルダを作成する outputpath, err = filepath.Abs(filepath.Join( "." , "file" , "output" , dir, "png" )) if err != nil { panic (err) } os.Mkdir(outputpath, 0777 ) //png画像として保存 draw2dimg.SaveToPngFile(filepath.Join(outputpath, name+ ".png" ), img) //各変数を初期化 exportFlag = false vertexs = [][] float64 {} lines = [][] float64 {} maxX = 0 maxY = 0 minX = 0 minY = 0 i = 0 } else { fmt.Println( "画像化対象のパーツが見つかりません。" , "パーツ名[" , name, "]" ) } } } 最後に、main関数を実装します。 func main() { // 正規表現を使って対象ファイルを設定 rep := regexp.MustCompile( "[A-Z]*[a-z]*[0-9]*.dxf" ) // Inputディレクトリがあるかどうか確認 inputparentpath, err := filepath.Abs( "./file/input/" ) if err != nil { panic (err) } dirs, err := ioutil.ReadDir(inputparentpath) if err != nil { panic (err) } var wg sync.WaitGroup cpus := runtime.NumCPU() // CPUの数 limit := make ( chan struct {}, cpus) // Inputディレクトリ配下の全ファイルを読み込み for _, dir := range dirs { dirName := dir.Name() if dir.IsDir() != true { continue } inputpath, err := filepath.Abs(filepath.Join(inputparentpath, dirName)) if err != nil { panic (err) } files, err := ioutil.ReadDir(inputpath) if err != nil { panic (err) } // 各ディレクトリ配下の全ファイルを読み込み for i, file := range files { fileName := file.Name() // dxfファイルが見つからない場合 if !rep.MatchString(fileName) { continue } if file.IsDir() { // ディレクトリはスキップ continue } // ファイル読み込み〜出力までは並列処理で実行 wg.Add( 1 ) go func (i int ) { defer wg.Done() limit <- struct {}{} fmt.Println( "処理開始" , "[" , i, "]" , dirName, fileName) //dxfファイル読み取り scangroup := getFileStream(inputpath, fileName) //データを構造体に格納 sec := makeSection(scangroup) //画像ファイル生成 exportPNGperBlk(sec, dirName) fmt.Println( "処理済" , "[" , i, "]" , dirName, fileName) <-limit }(i) wg.Wait() } } // 全ての処理が完了するまで待機 wg.Wait() fmt.Println( "全ての処理が完了しました。キーを押すと終了します。" ) fmt.Scanln() } 実行結果 実際に、冒頭で紹介したdxfファイル(デニムパターン)に対して本機能を適用してみました。 実行した結果出力された画像は以下の通り。 画像1: デニム左前身頃 画像2: デニム右前身頃 画像3: デニム左後身頃 画像4: デニム右後身頃 画像5: デニム後ポケット(左) その他、デニムパーツ多数出力(数が多いので省略) 想定通り、複数の図形データを持つdxfファイルを、画像に一括で変換できていることが確認できます。 最後に 今回は「POLYLINE」「LINE」という2つの図形に絞って画像化する機能を実装しました。 恐らく他にもdxfファイルに利用されている図形はあると思いますので、気になる方は是非続きを実装してみて下さい。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。一緒に楽しく仕事しましょう! www.wantedly.com
アバター
こんにちは!ZOZOテクノロジーズ開発部の池田( @yuji_ikepon )です。 本記事では、 ケアラベルデザイン発行を自動化した際に使用したパッケージの紹介と、プロトタイプ開発までのプロセスを紹介したいと思います。 ケアラベルとは ケアラベルとは上記の様な、繊維製品になくてはならない品質表示のことを指します。ケアラベルは、品質表示法の下に適切で明確な表示が義務づけられています。 プロトタイプ製作までの経緯 ケアラベルのデザインを行なっていた担当者から、「ケアラベルを自動的に発行できないだろうか」という相談が来ました。 担当者から聞いた仕様は以下の通りです。 エクセルのデータを元にアウトプット出来る事。 指定したフォントが使用できる事。 プログラミング未経験でも使える事。 Macだけでなく、Windowsでも動く事。 開発環境によらず実行できる事。 これらの条件を踏まえ、PDFでのデザイン出力を軸にプロトタイプを作成することにしました。 また、実行環境の事を考え、言語はGo言語を使用します。 使ったパッケージ、ライブラリ プロトタイプを実装した中で使った、Go言語のライブラリとソースコードの一例を以下に示します。 xlsx : https://github.com/tealeg/xlsx エクセルの任意のシートを読み込む // xlsx読み込み excel, err1 := xlsx.OpenFile(file) // xlsxが読み込めなかった場合 if err1 != nil { log.Printf("error: not open xlsx file.") return false } else { log.Printf("debug: success open xlsx file.") } var sheet *xlsx.Sheet = nil // シート名で該当のシートを検索 for k, _ := range excel.Sheet { searchSheet := excel.Sheet[k] if searchSheet.Name == "検索したいシート名" { log.Printf("debug: success open xlsx sheet.") sheet = searchSheet break } else { // 何もしない } } // 該当シートが見つからない場合 if sheet == nil { log.Printf("error: failur open sheet.") return false } // エクセルの値を参照 for rowKey, rowValue := range sheet.Rows { // このループではエクセルの1行ごとに処理ができる // 左から0番目のセルを参照できる cell := rowValue.Cells[0].Value } gopdf : https://github.com/signintech/gopdf pdfの作成 pdf := gopdf.GoPdf{} // rect作成 rect := gopdf.Rect{} rect.W = "pdfの幅" rect.H = "pdfの長さ" // gopdf作成 config := gopdf.Config{} config.PageSize = rect pdf.Start(config) fontPath := string("data/Font/" + data.FontPath + ".ttf") pdf.AddTTFFont(data.FontPath, fontPath) pdf.SetFont(data.FontPath, "", 2*13) pdf.AddPage() // 画像書き込み // 画像の場所 inputPath = "画像のパス" // 出力 size := gopdf.Rect{ H:"画像の高さ", W:"画像の幅", } pdf.Image(inputPath, "pdfのX座標", "pdfのY座標", &size) // テキスト書き出し // 座標を設定 pdf.SetX("pdfのX座標") pdf.SetY("pdfのY座標") // 出力 pdf.Cell(nil, "テキスト") 出来たデザイン 下記の様なデザインで出力する事ができました。 ※ 表示されている情報はあくまでテストの為の仮の表記です。 海外向けケアラベル 日本向けケアラベル(表面) 日本向けケアラベル(裏面) プロトタイプのデザイン自体はそれっぽく実装する事ができました。 しかし、使うフォントの種類によっては、表示される位置がずれてしまったり、改行の位置が揃わないといった問題がありました。 次回プロトタイプ改修の際にはこれらの問題点と合わせて、デザイン的な観点からも実装を考えられたらなと思います。 Go言語で書く上でつまずいた点 今回Go言語でプロトタイプを作成していく中で、以下の様な点で苦労しました。 パッケージ間の共有データをどう持つか エクセルからデータをインプット→PDF or PNGでアウトプットを行う際に、パッケージ間でデータの受け渡しが必要でした。 その為、データ共有のパッケージを作成し、そこに構造体を保持する事でデータを参照していました。 しかし、保持したいデータの中に構造体の配列を入れた際に、初期化のタイミングの違いにより、参照が上手くいかない場合があり苦労しました。 ライブラリの情報不足 Go言語のライブラリの中には、基本的な使い方しか表記されていないものが多く、自分でライブラリ内のコードを見ながら探っていくことが多かったです。 アウトプットするまで実装が正解か分からないこともあり、デバックに苦戦を強いられました。 最後に 今回はプロトタイプの作成のフローを紹介させて頂きましたが、この他にも色々な業務を行なっています。 制御系エンジニアに関しての詳しい情報は以下のリンクからご覧になれます。 https://tech.zozo.com/recruit/mid-career/detail29/ ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com
アバター
こんにちは、ZOZOテクノロジーズでプロジェクトマネージャーをしている 新井(@masamasaking) です。 最先端の小売業界のトレンドを体験できる全米最大のリテール向けのイベント「 NRF Big Show 」が今年も1月13日から開催されました。現地視察を通じて感じたことをレポートします。 この記事では気になった出展企業や全体を通して感じたことをご紹介します。 NRF Big Showとは 世界99カ国から機器やソリューションが800以上の出展。37,000人が参加するNRF(全米小売業協会)が行っている小売業界向けのイベントです。展示と並行して6つのステージで3日間セミナーやキーノートスピーチも行われ小売業界の今がここに集まっていきます。 気になった展示 AIがもはや当たり前かのように AIやMLを使ったオムニチャネル化、商品トレンドの分析、需要予測、ダイナミックプライシング、価格算出など顧客体験の向上や業務オペレーション改善だけでなく小売のいたるところにこれらの技術を使われるのは必須でした。オフライン上でのデータを収集およびユーザーごとにデータを蓄積し、足元の店舗の最適化からその先のダイナミックプライシングやディスプレイの見せ方の個別最適化の進んだ世界が近い将来くることが伺える。消費者が何を求めていて、どのタイミングで商品を並べるのか、どういった価格で・どういった情報を訴求するのか・・・・といった課題に対してAIやMLがこれでもかというくらい絡んでくる。これまでバズワードであった「AI」がいよいよ小売の現場に本格化したようです。 ディスプレイのリッチ化・インタラクティブ化 *1 これまで店頭は紙などのPOPが主流であったがデジタル化が進んでいる。情報をただ見せるだけであればオンラインのストアでもできるが、その先をいくインタラクティブな見せ方のソリューションが散見されました。 実際に商品を手に取るとすぐさまディスプレイに連動して、動画が流れ商品を訴求。またスマートミラーの出展もあり、消費者が試着をするたび鏡に商品情報が表示される。それだけにとどまらず近くにある商品で次はこれが似合うと思うのでいかがですか?とレコメンドまでしてくれます。 ディスプレイの表示内容の最適化と分析 *2 インテルが展示していたディスプレイが目を引きました。ただの大きいディスプレイだと思ったがよくみるとカメラが取り付けられている。そのカメラから見てるユーザーのデモグラを推測し、また視線の動きを追尾してどの商品・モデルに興味をもっているかが把握できる。また天候や曜日なども加味しそれらの情報を逐一分析し、商品ごとのスコアリングがされている。店舗側は推すべき商品が把握出来るのに加えて、ユーザーはついつい見てしまうデジタルサイネージができるかもしれない。また画像認識とオンラインの・オフライン合わせたユーザーデータの突合が進めばユーザーごとに見せる商品の出し分けができる未来がくるかもしれないです。 ドローンによる商品確認 店舗でドローンを飛ばして陳列棚を周遊しながら画像解析をして、商品の在庫確認ができる。商品が従来の位置になかったり・正面を向いていなくても認識ができます。それも瞬時に。例えば、高さ2m・長さ5mの商品棚であれば1分も掛からずに集計ができます。小さな店舗であれば不要かもしれないがショッピングモールやスーパーといった大型店舗になればなるほどこれまで人が随時手作業で在庫のチェックしていたものがドローンを活用することであっという間にかつ正確にできる未来がすぐそこまで来ています。 イノベーションラボ/スタートアップゾーン またスタートアップ系の企業・サービスが集まる2018年から新設されたエリアからもいくつか気になったものをご紹介します。 Texel 身体計測をする技術を持っているロシアの企業。3D化だけでなく、手足の長さなどの計測も可能。アプリ版(未リリース)もあり被写体を中心にアプリ(カメラ)を周囲360度を撮影することで計測が可能です。今後実用レベルまで開発が進みより多くの人が自分の正確な身体データをもてたらオンラインでもより洋服やアクセサリーが買いやすくなるので今後の開発・普及が楽しみです。 americhip プロペラを使ったホログラムを表示する技術をもった会社。多少音が気になるが投影物がVRゴーグルを使わなくても目の前に立体的に見えます。複数のプロペラ使えば大きな表示も可能です。 caper Amazon GOのように天井のいたるところにカメラやセンサーを設置し無人レジを実現するのに対して、こちらはスマートカートと呼ばれるカメラつきのカートを用いることでショッピング自動化を模索しています。導入コストが前述のものより低いため差別化できる。またショッピングカートに収集されたショッピングデータを陳列方式の改善や品揃えに活かす。カートに商品を入れずに売り場から立ち去った場合にどうなるか気になるものの違うアプローチでショッピング自動化を目指していて、要チェックです。 まとめ エクスポで目立ったのが「AI/ML」をベースにしたテクノロジーの活用でした。オフラインにある情報を店頭に設置したカメラやセンサーで画像解析等を通じてデータ化。これまでの店舗全体での商品のトレンドや売上の増減ではなく、各顧客の把握にシフトしています。またオムニチャネル化を進めてオンライン・オフラインに関係なくユーザーの情報を溜めて解析をしている。アルゴリズムをもとに算出された提案をもとに商品の価格設定を柔軟に変更したり、店頭での商品説明もPOPからサイネージに切り替わることでよりインタラクティブに、そしてゆくゆくはユーザーごとに最適化した魅せ方に変わっていく。 また商品の在庫確認や物流のロボ化なども進み人の手を介さずに小売ができるようになる「無人化」関連のテクノロジーが多くみられました。これらはオペレーションの効率化といった店舗コストの削減はもちろんのこと、ここでもこれまでオフラインにしかなかったユーザーの購買情報を収集・蓄積するために進化が進んでいるといったほうがただしく流れを読み取れそうです。無人レジにすることで人件費を削減よりかは店舗でのユーザーの行動を追尾ができるようになることでデータを収集・解析しその次に商品需要の予測やレイアウトの検証をしている。小売がテクノロジーを活用して、デジタル化を進めているだけでなく複数のデータソースから予測分析をAIを用いて収益最大化を図っています。 小売業界のデジタル化やテクノロジー化を「オペレーションコスト改善」とだけ見るか、その先にある「AI活用による個別最適化の推進」と見るかで近未来の予測を読み誤ったり、戦略策定を誤ったりするので意識の転換が必要です。 もはや小売業界にもAI活用の流れは不可逆であると感じます。 世界中を飛び回ってテクノロジーを使ったファッション革命を一緒に目指すPM・エンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com *1 : シューズを手に取るとそのプロモーションビデオがすぐさま流れる *2 : 黒い柱の中央にある黒い塊がカメラである
アバター
どうも品質管理部エンジニアチームの木村です。 最近の話ではないんですがWeb UIテスト自動化をしようとなった時の事を書きます。 まずは初期段階の実行環境についてです、自動テストスクリプトの構築や処理そのものはまた次回。 Seleniumでテストを自動化したい! ZOZOTOWN に限らず最近のサービスはなんでもリリース頻度が高いです。 そして何故なのか、いついかなる時も、開発スケジュールは押し気味になります。 これは業界七不思議の1つです。たぶん。 品質管理部としてのテストは開発スケジュールの一番最後に置かれます。 つまり…短期決戦必須となります…。 そんなよくある話からSeleniumを使ってWeb UIテストを自動にしたいという流れになりました。 リリース頻度が高ければ高いほど、リグレッションテストはおろそかになると思うので、そこを自動テストで改善できると素敵です。 じゃあ自動テストスクリプトは書くけれど、その前にどこで実行するの? という事で、正直なところSeleniumもよくわかっていないところから、どうしたら良いのかを調べていました。 実行環境への要望としては下記の5点です。 ・ 並列処理でテスト実行時間を最大限短くしていきたい ・ 作業中PCで自動テストを実行した時、ブラウザがピョコピョコ動いてるのは目障り ・ 実行するPCによって環境がかわるのは避けたい ・ でも実行環境は気軽に変えられるようにしたい ・ 実行する人数やテスト数が増えた時に環境を増強できるようにしたい 調べていると、Selenium Gridを使えば上記問題は全て解決できそうだとわかりました。 Selenium Grid / docker-selenium Selenium ブラウザ上でのwebページ操作をスクリプトから行うためのツールです。 https://www.seleniumhq.org/ http://www.selenium.jp/ Selenium Grid Seleniumで実行される動作を管理するツールです。 https://www.seleniumhq.org/docs/07_selenium_grid.jsp node内に起動するブラウザでテスト処理を実行し、hubがnodeを管理してテストスクリプトからの命令を仲介します。 nodeや、node内のブラウザを複数起動してもhubが接続を管理してくれるので並列処理なども簡単に行えます。 hubとnodeは分けて起動できるので、nodeの追加や変更時にhubやその他のnodeを止める事なく簡単に行えます。 またdocker-seleniumというDocker版SeleniumGridもありました。 Dockerのコンテナを使用するので、場所を選ばず、起動が手軽で、管理もしやすく、クジラも可愛いです。 Dockerよくわからないけど素敵です。 https://github.com/SeleniumHQ/docker-selenium とりあえず作ってみた環境 内容に合わせて3種類に分けました。 ・ 自動テストスクリプトを実行するPC ・ hubを起動するPC ・ nodeを起動するPC 自動テストスクリプトはhubの情報さえ持っておけば、nodeの起動場所を一切気にする必要なくテスト処理を行えるので、PCを分けても問題なく動いてくれます。 hubはhub専用、もしくは各テストスクリプトの設定データや結果等、自動テストスクリプトで使用される共有データを保管しておくのも良いかもしれません。簡易的な形でdbを入れてみたりとか。 nodeはnode専用にして、社内の片隅に放置されてる可哀想なPCを集めて使うのも優しい気持ちになれて良いかもしれません。モッタイナイ精神。 画像内に表示してるPCアイコンは適当に選んだだけなので、これらを使用しているわけではないです。 いざ環境作成 とりあえず1台のPC内で環境を作ってみます。 以降の環境作成を実際に試す時はDockerとPython 3.6をインストールしてください。今回使用していたPCは全てmacOS High Sierraです。 尚、以下の作成手順にはIPを書き込む部分がいくつかありますが、1台で動かしている間は全て同じIPです。 必要なファイルを全部まとめて作っておきます。 TestAutomation |_hub | |_docker-compose.yml | |_node | |_docker-compose.yml | |_selenium_grid_setup.py | |_test_automation.py hubの準備 dockerでhubを起動する為に、設定ファイルを作ります。 ./hub/docker-selenium.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium container_name : selenium-hub ports : - "4444:4444" environment : - GRID_BROWSER_TIMEOUT=120 - GRID_TIMEOUT=150 - GRID_MAX_SESSION=30 使用するselenium imageのバージョンは全て統一しないと動かなくなる時があるので、管理の事も考えてとりあえず書いておきます。 environmentのところで設定を変更できます。 GRID_BROWSER_TIMEOUTがブラウザのタイムアウト、クリック等で失敗を判断するまでの時間です。 GRID_TIMEOUTはhubがnodeを未使用の状態だ判断するまでの時間です。例えば自動テストスクリプトがエラーで終了した時、 hubに終了した事を伝えられないままになるのでnodeを掴んだまま解放されなくなります。なので、命令の無い状態が一定時間続いた時に解放するよ、という設定です。たぶん。 詳しくは公式で https://github.com/SeleniumHQ/docker-selenium/blob/master/Hub/Dockerfile.txt ymlファイルを作ったら起動します。 mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml up -d URLでアクセスできたら成功です。 http://<hub用PCのIP>:4444/grid/console nodeの準備 hubと同様にnode用の設定ファイルを作ります。 ./node/docker-selenium version : "3" services : browser_0 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome0 ports : - "55550:5900" - "5555:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5555" browser_1 : image : selenium/node-chrome-debug:3.14.0-helium container_name : chrome1 ports : - "55560:5900" - "5556:5555" environment : - NODE_MAX_INSTANCES=5 - NODE_MAX_SESSION=5 - HUB_PORT_4444_TCP_ADDR=<hub用PCのIP> - HUB_PORT_4444_TCP_PORT=4444 - REMOTE_HOST="http://<node用PCのIP>:5556" この設定ファイルでchromeを5つまで動かせるnodeを2つ起動できます。 browser_0とbrowser_1です。 portsを設定しておかないとホスト側のポートがランダムに選ばれるのでちゃんと書く必要があります。 左側がホストのポート、右側がコンテナのポートです。ホスト側のポートは空いているポートを指定してください。 portsの"5555:5555"がhubからnodeへの命令時に使用されるポートで、"55550:5900"がvnc接続に使用するポートです。 自動テストスクリプトの作成中はvncで確認したい事があると思うので、ポート番号がわかりにくいと面倒です。 各nodeのホスト側ポート「5555」「5556」は気軽に確認できるので、そこから連想しやすい番号だと楽です。上に書いたファイルの場合は末尾に0をつける形です。 nodeのenvironmentでもいろいろと設定できます。 NODE_MAXは1つのnode内で起動しておくブラウザの個数や、同時に動かせるブラウザの個数です。 タイムアウト等様々な設定がありますが必要ないものはもちろん、自動テストスクリプトの中で切り替える事ができるものもあります。 特殊な使い方をしないのであればほどほどで良いのかなと思います。 困ったら足すくらいの流れで。 https://github.com/SeleniumHQ/docker-selenium/blob/master/NodeBase/Dockerfile.txt ymlファイルをnodeフォルダに保存したら、起動します。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml up -d もう一度アクセスしてみて表示内容が変わっていれば成功です。 http://<hub用PCのIP>:4444/grid/console 自動テストスクリプトを実行した時にちゃんと動作しているかがわかりにくいので、vnc接続もしておきます。 Finder>移動>サーバーへ接続から下のアドレスを入力して接続です。vncのパスワードはsecretです。 vnc://<node用PCのIP>:55550 vnc://<node用PCのIP>:55560 Windowsの場合はUltraVNCとか入れると良いと思います。使い方は、わかりません! 必須ではないけれどnodeの起動方法 node起動時はnode数・使用ブラウザ・使用port等の設定内容を変更しながら試していたので、その都度ファイルを書き換えるのは面倒でした。 なので「ymlファイルを作ってからnodeを起動する」というスクリプトを書く形にしました。 node毎に設定するportはSTART_PORTで決めています。1つ目のnodeは5555、2つ目は5556といった形になっています。 ./selenium_grid_setup.py import os NODE_MAX = 5 START_PORT = 5555 def get_yml_text (node_count, hubip, myip, image, browser_name): text = 'version: "3" \n services: \n ' for i in range (node_count): text += ' browser_{num}: \n ' \ ' image: selenium/{image} \n ' \ ' container_name: {browser_name}{num} \n ' \ ' ports: \n ' \ ' - "{port}0:5900" \n ' \ ' - "{port}:5555" \n ' \ ' volumes: \n ' \ ' - /dev/shm:/dev/shm \n ' \ ' environment: \n ' \ ' - NODE_MAX_INSTANCES={NODE_MAX} \n ' \ ' - NODE_MAX_SESSION={NODE_MAX} \n ' \ ' - HUB_PORT_4444_TCP_ADDR={hubip} \n ' \ ' - HUB_PORT_4444_TCP_PORT=4444 \n ' \ ' - REMOTE_HOST="http://{myip}:{port}" \n ' . format ( NODE_MAX=NODE_MAX, image=image, browser_name=browser_name, hubip=hubip, myip=myip, num= str (i), port= str (START_PORT + i) ) return text def create_file (name, text): f = open (name, 'w' ) f.write(text) f.close() def launch_node (): hubip = '<hub用PCのIP>' myip = '<自身のIP>' node_count = 2 image = 'node-chrome-debug:3.14.0-helium' browser_name = 'chrome' yml_text = get_yml_text(node_count=node_count, hubip=hubip, myip=myip, image=image, browser_name=browser_name) yml_file_name = 'node/docker-compose.yml' create_file(yml_file_name, yml_text) os.system( 'docker-compose -f node/docker-compose.yml up -d' ) # 作る前に停止 os.system( 'docker-compose -f node/docker-compose.yml down' ) launch_node() ちょっと適当な感じが滲み出ちゃってますが、こんな形で作りたい内容に合わせてymlファイルを作って保存します。 yml設定ファイルを上書きする前に、古いyml設定ファイルで起動されているコンテナを停止するのだけ忘れないように。忘れるとコンテナで溢れかえります。 どうせ作るならhubの設定ファイルもスクリプトで作るようにするのが良いですね。imageのバージョンを1箇所で管理したいですし。 あと、流石に直接Pythonで文字列を作るより、PyYAMLを使うのが綺麗かもしれないですね。何も考えずに書いても長さは特に変わっていませんが。 import yaml from collections import OrderedDict # dict だと並び順が変わってしまったのでOrderedDictを使いました # OrderedDict だと出力の形式が違ったのですがyaml.add_representerで設定を変更できるみたいです def represent_odict (dumper, instance): return dumper.represent_mapping( 'tag:yaml.org,2002:map' , instance.items()) yaml.add_representer(OrderedDict, represent_odict) # get_yml_text を改変します def get_yml_od (node_count, hubip, myip, image, browser_name): services_dict = OrderedDict() for i in range (node_count): num = str (i) port = str ( 5555 + i) services_dict[f 'browser_{num}' ] = OrderedDict([ ( 'image' , f 'selenium/{image}' ), ( 'container_name' , f '{browser_name}{num}' ), ( 'ports' , [ f '{port}0:5900' , f '{port}:5555' ]), ( 'volumes' , [ f '/dev/shm:/dev/shm' ]), ( 'environment' , [ f 'NODE_MAX_INSTANCES=5' , f 'NODE_MAX_SESSION=5' , f 'HUB_PORT_4444_TCP_ADDR={hubip}' , f 'HUB_PORT_4444_TCP_PORT=4444' , f 'REMOTE_HOST="http://{myip}:{port}"' ]) ]) return OrderedDict([( 'version' , '3' ), ( 'services' , services_dict)]) # yaml の為にちょっと設定を追加します def create_file (name, yml_dct): with open (name, 'w' ) as file : yaml.dump(yml_dct, file , default_flow_style= False ) # 引数は適当です od = get_yml_od(node_count= 2 , hubip= '0.0.0.0' , myip= '1.1.1.1' , image= 'node-chrome-debug:3.14.0-helium' , browser_name= 'chrome' ) create_file( 'node/docker-compose.yml' , od) 実行してみる テスト処理を行うスクリプトを作って、実行します。 JavaとPythonでは書いてましたが、RubyやJavaScriptでは書いた事ないです。いろんな言語でかけるみたいです。 ./test_automation.py from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities def get_web_driver (): return webdriver.Remote( command_executor=f 'http://<hub用PCのIP>:4444/wd/hub' , desired_capabilities=DesiredCapabilities.CHROME ) driver = get_web_driver() driver.get( 'https://www.google.com' ) driver.quit() ↓せっかくなのでJavaの場合 import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.URL; public class Main { public static WebDriver getWebDriver(){ try { return new RemoteWebDriver( new URL( "http://<hub用PCのIP>:4444/wd/hub" ), DesiredCapabilities.chrome()); } catch (Exception e){ System.out.println(e); return null } } public static void main(String args[]){ WebDriver driver = getWebDriver(); driver.get( "https://www.google.com" ); driver.quit(); } googleを開いて、ブラウザを閉じるだけです。 スクリプトが動いてくれたら成功です。 ここまで1台のPCで動かしていたので、次はPCを3台にわけてみます。 分ける前に、今現在立ち上がってるdockerのコンテナを全て停止しておきます。 mac:TestAutomation user$ docker-compose -f node/docker-compose.yml down mac:TestAutomation user$ docker-compose -f hub/docker-compose.yml down 「実行用PC」「hub用PC」「node用PC」の3台にわける 追加するPCのdockerインストールやPythonのバージョンにご注意ください。 ここまで書いたファイルの<hub用PCのIP><node用PCのIP>の部分を書き直します。 IPを書き直したら全てのファイルを3台のPCにコピー&ペーストします。コピペなんてやってられるか!Gitだ!Git! と、おわかりの方はGitよろしくです。 あと、実際の運用を考えると、別途設定ファイルを用意して置いてそこにIPを書いて置いた方が切り替えやすいので良いですね。 各PCにファイルを揃えたらこれまでの手順通りに進めます。 1.hub用PCでhubを起動 2.node用PCでnodeを起動 3.起動したnodeにvnc接続 4.最後に実行用PCで自動テストスクリプトを実行 動いてくれたら完成です。もしも動かないという時は各IPを確認してください。 あとはhub用PCにも追加でnodeを起動してみたり、さらに4台目のPCを用意してnodeを追加してみたり、hubを複数にしてみたり。 使用中に起こった困った事 selenium使用中に発生した環境に関する問題で思い出せた事例を書いておきます。 <困ったこと1> 自動テストスクリプトの中でWebDriverに対して何らかの命令を送った時に、実行されなくなった事がありました。 各箇所で使用しているseleniumのバージョンが異なる時に、問題が発生しやすいみたいです。 絶対問題が出るというわけではないので余計に見落とします。 docker-compose.ymlで指定してるhubやnode、自動テストスクリプト内で使用してるselenium、ローカル上のブラウザで動かす時はそのpcに入っているブラウザ等、それぞれのバージョンに気をつける必要がありそうです。 docker-compose.yml version : "3" services : selenium-hub : image : selenium/hub:3.14.0-helium java - pom.xml <dependency> <groupId> org.seleniumhq.selenium </groupId> <artifactId> selenium-java </artifactId> <version> 3.14.0 </version> </dependency> Python - pip ターミナルから「pip list」でインストールしているバージョンを確認できるので確認。 <困ったこと2> 連続したテスト実行中に、開始時は問題なかったのに途中からWebDriverを掴めなくなる事がありました。エラーを見るとhubへの接続が失敗しているような雰囲気でした。 自動テストスクリプトの中でWebDriverに対して何らかの操作を行うと、 作業pc → hub → nodeという流れで命令を送信します。hubは中間地点として大量の命令をさばいてます。 ウィルス/スパイウェアで聞いた事があるような「自動で大量のリクエストを送信する」という行為と同じようなことをやってます。 社内環境だとセキュリティがしっかりかかってる事も多いと思いますから、そのウィルスのような挙動が原因となってセキュリティソフトに接続をブロックされる場合がありました。 リクエストが多いと言っても抑えられる範囲なので、同時実行数を抑えたり、hubを分けて分散させたりで解決できると思います。もしくはセキュリティチームに相談したり。 おわり ツールの使い方はいろんなところに書いてあるのですが、どんな環境・流れで動かしているか、どんなトラブルがあったかという記事は見当たらないことが多かったので書いてみました。 と言っても、まずは初期段階のSeleniumGrid環境についてでしたので基本的な事ばかりではありますが。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 www.starttoday-tech.com
アバター
こんにちは! ZOZOテクノロジーズ フロントエンドエンジニアの高橋(ニックネームはQ)です( @anaheim0894 ) 昨年12/26、毎年年末に行われる大忘年会(ZOZOCAMP2018)で、 グループ会社 も含めた1,000人規模でのリアルタイムアンケートを、FirebaseとVue.jsを使って制作しました。 当日会場にて弊社の昨年の事業紹介や、「楽しく働く」というコンセプトの動画を流し、動画の合間で質問をし動画と一体となるような演出を行いました。 その質問に対して全社員それぞれのスマートフォンで回答できるシステムを作ったので、その制作の裏側や、当日の様子などご紹介させていただきます。 まずは当日の様子の紹介 これを実現するまでの様子をご紹介いたします。 CAMP運営してくれている社員にもらった要件 CAMPの2週間前、運営の社員の方からこのような依頼をもらいました。 会場で、 リアルタイムで アンケートをWebでとり、その集計を即時に、目の前のプロジェクタに映し出す 選択肢はYES / NOの2つ 質問は全部で7〜8問 QRコードを読み取って、 スマホで回答できる いい感じのデザイン でプロジェクタに出したい 質問と回答は動画と動画の間に行いたい なかなか厳しい要件でした。 どうやったら実現できるか考える 依頼に答えるべく実現方法を検討しました。 Slackの Polly を使って全社員が投票、Slack APIを使ってプロジェクタに表示 → グループ会社含めるとSlackには全社員のアカウントがないため、NGに。 外部のサービスを使う →リアルタイムアンケートのサービス、 respon があったものの、カスタマイズができるか不明なこと、2週間で要件を完璧に実現できないと感じたためNGに。 PBグローバル を担当しているフロントエンドチームで自作 → Firebaseならリアルタイムデータ集計の可能性があったため、 こちらで進めていくことに決定しました 。 自作にあたって整理した要件 CAMP運営からの要件を元に、自作するための要件を整理しました。 質問は7〜8問 動画演出の間に質問を差し込み 動画と質問の切り替えはプロジェクタのスイッチングのみ 選択肢 Yes / No の二択 QRコードで読み取ってサイトを表示 回答結果がリアルタイムで集計され、すぐに会場のプロジェクタで表示 次の質問に移るときはリロードなし 質問の切り替えは管理画面を用意 スマホでの回答は1回のみで解除は不可 回答時間は15秒(質問画面10秒その後5秒のカウントダウン演出) 結果はYES/NOのパーセンテージ割合で表示 当日その場で質問の追加はなし 準備 / 事前に確認したこと CAMPは1,000人規模のイベントです。 そのため、 当日起きることをどれだけ予測できるか 予測外の事態にどれだけすばやく対応できるか が重要になると考え、様々なケースを想定して情報を集めました。 参加人数 約1,000人 1,000人に耐えられるネットワークの準備 イベント自体はイベント会社が入っていたため、イベント会社にWi-Fiの用意を依頼(NTT様にご協力頂きました) 会場スクリーンスペック 解像度:1920×1080 コンテナ:MOV コーデック:映像H.264 / 音声PCM 48kHz フレームレート:59.94または29.9 会場スクリーンの台数 4画面 スクリーンに繋ぐマシンのネットワーク環境 有線で用意 当日のリハーサルの可否 15時から30分〜40分リハーサルができた 1,000人規模のテスト 人数を用意できないため、ぶっつけ本番 (擬似的にコードレベルでの負荷テストは実施) 当日不具合があった場合の対応 質問だけのスライドを用意する。最悪挙手なども考えられる デザイン 社内のデザイナーに依頼 さまざまなスマホでの表示確認 社内のテスト検証を専門に行なっているチームに依頼 選定した技術 上記の情報から自作するために、以下の技術を選定し、作成しました。 データ管理: Firebase フロントエンド: Vue.js 結果のアニメーション: TweenMax 作成した画面 以下の3画面を作成しました。 回答画面 各ユーザーがスマートフォン上で回答を送信する画面 質問の開始・終了時に画面の活性を制御 質問/結果画面 会場のスクリーンに表示される質問や結果の画面 質問の開始・終了時に質問/結果画面を切り替え、結果画面には各ユーザーの回答を集計して表示 管理画面 進行管理者が質問を切り替えるための画面 質問を切り替えると同時に、質問の終了時刻も設定 Firebaseセットアップ 質問の表示切り替えと投票結果の集計をリアルタイムで行うため、リアルタイムリスナー搭載のFirestoreがあるFirebaseを選定しました。 開発に携わったのが全員フロントエンドのメンバーだということもあって、サーバーサイドの開発はせずクライアントJSからデータベースに直接アクセスする構成を取りました。 ここでは、FirebaseをWebアプリケーションで使用するためのセットアップ方法をご紹介します。 プロジェクトのセットアップ Googleアカウントが無ければ作成します。 Googleにログインして、 Firebaseコンソール からプロジェクトを追加します。 プロジェクト名を入力します。 アナリティクスやテクニカルサポートについては必要無さそうなのでチェックを入れずに進んでいきます。 自動でプロビジョニングが行われ、プロジェクトページに飛びます。 デフォルトでは無料のSparkプランが設定されています。ちょっとしたアプリケーションなら無料枠で問題なく動くと思います。 今回は1,000人規模のユーザーがリアルタイムで操作するということだったので、念のため従量課金のBlazeプランに設定しました。 Firestoreセットアップ Firestoreを設定していきます。 左側のナビゲーションから 開発 > Database を選択し、データベースを作成します。 セキュリティルール:今回は要件上セキュリティ面をさほど気にする必要がないので、スピード重視でテストモードを選択します。 コレクション(RDBでいうテーブルのようなもの)が無い空のデータベースが作成されます。 FirestoreはNoSQLなのでスキーマの作成も必要ありません。 Webアプリケーションとの連携を行います。コンソールトップのHTMLタグ風のアイコンをクリックします。 ポップアップでスニペットが出てくるのでこれを開発中のHTMLコードに貼り付けます。 (アプリケーション側のコードをまだ何も作成していなかったので、ここでVue.jsのプロジェクトをinitializeします) Vue.jsのセットアップは こちら を参考にしてください。 ポップアップのメッセージにある HTMLの一番下、他のスクリプトタグの前 ではなく、 main.js に貼り付けます。 .vue ファイル内で import firebase from 'firebase' と記述すれば、 firebase 変数でFirebaseのAPIが利用できます。 GUIコンソール上でガイドにしたがって画面操作するだけでセットアップが完了しました。 Firebase Hostingセットアップ WebアプリケーションのホスティングにもFirebaseを利用することにしました。 こちらはFirestoreと比較しても、とても簡単に設定できます。 左側のナビゲーションから 開発 > Hosting を選択し、 使ってみる ボタンから開始します。 ポップアップのガイドにしたがってfirebaseのコマンドラインツールをインストールします。 $ firebase login でGoogleアカウントにログインします。 開発中のvueプロジェクトのディレクトリに移動し、 $ firebase init でデプロイ設定ファイルを作成します。 生成された firebase.json の hosting.public にデプロイ対象ディレクトリを設定します。 今回はvueプロジェクトをwebpackでビルドしたコードが dist ディレクトリに出力される構成なので、 dist と入力します。 $ firebase deploy で、デプロイ完了です。 firebaseコンソールのHosting画面にデプロイ先のサーバーのドメインが表示されているので、このURLでブラウザからアクセスできます。 以上でFirebase側の設定は完了です。 実際20分くらいでインフラセットアップ作業が完了し、すぐさまアプリケーション側の開発に入れる状況まで持ってこられました。 Firestoreと画面の連携 アプリケーション側のFirestore連携部分の実装を紹介します。 3画面の状態を各端末で同期的に制御するために、Firestoreで以下を管理することにしました。 現在の質問id 回答の締切時間 ユーザーの回答 実際には以下のようなデータ構造でこれらを管理しました。 (実際のデータはJSONではありませんが、便宜上JSON形式で書いています) { " questions ": { " current ": { " id ": 1 , // 現在の質問id " endTime ": 1545906141 // 回答の締切時間 } , } , " votes ": { // 各ユーザーの回答 " 001EjYAwtSMXlrWWTP5r ": { // (Firestoreが自動付与するid) " answerId ": 0 , // 回答id " questionId ": 2 // 質問id } , " 004gO0YzXUJ2bNFSbc5y ": { " answerId ": 1 , " questionId ": 3 } , . . . } } 回答・質問/結果画面でこれらのデータの変更を購読します。 現在の質問idと回答の締切時間(回答画面・質問/結果画面) import firebase from 'firebase' export default { . . . created() { // this.db = firebase.firestore() this .unsubscribe = this .db.collection( 'questions' ).doc( 'current' ) .onSnapshot((doc) => { const currentQuestion = doc.data() // => // { // "id": 1, // "endTime": 1545906141 // } } ) } . . . } 各ユーザーの回答(質問/結果画面) import firebase from 'firebase' const db = firebase.firestore() export default { . . . created() { // this.db = firebase.firestore() this .db.collection( 'votes' ).onSnapshot((collection) => { this .votes = collection.docChanges().reduce((votes, c) => { const vote = c.doc.data() // => // { // "answerId": 0, // "questionId": 2 // } return { ...votes, [ vote.questionId ] : [ ...(votes [ vote.questionId ] || [] ), vote ] } } , this .votes) // => // { // . // . // . // "2": [ // 質問id毎に集計 // { // "answerId": 0, // "questionId": 2 // }, // . // . // . // ], // "3": [ // { // "answerId": 1, // "questionId": 3 // }, // . // . // . // ], // . // . // . // } } ) } } 結果画面では質問毎に集計を行うので、質問id毎に回答のデータをまとめています。 また、回答のデータに関しては課金を最小限にするため docChanges() を用いて差分だけ取得するようにしています。 一方、管理画面では以下のようなコードでFirestoreに書き込みを行います。 import firebase from 'firebase' import moment from 'moment' export default { . . . methods: { setQuestion() { db.collection( 'questions' ).doc( 'current' ).set( { id: questionId, endTime: moment().add(questionTime, 'second' ).unix() } ) } } . . . } 管理画面で質問を開始するときには、Firestoreに質問idと締切時間を書き込みます。 書き込みが行われた時点でFirestoreから会場の全端末に通知され、一斉に質問が開始されます。 そして、各端末が締切時間に応じて質問の回答を締め切ります。 以上でFirestore連携は完了です。 ここまで来たら、後は画面にアニメーションや装飾を施すのみです。 アニメーションについて 質問/結果画面のアニメーションに関しては TweenMax で実装しました。 TweenMaxについて GSAP(グリーン・ソック・アニメーション・プラットフォーム)モジュールの1つです。 使用可能ライブラリとプラグイン以下のとおりです(要は全部入り) TweenLite TimelineLite TimelineMax CSSPlugin AttrPlugin RoundPropsPlugin BezierPlugin EasePack (後々考えると今回の実装内容だとTweenLiteのみでよかったかも…) 採用した理由 Vue.jsのデータ駆動設計と相性が良さそうだった 複雑なアニメーション作成に適している 構文が直感的でわかりやすい 使用方法 import import { TweenMax } from 'gsap' Methods 今回は主に下記Methodsを使用しました。 値(初期値)を設定する TweenMax.set( target:Object, vars:Object ) 初期値から設定した値にアニメーションさせる TweenMax.to( target:Object, duration:Number, vars:Object ) 設定した初期値から設定した値にアニメーションさせる TweenMax.fromTo( target:Object, duration:Number, fromVars:Object, toVars:Object ) 配列化されたtargetをindex順に設定した初期値から設定した値にアニメーションさせる TweenMax.staggerFromTo( targets:Array, duration:Number, fromVars:Object, toVars:Object, stagger:Number ) 実装サンプル See the Pen countdown by masahito.ando ( @masahito_ando ) on CodePen . 質問画面にて、 questions.current.endTime (Firestoreに書き込まれた締切時間)をフロント側でも setInterval にて監視し、締切時間が残り5秒になったタイミングで上記アニメーションを開始しています。 See the Pen graph by masahito.ando ( @masahito_ando ) on CodePen . 締切時間に到達した時点で結果画面を表示します。 そのタイミングで votes.[Firestoreが自動付与するid].questionId を questions.current.id でフィルタリングして現在の質問の回答データを取得し、YESとNOのパーセンテージとグラフの高さを計算します。計算完了後、上記アニメーションを開始しています。 当日起きたこと 動画と動画の合間に質問を入れるタイミングを画面のスイッチャーと息を合わせる必要があったため、リハーサルで入念にタイミングを確認しました。 実際にリハーサルしてみると、動画と質問の間に違和感があり、スムーズに見えませんでした。 そのため、動画と一体になっているような演出に見えるよう質問の前に真っ白な画面を用意しました。また、動画が真っ白にフェードアウトするようにしてもらいました。 1,000人同時アクセスされた場合ちゃんと動くかテストできずぶっつけ本番だったため、1問目が終わった時にちゃんとデータが入った(動いた)時に 全員でガッツポーズ をしました。 裏方の様子 イベント会社の方が、 こんなリアルタイムでアンケートなんて、これまでやったことないです! どうやってるんですか? と興味津々に食いついていただけて、嬉しかったです。 料金について Firebaseのプランですが、今回1,000人規模かつ初の試みということもあり、従量課金Blazeプランにしました。 イベント終了後、請求金額を見てみると…。 2円 なんと2円でした! 笑 データの作り方にもよりますが、2円の予算で実現できます。 今後の課題 全体を通してうまく作ることできたのですが、当日、質問と質問の間の動画の尺が長く、開いていたiPhoneなどの端末がスリープしておりました。 質問の回答時間15秒だったため、スリープ解除している間に回答時間が終了してしまうという事態が発生してしまいました。 アンケートとしては十分な回答時間でしたが、動画と合わせたときの状況を考え、回答時間が適切かを考えるべきだったなと思いました。 最後に制作メンバーの紹介 今回の制作は弊社 PBグローバル のフロントエンドチームで制作をしましたので、ご紹介させていただきます。 進行管理:高橋( @anaheim0894 ) フロントエンド、アニメーション:安藤 Firebaseなどバックエンド担当:松井( @nahokomatsui )、茨木( @niba1122 )、権守( @AmatsukiKu ) 進行補助:松浦( @mtmn07384 ) こんな突発的かつ刺激的なことや、ZOZOを作ってみたい! という方。 弊社では一緒に作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください。 www.wantedly.com
アバター
こんにちは、ZOZO研究所の岩本です。 2018年12月5日から12月8日にかけて富山で開催された学会 SCIS & ISIS 2018 に、同じく研究所の岩崎と参加してきました。 SCIS & ISISについて SCIS & ISISはソフトコンピューティングと知能システムに関する国際学会です。 SCISとISISはそれぞれ別の学会ですが、今年は共同で開催されました。 また、ISWSというワークショップも同時に同じ会場で開催されていました。 今回はこの学会に参加して他の方の発表を聞いたり、研究成果をポスターで発表したりしてきました。 学会のプログラムは主に、招待講演、トークセッション、ポスターセッションに分かれています。 招待講演では国内外の大学教授の方などがホールでいくつかのテーマについてお話しされていました。 トークセッションでは、いくつかの部屋で20分程度の発表が並列して行われます。 ポスターセッションでは会場に20枚ほどのポスターが掲示され、参加者は興味のあるポスターの前に行って発表者から説明を受けたり、内容に関するディスカッションを行ったりします。 ポスターセッションでは自分たちも発表していたため他の発表を見ることが出来ませんでしたが、招待講演とトークセッションでは色々な興味深い話を聞くことが出来ました。 また、現在共同研究を行っている九州工業大学の古川研究室からも、4人の学生さんがトークセッションとポスターセッションで発表をされていました。 このレポートでは、今回私たちが行った発表の紹介と、その他の発表の中で特に興味深かったものを紹介したいと思います。 ZOZO研究所のポスター発表について ZOZO研究所からは「Visualization of User-item Rating Matrix by Hierarchical Tensor SOM Network(階層型Tensor SOMネットワークによるユーザー・アイテム評価行列の可視化)」というタイトルでポスター発表を行いました。著者は岩本海童、岩崎亘、古川徹生(九州工業大学)です。ここでは、簡単にその概要をご紹介します。 現在、岩崎と岩本(私)のチームは 九州工業大学との共同研究 で、大規模な関係データの可視化を研究しています。 関係データの可視化と言ってもイメージが難しいと思うので、例を使って説明してみます。 何人かの人にいくつかのアイテム(例えば食べ物など)の好みについて質問したアンケートデータがあるとします。 このようなデータを解析すると、「どんな人がどんなアイテムを好んでいるのか」や「ある人と好みが似ている人たち」「あるアイテムと好む人の傾向が似ているアイテム」を直感的に可視化することが出来ます。 しかし、ユーザーやアイテムの数が多い場合には、個別のユーザーやアイテムだけではなくもう少し俯瞰的な視点でデータを見たいことがあります。 例えば、「甘い食べ物が好きなのはどのユーザーなのか」「30代のユーザーはどんな食べ物が好きなのか」などです。 そこで、私たちは関係データと一緒にユーザー属性情報とアイテム属性情報を解析してみました。 属性情報とは、例えばユーザーの場合は性別や年齢層、アイテムの場合はカテゴリーや価格帯などです。 その結果、「あるアイテムはどんなユーザー属性の人に好まれているのか」や「あるユーザー属性の人は、どんな属性の商品が好きなのか」といった情報を視覚的に確認できるようになりました。特に後者が重要です。先行研究ではユーザー属性だけを利用していたのですが、今回はユーザーとアイテムの両方を一緒に解析し、それらの関係を可視化しました。 そうすることにより、 ユーザーとアイテムそれぞれ について、 個別のデータレベルの視点と属性レベルの2つの視点から関係データを視覚的に見る ことができるようになりました。 文章だけだとよく分からないと思うので、可視化の様子の一例を簡単に図にしてみました。 実際はこのように静的なものではなく、フォーカスするものを切り替えながら、インタラクティブに可視化された結果を確認できます。 (それを表現するにはポスターでは限界があったので、発表ではiPadで可視化のデモを行いました) 今回の発表では寿司の好みに関するアンケートデータを利用しましたが、最終的にはこの手法を使ってZOZOの購買データやWearのコーディネートのデータを可視化したいと考えています。 膨大なデータを可視化することで、今まで知ることのできなかったデータの一面が発見できるかもしれません。 また、今回発表したポスターで、 ポスターセッションアワードを頂きました 。 全く予想外の出来事で、大変嬉しく思います。 ポスター発表の様子 こちらはポスターの前で参加者に研究を説明しているところです。インタラクティブな可視化を見ていただくためにiPadでのデモンストレーションも行いました。 ポスターの前に立つ私です。 その他の発表の紹介 ここでは、学会で視聴した発表の中から特に印象的だったものをいくつかを紹介します。 なお、見出しの下の斜体の文は、発表者名と私が訳したタイトルです。 Cognitive Mechanism of Humor in Riddles: Examination of Relationship between Humor and Semantic Structure of Riddles Asuka Terai, 「謎かけのユーモアの認知メカニズム:ユーモアと謎かけの意味構造の関係の調査」 謎かけを自動生成して、その面白さと使われている単語との関係を調べる研究です。 ここでは、「AとかけてBと解く。その心はC(C')」(AとBはそれぞれ名詞で、CとC'は同音異義語でそれぞれAとBに関係がある)というような限定したスタイルの謎かけを扱います。 AとBの単語をランダムに選び、それぞれに意味が近い単語CとC'をWikipedia日本語版のコーパスとword2vecを使って選ぶことで、謎かけを生成します。 あらかじめ用意したいくつかの謎かけについて、ユーモアがあるかどうかのアンケートを行い、またA-C, B-C', C-C'間の単語の類似度を調べました。 その結果、A-CとB-C'の類似度が共に高い謎かけは高評価が得られやすかったそうです。しかし、生成した謎かけはA-CとB-C'が共に高くなることはあまりなかったそうです。 感想 ビジネス寄りの実用的な研究が多い中、こういうある種の「遊び」に関する研究はすごく面白いなと思います。 もっと複雑な文章の謎かけを生成したり、あるいはユーモアに関してもっと多角的に研究(ユーモアの種類、地域や世代などによる感性の違いなど)してみたりすると面白そうだと思いました。 Non-parametric Continuous Self-Organizing Map Ryuji Watanabe, 「ノンパラメトリックで連続な自己組織化写像」 九州工業大学の渡辺さんの発表です。 (現在行っている共同研究にも関連する内容です) 自己組織化写像(self-organizing map, SOM)は高次元のデータの低次元表現を離散的に求める手法です。 この研究では連続した空間での低次元表現を求められるようにSOMを改良しました。 また、求められる低次元表現はデータ点だけに依存する(ノンパラメトリック)ようになっています。 その結果、ユニット数を増やした高解像度なSOMと同じような表現を得られることが実験から確認できたそうです。 感想 離散化した写像を求めるSOMを、連続かつノンパラメトリックに拡張する流れがとても自然で美しいです。 データ数が増えた場合にいかにメモリと計算時間を節約するかが実用に向けた課題だと思います。 Visualized Onomatopoeia Thesaurus Maps based on Deep Autoencoder Daiki Urata, 「深層オートエンコーダーによる類似オノマトペのマップ」 オノマトペの類似度を可視化する研究です。 オノマトペを音声に変換したものをオートエンコーダーにかけて2次元の表現を得ます。 それを2次元上に表示すると、似た音のものや、同じカテゴリー(汁をすする音、飲み物を飲む音など)のものが近くに配置されるそうです。 未知のオノマトペに対しても、同様に2次元の表現を得て、そこからもっとも近くにあるオノマトペのカテゴリーをそのオノマトペのカテゴリーとします。 その結果、高い精度で未知のオノマトペのカテゴリーを推定できたそうです。 感想 オノマトペの研究というのを初めて知ったので、とても興味深かったです。 ファッションを表現する際にもオノマトペを使うことがあるので、雰囲気や質感を表す言葉から服やコーディネートを提案できたら面白いなと思いました。 An Extreme Learning Machine Based Pretraining Method for Multi-Layer Neural Networks Pavit Noinongyao, 「エクストリームラーニングマシンに基づく多層ニューラルネットワークの事前学習の手法」 エクストリームラーニングマシン(ELM)はある種のニューラルネットワークですが、勾配法を使わず、1層目の重みをランダムに初期化し2層目の重みを逆行列を使って決定的に求めます。 このELMをベースにしたオートエンコーダーを用いると、多層ニューラルネットワークの事前学習を行うことができます。 この研究では、通常のELMとは逆に2層目をランダムに初期化し1層目を計算するbackward-ELMという手法を提案し、事前学習の手法として有用であることを実験により示しました。 感想 ELMという手法について詳細を知らなかったのですが、勾配法を使わずに一気に重みを計算するというのはとても大胆だなと思いました。 時間があるときに詳しく調べてみたいです。 参加した感想 実は、今回は私にとって初めてのポスター発表でした。 私は人の前で話すのがあまり得意ではないのですが、視聴者の方々が優しく聴いてくださり、説明が不十分なところを適宜質問していただいたのでなんとか研究の内容をお伝えできたように思います。 また、今回は可視化の研究であり、iPadでのデモンストレーションにより視覚的に相手に訴えることができたというのも良かった点かなと思っています。 ポスター発表は1人(または少数の人)を前にして相互にコミュニケーションしながら発表を行うことができます。 これは多くの人の前で行う口頭発表にはない良さですが、一方で事前に頭の中で発表する内容をよく整理しておく必要があることに気づきました。 次にポスターで発表する機会があれば、もっとスムーズに説明できると良いなと思っています。 招待講演やトークセッションでは、色々な分野の発表を聞くことができて、大変勉強になりました。 また、今回の学会では自分の英語の能力の足りなさを実感しました。 今後、もっと英語の発表を聴いて良く理解したり、もっと英語で情報を伝えられるようになりたいと思いました。 おまけ 上に書いていない学会や富山の様子を写真で一部ご紹介します。 九州工業大学の学生さんたちによる発表 古川研究室から4人の学生さんがトークセッションとポスターセッションで発表されていました。 晩餐会 学会の最終日の前日の夜に、晩餐会が行われました。 運営の方の挨拶や学会の歴史についての紹介が行われた他、杖道のパフォーマンスも行われました。 富山の食べ物 富山名物の、富山ブラックと呼ばれるラーメンです。 元々、肉体労働者に食べてもらうために作られたこともあり、かなり醤油の塩味が効いています。 味が濃すぎるあまり途中でライスを注文しましたが、1杯では全然足りませんでした。 そして、富山といえば何と言っても海の幸です。 私たちも学会の期間中、何回かお刺身や焼魚などの海の幸を味わいました。 また、魚と一緒に飲む富山の日本酒も美味しかったです。 最後に、岩崎と私で会場前でパシャリ。 お疲れ様でした。 おわりに ZOZO研究所では、ファッションに関する研究を行なっています。 私と岩崎のようにデータの可視化を研究しているチームもあれば、コーディネートの研究をしているチームもあり、日々さまざまな課題に取り組んでいます。 あなたも一緒にZOZOのデータ資産を活用して「似合う」を研究しませんか? ご興味のある方は下のリンクからご連絡いただき、ぜひ一度オフィスに来てください。 研究所のオフィスは青山と福岡にあります。 www.wantedly.com www.wantedly.com
アバター
こんにちは。開発部の廣瀬です。 本記事では、昨年障害が発生してしまったZOZOTOWNの福袋発売イベントについて負荷対策を実施し、今年の福袋イベント期間を無傷で乗り切った話をご紹介したいと思います。 大規模サイトの障害に関する生々しい話はあまり公開されていないように思いますので、長くなってしまいましたが詳細に書いてみました。尚、今回のお話は弊社のサービスで使用しているDBMSの1つである、SQL Serverに関する話題がメインです。 福袋イベント「ZOZO福袋2019」とは 年に1度、多数のブランドの福袋が一斉に発売される、ZOZOTOWNの年末の風物詩的イベントです。今年は450以上のブランド様にご参加いただきました。お客様からも毎年大変ご好評いただいており、年間を通して最も多くのトラフィックを記録するイベントの1つです。 アクセスが殺到するが故に、昨年は福袋の発売直後からエラーが多発し、一時的に買い物し辛い状態を発生させてしまいました。昨年も負荷対策を実施していたのですが、それでもエラーが多発する状況となってしまい、エンジニア一同、お客様にご不便をおかけして申し訳ない気持ちで一杯でした。 今年こそは絶対に何事もなく福袋イベントをお客様に楽しんでいただこうと、負荷対策に力をいれましたので、順を追って紹介します。 昨年の福袋イベントで発生した障害について 昨年の福袋イベントにおける障害を時系列でまとめると以下のようになります。 12:00 福袋発売開始。 12:01 開始直後のみエラー無く好調だったものの、1分後から大量のエラーが発生。 主にDBサーバーへの接続エラーであった。 必ずエラーが発生する状態ではないものの、購入フローの途中でエラーとなるケースが多発。 注文し辛い状態が続く。 13:40 エラー多発状況が自然解消され、スムーズに注文ができるようになる。 注文数の推移からも、障害の影響が確認できます。 最初の1分間のみ多くの注文が入っています。しかし、その後急激に注文が入らなくなっています。13時40分ごろ急激に注文数が増大したことから、潜在的にはより多くのご注文をいただける余地があったと考えられます。購入フローにおいて、注文確定までの数回の遷移のいずれかでDBの接続エラー発生によりなかなか注文完了できなかったと思われます。 障害の原因調査 適切な負荷対策を実施するためには、昨年発生した障害の原因を突き止める必要があります。そのために調査を実施しました。原因調査には、障害発生時にリアルタイムで収集しておいた 動的管理ビュー の情報と、弊社で導入している監視製品の Spotlight を使用しました。 Spotlightについて簡単に説明しますと、サーバーの様々な情報を定期的に収集しておいて、後から任意の時間帯のサーバー状況を確認できるソフトウェアです。CPUやメモリなどのリソース使用状況や、秒間バッチ実行数、コネクション数などの各種メトリクスから実行中のクエリテキストまで様々な情報が保存されます。そのため、事後調査の際に重宝します。 サーバーの状況をSpotlightで追っていきます。秒間のバッチ実行数をSpotlight上で確認します。 12時の開始直後は普段の約5倍のクエリ実行要求があったようです。その後急速にバッチ実行数が低下しています。この波形が自然でないことは、エラーが大量に発生していた事実からも、注文数の推移からもいえると思います。一度は大量のバッチ実行数を記録したDBサーバーが、何らかの原因で実行数を抑えつけられているようです。DBの内部で起きていたことをさらに調査していきます。 コネクション数の推移です。SQL Serverは仕様で同時接続数が最大32,767と決められており、上限を超えるとクライントにはエラーが返されます。平常時より高いコネクション数で推移しているものの、上限値まで達するということは無く余裕があり、コネクションボトルネックではありませんでした。 次に、障害発生中のDBサーバーの待ち状態の傾向を確認します。 SQL Serverでは、クエリ実行要求を受け付けてから実行完了するまでの間に生じた待ち時間を、ロックなどの待ち事象の項目ごとに確認できます。この情報は動的管理ビューの1つである sys.dm_os_wait_stats から取り出すことができます。SpotlightからもこのViewと同等の情報を確認できます。 秒間の累積待ち時間が多い順に並び替えると、平常時は以下のような傾向を示します。 一方で、障害発生時は以下のような状況でした。平常時と比べて、まったく違う傾向を示していました。また、各項目の待ち時間も非常に高い数値を示していました。 ここでは、上位5つのWaitTypeについて紹介します。 THREADPOOL : ワーカースレッドの確保待ち LCK_M_S : 共有ロックの獲得待ち PAGELATCH_SH : 共有ページラッチの獲得待ち LCK_M_U : 更新ロックの獲得待ち PAGELATCH_EX : 排他ページラッチの獲得待ち 「ワーカースレッド」と「ページラッチ」については、聞いたことのない方もいらっしゃるかもしれませんので、それぞれの用語について少し補足をしておきたいと思います。 ワーカースレッドとは、SQL Serverのクエリ実行時にスレッドで使用されるリソース、ページラッチは、SQL Serverのデータ格納領域である「ページ」の一貫性を保つための排他制御の仕組みとイメージしていただければと思います。 (SQL Serverでは、1ページあたり8KBで管理され、ページの中には同一テーブルのレコードが複数入っています。1ページあたり何レコード格納されるかは、1レコードあたりのデータサイズに依存します。) 先ほどの、障害発生時の待ち事象の1位はTHREADPOOLのwaitが圧倒的に多く、次いでページラッチ系とロック系のwaitが2位から5位までを占めていました。平常時は、これらのwaitが多く発生することはありません。 CPUリソース不足時に高い数値を示すSOS_SCHEDULER_YIELDの値はTHREADPOOL待ち時間の100分の1程度でした。そのため、CPU負荷もボトルネックでは無いと判断しました。 THREADPOOL waitが圧倒的に多かったため、多発していたエラーとの関連性を調べるために、ローカル環境のDBでワーカースレッドを意図的に枯渇させてクエリを実行してみました。その結果、本番環境で多発していたものと同様のエラーが発生することを確認できました。 このことから、多発していたDB接続に関するエラーは、ワーカースレッドが枯渇したことが、原因の一つであると判断しました。ということで、昨年の福袋の障害の原因は、DBの状態としては「ワーカースレッドの枯渇により大量のTHREADPOOL waitが発生した」ということが確認できました。 ここで、「ではワーカースレッドの最大数を増やせば解決するのでは?」という疑問がわきます。 ただし、現状のワーカースレッドでも平常時の5倍の要求を一時的に受け付けていることから、単純にワーカースレッド数を増やせば解決する問題でもなさそうです。 次に、ワーカースレッドが枯渇してしまった原因について調べました。Spotlightを使って、エラー発生時に実行中だったクエリリストを確認します。 「Last Wait Type」「Last Wait Resource」から、同一のリソースに対して、大量のページラッチ獲得待ち(PAGELATCH ****)と、キーロック獲得待ち(LCK_M **)がそれぞれ発生していることが確認できます。「LCK_M_U」の情報から、獲得しようとしているロックの粒度がKEYであるため、特定のレコードに対する読み取り要求、更新要求が大量に実行されていることが分かります。 「PAGELATCH_SH」「LCK_M_U」の「Last Wait Resource」に表示されているリソースの値だけでは「どのデータが対象になっていたのか」までは、判断することができません。そこで、次の方法を使用して、データの取得を行いました。 PAGELATCH_SH で出力されていた、Last Wait Resourceの値を使って、具体的にどのテーブルのどのようなレコードが含まれているページへのアクセスであるかを調査しました。 ページラッチ待ちが多発しているLast Wait Resourceの値と「DBCC PAGE」を使ってページの中身をダンプさせます。 DBCC TRACEON( 3604 ) DBCC PAGE (N 'DB名' , 1 , 4xxxxx8, 3 ) DBCC TRACEOFF( 3604 ) 次に、ロック待ち(LCK_M_U)が多発しているLast Wait Resourceについて対応するテーブルとレコードを特定します。KEY:DBID:hobt_id(%%lockres%%)という構成になっているため、下記のクエリを使います。 --1.hobt_idを指定してテーブル名を取得 select sc.name as schema_name ,so.name as object_name ,si.name as index_name from sys.partitions as p join sys.objects as so on p.object_id = so.object_id join sys.indexes as si on p.index_id = si.index_id and p.object_id = si.object_id join sys.schemas as sc on so.schema_id = sc.schema_id where hobt_id = 72xxxxxxxxxxxxx28; --2.取得したテーブルのレコードを、%%lockres%%を使って絞り込んで特定 SELECT * FROM テーブル名 WHERE %%lockres%% = '(4bxxxxxxxx5d)' ; 以上のクエリを実行した結果、ページラッチとロックは同じテーブルの特定レコードに関連する待ちであることが分かりました。購入フローの中でDB上の在庫に関するデータを更新する処理があるのですが、人気福袋の購入希望者が殺到したことで読み取り要求、更新要求の競合が多発してボトルネックとなっていたようです。 図示すると以下のようになります。 ロックはレコード単位で競合するのに対し、ページラッチはページ単位で競合します。そのため、ページラッチについては、人気福袋と同一ページにある他の福袋商品へのアクセスとも競合してしまっていました。 障害発生中、ページラッチ待ちとロック待ちが発生しているクエリは、実行中の全クエリの約半分を占めていました。そのため通常よりワーカースレッド解放に時間がかり、大量のDBアクセスを捌ききれなくなり、ワーカースレッド確保待ちが大量に発生してエラーの多発につながったと考えられます。 この状況を図示します。まず、SQL ServerのCPUのアーキテクチャの概要図です。 SQL ServerはCPU1コアにつき1つのスケジューラが割り当てられます。各スケジューラに対して複数のワーカースレッドが割り当てられます。クエリ実行時にワーカースレッドが1つ以上利用され、クエリの実行が行われます。SQL Server では、作成できるワーカースレッドの数に上限があります。(作成できるワーカースレッド数の上限は、「max worker thread」という設定で変更できます。) 実行されていたクエリのうち、ページラッチとロック競合が発生していなかったクエリは全体の半分を占めていました。これらのクエリではワーカースレッドの利用と解放が高速に行われていました。一方で、解放および競合が発生していたもう半数のクエリでは、ページラッチやロックといったリソース獲得待ち状態で、ワーカースレッドの解放が遅くなっていました。結果として利用可能なワーカースレッドの低下による全体のスループットが低下し、にもかかわらずクエリの要求数は増え続け、慢性的なワーカースレッド枯渇状態に陥って大量のエラー多発、というシナリオだと思います。 人気商品以外の商品を閲覧、購入されていたお客様は、ワーカースレッドが確保できてしまえば、高速に要求を処理できていたと考えられます。一方で人気商品へのアクセスは、ワーカースレッドを運よく確保できたとしても、実行時にロック獲得待ちなどの理由で実行時間がかなり伸びてしまいます。そのためタイムアウトも発生しやすい状況であったはずです。 ただし、12時からの1分間は大量に注文が入っていることから、その後のエラー多発状態へと移行したタイミングで何が起きていたかの説明はできていません。 12時1分を過ぎたタイミングから、ロックおよびページラッチ獲得待ちが大量に発生していることから、ブロッキング(ロック競合)が発生していたと考えられるため、Spotlightでブロッキングのグラフを確認しました。 ブロッキングが大量に発生しています。ブロックされているプロセス(オレンジ色)が多数あるのに対して、ブロックを引き起こしているプロセス(青色)の数は多くありません。少数のプロセスがその他大勢のプロセスをブロックしていることが分かります。ブロッキングの情報をさらに追っていきます。 ↑13時15分時点のブロッキング情報の一部です。Head Blocker(黄色線の情報、ブロッキングを発生させる原因となっているプロセス)のStatusが「Seeping, blocking」となっています。つまり、スリープしているプロセスが、大量のプロセスをブロックしていたことになります。「スリープしてるのにブロックしてる」とはどういう状況なのだろうと調べてみたところ、同じような症状に関する記事を見つけました。 Sleeping SPID blocking other transactions この記事によると、明示的なトランザクションを張ったまま途中でタイムアウトしてしまうと、クエリの実行方法によっては、ロールバックされずトランザクションが継続された状態になります。したがってロックを獲得している場合はロックも保持し続けることになります。ローカル環境にて実験したところ、確かに再現しました。 ただし、ローカル環境で試す中でタイムアウト後に即座にロールバックされる場合もありました。調べたところ、明示的にトランザクションを開始し、ロックを保持したままタイムアウトした場合、そのロックの解放タイミングは「コネクションプールに戻ったコネクションが別のクエリによって再利用されるとき」でした。それは0.1秒後かもしれないし、数分後かもしれません。 ※コネクション/コネクションプールの仕組みについては この記事 で非常に詳しく書かれていました。 昨年の福袋イベントのケースにあてはめて考えると、大量にアクセスされていた状況ならすぐにコネクションが再利用されることでロックが解放されてもおかしくないのでは?という疑問がわきます。この疑問を検証するために実験を行ったところ、ロールバックされるためには、コネクションの再利用時にまずワーカースレッドを確保する必要があるとの結論に至りました。 これらの調査を踏まえて、福袋発売後に以下のシナリオが発生してバッチ実行数が著しく低下したと考えました。 1.購入フローにおいて、人気商品に関するデータへの読み取り要求、更新要求が大量に発生。 それに伴い特定のレコードに対してのロック、ページラッチの獲得待ちが発生。 2.人気商品データのロックを獲得したプロセスAが処理の途中でタイムアウト。 ロールバックされず、取得したロックが解放されないままコネクションプールに戻る。 3.別プロセスが該当コネクションの再利用を試みるも、 ワーカースレッド枯渇のためTHRADPOOL waitが長時間発生してエラー発生。 そのためロールバックとロックの解放が長時間おこなわれなかった。 4.プロセスAが保持したロックによって、他のプロセスが大量にブロックされる。 (複数の人気商品において2~4の事象が発生) 5.ブロッキングによって各プロセスの実行速度が急速に低下。 ワーカースレッドの解放が遅くなり全体としてスループットが低下した。 昨年の障害発生原因を踏まえた対応を実施 ここまでのまとめ ・昨年の福袋イベント中にエラーが多発したのは、DB内部でワーカースレッド確保待ちが大量発生したため ・ワーカースレッド確保待ちが大量発生したのは、人気福袋データに対するクエリ実行要求が集中し、かつタイムアウトしたプロセスがロックを解放しないことでブロッキングが急激に増大したため ・昨年の障害発生時には、以下の五項目の待ち時間が圧倒的に多かった THREADPOOL LCK_M_U LCK_M_S PAGELATCH_SH PAGELATCH_EX 以降では、昨年の障害調査を踏まえて実施した負荷対策についてまとめます。 対策1. プロセスをSleepingかつBlockingにさせない タイムアウトしたプロセスがそのままロックを掴みっぱなしになっていたことが、ブロッキングの状況を加速度的に悪化させた原因のため、タイムアウト時のトランザクションを適切に処理する必要があります。 対応方法としては、 SET XACT_ABORT ON をトランザクション開始前に設定しました。このオプションは、トランザクション内でエラーが発生すると即座にロールバック+ロックの解放を行うように指示できるオプションです。 この対策を実施することで、特定のクエリがタイムアウトした途端にブロッキングが加速するというリスクを無くすことができます。 対策2. 昨年多く発生していた待ち事象を減らす ■ THREADPOOL この待ち事象は、ブロッキングが大量に発生したためにワーカースレッドの解放が遅れたことが原因と考えられるため、THREADPOOL Waitを減らすためには、ブロッキングを減らす必要があります。ただし、万一ブロッキングが大量発生する等の理由でワーカースレッドが枯渇するときのことを考慮し、ワーカースレッドの最大数を2倍に増やしました。合わせて、maxサーバーメモリの値を減少させました。これは、ワーカースレッドの確保にはメモリが必要となり、このメモリはバッファプール用に確保しているメモリ(maxサーバーメモリ)とは別の領域から確保する必要があるためです。 そのため、maxサーバーメモリを減少させ、ワーカースレッドを作成する際に必要なメモリを確保できるように、メモリサイズの調整を行いました。 この対応により、THREADPOOL Waitを削減できるわけではありませんが、大量にワーカースレッドを使用する環境下においては同時実行性能の向上が期待できます。 ■ LCK_M_U 購入フローの一部にDB上の在庫に関するデータを更新する箇所があり、人気商品の購入希望者が殺到すると、人気商品の在庫に関するデータレコードの更新処理で大量のブロッキングが発生していました。これを軽減させるために、人気商品に関しては、サイト上の見え方およびユーザーには影響ない形で在庫に関するデータを分割して販売しました。 ■ LCK_M_S 一部のクエリにwith(nolock)というロックヒントを付与することで、Sロックよりも競合が少ないSch-Sロックを取得するに留めさせました。もちろん、ダーティリードを許可できる箇所である、というのが前提となります。 ■ PAGELATCH_SH / PAGELATCH_EX 獲得できるページラッチは1ページあたり1つだけのため、同一ページ内の異なるレコードへの更新要求同士であってもページラッチの競合は発生します。そのため、1ページの中に福袋商品の在庫に関するデータレコードが1つだけ存在する状況を意図的に作り出しました。具体的には、福袋商品以外のレコードは、サイト上からは見えないダミーの在庫に関するデータレコードで埋めました。これにより、ページラッチ競合の改善が期待できます。 これらをすべて対応した際のイメージを以下の図にまとめました。 昨年の状況と比較すると、以下のように読み取り要求、更新要求に伴う排他制御の分散が期待できます。 当日のモニタリング環境の整備 昨年の障害時はワーカースレッドが枯渇していたため、クライアントソフトから情報収集のためのクエリを実行することすらできない時間帯がありました。そこで今回は、ワーカースレッドの枯渇に備えて、 DAC で事前に接続しておき、いかなる状況でもDBから情報を採取できる体制を整えておきました。DACで接続すると、専用のワーカースレッドをつかえるので、ワーカースレッド枯渇時でもクエリが実行できます。今年はワーカースレッドが枯渇することはありませんでしたが、準備しておいたことでいつでも情報取得できるという安心感につながりました。 今年の福袋イベントについて 本記事で紹介した負荷対策を実施したことで、今年は福袋イベントを障害無しで乗り切ることができました。 以下のグラフは、去年と今年の福袋イベントにおける、分間注文数の推移をグラフにしたものです。 今年は、開始と同時に瞬間的に注文数が跳ね上がり、その後ゆるやかに減少しています。今年はほぼエラーが起きていないため、あるべき注文数の推移であり、お目当ての商品をスムーズに購入していただけたようです。昨年と今年の波形と比較すると、昨年は障害発生によって注文数の波形が不自然であることがよくわかります。 また、DB起因のエラーも、昨対比で99.99%以上削減でき、平常時とほぼ同じようなサーバー負荷状況でした。 まとめ 本記事では、昨年障害を発生させてしまった福袋イベントについて、障害原因に関する調査内容をご紹介しました。また、調査結果を踏まえて今年実施した負荷対策についても紹介しました。 人気商品にアクセスが集中するという特定の性質をもったワークロードにおいて、同時実行性能をできる限り向上させることに注力したことが功を奏しました。性質が異なれば実施すべき対応策も変わってくるため、ワークロードの性質を適切に把握することと、障害発生時はできる限り根本的な原因を特定することが、何よりも大切だなと感じました。 今年はお客様にストレスなく福袋イベントを楽しんでいただけたと思いますので、エンジニア一同がんばってよかったです! ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://www.wantedly.com/companies/zozo-tech/projects
アバター
こんにちは、ZOZOテクノロジーズ VPoEの今村( @kyuns )です。 この記事は ZOZOテクノロジーズ Advent Calendar の25日目の記事になります。 今年の4月にスタートトゥデイテクノロジーズ(現ZOZOテクノロジーズ)が発足してから約8ヶ月が経ちました。新型ZOZOSUITやプライベートブランド「ZOZO」の発表など今年は色々と新しいチャレンジをしていた弊社ですが、外から見たときにエンジニア観点だとまだまだ謎めいている部分がたくさんあると思います。 ちょうど先日代表の前澤がツイートしたことでTwitter採用が話題になりましたが、反響も非常に大きく、多くの方にご応募いただき実際に何名かのエンジニアを採用することができました。 news.yahoo.co.jp この祭りでも 非常に多くの質問 をいただきましたが、このエントリではZOZOテクノロジーズが行っている事業やプロダクトの話を交えつつ、ZOZOテクノロジーズが求めているエンジニア職を改めて紹介したいと思います。 この記事が少しでも弊社を知ってもらう機会となれば幸いです。 ZOZOテクノロジーズとは? 株式会社ZOZOテクノロジーズは株式会社ZOZOの100%子会社です。 ZOZOとZOZOテクノロジーズ、それぞれ主な役割が違っており、ZOZO本体にはZOZOTOWNを運営するために必要なスタッフ、例えばEC営業・マーケティング・カスタマーサポート・ZOZO BASEなどの物流まわりのスタッフやプライベートブランドの服作りをするスタッフなどが在籍しています。 一方ZOZOテクノロジーズにはWebサイトやアプリ、システム開発を行う為に必要なスタッフ、エンジニア・デザイナー・アナリスト・リサーチャーなどが在籍しています。 2018年12月25日時点ではZOZOテクノロジーズには約250名の従業員が在籍し、そのうちエンジニアは約150名ほどになります。 企業理念、事業理念、経営理念 我々の企業理念はZOZO本体と同じく 「世界中をかっこよく、世界中に笑顔を。」 テクノロジーズの事業理念は 「70億人のファッションを技術の力で変えていく」 です。 事業理念が示すとおり、我々の対象は世界70億人、そして領域はファッション、その手段として技術を重要視しています。 経営理念は 「いい人をつくる」 です。 ここでいういい人とは以下の3つを満たした人物です。 想像力と創造力に富んだ人 GIVE&TAKEのバランスがとれた人 いい人を作れる人 また、 前澤代表のメッセージ にもあるように 我々は 「楽しく働く」 ことを徹底するような行動を心がけています。 どんな事業が存在するのか? 現在ZOZOテクノロジーズが関わる主な事業は以下の4つになります。 日本最大級のファッションECサイト「ZOZOTOWN」 自分サイズのベーシックアイテムブランド「ZOZO」 日本最大級のファッションコーディネートアプリ「WEAR」 ファッションに関する研究開発を行うZOZO研究所 それぞれの事業が行っているプロダクト開発の内容を、どのようなエンジニアが向いているのかということも含めて紹介していきたいと思います。 ZOZOTOWN ZOZOのメイン事業である日本最大級のファッションショッピングサイトです。 年間購入者数は730万人以上と非常に多くのユーザーに利用されています。 ZOZOTOWNの特徴としてはシステム開発・デザイン・物流など、ECに関わる機能を全て自前で開発しています。そのため色々な箇所でエンジニアの力が必要になってきます。 募集職種 ZOZOTOWN SRE/インフラエンジニア(オンプレミス) ZOZOTOWNおよびWEARのオンプレのインフラを支えるエンジニアになります。 現在ZOZOTOWNのインフラのほとんどはオンプレに存在しています。画像サーバーは一部AWSに置くなど、クラウドへの移行も進めていますが、まだまだオンプレの役割も重要です。そのため、オンプレに詳しいインフラエンジニアが必要となります。オンプレでの運用経験があってハードウェアやネットワークに詳しいけど、クラウドにも興味があるというようなインフラエンジニアが非常に向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / 仮想化基盤 / 各種NW機器 ZOZOTOWN フロントエンドエンジニア(マークアップ) ZOZOTOWNのサイト改修やコーディングを行うエンジニアです。フロントエンドには現在ZOZOTOWNのUI改善や運用を行うチームと、特集などの企画ページの開発を行うチームがあります。ZOZOTOWNのトラフィック規模になると少しのUI改善が売上に直結します。たとえ0.1%の改善でも売上への影響は非常に大きく、様々なUI改善の施策をやりがいをもって開発することができます。また後者のチームでは、非常に多くの企画や施策を行います。しかもその企画を通した売上規模は何十億円となることもあります。どちらのチームともUI/UXにこだわりをもって開発していけるようなエンジニアが向いています。 また、モダン化も並行して進めているので、今のZOZOTOWNのフロントエンドをもっと新しくパフォーマンスよくしたい!という方、大歓迎です。 利用技術: HTML / CSS / SASS / JavaScript(ES5) / jQuery ZOZOTOWN フロントエンドエンジニア(iOS/Android) ZOZOTOWNのiOS/Androidアプリの開発を担当するエンジニアです。 利用ユーザーも非常に多く、どの画面をとってもUI/UXの改善が使い勝手や売上に直結します。 また、QAエンジニアと連携してテストの自動化などを行ったりもしており、より効率的な開発を行えるような基盤づくりなどもしています。とにかくこだわりを持って大規模ECサイトのアプリを開発したいアプリエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage / Bitrise Android: Android Studio / Java / Kotlin / CircleCI ZOZOTOWN バックエンドエンジニア(Web/API) ZOZOTOWNのサーバーサイドの開発を行うエンジニアです。サーバーサイドといっても非常に多くの機能があり、既存機能の改修や新機能の開発、iOS/Androidアプリに必要なAPIの開発なども行います。 商品検索やカート、決済処理部分など、ZOZOTOWNの非常に重要な部分を担う開発を担当しています。 商品検索を改善していきたい、様々なミドルウェアを扱いたい、APIの設計にこだわりをもってやりたい、と思えるようなエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server / ElasticSearch ZOZOTOWN バックエンドエンジニア(リプレイスプロジェクト) ZOZOTOWNの既存システムをクラウド移行するための開発を行うエンジニアです。 凄まじい勢いで成長してきたZOZOTOWNは、VBScriptを用いた開発を始めとして、システム的にもレガシーな部分が存在しているのが事実です。 そのため、このような技術的負債に対応するために将来的な拡張性や柔軟性の確保を見据えて、既存のオンプレ環境をパブリッククラウドへと移行するプロジェクトを進めています。単純にクラウドへ移行するといってもその規模は巨大ですし、システムの停止は極力避けなければなりません。 大規模プロジェクトになるため、いくつかのフェーズに分けて現在取り組んでいます。 アーキテクチャとしてはDocker/Kubernetesを中心としたクラスタ構成になっており、セールなどの負荷にも柔軟に耐えれるようなAPIの開発を行っています。 モダン技術を用いて巨大なAPIやインフラ基盤の開発を行いたいエンジニアが向いています。 利用技術: SQL Server / Java / Spring / Azure / Docker / Kubernetes / NewRelic / Wercker / SonarQube / Datadog / PagerDuty / Prometheus / Sentry リプレイスまでの道程や詳細については、 今月号のWEB+DB PRESS Vol.108 で特集されていますので、気になった方はぜひご覧ください。 ZOZOTOWN バックエンドエンジニア (マーケティングオートメーション) 何百万人ものZOZOTOWNのユーザーとの接点を受け持つ部分の開発を支えるエンジニアです。 プッシュ通知やメルマガ配信、LINE@のメッセージ配信など、ユーザーとのコミュニケーションを技術の力でより良くしていく部門であり、A/Bテストやデータを用いたレコメンデーションなどを行います。 ユーザーの行動に応じて、様々なチャネルで、最適な情報を届けるためのリアルタイムマーケティングシステムと呼ばれるシステムの開発も行います。 また、社内に存在するあらゆるデータを集約させるためのデータ基盤の開発も行っています。 データ基盤にはBigQueryを用いており、fluentd・Embulk・Digdagなども活用しています。 ここに集められたデータはサービスの改善や研究開発などに利用されます。 データの規模も非常に大きく、何百億レコード、数百テーブル、リアルタイムで飛んでくるイベントログなどをうまくさばく技術が必要になります。 データをつかってユーザーに最適な情報を届けたい、ユーザーとの接点をより良くしたい、大量のデータをうまく扱えるような巨大データ基盤を開発したい、と思うようなエンジニアが向いています。 利用技術: AWS / GCP / JBoss Data Grid / Aurora / Java / Ruby / fluentd / Embulk / Digdag ZOZOTOWN バックエンドエンジニア(基幹システム) ZOZOTOWNのバックオフィスと呼ばれる裏側を支えるシステムの開発を行うエンジニアです。 主に、社内や取引先に開放している販売管理システムや物流業務システムの企画・開発・改修やブランド様などからの問い合わせ対応などを行います。また、弊社は物流ロジスティクスも自前で保有しているため、エンジニアリング視点でどのオペレーションや業務を効率化できるか、という視点がとても重要になります。他社とのデータ連携などを受け持つデータ連携基盤の開発なども行っています。 ZOZOTOWNを支える裏側の仕組みに興味がある人、倉庫や物流などのオペレーションを技術で改善していくことに興味のあるエンジニアが向いています。 利用技術: Windows Server(IIS) / VBScript / SQL Server ZOZOTOWN ロボットエンジニア(BASE/倉庫) ZOZOTOWNの物流倉庫で利用予定のロボットの開発を行うエンジニアです。既にある程度自動化されているZOZOBASEですが、さらなる自動化のために各種ロボットの導入を検討しています。様々な最適化のためにシュミレーションを行ったり、実際にロボットの試作機の開発などを行います。ハードウェアやロボット開発に興味のあるエンジニアが向いています。 利用技術: C / C++ / Python / CAD / Solidworks プライベートブランド「ZOZO」 プライベートブランド「ZOZO」(以下PB)に関する事業です。 PBはZOZOSUITを用いて計測した、ユーザーのサイズにジャストフィットなベーシックアイテムを販売する事業になります。現時点ではTシャツやデニム、シャツやビジネススーツ、ニットなど様々なアイテムを販売しています。PBは日本以外の国でも販売を開始しており、海外向けのグローバルECサイトzozo.comをはじめとしてZOZOSUIT、スマートファクトリーなど最新の技術が活用されるプロダクトになります。 PB事業には以下のプロダクトが存在します。 海外向けグローバルECサイト zozo.com ZOZOSUIT 生産管理/ERP 生産管理/生産マネジメント パターン自動生成 スマートファクトリー PB事業ではウェブサイトだけでなく、ZOZOSUITやスマートファクトリーなど、ソフトウェアだけでなくハードウェアの技術なども必要とされているのが特徴です。 海外向けグローバルECサイト zozo.com プライベートブランド「ZOZO」を海外に向けて販売していくためのサイト「 zozo.com 」の開発を行います。海外の拠点とやり取りする機会も多く、業務内容によっては海外出張に行くこともあります。 募集職種 グローバルECサイト フロントエンドエンジニア(Web) グローバルECサイトのフロントエンドの開発を行います。グローバルECサイトのフロントエンドは現在TypeScriptとVue.jsを用いたSPAで作られており、多言語対応や決済機能まであるSPAの規模としても非常に大きいものになります。また、現在Nuxt.jsを用いたリニューアルを行うなど、フロントエンド周りの新しい技術や海外事業に興味のある人が向いています。 詳しくは こちらのスライド をご覧ください。 利用技術: HTML / CSS / Vue.js / Vuex / webpack / TypeScript / Nuxt.js グローバルECサイト フロントエンドエンジニア(iOS/Android) プライベートブランド「ZOZO」のiOS/Androidアプリの開発を行います。世界中で使われるアプリのため、多言語対応などももちろんのこと、使いやすいUI/UXを考慮しながら開発する必要があります。世界で使われるアプリを開発してみたいというアプリエンジニアが向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage - Android: Android Studio / Java / Kotlin グローバルECサイト バックエンドエンジニア(API) グローバルECサイトのサーバーサイドの開発を行います。フロントエンドがSPAなのでAPIサーバーを主に作ることになりますが、グローバルECサイトなので国際化対応に紐づくような決済、通貨、サイズ、配送など様々な事象を考慮したサイトの開発を行う必要があります。サーバーサイド側のAPIは Scala / Playで開発されており、Scalaを用いたマイクロサービスの開発に興味があるエンジニアが向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI グローバルECサイト インフラエンジニア/SRE(クラウド) グローバルECサイトやZOZOSUITの計測サーバーのインフラの開発および運用を行います。世界展開を考慮した上でのインフラ設計やマイクロサービス実現のためのインフラの構築を行います。現在はすべてAWSで動いています。グローバル展開しているサイトのインフラに興味がある人が向いています。 利用技術: AWS / Aurora / Postgres / DynamoDB / Scala / Play Framework / nginx / Sentry / CircleCI ZOZOSUIT ZOZOSUITの開発や計測精度向上などを行います。 募集職種 ZOZOSUIT バックエンドエンジニア(ZOZOSUIT) ZOZOSUITの計測データを受け取るサーバーの開発を行います。世界中から集まるZOZOSUITのデータを安全に管理し、様々なチームと連携しながら安全にデータが利用できるようなAPIの開発を行います。ZOZOSUITに興味のあるエンジニアがとても向いています。 利用技術: AWS / Aurora / Python / Flask ZOZOSUIT フロントエンドエンジニア(iOS/Android) ZOZOSUITの計測SDKの開発を行います。様々なチームと連携しながらZOZOSUITの計測精度の向上なども行っています。ZOZOSUITの計測精度向上に興味があるアプリエンジニアがとても向いています。 利用技術 - iOS: Xcode / Objective-C / Swift / Carthage / Bitrise - Android: Android Studio / Java / Kotlin / CircleCI 生産管理 バックエンドエンジニア(ERP) プライベートブランド「ZOZO」の生産管理や在庫管理、会計管理などを行う独自のERPの開発を行います。サイトとの連携APIだけでなく工場とのデータ連携など、非常に多岐にわたるシステムの開発を行います。生産の裏側を技術で支えていきたいエンジニアが非常に向いていると思います。また、このチームではGoをメイン言語として利用しています。 利用技術: AWS / Postgres / Go / Docker 生産管理 インフラエンジニア/SRE (ERP) プライベートブランド「ZOZO」の生産管理システムのインフラの開発、運用を行います。日本国内だけでなく海外の工場との連携などもあるため、世界各国からの利用を意識したインフラの構築を行う必要があります。世界を股にかけるインフラ基盤を構築してみたいインフラエンジニアが向いていると思います。 利用技術: AWS / Postgres / Go / CloudFormation / Docker 生産管理 バックエンドエンジニア(生産マネジメント) プライベートブランド「ZOZO」の生産工場へのシステム導入支援や、プロジェクトマネジメントを行います。海外にある生産工場に出向いて、工場へのシステムの導入支援や運用の改善、管理システムの開発などを行います。基本的に海外出張が多いので海外に興味があり、工場の効率化や自動化に興味のあるエンジニアがとても向いています。 利用技術: AWS / Postgres / Go / CloudFormation / Docker パターン自動生成 エンジニア(パターン自動生成) ZOZOSUITで計測したデータを用いて、ユーザー一人一人に応じた最適なサイズの型紙(パターン)を自動生成する完全オーダーメイド技術の開発を行います。エンジニアとして服作りを勉強してみたいエンジニアがとても向いています。 利用技術: 言語非公開 / 服作りの知識 / CAD スマートファクトリー 自動縫製工場におけるファクトリーオートメーション化の機械制御アプリケーションの開発や自動裁断機、ミシン、ハンガーラインなどの制御プログラムの開発、社外のハードウェアと連携したアプリケーションやIoTプログラムの開発を行います。 募集職種 スマートファクトリー バックエンドエンジニア(Web/アプリケーション) PB「ZOZO」の縫製工場におけるファクトリーオートメーション化にむけたアプリケーションの開発/運営を行うエンジニアです。システム設計、インターフェース設計、インフラ設計、データベース設計、コード設計、実装、レビューなど、スマートファクトリーの実現に関する非常に幅広い業務を担当していただきます。 WebやIoTなどに興味のあるエンジニアが向いています。 利用技術: AWS / Go スマートファクトリー バックエンドエンジニア(制御系) ファクトリーオートメーション化における機器を操作するためのアプリケーション(ミドルウェア)を開発するエンジニアです。モーターやセンサーなどを駆使して生産ラインの自動化を洋服のプロたちと主に目指します。Webシステムから自動裁断機、ミシン、ハンガーラインをコントロールするためのシステム全般業務をしていただきます。ロボットアームや、センサー制御など、ハードウェアの知識も必要になります。色々なメーカー出身の方々が集まってスマートファクトリーの実現を目指しています。回路設計やロボットアームの制御など、ハードウェア経験があって新しいことを実現したいエンジニアが向いています。 利用技術: C / C++ / Go / ROS WEAR 日本最大級のファッションコーディネートアプリです。世界1200万ダウンロードを超え、さらなる成長を目指しています。投稿されているコーディネートの投稿数も800万点以上、MAUが1100万と巨大なファッションメディアになります。また、10万人以上のフォロワーを持つWEARISTAと呼ばれるユーザーがいたり、高橋愛さんや吉岡里帆さんなどをはじめとした 多くの芸能人 にも利用されています。 募集職種 WEAR フロントエンドエンジニア(マークアップ/Web) WEARのPC/SPサイトのフロントエンドの開発を行うエンジニアです。WEARは多くの面白い特集や企画なども頻繁に行っています。またチームメンバーにはWEARがとにかく大好きなエンジニアが多く、ファッションメディアサービスを開発したいエンジニアが非常に向いています。 利用技術: HTML / CSS / JavaScript / jQuery / Windows Server(IIS) / VBScript / SQL Server WEAR フロントエンドエンジニア(iOS/Android) WEARのiOS/Androidアプリの開発を行うエンジニアです。ベストアプリも受賞したことのあるアプリになりますので、UI/UXを非常に大事にしています。メディア系サービスのアプリ開発を行いたいエンジニアが向いています。 利用技術: iOS: Xcode / Objective-C / Swift / Carthage Android: Android Studio / Java / Kotlin WEAR バックエンドエンジニア(Web/API) WEARのサーバーサイド側の開発を行うエンジニアです。います。コーディネートの検索機能の改善を行ったり、アプリのAPIの開発などもやコーディネートの検索をより使いやすくしたり、ユーザーファーストな開発を行っています。 利用技術: Windows Server(IIS) / VBScript / SQL Server WEAR バックエンドエンジニア(リプレイスプロジェクト) こちらも同じくサーバーサイドですが、現在のWEARプロジェクトを新しいアーキテクチャでリプレイスする開発を行っています。現在ZOZOTOWNやWEARはVBScriptで書かれていますが、ZOZOTOWN側にもリプレイスプロジェクトがあるように、こちらWEAR側でもRubyを用いたリプレイスプロジェクトを進めています。大規模メディアサイトをRailsで開発したいエンジニアが向いています。 利用技術: AWS / Ruby / Ruby on Rails / Aurora ZOZO研究所 ( ZOZO RESEARCH ) ZOZOテクノロジーズの研究開発部門として今年の1月に「ファッションを数値化する」をミッションとしてZOZO研究所は設立されました。 現在は青山および福岡に拠点が存在し、約20名程度のメンバーが在籍しています。 ZOZOのビッグデータを用いた研究開発や、機械学習、深層学習を用いたアルゴリズムの開発、新しい技術を用いたプロトタイプの開発など様々な活動を行っています。 現在ZOZO研究所では主に3つのテーマを軸に研究開発を行っています。 最近は コーデ生成に関する論文を投稿 したり、 海外カンファレンスでも発表 したりする機会が増えてきました。世界一のファッション研究所にすべく、新しいチャレンジを沢山行っているチームになります。 募集職種 リサーチャー ファッションに関する技術の研究開発を行うエンジニアです。研究領域の対象はECサイトの利便性向上だけでなく、物流最適化、ZOZOSUITに代表されるような計測デバイスなど、非常に多岐にわたります。リサーチサイエンティストは様々な課題に対し、各自が最も興味のある対象と得意とする手法を選択して研究に取り組むことができます。 有効性が確認された研究成果はジャーナルや国内外のカンファレンス等での発表以外に、エンジニアやデザイナーと協力しながら実際のサービスに導入します。ZOZOにしか出来ない研究を行い、世界をリードするような研究成果を出していきたい人が向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU MLエンジニア(機械学習/深層学習) リサーチサイエンティストとともに、ZOZOTOWNやWEARから得られるビッグデータを使い、ユーザークラスタリング、画像認識、画像生成タスクなどの機械学習・深層学習を用いた実装を行うエンジニアです。 アルゴリズムの開発、実装だけでなく、サーバー側への実装なども行います。自らの技術を用いて、ZOZOTOWNや事業に貢献したいエンジニアが向いています。 利用技術: 機械学習(MachineLearning) / 深層学習(DeepLearning) / Python / Chainer / TensorFlow / PyTorch / AWS / SageMaker / GCP / GPU / TPU リサーチエンジニア(最新技術) 今後流行するであろう技術を用いたPoCの開発などを行うエンジニアです。現在だとAR/VRなどを用いた3D試着やGoogle HomeやAmazon Echoなどのスマートスピーカーを用いたPoCの開発など様々な開発を行っています。未来のサービスのベースとなるようなコンセプトを、新しい技術やZOZOの資産を活用しつつ実現するエンジニアになります。Webやアプリ、サーバーサイドなど、幅広い技術や最新技術に興味関心のあるエンジニアが向いています。 利用技術: Web / iOS / Android / GCP / AWS / Google Assistant / Unity その他 上記で紹介した4つの事業以外にもエンジニアを募集しています。 募集職種 コーポレートエンジニア(社内情報システム) ZOZOグループの社内インフラや社内システムの開発、設定、運用を担当していただきます。オフィス各拠点だけでなく、各倉庫の拠点のための社内ネットワークやセキュリティ、デバイス管理やグループウェアなどその業務領域は非常に多岐に渡ります。自らの技術や働きによって、社員が業務を効率的に行えるために何ができるか、を自ら考え提案および改革を推進できる必要があります。現在はG Suiteへの移行やリモートワーク環境の整備なども行なっています。技術で社員を裏側から支えたいエンジニアが非常に向いていると思います。 利用技術: Azure Active Directory / Office 365 / G Suite / Firewall / 拠点間NW構築 / Cisco / リモートワーク関連技術 / ISMS / GDPR 20年度新卒エンジニア ZOZOテクノロジーズでは20年度卒の新卒エンジニアを募集しています。 フロントエンド、サーバーサイド、インフラ、iOS/Android、データサイエンティストなど幅広いエンジニアやデザイナーを募集しています。 労働環境について さて、ここまで30職種近いエンジニア職を紹介してきました。働く上で仕事内容も重要ですが、労働環境も重要です。ここからはZOZOの労働環境について紹介していきたいと思います。 勤務場所 ZOZOテクノロジーズのオフィスは現時点で青山、幕張、福岡の3拠点が存在します。 担当するプロダクトによって勤務場所が変わりますが、ざっくり分けると現時点では以下のような形です。 青山 PB(グローバルEC) / ZOZOTOWN(アプリ) / 新規事業 ZOZO研究所(青山) 幕張 ZOZOTOWN(アプリ以外) / WEAR / PB(生産) 福岡 ZOZO研究所(福岡) よく幕張勤務ですか?と尋ねられますが、担当するプロダクトによって変わるため必ずしも幕張というわけではありません。幕張と青山の人数比率はおおよそ6:4です。 勤務体系や働き方など ほとんどの社員はフレックスタイム制(コアタイム10:00〜17:00)になっており、 一部の専門スキルを持っているスペシャリストおよび研究所メンバーは裁量労働制となっています。 子供がいるママさんなどは時短勤務の制度がありますので、そちらを利用して働いています。ちなみに弊社は出産された方の産休復帰率が100%となっており、子供を持つ母親としても非常に働きやすい環境となっています。 また、リモートワークに関しては現時点では正式に制度としては存在していませんが、現在環境を整えている最中です。世の中の働き方が多様化する中、しっかりと弊社でもリモートワークができる体制や制度を整える必要があると感じており、着々と準備を進めています。 給与 一番良く質問されるのが「給与って全員一律なんでしょう?」という質問です。 この問いに対する答えは半分正解、半分間違いです。 現在、給与は 基本給+職能給+賞与+各種手当 という構成になっており、基本給および賞与は全員一律同じですが、職能給は各々によって違います。 職能給はいわゆる給与テーブルによって定義されており、年2回の人事考課面談によって決定されます。給与の上限は、天才枠採用の場合最大1億円となります。 あくまでもその人のスキルや実績に合った適切な報酬を支払う、というスタンスです。 環境、福利厚生、制度・手当等 ZOZOでは社員がパフォーマンスを最大限発揮できるための環境を最大限用意できるようにしています。特徴的なものをいくつか紹介いたします。 支給マシン エンジニアは入社の際に希望のPCスペックを選ぶことができます。MacやWindows、iMacやMacBookProなど、モニターやキーボードなど必要に応じたマシンやスペックを選択可能です。 公休 会社としての休みは土日・祝日、育休、慶弔などがあります。有給は試用期間終了を待たずとも、入社日に10日間付与されます。(付与日数は入社月によって変動) 夏休み、冬休みがともに3日間存在します。有給とあわせてまとめて1週間ほど休んで旅行に行く人などが多い印象です。 住宅通勤手当 住宅通勤手当として全員に月5万円が支給されます。会社の近くに住めば実質5万円がまるまる住宅補助となります。 家族手当 家族がいる人には家族手当として扶養家族一人あたりにつき月5,000円が支給されます。 社員割引 自社サービスを積極的に使ってもらう為にも、ZOZOTOWNの商品を社員特別価格で購入することができます。 退職金制度 3年以上在籍している社員には退職金が在籍年数に応じて支払われます。社員にはZOZOで長く働いてほしいという思いから、このような制度があります。 書籍購入補助 書籍購入の補助が全額出ます。事前の申請などは必要なく、上限も今の所存在していません。ルールとしてあるのは、本のレビューをslackに投稿するだけです。 カンファレンス参加費用補助 国内外のカンファレンスへの参加費用を全額負担します。例えば今年はGoogle I/O、WWDC、AWS Re:invent、Google Cloud Nextなど、海外の主要なカンファレンスに参加しています。航空券やホテル代などは全て会社が負担しますし、さらに出張手当として1日あたり一定の金額が支給されます。 社会人ドクター支援制度 ZOZO研究所の研究に関連する分野において、博士号の取得を目指す方を対象とした制度です。共同研究先との研究開発及び修学を優先的な業務とし、給与の支給のほか修学費用を支援します。これにより働きながら学位取得を目指すことができるため、経済的な不安解消と研究時間の確保によるスキル向上が可能となります。 副業OK 本業に支障をきたさないという前提で、会社として副業を許可しています。個人サイト運営、アプリ開発、友達の手伝い、書籍執筆など自らの可能性を広げるための活動を行っている人達もいます。 この他にも現在様々な制度を整備している最中です。どうすれば今よりももっと生産性が高く、より効率的に楽しく働ける環境にできるかどうか、日々模索しています。 テックカンパニーとしてのZOZO ZOZOテクノロジーズが誕生してまだ8ヶ月、僕自身がZOZOのエンジニアリング組織を任されるようになってからまだ日が浅いですが、着実にテックカンパニーへと生まれ変わろうとしています。 ZOZOにJOINしてから感じていることとして、この会社はZOZOSUITのように理想や夢を現実のものにできる、そんな素晴らしい力を持ったエンジニア達がたくさんいます。 テックカンパニーとしてのビジョンを描き、仕組みや制度を整えることによって、確実にもっと強いエンジニアリング組織にできるという確信があります。どういう取り組みを行っているかなど、ZOZOのエンジニアリング組織についてのお話はまた後日詳しく紹介できればと思います。 まとめ ここまで紹介してきたとおり、ZOZOでは非常に多様なエンジニアを募集しています。 言語もインフラも、ソフトウェアだけでなくハードウェアまで様々な技術スタックを活用してサービスの開発を行うことができますし、皆さんの技術を活かせる仕事が必ずあると思います。 ファッションの世界を技術で変えていくことに興味のある方はぜひ応募してみてください。 この記事を読んで、ZOZOに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com 2019年も世間をあっと驚かせるようなプロダクトを出していきたいと思います。 私達と一緒に楽しく働いてみませんか?
アバター
こんにちは。そろそろ生後7か月になる息子が可愛くて仕方がないうっちー(@k4ri474)です。 12/10〜12/13に開催された KubeCon+CloudNativeCon へ参加してきました。 大型カンファレンスらしい演出のKeyNoteやハンズオンセッション、プレゼンテーションなど盛り沢山なイベントでした。 今回、僕はKubernetes運用経験やDockerを用いた設計・開発の経験がほとんどない状態で参加しました。 現状はチーム・個人として業務の中心にはなっていないものの、今後の技術選定に関わるタイミングで選択肢としてKubernetesを持てるようにキャッチアップしておきたかったというのが理由です。 知っていて選定しないのと、知らずに選定できないのとでは大きな違いがあると思っています。 カンファレンスはKubernetesの盛り上がりを肌で感じて手を動かして身につけるキッカケにしようと意気込んで申し込みました。 本記事では、ビギナー目線で印象に残ったセッションをメインにしていくつかご紹介します。 KeyNoteは後ほど動画がアップロードされるかと思うので取り上げません。また、セッションのスライドURLを貼りましたが、埋め込みできる形式でアップロードされておりませんでしたので、リンクを開いてご覧いただければと思います。 KubeCon+CloudNativeConとは コンテナオーケストレーションツールであるKubernetesと、CNCFがホストするPrometheusやEnvoyなどのOSSについて、最新技術やユースケースなどが紹介されるカンファレンスです。 概ねKeyNoteから始まってプレゼンテーション・ハンズオンを挟み、KeyNoteで終わるといった構成になっています。 KeyNoteの時間以外は同時刻に15ほどの別々のテーマの発表が行われていて、参加するセッションを自由に選択する形式です。 また、常時スポンサーブースが開設されていて、営業・広報担当者だけでなく時には開発者と直接会話できます。 印象に残ったセッション You can't have a cluster[bleep] without a cluster You can't have a cluster[bleep] without a cluster heptioのSenior Developer Advocateによる、Kubernetes自体の複雑さ・リスクを認識した上でどのような取り組みをして乗りこなしていけばよいか、というセッションでした。 Kubernetesの望ましい特徴として回復性・効率性・反復性が挙げられるが、複雑性やリスクも内包している、ということが最初に強調されました。 Kubernetesはアプリケーション・OS・ハードウェアなど多くのコンポーネントに対して干渉できる故、把握すべき範囲が広がっていることが一因とのことでした。 また、先日発覚した CVE-2018-1002105 も触れられ、適切な設定で構築しないとKubernetesクラスタが攻撃者の手に落ちるというような大惨事も発生しうるということを改めて振り返りました。 これらの複雑性・リスクと向き合うためには、以下の4つが重要だそうです。 Learning to mitigate hazards and observe our systems Learning to measure our systems Learning to build tools to gain confidence Learning from our mistakes 僕はこれまでKubernetesの優れている話を聞く機会が多かったです。実際には便利なだけではなく複雑なシステムになりうることを認識し、うまく扱っていくことが重要なのだと認識できた良いセッションでした。 Monolith to Microservice Monolith to Microservice GitLabがシステムのマイクロサービス化に取り組んでいる中で過去に取ったアプローチや、今後の挑戦について語られたセッションでした。 モノリスとマイクロサービス の長所短所の紹介から始まり、Gitホスティングサービスの特性・直面する課題について触れつつ、モノリスが持つ制限から解放されるためにマイクロサービス化を推進し始めた話に入っていきます。 GitLabではディスクをスケール・高速化するために導入したNFSによって、ディスクI/Oとネットワークスループットという新たな課題に直面した過去があるそうです。 これを解決するために、 Gitaly というRPCを利用したOSSを開発し、プロダクション環境へ導入しています。 GitalyのREADME によると、GitLab 11.5の時点でほぼ全てのアプリケーションコードが、ディスクへの直接アクセスではなくGitalyを通じたアクセスに置き換わっているようです。 GitLabはGitalyによっていくつかの問題を解決していますが、ロードバランシングやスケーリングなどまだ課題があるようで、引き続き推進していきたいとのことでした。 システム全体を俯瞰で見た時にどのような課題があって、それをどうやって解決していくかを見据えて開発を行なっているようで、この視点・進め方は見習いたいと感じました。 MySQL on Kubernetes MySQL on Kubernetes Kubernetesの機能を利用することで、RDBも自然な形でコンテナとして管理することが可能だ、ということをデモを交えて紹介するセッションでした。 Cattle vs. Pets という例えがありますが、一般的にRDBはペットのように扱われるため、イミュータブルなコンテナでRDBを動かすことは難しいと僕は思っていました。 RDBのコンテナ運用は以下の機能・ソフトウェアを利用することで実現するようです。 Affinity and anti-affinity :DB用の特定のホストにMySQLのPodを配置する StatefulSets :ノード障害時の台数維持を行う Headless Service :MySQLのPodをディスカバリーする StorageClass, VolumeClaimTemplate :永続化ボリュームのプロビジョニングとその定義 MySQL Operators :Kubernetes上でCRDを使ってMySQLクラスターを作成・設定・管理するためのソフトウェア 上記のリストを見てわかる通り、MySQL Operatorsの存在がキーのようでした。 OSSとしては MySQL Operator や Presslabs MySQL Operator など複数あるため、要件に合わせて選択することになります。 僕は業務でAmazon RDSのAuroraクラスターを運用することがあるのですが、まさしくペットのように扱っています。このセッションを通してRDBもコンテナ運用できるという選択肢を持てたので、1つ自分の中の手札が増えてためになりました。 帰国次第、サンプル等を使って自分で手を動かして構築してみようと思っています。 まとめ Intro系セッションへの参加がメインでしたが、様々な切り口のプレゼンが行われていため毎日毎セッションが興味深く、意欲的に参加できました。 このブログには書きませんでしたが、心に残ったものが他にいくつもあります。 大変勉強になったと同時に技術・人の両方の盛り上がりを肌で感じられて非常に良い経験でした。 実際の業務へどのようにして取り入れられるか考えながら参加したため、自分ごととして学べたことが成果だと感じています。 それと同時にCNCFのプロダクトの魅力に触れ、自分も開発に携わりたいと感じました! ZOZOテクノロジーズでは、最新技術をキャッチアップしつつ地に足のついた技術選定ができるエンジニアを大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
アバター
初めまして。ZOZO Technologies 分析部部長の牧野( @makino_yohei )です。 今回はZOZOのビッグデータを収集・加工してビジネスに活用する私の部門、分析部について紹介させてください。 「分析部」のミッション ミッションは2つです。データを活用して・・・ 1.大きな売上を作る 2.業務の効率や精度を上げる としているのですが、まあ、それはそうだろうという感じでしょうか。 1に関しては、部門の中期目標は 部門発信の施策で年間取扱高1,000億円の純増を作る というもので、 一人称で売上を作るぞと言っているのが少しユニークなところかなと思っています。 現在の進捗は10数%くらい。頑張ります。 「分析部」の役割 仕事の中身は、およそ以下のように分類できます。 1. ビジネスプランニングのサポート 施策や事業自体をデータから事前・事後に評価して、次のアクションを決めるのを支援するという仕事です。 また、依頼のみではなく提案もよくやります。SQL→XLS→PPTという流れが多いです。 2. ビジネスロジックのモデリング 主に機械学習を使って業務の裏で使う予測モデルを作るという仕事です。 予測するのは人単位の購入意欲や、SKU単位の需要など。DataRobotも活用しています。 3. ビジネスインテリジェンス MicrosoftのPowerBIを使ってダッシュボードや分析ツールを作る仕事です。 BIの裏のデータマートも設計・構築します。 4. データエンジニアリング DWH(BigQuery)とDMPを中心としたデータ分析基盤・マーケティング基盤を設計・構築する仕事です。 また、2で作ったモデルをシステム上に実装するのもこのチームの仕事です。 5. ハンズオンサポート 仕事内容は上記の1,3ですが、特定の事業部門にはりついて作業をするのが特徴です。 現時点ではプライベートブランドZOZOの担当部門に数名のメンバーが常駐して、データで事業開発を支援しています。 メンバーの紹介 上記のように、分析部門としての一通りの業務機能を備えているのですが、実は昨年2017年の夏に設立した1歳半くらいの組織です。 1年半前に会社やグループを横断してアナリティクスサービスを提供する組織として、 外から入社した私と数人のメンバーで立ち上げ、現在は約15人、90%以上が2年以内に入社したメンバーです。 面接などでどんなメンバーがいるのかよく聞かれますので、いくつか数字でまとめてみます。 年齢の平均値 = 31歳、最頻値 = 37歳、最小値 = 26歳、最大値 = 37歳 20代後半から30代前半がボリュームゾーンです。世代が近いのでみんな仲が良いです。 女性比率 = 20%弱 男女問わず募集中です。 B2C企業出身メンバーの割合 = 約50% インターネット系の企業出身者が多いです。残りのB2Bはコンサル、SIer、ツールベンダーなど 千葉県在住率 = 約70%、徒歩・チャリ通勤率 = 約40% 自分は東京在住なので徒歩通勤、羨ましいです。。 1年半を経て続々と優秀なメンバーが入社してくれています。 ディレクター・マネージャー陣を何名か軽く紹介させてください。 私、牧野(37) 部門の責任者で、総合コンサルティング会社で、ビジネスコンサルティングを10年強経験してZOZOに入りました。 前職ではビッグデータを使ったビジネスサービスの分析・設計をしてきました。 KOさん(36) ZOZO社員歴13年。CRMの専門家・エンジニアで、CRMチームのリーダーから部門立ち上げ時に異動してくれました。 ゼロから一緒に体制を作ってくれました。 全体のマネジメントと、5のハンズオンサポートをリードしています。 MYさん(33) 7月にインターネット企業から移籍。前職では数100人のエンジニア組織のデータ責任者をやっていたメンバーです。 検索基盤・分析基盤・DMPなどあらゆるプラットフォームを作ってきたスーパーエンジニアで、 4のデータエンジニアリングをリードしてくれています。 MNさん(34) つい先日、11月にJOIN。DMP業界では名の知れたデジタルマーケティングのスペシャリストです。 彼は、1のビジネスプランニングを中心に、1から4まで暴れまわってもらいます。 YKさん(38) 3のビジネスインテリジェンスのところも年度内にスペシャリストがJOINしてくれる予定です。 2のビジネスロジックのモデリングは、特に強化していきたいので、ご興味をお持ちの方は是非連絡ください!  若手メンバーも皆、優秀で向上心も高く、そしていいやつです。 一様に「あれっ、入ったのそんなに最近だっけ?」という状況で、 気が付くと馴染んでいて、気が付くとメンバーが集まって幕張界隈で飲んでいます。 最後に、ZOZOの分析の仕事が面白い5つの理由 ブログは慣れないのですが、最後に私の所感をよくある感じで。。 1. みんな本当にいいやつ 経営理念が「いい人をつくる」で、実際に部内のメンバーも社内クライアントである各部門やグループ会社のメンバーも一緒に仕事をしていて気持ちの良い人ばかり。 縦割りの概念がなく、いい仕事を一緒にして成果が出て一緒に喜ぶというヘルシーな組織です。 当然、分析の仕事なのでトンネルをなかなか抜けられないという苦しいフェーズもありますが、 その先に一緒に喜べる仲間がいるというのは、分析の仕事をするうえで結構重要なんじゃないかと思います。 2. 大きな仕事ができる 良くも悪くも、データを使って改善すべき案件がゴロゴロしています。その分、大きな仕事ができます。 例えば、pythonでクーポンの売上を事前に予測するツールを作って数10億(後半)売上増を作ったり、新しい事業の根幹となるロジックを作ったり。 コンサル会社から来た若手メンバーも、こんなに大きくこんなに面白い案件をいきなり任せてもらえるなんて…と、士気高く頑張っています。 3. 次から次に新しいことができる 私も入社時はデータを使ってZOZOTOWNを改善していく業務が中心というイメージをもっていました。 入社して間もなくZOZOSUITが登場し、プライベートブランドZOZOが出て、海外展開が始まり…、 1年後どうなっているか全く分かりません! 4. 扱えるデータが多様 というのが面接中に魅力としてよく言われます。確かにZOZOTOWNの実績・マスタデータだけでなく、 ZOZOSUITの計測データ、WEARやおまかせ定期便の洋服同士の組み合わせデータ、一つ一つのアイテムにつけられたタグデータやサイズデータなど他社にはない種類のデータがたくさんあってそれらを使って分析やモデルを作ることも多いです。 ただ、個人的にすごいと思うのは横ではなく縦に大量のデータを持っているということで、普通の量なら傾向あるね、 統計的に有意だねとしか言えないことが滑らかなグラフとして出せたりします。 それを初めて体験したときは鳥肌が立ちました。 5. 部門自体が発展途上 まだまだ1歳半の部門でケイパビリティも全然足りていません。 ここから優秀なメンバーを集めたり、社内・社外のプレゼンスをあげたり、成果を積み重ねていくフェーズです。 ないものを嘆くのではなく自分で作れる人にとっては、自分自身も成長し、部門の成長を感じられる場所です。 メンバー一同で助け合いながら、部門を育てています。 というわけで、いいメンバー・面白い仕事が待っているので、ご興味を持たれた方は是非↓からご応募を!  よろしくお願いします。 データサイエンティスト tech.zozo.com ビジネスアナリスト tech.zozo.com データエンジニア tech.zozo.com
アバター
こんにちは。品質管理部エンジニアリングチームの遠藤です。 前回の壮絶な失敗を何事もなかったかのように忘れ去り、次のテーマへ移りたいと思います。 工夫しなければいけなかったこと ZOZOSUIT自動測定については前回のとおり何となくぼんやりとイメージはついていたのですが、「これはどうしよう」と思ったことが2つありました。 デバイスIDに依存したくない 外部ディスプレイにどうやってブラウザ開くの? といったことでした。 Androidデバイスの制御をする場合、デバイスIDをもとに制御するわけですが、これをそのままプログラム内に書いてしまうとその端末しか制御できません。またディスプレイが複数ついているためどのディスプレイにどの端末を向けているのかを事前に登録してからプログラムを動作させなければいけません。最初はとりあえずプロトタイプということでそのまま動かしていましたが、まずはここをなんとかしたかったです。 とりあえず、やってみます。 私やる!やるったらやる! では具体的にどうすればよいでしょうか。デバイスに接続しているUSBケーブルの情報から接続しているデバイスIDがわかればなんとかなりそうです。とりあえず何も接続しない状態で、USBのシステム情報を確認してみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 15> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> こんどはUSBポートを増設した状態でやってみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 17> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> +-o 4-Port USB 2.0 Hub@14100000 <class AppleUSBDevice, id 0x1000022ba, registered, matched, active, busy 0 (2 ms), retain 21> +-o 4-Port USB 3.0 Hub@14400000 <class AppleUSBDevice, id 0x1000022d0, registered, matched, active, busy 0 (1 ms), retain 21> ポートが認識されています。そこにデバイスを接続してみます。 % ioreg -p IOUSB -w0 +-o Root <class IORegistryEntry, id 0x100000100, retain 15> +-o AppleUSBXHCI Root Hub Simulation@14000000 <class AppleUSBRootHubDevice, id 0x100000351, registered, matched, active, busy 0 (0 ms), retain 17> +-o Bluetooth USB Host Controller@14300000 <class AppleUSBDevice, id 0x100000fc5, registered, matched, active, busy 0 (9 ms), retain 33> +-o 4-Port USB 2.0 Hub@14100000 <class AppleUSBDevice, id 0x1000022ba, registered, matched, active, busy 0 (2 ms), retain 22> | +-o F5321@14110000 <class AppleUSBDevice, id 0x100002315, registered, matched, active, busy 0 (7 ms), retain 25> +-o 4-Port USB 3.0 Hub@14400000 <class AppleUSBDevice, id 0x1000022d0, registered, matched, active, busy 0 (1 ms), retain 21> 14110000にF5321が接続されています。いろいろ抜き差しして調査してみた結果、この番号は基本的に変わらないようでした。 自分の環境ではこんな感じでした。このナンバーのケーブルをあらかじめ対応させる予定の外部ディスプレイに向けておけばいい感じにいけそうです。 で、さっきのコマンドでデバイスIDを確認してっと、 これよく見たらデバイスIDじゃなくてデバイス名じゃねーか! adb devices でデバイス名との対応ができればいいのですが、表示されません。 と思ってたら  adb devices -l で表示されました。 最初(さっきまで)はこれを知らず、 system_profiler SPUSBDataType から無理やり取得していました。 コードに置き換えてみる(python3) では早速これをコードに置き換えてみます。 import subprocess def cmd (_cmd_str): # print("cmd:" + _cmd_str) proc = subprocess.run(_cmd_str, stdout=subprocess.PIPE, shell= True ) return proc.stdout.decode( "utf8" ) #指定したUSB IDに繋がっているAndroidデバイス名を取得する def check_usb_connect (_check_usb_id): cmd_str = "ioreg -p IOUSB -w0 | sed 's/[^o]*o //; s/ <.*$//' | grep '.*@.*' | grep -v Hub | grep -v USB" res = cmd(cmd_str) # print(res) for line in res.splitlines(): dn, usb_id = line.split( '@' ) # print(dn, usb_id) if usb_id == _check_usb_id: return dn return "" #デバイス名からデバイスIDを取得する。 def deviceId_from_deviceName (_deviceName): cmd_str = "adb devices -l" res = cmd(cmd_str) for line in res.splitlines(): if "product:" in line: device_id, etc = line.split( ' ' ) elist = etc.split( ' ' ) # print(device_id) device_name = elist[ 4 ].split( ':' )[ 1 ] if device_name == _deviceName: return device_id return "" print ( "-----------" ) device_name = check_usb_connect( "14110000" ) print ( "device_name:" + device_name) device_id = deviceId_from_deviceName(device_name) print ( "device_id:" + device_id) できました! 注意点 ただ、これだと同じデバイス名の端末を接続すると見分けがつきません。なので system_profiler SPUSBDataType などの情報から、USBポートとの対応など考慮してデバイスIDを取得したほうがよさそうです。 次回予告 幾夜魘されたか知らない悪夢、目の前の僅かな一跨ぎ。それができない泥沼の中で 俺は喘ぐ。 俺はどうしようもない絶望の中、次のミッションに取り掛かった。 今回はうまく行ったが次の問題がうまく行くと保証されたわけじゃない。 だが前に進まねばならない。俺が俺でいるために。 人を人たらしめるもの、それが目の前の問題をただがむしゃらに考え生きる、 ということなのだとするならば、あるいは俺も・・・。 そんなことを考えながら、俺は目を閉じ、今日も水玉のスーツに袖を通す。 次回、最終章(発動編)。ブラウザの扉は開くのか? 最後に ZOZOテクノロジーズでは、一緒にサービスを作成、サポートしてくれるエンジニアを大募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com
アバター
こんにちは。開発部に所属している鶴見です。 弊社ではAzure SQLDatabaseを利用して運用している箇所があります。 SQLDatabaseのデータ検索に利用している列ストアインデックスについて紹介します。 はじめに Azure SQLDatabaseはMicrosoft社が提供しているマネージドデータベースであり、SQL Serverエンジンと互換性があります。 ZOZOTOWNで扱っている大量データの中から対象データをピンポイントに探す場合は、SQLDatabaseのB-treeインデックスを利用し高速に検索しています。 しかし、B-treeインデックスは広範囲の検索が苦手という弱点があります。 そこで、広範囲の検索をする場合は、列ストアインデックスを利用してみました。 主にデータウェアハウスやデータ分析にて利用されていますが、 ここ数年の進化によりOLTPでも利用しやすいインデックスとなりました。 列ストアインデックスとは 広範囲の検索で性能向上が見込める列指向型インデックスです。 B-treeインデックスが行単位でデータを扱うのに対して、列ストアインデックスは列単位でデータを扱います。 列ストアインデックスとB-treeインデックスの比較です。 インデックス種類 B-treeインデックス 列ストアインデックス 指向性 行指向 列指向 読込 ページ単位(1ページ8kb) セグメント単位(1セグメント約100万件) 圧縮 可 可 scan操作 可 可 seek操作 可 不可 単一行検索 得意 苦手 広範囲検索 苦手 得意 B-treeインデックスで1カラム100万件のデータ取得を行うと大量のデータページを読込みます。 対して、列ストアインデックスは1カラム100万件を1セグメントとしているため、1回の読込みで済みます。 また、列単位で圧縮されるため圧縮率が高く非圧縮データ サイズと比較して、最大10倍のデータ圧縮が見込めます。 列単位で管理されることによりB-treeインデックスのように指定するカラムの順番を気にする必要もありません。 よって大量データを集計する処理などデータ分析に向いています。 SQL Server2012で登場した当初は「行データの更新・削除・挿入は行えない」などの制限があり、 常にデータ更新のあるデータベースでは利用しにくい状況でした。 しかし、SQL Server2014~2017にかけて制限が緩和され、データ更新に対応しました。 SQLDatabaseで列ストアインデックスを利用できるモデルは次の通りです。 Premiumレベル Standard S3レベル以上 General Purposeレベル Business Criticalレベル 列ストアインデックス作成 次の構文にて列ストアインデックスを作成可能です。 CREATE NONCLUSTERED COLUMNSTORE INDEX [index_name] ON [table_name] ( [column_1], [column_2], [column_3] ) 性能検証 実際にテストデータで性能を検証してみます。 テストテーブルに1億件のデータを登録し、そのうち1千万件データを取得した場合のB-treeインデックスと列ストアインデックスの処理能力を検証します。 -- テストテーブル CREATE TABLE test ( id int NOT NULL , age int NOT NULL , name nvarchar( 100 ) null CONSTRAINT primary_key_test PRIMARY KEY CLUSTERED ( id ASC ) 1億件のテストデータを登録した後、 カラム[age]に対してB-treeインデックス、列ストアインデックスをそれぞれ作成します。 -- B-treeインデックス作成 CREATE NONCLUSTERED INDEX [NONCLUSTERED_INDEX] ON [dbo].[test] ([age]) GO -- 列ストアインデックス作成 CREATE NONCLUSTERED COLUMNSTORE INDEX [COLUMNSTORE_INDEX_test] ON [dbo].test ([age]) GO 検証のため各インデックスが利用されるようヒント句でインデックスを固定しクエリを発行します。 (テストデータ1億件のうち、age = 21が1000万件となるようにデータを作成しています) -- 1000万件取得クエリ B-treeインデックス使用 select count(*) From test with ( index (NONCLUSTERED_INDEX)) where age = 21 -- 1000万件取得クエリ 列ストアインデックス使用 select count(*) From test with ( index (COLUMNSTORE_INDEX_test)) where age = 21 検証結果 検証結果は次の通りです。 B-treeインデックス(非クラスター化インデックス)を利用した場合。 実行プラン / 実行時間 列ストアインデックス(非クラスター化列ストアインデックス)を利用した場合。 実行プラン / 実行時間 列ストアインデックスが利用されていることが確認できる。 クエリにてCOUNTを行ったためハッシュ ベースの集計関数が利用されHash Match操作となった。 並列実行のためParallelism操作となった。 インデックス種類 CPU time elapsed time B-treeインデックス 2220ms 1647ms 列ストアインデックス 46ms 8ms CPU time及び、elapsed time(経過時間)の結果から、列ストアインデックスを利用した方が高速でした。 処理時間が「CPU time > elapsed time」となっているのは、並列で処理されているためです。 実行プラン詳細にて、論理操作はインデックススキャンであることが確認できます。 これは列単位で全件スキャンを行っているためです。 また、列ストアインデックスを利用すると実行モードはBatchとなります。 気を付けたいポイント 一見良さそうな列ストアインデックスですが、メンテナンスを怠ると性能が低下します。 列ストアインデックスに含まれる列データが大量に更新された場合、更新データは非圧縮状態になります。 圧縮されていることで最適なパフォーマンスとなるため、非圧縮データが増えてしまうと読込むデータが増え性能低下します。 列ストアインデックス状態確認 次のクエリで列ストアインデックスの圧縮状況を確認できます。 -- 列グループ状態確認 select OBJECT_NAME(object_id) AS object_name ,* from sys.column_store_row_groups total_rowsの値を確認すると、約100万件の単位でグループ化されていることが分かります。 state_descriptionの値がCOMPRESSEDの場合はデータ圧縮されており最適な状態です。 データ更新されるとstate_descriptionの値がOPENに変わり非圧縮状態となります。 OPENの件数が増えた場合は、インデックス再構成を実行して圧縮状態に戻すことができます。 インデックス再構成はオンラインで実行可能です。 更新が多いテーブルで列ストアインデックスを利用する場合、どのようなタイミングでインデックス再構成を行うか考える必要があります。 実際に利用する場合は、よく検証してから利用することをお勧めします。 まとめ 狭域の検索はB-treeを利用し、広範囲の検索は列ストアインデックスに置き換えました。 これにより、どのような検索であっても高速に処理することが可能となりました。 B-treeインデックスに比べると、考慮すべきことが多い列ストアインデックスですが、使いどころによってはかなり有効かと考えます。 参考文献 列ストア インデックス: 概要 | Microsoft Docs 最後に 本文でも紹介しましたが、本システムにはまだまだ問題が残されています。弊社では一緒にデータ基盤を作ってくれる方を大募集しています。 ご興味がある方は以下のリンクから是非ご応募ください! www.wantedly.com
アバター
こんにちは、サーバーサイドエンジニアの竹若です。11/13 ~ 11/15にかけてロサンゼルスで開催された RubyConf2018 にZOZOテクノロジーズから竹若・高木( @rllllho )・田島( @katsuyan121 )の3人が参加しました。 今年のRubyConfは講演数60、参加者数840の大規模なカンファレンスでした。この記事では私たちが興味を持った講演をいくつか紹介させていただきます。 Opening Keynote Sweat the Small Stuff Being Good: An Introduction to Robo- and Machine Ethics Empowering Early-Career Developers Ethical Data Collection for Regular Developers The Ruby Developer's Command Line Toolkit Pointers for Eliminating Heaps of Memory Parallel programming in Ruby 3 with Guild Building Serverless Ruby Bots Runnning a Government Department on Ruby for over 13 Years It's Down! Simulating Incidents in Production Yes, You Should Provide a Client Library For Your API Unlearning: The Challenge of Change まとめ Opening Keynote Opening Keynoteは、Rubyのパパであり弊社ZOZOテクノロジーズの技術顧問でもあるMatzさんの発表です。 コミュニティの力や、Rubyのこれからについて話されていました。 プログラミング言語は人間によって作られたものであり、C言語はOSを書くためHaskellは教育のためなどそれぞれの言語が作られた背景には明確な目的や意図があります。 RubyはMatzさんのアイデアを表現するために開発されたそうです。 Matzさんが自分のために作り始めたRubyが世界中で使われるようにまでなった理由として、Rubyコミュニティの力があるそうです。 OSSコミュニティは人の集まりではなくバーチャルなようなものです。 コミュニティは例えるなら台風で、目には見えないがとても巨大なものであるとおっしゃられていました。 またRubyコミュニティは誰か個人のものではなく、Matzさん自身もコミュニティの一部に過ぎないとおっしゃられていました。 OSSコミュニティは知的好奇心、コミュニケーション、責任の3つが大事で、Rubyはこれらを持ったとてもよいコミュニティであるとおっしゃられていました。 "Ruby is dead" という意見を聞くことがあります。 ハイプ・サイクルをもとに考えるとRubyは『Trough of Disillusionment(幻滅期)』のフェーズに位置しており、今こそRubyへ新しい投資ができるフェーズで積極的にRubyを使って欲しいとおっしゃっていました。 近年開発されたSwiftやGoなどのプログラミング言語は会社が開発したものですが、Rubyはコミュニティによって開発されています。 これからRubyが生き続けるために、 Never give up Keep moving Community Be nice Be happy であることが大事であるとお話しされていました。 Rubyのこれからの改善については、2018年12月にRuby2.6がリリースされ2020年にRuby3、2025年ごろにはRuby4の開発もありうるかもしれないとお話しされていました。 発表を聞き改めてコミュニティの大事さと自分もRubyコミュニティに貢献していきたいと思いました。 RubyConfはとても初めての参加者が多く、このような間口の広さもRubyコミュニティのいいところだと思います。 Matzさんが2025年には60歳になるとのことで、Rubyの言語デザインについてはどのように引き継がれていくのかについて気になりました。MatzさんのAI化も本当にありうるかもしれません。 これからのRubyも楽しみです。 Sweat the Small Stuff Aaron Harpoleさんによる、組織をスケールさせる際によく見られるアンチパターンとその対処法を紹介する講演でした。 「チームが小さかった頃は全員がプロダクトを熟知していて、ミーティングもなく、デプロイとフィードバックのサイクルを小さく収めることができていた。しかしチームが大きくなるに従い既存のやり方が通用しなくなりチームの歯車がうまく回らなくなっていった」 といった事態に陥らないためのTIPSが紹介されていました。 具体的には以下のような内容でした。 エンジニアが生産性を高めるための投資を惜しまない(CIサービス、マシンのスペック) トライ&エラーがしやすい開発環境を用意してデプロイを安全にする ミーティング前に各々が論点を紙にまとめておいてミーティングの最初に読み合う エンジニアの採用ではまず最初にエンジニアが採用候補にコンタクトする またチームの既存文化を変えることの難しさを説き、リクルーティングは文化を構築する最大のチャンスであると述べていました。 私が所属しているチームもまだ編成されて日が浅いのですが、今回のAaronさんの講演は今後チームをスケールさせていく上で気を配っておくべきポイントを明確化するのに役立つと思いました。 参考 : https://icanthascheezburger.com/wordpress/?page_id=222 Being Good: An Introduction to Robo- and Machine Ethics Eric Weinsteinさんによる、技術者が持つべき倫理と人工知能が持つべき倫理に関する講演でした。 技術者が持つべき倫理のお話ではソフトウェアのバグが原因で複数の死者を出した放射線療法機器「セラック25」やシステムの脆弱性をハックされ巨額のお金が不正送金されたブロックチェインを使ったプロジェクト「The DAO」を引き合いに出しながら以下のことを述べていました。 医療と同じで技術も技術者が標準治療を遵守しなければ人命を危険にさらしてしまうこと システムの要件を満たすのに十分に強力であると同時に必要以上に強力ではない、必要最低限の力を持ったプログラムを書くことが必要であること 人工知能が持つべき倫理のお話ではFace IDや自動運転技術を例に出して以下のことを述べていました。 人工知能に「決定」や「認識」などの人間的な行いをさせるということは暗黙的に人工知能の行動に倫理的な重みを持たせているということ 人工知能の行動に対する説明とその責任を取ること 今後技術が発展し今まで人間のやってきた仕事が機械に置き換えられていくに従い、機械の保つべき倫理の水準などの議論が更に増すはずです。 技術者の関心は何かと技術そのものだけに集中してしまう傾向にあります。しかしそれにまつわる倫理や付随する責任を考慮して初めて世に役立つものが生まれると思います。 今回のEricさんの講演の内容には今後技術と倫理に関する議論をするために必ず認識しておかなければならない重要な事実が詰まっていると感じました。 Empowering Early-Career Developers Mercedes Bernardさんによる、自身の経験に基づいた経験の浅いエンジニアをベテランエンジニアにまで育てるためのアプローチを紹介する講演でした。 Mercedesさんのチームでは経験の浅いエンジニアを育てるためにキャリアの早い段階で色々な裁量を与えているそうです。 経験の浅いエンジニアがいきなりベテランエンジニアの仕事を振られて困らないように早めに経験を積ませてあげることが重要であるとおっしゃっていました。 Mercedesさんはコンサルタントのチームを率いているそうで、具体的な内容はエンジニアに当てはまるものではありませんでした。しかし経験の浅いエンジニアをベテランエンジニアにするためのアプローチは、フレームワークとしては有用な気がします。 個人的にはメンバーそれぞれが優秀で頼りあうことのできるチームが理想です。チームの他のメンバーをレベルアップするアプローチを紹介する今回のMercedesさんの講演は非常に興味深かったです。 参考 : http://downloads.ctfassets.net/kueynwv98jit/2Dmircv1tmgoSAkc6UKggO/2120031f5d7f810ab6a13edae3642fe5/EmpowerEarlyCareerDevs.pdf Ethical Data Collection for Regular Developers Colin Flemingさんによる、自身の経験を基にした機密性の極めて高いデータを扱う際の倫理についての講演でした。 昨今の巨大テックカンパニーのデータの扱いをみて、データの扱いにもACM Code of Ethicsのようなベストプラクティスが必要になっているとのことでした。 ACM Code of Ethicsとはコンピューターのプロフェッショナルが倫理的な決断を下すのを助けるためのガイドラインです。 ColinさんはこのACM Code of Ethicsをシンプルに3つのポイントに要約していました。 Everyone is a stakeholder in computing Avoid harm and protect privacy avoid yolo-driven development あくまで倫理に関する講演だったのでデータの収集、セキュリティなどの技術的なお話はありませんでした。 今回のColinさんの講演は中絶に関するデータという非常に機密性の高いデータを扱っているところに興味を惹かれたので、この講演を1日目で一番楽しみにしていました。 機密性の高いデータをクライアントから集める時にもう一度聞き返したくなるような内容でとても勉強になりました。 The Ruby Developer's Command Line Toolkit Brad Uraniさんによる、生産性を上げるツールの数々を紹介する講演でした。 dotfiles zsh tmux fzf のような多くの開発者の支持を得ている便利ツールが中心に紹介されていました。 他にはhomesickやgit/hooksでcommitの前に何かを仕込むTIPSであったり、gemrcやrailsrcを使って開発を少し便利にするTIPSの紹介もありました。 またQ&Aでは質問者が便利だと思うツールが挙がったりして、moshなどのツールが紹介されていました。 中心的に紹介されていたツールはどれも素晴らしく便利なものばかりで、開発を始めたばかりの人なら誰にでも聞く価値のある講演でした。 私も開発を始めた頃にこれらの便利ツールを知っていたら生産性が上がって幸せになっていただろうなぁと思います。 もし今回のBradさんの講演で紹介されていたツールの中に試したことのないツールがあればぜひ試して見ることをお勧めします。 使ったことのないツールが自分の作業効率にどれだけ貢献してくれるかは使ってみなければわからないので、すぐに試してみるのがよいと思います。 私もhomesickやfzfを試してみたいと思います。 参考 : https://docs.google.com/presentation/d/18LyKvXRYZZA1RuPsZ4MvwKlufMhXDhWIM15LmK3oQ8A/edit#slide=id.p Pointers for Eliminating Heaps of Memory MRI、RailsのコアコミッターであるAaron Pattersonさんによる、Rubyのメモリ消費量を削減する2つのパッチを紹介する講演でした。 1つ目のパッチは require のメモリ消費量を削減するものでした。 このパッチを理解するのに前提知識として必要なのがShared Stringという概念です。 これはRubyの文字列オブジェクト(RString)が同じ文字列を共有することなのですが、1つ注意点があります。 それはRubyの文字列オブジェクト(RString)が参照する文字列が最後の文字まで等しくないと、同じ文字列が共有されないということです。 例えば /a/b/c.rb と /b/c.rb の文字列オブジェクトは /a/b/c.rb という文字列を共有しますが a/b/c.rb と a/b/c の文字列オブジェクトは文字列を共有せず、それぞれ a/b/c.rb と a/b/c という別の文字列を参照します。 require '/a/b/c.rb とすると loaded_fatures_index というハッシュに以下のような8つの文字列がキャッシュされます。 '/a/b/c.rb', '/a/b/c', 'a/b/c.rb', 'a/b/c', 'b/c.rb', 'b/c', 'c.rb', 'c' 接尾辞に .rb とつく文字列オブジェクトは /a/b/c.rb という文字列を共有するのですが、接尾辞に .rb がない文字列オブジェクトはそれぞれ別の文字列( /a/b/c , a/b/c , b/c , c )を参照するようになっています。 今回そこで接尾辞に .rb とつかない文字列オブジェクトが全て /a/b/c という文字列を参照するようにすることで同じ文字列を共有できるようにしたそうです。 結果シンプルなRailsアプリケーションの起動時のメモリ消費量を4.2%削減することに成功したそうです。 2つ目のパッチはISeqオブジェクトから文字列を直接参照することでメモリ消費量を削減するものでした。 ISeqオブジェクトとは、Rubyのコードをコンパイルすると生成されるオブジェクトです。 そしてこのISeqオブジェクトの中にバイトコードが格納されます。 ここで登場するのが mark array というオブジェクトがGCに消されないようにオブジェクトをマークするのに使われる配列です。 今まではISeqオブジェクトはこの mark array を通してオブジェクトを参照していました。 そこでこのパッチでISeqオブジェクトから直接オブジェクトを参照するようにしたようです。 結果Railsプロセスのメモリ消費量を6%削減することに成功したそうです。 今回のAaronさんの講演はRubyの内部をわかりやすく紹介する内容にもなっていてとても面白かったです。 またAaronさんはハンバーガーの帽子をかぶって登場し、GithubがMicrosoftに買収されたことにかけたダジャレを連発したりしてとてもユーモアがあって素敵でした。 Parallel programming in Ruby 3 with Guild Koichi SasadaさんによるGuildを使った並列処理の講演でした。 GuildとはRuby 3で導入予定の新しい並列処理です。 1つのGuildは1つ以上のスレッドを含み以下のような構成になっているそうです。 異なるGuildに入っているスレッドは並列処理できる 同じGuildに入っているスレッドは並列処理できない またGuild間では共有できるオブジェクトが決まっており、Guildを使うことでスレッドセーフな処理を簡単に書くことができるとのことでした。 Guild間で共有できるオブジェクトには以下が挙げられていました。 イミュータブルなオブジェクト(fronzeしたオブジェクトはそのオブジェクトに含まれるオブジェクトもすべてfrozenされている必要がある) Class/Moduleオブジェクト Isolated Proc また、共有できるオブジェクトについては特別なプロトコルを設けたいとも話していました。 案としては紹介されていたものは、GuildからGuildへのオブジェクトの受け渡しです。 Guildから他のGuildへオブジェクトを渡し、元のGuild内からは渡したオブジェクトが参照できなくなると言ったアクターモデル。 または、Goのchannelのような特別なオブジェクトを利用したCSPモデルが案として紹介されていました。 またこのGuildという名前はまだ決定しておらず code name だそうです。 デモではフィボナッチ数列を40vCPUの上で40個のGuildを使って並列処理した結果とシリアル処理した結果の比較を見ることができました。 n = 30の時点で実行時間に約16.2倍の差が出ていました。 今回のSasadaさんの講演ではGuildの実装ステータスなどのお話もあり、Guildに関する色々な情報を知ることができてとても面白かったです。 Ruby 3でGuildを使った並列処理を書くのが楽しみで仕方がないです。 参考 : http://www.atdot.net/~ko1/activities/2018_rubyconf2018.pdf Building Serverless Ruby Bots NetflixのDamir SvrtanさんによるServerlessをRubyで実現するお話でした。 AWS Lambda上でRubyを動かす話が中心に行われました。 現在AWS LambdaはRubyに対応しておらず、直接RubyをLambda上で動かすことができません。 そこで、 Traveling Ruby ・ mruby ・ jruby の3つRuby処理系を利用しRubyをLambdaで実行可能にする方法が紹介されました。 Traveling Ruby、mrubyの場合はjsにコンパイルを行い、jrubyはjarにコンパイルしLambda上でRubyの実行を実現していました。 それぞれの処理系のコードサイズ、メモリ使用量、実行時間についての比較が行われていました。 処理系 コードサイズ メモリサイズ 処理時間 ウォームアップ後の処理時間 Traveling Ruby 6.7MB 25MB 3900ms 3300ms mruby 945KB 40MB 3900ms 3300ms jruby 23.2MB 150MB 25000ms 1200ms やはりLambdaのコールドスタートの影響で、jvmの起動に時間がかかると示されました。 ただしウォームアップされた状態だとjrubyの処理時間がダントツで早くなったそうです。 AWS Lambda以外でのServerlessについてもお話され、IBMクラウドのサーバレスのシステムがRubyに対応していることが紹介されました。 また、Rubyに対応したServerlessフレームワークServerless/FaaStRubyが紹介されました。 AWSLambdaでRubyを利用するには、まだまだ必要であり早く公式対応をしないかなと思いました。 3つの処理系でのメトリクスの比較はウォームアップ後にjrubyでの実行が高速になることなど、Lambdaやjvmの特性に合わせて計測を行っており参考になるなと感じました。 また知らないクラウド技術やフレームワークが紹介されておりとても興味深かったです。 Runnning a Government Department on Ruby for over 13 Years Jeremy Evans( @jeremyevans0 )さんによる California State Auditor という政府組織のレガシーなシステムをRubyを利用してモダナイズしているというお話でした。 もともとのシステムはすごくレガシーな状態でRubyを利用してモダナイズしているそうです。 モダナイズにRubyを選んだ理由としては以下の4つ挙げていました。 高速に開発が可能 簡単にモダンなアプローチを採用できる 容易に学習が可能 楽しい 高速開発、並びにモダンなアプローチは様々なOSSを駆使することが可能で、Rubyには豊富なOSSライブラリが揃っています。 また、政府組織のシステムということもあって採用の問題があると語っていました。そのため、学習が容易であるRubyを利用することで、誰でも開発に参加できます。 また、スピーカーの方は Roda というルーティングライブラリ。 Sequel というDBライブラリを作成しており、California State Auditorのシステムで利用しています。 Railsを利用せずにCodaやSequleを開発している理由としては、モダナイズのプロジェクトを進めるにあたり、スタートは小さくシンプルにしたかったとのことでした。 利用している技術スタックがFreeBSD/PostgreSQL/nginx/Unicornと私達にとってすごく身近なものでした。 勝手な思い込みですが政府のシステムでRubyが使われているとは思っていなかったのですごく興味深かったです。 参考 : https://code.jeremyevans.net/presentations/rubyconf2018/index.html It's Down! Simulating Incidents in Production Kelsey PedersenさんによるStitch Fixで実際に行っているインシデント発生時のシミュレーションやカオスエンジニアリングについてのお話でした。 インシデント発生シミュレーションは9つのステージにそって行われます。Stich Fixではシミュレーションの当日をGameDayと呼んでいるそうです。 1. Define simulation まずはじめに、どのインシデントを対象にしたシミュレーションを行うのかを決定します。インシデントはデータベース障害・インスタンス障害・外部サービスの障害など様々な障害が原因で発生します。 発表ではサービス障害インシデントのシミュレーションを選択し、サービスが500エラーを返した場合についてシミュレーションされていました。 2. Implementation 1で設定したインシデントが発生するように実装を行います。 今回のケースではHTTPクライアントライブラリの Faraday にシミュレーション用の実装をしていました。 まず、インシデントを発生させるためのユーザーを作成します。そして、対象ユーザーの場合のみFaradayが常に500を返すようにoverwriteしていました。 3. Gather expectations チーム内で、対象のインシデントがサービスにどのような影響が出るのか議論を行います。 実際に行なった議論では、動かなくなる部分は発生するがクリティカルな問題は発生しないという意見が7割であったそうです。 4. Huddle as a team テーム全員が集合します。インシデントを想定して各人がリモートでビデオチャットを利用し議論している様子が見られました。 5. Talk through expectaion インシデントに対してどのようなことが起こるのか議論し、洗い出しを行います。それを以下の2点の観点からまとめを行っていました。 bullet point list of expectations list the set of instructions 6. Run it! 以下のようなコマンドで、準備したインシデントを発生させます。 bundle exec rake gameday:start そして、実際に何が起こるのかをサービスのページ・サービスのメトリクスを見て確認します。 最後に以下のコマンドで、GameDayを終了します。 bundle exec rake gameday:end 7. Revisit expectation 事前に想定していたインシデントと比べ、実際におきたインシデントがどう違ったかを振り返ります。 8. Write and edit runbooks インシデントが起こった時にどのような対応を行ったかをゲーム実施前のドキュメントに追加します。 9. Update dashboards エラー監視ツールのダッシュボードなどにGameDayなどタグ付をし、エラーメトリクスがGameDayのものであることを判別できるようにします。 実際のインシデントに対してシミュレーションを行っておくことで、いざそのような状況が発生した場合に焦らず対応ができるようになるだろうなと思いました。 さらにインシデントが発生した場合に起こる問題を事前に対策できるなと考えました。 シミュレーションを行う環境を整えることは大変なことですが、安全で柔軟なシステム運用を行うにあたりとても大切なことであると実感しました。 弊社でもこの発表を参考にインシデントに対するシミュレーションを行なってみたいと思いました。 Yes, You Should Provide a Client Library For Your API Daniel Azumaさんによる、APIのクライアントライブラリについての話でした。 Danielさんは、Google Cloud Platform(GCP)のRubyAPIクライアント開発者の方です。 Googleでは膨大な数のAPIとAPIのクライアントライブラリを提供しています。 それらをメンテナンスし続けるにあたり、APIクライアントライブラリを開発する際に気をつけるべき点についてお話しされていました。 具体的にはAPIクライアントライブラリ内で以下の4つのことを意識することが大事とのことでした。 独自で処理をラップしたクラスなどを作らずRubyの記法をつかって開発を行う エラーハンドリングを適切に行う セキュリティを担保する パフォーマンスを意識する パフォーマンスを意識するという話の中で、ログの書き込み処理などについてはユーザーからのリクエストを同期的に処理せず、非同期にバルク処理を行うことでパフォーマンスを向上させていると話されておりとても参考になりました。 またOpenAPIのようなインタフェース記述言語を使用し、コードジェネレータを用いてAPI定義書からAPIクライアントライブラリのインタフェースを生成することを推奨されていました。 API定義書からコードジェネレータを用いることで、APIクライアントライブラリのインタフェース以外にも リファレンスのドキュメント サーバーのスケルトン テスト などを生成することもできるため、API開発と保守の双方の役に立つということでした。 この発表を聞き、REST APIの開発はSwaggerなどのAPI定義書からAPIのインタフェースやドキュメント、テストはコードジェネレータで生成するという開発の流れがよさそうだと思いました。 発表スライドがとてもわかりやすいので、興味のある方はぜひご参考ください。 弊社でも以前、SwaggerからAPIのシリアライザーを生成するgemを開発した記事を公開しているのでこちらも参考にしてください。 techblog.zozo.com Unlearning: The Challenge of Change RubyConfでは、女性の参加者・登壇者の多さがとても印象的でした。 カンファレンス自体とても初心者歓迎の空気を感じましたし、Rubyの話だけでなく開発手法やエンジニアとしての働き方などの話もあり女性の方も参加しやすいのかなと感じました。 また3日間の最初のKeynoteの登壇者が、Matzさん以外女性であったこともとても印象的でした。 3日目のKeynoteはJessie ShternshusさんのUnlearningについてのお話でした。 Unlearningとは今まで学んだ知識や自分が持っている価値観を意識的に捨て、新たに学び直すことを意味するそうです。 新しい知識やスキルを身につける際、既存の知識があると既存の知識や習慣に頼ってしまいがちです。 しかし未学習の場合は外部から様々な学習法を探し取り入れようとします。そのため組織やチームで、新しいことを身につけたり変化を起こそうとする時に、すでに知識を身につけていることが足枷になることがあります。 そのためunlearningnの重要性についてお話しされていました。 一度自分がもってしまった価値観を捨てることは難しいため、日常的に何をunlearningしたいのかを考え意識的に習慣や行動を変えることが必要とのことでした。 発表中にunlearningを体験するために、2人1組で相手が空中に書く数字を反対側からなぞるというペアワークを行なったり、自分がunlearningしたいものを書いて紙飛行機を飛ばしたりと体験が混ざった発表で楽しかったです。 まとめ 今回私にとって初めての海外カンファレンス参加でしたが、会場の雰囲気がとても暖かくオープンで世界中から集まったRubyist達と交流できた素晴らしい経験になりました。 ロサンゼルスは少し肌寒かったです。それなのに会場は冷房がかかっていて思わぬカルチャーショックでした。食べ物に関してはさすがアメリカの中でも特に人種的な多様性に富んだ州ということで、様々な国の食べ物を楽しむことができました。特にメキシコ料理が良かったです。 講演は全て英語だったわけですが、中には早口なスピーカーもそれなりにいて英語慣れしていない人には厳しいように感じました。個人的には会話中に声量がなさ過ぎて聞き返されることが多々あったので、少しうるさいと感じるくらいの声量を意識して会話をしていました。 また、今回のRubyConf参加に関する費用はすべて会社が負担してくれました。 来年のRubyConfはテネシー州ナッシュビルで開催予定です。一緒に行きたい! という方がいましたら、ぜひ以下のリンクからご応募ください。お待ちしております。 www.wantedly.com
アバター
こんにちは。イノベーション推進部の武田です。 Google Assistantアプリを開発するときの本番環境と開発環境の切り分けについて紹介します。 はじめに 最近Google AssistantやAlexaなどのVoice User Interfaceが熱いですね。 毎日のように新しい記事を目にしますし、新しいハードもどんどん登場しています。 プラットフォームごとにSDKも公開されており、誰でもアプリを開発し公開できます。 開発の初期段階では自分だけが触るので、試せる環境が1つあれば十分です。 しかし、開発したアプリを審査に出したりリリースしたとなるとそうは行きません。 そこで 審査や実際に稼働している処理に開発の影響を出さない ことを目的とした環境の構成を紹介します。 アプリの構成と役割 Google Assistantアプリは基本的にActions on GoogleとDialogflow、 そしてCloud Functionsなどのバックエンドサーバーの3つで構成されています。 簡単に説明すると以下のような役割を持ちます。 Actions on Google アプリ自体の管理を担当。アプリ名や言語などを設定したり、審査やリリースといった操作が可能。 Dialogflow 発話内容の分析を担当。会話のパターンや認識する単語を設定できる。 バックエンドサーバー(Cloud Functionsなど) 発話内容に応じて任意の処理を実行する。自分でサーバーを用意することになるため、AWS Lambdaなどでも良い。 ユーザーからのリクエストを含めた処理の流れが下記の図です。 場合によってはバックエンドサーバー自体がなかったり、 バックエンドサーバーの先に別のAPIだったりDBだったりがつらなってきます。 今回は図に示した構成で考えていきます。 本番環境と開発環境の切り分け 切り分けにはActions on GoogleのスナップショットとGoogle Cloud Platform(以降GCP)のプロジェクトを利用します。 図にすると以下のようになります。 切り分け方ごとに説明して行きましょう。 Actions on GoogleとDialogflow Actions on GoogleとDialogflowはスナップショットの仕組みを利用します。 Actions on Googleはバージョン管理機能を備えており、 スナップショットは各バージョンの処理内容を保存したものです。 スナップショットは審査の提出やテスターへの配信の際に自動で作成されます。 開発している途中のようにスナップショット作成時から何かしら変更がある場合、 現在の状態はドラフトとして保存されています。 作成されたスナップショットは、審査やテスト配信など状況に合わせた環境へデプロイされます。 開発中の内容が保存されているドラフトは、開発者のアカウントやシミュレーターで実行可能です。 そのため、無理に別のプロジェクトに分けなくても動作環境は分離されているわけです。 バックエンドサーバーの接続先はDialogflowで設定します。 スナップショットとドラフトで別の設定をしておけば、 リクエストを別々のバックエンドサーバーに飛ばすことができます。 ただ、接続先を手動で切り替えることになるため開発体制によっては何かしら対策が必要です。 Actions on GoogleとDialogflowもGCPの一部であるため、 プロジェクト自体を切り替えて環境を分けることは可能です。 しかし、これらはGUIでの操作がメインです。 プロジェクトを分けると情報の同期が手間になってしまうので、スナップショットを利用する方法を取っています。 バックエンドサーバー(Cloud Functions) Cloud Functionsを利用している場合、 GCPのプロジェクトを別にしてしまうのが良いと思います。 下の図の赤枠の部分です。 Cloud Functionsはデプロイせずにローカルで検証していくことがある程度可能ですので、 プロジェクトを分けなくても動作環境は分離できます。 しかし、Google Homeなどのデバイスやシミュレーターで会話の検証をする場合、 設定したエンドポイントにリクエストが飛ぶことになるためデプロイが必須となってきます。 また、開発するアプリがFirebaseのMessagingやFirestoreを利用していると、 プロジェクトを切り替えれば全てまとめて切り替わってくれるのは楽です。 複数のプロジェクトの管理はFirebase CLIを利用すれば簡単に行えます。 まとめ この方法を使えば、審査や本番に影響を出さずに開発もガンガン進めていくことが可能です。 しかし途中で挙げた手動切り替えなどの問題を考慮すると、 Actions on GoogleとDialogflowも別プロジェクトに切り分けた方がいい状況もあると思います。 このあたりはCLIでの管理が進んできたり、開発のプラットフォームが安定したら考えていきたいところです。 VUIはチャレンジングで非常に面白い領域です。 プロダクトのUXを考えるのもそうですし、開発の方針や設計も手探りな状態なのでやること全てが挑戦です。 今回は技術寄りの内容でしたが、もっとプロダクト寄りの話もしていきたいと思ってます。 最後に DroidKaigi 2019 に採択されました! Google Assistantアプリのベータテスター配信やCLIでテストしやすい構成など、ちゃんとサービスを作るための内容を発表します。 もし都合が合う方はぜひお越しください! VUI界隈を盛り上げたかったり、新しいプロダクトを作りたい人を募集中です! 一緒に世界をカッコよくしていきましょう! www.wantedly.com
アバター
こんにちは。品質管理部エンジニアリングチームの遠藤です。 私の所属している品質管理部では、業務の一環として、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
アバター