TECH PLAY

BASE株式会社

BASE株式会社 の技術ブログ

576

こんにちは!基盤グループのめもりー ( @m3m0r7 ) です。最近発売された MacBookPro 16 inch を個人的に買ってご満悦です。 この記事は、 BASE アドベントカレンダー24日目 の1つ目です。 みなさんは普段プログラミングをするのはどういうときでしょうか。仕事やプライベート、そもそもプログラミングそのものが趣味…などいろんなシーンがあるかなと思います。 私は会社ではプロダクトのためのコードを書いていますが、プライベートでは本来プロダクトで触らない、そう例えば PHP で JVM を実装したり、 PHP そのものを JVM 言語にしていたり、それ以外にも自宅のペットの監視システムを作ったりなどなどしています。 さて、早速ですがこれをご覧ください。 .hello-world { property 1 : value 1 ; property 2 : value 2 ; } これを見てあなたはどう感じましたか?「CSS だ!」と感じましたか、それとも「どうやったらパースできるんだろう」と感じましたか? 今日のアドベントカレンダーはプロダクトとは少し離れたお話で、PHP で CSS をパースしてみようと思います。 CSS は Selector Level 3 からいろんな書式が扱えるようになりました。 とはいえ、これをすべてパースするのはとても時間がかかります。簡単なセレクタとプロパティをパースしてプログラム側で扱いやすいようなところまでをゴールとしてやってみようと思います。 この要件としては .hello-world をセレクターとして書き出したい。 property1, property2 を .hello-world のプロパティ一覧として扱いたい。 といったところでしょうか。 これをコードで表すには概ね下記のようにする必要があるかと思います。 { までをセレクターとして扱う { , } の間の値をプロパティ文字列として扱う、言い換えると { を読み取った位置から } までをプロパティ文字列として扱います。 プロパティ文字列から : までをプロパティ名、 ; までを値として扱い、終わるまで繰り返す。 まずは、 { や } , : , ; までを読み込む、つまり任意のトークンまでの文字列を取得する関数を定義します。 function readTo(string $text, string $delimiterToken, int & $i): string { $length = mb_strlen($text); $string = ''; // 指定したトークンまで読みすすめる while (($char = mb_substr($text, $i, 1)) && $char !== $delimiterToken && $i < $length) { $string .= $char; $i++; } return $string; } 上記のような形にします。この関数は、 指定したトークンまで読みすすめる、または CSS の終端までの間の文字列を返す ためのものです。 例えばこれを readTo($css, '{', $i); 上記のように呼び出すと、 { までの文字列を取得します。これを使って { と } までのそれぞれの文字列を読み込みます。 $selectors = []; $length = mb_strlen($css); for ($i = 0; $i < $length; $i++) { $statement = [ 'selector' => '', 'properties' = > [], ]; // { まで読みすすめる $statement['selector'] = trim(readTo($css, '{', $i)); // この時点だとまだ { なので、一つすすめる。 $i++; $statement['properties'] = trim(readTo($css, '}', $i)); $selectors[] = $statement; } これを出力すると array(1) { [0]= > array(2) { ["selector"]= > string(12) ".hello-world" ["properties"]= > string(41) "property1: value1; property2: value2;" } } 上記のようになります。次に与えられたプロパティ文字列をパースするために下記の関数を定義します。 function parseProperties(string $propertyString): array { $length = mb_strlen($propertyString); $properties = []; for ($i = 0; $i < $length; $i++) { $ name = trim(readTo($propertyString, ':' , $i)); $i++; $ value = trim(readTo($propertyString, ';' , $i)); if ($ name = == '' ) { continue; } $properties[$ name ][] = $value; } return $properties; } これも上記と原理は同じで、 : までと ; までの文字列をそれぞれ取得し、それを配列に入れています。 また、名前がない箇所はプロパティとして考えないようにもしています。 これを $statement['properties'] = trim(readTo($css, '}', $i)); に通してあげて、 $selectors = []; $length = mb_strlen($css); for ($i = 0; $i < $length; $i++) { $statement = [ 'selector' => '', 'properties' = > [], ]; // { まで読みすすめる $statement['selector'] = trim(readTo($css, '{', $i)); // この時点だとまだ { なので、一つすすめる。 $i++; $statement['properties'] = parseProperties(trim(readTo($css, '}', $i))); $selectors[] = $statement; } 上記のようにして出力すると、下記のように取得できます。 array(1) { [0]= > array(2) { ["selector"]= > string(12) ".hello-world" ["properties"]= > array(2) { ["property1"]= > array(1) { [0]= > string(6) "value1" } ["property2"]= > array(1) { [0]= > string(6) "value2" } } } } CSS のパースができました。上のコードをまとめると下記のようになるかと思います。 <?php $ css = <<< EOS . hello - world { property1 : value1; property2 : value2; } EOS; $ selectors = [] ; function readTo ( string $ text , string $ delimiterToken , int &$ i ) : string { $ length = mb_strlen ( $ text ) ; $ string = '' ; // 指定したトークンまで読みすすめる while (( $ char = mb_substr ( $ text , $ i , 1 )) && $ char !== $ delimiterToken && $ i < $ length ) { $ string .= $ char ; $ i ++ ; } return $ string ; } function parseProperties ( string $ propertyString ) : array { $ length = mb_strlen ( $ propertyString ) ; $ properties = [] ; for ( $ i = 0 ; $ i < $ length ; $ i ++ ) { $ name = trim ( readTo ( $ propertyString , ':' , $ i )) ; $ i ++ ; $ value = trim ( readTo ( $ propertyString , ';' , $ i )) ; if ( $ name === '' ) { continue ; } $ properties [ $ name ][] = $ value ; } return $ properties ; } $ length = mb_strlen ( $ css ) ; for ( $ i = 0 ; $ i < $ length ; $ i ++ ) { $ statement = [ 'selector' => '' , 'properties' => [] , ] ; // { まで読みすすめる $ statement [ 'selector' ] = trim ( readTo ( $ css , '{' , $ i )) ; // この時点だとまだ { なので、一つすすめる。 $ i ++ ; $ statement [ 'properties' ] = parseProperties ( readTo ( $ css , '}' , $ i ) ) ; $ selectors [] = $ statement ; } var_dump ( $ selectors ) ; 本来は media クエリのような { , } などのネストにも対応させたり、セレクタの優先度があったり複雑になるのですが、ブログの 1 記事に収められなくなってしまうため、今回は簡単にパースするまでのお話でした。
アバター
こんにちは。基盤グループのめもりー ( @m3m0r7 ) です。 この記事は、 BASE アドベントカレンダー24日目 の 2 つ目です。 みなさんは、いざ登壇をするとなった時の準備に何を意識されていますか。例えば「本番で失敗しないように練習をたくさんする」だとか「場数をこなす」とか「伝わらないと困るからとりあえず資料に盛り込んどく」などなどあるかと思います。 私の初めての登壇は 2019 年の 1 月 26 日の PHP カンファレンス仙台が初めてで、多くの方に「え?本当ですか?信じられない」と言われたりもしました。今年はいろんな場所で、幸いにも登壇させていただける機会をいただき、初めての登壇回数は 15 回、登壇時間 270 分までにものぼりました。 とはいえ、私自身、いろんな方の登壇を見させていただいてもっと精進しなければならないと痛感しており、日々どうしていくべきか試行錯誤しています。 そんな中で、登壇についての心構えや資料作成といったベストプラクティスのような解説を書かれている文献がそう多くないと主観ではありますが感じてきていました。そのためどう資料を構成したり、登壇までに何をしたらいいのかがわからず、多くの方は登壇自体のハードルが高いと感じてしまうのではないかと思っています。私も実際そうでした。 私自身エンジニアとしてはひよこですが、今まで1日に何個も企画資料や開発資料、外部へのアウトプットサービスを通じて得た経験を元に社内のドキュメントに「資料作成のすゝめ」を投稿したところ、ブログに公開してほしいと嬉しいお声がけを頂いたので、ブログ向けにリライトしたものを公開したいと思います。 資料の作成 相手に「何を伝えたいのか」を自分自身が完璧に理解する 資料の作成には自分自身の納得と理解が必要だと感じています。私はなにを伝えたいのか?という自問自答を繰り返していきます。そして、発表タイトルから話すゴールを決定します。例えば私の場合であれば「PHP で JVM を実装して、 HelloWorld を出力してみる」という内容であれば「Hello World を出力する」というのをゴールとして置きます。そして、ゴールとは別に、伝えたいことを設定します。 「JVM の実装はそこまで難しくない」というのがこの発表での伝えたいことでした。 JVM を Go, R や Ruby などで実装してくれる方々が出てきてくださってとてもうれしく感じました。私自身は登壇の前にはこの 2 つを設定するようにしています。 Go製自作JVM、MITライセンスつけてオープンソースにしました。 main.goの1ファイルのみで700行程度です。 めもりーさんの発表資料とJVM公式仕様書のみを見て作りました。 https://t.co/I8f80A70s2 — ドキュネオ (@DQNEO) September 13, 2019 書いた。みんなも好きな言語でJava VM作ると良いよ。 Java VM 自作 方法 https://t.co/BcV8bDv7tG — igjit (@igjit) December 19, 2019 "RubyでJVMを実装してみる" の発表資料です Google Slides: https://t.co/3liwmqFrZ4 … #heiseirubykaigi #heiseirubykaigiB (ハッシュタグつけ忘れてたので再ツイート) — Daiki Miura (@daikiii5555) December 14, 2019 いろんな方にご興味を持ってもらえたのがスピーカーとしてとてもうれしく思っています。 アジェンダの構成 資料におけるアジェンダは重要な役割を担っています。オーディエンスがどういったトークになるのかを頭の中で流れを理解できる唯一の情報源になるからだと考えています。 起承転結で書くと言われてもそこまでのイメージが湧きづらく筆がなかなか進まないこともあり、スタートから作っていくことを私は辞めました。 発表タイトルからプロフィール、そして話したいことを順番に作っていくとなると予め頭の中に何を話したいのか順序立てられており、スライドのイメージが既に湧いていないと難しいなという結論です。 したがって、私が資料を作る際には上にも書いてあるようにゴールと伝えたいことの 2 つ定義をした後、ゴールからスタートまでの大きな章を 4 〜 6 つほどに分割します。 そして、スタートは「なぜしたのか?」というところに落ち着くようにします。 例ですが、PHP で JVM を実装するという話では主に下記のように章分けをしました。 ▲ゴール Hello World を出力するまで JVM のオペレーションコードの話 PHP と JVM の互換性の話 JVM を実装するとは? なんでこんなことをしたのか ▼スタート そして、章分けをした後それぞれの章が一つの発表内容だと考え、それぞれの章をさらに章分けをします。 ▲ゴール Hello World を出力するまで 必要なもの 使うもの JVM 実装までの大まかな流れ オペランドスタックの説明 オペレーションコードを知る オペレーションコードの処理を実装する JVM のオペレーションコードの話 オペレーションコードとはなにか? Constant Pool とは? PHP と JVM の互換性の話 制約 それでも実装するのか? JVM を実装するとは? コンパイルされた class ファイルを読んでいくこと コンパイルされた class ファイルとは? JVM の実装は難しくない なんでこんなことをしたのか 刺激がほしかった みんなやってないことをしたかった PHP という言語そのものの可能性 ▼スタート 上記のように章に分割できないまで繰り返していきます。そうすると必然と細かい箇所で話したい内容が明確になったり前後のスライドで辻褄が合わないから変えようといった具体的な改善点が見いだせます。 これを何度も繰り返していき、資料のストーリーを完成させます。 最後にストーリーを埋めていく 最後にスライドのストーリーを埋めていきます。これはアジェンダの次にとても大事なことです。 立てられた章のタイトルから話が脱線しないようにその内容に関係する方法や術、内容だけを書いていきます。そうすることにより、オーディエンスが聞いた時にコンテキストのスイッチが発生せずに済み、すんなり内容が入りやすくなるためだと考えているからです。 資料の存在意義 資料の存在意義で最初に思い浮かぶのはなんでしょうか。「自分のセッションを聴きに来てくれた方に伝えたいことがあるから」というのが理由の一つではないでしょうか。 私はもう 2 つ理由があると思っています。 当日セッションを迷いに迷って見に来れなかった人に向けての資料 自分自身が資料を暗記するという辛さから逃げるための術 当日セッションを迷いに迷って見に来れなかった人に向けての資料 当日魅力的なセッションがたくさんあります。自分のセッションが見れなかった人が資料を公開した際に見る可能性があります。その資料がワンラインだけだと、伝えたいことが意図と反して伝わっていたり、そもそも伝わららない可能性があります。せっかく興味を持ってもらえているので、それは避けたいです。 自分自身が資料を暗記するという辛さから逃げるための術 そして、もう 1 つが自分自身が暗記をしなくて済むようにするためでもあります。登壇当日、緊張しないとは限らないため予防線の一つとして、話したいことを資料に粗く書くことにより、頭の中が真っ白になったりしても資料に書いてある文字を読み上げるだけで済む逃げ道ができます。緊張が解れたら、書いてあること以外に自分が伝えたかった内容を話せばよいと私は考えています。今ではほとんど緊張しないものの、一番はじめの登壇である PHP カンファレンス仙台の際はとても緊張していましたが、この予防をしておいたため、伝えたいことを伝えれたと思っています。 そして、粗く書くというのは、見れに来れなかった人や資料を読み返した人でも、ある程度は伝えたいことのニュアンスが、すんなり入って来るのではないかと思います。 資料は熟成させる 資料は普段は登壇 1 ヶ月前くらい、遅くても 3 日前に作り終わるように心がけています。この理由は 発表内容を忘れるため です。発表内容を忘れることにより、発表数日前に読み返した時に理解できるのか?を見ます。つまり資料を熟成をさせます。熟成した資料は誤字脱字、そして公の場で公開するのに適切であるのかを冷静な判断で見ることができます。例えば深夜の 26 時に勢いで書いたコードを翌日見直すと「なんだこれ!!」となる経験は誰にでもあると思います。資料も同じで、その時できたと思っていても数日立つと前後のスライドで話が噛み合っていなかったり、唐突に現れる専門用語だったりが浮き彫りになることもあります。そのため熟成という過程はコードにおいても資料においても大事であると考えます。 登壇の準備における心構え 「意義」のあるトーク練習をする 「意義」のあるトークとは何か?それは伝えたいことを伝えきることだと私は考えています。 そのためには実際のトークにかかる時間を綿密に計測し、時には伝えたいことを減らす、または増やすということが必要です。 慣れてくると発表時間と自分のトークに対してどれほどの資料の枚数が必要なのか自然とわかるようになってきます。 登壇前日に行うべきこと デモがある場合は事前に動くかテストをし、そのあとは発表のことは忘れて酒を飲んで楽しみます。 登壇本番 自分はピエロだと思いこむ 登壇当日私は自分自身のことを「ピエロ」だと思いこむようにしています。これは誰かにそう言われたからではなく、自分自身の緊張を解す意味でもそう思うようにしています。ピエロであるからには、オーディエンスの皆様に楽しんでもらって帰ってもらいたいという気持ちを持つようにします。 楽しんでもらうわけですから、誰か一人でも傷つけたり不快な思いにならないように最善の注意を払います。 画面に映された資料は凝視してもいい 緊張をするとどうしても資料を凝視しがちです。それが良くないと全く思ってなくて、逆に凝視してもいいのではないかと思っています。楽しくなってもらえて、更に伝えたいことを伝えれることができれば、それ以上のことは慣れてからでも問題ないと強く思っています。 ここまで書いてきましたが… ここまで書いていて私自身もできていないことが多くあります。私も迷ったらこの記事自体を見返すかもしれません。それでも完璧にやる必要はなく、あくまで一つの手段だと思っていただけると幸いです。これよりももっと良い方法を自分たちで作っていければ、もっとよりコミュニティ活動及び貢献が活発になるのではないかと考えています。
アバター
2019 アドベントカレンダー23日目 この記事は BASE Advent Calendar 2019 の23日目の記事です。 devblog.thebase.in こんにちは、11月よりBASE BANK株式会社に入社し、Dev Divisionに所属している清水( @budougumi0617 )です。 23日目の本記事では、レイヤードアーキテクチャを採用している上で頻出するであろうボイラープレートの悩みを共有します。 そして、Go言語(以下Go)でコードを自動生成するためのツールを作った話と、利用した text/template パッケージなどの公式パッケージの概要に触れます。 自動生成時に利用するテンプレートはGoの text/template パッケージを利用しますが、テンプレートファイルは自由に変更できるので、将来的にはGo以外のコードも生成する予定です。 BASE BANKにおけるアーキテクチャ構成 私が所属するBASE BANK株式会社では、「 YELL BANK(エールバンク) 」という即時に資金調達ができる金融サービスを提供しています。 この「YELL BANK(エールバンク)」というサービスはGo製のサーバとPython製のサーバを組み合わせたバックエンド構成で提供されています。 Go製のサーバに関しても、Python製のサーバに関してもウェブアプリケーションフレームワーク(WAF)を用いていないため、実装はレイヤードアーキテクチャに則ったコード、ディレクトリ構成となっています。 Go製のサーバのpackage構成(ディレクトリ構成)と各々のパッケージの実装内容についてはBASE BANK同僚の東口さん( @hgsgtk )の登壇資料をご参照ください。 このようなレイヤードアーキテクチャによる設計アプローチは、言語やWAF特有な構成に依存しないため、Goで実装されていても、Pythonで実装されていてもディレクトリ構成自体はほぼ同じです。 また、適切に分割された各コンポーネントのコードが担う責務はシンプルな単一責務であるため、プロダクトに途中から参加したような私でも比較的簡単にコードを理解することができました。 ただ、レイヤードアーキテクチャやクリーンアーキテクチャというアプローチを取ったとき問題になるのが、似たような構造のコードを大量に生成する必要が発生することです。 レイヤードアーキテクチャとボイラープレート レイヤードアーキテクチャやクリーンアーキテクチャに基づいて実装を行なっていくと、ひとつのユースケースに対して大量のファイルを作成する必要が発生します。 たとえば、「ユーザーを登録する」というような機能を作成する場合、我々のプロダクトでは、最低でも7ファイルを作成する必要があります。 ./ ├── domain │   ├── model/user.go │   └── repository/user.go │  ├── infrastructure/datastore/user.go │ ├── interfaces/controller │      ├── create_user_controller.go │      └── create_user_controller_test.go │ └── service ├── create_user_service.go └── create_user_service_test.go クリーンアーキテクチャやレイヤードアーキテクチャに詳しい方ならば、なんとなくファイル内容は想像できるかと思いますが、一部を記載するとこんなカタチです。 // domain/model/user.go type UserID int type User struct { ID UserID // has some fields... } // domain/repository/user.go package repository type UserGetter interface { Get(model.UserID) (model.User, error ) } type UserSaver interface { Save(model.User) (mode.UserID, error ) } // infrastructure/datastore/user.go package datastore type UserStore struct { db *sql.DB } func (*UserStore) Get(id model.UserID) (model.User, error ) { /* do anything... */ } func (*UserStore) Save(u model.User) (model.UserID, error ) { /* do anything... */ } // service/create_user_service.go package service type UserRegisterService interface { Run(UserRegisterInput) (UserRegisterResult, error ) } func NewUserRegisterService( /* some args... */ ) UserRegisterService { return &userRegisterService{ /* fill fields... */ } } type userRegisterService struct {} // service/create_user_service_test.go package service func TestUserRegisterService_Run(t *testing.T) { tests := [] struct { /* test conditions */ }{ { /* test case 1 */ }, { /* test case 2 */ }, } for _, tt := range tests { t.Run(tt.name, func (t *testing.T) { // Arrange, Act, Assert... }) } } // interfaces/controller/create_user_controller.go package controller type UserRegisterController struct { Service service.UserRegisterService } func NewUserRegisterController(urs service.UserRegisterService) UserRegisterController { return UserRegisterController{ Service: urs, } } func (c *UserRegisterController) Handler(w http.ResponseWriter, r *http.Request) { // Handle Request by UserRegisterService } このように、ひとつのリクエストを処理するために複数の実装をする必要があります。 上記ではユーザー登録用のコードを例にしましたが、ユーザー削除を行う際には( model コードを除いて)上記の例のように同じ構成のファイルの組み合わせを「新たに」実装します。ここで、新たに作るユーザー削除用のコード群は各構造体の引数やメソッドの処理内容は違えど、ファイルを構成する要素はユーザー登録時とほとんど同じになります。 以上の実装方針に則ると、ひとつのレイヤー(ディレクトリ)の中には上記のようなコードのファイルが大量に増えていきます。 ./ └── service ├── create_user_service.go ├── create_user_service_test.go ├── read_user_service.go ├── read_user_service_test.go ├── update_user_service.go ├── update_user_service_test.go ├── delete_user_service.go ├── delete_user_service_test.go ├── create_company_service.go …… 同様の設計アプローチを取られている方ならば、同様の悩みをもったことがあるのではないでしょうか。 ほぼ似た構造体をつくることになるので、私は作成済みのエンドポイントのファイルをコピーして再利用可能な部分だけ残してから実装を行なっていました。エンジニアの三大美徳(怠惰(怠慢)、短気、傲慢)に反してますね。 rails generate コマンドや cake bake コマンドのようなWAFのコード自動生成機能を使えるならばスケルトンコードをすぐに生成できるでしょう。 ということで、今回は与えた「モデル名」と「アクション名」、「テンプレートファイル」に基づいて各レイヤーのボイラープレートを自動生成するためのCLIツールを作りました。 レイヤードアーキテクチャ用ボイラープレートの自動生成ツール lgen 作成したCLIツールは以下になります。 レイヤードアーキテクチャの実装はプロダクトやチームによっても微妙に差異があります。 そこでこのツールは以下のような特徴を持ちます。 実行オプションに渡された モデル名 と アクション名 を使ってコードを自動生成する 生成するコードは指定されたディレクトリ配下にあるファイルを読み込んで決める ディレクトリ構成もコピーすることで、各プロダクトのレイヤー構成に対応する テンプレートやディレクトリ構成を自由に設定できるので、様々なレイヤー構造に柔軟に対応できる ざっくり使い方を書くと、以下のようになります。 まず最初に、後述する text/template パッケージを使って アクション名 と モデル名 を変数としたこのようなボイラープレートの雛形を書いておきます。 // templates/usercases/get_user_usecase.go package usecase type {{ .Action | title}}{{ .Model | title }}Input struct {} type {{ .Action | title}}{{ .Model | title }}Result struct {} type {{ .Action | title}}{{ .Model | title }}Usecase interface { Run({{ .Action | title}}{{ .Model | title }}Input) ({{ .Action | title}}{{ .Model | title }}Result, error ) } func New{{ .Action | title}}{{ .Model | title }}Usecase() {{ .Action | title}}{{ .Model | title }}Usecase { return &{{ .Action }}{{ .Model | title }}Usecase{ } } type {{ .Action }}{{ .Model | title }}Usecase struct {} func (u *{{ .Action }}{{ .Model | title }}Usecase) Run( in {{ .Action | title}}{{ .Model | title }}Input, ) ({{ .Action | title}}{{ .Model | title }}Result, error ){ // Need to implement usercase logic return {{ .Action | title}}{{ .Model | title }}Result{ // Need to build result } } 次に、自分のプロダクトのレイヤー(ディレクトリ)構造に合わせてそれぞれのファイルを配置します。 $ tree templates templates ├── repositories │   ├── repository.go │   └── repository_test.go ├── controllers │   ├── controller.go │   └── controller_test.go └── usercases    ├── usecase.go └── usecase_test.go そして モデル名 と アクション名 などを指定してツールを実行します。 次の例では、 GetUser 操作に関するコードを templates ディレクトリ配下のレイヤー構造とテンプレートを使って、 myproduct ディレクトリ配下に自動生成します。 $ lgen -action get -model user -template ./templates -dist myproduct 実行すると、以下のように templates ディレクトリと同じディレクトリ構造の場所にファイルが生成されます。 $ tree myproduct myproduct ├── repositories │   ├── get_user_repository.go │   └── get_user_repository_test.go ├── controllers │   ├── get_user_controller.go │   └── get_user_controller_test.go └── usercases    ├── get_user_usecase.go └── get_user_usecase_test.go 最初に書いたボイラープレートの中身は、 モデル名 と アクション名 が展開されて以下のようなコードが生成されました。 // myproduct/usercases/get_user_usecase.go package usecase type GetUserInput struct {} type GetUserResult struct {} type GetUserUsecase interface { Run(GetUserInput) (GetUserResult, error ) } func NewGetUserUsecase() GetUserUsecase { return &getUserUsecase{} } type getUserUsecase struct {} func (u *getUserUsecase) Run( in GetUserInput, ) (GetUserResult, error ) { // Need to implement usercase logic return GetUserResult{ // Need to build result } } サンプルではテンプレートを数個しか用意しませんでしたが、これに加えて他のレイヤーのコード、対になるテストコードもテンプレートさえ用意すれば全て自動生成することができます。 この結果、いままで新しいロジックを書くときにコピペと置換を繰り返していた時間を削減できます。 これで2020年はロジックに集中してコードを書けそうです。 ツールだけでの紹介では終わってしまうので味気ないので、利用しているGoの標準パッケージについて紹介しておきます。 text/templateパッケージ Goにおいて、コードの自動生成ツールをつくるのならば、 text/template パッケージ 1 を使うことになるでしょう 2 。このパッケージは名前の通りテンプレートエンジンの機能を提供しています。 あまりこのパッケージを直接利用することはないかもしれませんが、Go製のOSSでそのまま text/template パッケージの仕組みが使われていることも多いです。 例えば、KubernetesエコシステムのひとつでGoで作られているHelmのChartの書き方も text/template パッケージそのままです。 https://helm.sh/docs/topics/chart_template_guide/ よって、直接利用することはなくとも text/template パッケージの記法を覚えて損はないと思います。 text/template パッケージの使い方はGoDocの該当パッケージの説明を読むのが一番です。 ざっくりと使い方を書くと次のようになります。 // https://play.golang.org/p/-bAjX-K1_TT buf := bytes.Buffer {} tmpl := ` username: {{ .Name }} email: {{ .Email }} ` params := struct { Name string Email string }{ Name: "John Doe" , Email: "john.doe@exampl.com" , } if err := template.Must(template.New( "samples" ).Parse(tmpl)).Execute(&buf, params); err != nil { panic (err) } fmt.Printf( "%s \n " , buf.Bytes()) // username: John Doe // email: john.doe@example.com buf は変数などを展開したあとの最終結果を格納するための変数です。 tmpl がテンプレート文字列です。 {{ .Name}} などは「 Execute メソッドの引数で受け取った構造体の Name フィールドをここに出力する」という意味になります。 params は tmpl テンプレートに埋め込むための構造体です。 if 文では samples という名前で tmpl テンプレート文字列からテンプレートを作り、 params 構造体を使ってテンプレートを展開する。という処理をしています。 ツールの紹介で記載したテンプレートでは、 {{ .Model | title }} というような記述をしていました。これは変数の値を出力前に関数にパイプして加工することができる機能です。 利用関数は type FuncMap map[string]interface{} が実体である template.FuncMap にマッピングしたあと、 Execute メソッドを呼ぶ前に Parse メソッドを使うことでテンプレート内で利用することが可能になります。 今回自作したツールの場合は strings パッケージの strings.Title 関数を title として登録しています。 // templates/usercases/get_user_usecase.go var fmap = template.FuncMap{ "title" : strings.Title, } if err := template.Must(template.New(sp).Funcs(fmap).Parse(dtmpl)).Execute(&buf, l.params); err != nil { return err } もちろん自作関数を登録しておくことも可能です。 文字列長を返す len などは既定で登録済みだったりします。 その他、 if 文や range などの制御構文もテンプレート内で利用することができます。詳細は text/template パッケージのGoDoc冒頭をご覧ください。 ちなみに、GoLandを利用している場合は、テンプレートファイルに {{- /*gotype: package/import/path.type_name*/ -}} といったコメントを入れておきます。 そうすることで、テンプレートファイル中でも構造体のフィールドに対する補完が有効になります。 path/filepathパッケージとosパッケージを使ったファイルの処理 今回自作したツールでは、コードの自動生成をする上で、 指定されたディレクトリにあるテンプレートファイル群 を利用します。 Goで「あるディレクトリの配下にあるディレクトリ、ファイルを使った処理」を書くには、 path/filepath.Walk 関数を使います。 他にも相対パスを絶対パスに変換するなどの処理があるのですが、ファイル・ディレクトリ処理に関しては @mattn さんがまとめてくださっている記事を見ればほぼ完結するので、ここでは説明を省略させていただきます。 今回は使っていないのですが、(上記記事が公開されたあとにリリースされた)Go1.12やGo1.13で、実行ユーザーのホームディレクトリを返す os.UserConfigDir 関数や os.UserHomeDir 関数も追加されています。 おわりに 今回はレイヤードアーキテクチャを使ってGoの実装をする際の悩みの共有をし、課題解決のためにコードを自動生成するコマンドラインツールを実装した話を共有しました。 みなさんのプロジェクトでどのようにコードのボイラープレート問題を解決しているのかも聞けたら幸いです。 また、コマンドラインツールを作る上で役に立つ text/template や file/filepath パッケージの紹介をしました。 Goでコマンドラインツールを作ればクロスプラットフォーム対応が簡単にできます。みなさんもぜひ自作コマンドラインツールを作ってみてください。 また、「こういう機能があるなら lgen を使ってもいいんだけどなあ」みたいな要望があれば issue を立ててもらうか、Twitterで 私にメンション を飛ばしてもらえると嬉しいです。 明日は、VP of Productの神宮司さんと、基盤グループのめもり〜さんです。 参考 最後に今回の記事・ツールの作成時に参照した情報を再掲しておきます。 https://github.com/budougumi0617/lgen 私が愛した怠惰・短気・傲慢 - BASE開発チームブログ The Chart Template Developer’s Guide - helm.sh text/template上で動く計算機を作る #golang - Qiita Go templates made easy | GoLand Blog Package template - The Go Programming Language Package filepath - The Go Programming Language Package os - The Go Programming Language Big Sky :: Golang で物理ファイルの操作に path/filepath でなく path を使うと爆発します。 今回は触れませんが、HTML生成用のテンプレートエンジンとしては html/template パッケージもあります。 ↩ Goでコードの自動生成というと、 gogenerate もありますが、今回は用途が違うので触れません。 ↩
アバター
この記事は BASE Advent Calendar 2019 の22日目の記事です。 devblog.thebase.in こんにちは、Data Strategyチームの id:tawamura です。BASEには今年の8月に入社し、今月で5ヶ月目になります。 DSチームでは、ネットショップ作成サービス「BASE」のデータを集計し、機械学習など様々な利活用を行なっています。主に挙げられるのはおすすめ商品のレコメンデーション、商品のカテゴリ・属性推定、不正利用の検知などです。 弊社では、DSチームが作成したこれらの学習モデルを、DS側で立てたAPIサーバを介して利用してもらう形を取り入れています。ALBを通したAPIプロキシサーバが、各推論サーバ(ECSインスタンスやLambda)にリクエストを投げるようにしています。 基本的にDSチームの立てたAPIのレスポンスは即時で返されています。しかし、今回私が作成した推論APIは、実行にかなりの時間がかかってしまうものでした。リクエストで受け取る単一のIDについての結果を返すために、推論で必要な素性を都度RDSから収集しているのが直接の原因です。しかもリクエスト内容によって素性となる対象データ量が異なるため、実行時間が数秒〜数十秒まで変動してしまいます。 幸いこのAPIは元々リアルタイム性が求められるものではなかったので、ひとまず実行時間を受け入れつつ、この重い処理をどうにかして扱えるように色々と試行錯誤した、という話を紹介できればと思います。 ちなみに、この話は本番環境への導入前の開発環境での話となります。 初期案. リクエストに対して計算し、そのままレスポンスを返す (CDNやVPC、セキュリティグループなどは省略しています) まず最初に、APIプロキシサーバを介してリクエストを投げそのまま計算結果レスポンスを返す、従来通りの構成を検討しました。他のDS APIも多くはこの形を採用していました(今はLambdaに移行しつつあります)。最初の段階では実行に時間がかかるものがあるとわかっていなかったため、この形で試しています。 推論サーバにはECSインスタンスを使用しており、aiohttpを使用してリクエストに対し推論結果を返すWebサーバを立てています。 この場合、実行時間が数十秒かかる際に、APIプロキシサーバから事前に設定されたタイムアウトエラーが返されます。これ自体は正しい挙動です。レスポンスが返ってくるまで数十秒も待つという仕様は、リクエストを投げるサービス側にとっても良くありません。 案1. 推論部分を分離、SNS→SQS→Lambdaで推論 そのため、このAPIが扱っている「リクエストを受け取る、レスポンスを返す」「リクエストについての素性データ抽出、推論」という機能を別々のAPIとして分離することにしました。 また、この頃チーム内でAPIサーバとしてECSではなくLambdaを使用するようにしていったこともあり、Lambdaでのリクエスト処理に移行することにしました。 まず、サービス側から叩く リクエストAPI としてのLambdaの挙動は、リクエストについての計算済み結果があれば即時返す(今回は保存場所にDynamoDBを使用)、無い場合は「計算開始」または「計算中」のステータスを即時返すというものに限定します。これにより、タイムアウトなくレスポンスを返すという部分についてはクリアできます。 そして、「計算開始」または「計算中」となった場合は、同時に計算イベントを投げ、別で計算させるようにします。今回この計算イベントの投げ先にSNSを指定しました。 SNSに投げられたイベントは、サブスクライブしているSQSを経て、メッセージとして「リクエストについての素性データ抽出、推論」を行う 計算API としてのLambdaに到達し計算されます。 ここでの計算結果がDynamoDBに格納され、次回以降リクエストAPIを叩かれた場合に、即時に計算結果が返却されることになります。さらに、計算が終了したことがわかるように、計算終了時に終了イベントをSNSに投げています。ここでは、DynamoDBで計算が完了したレコードに注視し、イベントを投げていますが、計算終了時に直接SNSにイベントを投げても同じかと思います。 計算APIのタイムアウトについてですが、Lambdaはデフォルトでタイムアウトの時間を最大値900秒に設定することができます。そこまで時間がかかることはほぼあり得ないと思われるので、今回のユースケースの場合、全てのリクエストについて理論上計算可能となります。今回はとりあえず120秒としました。 その間、SQSに溜められた計算はLambdaの処理を待つ形になります。 エラーログとして、10回以上処理が失敗(10回以上受信)したものはSQSのデッドレターキューに流すようにします。 設定として、計算APIの起動Lambda数は2、可視性タイムアウトはLambdaのタイムアウトに合わせて120秒にしました(可視性タイムアウトは、メッセージ受信時にSQSの該当メッセージを見えなくし、別のワーカーによって重複して呼ばれることを避けるためのものです)。 わかりやすさのため計算API Lambdaに「×2」と表記していますが、実際は他のECSなども複数サーバーが動いています。 多くの計算イベントが意図せずデッドレターキューへ😖 これでうまくいくと思っていたのですが、実際に数百程度の計算イベントを投げてみると半分くらいのイベントがデッドレターキューに入ってしまっていることがわかりました。このデッドレターキューに流れてくるものの想定は、計算APIでの処理が10回失敗してしまったものだと思っていたので、ここがあまりにも多いのはちょっとおかしいです。一部の計算イベントについて時間がかかることで、一部のイベントがLambdaを占有してしまうことは想像がつくのですが、それでもSQSで処理を待つことで最終的には全ての計算が行われることを期待していました。 調査の結果、計算APIのLambdaでログをとっていたものの、そこでは一切エラーログは出ていないことがわかりました。つまり、Lambdaで一度も処理されることなく、デッドレターキューに入っているものがほとんどだということです。 おそらく勘違いをしていたのですが、SQSから受け取って計算APIのLambdaでの処理が失敗される度に、受信回数が1回増えるのではなく、「SQSから受け取ったが処理できるLambdaがいないのでSQSに返却された場合も受信回数が1回増える」なのではないか、ということがわかりました。 似たような事例が別の方からも挙げられていました。 medium.com 対策として考えられるのは、Lambdaの同時実行数を増やす、可視性タイムアウトを伸ばしてみる、などかと思います。 ただ、大きな前提問題としてこの計算APIはRDSへの接続を行なって動作しています。Lambdaの同時実行数を増やす場合、RDSへの接続数も比例して増えてしまいます。この結果、RDBMSの最大同時接続数を超えてしまう場合、他のDS APIのパフォーマンスに影響を及ぼすため、できれば避けたいものでした。可視性タイムアウトも根本的な解決にはなりそうにないとのことで、検討を見送りました。 実際に、弊社でサポートしていただいているAWSの方に意見を伺った際に、Lambdaの同時実行数が少ない場合に同様の現象が起きるということが確認されたようです。 案2. 推論部分を分離、SNS→SQS→ECS workerで推論🎉 AWSの方との話をした際に、別途こちらで考えてみた構成案を話させていただいており、現状はその案をとっていただくと良さそう、という回答をいただきました。 SQS→Lambdaに繋ぐのではなく、SQSのメッセージをECSインスタンスが取りに行って同期的に処理するというものです。 案1の問題は、LambdaがSQSにメッセージを取りに行く回数が実行時間に対して多く、処理する前にメッセージの受信回数が上限を超え、デッドレターキューに流れてしまうという点でした。では、メッセージの受信動作をこちらで制御し、計算中はメッセージを新たに取りに行かないようにしよう、というのがこの案のモチベーションです。 ECSのインスタンスとして2つのworkerを立てて、それぞれが同じSQSからメッセージを取って、順次計算し結果を格納する、格納し終わったらまたメッセージがあるか見に行く、という動作をし続けます。こちらも各メッセージを処理する際のタイムアウトの時間は十分長くとるようにします。 この方法により、時間は多少かかりつつも問題なく全リクエストをさばけるようになりました。 ・・というか、この案はどうやらLambdaがSQSをソースで扱う前の、従来のSQSのポーリングの仕組みになるようです。 振り返ってみると、APIがそれぞれRDSへ接続をしており、実行時間もかかる、というのがそもそもLambdaとの相性としてあまりよくなかったかなと思います。 まとめ 今回は時間のかかる推論APIをどうにかして処理させるために試行錯誤した話を紹介しました。 もちろん、推論の速度をあげるべきと言う指摘はもっともです。ただ、今回の場合は即時に結果が返る必要のない条件下での推論でもあったため、速度の追求よりも導入の実現性にフォーカスして対応を進めておりました。 AWS上のアーキテクチャを活用して上記のようなシステムを構築しましたが、自分自身AWSに触れるのはこの会社にきてからが初めてでした。ですが、チームのメンバーやテックリードの方々の手厚いサポートにより、ここまで理解と実装を進めることができました。 ちなみに私が作成したAPIはとある「不正検知API」です。 BASEではユーザのみなさまが安心してサービスをお使いいただける環境を提供するため、多方面からの協力も得ながら不正利用の検知と対応に尽力しております。 これからもネットショップ作成サービス「BASE」をどうぞよろしくお願いいたします。 明日はProduct Managementの山田さんとBASE BANKの清水さんです。お楽しみに。
アバター
この記事はBASE Advent Calendar 2019 22日目の記事です。 devblog.thebase.in こんにちは。最近はCorporate Engineeringをやっている山根 ( @fumikony )です。すこし前まで、即時に資金調達ができる金融サービス「 YELL BANK(エールバンク) 」のインフラまわりに関わっていました。 今回は「YELL BANK」のインフラにおけるTerraform運用について紹介します。 目次 目次 概要 実行する場所について レビューのやり方について プルリクエスト作成からterraform applyまでのワークフロー 詳細 tfnotifyについて AWSアカウントとTerraformディレクトリ構成について CodeBuildについて 実行例 今後の発展など おわりに 参考リンク 概要 「YELL BANK」のインフラはAWS上に構築していて、Terraformによるコード管理を行っています。Terraformのコード(tfファイル)はGitHubで管理し、変更を加えたいときはプルリクエストを作成します。普通ですね。 さて、Terraformをプルリクエストベースで運用する場合、いくつか考えることが出てきます。具体的には 実行する場所 レビューのやりかた プルリクエスト作成から terraform apply までのワークフロー です。 実行する場所について 「YELL BANK」では、 terraform plan をAWS CodeBuild上で実行し、 terraform apply はTerraform実行用のEC2インスタンス上で実行しています。 「YELL BANK」の開発では主にCircleCIを使っているのでTerraformもCircleCI上で実行することも考えました。しかし、CircleCIにあまり強いIAMの権限を付けることがためらわれたので、CodeBuildを選択しました。 レビューのやり方について tfファイルの変更をプルリクエストにすると、その差分としては当然、tfファイルのみが出てきます。しかしながらレビューの際には terraform plan の結果を確認したいものです。 はじめのころはプルリクエストに terraform plan の結果を(手で)貼ってレビューしたりしていたのですが、ここにCodeBuildと tfnotify を導入しました。 terraform plan の結果がプルリクエストのコメントとして(自動的に)返ってくるようになり、レビューがやりやすくなりました。 プルリクエスト作成からterraform applyまでのワークフロー 現在のところ、「YELL BANK」のTerraform変更のフローは以下のようなものになっています。 ブランチを切ってtfを編集 git commit , git push プルリクエストを作成 terraform plan の結果がプルリクエストのコメントとして返ってくる レビュー マージ Terraform実行サーバ上で terraform apply 詳細 以下、上でのべた構成の各部分について、詳しく説明していきます。 tfnotifyについて tfnotify はメルカリさんがOSSとして公開しているツールで、 teffaform plan や terraform apply の出力をGitHubやSlackなどに通知するためのものです。CIの中で使う想定で作られています。 以下に設定ファイルの例を示します。これはCodeBuildからtfnotifyを使用して、 terraform plan の結果をGitHubに通知するための設定です。 .tfnotify.yaml --- ci : codebuild notifier : github : token : $GITHUB_TOKEN repository : owner : "orgname" name : "reponame" terraform : plan : template : | {{ .Title }} for Production <sup>[CI link]( {{ .Link }} )</sup> {{ .Message }} {{ if .Result }} <pre><code> {{ .Result }} </pre></code> {{ end }} <details><summary>Details (Click me)</summary> <pre><code> {{ .Body }} </pre></code> </details> これを terraform コマンドを実行するディレクトリに置いておくことで、 template に記載した内容のコメントがプルリクエストに通知されます。 {{ .Title }} などはtfnotifyが埋めてくれます。詳細はtfnotifyのREADMEをご覧ください。 AWSアカウントとTerraformディレクトリ構成について 「YELL BANK」のインフラには本番(prd), ステージング(stg), 開発(dev)の環境ごとにAWSアカウントがあります。このうちprdとstgをTerraform管理下に置いています。 Terraformのディレクトリ構成としては、 - 環境ごとに別のディレクトリを作成 - リソースの種類ごとにtfファイルを作成 という方針をとっています。 具体的には以下のようなディレクトリ構成になっています。 . ├── .gitignore ├── README.md ├── aws-basebank-prd │ └── bb-prd │ ├── .tfnotify.yaml │ ├── README.md │ ├── cloudfront.tf ... │ └── vpc.tf ├── aws-basebank-stg │ └── bb-stg │ ├── .tfnotify.yaml │ ├── README.md │ ├── cloudfront.tf ... │ └── vpc.tf ├── bin │ └── dir_is_changed └── buildspec.yml buildspec.yml がCodeBuildの設定ファイル、 .tfnotify.yaml がtfnotifyの設定ファイルです。 CodeBuildについて AWS CodeBuild は、AWSのCIです。 CodeBuildを実行のトリガとしてはGitHubのプルリクエストの作成と更新を使います。 また、prd,stgそれぞれのAWSアカウントにおいてCodeBuildを設定しています。 これは実際に使用している buildspec.yml です。 buildspec.yml phases : install : runtime-versions : golang : 1.12 commands : - git clone https://github.com/tfutils/tfenv.git ~/.tfenv - ln -s ~/.tfenv/bin/* /usr/local/bin - tfenv install 0.12.6 - wget https://github.com/mercari/tfnotify/releases/download/v0.3.1/tfnotify_v0.3.1_linux_amd64.tar.gz - tar xzf tfnotify_v0.3.1_linux_amd64.tar.gz - cp tfnotify_v0.3.1_linux_amd64/tfnotify /usr/local/bin/ - cp bin/dir_is_changed /usr/local/bin/ build : commands : # ref. https://blog.hatappi.me/entry/2018/10/08/232625 - | if dir_is_changed $TERRAFORM_DIR; then cd $TERRAFORM_DIR terraform init -no-color terraform plan -no-color | tfnotify plan fi ここで行っているちょっとした工夫として、 dir_is_changed と $TERRAFORM_DIR があります。 $TERRAFORM_DIR はCodeBuildで設定している環境変数で、 terraform plan を実行するディレクトリを指定します。 また dir_is_changed は以下のようなシェルスクリプトで、引数にあたえたディレクトリ以下に変更があったかどうかを判定します。 dir_is_changed #!/bin/bash # ref. https://blog.hatappi.me/entry/2018/10/08/232625 DIFF_FILES = ( `git diff origin/master --name-only --relative= ${1} ` ) if [ ${#DIFF_FILES[ @ ]} -eq 0 ]; then exit 1 else exit 0 fi これによって、prdかstg、変更があった方だけでplanを実行するという振る舞いを実現しています。CodeBuild自体はプルリクエストの作成・更新をトリガとして常に両方の環境で動くのですが、変更が無いほうでは何もせずに終了します。 ちなみに、この方法では一つのプルリクエストでprdとstgの両方を変更するとうまく動かないため、別々のプルリクエストにしておく必要があります。 せっかくなのでCodeBuildの設定画面のスクリーンショットも貼っておきます。よかったら参考にしてください。 実行例 下図は、CodeBuild上で terraform plan が実行され、その出力が tfnotify 経由でプルリクエストコメントとして通知されている様子です。 Details (Click me) というところをクリックすると、びろ〜んと伸びて terraform plan の出力が出てきます。 今後の発展など 今のところはCodeBuildで実行するのは terraform plan までにとどめており、 terraform apply はサーバ上で手動実行しています。apply までやってしまいたい気持ちはありましたが、 GitHubでプルリクエストをマージしただけで本番インフラが変更できてしまうのはちょっと怖い 手動でAWSリソースを作ったあとで terraform import して辻褄をあわせるようなケースが、たまに有る というのがあり、いったんplanまでにしました。 上については、applyの前に人間による承認をはさみたいのでCodePiplelineにするのがよいだろうか?🤔 と思っていますが未検証です。 下については、そういうことをなるべくしないというのと、それようの場所をそれはそれで用意した上で、普段はCIに任せるのが良いかなあと思っています。 おわりに 若干とりとめがなくなりましたが、「YELL BANK」のインフラにおけるTerraform運用について紹介しました。 tfnotifyはかなり便利で、オススメできるツールだと思います。 最後に、同じようなジャンルのツールを紹介しておきます。まだ詳しく調べていないのですが、面白そうなので機会を見てさわっておきたいところです。CI+tfnotifyの構成を検討する場合は、これらも検討に入れておくといいと思います。 Atlantis これはGitHubのプルリクエストコメントにコマンドを書くと裏でterraformを実行してくれる感じのツールです。 Terraform Cloud Terraform開発元のHashiCorp自身が運営しているSaaSです。 さて明日は、BASE BANKの清水さんとProduct Managementの山田さんです。お楽しみに。 参考リンク https://tech.mercari.com/entry/2018/04/09/110000 https://medium.com/mixi-developers/terraform-on-aws-codebuild-44dda951fead https://github.com/mercari/tfnotify https://blog.hatappi.me/entry/2018/10/08/232625
アバター
やあ id:chris-x86-64 a.k.a クリスです。BASE株式会社100%子会社のPAY株式会社でセキュリティエンジニアをやっています。 新卒でここで勤めはじめて3年半が経ちました。わたしは大学在学中に畑を開墾し、大学を卒業する直前くらいにはこのまま農家になるんじゃないかと噂されていたものです。 卒業・就職した後も当該の畑を続け、社内でも農家転身やすでに本業が農家なんじゃないかなどと噂されていましたが、なんと後輩たちが卒業しさらに就職に伴い東京へ吸い込まれすっかりスーツとパソコンの人にトランスフォームするなど、27歳にして過疎化と農業の高齢化に直面し、畑は丸4年でサービス終了となってしまいました。それが今年3月のことです。 さて、畑を失ってしまったわたしですが、ほとんど同時に次の生きがいを見つけてしまいました。 それは、野生化――もとい、キャンプ。 以下は、セキュリティエンジニアというコッテコテのコンピュータギイクなわたしが、そのまるで対極に位置するアクティビティである野生化とのへんてこりんな関係について触れ、テクノロジーの世界に生きる者が母なる自然に回帰して考えたことなどを綴ろうと思います。 どうしてキャンプするんですか ぜんぜんわからない。俺たちは雰囲気でキャンプをやっている。 実はキャンプについて考え始めたのは今よりもだいぶ前、2013年のことでした。 いつものように漫然とTwitterを眺めていたら、 Thomas Backlund さんという起業家の特集記事が目に留まりました。なんでも彼は起業して自らコードを書くエンジニアであると同時に、アパートを引き払って自らの意志でホームレスとなりスウェーデンの森の中で暮らすという、ぶっ飛んだギーク特有の強烈な二面性 1 を持ち、それに惚れ込んだわたしもエンジニアとして、またホモ・サピエンスとして、強く生き抜いていきたい、そう感じさせてくれました。目標達成のために浮世のすべてを捨てて野生に還る生き様を、わたしも体験すべきだと考え始めました。 そうは言いましても当時わたしは学生。すぐさまキャンプ道具を揃えてどこでもないどこかへ脱出したい気持ちだけ高ぶらせるも、大人の機動力(要するに資金力)は持ち合わせがなく、大学が森だの原野だの評されていることをいいことに、ここで暮らしていれば実質ビッグフットだと自分をごまかし続けました。 結局決心がついたのは、あの記事を読んでから5年後。言わずもがな例のアニメが後押しとなりました。キャンプへの思いを長きにわたり燻ぶらせていたところに、あの気象レーダーみたいなでっかいお団子ヘアのキャラクターがAmazon Prime Videoに現れたのです。 今やるしかない。 脳天に雷が落ちて30分もしないうちに、わたしのAmazon.co.jpアカウントには7件ほどの注文履歴が並び、翌日にはわたしの社内Slackのアイコンが山梨銘菓になりました。これが2018年7月のこと。翌月ソロキャンデビューを果たしました。 (本当のところ)どうしてキャンプするんですか 実際にキャンプしてみると、健全な自分が取り戻せるように感じられます。 五感を取り戻す はじめに聴覚――どんなに静かなオフィスでも人間がいる限り音がします。人間の音がやたらめったら苦手であるわたしは、チャンスのある限り余計な音がしない空間に隠遁しないことには生活がままなりません。実際のところそれがわたしが今もつくば市に住み続けている理由の主たるものですが、それでも小川のせせらぎや木々のざわめきにはちょっと遠いです。そこで、川辺や森の中などのキャンプサイトにテントを張って大の字になってみると……川、木々、大小の鳥、シカ、ノウサギ、クマ(これはちょっと困る)など、我々と共に暮らしている友の声がします。耳をすませる自由がそこにはあります。わたしは常にこの「耳をすませる自由」を求めています。 次に視覚――職場も通勤時間も自由時間もブルーライトまみれ。人類を未来へ導く存在として崇められてきた「光」ですら牙をむく21世紀ですが、森の木の葉に反射された太陽光だけは今も昔も変わりません。太古から人間を包み込んできた光に回帰すると、視覚が解放されます。山の上のほうのキャンプサイトなら、数十km、数百km先の景色が見えることなどもあり、上方向なら250万光年離れたアンドロメダ星雲も見えます。360°前後左右上下ヒトヒトヒトヒトヒトアンドヒトな都心とはわけが違います。 次に嗅覚――日常的に人間や機械類の排ガスを浴びているので、たまには光合成する木々の真横で寝泊まりして呼吸器の浄化を図ります。キャンプ場でする匂いといえば、木の葉、小川、焚き火、露のついた石、肉……原始時代にもきっと同じ匂いがしていたと思います。鉄と油と汗だけが人類文明ではないことを噛みしめると、なんだか安心します。 次に触覚――風が気持ちいい(または寒い)、焚き火が暖かい、温泉でとろけそうになる、新雪を踏んだときの「くわっ」という沈み込み……これらはみんな肌の感覚です。 そして満を持して、味覚――これは自由を取り戻しているというより、 外ごはん効果で3倍おいしい(例の山梨のでかい団子談)飯が食えるという、幸せ要素になります。焚き火で焼いた肉、ちょっと焦げた米、やっぱり焦げたツナホットサンド、お隣キャンパーさんのカレー、焼いたマシュマロ、約束された勝利のコーヒー。もちろんウォッカも欠かせません。焚き火にはステンレスのフラスコに入れたウォッカが合います。 人生を取り戻す わたしはエンジニアでありながら電源のひとつ存在しない空間でも息をすることができる、これだけで強く生きている実感がわいてきます。酸素にも並ぶ生命線であるインターネットコネクティビティの有無によらず生存できる、両生類みたいなものを目指しています。 キーボードをカタカタやって生計を立てているエンジニアにこそ、電源だのWi-Fiだのがきれいさっぱり存在しない空間を生き抜いて、アナログでローテクな生活に慣れ親しむことに大きな意義があると考えています。我々の生活は高度な分業化のおかげでいろいろな道のプロがいて、ほかのプロと専門分野を分け合って全体で巨大文明を作ったうえに成り立っていますが、本当は一人ひとりが自分だけの生命を紡げるはずなのです。 以前からわたしは自分の文明を持ちたいと考えており、そのためには自分ひとりでできることを可能な限り増やす必要がありますが、その基礎がサバイバルスキルというわけです。このご時世なのでそこら辺の公園は軒並み焚き火禁止ですし、野生に還ろうにもほぼすべての国土には所有者がいて、勝手に住み着いて狩猟採集生活を営むと罰されます。そんな時代において人類が初めてエネルギーを手にした様子をそっくりそのまま再現できる場所がキャンプで、わたしはそれに救いを見出しています。 気持ち キャンプの話で社員と雑談していたら飛び出してきた疑問がこちら: 「キャンプ中は何を考えるんですか?」 これにはわたしなりに答えがあります。キャンプ中は考え事が無いことが健康の指標だと考えています。 調子がいいときは、憂いが無いので考え事が無く、ただ焚き火を眺め、ごっつい肉を食らい、暗くなって気温が下がるのを肌で感じるのみです。逆に調子が悪いときは、焚き火を見つめながら頭の中をいろんな心配事が渦巻く傾向にあります。 こんなふうにキャンプ中に考え事があるかないかは、自己の精神の健康状態の理解の助けになります。 ところで精神状態にかかわらず、河原に座って川の音に集中すると、ただのひとつも考え事をしない、最も落ち着いた自分を体験することができるように思えます。これがわたしにとってのアウトドア――精神統一の場です。 余談ですが、キャンプを始めたての頃、川の音に全身全霊で集中していたとき、涙したことがあります。精神統一を試みて逆にありとあらゆる感情に圧倒される現象は、初めてヨガを体験した人にも起こることと話にきいています。 キャンプ場の決め方 さて、これだけの目的を持ってキャンプに臨むには、相当な下調べが必要になることがあります。 ここは21世紀らしく、キャンプ場検索・予約サイトを使っています。最もよく使うのは「なっぷ」です。 候補の絞り方はだいたい次のとおり: より空いているところ 人間の生活音から可能な限り距離を取ることが目的 予約状況をなっぷで確認して、空きが多いことが確認できたら有力候補 オフシーズン、冬季可 雪上キャンプは人が少ないうえ、雪に騒音が吸収されるので非常に静かで心地よい 周辺環境や立地 川があると天然のホワイトノイズがあるのでリラックス効果大 標高が高いところは空気が澄んでいて気温が低めで、リフレッシュ効果大、星も見えやすい 猪苗代湖モビレージ 2019年1月 これまでに行って良かったキャンプ場には、猪苗代湖モビレージ(福島県)、小国白い森オートキャンプ場(山形県)、洞爺水辺の里財田キャンプ場(北海道)を挙げます。とくに猪苗代湖モビレージは通年営業で積雪期も営業しているので、雪上キャンプが楽しめます。 なお、そういったキャンプ場は往々にして辺鄙なところにあって、マイカー無しにたどり着くことは極めて困難です。やっぱり20世紀21世紀の人類文明や資本主義経済の囚人であることに変わりありませんでした。これからも人々の資本主義経済を支え守ってゆきます。どうぞPAY.JPをよろしくお願いいたします。ありがとうございました。 次回予告 明日12/22は、Data Strategyチームの粟村さんと、CSE Groupの山根さんです!お楽しみに! ここでは「二面性」は褒め言葉として使いました。 ↩
アバター
この記事はBASE Advent Calendar 2019の21日目の記事です。 devblog.thebase.in こんにちは。Owners Growthチーム所属の大木( @y_abcinema )です。 まず「Owners Growth」という言葉。聞き慣れない方がほとんどではないのでしょうか。 これはBASE内のチームの名称で、『「Owners(オーナーズ)」と呼んでいる「BASE」のショップオーナーさんの「Growth(成長)」を支援したい』という思いで、最適な支援が届くよう戦略を立てて、各施策を実行しています。 他社では見かけないチーム名で、私はとっても気に入っています! Owners Growthについては、16日目の記事「 80万ショップの成長を支援するOwners Growthチームの取組とは? 」もぜひご覧ください。 今日はその中でも、SNSで実施している取組についてフォーカスしてお話しいたします。 オーナーズに想いが届く「場」ってどこだろう? Owners Growthチームは2018年1月に誕生しました。 私がチームにジョインするに当たり気が付いたのは「ショップオーナー(以下「オーナーズ」)にオンライン上で成長支援できる場の数は、思っているほど多くはない」ということです。 こちらから届けられる成長支援のアドバイス提供場所は、現在は主に下記の3つになります。 ショップオーナーさんが使う「BASE」の管理画面に表示される「お知らせ」 メールマガジン オウンドメディア BASE U この3箇所は双方向のコミュニケーションをする場ではないため、どうしてもショップオーナーさんの反応が見えづらくなってしまいます。 BASEの公式SNSを活用すれば「アドバイス提供場所+ コミュニケーションを図る場 」としてコミュニティの構築と活性化にも繋がる思い、先の3箇所に加えSNSの運用に力を入れ始めました。 これなら一方通行のラブレターではなく、オーナーズと相思相愛になれるかも! BASEにおける3SNSのセグメント 現在BASEでは Twitter , Instagram , Facebook の3つのSNSを公式アカウントとして運用しています。 大枠のターゲットは「BASE」というプロダクトと同じく「ネットショップを運営している方」「プラットフォームを通じて素敵なショップ/商品に出会いたい方」ですが、その中でも各SNSごとにターゲットが少しずつ異なります。 Twitterは親近感と速さを重視 オーナーズの中には、 ショップを一人で運営されている方も多い です。 一人で運営をしていると「困った時は誰に相談すれば良いんだろう」「成長を高め合える仲間が欲しい」という壁もあるのではないでしょうか。 Twitterでは「BASEがオーナーズの成長を高め合える“仲間”として寄り添う」というミッションを掲げ、いち早く新機能のリリース情報や、ショップ運営に有益な記事を届ける場として活用しています。 KPIとして大事にしているのは「いいね」や「RT数」よりも「リンクのクリック数」です。 Twitterのタイムライン上では「いいね」を押した場合、他のユーザーに「いいね」を押したことが伝わるため、押しにくい……というオーナーズも多いと思います。 「本当にオーナーズが求めている情報は何か」を探るため、「いいね」の数だけに惑わされずリンクのクリック数の向上をKPIに掲げています。 Instagramはオリジナル写真を使った連載を開始 現在、ショッピングアプリ「BASE」では 特集コンテンツが自動で生成される仕組み を導入しています。これによってアプリのユーザーさんごとにおすすめのショップや商品をレコメンドできるようになりました。 以前はフィード投稿にておすすめのショップ/アイテムを投稿していましたが、特集コンテンツの自動化にともない、「オーナーズの成長を促せるような投稿」の連載も開始しました。 ハッシュタグ「 #BASEアドバイス 」を付けた投稿では、商品写真の撮り方や画像の加工方法、Instagramで有効的なアプリケーションなどをご紹介しています。 連載開始後、「#BASEアドバイス」と称した投稿のインプレッションを見てみると、他の投稿の4倍近くのコレクション数が。 Twitter同様「いいね」の数よりも、コレクション数を参考に投稿を作っています。 また、オーナーズのSNSを見たところ、3つのSNSのなかで最も利用ユーザーが多いのがInstagramです。 他のSNSは運用しておらず、Instagramのみ運用しているオーナーズも多いため、BASEとの架け橋になれるよう心がけています! Facebookは「日常に寄り添う」が合言葉 先にご紹介した2つのSNSと少し変わり、Facebookではプロダクトが素敵なショップ/アイテムやイベント情報を中心にご紹介しています。 理由としてはFacebookページを持っているオーナーズでも、「ショップのアカウントからBASEのページを閲覧する」のではなく「個人アカウントからBASEのページを閲覧する」傾向があるため、ショップの成長支援アドバイスを提供しつつも、日常を共有する/見る場の妨げにならないようにするためです。 またInstagramのように「検索ツール」や「ショッピング目的」ではなく、近況報告やビジネスの場として使っている人もいるため、日常に溶け込むような投稿を目指しています。 ちなみにショップ/アイテム紹介の場合はストーリー性があり、読んだときにパッと制作の裏側が思い描くことができ、思わずシェアしたくなる投稿が人気の傾向です。 最近では OIOI BASE MARKET で販売中の商品の紹介や、期間限定のポップアップショップなど休日に足を運びたくなるイベント情報もお届けしています。 終わりに なかなか表に出てくることがないOwners Growthチームの取組や熱い想い、伝わりましたでしょうか!今回私からはSNSを通じた、ショップへの成長支援を中心にご紹介しました。 BASEの行動指針のひとつである「Move Fast」を基に、チームでは柔軟な姿勢で、変動するネットショップの傾向やトレンドに対応できるよう励んでいます。 日々感じるのは、熱血で和気あいあいとしながらも「みんな心からオーナーズが好きなんだなぁ」ということ。その一言に尽きます。 現在は3つのSNSを中心に運用していますが、「オーナーズにとってより最適な成長支援になる場」を目指し、オンライン/オフラインを問わず、どんどん新しい場所(面)も増やしていく所存です。 明日は、Data Strategyの粟村さんとCorporate Engineeringをやっている山根さんです。楽しみー!
アバター
この記事はBASE Advent Calendar 2019の20日目の記事です。 devblog.thebase.in こんにちは、ふーです! みなさん、絵文字つかっていますか? 絵文字は、チームのコミュニケーションを円滑にしてくれる素敵な表現ですよね。Slack等で普段使われている方も多いのではないでしょうか。 ちなみに、最近GitHubで絵文字を簡単に挿入できるChrome Extensionをつくりました。 BASEでは、Pull Requestを活用して、コードレビューが行われているので、私自身はそういった場面で活用しています!活用してくれているメンバーもいるようでとても嬉しく思っています。ご興味のある方は下記リンクからインストールできるので、もしよければ、活用してみてください! Emoji Palette さて、そんな素敵なパワーのある絵文字ですが、今回は「絵文字」を中心にBASEのカルチャーを感じてみたいと思います! BASEの絵文字カルチャー BASEでは、業務におけるコミュニケーションのプラットフォームとしてSlackを活用しています。 Speak Openly という行動指針にもある通り、様々な会話が活発に行われています。そんな普段の会話で活用されている絵文字から、わたしの独断と偏見でBASEのカルチャーが伝わるような絵文字たちを紹介します! 行動指針系絵文字 BASEには、以下のような行動指針があります。 「Be Hopeful」 楽観的でいること。期待した未来は実現すると信じて、勇気ある選択をしよう。 「Move Fast」 速く動くこと。多くの挑戦から多くを学ぶために、まずはやってみよう。 「Speak Openly」 率直に話すこと。より良い結論を得るために、その場で意思を伝えよう。 これらが絵文字となって、普段の会話や、Slackの絵文字としてアクションされています。 みんな、この指針に共感と愛情を持ってお仕事をしているんだなあ。というのがこの絵文字文化から伝わります。 ちなみに、こんな感じの派生系もあります! また哲学である Stay Geek から生まれた絵文字もあります! さくわい これは、気軽にさくっとご飯いきましょー!というさくわい文化を表す絵文字たちです!#sakusakuwaiwai というさくっと食事に行きたいときに投稿するChannelから生まれた絵文字です。 BASEでは、行きたいひとたちが行きたいときにさくっと食事に行く場面をよく見かけます! 「やりたい!」という気持ちを大切にしてみんなでやる!そんな文化も感じますね。 ダサいぞ これは、プロダクトにおけるイケてないところをシェアする #ダサいぞ というChannelから生まれた絵文字です。このダサいぞ絵文字、アクションするとダサいぞChannelにシェアされる設定になっているようなので、使いどころは少し注意が必要ですね! さいごに Slack絵文字を中心として、BASEのカルチャーを感じてみました! みんなが行動指針に共感と愛情をもっていることであったり、やりたい!という気持ちを大切にする姿勢であったり、プロダクトを大切にしているカルチャーを私は感じました。 BASEのSlackには、 #twitter というまさにTwitterのように雑談をするChannelなどもあって、「インターネットっぽさ」も感じるなあ。とも思っています。 明日はOwners Growthチームの大木さんと、PAYのセキュリティエンジニアのクリスさんです。お楽しみに!
アバター
この記事はBASE Advent Calendar 2019の20日目の記事です。 devblog.thebase.in DataStrategyの岡が担当します。 Prophet is 何? ProphetはFacebook社製の時系列予測ライブラリです。RとPythonから利用でき下記gitで公開されています。 https://github.com/facebook/prophet 分析者仲間の間で「時系列予測ならまずこれを使っとけ」と言われるくらい高精度らしいのですが、私自身がイマイチ理論を把握してない & ググってもさらっとした解説の日本語ドキュメントしかない印象です。 Prophetの元となる論文は下記にて公開されています。 https://peerj.com/preprints/3190.pdf 冒頭だけ読むと、時系列分析の知見のない人でもドメイン知識を組み込みながら予測ができるようなツールを目指して開発されたようです。論文タイトルが「Forecasting at Scale」となっていて「Scale」というのはここでは「誰でも使える、スケールしやすい予測ツール」みたいな意味で使われています。 論文に書かれているProphetのモデル式をしっかり理解できるように丁寧になぞっていこう、というのがこの記事の趣旨です。 モデル式の概要 時系列データは、トレンド + 季節要因 + ノイズ などの複数の要素から成り立っていて、これらの要素に分解することで理論値の予測ができる、という考え方があります。 Prophetでは、時系列は下記のような構成要素をもつと捉えています。 さらに、時系列はこれらの要素の和と捉え下記のようなモデル式を組み立てています。 このモデル式を理解するため、誤差項以外の3要素を一つずつ見ていきます。 1. : トレンド関数 まず ですが ロジスティック非線形トレンド 線形トレンド の2種類があります。1のほうから読み解いてみます。 1-1. ロジスティック非線形トレンド ごく単純化すると下記のように表されます。 これはロジスティック曲線をベースに作られているそうなので比べてみます。 Prophetのトレンド関数(1)式と比べると、 となり の中身がシンプルになっています。 この関数の動きを把握するため、 を動かすとき がどのようになるか図示してみます。 のとき のとき となり出力 の下限と上限が決まっています。この基本となる式(2)に一つずつ の3要素を継ぎ足してトレンド関数の形に近づけていきます。 生物の個体数やプログラムのバグ発見数など、初めは少ない → 途中は多くなる → その後また少なくなる という流れを持つ現象はロジスティック曲線で説明されることがあります。 生物の個体数などには何かしら上限数があると考えられ、トレンド関数では をつけることでこの上限を設定しています。 トレンド関数にならって、ロジスティック関数(式(2))の分子を に変えてみます。 この の値を1, 2, 3,...と変化させると先ほどの曲線は下図のように変化します。 のmax値が の値に等しくなり、これで上限を設定できるようになりました。 続いて を加えてみましょう。 を固定した状態で、 の値を-1, 0.5, 1,...と変化させると曲線は下図のように変化します。 が大きいほど曲線が急になり、小さいほど緩やかになるのが見て取れます。 の時はもはや減少関数となり、 は「傾き」のようなものだと解釈できます。 生物の個体数でいうなら、成長スピードの早い群はより早く上限に達し、成長の遅い群は上限に達するのも遅い、といった知見をパラメタ で表現できます。 続いて、 を追加していきます。 これで冒頭のトレンド関数(式(1))と同様の式になりました( と読み替えてみてください)。これも を変化させながら曲線の動きを見てみましょう。 は の値をダイレクトに減算する式になっているので単に曲線が左右に動くだけですね。言い換えると同じ でも の値を上下させる働きを持っています。線形回帰でいうところの切片のようなものとイメージして下さい。 以上で下記の式の説明は終わりです。 ただ、これはあくまでトレンド関数を単純化した式です。 論文ではさらに、 も も一定ではなく時間によって変化するものと捉え、 と展開しています。 は各 時点で異なる上限を設定できるようになった、というだけの話なので説明はこれくらいに留め、 のほうをじっくり見ていきます。 は転換期のようなものを迎えた場合かなり異なったレートに変化するはず、というニュアンスを数式で表現したいとします。仮にそのような転換点がS個あったとして、そのときの 時点を と表します。また、その における成長率を調節する変更率として、 を定義します。 に対応してS個分あるので というベクトル(慣例にならってベクトルは太字で表記)も定義しておきます。 ある時間 における成長率は基本レート と 時点までに出現した変更率 の総和として下記の式で表されます。 論文ではさらに の部分を扱いやすいベクトルで表記するため、 というS個分の0または1の要素から成るベクトルを用意し、その各要素を と定義しています。例えば全部で10時点の があって、そのうち3回の変更点 が3,5,7番目に起きたとすると の内訳は下記のようになります。 結局 は各変更点 を迎えるごとにひとつずつ変更点のフラグが立つだけのベクトルと言えます。これと先ほど定義した変更率 のベクトル、 を組み合わせると式(3)は、 と変形でき、「一定ではなく時間によって変化する成長率」を表現することができました。 ここまでの式をトレンド関数に反映してみましょう。 これでt軸の転換点ごとに曲線の傾きが ぶん変化していれば良いわけですが、実はこのままだと曲線の変化がなだらかになりません。 この成長率の変化によってどのような曲線が描かれるか可視化してみましょう。 import numpy as np from matplotlib import pyplot as plt T = np.linspace(- 6 , 6 , 100 ) S = np.array([- 3 , 1 , 3 ]) # -3, 1, 3の時点で成長率に変化が起きる delta = np.array([ 0.1 , 0.3 , - 0.6 ]) # 各S時点での成長率の変更率 def logistic_trend (T, S, delta, k= 1 , C= 1 , m= 0 ): a = np.vstack([np.where(S < t, 1 , 0 ) for t in T]) y = C / ( 1 + np.exp(-(k + (a * delta).sum(axis= 1 )) * (T - m))) return y out = logistic_trend(T, S, delta, k= 0.1 , m= 0 ) plt.plot(T, out) plt.xlabel( "t" , fontsize= 16 ) plt.ylabel( "y" , fontsize= 16 ) # plot change point ymax = out.max() ymin = out.min() plt.vlines( S, ymin=ymin, ymax=ymax, linestyle= 'dashed' , color= 'gray' , label= 'change point' ) plt.legend() plt.show() 変化点ごとに曲線が切れてしまってます。 さきほど は「傾き」のようなもので、 は「切片」のようなものだと説明しました。 のようなシンプルな一次関数と同じように考えて欲しいのですが、傾きaは  のようにxの領域によって変化し切片bは一定で0として可視化すると、さきほどと同じように途切れた直線になってしまいます。 T1 = np.linspace(- 6 , - 2 , 30 ) T2 = np.linspace(- 2 , 6 , 70 ) def sample_linear (T, a= 1 ): y = a * T return y out1 = sample_linear(T1, a= 1 ) out2 = sample_linear(T2, a=- 1 ) plt.plot( np.hstack([T1, T2]), np.hstack([out1, out2]), ) plt.xlabel( "t" , fontsize= 16 ) plt.ylabel( "y" , fontsize= 16 ) plt.show() この場合は切片bを調節することで連続した直線が得られますが、話をトレンド関数に戻して考えるとオフセット項mを調整すれば連続した曲線が得られそうです。 転換点S個だけ調節する値が必要なので と同じく というベクトルを用意し、その内訳を下記のように定義します。 これを用いて式(4)のオフセット項mを調整するとトレンド関数は最終的に下記のようになります。 この修正を先ほどのコードに加えると下記のようにグラフ化できます。 T = np.linspace(- 6 , 6 , 100 ) S = np.array([- 3 , 1 , 3 ]) delta = np.array([ 0.1 , 0.3 , - 0.6 ]) def logistic_trend (T, S, delta, k= 1 , C= 1 , m= 0 ): a = np.vstack([np.where(S < t, 1 , 0 ) for t in T]) gamma = np.zeros(S.shape) for j in range ( 0 , gamma.shape[ 0 ]): gamma[j] = (S[j] - m - gamma[:j].sum()) * ( 1 - ((k + delta[:j].sum()) / (k + delta[:j + 1 ].sum()))) y = C / ( 1 + np.exp(-(k + (a * delta).sum(axis= 1 )) * (T - (m + (a * gamma).sum(axis= 1 ))))) return y out = logistic_trend(T, S, delta, k= 0.1 , m= 0 ) plt.plot(T, out) plt.xlabel( "t" , fontsize= 16 ) plt.ylabel( "y" , fontsize= 16 ) # plot change point ymax = out.max() ymin = out.min() plt.vlines( S, ymin=ymin, ymax=ymax, linestyle= 'dashed' , color= 'gray' , label= 'change point' ) plt.legend() plt.show() これで連続した曲線が得られました。 1-2. 線形トレンド 線形トレンドは下記の式で表されます。 式(5)と見比べると、expの中身が出てきて成長率の部分はロジスティックの時と同様の式です。 オフセット項の部分だけ異なっていて、ここでは という定義の ベクトルで連続した直線が得られるよう調整されています。 参考までにコードを書くと下記のようになります。 T = np.linspace(- 6 , 6 , 100 ) S = np.array([- 3 , 1 , 3 ]) delta = np.array([ 0.1 , 0.3 , - 0.6 ]) def linear_trend (T, S, delta, k= 1 , m= 0 ): a = np.vstack([np.where(S < t, 1 , 0 ) for t in T]) gamma = -S * delta y = (k + (a * delta).sum(axis= 1 )) * T + (m + (a * gamma).sum(axis= 1 )) return y out = linear_trend(T, S, delta, k= 0.1 , m= 0 ) plt.plot(T, out) plt.xlabel( "t" , fontsize= 16 ) plt.ylabel( "y" , fontsize= 16 ) # plot change point ymax = out.max() ymin = out.min() plt.vlines( S, ymin=ymin, ymax=ymax, linestyle= 'dashed' , color= 'gray' , label= 'change point' ) plt.legend() plt.show() 1-3. 変化点の自動検出 変化点 はユーザー自身で設定することもできますが、スパース推定のようなことをして自動検出も可能です。 論文によると変化点 の上限数を多めにとり、各点の変更率 に対し という事前分布を仮定すれば良いとあります。 はラプラス分布のことですが、その形状を確認してみましょう。 正規分布よりも0付近の値が出現しやすくなっている、という特徴がありそうです。 さらに の部分を変化させると下記のような分布が得られます。 が0に近づくにつれ、ほとんど0の値しか出現しない(スパースな)分布になっていることが見て取れます。 ここまでの話をまとめると、、、変更率 にラプラス分布を仮定すると、多めの変更点をとっておいても変更率はほぼ0になり、かつ稀に大きな変更率が発生するという事象を再現することができます。また、変更率の大きさそのものは を小さく調整することで抑えることが可能になります。 1-4. トレンド関数の予測 ここまで扱ってきた変更率 ですが、実際の予測の際にはどのような値を取ると良いでしょうか。論文を読むと、時系列データを予測する際に変更率 を下記のようにシミュレーションさせるとあります。 まずラプラス分布のスケール を決定するためベイズ推定で事後分布を得るか、そうでなければ最尤推定的に解いて を分布のスケールとします。この場合の は過去に出現した変更率 の絶対値の平均です。 過去の時系列の長さが 個、そのうち成長率の変更のあった時点が 個と定義したので、変更点の発生確率は 、発生しない確率は と言えます。 これらを踏まえ、論文では将来の について下記のように定義しています。 左の式ですが、 は全称記号なので より大きい任意の つまり未来に起きるすべての変更点について、という意味になります。右の式は、 は with probability の略なので の確率で の確率で は の分布に従う乱数 という意味になります。 まとめると、未来の変更率 を求めるには、まず の確率で が0になるかラプラス分布に従う乱数となるかが決まり、ラプラス分布に従う場合は その分布の乱数が変更率 となる...以上のプロセスを予測したい時点ぶん繰り返すことになります。 この一連のシミュレーションをコードで書くと下記のようになります。トレンド関数にはロジスティックのほうを用いています。 class LogisticTrendEstimator : def fit (self, T, S, delta, k= 1 , C= 1 , m= 0 ): self._T = T self._S = S self._delta = delta self._k = k self._C = C self._s_freq = len (S) / len (T) self._mu_delta = np.abs(delta).mean() self._y, self._gamma = self._logistic_trend(T, S, delta, k, C, m) def _logistic_trend (self, T, S, delta, k= 1 , C= 1 , m= 0 ): a = np.vstack([np.where(S < t, 1 , 0 ) for t in T]) gamma = np.zeros(S.shape) for j in range ( 0 , gamma.shape[ 0 ]): gamma[j] = (S[j] - m - gamma[:j].sum()) * ( 1 - ((k + delta[:j].sum()) / (k + delta[:j + 1 ].sum()))) y = C / ( 1 + np.exp(-(k + (a * delta).sum(axis= 1 )) * (T - (m + (a * gamma).sum(axis= 1 ))))) return y, gamma def forecast (self, length= 10 , seed= None ): np.random.seed(seed=seed) # generate future change point, and its change rate occurrence = np.random.binomial(n= 1 , p=self._s_freq, size=length) generated_s = np.where(occurrence == 1 )[ 0 ] + self._T.max() generated_delta = np.random.laplace( 0 , self._mu_delta, generated_s.shape[ 0 ]) # predict future = np.arange(length) + self._T.max() future_y, _ = self._logistic_trend( T=future, S=generated_s, delta=generated_delta, k=self._k, C=self._C, m=self._gamma[- 1 ] ) # plot y plt.plot(self._T, self._y, c= 'steelblue' , label= 'past' ) plt.plot(future, future_y, c= 'darkorange' , label= 'predict' ) plt.xlabel( "t" , fontsize= 16 ) plt.ylabel( "y" , fontsize= 16 ) # plot change point ymax=np.max([self._y.max(), future_y.max()]) ymin=np.min([self._y.min(), future_y.min()]) plt.vlines( np.hstack([self._S, generated_s]), ymin=ymin, ymax=ymax, linestyle= 'dashed' , color= 'gray' , label= 'change point' ) plt.legend() plt.show() return future_y T = np.arange( 100 ) S = np.array([ 20 , 60 , 80 ]) delta = np.array([- 0.03 , 0.01 , 0.02 ]) estimator = LogisticTrendEstimator() estimator.fit(T=T, S=S, delta=delta, k= 0.01 , m= 0 ) pred = estimator.forecast(length= 100 , seed= 123 ) 2. : 季節変化 次に季節変化を表現する下記の式を理解していきます。 英語の直訳で「季節変化」と書きましたが、意味的には季節を含め、週、月、年といったあらゆる周期性を で扱えます。 季節による変動がある → 周期性がある → 信号処理っぽく表現できる、という発想で は下記のように一般的なフーリエ級数で表現されています。 この式を理解するために、まずフーリエ級数展開の気持ちを簡単に復習します。 フーリエ級数展開について 下記のような曲線をどうにかして関数 で表したいとします。 (これはもちろん私が作ったので事前に知ってるだけですが)調べたら下記の式で表せることがわかりました。 どうやらこの曲線は3つの三角関数の和で表現されているようです。3つの三角関数をバラバラにプロットした図をみると下記のようになります。 このようにマクローリン展開などと違って、sinやcosなどの三角関数の和で関数近似しようというのがフーリエ級数展開の特徴です。 季節性の売上など、周期性をもった波形のデータであれば というように各周波数(ここではt, 2t, 3t...のこと)の成分を追加していけばどのような波形でも表現可能になります。 ここまでの話をまとめるため、少し強引に一般化した式に直すと のようにsin波とcos波の和で表現できます。 三角関数の周波数について Prophetの季節関数を理解するためにあと一点だけ、周波数 の部分について深掘っていきます。 周波数はsin波cos波の振幅数を表しています。 という単位で一周するので、例えば30日のうち1週間ごとに一周するsin波を表現したい場合は という式になり、周波数の分母で周期の単位(ここでは1週間なので7)を指定します。 これをグラフ化すると下記のようになり、30日の間にsin波が4周している(30日とは4週間ちょっとの期間なので)ことが見てとれます。 Prophetの季節関数では、このような週、月、年単位の周期を持つことを柔軟に表現できるよう変数 を用いて という変形をしています。さらに が無限大だと計算量が過多になる & 正の実数のみで十分に季節変化を表現できるため、 を に直し、 に書き換えると と変形でき、最初に示した季節変化の関数 が得られます。 パラメタ推定しやすい形に変形する このとき最適化すべきは ] の部分、合計2N個のパラメタなので扱いやすいベクトルで抜き出して表現します。 残りの三角関数の部分もベクトル化して抜き出します。仮に の粒度までで関数近似し、年単位の季節変化に見るために とした場合には、 というベクトルを作ります。 ※ 閏年を含めると1年の平均日数は365.25 これらを用いて結局 は下記のように変形できます。 論文ではさらに とし、フーリエ級数の各係数に正規分布を仮定しているようです。 ちなみにパラメタ は多いほど周期変動にうまく当てはまるようになりますが、同時にオーバーフィッティングしてしまう問題も抱えています。論文では、年単位の周期なら 、週単位なら くらいで程よくフィッティングすると書いてあります(個人的にN = 10 はやや多いのでは、という気もしますが)。 3. : 休日効果 最後に突発的なイベント効果をモデルに組み込む について見ていきます。 これも英語の直訳で「休日効果」と書きましたが、意味的には休日含むイベント全てを で扱えます。 休日やイベントの多くは事前に予見できるわりに周期といったものはなく、季節変化 では取り入れにくい要素です。そのため自動検知云々は諦め、Prophetでは分析者自身がイベントカレンダーのリストを作ってモデルに組み込めるように設計しています。 この設計をProphetではどのような数式で表しているのかを見ていきます。 あるイベントを とし、それに該当する日付を全て含んだベクトル を作ります。 たとえば なら というように過去と未来全ての12月25日を含むことになります。さらに各時点 が に該当するか否かを表すインディケーターとして というベクトルを定義します。ある時点 が各イベント に該当するかのフラグが 0 or 1で入っています。 各イベント に対する係数パラメタを とし、そのベクトルを で表すと、最終的に休日効果 は となり、季節変化 と同様にパラメタ部分だけをベクトルに分離して表されています。 季節変化のパラメタ と同様に も下記のように正規分布が仮定されています。 まとめ 冒頭のProphetのモデル式を再掲すると下記のような、主に3つのコンポーネントからできていました。 各要素は最終的に下記のように展開できました。 ...本当はこれら未知のパラメタの最適化について書かないとキリが悪いのですが、 もう体力が 記事のボリュームが限界なので簡単に述べます。 諸々のパラメタを定義していく中で、記事中では下記の3つについてわざわざ確率分布を仮定していました。 パラメタごとに分布を仮定しておくと状態空間モデルとして扱うことができます。実際にProphetではこのモデル式がStanで記述され、L-BFGS法などで最適化されているようです。 最後に 私自身が数学があまり得意でないのでかなり噛み砕いて書いてみました。どなたかの理解のとっかかりになれば幸いです。 明日のアドベントカレンダーは Owners Growthチームの大木さんと、PAY株式会社のセキュリティエンジニア、クリスさんです! お楽しみに!
アバター
この記事はBASE Advent Calendar 2019の20日目の記事です。 devblog.thebase.in PAY株式会社でテックリードを務める東と申します。 主にバックエンド全般に広く携わっています。最近はサーバーアプリばかり書いていますがインフラもわりとやります。 当ブログの読者の方には弊社のことをご存じない方もたくさんいらっしゃるかと思いますので、簡単に社の紹介をさせていただきます。 PAY株式会社はBASE株式会社の100%子会社で、オンライン決済サービス「PAY.JP」とID決済サービス「PAY ID」などの決済サービスを開発・運営している会社です。 「支払いのすべてをシンプルに」をミッションに掲げ、お金を扱うすべての事業者・個人がもっと豊かな生活ができることを目指しています。 さて、決済というミッションクリティカルなテーマを扱うにあたって、品質保証は最も重要な課題です。 弊社のメインプロダクトたるオンライン決済サービス「PAY.JP」にも5000ケース程度の自動テストが記述されており、常時CIやローカルで実行され続けています。 自動テストはその実行時間がネックとなることが多々あります。今年の9月頃まで、PAYのテストはCircleCIで15分程度かかっていました。 テストが遅いと開発のテンポが落ち、CIが詰まりはじめ、そのストレスが限界を超えればお金で解決することを余儀なくされます。 しかもいくらお金を積んだところでcpu1コアあたりのクロックは頭打ちであり、1テストあたりの実行時間はそう簡単には短くはなりません。 そのままテスト実行時間が育ち続ければ、早晩 一人三勤制 の世界が到来することでしょう。 幸いにして昨今は個人の開発マシンですら8core16coreの時代です。これを生かさない手はありません。 今回は、並列化を軸にしたPAYのテスト高速化の取り組みについてお話させていただきます。 構成 言語 python3 フレームワーク pyramid テストランナー pytest 主要なミドルウェア PostgreSQL, Redis ローカルテスト実行環境 Docker, docker-compose 今回の内容にはpyramidフレームワーク固有の事情はほぼ出てこないため、python3+pytest全般のtipsとして利用できるかと思います。 テストランナーを並列実行に対応させる まずはpytestの並列化拡張を探すところから始めました。 2年ほど前に検討した時点では pytest-xdist という拡張が存在していましたが、並列タスクを実行する場所を色々選べる反面制約が多く、弊社の用途では使い物になりませんでした。 一方去年現れた pytest-parallel はpytestの実行をpythonのthreading/multiprocessingで分散するシンプルなもので、まさに我々の求めているものでした。 pytest-parallelの導入 pytest-parallel自体はインストールして--workers=NUMのようなオプションをつけるだけで並列化できるのですが、とりあえずテスト実行をしてみたところ実行が10倍以上遅くなりました。 調べたところfixtureのscope(どのような単位でfixtureを再生成するかの設定)機能が効かなくなり、すべてfunction scope扱いになってしまっているようでした。 PAYのテストでは、FunctionalテストのためにpyramidのWSGIアプリケーションインスタンスを作る巨大なfixtureがsession scopeで用意されています。そしてscope判定が壊れているせいでこれがテストケース実行のたびに再生成されているようです。 さすがにこれが直らないことには話がはじまらないということで、直したものがこちらになります。 https://github.com/feiz/pytest-parallel/tree/pass-nextitem-to-runtest_protocol これを導入することでテストランナーの並列化対応は解決しました。 1 multithread vs multiprocess pytest-parallelはマルチスレッド/マルチプロセスともに対応していますが、SQLAlchemyがスレッドセーフでない、そもそも後付けでスレッドセーフにするのは難易度が高いなどの問題により、マルチプロセスのみを使うことにしました。 割り切ることはたいせつです。 データベース 次にデータベースです。自動テスト用のデータベースを準備する機能自体はもとからsession fixtureとして実装されていましたが、マルチプロセス化するとこれらが各プロセスでそれぞれ実行されてしまい、同じ名前のデータベースを生成しようとしてクラッシュします。 また、同一のdbに複数のプロセスからテストを実行する場合、トランザクションで詰まってパフォーマンスに悪影響があるということも考えられます。 そこで、db名末尾にpidを付与してプロセスごとに独立したデータベースを参照するように手を加えました。 - DB_URI = "postgres://db:5432/test_payjp" + import os + DB_URI = f"postgres://db:5432/test_payjp_{os.getpid()}" これは簡略化した擬似コードですが、PAYの設定ファイルはpythonファイルになっているため、割と簡単に実現できました。 また、テスト環境のPostgresイメージをpostgres-ramに変更することで、すこしばかりのパフォーマンス改善も行いました。 本当はin-memory SQLiteに差し替え可能にできれればよかったのですが、PAYはPostgresに依存しているところが多く、残念ながら実行できていません。 ソケット枯渇問題 テストのために64coreマシンで128並列実行などをして遊んでいたところ、テスト実行中に突然DBコネクションが張れなくなる問題が発生しました。 察しのいい方ならピンときそうなトラブルですが、TIME_WAITなコネクションが増殖してアプリケーションコンテナ側のポートが食いつぶされてしまったのが原因でした。 2 これはテスト中だけSQLAlchemy側のコネクションプールを有効にすることで解消できました。 余談 並列化の実験にはGCPのプリエンプティブルインスタンスが安くて手軽で非常に便利でした。個人のアカウントで64coreぐらいのマシンを立ち上げて実験してすぐ落とすぐらいの使い方であれば月300円ぐらいで十分に遊べます。 並列化後にボトルネックがどういうところに現れるのかの肌感を掴んでおくためにも、一度はやっておくとよいでしょう。 Redis ストレージ整備の一環としてRedisのテスト環境も見直しました。これまではdocker-composeで用意された本物のRedisにredis-pyでそのまま接続していましたが、これをすべてin-memory実装である FakeRedis に置き換えられるように改修しました。 cpuコアと同様、昨今の環境ではメモリも大量に余っていることが多いため、やれるものをinmemoryにしてしまうのは手っ取り早いテスト高速化の手法です。 3 過去の話ですが、djangoを使っていた頃は storage api と inmemorystorage でファイルシステムを触るテストコードを高速化する手法はお気に入りでした。 テスタビリティ全般に通じる話ですが、重要なのはミドルウェアや外部環境との接続部分をラップして差し替えが自由にできる構造を最初から組んでおくことです。 テストコードの順番依存 テスト環境の改善により実行はできるようになりましたが、不可解なFAILがランダムかつ大量に発生するようになりました。 これまでのテストは直列かつ同一の実行順で実行されていましたが、マルチプロセス分散されたことで実行順が不安定になり、前提条件が満たされていない実行が発生してしまうようになってしまったようです。 PAYではテストケースごとにDBをrollbackするようになっていないため、このような問題が起こりえます。実行の前提条件構築がテストケース間で正しく分離されていない悪いテストを書いてしまっているということです。これはひどい。 2ヶ月程度をかけて、これをしらみ潰しに改善しました。 残念ながらここについては一般的な解法はなさそうに思います。参考までに、原因を特定する手順として社内ドキュメントに書いた内容をかいつまんでご紹介します。 1. エラーの直接の原因を把握する 大抵、常識的に考えて当然(ある|ない)はずのデータが(ない|ある)といった不可解なエラーとして表出します。混乱せずにまずは何が起こったのかを正しく読み解きます。 2. できるだけ狭い範囲で再現させる テストランナーの個別実行機能を使ってできるだけ狭い範囲で100%再現できるように発生条件を絞り込みます。 調査中は並列実行をやめる エラー内容から原因にアタリをつけ、標準の実行ケース指定機能で範囲を絞る pytest tests/integration/api/test_charge.py::TestCharge randomize拡張(pytest-randomなど)でランダム実行を繰り返す 失敗するパターンを見つけたら、ランダムシードを記録して追試する(どこのrandomize拡張にもあると思います) 再現したら、デバッガを仕込んで個別に追う -> 原因特定 3. 対処する 多くの場合、前提データの前処理や後処理が悪いことが原因のため、テストデータ生成基盤に手を加えて改善することになります。 pytestの[yield fixture]( https://docs.pytest.org/en/latest/fixture.html#fixture-finalization-executing-teardown-code ) はsetUp/tearDownメソッドのような手法よりもfixture個別の前後処理の記述がやりやすいため、積極的に活用するとよいでしょう。 本来の文章では、実際に直したケースを再現できるよう、私が実際に調査・修正した過程をchangeset idを添えて記載していました。 完成 以上のような改善を経て、pytest-parallelの導入を決意してからおよそ7ヶ月、並列化を志したときから数えれば実に1年と1ヶ月をかけてちゃんと動く並列化を実現できました。 結果として、CircleCIの自動テストタスク実行時間が15分34秒 -> 2分53秒に改善しました。(5.3倍!) before ↓ after テスト基盤を整えよう。今すぐ。 私の経験上、テストはアプリケーション本体以上にコードが腐敗しやすい場所に思えます。 もし、あなたがこれからテストコードを書き始めるか、プロジェクトのテストコードがまだ育っていないなら、今すぐにでもお使いのテストランナーのrandomizeオプションをオンにしてみるべきです。それだけで今回一番つらかった問題(順番依存)が起きてしまうリスクを大幅に低減できます。 必要に駆られる前から並列化を行っておくこともおすすめできます。本来テストケースは分離されていて当然のものであり、きれいなテストを書いていれば簡単に並列化できるものだからです。 これらは不健全なテストをふるいにかけ、テストコードを健全に保つ大きな助けになります。 ミドルウェアへの直接的な依存もよく吟味すべきです。昨今はdockerの台頭でテスト環境にミドルウェアそのものを用意することのハードルが非常に低くなっているため、手を抜きがちな部分ではあります。 しかし、テストダブルで依存を自由に管理できないとそもそもテストを書くのが面倒極まりないですし、書くのが面倒なテストは書かれなくなるか、コピペが横行して加速度的に腐敗します。 残念ながらすでに問題が起きているのなら、諦めて粛々と改善しましょう。それが一番の近道です。 おわりに テストに実行速度の問題が表出するようなタイミングでは、対処しようにも他の問題と複合していて手が出せない状態になっている可能性が高いです。このような問題に拘って本来やるべき開発に支障がでたり、CircleCIに無限に予算を吸われてしまうようになる前に、ぜひテスト実行基盤を見直してみることをおすすめします。 この記事が、みなさんの快適な開発の助けになれば幸いです。 明日は 明日は BASE Owners Growthチームの大木さんと、PAY セキュリティエンジニアのクリスです。 使用には耐えるものですが、魔改造の粋を超えられていない(スレッド実行機能が死んでいる)こともあり、本家にPRは送れていません。すみません。;( ↩ PAYの本番環境にはPgBouncerが居るため、アプリケーション側ではコネクションプーリングは行わない設定になっていました。 ↩ 手っ取り早いのは確かなのですが、「本来使うものとは違うもの」でテストしているのもまた確かなので、場合によっては速度を度外視して本来のものに近い環境でテストするステージを設けることも検討するべきです。 ↩
アバター
はじめに この記事は BASE Advent Calendar 2019 の19日目の記事です。 devblog.thebase.in こんにちは、BASE株式会社 Native Application Groupの小林です。 主にAndroidのアプリ開発をしています。 「BASE」のAndroidアプリは3種類リリースしています。 その中でも、ショッピングアプリ「BASE」のレイアウトをより見やすく、使いやすくしていく活動をデザイナーと共に行っています。 その中で、レイアウトがちょっとずれている原因を調べる時に使っている手段を紹介したいと思います。 Android StudioのLayout Inspector 「画像の高さが違う」、「データは来てるはずなのに画面に文字が表示されない」といったレイアウト違いを調べたいときに便利な機能です。 起動中のアプリで表示している画面で、Viewの設定値を知りたい場合に、Android StudioのLayout Inspectorを使って知ることができます。 使用方法 1.Android StudioのメニューバーからTools → Layout Inspectorを選択 2.デバッグ起動しているアプリの一覧が表示されます。 本番用に起動しているアプリは基本的に選択肢に表示されないので、テスト版のアプリで試しましょう。 (root化していたりエミュレータの場合は本番用のアプリも表示されます) 3.起動中のアプリにスタックされているActiivtyが表示されます。 画面に表示されているActivityを選択します。 余談ですがこの機能、プロジェクトに参画したての時に画面に紐づくActivity名がわからない時によく使ってます 4.暫く待つと、このように画面の構成要素が表示されます この機能でできること Layout Inspector ではxmlに記述していた高さや幅、動的に変更している値の結果を確認することができます。 (画像の高さを確認しています) よく使うのは、判定式で表示/非表示を切り替えてる動的な高さを持つViewが表示されない原因調査です。 ・そもそもViewは表示されてるなのか? →Visibility=INVISIBLE/GONEになってないか ・VISIBLEの上で高さが0dpなのか? →height weight が0dpになってないか ・高さもあるが、透明な状態なのか? →Textが入っているか、backgroundcolorが設定されているか、x,yが0やマイナスになっていないか こういった調査をこの機能1回で調べられます。 Live Layout Inspector 現在のLayout Inspectorは、取得時点のレイアウト情報を表示していて、端末側で操作しても反映されません。 Android Studio 4.0ではLive Layout Inspectorという機能が追加され、リアルタイムに反映されるそうです。 リンク [Android Developers] Layout Inspector https://developer.android.com/studio/debug/layout-inspector [Android Developers] Live Layout Inspector https://developer.android.com/studio/preview/features?hl=en#4.0-live-layout-inspector 開発者オプションのレイアウト境界を表示 View間の余白がずれていてどちらのViewが幅を取っているのか知りたいときに便利なAndroid OSの機能です。 Viewが確保しているスペースやmarginをわかりやすくするための「レイアウト境界を表示」という開発者オプションがあります。 使用方法 設定するには、開発者オプション→描画項目にある「レイアウト境界を表示」をONにします ショッピングアプリ「BASE」の場合、このように表示されます 通常 レイアウト境界ON Viewの高さや幅の設定で確保されているスペースを赤と青角の線、merginで確保されているスペースが赤く塗りつぶされて表示されています。 Android 9からはトップのクイックタイルに登録してワンタップで切り替えができるようになり、より気軽にチェックができるようになっています 開発者オプション→クイック設定開発者用タイル→レイアウト場所を表示をON→クイックタイルから切り替え リンク [Android Developers] デバイスの開発者オプションを設定する https://developer.android.com/studio/debug/dev-options#drawing さいごに 今回はAndroid Studioからレイアウト構成を調べる方法と、端末の開発者オプションから余白を調べる方法を記載しました。 どちらかでも新しく知っていただいた方がいればとても嬉しいです。 レイアウトがずれてる原因は様々で、一つの方法ですべてが分かることがない事が多いです。 いろんな調べ方を組み合わせて、素早くレイアウトの改善につなげていきたいです。 明日はDataStrategyの岡さんとPAY株式会社のテックリードの東さんです。お楽しみに!
アバター
この記事はBASEアドベントカレンダー 19日目の記事です。 devblog.thebase.in Webアプリの解体 こんにちは。フロントエンドエンジニアの松原( @simezi9 )です 近年、Webアプリはクラウドの発展とともにそのあり方を大きく変質させてきました。 具体的にはXaaSの発展により、Webアプリはその構成要素をあらゆるレイヤにおいて細かく分解され、それらを開発者が組み合わせることで作られるようになりました。 このアプローチによりシステムはプラガブルになり可用性の面でも品質面でも大きく進化しました。 その反面で当然このアプローチにも問題があります。その最たるものが組み合わせの複雑度の増加だと思います。 あらゆるものがプラガブルになった反面、それぞれのサービスをどのように組み合わせてWebアプリを構築していくのか自体が一つの知見・分野となり、「昔はWebアプリフレームワーク一個さえあればそれでよかったのに」というような声が出てくる一因となっているようにも思います。 そうした流れの中にあっては、必然的にプラガブルになったサービスをまとめること自体をサポートするサービスやフレームワークが出てきます。それはかつてのRailsであり、AWS Elastic Beanstalkであり、最近で言えばNuxt.jsのようなものであったりします。 Stackbit はそうした「Webアプリ開発にレールを敷く」サービスの1つであり、 近年急激に隆盛した「JAMstack」アプリを一気通貫に作成することができます。 JAMStackの登場 JAMstack とはJavaScript+APIs+Markupの頭文字をとった概念で、 アプリとしての動作や機能はすべてクライアントサイドのJSに集約し、 データやロジックはすべてAPI経由で取得し、 そのデータを表示するテンプレートをデプロイ時に事前にビルドした静的ファイルで返却する ようなWebアプリを指す概念です。 こうした構成を取るメリットはいくつもありますが、とりわけ「静的ファイル(HTML,CSS,JS)配信を中心にした構成」によるサイトパフォーマンスの向上やスケーリングの楽さが多く取り上げられています。 JAMStackは特にWordpressなどのCMSが担ってきた領域のアプリを実現するための手法としてよく登場します。 というよりも個人的な印象で言えば、Webアプリで多く使われていたCMSを分解しプラガブルなサービス群へと変換していく中で生まれたベタープラクティスがJAMstackという形になったというような気もします。 JAMstackなCMSを構築する過程では機能とそれを提供するサービスは大きく以下のように分類されます HTML/CSSで静的ページを出力する( 例:Hugo, GatsBy, VuePress ・・・etc) クライアントサイドでのルーティングやAPIとの連携を行う機能 (JavaScript) コンテンツの配信・管理を行い、APIでデータを提供する機能(例:Netlify CMS, Contentful・・・etc) 生成したファイルをホスティングする機能(例:Netlify, Github Pages・・・etc) これらの組み合わせをあれこれ試しつつサイトを構築するのは一人のエンジニアとしては腕の見せ所でもありとても楽しい部分ではありますが、実際にサービスを作る上では一定の知見や検証が必要になります。 その部分を代わりに担保してくれるのが Stackbit のようなサービスになります。 実際にこのStackbitを使ってサイトを生成してみようと思います。 Stackbitを試してみる Stackbitでは以下の事を自動で設定してくれます(ただし本記事執筆時点ではBETAのため、組み合わせるサービスの選択肢は多くがCOMING SOONの状態) - 生成されるサイトのビジュアルテーマの設定 - サイト生成に使用する制定サイトジェネレータの決定 - Headless CMSとして仕様するサービスの設定 - Gitによるコードのホスティングとデプロイフローの整備 サイトの生成までは特に難しいことは有りません サイトの生成を始める サイトのテーマを選ぶ 用途にあわせて標準のテンプレートからサイトのスタイルを選びます。 ひとまず一番先頭にある「Exto」を選びます サイトジェネレータを選ぶ サイトの構築に利用する静的サイトジェネレータを選びます。 ここではとりあえずGatsbyを選択します CMSを選ぶ サイトのコンテンツを管理するためのCMSを選びます。 ここではサイトのデプロイにNetlifyを使うので、NetlifyCMSを選ぶことにしてみます githubとの接続 生成されたサイトのバージョン管理をするために、GitHubアカウントと生成するサイトの紐付けを行います。 サイトの生成 これで最初の作業は完了し、サイトの構築・ビルド・デプロイまでが開始されます Github サイト生成のタイミングでGithub上にリポジトリが作られて、サイトの開発・記事の投稿、デプロイフローなどの整備が全て整った状態でリポジトリが用意されます。 ローカル開発を開始するための手順なども用意されていて、静的サイトジェネレータを触ったことがある方にはすぐに開発できるような状態が整っています 実際に出来上がったサイト 標準状態で、一通りの機能を備えたサイトがWebサイトとして公開されるところまで自動実行されます。 これらはソースはGithub、サイトはNetlify上に展開されており、自前で管理するサーバーが一切ないという広義でのサーバレスが実現されています。 Lighthouse Lighthouseを使ってページのメトリクスを測定してみます。 性能にスロットルをかけたモバイル環境の設定で実行してもご覧の通り、かなり高いパフォーマンスを持ったサイトになっています 記事の投稿 今回はNetlify CMSを利用しているため、記事の投稿はそちらを経由して行います。 ページ右上部にStackBitが提供するコントロールパネルがあるのでそこからアクセスをします するとCMSの管理画面が出てくるのでここから記事を投稿します。 このあたりは、HeadlessCMSらしく、コンテンツの管理に必要な機能だけが搭載されたシンプルなものになっています。 記事を投稿すると、その投稿がGithubリポジトリにコミットされ、静的サイトジェネレータが実行されサイトの再ビルドが動き始めます。その様子はサイトのコントロールパネルからも確認できるようになっています。 まとめ StackbitはWebアプリ構築の第一歩を強烈に加速してくれるサービスでした。 そして不要になったらGithub上にコードがあるのですぐにサービスを切り離すことも可能です。 カスタマイズ性の高さと構築の定形作業のコストを軽くしてくれるという意味でとてもバランスのよいサービスだと思います。 このサイトを作るまでの工程は、非エンジニアの方でもおそらく数十分〜数時間程度あれば可能なほどに容易です。 ただし、あくまでWeb開発のベタープラクティスを集合させたサイトの雛形を提供してくれるものなので、 Webサイトの構成がどうなっているのかを把握できていないレベルの方ではサイトのレイアウトを変えたり、機能を追加開発することは厳しいのではないかと思われます。 そういう意味ではある程度フロントエンド側の構築に理解がある方がパパっとポートフォリオだったりブログサイトを作るのにはとても便利なサービスかと思います。もちろん技術構成自体は普通に大規模サイトでも通用するようなものなので、その第一歩として活用することも十分可能だと思います。 自分でWordpressを構築して、デプロイできるサーバーを探して・・・とやっていた時代がそれほど昔でないことを思うと、 この生産性の高さは強烈だなあ、すごい時代だなあと思わずにはいられません。 サービス自体はまだベータ版ですが、正式版のリリースが待ち遠しいサービスだと思います。 明日はData Strategy Sectionの岡さんとPAY株式会社 テックリードの東さんです。お楽しみに!
アバター
この記事は BASE Advent Calendar 2019 の 18 日目の記事です。 devblog.thebase.in はじめまして、Owners Marketingグループの栗田です。 現在は主にサーバーサイドエンジニアとしてオーナーズ(ショップオーナーさん)を支援できるような機能の開発をしております。 最近では、ネットショップ作成サービス「BASE」の拡張機能であるBASE Appsの一つである予約販売 App の開発や増税対策のための機能改修などを行っておりました。 この記事ではBASE社内の部活動のひとつ、スパルタン部で日本初開催のスパルタン・ビーストを攻略した話を紹介します。 下記記事にてランニング部も紹介されてますので BASEのランニング部をきっかけにフルマラソンに挑戦 この記事と合わせてBASEの部活動の雰囲気を感じていただければと思います。 スパルタン部とは みんなでスパルタンレースに出る部活です。 日々各自トレーニングをしたりスパルタンレース情報交換や攻略のノウハウを共有しています。 スパルタンレースとは 世界最高峰の障害物レースといわれています。 今回出場したのはその中でも非常に過酷と歌われているスパルタン・ビースト ※ スパルタン・ビーストの説明 21キロの道に30以上の障害物が待ち構えるスパルタン・ビーストでは、筋力、持久力、揺るがない決意が試されます。予測不可能かつ巧みに設計されたコースや障害物の数々は、肉体・精神ともに追い込み、自身の限界を打ち破らなければ達成することのできない非常に過酷なレースです。 ( https://spartanracejapan.info より) 障害物レース??個人戦?? 障害物競走と聞きますと個人戦のようですが、チームでエントリーして協力・助け合いながら攻略していきました。 1人では超える事のできない障害物を協力して乗り越えたり、1人では持ち上げる事のできない重りを協力して持ち上げたりしました。 また、障害物を攻略できないとペナルティのジャンピング腕立て伏せを数十回行わなければいけないのですが、時には分担する事より攻略していきました。 参加してきました! レース中はチームワークで乗り切るところもあったり、時には個人の力で乗り切るところもありました。 20kmを超えるレースですが、スタート直後から過酷な旅が待っておりました。 炎天下の中、普段はゲレンデの下山コースとして使用されている2km以上にも及ぶ山道の登りから始まり 時には数十キログラムを超えるサンドバッグを持って山道を歩いたり、張り巡らされた有刺鉄線の下を匍匐前進で進んだり 力を合わせてロープを引っ張り数十キログラムを超える重りを頂点まで上げたりなど、辛いシーンが多々ありました。 (※レース中に協力している様子です。 https://www.youtube.com/watch?v=juit1t9akds&feature=youtu.be&t=2235) 合計時間も7時間を超え、辛さから気持ちや身体の浮き沈みもありまるで短期決戦のプロジェクトのようでしたが 個人個人が自分と向き合い、助け合いながら諦めずに無事に全員でスパルタン・ビーストを攻略する事ができました! (※ ゴール後にメンバーのみんなで記念写真) まとめ 普段乗り越えた事が無い壁を乗り越える事は格別ですね。 集中して濃密な時間を経験する事ができました。 今回色々な部署のメンバーと共通の課題を乗り越える事ができ 仕事でのプロジェクトをプロジェクトメンバーと乗り越えた時と同じような感覚がありました。 超える壁は大きいほど達成感はありますし、それがチームで達成できるのはとても嬉しいですね。 来年は海外のスパルタンレースに挑戦しようという声が上がってます。 スパルタン部の挑戦は来年も続きます。 明日は、Frontendグループの松原さんとNative Applicationグループの小林さんです!
アバター
この記事はBASE Advent Calender 2019の18日目の記事です。 こんにちは。 Product Management Groupでディレクションを担当している、藤井と申します。 ふだんはネットショップ作成サービス「BASE」やショッピングアプリ「BASE」の新機能および改善のディレクション業務を行っています。 そのディレクションのなかでも、個人的にとくに強い関心を持って取り組んでいるのが「UXライティング」という分野です。とはいえ、「ん、UXデザインってのは聞いたことがあるけれど、UXライティングってなんぞや?」という方もきっと多いのではないでしょうか? そもそも、UXライティングってなんぞや? UXデザインを「グラフィックによってユーザー体験をデザインすること」と定義するならば、UXライティングとは「テキストを設計することによってプロダクトとユーザーのコミュニケーションをデザインすること」と定義されているようです。つまり文章を最適化することによって、何を伝えようとしているのかがストレスなく伝わるようにする、いわばテキストによってコミュニケーションをデザインすること、なのです。 (※参照  「シリコンバレーのUXライターが語る、UXライティングの重要性」参加レポート ) UXライティングに興味を持ったきっかけ 自分たちが世の中にECのプラットフォームを提供している、というのはさておき。日常的にネット上で買い物をするのが当たり前になってきているなか、たとえばお問い合わせは電話ではなくメールやチャットで行っていたり、結局は商品の写真と紹介文で判断していたりと、直接のやりとりではなくテキストでのやりとりがほとんどになってきています。だからこそ、文章の意味が理解できなかったり、敬語がおかしかったり、そもそも日本語が怪しかったりするサイトでの買い物はためらってしまいます。つまりテキスト・コミュニケーションもデザイン同様、コミュニケーションのための必須要素であり、トンマナ(トーン&マナー・一貫性のこと)とはテキストのデザインでありUXなんだなあ、と。そんなことを考えているうちに、がぜん「BASE」におけるテキストのあり方が気になって仕方なくなってきたのです。 時代背景からも見て取れる、テキスト重視の機運 でも、そんなことを考えているのは自分だけ?なんて思いつつ、「UX」「テキスト」なんてキーワードであれこれ検索してみると、同じようなことを感じている先人は数多くいたようで。 「『なぜUXライティングが必要なのか?』答えはシンプル、『Words are everywhere』、つまり『言葉はいたるところで使われているから』」 「マネジメントはマネジメントの言葉、エンジニアはエンジニアの言葉、PM、デザイナーなどプロダクトに関わるメンバーが、各々の価値観をベースにした異なる言葉をプロダクトに注ぎ込んでしまうと、ユーザーの体験は断片化され、プロダクトのあちこちにに矛盾が生じる、ユーザーはまるで様々な方向から沢山の人に話しかけられているような感覚に陥り、混乱してしまう」 「洗練されたワードほど自然で違和感を与えない、結果としてユーザに意識されない。ビジュアルを作るとき、無駄のないデザインほど難易度が高いのと同じ」–All Turtles/Jessica Collier 「私たちが触れる様々なプロダクトやサービスは多くの“言葉”で構成されていますので、肝心の言葉を後回しにするなんて間違っていました。ことさらAI時代においては、プロダクトとのコミュニケーションが会話形式で行われるなど、“言葉”はますます重要な要素になるはず」 –All Turtles CEO/Phil Libin (※参照  All Turtles 創業メンバーが語る、UXライティングの重要性 ) かくしてデジタルプロダクト市場が成熟し、よりよいユーザー体験が求められるなかで、言語表現にもボトルネックが存在していることは明らかなようで。2010年代中盤からは、デザインプロセスにおいて言葉のデザインを重要視する流れも実際に起きはじめているようです。なんでもGoogle, Spotifyなどの企業では、“言葉”のプロフェッショナルをデザイン・チームに加える、という取り組みもすでに生まれているのだとか。 「BASE」におけるUXライティングって、どうあるべき? では、「テキストを設計することによってプロダクトとユーザーのコミュニケーションをデザインすること」って、じゃあ具体的にどんなことをするの?というと、シンプルにまとめると次の3つに集約されるように思います。 ユーザー体験をロジカルに分析すること(どう言われると、どんな感情を抱く?) 課題を見つけること(言いたいことが、額面通りに伝わっている?) 言葉をブラッシュアップすること(言いたい、ではなく伝えたい) そして「BASE」にとってUXライティングのあるべき姿としては、現段階ではこんなふうに考えています。 「BASE」というプラットフォームに接するときに目にするワード構成を設計(誰が/どこで/どのように使うのか)する それによって、プラットフォームとショップオーナー様/お客様の間のコミュニケーションをデザインする 書き出してみるとなんてことないことかもしれませんが(ごめんなさい!)、じゃあ実際にできているかというと、取り組みはじめて約1年、目指すべき場所はまだまだずっと先にあるように思えます……。 いま「BASE」が取り組んでいる、UXライティングにおける具体的アクション/視点 そんな「BASE」が現在取り組んでいる、UXライティングにおける具体的アクション/視点とはどのようなものか、その一部をご紹介します。 用語リストを作成/運用する  →誰もが参照できる用語リストを作り、プロダクト内に言葉の矛盾が起こるのを防いでいます。 既存の言葉を使う  →ユーザーがふだん使っている言葉に合わせるべきであり、すでに理解されている表現があるときに、いたずらに新しい言葉を作り出さないよう心がけています。 ダミーテキストを使わない  →デザインするさいにダミーの文章を流し込まず、最初から限りなくリアルなコンテンツやテキストを当てることで、UXが迷子にならないようにしています。 設計を修正するごとにコンテンツも修正する  →デザインと同じく、ライティングの大半は下書きと修正であり、削り落とす作業だと考えています。 形を機能に従わせる  →美しさ<機能。言葉のほうが伝わるのか、画像のほうが伝わるのか、目的達成のために効果的な方を選んでいます。 ゴール・オリエンテッドである  →その画面において必要な情報のみを、その都度提供するようにしています。 これから というわけで、「BASE」のUXライティングの取り組みは、まだまだはじまったばかり。社内にもその必要性/重要性を懸命に訴えつつ、ひいては「BASE」というプラットフォームを使ってくださるショップオーナー様/お客様の体験を、さらに一つ上のステージへと導きたく日々格闘しています。すべてはわかりやすく、正確に、そして行動できるようにーー 「The details are not the details. They make the design」 – Charles Eames(チャールズ・イームズ/デザイナー)・神は細部に宿る (※参照  https://www.brainyquote.com/quotes/charles_eames_169188 ) ーーそう、細部への気配り(一つひとつの言葉選び)だけがプロダクトに差をつけると、「BASE」は本気で信じています。 明日はFrontendの松原さんと、Native Applicationの小林真さんです!
アバター
この記事は BASE Advent Calendar 2019 の 17 日目の記事です。 devblog.thebase.in こんにちは。Platform Dev Section マネージャーの大窪( @bonnu )と申します。 BASE には 2018 年の夏に入社し、現在はバックエンド基盤や SRE(Site Reliability Engineering)、フロントエンド基盤を担う各グループのマネージャーを務めさせてもらっています。 今回のアドベントカレンダーでは実用的なネタを思いつくことができませんでしたので、ちょっとしたコラムのようなものを綴らせていただこうかと思います。 私が愛した言語・Perl 突然ですが、私がこれまでエンジニアとして仕事をしてきた中で一番長く書いていた言語は Perl です。 現在は日本のWeb業界においてのシェアを近代的な言語(※ Golang、Java、Scala、その他 LL 等)に奪われ、かつて Perl で作られていたサービスが別言語にリプレイスされた話を聞く事もあります。なのでよく採用されている言語とは言えませんが… テキスト処理に対して強力なユーティリティが多く、また豊かな表現力(TMTOWTDI 1 )と低レイヤーへのアクセスの良さ 2 から機能実現力が高く、コマンドラインツールに限らず掲示板や小規模サイトからブログシステム、大規模ウェブサービスまで様々なシーンで利用されていました。 (かつては C や Java、C++ など、今もよく使われている言語に肉迫していたようですね。2000年代は頑張った…) https://youtu.be/Og847HVwRSI Larry Wall が定義した「プログラマの三大美徳」 そんな Perl の生みの親、 Larry Wall 氏はかつて Perl のラクダ本(プログラミング Perl 3 。Perl の実用的な解説書)でプログラマの三大美徳を定義しています。 laziness, impatience, and hubris 怠惰(怠慢) Laziness 短気 Impatience 傲慢 Hubris これは Perl に限らずプログラマ界隈においてとても有名でよく引用されている言葉であるため、現在エンジニアとして活躍されている方にとっては今更説明の必要もないかもしれませんが、今回は改めてこの美徳をテーマにしたいと思います。 それぞれの美徳について 怠惰(怠慢) Laziness The quality that makes you go to great effort to reduce overall energy expenditure. It makes you write labor-saving programs that other people will find useful, and document what you wrote so you don't have to answer so many questions about it. Hence, the first great virtue of a programmer. 全体的なエネルギー消費を削減するために多大な努力を払う気質。他の人が役に立つと思う省力プログラムを作成し、あなたが書いたものを文書化するので、それほど多くの質問に答える必要はありません。したがって、プログラマーの最初の大きな美徳。 例えば繰り返し行われる作業、同一の処理を表現するコード、同一概念を扱う多数の関数など、それらの抽象度を見極めて適切な粒度・単位でカプセル化することで同じ事を繰り返さないようにしたり、再開発しなくてよいように工夫する気質と読み取っています。 さらにそれにはドキュメントを付属させることで、他者に伝えるための時間や手間といったものを省くことができるとまとめられています。 私はエンジニアであった当時よりドキュメントを書くのがあまり得意ではなかったので(現在も目下の課題です)、この点については片手落ちだったと反省があります。 短気 Impatience The anger you feel when the computer is being lazy. This makes you write programs that don't just react to your needs, but actually anticipate them. Or at least that pretend to. Hence, the second great virtue of a programmer. コンピューターが怠けているときに感じる怒り。これにより、ニーズに対応するだけでなく、実際にそれらを予測するプログラムを作成できます。または少なくともそのふり。したがって、プログラマの2番目の大きな美徳。 機械翻訳した上でですが、私の解釈として「怒り」という表現は「不便な事を思い通りにしようと直感的に思う」事を指すと考えています。 ビジネス要件、機能要件に対して先々も応えられる処理として仕上げるべく熱量をもてたり、すぐにロジックの整理に頭が向かう様がイメージできます。 また、短期的なニーズだけをただ満たすだけでは足りず、同一パターンの処理が想定できるならばそれらを予め想定しておけという意味合いがあるようです。 小飼弾さんの記事 ではテンプレート処理に関しての言及がありますね。 短気の裏に潜む焦りや不安から、先んじて検討して対処しておけ、というところでしょうか。(Impatience に「焦り」「苦痛に耐えられない」という意味を見つけましたため) 傲慢 Hubris Excessive pride, the sort of thing Zeus zaps you for. Also the quality that makes you write (and maintain) programs that other people won't want to say bad things about. Hence, the third great virtue of a programmer. 過度のプライド、ゼウスがあなたを驚かせる(神罰のような)もの。また、他の人が悪いことを言いたくないプログラムを作成(および保守)できる気質。したがって、プログラマーの3番目の大きな美徳。 プログラマとして品質に妥協せず、またその品質を示すべく説明できること。 プログラムであればテストをはじめとして、その成果物の妥当性・正当性を担保する仕組みを整えることで、その自尊心を守りなさいという美徳と言えます。 これもまた 小飼弾さんの記事 を参考にしますが、傲慢さは他者に対して発揮されうる気質であり、互いに正当性を示し合うことで議論や競争を生み、それが結果として発展を促すという捉え方ができます。 何を「愛して」いたのか 美徳は必ずしも「態度」に表れることを指していない 広くインターネットでの三大美徳に関する議論を眺めてみると、それぞれの美徳についての定義と定義名( Laziness 、等)について意味が対照的でなく、ひねくれていると捉えられる事があるようです。それら定義名はどれもネガティブな印象を持ってしまいそうなものばかりですが、その理由は Larry 氏のユーモアから来ていると私は考えます。 美徳それぞれの意味はどれも高い視座を表現していますが、そのまま定義名を厳格に表してしまっては極論、あまりついてくる人はいないでしょう。まずは「なんだそんな事でいいのか」と思わせるキャッチーなところから入り、その上で解釈が進むことで本来の定義が意識に刻み込まれるような、そんな美徳になっていると感じています。 なのでこの定義名をそのまま態度に表してしまうと… 特に組織活動において、様々なしくじりが生じてしまいます。私は自分の失敗の数を数えていませんが。 オープンソースの原点的考え方として捉える 今でこそどんなプログラミング言語でもソースコードを共有するエコシステム(CPAN、npm、Packagist、Golang+GitHub、etc…)が当たり前になりましたが、当時 Larry 氏が Unix のツールを開発していた時代は一体どんな状況だったのでしょうか。 ネットがそれほど速くない時代(そもそも商用 Internet がなかった)、今よりも手元だったり、企業や大学のイントラ内にだけコードがあることが殆どで、プログラムが人の目に触れる機会はとても少なかったのではないでしょうか。 そういった状況で開発された OSS が安心して使えて高い品質を保つ事を担保するためにも、草の根活動的な呼びかけの一種としてこの美徳があったのではないか、と私は勝手に考えてしまいます。 いま、マネージメント業務をする立場としてどう捉えるか ここまで懐古厨として話を進めてきましたが、そろそろ現在の私の立場に話を戻します。 私がこれまでプログラミングを公私ともにやってきて美徳を正しく体現できたとはとても言えず、その視座の高さに打ちひしがれる思いがありますが、やはりこの美徳は改めて自分の内に秘めて持っておきたいと感じました。 マネージメント業務をする立場においては、各種業務改善や日々の運用をしていくにあたってこれら美徳を判断の参考にすることができます。会議量やフローの適切化やコミュニケーション・パスの整理など、 怠惰 と 短気 の美徳に従えばどうすべきか…。 また今回挙げた三大美徳を一種の宗教だと捉えた場合には当然その他の宗教を持つ方もいると思いますが、各々がその教えや意志を大事にしながらエンジニアリングしていくことで、 傲慢 の美徳に従ってコラボレーションを活発にさせることができます。 最後に 日々の業務でこれらの美徳をそこまで意識する事はありませんが、たまにでも思い出してみる事で自身の振る舞いや判断を振り返るきっかけにできると感じました。 最近はコードを書く機会が減ったことで純粋にプログラマーとしての価値観や観点を忘れてしまうことがあり、今回は自戒の意味を含んで、この三大美徳をテーマとさせていただきました。 明日のアドベントカレンダーは Owners Marketing グループの栗田さんと Product Management グループの藤井さんです。 刮目して見よ。 There’s More Than One Way To Do It:やり方はひとつじゃない ↩ XS:C のコード(または C ライブラリ)との間の拡張インターフェースを作るのに使われるインターフェース記述ファイルフォーマット ↩ https://en.wikipedia.org/wiki/Programming_Perl ↩
アバター
この記事はBASE Advent Calendar 2019 17日目の記事です。 devblog.thebase.in こんにちは、DataStrategyの杉です。 DataStrategyではデータを用いて問題解決を行なっていたり、より使いやすいサービスのための改善をしています。10日目の記事として 類似商品APIについて がありましたが、このようにテキストや画像の特徴量からレコメンドの作成なども行なっています。今回は私がテキストの特徴量を用いて試してみたことについて書きたいと思います。 概要 Eコマースプラットフォーム「BASE」には様々なカテゴリが存在します。 例えばアパレルのショップであれば「トップス」「ボトムス」などショップごとに設定されているカテゴリもあれば、「アパレル」などといったショップ自身を表すカテゴリなど、多いものでは約1000種類の分類がされているカテゴリもあります。 これらのカテゴリを自動で分類できるようになることで、レコメンドの精度向上や入力項目の削減などができるようになります。 しかし、このカテゴリをテキスト特徴量を用いて分類しようと思うと以下のような問題が出てきました。 カテゴリによって偏りがでてきてしまう 精度が上がりにくい カテゴリによって偏りがでてきてしまうことはどのようなタスクでも存在する問題ではないかと思います。しかし、少ないカテゴリに数を合わせてしまうと全体の量が減ってしまい、より精度が悪くなってしまうということも考えられます。 この問題に対して、テキストのDataAugumentationを用いて解決できないかということを試してみました。 具体的には、CutMixと呼ばれる、主に画像のDataAugumentationで使用されている手法を試してみたので、その内容について書きたいと思います。 また、詳しくは下で書かせてもらいますが、このCutMixをテキストに使用するということはあまり推奨されていない手法な可能性もあります。そのため、 こんなこと試してみたんだ という気持ちで読んでいただけると嬉しいです。 テキストのDataAugumentationについて 画像のDataAugumentationといえば画像を回転させたりノイズをのせたりなど様々なことが考えられます。 しかし、テキストでは順序なども影響するため回転などは推奨されていません。 例えば 今日 は 良い 天気 です ね 。 という文章を回転させてしまうと 。 ね です 天気 良い は 今日 となってしまい、日本語として伝わらない文章になってしまいます。 そのため、テキストのDataAugumentationとしては - 類似の単語で置き換える - ルールベースで単語を置き換える などの手法が使われることが多くなっています。 例えば 朝ごはん に パン を 食べ まし た 。 という文章は 朝ごはん に サンドイッチ を 食べ まし た 。 に置き換えても違和感がなく伝わる文章になります。 このようにしてテキストのDataAugumentationが行われています。 CutMixとは 今回試してみたCutMixはこれらのDataAugumentationの手法のひとつです。 論文: https://arxiv.org/pdf/1905.04899.pdf MixupやCutoutを組み合わせたような手法です。 論文の図がとてもわかりやすいと思います。 画像でこれらのことを行うとふたつの画像を半分ずつ合成するMixupとある区間をカットしてしまうCutoutを組み合わせて ある部分を違う画像で置き換える という手法がCutMixです。 Mixupでは合成をすることで不自然になってしまうことや、Cutoutではカットした部分に重要な情報がはいっていたなどということがあるため、CutMixはそれらの問題を解決でき、精度も向上する結果を出しています。 これらは画像での紹介になっていますが、Mixupはすでにテキストでの実装例もあり 論文: https://arxiv.org/pdf/1905.08941.pdf こちらではMixupを使うことで精度向上ができたとの記載があります。 では、CutMixをテキストで表現するとどうなるでしょうか? 例えば以下のふたつの文章があったとします。 文章1: 明日 は 遊園地 に 遊び に 行こ う と 思い ます 。 文章2: 動物 の 中 で は 犬 が 1番 好き です 。 これをCutMixすると 動物 の   遊園地 に 遊び に   が 1番 好き です 。 となります。 これに対して混ぜた割合を出力とすることで学習することが可能となります。 文章としてよくわからない内容になってしまっているため、上で書いたような回転と同様の現象が起きてしまっている可能性があり、推奨されていない可能性があると書きました。 内容 今回は約1,000カテゴリを分類してみました。 データはBASEの商品データのテキストを使用しました。また、これらのデータに前処理を行いMeCabで形態素解析されたものを使用しました。 入力を400ワードで固定し、足りない部分は0埋めしてあります。 CutMixの実装は どの範囲を切るのか どのくらいの単語を切るのか について考える必要がありますが、今回は0埋めしている部分などもあるため 0埋めされていない部分で入れ替えを行う 0-20 wordsでの中でランダムに入れ替えを行う 出力の割合は0埋めを除いた部分で行う としました。以下はkerasのgeneratorを使った場合の実装例です。embeddingを行いCNNで学習をしました。出力は1-hot-vectorです。 ▼CutMix実装例 CATEGORY_NUM = 1000 def generate_input_cutmix (x_data, y_data, batch_size= 32 ): max_len = x_data.shape[ 0 ] seq_len = x_data.shape[ 1 ] while True : x = np.zeros((batch_size,seq_len)) y = np.zeros((batch_size, CATEGORY_NUM)) cnt = 0 while cnt < batch_size: try : r = random.randint( 0 , max_len- 1 ) r_mix = random.randint( 0 , max_len- 1 ) if y_data[r]!=y_data[r_mix]: target_idx = np.where((x_data[r,:]> 0 )&(x_data[r_mix,:]> 0 ))[ 0 ] r_num = random.randint( 0 , 20 ) r_idx = random.randint( 0 , len (target_idx)-r_num) x[cnt,:] = x_data[r,:] x[cnt,r_idx:r_idx+r_num] = x_data[r_mix,r_idx:r_idx+r_num] rate = r_num/ len (np.where(x_data[r,:]> 0 )[ 0 ]) one_hot_r = np_utils.to_categorical(y_data[r], CATEGORY_NUM) one_hot_rmix = np_utils.to_categorical(y_data[r_mix], CATEGORY_NUM) y[cnt,:] = ( 1 -rate)*one_hot_r + rate*one_hot_rmix cnt+= 1 except : # 単語数が足りていないこともあるため pass yield x, y もしword数を変えたい場合には r_num = random.randint( 0 , 20 ) ここの数字をいじることで可能です。 また、ここを固定しない場合は r_num = random.randint(0, target_idx) でランダムに決めることができます ▼学習 model.fit_generator(generate_input_cutmix(x_train, y_train), verbose= 1 , steps_per_epoch=x_train.shape[ 0 ] // batch_size, validation_data = generate_input_cutmix(x_validation, y_validation), validation_steps = x_validation.shape[ 0 ] // batch_size, epochs= 30 ) 結果 (1) loss なにもしていない場合とCutMixを使用した場合のlossは以下になりました。 lossを見るとCutMixよりも何もしないバージョンの方が良い落ち具合となっています。画像の場合のCutMixでは、CutMixを使うことでlossの落ち具合が早くなるなどあるため、想定と異なる結果となりました。 これだけをみていると何もしないバージョンの方がよく見えますが、もっと詳細な正答率について調べてみましょう。 ※何もしないバージョンの方は11epochあたりからtestのlossが上昇してしまっているため 過学習をしている可能性も考え、10epochのmodelを使用していきます。 (2) カテゴリの正解率 まず、予測結果の中で最も高い値となったカテゴリが、正解としているカテゴリと一致しているかについてみてみました。 テストには1000データを使用しました。 import numpy as np pred = model.predict(x_test) pred_argmax = np.argmax(pred, axis= 1 ) print (np.sum(pred_argmax==y_test)) ▼結果 何もしない: 21.8% CutMix: 25.0% カテゴリをどれだけ当てられたかで考えると、CutMixの方が良い結果となりました。 (全体的に精度は低いのでですが1,000クラス分類なのでおおめに見てください...) また、何番目に正解データがでたかを累計してみた結果です。 この結果からも、CutMixを使用した方がうまく予測ができている結果となりました。 今回何もしていない方は過学習をしている可能性を考えて10epochを使用していますが、loss的にはとても落ちている20epochを使用しても大きく変わりはしませんでした。 (3) 新しい文章に適用してみる 私が架空の商品説明文を作ってみました。これに適用してみるとどのような結果となるでしょうか? カテゴリ名はそのまま使用できないため、ふわっとしたカテゴリ名に置き換えています。 ①ぬいぐるみ系 ペンギンのぬいぐるみ! 布から作っているペンギンのぬいぐるみです。 ほどよい綿の詰め具合でなんとも言えないもふもふ感を味わえると思います。 ペンギンの種類はアデリーペンギンをイメージしており、癒しの動物を目指しています。 大きさは30cmほどとなっているため部屋にちょっと飾るにもちょうどいいです。 また、安全に配慮して作成しているため 小さいお子様の遊び相手にもぴったりだと思います。 大きさ 縦: 30cm程度 幅: 10cm程度 また、大きさは手ではかっているため多少ずれていることもあります。 ▼結果 なにもしないバージョン: キャラクタ CutMix: 手作り作品 ②食品系 りんごのたくさんはいったアップルパイ20cm りんごの収穫も当園で行なっています。 今年のりんごもとても美味しい出来上がりになりました。 そのまま食べても美味しいりんごですが 今回はお店でも販売をしているアップルパイが期間限定で登場しました! コーヒーにも紅茶にも合う仕上がりとなっています。 贈り物としても選ばれるおすすめの商品です。 ぜひこの機会にいかがでしょうか。 ▼結果 なにもしないバージョン: コーヒー CutMix: 食べ物 今回2作品を創作してみましたがどちらもCutMixではうまく特徴を捉えることができているのではないかという結果となりました。 (4) 可視化 CutMixでは可視化も行うとおもしろい結果をみることができます。可視化では、そのmodelが これが答え! とだした際にどこを判断してそのような答えになったかを確認することができます。 例えば犬と猫をCutMixしたものに対して出力"犬"として入力の可視化をすると犬の部分のみが判断できているなどが可能です。 同様のことがテキストでもできないかと思い試してみました。上の2作品を合体します。形態素解析後なので文章として読みにくいのですが、以下の文章を使用しました。 青が①ぬいぐるみ系の文章で赤が②食品系の文章です。 りんご たくさん いっ アップルパイ cm りんご  ペンギン ぬいぐるみ よい 綿 詰め 具合 なんとも 言え ふも  なり そのまま 食べ 美味しい りんご 今回 店 販売 し いる アップルパイ 期間 限定 登場 し コーヒー 紅茶 合う 仕上がり なっ い 贈り物 選ば れる おすすめ 商品 ぜひ 機会 いかが 今回modelとしてCNNを使用していたため、grad-camを使用して可視化を行なってみました。可視化では、それぞれ ①手作り作品 ②食べ物 と出力した際に注目されていたwordを上位5つピックアップしてみます。 ▼①手作り作品 りんご たくさん いっ アップルパイ cm りんご ペンギン ぬいぐるみ よい 棉   詰め 具合  なんとも 言え  ふも なり  そのまま 食べ 美味しい りんご 今回 店 販売 し いる アップルパイ 期間 限定  登場  し コーヒー 紅茶 合う 仕上がり なっ い 贈り物 選ば れる おすすめ 商品 ぜひ 機会 いかが ▼②食べ物 りんご たくさん いっ アップルパイ cm りんご ペンギン ぬいぐるみ よい 綿 詰め 具合 なんとも 言え ふも なり そのまま 食べ 美味しい  りんご 今回 店  販売 し いる アップルパイ 期間 限定  登場  し コーヒー 紅茶 合う  仕上がり  なっ い 贈り物 選ば れる おすすめ 商品 ぜひ 機会 いかが ①手作り作品では綿を"詰め"るという部分に注目されていますが②食べ物といれると"りんご"に反応しています。 これらの結果からも、CutMixで学習ができているのではないかという結果となりました。 今回はデータも少なく、学習もepoch決め打ちでやっているので条件を変えていくことでCutMixの良さを引き出せる可能性もあるかもしれません。 まとめ 今回はテキストでのカテゴリ分類に対してCutMixを使用してみました。 様々な工夫をすることでテキストにCutMixを使用するということは効果がある可能性がわかりました。 明日はOwners Marketingの栗田さんとProduct Managementの藤井さんです!
アバター
この記事は、「 BASEアドベントカレンダー2019 」16日目の記事です。 devblog.thebase.in こんにちは。Owners Growthチームの宮川です。 BASEでは、ショップオーナーさんのことを「Owners(オーナーズ)」と呼んでおり、私たちオーナーズの成長を支援するチームはOwners Growthといいます(オーナーズと呼ぶことになった経緯は こちら )。 今回はOwners Growthで実際どのように支援しているか簡単にご紹介します。 目次 オーナーズとは? ネットショップ運営の構成要素 ショップ運営でやること オーナーズの成長を後押しする 情報提供手段 まとめ オーナーズとは? 一言でオーナーズと言っても、80万ショップの様々なオーナーズがいらっしゃいます。商品のカテゴリだとアパレル・インテリア・コスメ・食べ物など多岐に渡り、個人でモノづくりをされている方から、企業として運用されている場合も。 また、実店舗を持っているのか、ネットショップだけなのか。オリジナル商品なのか、セレクト商品なのかなど、ショップの特性は幅広いです。 すこし前の情報ですが、ネットショップ作成サービス「BASE」をお使いのオーナーズについての調査結果が下記に掲載されています。 「BASE」が初のオーナーズ調査を実施 – 個人・法人を問わずブランドを立ち上げる時代の流れが顕著に - https://binc.jp/press-room/news/press-release/pr_20190514 「BASE」のショップオーナーさんの中には、初めてネットショップを開設・運営される方もたくさんいらっしゃいます。また上述の調査でもあるように半分以上が個人で運営されており、ネットショップの運営を周囲に相談できる方も多くはないように思います。 Owners Growthではそんなオーナーズに、ショップ運営のパートナーとして寄り添い、ショップを継続的に運営していただくための支援ができればと思っています。 具体的には下記のような施策を通じて、ショップごとの運用プロセス最適化のサポートを行っております。 ショップの状況に応じた効果的なメール配信 ネットショップ作成サービス「BASE」のショップオーナーさんが使う管理画面におけるアドバイス機能の提供 オウンドメディア( BASE U )におけるショップ運営・販促ノウハウの紹介 ネットショップ運営の構成要素 はじめに、ネットショップの運営はどのような要素でなりたっているか確認しましょう。 訪問者数 ネットショップに来ていただいているお客様のことです。「新規」と「リピーター」にわけることができます。“一見さん”で来ていただいたお客様を、いかに“常連さん”として再訪問していただくかが重要となります。 購入率 来ていただいたお客様のうち、実際何名に購入されたかの割合です。ここは、「ショップデザイン」「商品画像」「決済方法」と他にもたくさんの要素にわけられます。 購入単価 「商品価格」「まとめ買い」といった、お客様の合計注文額となります。 したがって、ショップ運営はこれらの要素を最適化する必要があります。 ショップ運営でやること それでは、ショップの成長支援をするためには何をすれば良いか。さきほどの構成をもとにして、運営に必要な内容を一部ですがまとめてみました。 訪問者数は「集客」として、「SNS(Instagram・Twitter・YouTubeなど)」「SEO」「広告」などを活用して獲得することできます。 一方、購入率は「ショップ運営」としての「ショップデザイン」「商品画像」「決済方法」などが重要な内容となります。 ショップデザインであれば、訪問していただいたお客様にとってショッピングしやすく、楽しくワクワクするようなデザインになっているのか。また、商品画像は商品の魅力が伝わるような分かりやすい画像になっているのか。というように、各項目ごとの運営内容を検討していきます。 オーナーズの成長を後押しする 上述のように、ショップ運営の成長要素は幾つもあり、Owners Growthではこれらを適切に実施してもらうための情報を、下記のような手段で提供しています。 情報提供手段 「BASE」のショップオーナーさんが使う管理画面の「お知らせ」 メール BASE Creator アプリ(ショップ管理を行えるアプリ)のプッシュ通知 SNS(Instagram・Twitter・Facebook) 以上の4つがあり、下記のような目的で活用しております。 「管理画面」「メール」「プッシュ通知」はショップのカテゴリ・売上・「BASE」の拡張機能であるAppsの活用状況など、ショップのセグメントごとに配信内容を変更できるため、ショップのステータスにあった細かくより深い情報を送ることが可能です。そのため、実行して欲しいアクションをうながすために効果的です。ただし、この3つの中でも、セグメント条件や配信内容によって効果はさまざまあります。 一方、SNSでは配信先をしぼることはできない(Facebookは年齢や地域などで絞ることは可能)かつ、オーナーズ以外のユーザーもいるため、イベント情報や新機能リリースなどライトな情報を送るのに適しています。また、お困りごとや機能に関するフィードバックなどをコメントでいただく可能性があるので、オーナーズとのコミュニケーションの場として有効です。 実際の運用方法としてそれぞれ異なる部分はありますが、基本的には下記のような流れで進めています。 仮説立て 例えば、Instagramのショッピング機能を使ったショップの売上は訪問者数によって変わるのか、等 仮説検証のためのデータ抽出 これを、Re:dash・Google Analytics(Google スプレッドシートにアドオンを入れて連携)などの分析ツールを用いてデータを抽出 施策の立案 仮説が合っていれば、施策への落とし込みを行う この場合、「Instagramのショッピング機能を使い、訪問数が◯◯数以上のショップだと、それ以外にショップに比べると◯◯%売上が高い」というデータが見つかれば、これにもとづく施策を立案 特にデータの変化が見えなければ、別の仮説を検討 施策の実行 この仮説を実証するために、「Instagramのショッピング機能」を利用していないショップには利用を促し、すでに利用しているショップには訪問者数を上げるための施策を提供 効果検証 施策を実施後、実際に効果があったか上述の分析ツールを使って検証し、改善 ちなみに、「管理画面」「メール」「プッシュ通知」のうち、施策によらず高い開封率をあげているのが「メール」です。世間では「古いツール」「開封率が低い」というイメージが先行しがちなメールですが、オーナーズ向けの施策としては効果的な結果が出ています。 最初に、オーナーズは十人十色という内容を記載いたしましたが、本当にショップの運営状況はさまざまです。 ショップがどのような設定内容で何を登録しているかやどういう行動をしているのかなど、多様なデータと照らし合わせる定量分析はもちろん、ときには定性分析も用いながら最適な情報を適切なタイミングで提供する必要があります。 情報が決まれば適切なツールは、メールなのか管理画面内でのお知らせなのか、それともBASE Creator アプリのプッシュ通知なのか、なども決めなければいけません。 「オーナーズが何に困っているかを見つけてそれを解決したい!」という一心で日々の業務に励んでおります。 まとめ 80万ショップの支援を通じて、これまで売上がなかったショップがはじめて売上ができたり、毎月コンスタントに売上があがっていなかったショップが毎月売上が上がるようになったり、とオーナーズのショップ運営を後押しできたときはすごく嬉しいので、大変やりがいのあるお仕事だと感じています。 “ネットショップは顔が見えない”とは良く言われますが、日々膨大な定量データとアンケートなどの定性データの両面と向き合いながら、オーナーズの顔を想像して日々業務に取り組んでいます。 80万ショップの成長を支援するという、他にはないユニークな業務に少しでも興味を持ってくださった方がいれば、 採用ページ で会社の雰囲気や実際はたらいているメンバーの様子などを見ていただければと思います。 オーナーズの未来を一緒に支えていきませんか? 明日は、Platform Devマネージャーの大窪さんとData Strategy所属の杉さんです!
アバター
この記事は BASE Advent Calendar 2019 の16日目の記事です。 devblog.thebase.in エンジニアの田中( @tenkoma )です。 あなたのマシンにインストールされているPHPのバージョンは何ですか? 仮想マシンやコンテナで開発環境を作ることが増えているので、ホストOSにはPHPが入ってない・気に掛けたことがない、ということも多いかもしれません。 僕は、新しいバージョンを試すために php-build を使ってmacOSでビルド・インストールしています。(また、プロジェクト毎にバージョンの切り替えがしやすいよう direnv を使っています) 今回はphp-buildを使った複数バージョンビルドを、コードを書いて少し省力化してみたので紹介します。 多くのバージョンのPHPをそろえてみました。ただし、Catalinaでは7.0.19未満の動作が実現できていません 前提 この記事で紹介するコードは以下の環境で実行しています。 OS: macOS 10.15 Catalina 依存ライブラリのインストールはHomebrew php-buildは motemen/ghq でローカル環境にclone php-buildの導入・トラブルシュートは以下の記事が参考になります。 複数バージョンの PHP をインストールして使う - OTOBANK Engineering Blog Macのphpenv(php-build)でビルドしようとしたら出るエラーと解決まとめ - Qiita 作った理由 php-buildを使うと、自分でソースコードをダウンロードしてビルドするよりは楽に、ビルド・インストールができます。(ただし、ビルドエラー時に必要な依存ライブラリについて調査したりするので、導入時にある程度の知識や調査の時間が必要です) 例えば、以下のようなコマンドでビルド+インストールします。 $ php-build -i development 7 . 3 . 12 ~/ local /php/ 7 . 3 . 12 / これでインストールできたらめでたいのですが、macOS をアップグレードしていくと、なぜか依存ライブラリが見つからなくなるようになってきたので、ビルドのためのオプションを付けて以下のように実行しています。(macOS Catalina 10.15.1にて実行) $ PHP_BUILD_CONFIGURE_OPTS = " --with-zlib-dir= $( brew --prefix zlib ) --with-bz2= $( brew --prefix bzip2 ) --with-iconv= $( brew --prefix libiconv ) --with-libedit= $( brew --prefix libedit ) --with-openssl= $( brew --prefix openssl ) --with-libxml-dir= $( brew --prefix libxml2 ) --with-curl= $( brew --prefix curl ) --without-tidy " YACC = $( brew --prefix bison ) /bin/bison PHP_BUILD_EXTRA_MAKE_ARGUMENTS =-j4 php-build -i development 7 . 3 . 12 ~/ local /php/ 7 . 3 . 12 / さて、PHP の新しいバージョン(ポイントリリース)はだいたい1〜2ヶ月に1度リリースされているようですが、このとき、7.3と7.2と7.1の新しいバージョンがほぼ同時にリリースされるという感じなので、そのたびに以下のようなコマンドを実行することになります。 $ ghq look php-build $ git pull $ ./install.sh $ exit $ export PHP_BUILD_CONFIGURE_OPTS= " --with-zlib-dir= $( brew --prefix zlib ) --with-bz2= $( brew --prefix bzip2 ) --with-iconv= $( brew --prefix libiconv ) --with-libedit= $( brew --prefix libedit ) --with-openssl= $( brew --prefix openssl ) --with-libxml-dir= $( brew --prefix libxml2 ) --with-curl= $( brew --prefix curl ) --without-tidy " $ export YACC= $( brew --prefix bison ) /bin/bison $ export PHP_BUILD_EXTRA_MAKE_ARGUMENTS= -j4 $ php-build -i development 7 . 3 . 12 ~/ local /php/ 7 . 3 . 12 / $ php-build -i development 7 . 2 . 25 ~/ local /php/ 7 . 2 . 25 / $ php-build -i development 7 . 1 . 33 ~/ local /php/ 7 . 1 . 33 / php-build コマンドを打つこと自体は対して大変ではありませんが、マイナーバージョンごとの最新バージョン番号を確認するのが面倒ですし、自動化出来そうだったのでやってみました。 複数のPHPバージョンをビルドするスクリプト 以下のスクリプトを作りました。 php-build-auto.sh #!/usr/bin/env bash function usage_exit() { echo " Usage: $0 [OPTIONS] <version1> [<version2> [...]] " echo echo " Options: " echo " -h, --help " echo " --parallel num (default: CPU physical core number) " echo " --install-root-path path (default: \$ HOME/src/local/php " echo " --override " echo " --show-versions " echo exit 1 } # option defalut PARALLEL = $( sysctl -n hw.physicalcpu_max ) INSTALL_ROOT_PATH = " $HOME /local/php " OVERRIDE = false SHOW_VERSIONS = false param = () for OPT in " $@ " do case $OPT in -h | --help ) usage_exit exit 1 ;; --parallel ) PARALLEL = $2 shift 2 ;; --install-root-path ) INSTALL_ROOT_PATH = $2 shift 2 ;; --override ) OVERRIDE = true shift 1 ;; --show-versions ) SHOW_VERSIONS = true shift 1 ;; * ) if [[ -n " $1 " ]] && [[ ! " $1 " =~ ^-+ ]] ; then param+ = ( " $1 " ) shift 1 fi ;; esac done if [ -p /dev/stdin ]; then IFS =$' \n ' for line in $( cat - ) do param+ = ( " $line " ) done fi BUILD_VERSIONS = () SKIP_VERSIONS = () if [ $OVERRIDE = true ]; then BUILD_VERSIONS = $param else for VERSION in " ${param[ @ ]} " ; do if [ -e " $INSTALL_ROOT_PATH / $VERSION /bin/php " ]; then SKIP_VERSIONS+ = ($VERSION) else BUILD_VERSIONS+ = ($VERSION) fi done fi echo " skip versions: " echo " ${SKIP_VERSIONS[ @ ]} " echo " build versions: " echo " ${BUILD_VERSIONS[ @ ]} " if [ $SHOW_VERSIONS = true ]; then exit 0 fi export PHP_BUILD_CONFIGURE_OPTS= " --with-zlib-dir= $( brew --prefix zlib ) --with-bz2= $( brew --prefix bzip2 ) --with-iconv= $( brew --prefix libiconv ) --with-libedit= $( brew --prefix libedit ) --with-openssl= $( brew --prefix openssl ) --with-libxml-dir= $( brew --prefix libxml2 ) --with-curl= $( brew --prefix curl ) --without-tidy " export YACC= " $( brew --prefix bison ) /bin/bison " export " PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j $PARALLEL " echo " ${BUILD_VERSIONS[ @ ]} " | xargs -n 1 -t -I@ php-build -i development @ " $INSTALL_ROOT_PATH " /@/ 使い方ですが、引数でPHPバージョンを指定すると、まとめてビルドしてくれます。 $ ./php-build-auto.sh 7 . 0 . 33 7 . 1 . 33 7 . 3 . 12 skip versions: 7 . 0 . 33 build versions: 7 . 1 . 33 7 . 3 . 12 [ Info ] : Loaded extension plugin [ Info ] : Loaded apc Plugin. ( 以下略 ) すでにインストール済みのバージョンがあれば、ビルドはスキップされます。もし再ビルドしたい場合は --override オプションを付けます。 $ ./php-build-auto.sh --override 7 . 0 . 33 7 . 1 . 33 7 . 3 . 12 このスクリプトですが、作りはじめたときは xargs -P を使って php-build コマンドを並列実行させるのが最大の特徴でした。しかし、PHPビルド後のXdebugビルドは、同じディレクトリで実行されるので、複数のXdebugビルドを1ディレクトリで同時にやってしまい、エラーになってしまったのでその機能を削除しています。 マイナーバージョン毎の最新バージョン番号を列挙する php-build-auto.sh は、新しいポイントリリースが出たときにまとめてビルドしたいときに使います。そこで、最新のポイントリリースバージョンを列挙するスクリプトを別途作りました。 listversion.php #!/usr/bin/env php <?php declare ( strict_types = 1 ) ; /** * usage: php listversion.php [--filter stable|minor-head] [--oldest-version version] [--definitions-path path] */ class PhpVersion { const VERSION_PATTERN = '/(?P<major>\d+)\.(?P<minor>\d+)\.(?P<point>\d+)/' ; public static function getMinorVersion ( string $ version ) { preg_match ( self :: VERSION_PATTERN, $ version , $ matches ) ; return sprintf ( '%s.%s' , $ matches [ 'major' ] , $ matches [ 'minor' ]) ; } public static function isStable ( string $ version ) { return preg_match ( self :: VERSION_PATTERN, $ version ) === 1 ; } } $ argvOptions = getopt ( '' , [ 'filter:' , 'oldest-version:' , 'definitions-path:' ]) ; $ options = [ 'filter' => !empty ( $ argvOptions [ 'filter' ]) ? $ argvOptions [ 'filter' ] : 'minor-head' , 'oldest_version' => !empty ( $ argvOptions [ 'oldest-version' ]) ? $ argvOptions [ 'oldest-version' ] : '5.6.0' , 'definitions_path' => !empty ( $ argvOptions [ 'definitions-path' ]) ? $ argvOptions [ 'definitions-path' ] : '/usr/local/share/php-build/definitions/' , ] ; $ definitionsIter = new DirectoryIterator ( $ options [ 'definitions_path' ]) ; $ versions = [] ; foreach ( $ definitionsIter as $ definition ) { if ( $ definition -> isDot ()) { continue ; } $ version = $ definition -> getFilename () ; if ( ! PhpVersion :: isStable ( $ version )) { continue ; } if ( version_compare ( $ version , $ options [ 'oldest_version' ] , '<' )) { continue ; } if ( $ options [ 'filter' ] === 'minor-head' ) { $ minorVersion = PhpVersion :: getMinorVersion ( $ version ) ; if ( !isset ( $ versions [ $ minorVersion ]) || version_compare ( $ version , $ versions [ $ minorVersion ] , '>' )) { $ versions [ $ minorVersion ] = $ version ; } } else { $ versions [] = $ version ; } } echo implode ( " \n " , $ versions ) . PHP_EOL; 実行すると、各マイナーバージョン毎の最新バージョンを列挙します。 $ ./listversion.php 7 . 2 . 25 7 . 3 . 12 7 . 1 . 33 7 . 0 . 33 僕の環境だと 7.0.19 未満のバージョンはビルドエラーになったので、列挙しないようにしています。列挙したい場合は --oldest-version オプションを使います。 $ ./listversion.php --oldest-version=5.3 7 . 2 . 25 5 . 4 . 45 7 . 3 . 12 7 . 1 . 33 5 . 3 . 29 5 . 5 . 38 7 . 0 . 33 5 . 6 . 40 php-build-auto.sh は引数と標準入力の両方でビルド対象を指定できるので、 listversion.php と組み合わせて、「マイナーバージョン毎の最新バージョンをまとめてビルド」、が実現できます。 $ ./listversion.php | ./php-build-auto.sh skip versions: 7 . 1 . 33 7 . 0 . 33 build versions: 7 . 2 . 25 7 . 3 . 12 php-build -i development 7 . 2 . 25 /Users/kojitanaka/ local /php/ 7 . 2 . 25 / [ Info ] : Loaded extension plugin [ Info ] : Loaded apc Plugin. ( 以下略 ) 2つのスクリプトを作った結果、PHPのバージョンアップ時の作業は以下のように単純化できました。 $ ghq look php-build $ git pull $ ./install.sh $ exit $ cd ~/src/github.com/tenkoma/php-build-tools $ ./listversion.php | ./php-build-auto.sh 今回実装したスクリプトは tenkoma/php-build-tools にて公開しています。 まとめ 複数のPHPバージョンを手元にそろえるときに使えるphp-buildの運用を多少楽にするためにコードを書き、バージョンアップ時に考える要素を減らしました。最初やりたかった複数バージョンの並列ビルドはできていません。 また、Homebrew でライブラリをアップグレードすると、php-build でビルドしたPHPが動作しなくなることもあります。2年前は5.3〜7.1まで揃えられましたが、Catalinaではまだ5.5, 5.6 のビルドができていません。 最新のmacOSで古いPHPを動かしにくくなってきている気がするので、PHPの古いバージョンを揃えたい場合は、 phpallのDocker版を作ってみた話 - hamacoの日記 のように、Docker を使った方がいいかもしれません。 明日はPlatform Devマネージャーの大窪さんとData Strategyの杉さんです。
アバター
この記事はBASE Advent Calendar 2019の15日目の記事です。 devblog.thebase.in DataStrategyの齋藤( @pigooosuke )が担当します。 ONNXの概要 Open Neural Network Exchange(ONNX)とは、機械学習モデルを表現するフォーマット形式のことです。ONNXを活用すると、PyTorch, Tensorflow, Scikit-learnなどの各種フレームワークで学習したモデルを別のフレームワークで読み込めるようになり、学習済みモデルの管理/運用が楽になります。今回の記事では、よく利用されているLightGBMモデルからONNXへの出力方法の確認と、ONNXの推論を行う実行エンジンであるONNX Runtime上での推論速度の改善がどれほどなのかを検証していきたいと思います。 https://onnx.ai 学習モデルの用意 今回は、KaggleのTitanicデータを使用して、binary classificationの予測モデルを作成します。 Dataset: https://www.kaggle.com/c/titanic import pandas as pd from sklearn.model_selection import train_test_split import lightgbm as lgb data = pd.read_csv( "path/train.csv" ) y = data[ 'Survived' ] X = data.drop([ 'Survived' , 'PassengerId' , 'Name' , 'Ticket' , 'Cabin' ], axis= 1 ) # カテゴリー変数をbooleanに展開 # 現在、LightGBMのカテゴリー変数を直接ONNXに変換することが出来ないため category_cols= X.select_dtypes( 'O' ).columns.tolist() X = pd.get_dummies(X, columns=category_cols, drop_first= True , dtype= bool ) X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size= 0.1 , random_state= 2019 ) # training train_data = lgb.Dataset(X_train, label=y_train) valid_data = lgb.Dataset(X_valid, label=y_valid) train_params = { 'task' : 'train' , 'boosting_type' : 'gbdt' , 'objective' : 'binary' , 'num_leaves' : 28 , 'learning_rate' : 0.01 , 'verbose' : 0 , } gbm = lgb.train( train_set=train_data, params=train_params, num_boost_round= 1000 , valid_sets=[train_data, valid_data], early_stopping_rounds= 10 , verbose_eval= 10 ) # Training until validation scores don't improve for 10 rounds # [10] training's binary_logloss: 0.625936 valid_1's binary_logloss: 0.582429 # [20] training's binary_logloss: 0.588612 valid_1's binary_logloss: 0.550251 # ... # [240] training's binary_logloss: 0.331639 valid_1's binary_logloss: 0.346977 # [250] training's binary_logloss: 0.327381 valid_1's binary_logloss: 0.346515 # Early stopping, best iteration is: # [248] training's binary_logloss: 0.328348 valid_1's binary_logloss: 0.346271 かなり雑ですが、モデルの用意が出来ました。 # 型の確認 X.info() # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 891 entries, 0 to 890 # Data columns (total 8 columns): # Pclass 891 non-null int64 # Age 714 non-null float64 # SibSp 891 non-null int64 # Parch 891 non-null int64 # Fare 891 non-null float64 # Sex_male 891 non-null bool # Embarked_Q 891 non-null bool # Embarked_S 891 non-null bool # dtypes: bool(3), float64(2), int64(3) # memory usage: 37.5 KB # データの確認 X.head() # Pclass Age SibSp Parch Fare Sex_male Embarked_Q Embarked_S # 0 3 22.0 1 0 7.2500 True False True # 1 1 38.0 1 0 71.2833 False False False # 2 3 26.0 0 0 7.9250 False False True # 3 1 35.0 1 0 53.1000 False False True # 4 3 35.0 0 0 8.0500 True False True ONNX変換 ONNXに変換するためには、事前にinputの型を定義する必要があります。 用意されている型は以下の通りです。 整数型: Int32TensorType, Int64TensorType 真偽型: BooleanTensorType 浮動小数数型: FloatTensorType, DoubleTensorType 文字列型: StringTensorType 辞書型: DictionaryType 配列型: SequenceType 今回は、全てnumpyのfloat32でinputを受け付けるようにします。 この設定は活用している学習モデルなどによって変わってきます。 例えば、scikit-learnのPipelineを活用して、テキスト入力をtfidfで変換する処理などを含めてONNX化したい場合は、 inputにStringTensorTypeを設定する必要があります。 参考URL: http://onnx.ai/sklearn-onnx/auto_examples/plot_tfidfvectorizer.html#tfidfvectorizer-with-onnx LightGBMをONNXに変換するために onnxmltools が必要になるので、事前にライブラリをインストールします。 https://github.com/onnx/onnxmltools import onnxmltools from onnxmltools.convert.common.data_types import FloatTensorType, BooleanTensorType, Int32TensorType, DoubleTensorType, Int64TensorType # 入力の型定義 initial_types = [[ 'inputs' , FloatTensorType([ None , len (X.columns)])]] # LightGBM to ONNX onnx_model = onnxmltools.convert_lightgbm(gbm, initial_types=initial_types) # save onnxmltools.utils.save_model(onnx_model, "lgb.onnx" ) # モデルをvizualize可能 onnxmltools.utils.visualize_model(onnx_model) inputs は、入力のラベル名です。 入力のshapeは [None, 特徴量数] のFloatTensorを指定しています。 参考までに、LightGBMのclassifierのモデルは下図のような構成になっています。(visualize_modelで生成) 入力値を決定木を通じて、予測ラベルと予測確度を出力しています。 推論 ONNX用の実行環境として、Microsoftが出しているonnxruntimeを使います。 こちらもインストールします。 https://github.com/microsoft/onnxruntime import onnxruntime session = onnxruntime.InferenceSession( "lgb.onnx" ) # 入力のラベル名の確認 print ( "input:" ) for session_input in session.get_inputs(): print (session_input.name, session_input.shape) # 出力のラベル名の確認 print ( "output:" ) for session_output in session.get_outputs(): print (session_output.name, session_output.shape) # vizualizeした図と一致 # input: # inputs [None, 8] # output: # label [None] # probabilities [] # 推論実行 preds = session.run([ "probabilities" ], { "inputs" : X_train.values[ 0 ].astype( "float32" ).reshape( 1 , - 1 )}) print (preds) # [[{0: 0.0961046814918518, 1: 0.9038953185081482}]] # LightGBMの予測 preds = gbm.predict(X_train.values[ 0 ].reshape( 1 , - 1 )) print (preds) # array([0.90389532]) 第1引数に出力ラベル名(今回はprobabilitiesのみを出力)。 第2引数に入力ラベル名と値をセットして推論を実行します。 予測結果もLightGBMの予測とONNXの予測がちゃんと一致していました。 速度計測 # onnx %%timeit -r 30 for v in X_train.values: pred = session.run([ "probabilities" ], { "inputs" : v.astype( "float32" ).reshape( 1 , - 1 )}) # 43.3 ms ± 7.86 ms per loop (mean ± std. dev. of 30 runs, 10 loops each) # lightgbm %%timeit -r 30 for v in X_train.values: pred = gbm.predict(v.reshape( 1 , - 1 )) # 84.4 ms ± 8.96 ms per loop (mean ± std. dev. of 30 runs, 10 loops each) MacOS 10.14.6 Intel Core i5 3.1 GHz python=3.7.3 numpy=1.15.2 lightgbm=2.3.1 onnx=1.6.0 onnxconverter-common=1.6.0 onnxmltools=1.6.0 onnxruntime=1.0.0 上記の条件で計測したところ、ONNXモデルはpureなLightGBMに比べて約半分ほどの時間で推論が出来ているのが確認できました。 ONNXは途中で型変換を入れているので厳密に平等な比較とは言えませんが、それでも十分早かったです。 モデルファイルサイズ計測 import pickle with open ( "lgb.pkl" , "wb" ) as f: pickle.dump(gbm, f, protocol=pickle.HIGHEST_PROTOCOL) !du -h lgb.pkl # 740K lgb.pkl !du -h lgb.onnx # 500K lgb.onnx モデルファイルサイズに関しても、pickleでの圧縮に比べ、68%まで軽量化することが出来ました。 今回は、LightGBMでの手順を確認しましたが、 https://github.com/onnx では、各種フレームワークの対応が次々に進んでいます。 独自カスタムした計算をしていない限り対応出来ると思うので、学習モデル運用でONNXを検討してみてはいかがでしょうか。 まとめ 今回、LightGBMのモデルからONNX形式でモデル出力をする手順の紹介と、ONNX上での推論速度の検証を行いました。 ONNXを利用することで学習フレームワークに依存せず、高速な推論ができる環境を作ることが出来そうですね。 明日は基盤グループの id:tenkoma さんとOwners Growthの id:MiyaMasa です!お楽しみに!
アバター
この記事はBASE Advent Calendar 2019の15日目の記事です。 こんにちは。フロントエンドグループの加藤です。 私達は、「 Payment to the People,Power to the People. 」というミッションを掲げ、日々サービスづくりを頑張っています。 Peopleとは誰か このミッションにある、 People とは誰のことを指すのでしょうか? 自分の周りの環境を想像しても、実に多様な人がいることがわかります。 また、日々ショップオーナーさんや購入者さんからいただく様々なお問い合わせの内容を見ていると、ほんとに様々な背景を持った方々に使っていただいているんだなと思います。 Webフロントエンド開発者としては、自分の力で出来ることがあれば、出来る限り多様な使われ方に対応できるプロダクトにしていきたいという思いがあります。 何を指針とするか では、まず何をどうすればいいのでしょうか。よくわかりません。 調べると、どうやらWeb技術の標準化を行う非営利団体であるW3C(World Wide Web Consortium)が勧告しているガイドラインが存在するようです。 Web Content Accessibility Guidelines (WCAG) 2.0 また、WCAG2.0に関しては実際に対応する時に参考にできる解説書もありました。どちらも日本語化されています。大変ありがたいです。 WCAG 2.0 解説書 細かく言うと、この項目をどれだけ対応するかによってレベルA~AAAなどのレベル付けがあるようです。今回は絶対どのレベルを厳守するんだ!というよりも、自分の中で実装時に意識すべきことを掴みたい、まずは慣れたい、といった動機で始めているので、まずはAの中でも対応できそうなものからやってみたいと思います。 練習してみよう ということで、実際に弊社でも利用しているフレームワーク、Vue.jsを使って、かんたんなウェブページをよりアクセシブルにしていく素振りをしてみます。 まずは何も考えずに作っていく 作るものはなんでもよいので、「自由に投稿できる動物ずかん」をイメージして作ります。一覧画面と詳細ページ、そしてモーダルで開いて入力する画面があるとしましょう。 できました。 CodeSandboxのリンク 特筆すべき点はないですが、 モーダルとボタンをそれぞれ共通のコンポーネントとして切り出した vue-routerを利用してクライアントで一覧と詳細画面をそれぞれルーティングさせている というところで、非常に簡単ではありますが実際のフロントエンド開発でよくあるシーンを再現してみました。(今回はVue.jsやその他のライブラリの詳しい説明に関しては省きます) 課題を発見していこう とにかく動くものを作ったのですが、そもそも課題が何なのかわかっていません。 今回はWCAG2.0をバイブルとして進めていくので、一つ一つ目を通して、これは守れてないなと思ったものを地道にクリアしていくことにしましょう。 原則 1: 知覚可能 - 情報及びユーザインタフェース コンポーネントは、利用者が知覚できる方法で利用者に提示可能でなければならない。 1.1.1 非テキストコンテンツ: 利用者に提示されるすべての 非テキストコンテンツ には、同等の目的を果たす テキストによる代替 が提供されている。 ただし、次の場合は除く (!) コントロール、入力: 非テキストコンテンツが、コントロール又は利用者の入力を受け付けるものであるとき、その目的を説明する 名前 (name) を提供している。 これは、ひとまず作ったボタンコンポーネントに問題があります。 <template> <div @click="$emit('click')" class="button"> <slot></slot> </div> </template> divですね。 コントロール又は利用者の入力を受け付けるもの だと全く伝わりません。初歩的ですが、気を抜くと似たようなことはよくやってしまいます。 この項目に リンクされている解説書の項目 では、 アクセシブルなウェブコンテンツ技術の標準コントロールを使用する場合、このプロセスは簡単である。ユーザインタフェース要素が仕様に準じて使用される場合、この条件に条項は満たされる。 とされており、セマンティックなマークアップを守れば特別な工夫をせずとも条件を満たすことができるようです。以下のようにしてみました。 <template> <button @click="$emit('click')" class="button"> <slot></slot> </button> </template> よさそう。しかしこれもまだ問題があります。 これはアクセシビリティの問題ではなく、このボタンというコンポーネントは、機能としてのボタンを切り出したいのではなく、単なるプレゼンテーションとしてのボタンぽい見た目をただ切り出したいのです。ボタンの見た目を提供するのに、buttonタグとしてしか使えないと、使われる文脈によっては正しくないマークアップを強制してしまう可能性があります。 ここは、デフォルトはbuttonタグで、必要に応じてprops経由でタグを指定出来るようにしてみましょう。こういったケース(ルートエレメントを動的に変えたい)は、jsxで書くことで実現できます。詳しくは、 弊社松原のVue.js+JSX基本文法最速入門 という記事を見ていただくとよく理解できるかと思います。 export default { props: { tag: { type: String, default: "button" } }, render(h, context) { const tag = this.$props.tag; return ( <tag {...this.$attrs} class="button" onClick={() => this.$emit("click")}> {this.$slots.default} </tag> ); } }; こうすることで、以下のようにaタグをボタンにしたい場合でも対応することが可能になりました。 <custom-button tag="a" href="/hoge" /> また、実はもう一つ課題があります(インタラクティブな要素の実装は本当に大変ですね)。 よくあるケースなのですが、マウスでクリックしたあと、その要素がフォーカスされるので、フォーカスインジケータが表示されるのですが、これをポインティングデバイスによるフォーカスでは出さないようにしたいのです。 しかし、これを素朴に .button:focus{outline: none;} としてしまうと、ポインティングデバイス以外の入力によってフォーカスされた場合、視覚でそのことを伝えることができません。先程の達成基準 4.1.2 の解説にも 特に重要なユーザインタフェース コントロールの状態は、フォーカスを持つかどうかである。 とありますが、マウス以外の入力では、フォーカスされた状態をユーザーに伝えることは非常に重要そうです。 幸いにも、CSSの *:focus-visible 疑似クラスによってこれは達成できます。これは、ユーザーエージェントが要素にフォーカスを明示するべきであるとした場合にのみスタイルを適用することが出来る便利な擬似クラスです。しかしながら、まだこれは草案の段階であり、ほとんどのブラウザで実装されていませんので、今回はpolyfill( focus-visible )を導入し、該当のフォーカス時にのみ要素に適用されるdata属性に対してフォーカスのスタイルを当ててみましょう。 .button:focus { outline: none; } .button[data-focus-visible-added] { outline: 2px solid #000; } これでbuttonコンポーネントは一旦大丈夫そうです。 tableタグ captionをつける 今回、動物の一覧を並べるのにtableタグを使用しました。CSSの表現力が高まるにつれて使う機会が徐々に減ってはいますが、管理画面などで一覧性を担保しながら要素を表に並べるという用途では未だによく使うタグでもあります。 今回は以下の達成方法にある、caption要素を使用するとより何の表なのかがわかりやすいのではないかと思いました。 H39: データテーブルのキャプションとデータテーブルを関連付けるために、caption 要素を使用する | WCAG 2.0 達成方法集 行全体をクリッカブルにする <tbody> <tr :key="animal.id" v-for="animal in animals" @click="onRowClick(animal.id)"> <td>{{ animal.name }}</td> <td>{{ animal.emoji }}</td> </tr> </tbody> 行全体をクリッカブルにして、押されたらその行を詳しく見る/操作できる詳細画面が開く、ようなアプリケーションはよくあると思いますが、今回もそうしてみました。ただ、trのクリックイベントでページ遷移させていて、先ほどのようにセマンティックなマークアップでないがために、押せることもわからないし、キーボードで操作することもできません。困った。 一度、全体をクリッカブルにしたい!というところから一歩引いて、そもそもこれは何ができればいいのかを考えてみます。詳細ページに飛ばしたいんですよね。であれば、aタグでマークアップされるべきです。ただ今回はtableでマークアップしていて、trをaタグで囲うことができません。ただ、cellの中には当然aタグを置くことはできます。という発想から、以下のようにしてみました。 <table class="table"> <caption>現在登録されている動物の一覧</caption> <thead> <tr> <td>名前</td> <td>emoji</td> <td class="util-hidden">リンク</td> </tr> </thead> <tbody> <tr class="row" :key="animal.id" v-for="animal in animals" @click="onRowClick(animal.id)"> <td>{{ animal.name }}</td> <td>{{ animal.emoji }}</td> <td class="util-hidden"> <router-link :to="{name: 'detail', params: { id: animal.id }}">{{ animal.name }}の詳細を見る</router-link> </td> </tr> </tbody> </table> export default { // ... methods: { onRowClick: function(id) { this.$router.push({ name: "detail", params: { id } }); } } }; .row { cursor: pointer; } .row:hover { background-color: #ddd; } .row:focus-within { background-color: #ddd; } .util-hidden { position: absolute !important; clip: rect(1px, 1px, 1px, 1px); } 思い切って、テキストリンクを配置した列を新たに追加しました。ただ、その列を表示上は非表示にします。( .util-hidden という名前のユーティリティクラスを付与していますが、これはアクセシビリティの対応でよく使われるCSSです。スクリーンリーダーなどの支援技術で利用してもらいたいのですが、視覚上からは非表示にしたほうが都合が良いケースで使われます。) これにより、リンクにキーボードでフォーカスすること自体は可能になりました。その際に視覚上で行全体のフォーカスを表現するため、focus-within疑似クラスを使用します。これは、内包する要素がフォーカスされていた場合にスタイルを適用することが出来る便利な擬似クラスです(これもfocus-visibleと同じように全てのブラウザで対応しているわけではないので、 focus-within-polyfill を利用しています)。 trタグにfocus-withinで内包する要素がフォーカスされている場合にスタイルを適用することにより、キーボードでも現在フォーカスしている行を視認しながら移動することが可能になりました。 最後に、マウスでクリックできることを表現するために、hover擬似クラスでカーソルをpointerに設定し、trがクリックされたら、詳細画面へのルーティングを実行するようにします。 こんな感じでしょうか…? モーダルダイアログ モーダルはWCAGを見る前から何かあるだろうな…と思いましたが非常に問題が多そうです。ただ、モーダルをアクセシブルに作る方法はある程度約束事が決まっており、少し前ですが ヤフー株式会社の福本さんによるスライド がわかりやすく非常に参考になります。 端的に言えば以下の対応になります。 マシンリーダブルなコードにするため、WAI-ARIA属性によりマークアップの情報を増やす キーボード操作に対応する フォーカストラップを実装する escキーで閉じれるようにする 開いた際に最初のインタラクティブな要素にフォーカスを移す 閉じた際に元々フォーカスしていた要素にフォーカスを戻す まるっと勢いで実装してみます。 マシンリーダブルなコードにするため、WAI-ARIA属性によりマークアップの情報を増やす <div role="dialog" aria-modal="true" :aria-labelledby="titleId" :data-show="`${show}`" @click="$emit('cancel')" class="wrapper" > </div> --- props: { titleId: String, rootId: String }, methods: { onShow: function() { this.rootId && document.getElementById(this.rootId).setAttribute('aria-hidden', true); }, onHide: function() { this.rootId && document.getElementById(this.rootId).setAttribute('aria-hidden', false); }, role属性によりダイアログであることと、aria-modal属性により現在のダイアログの下にあるウィンドウは不活性であることを支援技術に伝えます。 また、モーダルダイアログのタイトルとなる要素のidと、本文のコンテンツをラップしている要素のidをprops経由で渡します。前者はaria-labelledbyでモーダルコンテンツのタイトルを伝え、後者はモーダルを開いている際に、裏側のメインコンテンツが非表示になっていることを伝えるために使用します。 開いた際に最初のインタラクティブな要素にフォーカスを移す 今回は、props経由で最初にフォーカスすべき要素のidを受け取るシンプルな作りにしました。 props: { //... initialFocus: String }, methods: { onShow: function() { this.initialFocus && document.getElementById(this.initialFocus).focus() }, //... ---- <modal initialFocus="animal-name" :show="modalShow" @cancel="closeModal"> 閉じた際にフォーカスを戻す data: function() { return { lastActiveElement: null }; }, watch: { show: function(next) { if (next === true) { this.onShow(); } else { this.onHide(); } } }, methods: { onShow: function() { this.lastActiveElement = document.activeElement; }, onHide: function() { this.lastActiveElement && this.lastActiveElement.focus(); }, フォーカストラップを実装する フォーカストラップとは、特にモーダルな状態においてそのコンテンツ内でフォーカスをループできるようにすることで、コンテンツ内での操作性を向上させるための機能です。 今回は実装についての詳細な説明を省きたいので、 vue-focus-lock というライブラリの力を借りました。モーダルのコンテンツを包むだけで上記の機能を実現してくれます。 <template> <div :data-show="`${show}`" @click="$emit('cancel')" class="wrapper"> <div class="contents" @click.stop> <focus-lock> <slot></slot> </focus-lock> </div> </div> </template> escキーで閉じれるようにする props: { escExit: { type: Boolean, default: true } }, methods: { onShow: function() { document.addEventListener("keydown", this.checkKeyDown); }, onHide: function() { document.removeEventListener("keydown", this.checkKeyDown); }, checkKeyDown: function(event) { if ( this.escExit && (event.key === "Escape" || event.key === "Esc" || event.keyCode === 27) ) { this.$emit("cancel"); } } } キーコードを愚直に見て、エスケープキーであれば親にキャンセルイベントをemitします。 完成 完成版のCodeSandboxのリンク 振り返って 率直に言えば、普段何気なく実装している機能でも、まだまだやれることがたくさんあったというのは技術者としては少しショックではありましたし、単純に時間がとてもかかったので、普段の開発でどのようにこういった取り組みを持続的に進めていくかは、深く考える必要があると感じました。 ただ、とても良いと感じたことが3つあります。 コンテンツやサービスの中身が何であるか/どうあるべきかを深く考えるきっかけとなる 実装の手法以前に、そもそもこのコンテンツはどういったユーザーがアクセスし、どういった特性があるのかなど、アクセシビリティのガイドラインを意識しよう/セマンティックなマークアップを実現しようと思うと、それを深く考えることを避けて通れなくなるように感じました。これはとてもよいことではないでしょうか。 結果的にどんな人にとっても使いやすい機能やサービスになる より多様な使われ方に対応していく過程の中で、「これをこうすることでこういう使われ方をした際の利便性が下がる」といったトレードオフが発生したケースは今回一つもなく、これからもなさそうに思いました。それどころか、今まで対応してきた使われ方での利便性もより向上しています(例えば、モーダルダイアログを開いた際に最初のinput要素にフォーカスを当てるようにしましたが、これはどんな使われ方をされたとしてもすぐ入力できるので使いやすくなっています)。 今回はあまり触れませんでしたが、カラーコントラストや、テキストの表現の仕方など、基本的にはどのようなユーザーにとってもよりわかりやすい/使いやすいものになるような改善もまだまだたくさんありそうです。 インターネットっぽくていい どんな人がどんな使い方をしても、平等に情報やサービスにアクセスできるというのはとてもインターネットっぽい感じがあります。 最後に まだまだ課題は多くありますが、会社のミッションを実現できるサービスづくりを進めていくために、引き続きできることからやっていきたいと思います。もしご興味ある方は、まずは素振りから始めてみてはいかがでしょうか。 参考 最後に、参考となった記事を羅列になりますが以下に記します。 https://waic.jp/docs/WCAG20/Overview.html https://waic.jp/docs/UNDERSTANDING-WCAG20/Overview.html https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html https://a11yproject.com/ https://qiita.com/simezi9/items/ec9dfbb3c7af09088898 https://www.slideshare.net/techblogyahoo/scripty05 https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html 明日は基盤グループの田中さんとOwners Growthの宮川さんです。お楽しみに!
アバター