株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

あけましておめでとうございます。 バックエンドエンジニアの塩崎です。 今年の抱負として「テクノロジー系の同人誌を書く!」と言ったら、「アニメの女の子が出てくる漫画」のことだと勘違いされてしまいました。 いつもはiQONに関することを書いているこのTECH BLOGですが、今回の記事はiQONには全く関係のない内容です。 新年会用に低温調理器具を作った話を紹介します。 はじめに 今年のVASILYの新年会は「各地の温かいもの」を持ち寄るという企画を行いました。 しかし、僕は実家に帰らずにアキバ近辺をうろうろしていました。 アキバで温かいもの言ったら、「おでん缶」か「アニメ店長」くらいしか思いつかないため、温かいもの探しに困っていました。 そんな時に、秋月電子でいいものを見つけました。 これです。 アキバ名物(?)メタルクラッド抵抗です。 これに電流を流せばジュール熱が発生するので、お土産の要件は満たしています。 さらに、ボディがアルミ製なので、熱伝導率もバツグンです。 何を作るか この抵抗は人間が食べることができないので、抵抗を使って何か食べられるものを作る必要があります。 お題が「温かいもの」なので、加熱調理を行う調理器具を作ることにしました。 とはいえ、ただ加熱するだけですとティファールの下位互換品です。 なので、この前amazonで調べてみて目玉が飛び出るほど高かった低温調理器具を自作します。 https://www.amazon.co.jp/dp/B00XV556OQ/ 低温調理とは 低温調理とは、タンパク質が変性・凝固する温度である55°Cから68°Cを下回る温度で長時間(1時間〜)加熱することで、肉や魚全体を均一に加熱する調理方法です。 肉が硬くなってしまう温度以下で加熱するため、ぱさぱさにならず調理が可能です。 この温度を長時間維持することを手動で行うのは厳しいため、専用の調理器具が市販されています。 もしくは簡易的に魔法瓶のような断熱性の良い容器に食材を入れて余熱で火を通すということも行われています。 システム全体図 以下にシステムの全体構成図を示します。 また、以下に動作中の写真を示します。 水を入れた鍋の底にメタルクラッド抵抗を配置して鍋を温めます。 湯温は温度センサーによってリアルタイムに監視され、Micro Controller Unit(MCU)が温度を元に半導体スイッチのON-OFFをし、フィードバック制御を行います。 また、温度ログはMCUからPCにWiFiを経由して送られ、温度の時系列的な変化のデータをブラウザで確認できます。 鍋の中に入っている黄色い部品は攪拌用の水中モーターです。 ちなみに、制御基板にはトランスが搭載されていますが、これはAC電源のゼロクロス検知ができたらいいなと思ってつけたはいいが企画倒れしたものです。 無駄に皮相電力を大きくさせているだけの部品です。 ハードウェア部分 まずは、ハードウェア部分の部品選定についてお話しします。 Micro Controller Unit(MCU) まずは一番重要な部品であるMCUです。 MCUの選定には以下のことを重視しました。 CPU、メモリ、IOがワンチップに収まっている WiFiで接続可能 リアルタイム制御が可能 温度センサーとのインターフェースを備えている(SPI、I2C、ADC、etc.) 小型 安価 現在発売されているマイコンでこれらを満たすものとして、ESP-WROOM-02Cを選択しました。 わずか数百円のボードの中にWiFi機能が詰まっています。 さらに、SPI、I2C、ADC機能もそれぞれ1chずつ搭載されています。 プログラムの書き込みはArduino SDKで行うことができます。 これの他に、Arduino、Raspberry Pi、Intel Edisonなども検討しましたが、どれもESP-WROOM-02Cの小型さ、安価さには敵いませんでした。 ヒーター 100Ω 50Wのメタルクラッド抵抗4つを直並列に接続して、100Ω 200Wの抵抗にしました。 ここにAC 100Vを供給し発熱をさせるため、この抵抗での商品電力は100W(実効値)です。 メタルクラッド抵抗と鍋底との間の接着には熱伝導性の良いシリコン接着剤を使用しました。 温度センサー 温度センサーにはADT7310を使用しました。 http://akizukidenshi.com/catalog/g/gM-06708/ MCUとのインターフェースがSPIなためESP-WROOM-02Cで簡単に読み出すことができます。 また、温度校正が不要なため使用するのが非常に楽です。 この温度センサーを温水中に投入するため、ホットボンドで全体をモールドしました。 耐水性試験、耐熱性試験は行っていませんが、今の所は壊れていないので、とりあえずはこれで良しとします。 半導体スイッチ AC 100VのON OFF制御を行いために、トライアックを利用します。 今回はスナバレスタイプを使用し、また、誘導性負荷の制御も行わないため、バリスタは省略しました。 MCUからの制御信号と高電系の間はフォトトライアックで絶縁しました。 フォトトライアックを使用することで、AC 100Vが正極性の時でも負極性の時でもトライアックをONすることができます。 ソフトウェア部分 MCU側 温度センサーから温度を読んでヒーターのON OFF制御を行う部分はミッションクリティカルなため、MCU側に実装しました。 これはWiFi接続が不安定になってしまってもスタンドアローンで動作できるようにするためです。 温度制御 温度センサーからを現在の温度を読み、目標温度以上か以下かによって、ヒーターのON OFFを行います。 void control_heater() { if (temperature > target_temperature) { heater_off(); } else { heater_on(); } } この処理を1秒ごとに処理するためにTickerモジュールを利用しました。 Tickerモジュールは一定時間ごとにメインループとは別の処理を同時に行うためのモジュールで、裏ではタイマ割り込みが使われています。 温度ログ 温度ログの保存にはmilkcocoaを使用しました。 milkcocoaはIoT用のBaaSでシンプルなPubSub APIをそなえています。 milkcocoa以外のBaaSとしてFirebaseやPubNubも検討しましたが、APIのシンプルさと日本語の情報量の豊富さからmilkcocoaを選定しました。 以下のようなコードでmilkcocoaに温度ログをPublishします。 #include <ESP8266WiFi.h> #include <Milkcocoa.h> #define MILKCOCOA_APP_ID "XXXXXXXX" #define MILKCOCOA_SERVERPORT 1883 WiFiClient client; const char MQTT_SERVER[] PROGMEM = MILKCOCOA_APP_ID ".mlkcca.com" ; const char MQTT_CLIENTID[] PROGMEM = __TIME__ MILKCOCOA_APP_ID; Milkcocoa milkcocoa = Milkcocoa(&client, MQTT_SERVER, MILKCOCOA_SERVERPORT, MILKCOCOA_APP_ID, MQTT_CLIENTID); void milkcocoa_data_push() { DataElement elem = DataElement(); elem.setValue( "temperature" , temperature); elem.setValue( "temperature_target" , temperature_target); milkcocoa.push( "nabe" , &elem); } この処理もTickerに登録し、5秒ごとに温度データの送信を行います。 目標温度設定 目標温度の設定部分にもmilkcocoaを使用しました。 先ほどの温度ログの送信機能とはデータの流れが逆で、MCU側がSubscribeします。 目標温度が更新された時のコールバック関数を登録しておきます。 void control_param_changed(DataElement *elem) { float target_val = elem->getFloat( "temperature_target" ); // 小数部がない数値を送るとint型にされてしまうため if (target_val == 0.0 ) { target_val = elem->getInt( "temperature_target" ); } temperature_target = target_val; } void setup() { milkcocoa.on( "nabe_control" , "push" , control_param_changed); } HTTPサーバー MCUにHTTPサーバーを立てて、温度ログを表示するための画面のホスティングを行います。 ESP-WROOM-02CのSDKにはHTTPサーバーのサンプルがあるため、それを利用しました。 #include <ESP8266WebServer.h> ESP8266WebServer server( 80 ); const char * index_html() { return "indexの内容をここに書く" ; } void setup() { server.begin(); server.on( "/" , [](){ server.send( 200 , "text/html" , index_html()); }); } mDNSサーバー このままですと、このサーバーにアクセスするためにはIPアドレスを直に指定する必要があり面倒なので、mDNSサーバーもMCUにたてます。 nabe.local のドメイン名が自分自身に名前解決されるようにします。 #include <ESP8266mDNS.h> void setup() { MDNS.begin( "nabe" ); } PC側 温度ログのグラフ ブラウザでのグラフの表示にはHighchartsを使用しました。 簡単にリッチなグラフを書くことができ、非商用ならば無料で使用することができます。 ページのロード時にmilkcocoaのAPIからデータを取得し、グラフにプロットします。 Highcharts.setOptions( { global: { useUTC: false } } ); var milkcocoa = new MilkCocoa( 'XXXXX.mlkcca.com' ); var nabe_datastore = milkcocoa.dataStore( 'nabe' ); function initial_draw(data_set) { var temperature_nabe = data_set.map( function (d) { return [ d.datetime.getTime(), d.value.temperature ] ; } ); var temperature_target = data_set.map( function (d) { return [ d.datetime.getTime(), d.value.temperature_target ] ; } ); myChart = Highcharts.chart( 'container' , { chart: { type: 'spline' } , animation: Highcharts.svg, title: { text: '鍋温度モニター' } , xAxis: { type: 'datetime' , title: { text: 'time' } } , yAxis: { title: { text: 'temperature (°C)' } } , tooltip: { headerFormat: '<b>{series.name}</b><br>' , pointFormat: '{point.x:%H:%M:%S}: {point.y:.2f} ℃' } , legend: { layout: 'vertical' , align: 'right' , verticalAlign: 'middle' , borderWidth: 0 } , series: [ { name: '鍋温度' , data: temperature_nabe } , { name: '目標温度' , data: temperature_target } ] } ); } nabe_datastore.stream().size(300).next( function (err, data) { var data_set = data.map( function (datum) { return { datetime: new Date (datum.timestamp), value: datum.value } ; } ).filter( function (datum) { // 古すぎるデータは表示しない var diff = new Date ().getTime() - datum.datetime.getTime(); return diff < 3 * 60 * 60 * 1000; } ); initial_draw(data_set); } また、新たなデータが追加された時にはそのイベントをmilkcocoaから取得して、グラフの更新を行います。 nabe_datastore.on( 'push' , function (pushed) { var datetime = new Date (pushed.timestamp).getTime(); var temperature = pushed.value.temperature; var temperature_target = pushed.value.temperature_target; myChart.series [ 0 ] .addPoint( [ datetime, temperature ] , true , true ); myChart.series [ 1 ] .addPoint( [ datetime, temperature_target ] , true , true ); } ); また、このページをホスティングしているMCUは貧弱なため、HTTPリクエスト数はなるべく減らしたいです。 そのため、jsのファイル分離をせずに、1つのHTMLファイル内に全ての処理を書きました。 目標温度の設定 目標温度の設定は以下のコードで行いました。 milkcocoaのAPIがシンプルなおかげでとても簡潔に書くことができました。 function set_target_tamperature() { var temperature_target = parseFloat(document.nabe_controll.temperature_target.value); milkcocoa.dataStore('nabe_control').push({ temperature_target: temperature_target }); } < form name = "nabe_controll" id = "nabe_controll" action = "" > < input type = "number" step= "0.1" min= "50.0" max= "60.0" name = "tamperature_target" id = "temperature_target" value = "55.0" /> < input type = "button" value = "目標温度設定" onclick="set_target_tamperature () ;" /> </ form > 調理してみた結果 とりあえず、牛肉のステーキを作ってみます。 牛肉の両面にクレイジーソルトをまぶした後に、ジップロックに入れます。 調理中に肉からドリップが出てくるので少し多めにまぶすといいです。 ジップロックの中に空気が入ってしまうと熱の通りが悪くなってしまうため、水中に沈めながら空気抜きを行いました。 あとはこれを鍋の中に入れて加熱すればいいだけです。 加熱時間はこちらのサイトを参考にしました。 http://www.douglasbaldwin.com/sous-vide.html 肉の厚みが25mmだったため、55 °Cで2時間45分加熱しました。 加熱が終わった後の牛肉は全体的に茶色になっています。 表面の殺菌と香りづけを兼ねて、表面に焦げ目をつけます。 先ほどの低温調理で中まで十分に加熱されているため、軽い焦げ目がついたらOKです。 表面だけに焦げ目をつけるため、内側はまだ赤いままのミディアムレアな焼き加減です。 食べてみると肉の内側までとても柔らかくジューシーでした。 また、サケの香草焼きも同様に作ってみました。 サケの厚みが20mmだったため、60 °Cで60分間加熱しました。 ぱさぱさになりがちなサケを中までジューシーに調理することができました。 改善点 今回、低温調理器具を作ってみて、温度が振動する問題とmilkcocoaが不安定な問題がありました。 温度が振動する 温度が完全には一定にならず、±0.5°C程度の範囲で振動することがありました。 これは制御対象の系に位相遅れを引き起こすような成分が含まれているためです。 この程度の温度変化であれば実用上は問題ないです。 もっと作り込んで、この振動を消すのならば、PID制御を行うことが考えられます。 milkcocoaが不安定 開発中に何回かmilkcocoaが不安定になることがありました。 詳しく調べてみると、milkcocoaのサーバーが500番台のエラーを返していることがわかりました。 今回は簡単にプロトタイプを作る目的でmilkcocoaを採用しましたが、もっとしっかり作るのならばFirebaseなどのBaaSに乗り換えもありだと思います。 まとめ 今年の抱負は「テクノロジー系の同人誌を書く」なので、この鍋の完成度を上げて製作記を同人誌にするかもしれません。 VASILYでは正月休暇でもモノづくりをしてしまうほど、エンジニアリングが大好きなエンジニアを募集しています。 我こそはというからは以下のリンクからぜひご応募ください。
こんにちは、iOSエンジニアのにこらすです。 SwiftがiOSの主な開発言語になってから、多くの良いプログラミング習慣が標準になっています。 型安全な設計やコンパイル時のエラー検出が当たり前になりましたが、まだSwiftの型システムを活用せずに、Objective-C時代から残る慣習でランタイムエラーになりやすいところがあります。 今回の記事は、古くてインタフェースが良くないAPIをいかに現代のSwiftプロジェクトに取り入れるかという話です。 古いAPIを使う前に、拡張するかラッパークラスを作ることが必要になるかもしれません。 特に UIFont と NSAttributedString が良くないと思うので二つのライブラリを作りました。 この記事を読みながら付随する UIFont ライブラリと NSAttributedString のライブラリを参考してください。 ここに付随するライブラリがあります: もし気になったらスターをつけると嬉しいです! github.com github.com 例:Selector Swift 2.1 以前は Selector はまだただの文字列で、このAPIデザインには二つのデメリットがありました。 一つはコンパイラが Selector 文字列が本当に存在するメソッド名のチェックをしないため、存在しないメソッド名の場合は実行時にクラッシュしてしまいます。 もう一つの問題は、メソッド名のコード補完が効かないのでミスタイプの可能性が高くなります。 Swift 2.1までの Selector 使い方: // Swift 2.1 navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem : .Add, target : self , action : "addNewMemo" ) 幸いにもSwiftチームがこの問題を解決してくれたので、Swift 2.2でもっと良い構文になりました。 // Swift 2.2 navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem : .Add, target : self , action : #selector(addNewMemo)) Swift 2.2から Selector はただの文字列から型になりました。 #selector(methodName) の methodName が存在しないメソッド名なら、コンパイル時にエラーを検出できます。 この新しい型のおかげで Selector 系のランタイムクラッシュの可能性がなくなりました。 さらにコード補完でメソッド名が出てくるようになったので、このプログラミングインタフェースがもっと使いやすくなりました。 例:UIFont UIFont のコンストラクタは一見簡単ですが、正しいフォント名の文字列をタイプしないといけません。 現在の UIFont コンストラクタを使ってフォントオブジェクトを作ろうとしたら下記のようになります: let font = UIFont(name : "Arial-BoldItalicMT" , size : 12.0 ) ! このインタフェースはあまり良くないと思います。 さきほどの Selector と同じ問題があり、フォント名の文字列を間違っていてもコンパイル時に気付く事はできません。 UIFont は FileManager や URL と違って、実行時にiOS標準のフォントの存在が保証されています。 iOS標準のフォントは有限なので、 enum を使うのが良いと思います。 UIFont で使えるフォントを全て enum にすると、正しいフォント名を知らなくてもコード補完で使いたいフォントを探せます。 extension UIFont { /// Create a UIFont object with a `Font` enum public convenience init ?(font : Font , size : CGFloat ) { let fontIdentifier : String = font.rawValue self . init (name : fontIdentifier , size : size ) } } public enum Font : String { // Font Family: Copperplate case copperplateLight = "Copperplate-Light" case copperplate = "Copperplate" case copperplateBold = "Copperplate-Bold" . . . // Font Family: Bodoni 72 Oldstyle case bodoniSvtyTwoOSITCTTBook = "BodoniSvtyTwoOSITCTT-Book" case bodoniSvtyTwoOSITCTTBold = "BodoniSvtyTwoOSITCTT-Bold" case bodoniSvtyTwoOSITCTTBookIt = "BodoniSvtyTwoOSITCTT-BookIt" } 下記のように UIFont を宣言できます: let font = UIFont(name : .arialBoldItalicMT, size : 12.0 ) ! enum を使えば、コード補完が効くようになります。もし名前が間違っていてもコンパイル時にエラーが検出できます。 例:NSAttributedString NSAttributedString を使ったことあるなら、不愉快な経験をしたことがあるかもしれません。 NSAttributedString を作るとき、属性をDictionaryで指定しますが、キー名と対応する値の型を調べる必要があります。 例えば、 Chalkduster というフォントで、文字色を赤、文字に下線を引く NSAttributedString オブジェクトを作ろうとすると、下記のようなコードになります。 let attributes : [String: Any] = [ NSForegroundColorAttributeName : UIColor.red , NSFontAttributeName : UIFont (name : "Chalkduster" , size : 24.0 ) ! , NSUnderlineStyleAttributeName : 1 , ] let text = NSAttributedString(string : "Hello" , attributes : attributes ) attributes のDictionaryの型は [String: Any] 型なので、 NSFontAttributeName: UIColor.red と書いてしまっても、コンパイル時にエラーを検出することはできません。 属性付き文字列に値を設定するたびに、そのキーの識別子を確認する必要がありますし、キーに対する値のコード補完も効かないため、正しい値を見つけることができません。 このインターフェースは現代のAPI標準と同じではなく、不便だとみなされるべきだと私は考えています。 私たちができることは、単純な薄いラッパーにこのインタフェースをラップすることです。各ラッパーは、各属性に対して明示的に定義された型を持つすべての値を設定するメソッドを持ちます。 let attributes = Attributes { return $0 .foreground(color : .red) .font(UIFont(name : "Chalkduster" , size : 24.0 ) ! ) .underlineStyle(.styleSingle) } "Hello" .attributed(with : attributes ) ここでは、入力された値を使って各属性を設定するためのメソッドを定義しているので、どの型の値が期待されているのか正確に知ることができます。 NSUnderlineStyleAttributeName もマジックナンバーを使用しなくてもよくなりました。 また、アンダーラインスタイルが通常の下線 ( .styleSingle ) であることをはっきりと見て取ることができます。 このような拡張機能のさらなる利点として、異なる属性の単語を含む属性付き文字列を作成することが簡単にできます。 属性付き文字列のための + 演算子を定義すると、連結のための非常に簡単なインターフェースを作成できます。 たとえば、ユーザー名が白で強調表示され、テキストを目立たせるために特別なカーニングを使用して赤いテキストのベースを作成する場合、これは通常の NSAttributedString APIを使用して構築できます。 let attributes : [String: Any] = [ NSForegroundColorAttributeName : UIColor.red , NSFontAttributeName : UIFont (name : "Chalkduster" , size : 14.0 ) ! , ] let userName : String = "@trent" let attributedString = NSMutableAttributedString(string : " \(userName) has commented on your post." , attributes : attributes ) let nameAttributes : [String: Any] = [ NSForegroundColorAttributeName : UIColor.white , NSKernAttributeName : 4.0 , ] attributedString.addAttributes(nameAttributes, range : NSRange (location : 0 , length : userName.characters.count )) このコードでも正しく動作しますが、少し不自由な感じです。 上記のコードを入力している間は、共通の属性ベースを持っていることが明らかです。フォントのような既存の属性に加えて、文字列の特定の部分に追加の属性を適用したいだけです。 最初に基本となる属性を宣言し、基本属性から派生した userName のみにかかる明示的な属性を作成します。 let baseAttributes = Attributes { return $0 .foreground(color : .red) .font(UIFont(name : "Chalkduster" , size : 24.0 ) ! ) } let nameAttributes = baseAttributes.foreground(color : .white) .underlineStyle(.styleSingle) .kerning( 4.0 ) let message = " has commented on your post." .attributed(with : baseAttributes ) let userName = "@trent" .attributed(with : nameAttributes ) messageLabel.attributedText = userName + message 両方の方法で同じ結果が得られますが、後者の方がはるかに単純です。 まとめ 今回は、文字列で値を設定するメソッドの改善方法について紹介しました。 UIKitやFoundationなどの標準の仕組みであっても、使いにくいインターフェースであれば、独自のラッパーを作っても良いと思います。 今回の記事のコード例は、より簡単に操作できる安全なAPIを設計する方法の例ですが、決して唯一の方法ではありません。 私は、今後も既存のAPIを管理しやすくするため、GitHubの既存のプロジェクトなどを見ることを楽しみにしています。 VASILYではiQONを一緒に開発してくれるiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。 https://www.wantedly.com/projects/62340 www.wantedly.com また次回。 にこらす
はじめに こんにちは、CTOの今村です。 先日弊社のiQONが3年連続でGoogle Play「2016年ベストアプリ」に選ばれました。また、今回 ベストイノベーティブ部門の大賞 を受賞しました。 イノベーティブ部門ということなので、Androidアプリの品質だけでなく、アプリの中にある様々な機能の技術的な取り組みも評価してもらった背景があるのかなと個人的には感じています。 さて、ちょうど先日 Minami Aoyama Night #1 にて、弊社のデータまわりのアーキテクチャについてお話させていただく機会がありました。 今回は2016年12月時点での、機械学習とデータ分析を支えるAWSとGCPを利用したマルチクラウドアーキテクチャについて紹介したいと思います。 最近のデータまわりの取り組み 今年になってからVASILYは過去のテックブログでも紹介したように、データまわりの取り組みを一層強化しています。 機械学習関連の事例 ユーザーの好みに応じたレコメンドエンジン iQONにはfor Youというユーザーの好みを学習してアイテムを推薦する機能があります。 ここにも機械学習やディープラーニングが取り入れられています。 VAEとGANを活用したファッションアイテム検索システム IBIS2016 で弊社が発表した、VAEとGANを応用したファッションアイテム検索システムです。 例えば元画像のワンピースの形を保ったまま、色の違うワンピースなどを検索することができます。 http://tech.vasily.jp/entry/retrieval_using_vae_gan ディープラーニングによるファッションアイテム検出と検索 最近では似たような取り組みをしている事例もちらほら出てきていますが、精度の面ではかなりの高さを誇っています。 http://tech.vasily.jp/entry/detection_and_retrieval ディープラーニングを活用して画像から商品カテゴリを判定する http://tech.vasily.jp/entry/deepleaning_microservice 弊社の強みであるクローラーの部分でももちろんディープラーニングは活用しています。 画像からカテゴリを判定して、自動的にアイテムを分類することでメタデータからだけでは分類しづらいようなアイテムにも対応しています。 このように機械学習関連やディープラーニング関連だけでも様々なことに取り組んでおり、実際のサービスへと展開しています。 マルチクラウドの基本方針 主にAWSをサービスの メインインフラ として、GCPを データ分析/計算のインフラ として利用しています。マルチクラウドと言っていますが、実際はオンプレ環境も利用しています。 基本的な方針としては以下の通りです。 AWSでできることはAWSでやる Pythonを用いたり、Spark(DataProc)を扱ったり、大量の計算を扱う部分はGCPでやる GPUなどクラウドでの値段が高くて、検証中にリソースを常時大量消費するものはオンプレ 最終的な計算済結果はRedisに保存して、WebAPIから高速に呼び出せるようにする 各クラウド間の実装はBigQueryを中心として疎結合を意識する 例えば類似アイテム画像APIのアーキテクチャは以下のようになっています。 機械学習を支えるアーキテクチャ 類似アイテム画像APIのアーキテクチャ こちらはあるアイテムに似ているアイテムを表示する類似アイテム画像APIの例になります。 まず、データ生成には3つのステップが存在しています。そして、それぞれのステップをそれぞれ異なる基盤で行っています。 もう少し詳しくしたのが以下の図です。 まず、オンプレ環境で大量の画像の特徴量抽出を行います。ここはGPUの真価が発揮されるところであり、リソースを大量に消費する部分です。特徴量抽出を行うべき画像のリストなどをBigQueryから取得し、画像をダウンロードして、処理を行います。処理を行って得た特徴量はBigQueryに保存します。 次にGCP内のBatchにて、BigQueryに保存した特徴量を取得し、Pythonで類似度の計算を行います。計算した類似度の結果は再度BigQueryに保存します。 次にAWSにてBatch処理で、BigQueryから計算済の類似アイテムのリストを取得し、APIから参照するRedisに保存します。 あとはアプリケーション側からそのRedisを参照することによって、高速に類似アイテムを表示することができます。 上記の3ステップの中心となる存在はご覧の通り BigQuery です。このようなシステムを組む場合、リトライや冪等性を意識した仕組みにする必要性があったり、ジョブの関連性が複雑になったりしがちですが、それぞれの基盤からBigQueryを介することによって、システム全体としての結合度を下げて運用しやすくしています。 タスク制御 それぞれの基盤でバッチ処理を安全に運用するためにはワークフローマネージャーは必須といえます。弊社では、主に以下の2つの製品を利用しています。 AirFlow 弊社では AirFlow をメインのワークフローマネージャーとして使っています。運用して約1年ほどになりますが、今のところ問題なく動いています。もともとはAirbnb社が開発したものですが、3月にApache IncubatorのOSSになっています。 Luigi 最近ではSpotify社が開発した Luigi も使い始めました。Airflowは高機能すぎるため、シンプルにジョブ制御を行いたい場合に利用しています。 データ分析を支えるアーキテクチャ 次にデータ分析を支えるアーキテクチャの紹介です。 施策の数値確認やダッシュボード 弊社では、アプリ内行動ログやユーザーデータの分析などあらゆるデータの分析を常に行っています。 現在上図のようなビューが約300以上存在し、様々な施策のKPIを日々確認しています。 いくつかの重要なビューは、レンダリングされたグラフとともにSlackの全体チャンネルに日々自動的に共有されるようになっています。 ログデータまわりのアーキテクチャ アプリ内行動ログや、マスターデータ、あらゆるログデータ関しては、全て BigQuery に保存しています。 アプリ内行動ログの取得にはAndroidはCookpad社製の Puree 、iOSは独自のSDK(Yabaimo)を用いています。 ログを待ち受けるサーバー部分は自分達でサーバーを用意してfluentd経由でBigQueryに保存しています。昔はLocalyticsを使っていたのですがサポート面の遅さや料金の問題もあり、今は利用していません。 どのBIツールがいいのか? 最近は Google Data Studio 、 re:dash 、 superset 、など様々なBIツールが登場してきており、どれを採用していいか迷うことが多いかと思います。 上記の表は2016年12月時点での弊社の独自の調査に基づくものです。 Tableu Desktop + Tableau Server 弊社ではデータ分析には Tableau Desktop + Tableau Server の組み合わせを使っています。 高度な分析を必要とするデータサイエンティストやエンジニアには Tableau Desktop 、さらに彼らが作ったダッシュボードを他の職種の人たちに共有して閲覧できるように Tableau Server を利用しています。データサイエンティスト以外の人たちは、Tableau Server経由で共有されたKPIやダッシュボードを閲覧します。 最近は無料のものから有料のものまで、様々なBIツールが登場していますが、BIツールを選ぶ際に重要視していることは、 BigQueryに対応していること と ツール自体にバグがないこと です。重要指標を多く含むため、ツール自体にバグがあって数値がうまく表示されなかったりすると誤った判断をしてしまうかもしれない為、安定性を重要視しています。安定性の観点ではTableauはサポートも充実しており、弊社環境でしか起こらないようなバグなども全て対応してもらっています。 まとめ 上記をまとめたものを以下のスライドにしてありますので、ぜひ御覧ください。 現時点ではAWSやGCPもそれぞれのプラットフォームで得意なものが違うので、 うまく使いこなして、運用負荷とコストを削減していくのが今のところベストなソリューションかなと感じています。 また、今はAWSとGCPを主に利用していますが、AzureにもGPUインスタンスが登場しましたし、 今後はAzureの検証も進めていければと思います。 総括になりますが、今年はVASILYとして、弊社の持つ膨大な量のファッションのデータを使った取り組みを色々と強化することができました。 来年も引き続き、機械学習や深層学習をはじめとするAI分野に投資をしていきたいと思います。 データを使って面白いことがしてみたい、研究もしたいし、プロダクトに自分の技術を使ってユーザーに価値を届けてみたい、そんな気持ちを持っているデータサイエンティストの方々、ぜひご応募お待ちしています。
フロントエンジニアの茨木です。 皆様はCSSを書く際にコーディング規約を意識しているでしょうか。かつて、弊社にはCSSのコーディング規約が存在せず、CSSファイルの肥大化・クラス命名規則の不統一が発生していました。メンテナンスが難しくなってきた為、1年半ほど前にCSSコーディング規約を設けました。若干のルール追加を伴いながら、現在まで問題なく運用できています。本記事ではフロントエンドで運用しているCSSのコーディング規約に関して紹介します。 導入環境 本記事では以下の環境を前提にしています。 Ruby on Rails 5.0 Sass 3.4 Slim 3.0 CSSコーディング規約のコンセプト 初めにチームでヒアリングを行い、以下のようなコンセプトを決定しました。 読みやすい、書きやすいクラス名やタグ構造にする スタイルを再利用できる セレクタによるスタイルの競合を少なくする 既存の設計手法であるBEM *1 やSMACSS *2 などの検討を初めに行いました。検討の結果、最もコンセプトに近かったSMACSSを基に、チームの実情に即したルールを独自で設けることにしました。 CSSコーディング規約の概要 上記の3つのコンセプトに基づき、それぞれに対応する以下のルールを設けました。 ルール1: 読みやすく書きやすいクラス名やタグ構造にするためのルール ルール2: スタイルを再利用するための構成要素のルール ルール3: スタイル競合を少なくするためのルール 構成要素のルールはSMACSSのそれにかなり近いですが、他のルールは独自で定義したルールです。 これから、それぞれのルールについて紹介していきます。 ルール1: 読みやすく書きやすいクラス名やタグ構造にするためのルール 読みやすい、書きやすいクラス名やタグ構造にするためのルールです。タグ構造自体はHTMLの話ですが、CSSのコーディングに直接関わる部分なので規約に含めています。クラス名やタグ構造に関しては3つのルールを設けています。 ネスト構造をクラス名に含めない & クラス名はハイフン区切り 読みやすさ・書きやすさのためにネスト構造をクラス名に含めないことにしました。2単語以上のクラス名の場合は、CSSの言語仕様に則ってハイフン区切りで単語を区切ります。 class.html.slim // 良い例 section . popular-users ul li . user span . name hogehoge span . age 25 // 悪い例 section . popular-users ul . users // 以下3行はネスト構造が含まれている li . users-user span . users-user-name hogehoge span . users-user-age 25 タグやクラス名をセマンティックに タグやクラス名をセマンティックにするために、以下の3つのルールを設けています。 文章構造に適したタグを可能な限り使う HTML5で定義されたheader、footer、navといったセマンティックなタグを積極的に使うようにしています。 クラス名は、必要最小限・セマンティックに命名する クラス名は、タグで表現しきれない意味を表すために必要最小限だけ振るようにしています。特にタグとクラス名の意味が重複しないように注意しています。 divやspanを使う場合は、必ずクラス名を振る divやspanはタグ自体が意味を持たないので、必ずクラス名を振って意味を与えるようにしています。デザイン上必要なdivには .wrapper など目的が分かるクラス名を与えています。 correct_tag.html.slim // 良い例 section . popular-users // デザイン目的のタグであることをクラス名で明示している . wrapper ul li . user span . name hogehoge span . age 25 // 悪い例 . popular-users // divには必ずクラスを振らなければならない div // タグとクラス名の意味が重複している ul . list li . user // spanには必ずクラスを振らなければならない span hogehoge // 文章構造が分からないクラス名 span . small-number 25 文書構造上意味のないタグを可能な限り削減する 擬似要素などを積極的に使い、文書構造上意味のないタグを可能な限り削減しています。 HTML自体がかなり読みやすいものとなるのは勿論、タグが減少することによりスタイル競合のリスクが減少します。 擬似要素が役に立つ例として、以下の画像のような矢印付きリンクボタンを紹介します。 矢印をimgタグで定義することは勿論可能です。しかし、矢印は文書として意味を持たないので、本来はHTML文書内に無い方がセマンティックです。このような場合に、タグ内にCSSで要素を追加できる擬似要素が有用です。以下にコードを示します。 link_with_arrow.html.slim ul . links li a href= '/page1' page1 li a href= '/page2' page2 link_with_arrow.sass ul . links li list-style : none border-top : solid 1px #999 width : 200px & : last-child border-bottom : solid 1px #999 a display : block height : 50px line-height : 50px position : relative text-decoration : none // 擬似要素:afterにより右矢印のスタイルを定義 & : after content : '' display : block position : absolute top : 0 right : 10px width : 14px height : 50px background : url( //cdn.hoge.fuga/images/right_arrow.png ) no-repeat center background-size : 6px 12px ルール2: スタイルを再利用するための構成要素のルール スタイルの再利用性を高めるために、ベース・モジュール・レイアウト・ユーティリティ・ステートという5つの構成要素を定義しています。ベース・モジュール・レイアウト・ステートに関しては、SMACSSと基本の考え方は同一です。ユーティリティはモジュールと類似していますが、スタンドアローンでない点がモジュールと異なります。ユーティリティは規約策定当初モジュールに含まれていました。運用していく中でモジュールの書き方が多様になってきたので、ユーティリティを別に定義しました。 ベース 各ページ共通のスタイル、各タグの基本となるスタイルを定義します。 _base.sass header width : 100% h1 font-size : 30px input border : solid 1px #ccc モジュール 画面上で再利用されるひとまとまりの要素に適用されるスタイルです。スタンドアローンで成り立ちます。 トップレベルに .m-モジュール名 のクラスを必ず持つことを規定しています。mixinにする場合は、後述のユーティリティとの区別のため、 m- から始まる名称にします。 _mixins.sass @mixin m-items ul . m-items li . item img width : 100% . name font-size : 20px . . . レイアウト 画面上の大枠のレイアウトを指定するための要素に適用されるスタイルです。 l- で始まるクラス名で定義します。 _base.sass . l-sub float : left width : 100px . l-main margin-left : 120px ユーティリティ 頻繁に使用されるスタイルの集合です。スタンドアローンでない点がモジュールと異なります。スタンドアローンでないことを明確にするために、必ず @mixin で定義すること、一切セレクタを持たないことを規定しています。この構成要素は当初の規約にはありませんでしたが、運用過程で暗黙に使われていました。次第にモジュールと混同されるようになってきたので、ルール化しました。 _mixins.sass @mixin circle( $diameter ) width : $diameter height : $diameter border-radius : $diameter / 2 util.sass @import 'mixins' . rank-badge @include circle( 20px ) background-color : #ccc ステート ステートは状態に応じてスタイルを上書きするためのクラスです。適用元のクラスを存置したままマルチクラスで定義します。ステートのクラス名は、適用元要素のクラス名を原則含みません。 state.sass . m-like-button width : 100% // ステート & . active background-color : #ccc border : solid 1px #999 state.html.slim ul . users li . user . m-like-button . active いいね済み li . user . m-like-button いいねする ルール3: スタイル競合を少なくするためのルール 1ビュー1CSSのファイル構成 Railsではデフォルトで全CSSがapplication.cssにまとめられますが、スタイル競合のリスクが高まります。そのため、本規約ではビューごとにCSSの定義ファイルを別にしています。各ページ共通で用いるスタイルは別ファイルで定義し、 @import で読み込むようにしています。 /app/assets/stylesheets/ │ ├ shared/ │ │ │ ├ _base.sass │ ├ _mixins.sass │ └ _reset.sass │ ├ users/ │ │ │ ├ show.sass │ ├ create.sass . . . . . . show.sass @import 'reset' @import 'base' @import 'mixins' ルートでのクラス名/要素名単独のセレクタは禁止 ルートのセレクタはかなり影響範囲が大きいです。ルートでクラス名や要素名を単体で指定した場合には予期せぬ要素にスタイルを定義してしまう危険が高くなります。そのため、ルートでのクラス名/要素名単独のセレクタを禁止しています。このルールは、ベース・モジュール・レイアウト・ユーティリティには適用しません。このルールは当初暗黙のルールでしたが、今後メンバーが加わることを想定してルール化しました。 root_selector.sass // 良い例 section . popular-articles h2 font-size : 30px // 悪い例1 . popular-articles h2 font-size : 30px // 悪い例2 section h2 font-size : 30px まとめ 規約により、読みやすさ・書きやすさ・再利用性が向上し、メンテナンスがとても楽になりました。BEMのような厳格な命名規則ではありませんが、セレクタやファイル構成の規約のおかげでスタイルの競合はほとんど発生しません。途中、若干のルール追加を伴いながら1年半運用できており、当初の目的は達成されたといえます。成功の要因としては、安全性と読みやすさ/書きやすさの双方を考慮した点、コードレビューを徹底している点が挙げられます。 読者の皆様も、CSSの実装中にスタイルの競合や再利用性の問題に遭遇することはよくあると思います。そのような場合、CSSの書き方だけでなくファイル構成やタグ構造も見直すことが解決につながるかもしれません。また、規約を継続運用するためには、安全性だけでなく読みやすい・書きやすいという点も重要かもしれません。 最後に VASILYではWebの力で世の中を変えたいエンジニアを募集しています。 興味がある方は是非こちらからご応募ください。 *1 : Methodology / BEM https://en.bem.info/methodology/ *2 : Scalable and Modular Architecture for CSS https://smacss.com/
こんにちは。 モルトとシガーで生きてます。インフラエンジニアの光野(@kotatsu360)です。 先日、crontabで管理しているバッチ処理の監視にhorensoというツールを導入したのですが、 監視の品質が向上 毎分届く大量の実行結果メールから開放されQoL向上 という効果がありました。本日はその取り組みについてご紹介いたします。 ジレンマ:動作監視と大量のメール 冒頭の通り、VASILYでは定期的に実行したいバッチ処理をcrontabで管理しています。 真新しさはありませんが、実行時間の指定が簡潔かつ柔軟で未だに愛用しています。 2016年12月現在、crontabは120行ほどです。 さて、そんなcrontabによる処理で前々から課題になっていたのが、 crontabで管理しているスクリプト達(以下cronスクリプト)の動作監視です。 当時、VASILYのcronスクリプトは3箇所に結果を出力していました。 (ログファイル・スクリプト・歯車・メールの画像クレジット *1 ) 1. のログは調査用、 2. のSlack通知は異常検知、 3. のMAILTOは過去の動作記録とそれぞれ役割をもっているのですが、ここで問題になるのは 3. です。 ご存知の通り、crontabにてMAILTO要素にメールアドレスを設定しておくと、cronスクリプト実行後にメールを送信してくれます。 お手軽で便利なのですが、cronスクリプトの量が増えてくるとspamよろしくメールが届きます。 正直、邪魔です。しかし、「動いた」という記録を取る意味では大変有用なため、日々大量のメールを受け取り続けていました。 問題発生:解決手段の模索 そんな状況をしばらく容認していたのですが、ある日メールがパタリと届かなくなるという問題が発生したため、一念発起、改善に着手します。 ちなみにメールが届かなくなった原因ですが、なんということはなく、あまりに量が多いため迷惑メールフォルダに分類されていたのでした その際、手段選定の条件としたのが次の3つです。 特別な実行環境が不要 既存のスクリプトをそのまま使う 監視と本処理を独立させる 1. と 2. については、短期間で移行を完了するためです。 最初は「はやりのリッチなUIをもつジョブ管理システムを導入だ!」とも考えたのですが、 既存のスクリプトを短期間かつ安全に移行することを考えるとあまり現実的ではないと思い止めました。 3. については、cronスクリプトをこれ以上肥大化させないためです。バッチ処理のスクリプトはユニットテストもしづらく、 監視のような「運用上は必要だけれども、スクリプト自体の処理には必要ない機能」を持たせたくありません。 そして、これらの条件を満たせるものとして horenso を採用しました。 horenso horensoはSongmuさんが開発された各種コマンド用のラッパーツールで、 スクリプト実行結果の「報告」をつかさどってくれます。 Goで実装されており、各プラットフォームに対応するバイナリを置くだけで利用可能です。バイナリを配置後、以下のように実行します。 $ /path/to/horenso --reporter report.rb -- /usr/bin/ruby /path/to/batch/script.rb arg1 arg2 -- の後ろにあるのが、既存のcronスクリプトです。 このようにして実行すると、 --reporter で指定したスクリプトがcronスクリプトの実行結果をJSONで受け取ります。 { " command ": " /usr/bin/ruby /path/to/batch/script.rb arg1 arg2 ", " commandArgs ": [ " /usr/bin/ruby "," /path/to/batch/script.rb "," arg1 ", " arg2 " ] , " output ": " message \n error ", " stdout ": " message ", " stderr ": " error ", " exitCode ": " 0 ", " result ": " command exited with code: 0 ", " pid ": " 13813 ", " startAt ": " 2016-12-05T06:32:01.123456789+09:00 ", " endAt ": " 2016-12-05T06:32:11.987654321+09:00 ", " hostname ": " cron.iqon.example.com ", " systemTime ": " 0.716 ", " userTime ": " 5.812 " } 後はこのJSONをreporterのスクリプトで好きなように扱えば完了です。reporterには任意の実行権限がついたファイルを指定できます。 詳細な使い方は、SongmuさんのブログエントリやGithubのREADMEをご覧ください。 horensoというcronやコマンドラッパー用のツールを書いた | おそらくはそれさえも平凡な日々 Songmu/horenso: Command wrapper for reporting the result. It is useful for cron jobs. cron監視ver2.0 horensoを使うことによってcronスクリプトの監視方法は以下のように変わりました。 (ほうれん草の画像クレジット *2 ) crontabの中では、以下のサンプルのように呼び出します。 0 0 * * * /path/to/horenso --reporter horenso.rb -- /usr/bin/ruby /path/to/batch/script.rb args... 用意したhorenso.rbの動作は大きく2つです。 exitCodeが0以外、またはstderrに出力がある場合、Slackに通知 ログをfluentdにフォワード このログは最終的にBigQueryとS3に保存され記録されます。 これにより、ほぼspamと化していたメールを廃止することができ、かつ動作記録が確実に記録できるようになりました。BigQueryにもインサートされているため、参照も非常に容易です。 なお補足をいたしますと、horenso.rbに集約したSlack通知はあくまでも監視を目的とした通知です。 個々のcronスクリプトの仕様として必要なSlack通知は、そのスクリプトで実装することにしています。 導入初期に発生した諸々 horensoによって、劇的にモダンになったcron監視環境ですが、導入初期にはいくつか問題も発生しました。 cronスクリプト自体を直したほうが良いものもありますが、原則既存のスクリプトに手を入れず対応しています。 実行ディレクトリについて 幾つかの古いスクリプトでは内部で相対パスを使っており、 cd /path/to/dir ; ./batch/script.rb というような呼び出し方をしているものがあります。 しかし、horensoでは -- 以降で、 cd をしても意味がありません。 そこで、次のように移動した上でhorensoを呼び出すことで解決しています。 cd /path/to/dir && (horenso --reporter horenso.rb -- bin/rails runner -e production batch.rb) stderr 当初、exitCodeのみを基準に実装をしていたのですが、例外ケースがあり正しく検知できていませんでした。 VASILYのcronスクリプトはRubyですが、幾つかのスクリプトでは `` でシェルコマンドを呼び出しているものがあります。 シェルコマンドが失敗した場合、Rubyとして例外は出力されないため、exitCode=0(ただし失敗している)ということになります。 そのため、内部で失敗を検知し損ねているケースを考え、今のreporterスクリプトではstderrの有無も確認するようになっています。 BigQueryのクォータ 2つめの図の通り、horensoで受け取ったログは最終的にBigQueryとS3まで送られます。fluentdからBigQueryへはストリーミングインサートをしているのですがここには各種の制限が存在します。 割り当てのポリシー | BigQuery のドキュメント スクリプトによっては大量のログを出すものがあり、それらが一度のリクエストに重なると制限に引っかかる恐れがありました。 現在は、ログを次のように加工してからフォワードしています。 outputを削除 outputはstdoutとstderrを合わせたもの stdout/stderrを1KBで切り詰める startAt/endAtをISO 8601形式に変換 commandArgsをJSON文字列に変換 なお、後半の2つはあとで扱いやすくするだけのものなので、BigQueryの制限とは関係ありません。 切り詰める前のログはサーバ側に残っているため、どうしても全てのログを見たいケースがあればそちらを見ます。 まとめ モダンなcron監視環境についてご紹介しました。horensoを使うことで既存のcronスクリプトをそのままに、 動作記録を参照しやすい形で保存することが可能になりました。またhorensoによって、実行時間など これまで見えていなかった情報も得られるようになりました。今後はこの環境を活かしてcronスクリプト自体の改良も行いたいと考えています。 最後に VASILYではiQONを一緒に作っていく仲間を募集しています。 興味のある方は以下のリンクからご応募お願いいたします。 *1 : logfile / script icon: Icons made by Freepik from www.flaticon.com is licensed by CC 3.0 BY gear icon: Icons made by Gregor Cresnar from www.flaticon.com is licensed by CC 3.0 BY mail: Icons made by Nikita Golubev from www.flaticon.com is licensed by CC 3.0 BY *2 : Icons made by Madebyoliver from www.flaticon.com is licensed by CC 3.0 BY
Solr 6でneologdが組み込まれたkuromojiを使う方法 こんにちは、VASILYバックエンドエンジニアの塩崎です。 VASILYでは商品情報の全文検索を行うためのバックエンドに、Apache Solr(以下、Solr)を利用しています。 先日、Solrのメジャーバージョンを最新の6にアップグレードしました。 それに伴ってSolrの形態素解析エンジンであるkuromojiに新語辞書であるmecab-ipadic-neologd(以下、neologd)を組み込みました。 この記事では、組み込むことのメリット及び、具体的な組み込み方を紹介します。 kuromojiにneologdを組み込むことのメリット では、まずkuromojiにneologdを組み込むことのメリットを、転置インデックスを利用した全文検索の仕組みに基づいて説明します。 転置インデックスを利用した全文検索の仕組み Solrは全文検索を行うために、転置インデックスと呼ばれるインデックスを生成します。 これは、文章中に登場する単語をキーとし、その単語が出現した文章IDの配列をバリューとする連想配列です。 Solrはこの転置インデックスを用いることで、文章全体をスキャンすることなく、全文検索を行います。 その結果として、膨大な文章に対する全文検索を高速に行うことができます。 全文検索のインデックスに形態素解析を用いるときの欠点 さて、先ほどの例のような英語(?)の文章では単語と単語の間が空白で区切られていました。 一方で日本語の文章は単語境界に空白がありません。 そのために、日本語の文章に転置インデックスを作るためには、空白文字に頼らずに文章を単語単位に分解する必要があります。 この単語分解の方法には大きく分けて2つの手法があります。 機械的にn文字ずつに単語に分解するngramと、文法的に意味のある単位で単語に分解する形態素解析です。 ngramは機械的に文章を分割するために、検索ノイズが多いという欠点があります。 (例: "京都"で検索をした時に、東京都がヒットしてしまう) そのため、iQONでは形態素解析を用いた転置インデックスの生成を行い、それを使い全文検索を行っています。 しかし、形態素解析を使った転置インデックスの作成にも欠点があります。 それは形態素解析エンジンが対応していない単語に対しては、正しくインデックスを作成できないということです。 kuromojiの標準辞書では、カバーされている単語が少ない kuromojiに標準搭載されている辞書はIPA辞書です。 この辞書は2007年を最後に更新が止まっているため、それ以降に作られた単語に対しては正しく形態素解析を行うことができないケースが多いです。 また、辞書を作る時の元データがファッション用語に乏しいため、iQONに登場するファッション用語に対応できないケースがあります。 例えば、以下のような単語は辞書に登録されていません。 ロクシタン スナイデル neologdを組み込むことで、辞書の語彙力を底上げ この問題に対応するためには、全文検索を行うアプリケーション毎に、必要な語彙を辞書に追加する必要があります。 しかし、その作業はとても地道で時間の掛かるものです。 そこで、@overlastさんのmecab-ipadic-neologdをkuromojiに組み込むことを考えました。 この辞書は、以下の公式ドキュメントに書かれているように、Webの情報から新語を追加しています。 mecab-ipadic-NEologd は、多数のWeb上の言語資源から得た新語を追加することでカスタマイズした MeCab 用のシステム辞書です。 https://github.com/neologd/mecab-ipadic-neologd/blob/master/README.ja.md また、辞書の更新ペースも非常に早く、週に2回も辞書が更新されています。 もちろん、これを行えば全ての単語に対応できるわけではありません ですがkuromojiの標準辞書だけを使うことに比べて、大幅な語彙力の向上が見込まれます。 組み込み方 ソースコードの入手 今回はgithubに公開されているlucene-solrにパッチを当ててneologdを組み込みます。 https://github.com/apache/lucene-solr なお、具体的なパッチの当て方は、moco(beta)さんや@kazuhira_rさんの以下の記事を大変参考にさせていただきました。ありがとうございます。 http://mocobeta-backup.tumblr.com/post/114318023832/neologd-kuromoji-lucene-4-10-4-branch http://d.hatena.ne.jp/Kazuhira/20150316/1426520209 これらの記事を元に、Solr6用のパッチを作成しました。 パッチは以下のGistで公開してあります。 https://gist.github.com/shio-phys/02ece250ecbc291e1ec4923ed119d182 また、このパッチを適用させたソースコードをgithubに公開してあります。 https://github.com/vasilyjp/lucene-solr また、ビルド済みのjarファイルもgithubに公開してありますので、とりあえず試したい方はご利用ください。 2016/12/02時点での最近のneologdを組み込んでいます。 Solr 6.2.0用 Solr 6.2.1用 Solr 6.3.0用 これらのファイルはneologdのバージョンアップには追従していないので、バージョンアップが頻繁なneologdの特性を活かすためには、その時点での最新のneologdを利用することをお勧めします。 ビルド 必要な環境 まずは、以下のドキュメントを参考に、neologdのビルドに必要なパッケージをインストールしましょう。 https://github.com/neologd/mecab-ipadic-neologd#getting-started その後に、kuromojiをビルドするためのパッケージをインストールしましょう。 Ubuntu 16.04.01であれば、以下のパッケージが必要です。 $ sudo apt-get install default-jdk ant subversion jarの生成 パッチ適用済みのリポジトリを取得し、使用しているSolrのバージョンに合わせたブランチをチェックアウトします。 2016/12/02現在、以下のブランチを用意しています。 使用しているSolrのバージョンに合わせたブランチをチェックアウトしてください。 kuromoji_neologd_6.2.0 kuromoji_neologd_6.2.1 kuromoji_neologd_6.3.0 $ git clone https://github.com/vasilyjp/lucene-solr.git $ cd lucene-solr $ git checkout <ブランチ名> build.propertiesに書かれているneologdのバージョンを最新のものに合わせます。 以下のディレクトリに mecab-user-dict-seed.*.csv.xz というファイルがあるので、このファイル名のタイムスタンプ部分をneologd.versionに指定します。 https://github.com/neologd/mecab-ipadic-neologd/tree/master/seed $ vim lucene/build.properties あとは、以下のコマンドを順番に実行することで、kuromojiのjarが生成されます。 $ ant ivy-bootstrap $ ant compile $ cd lucene/analysis/kuromoji $ ant clone-neologd $ ant build-dict $ ant jar-core 以下のディレクトリにjarが生成されます。 lucene/build/analysis/kuromoji/ jarのファイル名は以下のようなものになります。 実際にはSolrのバージョン名の部分とneologdのバージョン名の部分が変わり得ます。 lucene-analyzers-kuromoji-ipadic-neologd-6.3.0-SNAPSHOT-20161128.jar Solrに読み込ませる 上記の手順によってつくられたjarファイルを以下のパスに配置することで、Solrに読み込ませることができます。 server/solr-webapp/webapp/WEB-INF/lib クラス名を変えていないため、もともとあったjarを上書きする必要があります。 読み込まれたことの確認 さて、このjarが正しく読み込まれたことをsolrのweb consoleで確認してみましょう。 solrのweb consoleにはtokenizeの結果をその場で確認する機能があるので、それを使って確認します。 Core Selectorから適当なcoreを選択した後に、Analysisを選択します。 テキストボックス内に文章を打ち込んで、Analysis Valuesボタンを押すと、その文章の解析結果が表示されます。 以下の例では「ロクシタン」という固有名詞が1つの単語として認識されていることが分かります。 neologdを組み込んだことによる問題と、その対処 ブランド名の部分一致検索ができなくなった neologdを組み込むことで辞書の語彙が大幅に増えましたが、増えすぎることによる問題も発生してしまいました。 複数語からなるブランド名を部分一致検索できなくなってしまいました。 例えば JIMMY CHOO というブランド名が1つの単語としてtokenizeされるために、 JIMMY という検索クエリでヒットしなくなってしまいました。 この問題に対しては、ngramでtokenizeしたフィールドに対しても同時にクエリを投げることで解決しました。 ngramでtokenizeしたフィールドを併用することによって、このような検索漏れを無くすことができました。 まとめ VASILYではiQONを一緒に作っていく仲間を募集しています。 興味のある方は以下のリンクからご応募お願いいたします。
こんにちは、バックエンドエンジニアのjoeです。主にAPIを担当しています。 VASILYのAPIでは、速度向上のためにModelオブジェクトをキャッシュしています。 最近、Modelキャッシュの仕組みを実装したので、その実装方法を紹介します。また、既存ライブラリとの比較についても書きたいと思います。 Modelキャッシュとは Modelキャッシュを簡単に言うと、下記の結果をキャッシュすることです。 > Item .find( 1 ) => #<Item:0x007fdfe398a678> このように、1レコード単位のActiveRecordをキャッシュすることを本記事ではModelキャッシュと呼びます。ActiveRecordをキャッシュすることで、データベースへの読み込み回数を減らし、レスポンス速度を向上させることができます。 既存ライブラリの紹介と問題点 Modelキャッシュを実現できるGemとして、 IdentityCache というShopify社の優秀なライブラリが存在します。 ActiveRecordをキャッシュする際、 has_many や has_one などのリレーションで紐付けられたレコードも同時にキャッシュで引けたり、LRUキャッシュで実装してあったりと機能が豊富です。 その他にも、 CachingPolymorphicAssociations(複数外部キーの関連性の指定) CachingAttributes(キャッシュするカラムの指定) SecondaryIndexes(primary-key以外での検索) MemoizedCacheProxy(メモ化機能) 等があり、様々な機能が提供されています。 ただし、 削除が失敗して情報が更新されないことがあります。 (IdentityCacheの説明文では、「プロセスが死んだり、ネットワークの不調などで削除が失敗することがあるので一貫性を保証できない。大事な場所では使うな」と書いてあります。) This is because there is no way IdentityCache is ever going to be 100% consistent. Processes die, execeptions happen, and network blips occur, which means there is a chance that some database transaction might commit but the corresponding memcached DEL operation does not make it. This means that you need to think carefully about when you use fetch and when you use find. For example, at Shopify, we never use any fetchers on the path which moves money around, because IdentityCache could simply be wrong, and we want to charge people the right amount of money. 情報の更新が失敗することや、現時点で使っていない機能が多かった事から、今回はModelオブジェクトをキャッシュしてシンプルに主キーでキャッシュを引くだけの実装を試してみました。 Modelキャッシュの仕組み 仕組みはシンプルで、ActiveRecordオブジェクトの必要な部分をキャッシュするだけです。 使うときは、キャッシュした内容をもとにActiveRecordオブジェクトを復元して使います。 ActiveRecordオブジェクトに復元することで、レコードの要素に簡単にアクセスできたり、Modelに実装してあるメソッドを使えるのでとても便利です。 例えばModelキャッシュの仕組みを fetch というメソッド名で実装したとしたら下記の例のようにActiveRecordと全く同じ用途で使えます。 class User < ActiveRecord :: Base include ModelCache has_many :my_book def young? age < 18 end end >user = User .fetch( 1 ) => #<User:0x007fdfe398a678> # UserのActiveRecordオブジェクト >user.id => 1 >user.age => 11 >user.young? => true >user.my_book => [ #<Book:0x00394fd87987639>, #<Book:0x007fdfe398a679>] 実装の概要 下記の実装例ではActiveRecordの必要な情報を抜き出したものを coder , ActiveRecordそのままのオブジェクトを record という変数に置いています。 # ./lib/model_cache.rb module ModelCache extend ActiveSupport :: Concern # 各レコードが更新された際のキャッシュ削除 included do | base | base.after_commit do | _record | self .class.expire_model_cache(id) end end module ClassMethod def fetch (id) coder = memcache.get(key) # coderをrecordに変換して返す return record_from_coder(coder) if coder.present? record = find(id) # recordをcoderに変換してキャッシュする coder = coder_from_record(record) memcache.set(coder) record end # 複数IDを受け取るメソッド def fetch_multi (*ids) # キャッシュにキーがないidは一括でデータベースに問い合わせて渡されたid順に並べてActiveRecordオブジェクトを返す end # ActiveRecordの必要最低限に絞る def coder_from_record (record) { attributes : record.attributes_before_type_cast.dup, record_class : record.class } end # ActiveRecordの復元 def record_from_coder (coder) klass = coder[ :record_class ] klass.instantiate(coder[ :attributes ].dup) end def expire_model_cache (id) # キャッシュの削除 end end end あとは実際にincludeするのみです。ActiveRecordオブジェクトを復元する部分とActiveRecordから必要な情報を取り出す部分以外はキャッシュして取り出すだけの実装なので、難しくありません。 (今回はざっくり書きましたが、後日コードを公開します。) Modelをキャッシュして主キーで引く(単一のキーと複数のキーが存在する)という用途で必要なのはこれだけです。 様々な機能を削りましたが、アプリケーションはこれだけでも正常に動きます。 既存ライブラリと自前実装のメリット・デメリット  IdentityCache 自前実装 メリット ・できることが多い ・LRUキャッシュで実装されている ・好きなように拡張しやすい ・キャッシュキーを自由に決定できる ・シンプルなのでバグが追いやすい デメリット ・キャッシュの削除がたまに失敗する ・コードが複雑 ・やりたいことが増えるとそこそこの工数がかかる 既存ライブラリとの速度比較 IdentityCacheと自前実装の速度を比較してみました。 # 下記を1000回実行した平均を計測 ids = [ 1 .. 100 ] ids.each do | i | Test .fetch(i) end Test .fetch_multi(ids) キャッシュバックエンドをRails.cacheに揃えた場合 ほんのすこしですが機能を削った分速くなっています。 内容 user(ms) system(ms) total(ms) real(ms) 自前実装(single) 20 10 30 (26.562) IdentityCache(single) 20 10 30 (30.014) 自前実装(multi) 20 0 10 (21.368) IdentityCache(multi) 20 0 20 (24.547) 自前実装のキャッシュバックエンドに arthurnn/memcached を使った場合 キャッシュバックエンドをRails.cacheから arthurnn/memcached にするとmultiで引く際の速度は2倍になります。singleの方は1.2倍程度速くなります。 自前実装なら簡単にキャッシュバックエンドを変えることができるのもメリットの一つです。 内容 user(ms) system(ms) total(ms) real(ms) 自前実装(single) 20 0 20 (23.451) IdentityCache(single) 20 10 30 (29.303) 自前実装(multi) 10 0 10 (10.967) IdentityCache(multi) 20 10 30 (23.824) まとめ Modelキャッシュは機能をそぎ落とせばかなり簡単な実装で実現できます。 IdentityCacheを使うのは敷居が高い、そこまでの機能はいらないと感じている方は軽量な自前実装で試してみてはいかかでしょうか。 VASILYではバックエンドチームで一緒に開発してくれるエンジニアを募集しています。 興味がある方は以下のリンクをご覧ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
データサイエンティストの中村です。今回はイメージファーストなファッションアイテム検索システムを作ってみたのでそちらの紹介をしたいと思います。 本記事で紹介する技術は IBIS2016 でも報告しています。 概要 ファッションアイテムを探すとき、見た目の印象はとても大事な要素です。ファッションは感覚的なものなので、自分が欲しい服について言葉で説明することは難しいですが、そのアイテムの良し悪しは画像を見ただけで判断できるからです。 今回開発した検索システムは見た目の印象を大事にしたいので、 画像をクエリ とします。ただし、ただの画像検索では面白くないので、 色や形状などの属性情報を付加した状態で検索 を実行できるようにしました。 例えば、「シルエットは良いんだけど、これの赤いやつが欲しい」のような感覚的な注文を、以下のGIFのように画像に属性を付加する形で拾っています。 よくある検索システムではカテゴリによる絞込やフリーワードがクエリになりますが、この方式でカバーできていない部分を解決できればと考えて作りました。 アイデア アイテムの画像に属性を付加するわけですが、付加する属性の要素の大きさに応じてアイテムの画像が連続的に変化してくれると嬉しいです。 このような性質を持つ空間を生成モデルで張れないかと考えました。 参考にしたのはLarsen2015 *1 です。論文中のFigure 5. に属性が連続的に変化している様子が確認できます。これと同じように、例えば「Red」という属性を付与したら、元のアイテムの印象を残したまま色だけ赤く変化する状態を目指します。 モデル Larsen2015を改良しました。論文中の手法をそのまま使って実験しても思い通りに行きませんでした。顔の画像とドレスの画像は勝手が違うようです。 モデルを変えながら試行錯誤をした結果、以下のモデルで結果がだいぶ改善しました。 Discriminatorの出力に、属性を予測するレイヤーを追加し、予測誤差をDiscriminatorとGeneratorに返すようにしています。予測する属性はひとつとは限らないので、softmaxではなくsigmoidを使っています。 属性予測タスクを追加することで、学習が安定し、再構成がきれいにできるようになりました。 なお、GANにタスクを追加してモデルを改善する例は他にもInfoGAN *2 やACGAN *3 などが報告されています。 検索 上記のモデルを学習させた後、学習済みEncoderを使って検索対象のアイテムから特徴抽出します。アイテムの特徴量空間が完成しました。 アイテムを検索するには、構築した空間内でクエリの近傍を取得すれば良いです。この操作は検索対象アイテム数が増えると計算量が増加するため、近似を使って効率よく近傍探索できるアルゴリズムを用いました。 近似近傍探索に関しては Annoy はとても優秀なライブラリなので、近傍探索に興味のある方はぜひ触ってみてください。 結果 属性を付加しない場合の検索結果(通常の画像検索)と属性を付加した場合の検索結果を載せます。 左は「Red」という属性を足した場合で、クエリ画像とよく似た赤いアイテムがヒットすれば成功です。右は「Long sleeve」という属性を引いた場合です。半袖やノースリーブのアイテムが出てくることを想定しています。 現状、色や形状は比較的結果が安定しています。ただし、素材やシチュエーションのように、画像から判断し辛いものやコンテキストが必要な属性は失敗もあります。 まとめ 画像に属性を付加してファッションアイテムを検索する仕組みについて紹介しました。VAEとGANを組み合わせたモデルに予測タスクを課すことで学習が安定し、検索結果も改善することが確認できました。 とはいえ検索結果にはまだムラがあったり、複数の属性の組み合わせだと精度が低下したりと課題も多いので、精度改善の必要性を感じています。 雑談 今回は生成モデルでアプローチしましたが、この記事の執筆に着手した直後にUpchurch2016 *4 の存在を知りました。 詳しくはarXivに上がっている 論文 を見ていただきたいのですが、ほぼ同じタスクを識別モデルを使って鮮やかに解いています。 生成モデルでは扱えないような高解像度な画像について違和感なく属性を変化させている様子が紹介されていて、その精度の高さに驚きました。 雑談2 本記事の内容はIBIS2016でも発表しています。その時の様子などをまとめたブログも公開していますので、そちらもぜひ合わせてご覧ください。 tech.vasily.jp 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 *1 : A. B. L. Larsen, S. K. Sønderby, and O. Winther. Autoencoding beyond pixels using a learned similarity metric. arXiv, 2015. *2 : X. Chen, Y. Duan, R. Houthooft, J. Schulman, I. Sutskever, and P. Abbeel. InfoGAN: Interpretable Representation Learning by Information Maximizing Generative Adversarial Nets. arXiv, 2016. *3 : A. Odena, C. Olah, and J. Shlens. Conditional image synthesis with auxiliary classifier gans. arXiv, 2016. *4 : P. Upchurch, J. Gardner, K. Bala, R. Pless, N. Snavely, and K. Weinberger. Deep Feature Interpolation for Image Content Changes. arXiv, 2016.
こんにちは、データチームの後藤です。 VASILYデータチームは2016年11月16日~18日にかけて、京都大学で行われた第19回情報論的学習理論ワークショップ(以下、IBIS2016)に参加しました。本記事では、発表の様子や参加した感想をお伝えしたいと思います。 IBIS2016 IBISは、機械学習に関する国内最大規模の学会です。機械学習や統計学、情報理論などの理論研究や、機械学習の応用的な研究が対象となります。VASILYがこれまで機械学習を応用した様々なサービスを開発しており、その中でもファッションアイテムの検索システムはオリジナルの貢献を含んでいるので、今回発表することに決めました。 世の中の人工知能や深層学習への期待感からか、今年は例年よりも参加者が格段に増えたようです。具体的な数は聞かされていませんが、論文・ポスター合わせて200程度の発表があったので、参加者はその2~3倍はいるのではないでしょうか。例年、ポスタープレビューの一人あたりの持ち時間が1分だったところ、今年は一人30秒に削減されたほどです。 発表 我々は、初日のディスカッショントラックで 「VAEとGANを活用したファッションアイテムの特徴抽出と検索システムへの応用」 というタイトルで研究・開発の成果を発表しました。 参加機関は大学や研究所、企業など様々で、企業の発表は全体の1/4ほどを占めていました。その中でもほとんどは大企業の研究所で、VASILYのようなITベンチャーは他にありませんでした。発表内容もファッションということで、かなり異色で「みんなファッションなんて興味を持たないんじゃ。。。」と不安がよぎりましたが、蓋を開けてみれば2時間半の間、息をつく暇もないほどの盛況で、ずっと喋り続けることになりました。事前に準備していた配布用資料も配り尽くしてしまい、数が足りないほどでした。 ディスカッション・トラックでは、皆様が積極的に内容を理解しようとしてくれたので、とてもやりがいがありました。近傍探索アルゴリズムや評価方法についてもアドバイスをいただけて、たいへん有意義な時間でした。ディスカッション・トラックを聞きに来てくださった皆様、ありがとうございました。 技術的な詳細は、別の記事にしましたので、目を通して頂けると幸いです。 tech.vasily.jp 感想 この学会を通じて、機械学習の様々な研究に触れることで、その応用範囲の広さに驚きましたし、特に医療やヘルスケア、車の自動運転への応用などは世の中の期待を背負っているなと感じました。 今年のIBISのテーマは「ブームを乗り越える」ということで、招待講演では近年の人工知能・機械学習ブームをブームのままで終わらせないための、基礎研究の進展や実際に解決すべき問題が紹介されました。 深層学習の基礎的な理解のために理論解析を試みたり、神経細胞の信号伝達の優れた点を実験的、数理的な側面から理解しようという研究があるようです。深層学習で現実の問題を解決するのに生物と同じやり方である必要はないといった立場だったり、逆に生物が行っているやり方を深く追求することで、脳の情報処理の本質的な理解を得ようとする研究だったり、様々な観点の研究があることも知りました。 我々は技術とデザインの力でより便利なサービスを仕立てることが仕事なので、研究者とは目的が大きく異なります。極端に言えば、機械学習の基礎的な理解よりも、精度を出すことのほうが大事だったりします。しかし、我々が活用している技術を完全に理解し、高速化・高精度化を目指すのであれば、基礎研究をきちんと追いかける必要があると感じました。 ブームを乗り越えるという意味では、我々がやっている機械学習の研究を実際に使えるサービスに変えていくという仕事も、その一旦を担うことができるのではと考えています。データチームはファッション×IT領域特有の問題を機械学習を使って解決し、少しずつ着実に実績を作っています。将来的には、ファッションの分野においてVASILYの技術が必要不可欠であるようになれば大成功でしょうか。 来年のIBIS2017は東京大学本郷キャンパスで行われるようです。データチームはすでにどんな内容を持っていこうかと相談中です。皆様がどんなふうにブームを乗り越えてくるのか、今からとても楽しみですね。 幾つか、講演の発表資料が公開されていますので、リンクをまとめておきます。講演資料が無いものに関しては紹介されていた論文のリンクを張っています。 IBIS2016 招待講演・企画セッション資料 鈴木大慈「低ランクテンソルの学習理論と計算理論」 http://www.slideshare.net/trinmu/ibis2016 岡野原大輔「深層学習は世界をどのように変えられるのか」 http://www.slideshare.net/pfi/ibis2016okanohara-69230358 瀧川一学「科学と機械学習のあいだ:変量の設計・変換・選択・交互作用・線形性」 http://www.slideshare.net/itakigawa/ss-69269618 恐神貴行「動的ボルツマンマシン」 http://ibisml.org/ibis2016/files/2016/09/IBIS2016-osogami.pdf IBIS2016 講演関連論文 山田誠「Beyond Ranking: Optimizing Whole-Page Presentation」 http://www-personal.umich.edu/~raywang/pub/wsdm402-wang.pdf 気になった研究 D2-24: b-GAN: 密度比推定の視点から見たGenerative Adversarial Nets 上原雅俊(東京大学)、佐藤一誠(東京大学)、鈴木雅大(東京大学)、中山浩太郎(東京大学)、松尾豊(東京大学) 密度比推定によって最適化するGANを提案した。密度比推定の知見を使うことができるため、理論的に扱いやすい。 D2-30: Weight Normalizationに基づく自然勾配法の実現 唐木田亮(東京大学大学院新領域創成科学研究科),岡田真人(東京大学大学院新領域創成科学研究科),甘利俊一(理研BSI) パラメータの最適化を、動径と方向の座標成分に分解して最急降下させる勾配法が何故うまくいくのかを理論的に明らかにし、より良い手法を提案した。 D2-44: Theoretical Analysis for Parameter Transfer Learning 熊谷亘(神奈川大学) https://arxiv.org/abs/1610.08696 転移学習の理論解析の話。サンプル数と転移学習可能性についての関係が得られている。 おまけ 学会の合間に、秋の京都を満喫しました。紅葉がピークを迎えており、非常に美しい紅葉を拝むことができました。 南禅寺(夜) 三千院(早朝) 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。
こんにちは。 今日も元気にクローラー作成!バックエンドエンジニアのりほやんです。 最近クローラーを作成する機会が多く、その時にXPathが改めて便利だと思ったので XPathについてまとめてみました!XPathを学ぶ方の役に立てれば幸いです。 初級編 XPathとは XPathはXML文章中の要素、属性値などを指定するための言語です。 XPathではXML文章をツリーとして捉えることで、要素や属性の位置を指定することができます。 HTMLもXMLの一種とみなすことができるため、XPathを使ってHTML文章中の要素を指定することができます。 例えば、 < html > ... < body > < h1 > ワンピース </ h1 > < div class = "item" > < span class = "brand" > iQON </ span > < span class = "regular_price" > 1,200円 </ span > < span class = "sale_price" > 1,000円 </ span > </ div > </ body > </ html > このようなHTMLの場合であれば、ざっくりと下記のようなツリー構造に表すことができます。 XPathはこのようなツリー構造から要素を取得します。 XPathの基礎 ロケーションパス XPathは、ロケーションパスによって表されます。 ロケーションパスとは、ツリー構造から特定の要素を指定するための式のことです。 ロケーションパスは、URLのように『/』で要素を繋げて書きます。 < html > ... < body > < h1 > ワンピース </ h1 > < div class = "item" > < span class = "brand" > iQON </ span > < span class = "regular_price" > 1,200円 </ span > < span class = "sale_price" > 1,000円 </ span > </ div > </ body > </ html > このHTMLにおいて『h1要素』を取得するXPathは、 ツリー構造の上から順に『html要素→body要素→h1要素』と指定します。 ロケーションパスで表すと、 /html/body/h1 このようなXPathになります。 中級編 属性について classのような要素に紐づく属性をXPathでは『@』で表します。 < html > ... < body > < h1 > ワンピース </ h1 > < div class = "item" > < span class = "brand" > iQON </ span > < span class = "regular_price" > 1,200円 </ span > < span class = "sale_price" > 1,000円 </ span > </ div > </ body > </ html > この『1,200円』という要素を取得したい場合は、属性を用い下記のように書くことができます。 /html/body/div/span[@class='regular_price'] //を用いて途中までのパスを省略 /html/body/div/span[@class='regular_price'] このXpathを『//』を用いて、ノードパスを省略することができます。 『//』は、descendant-or-selfの省略形です。 すなわち起点となるノードの子孫すべての集合を表します。 例えば、 /html/body/div/span[@class='regular_price'] このXPathを『//』を用いて省略すると、下記のように書くことができます。 //span[@class='regular_price'] 指定する文字列が含まれている要素を取得する: contains containsは、指定する文字列が含まれている要素を取得します。 < img class = "large_image" > < img class = "small_image" > < img class = "thumbnail" > 上記のHTMLからclassにimageがつくものをすべて取得したい場合、『contains』を用いることができます。 contains関数は、第1引数文字列内に、第2引数文字列が含まれているかどうかを調べる関数です。 classにimageがつく要素すべて取得する、という条件をcontainsを用いて表すと下記のような書き方になります。 //img[contains(@class, 'image')] このXPathは、classにimageを含むimg要素を取得するという意味になります。 またテキスト中に含まれている文字を指定したい場合には、text()とcontainsを組み合わせます。 < div class = "item" > < h1 > ワンピース </ h1 > < div class = "price" > 1,200円 </ div > < div class = "description" > 冬に最適なニットワンピースです。 品番:100000000 </ div > </ div > </ div > このHTMLから『品番』という文字を含んでいる要素を指定したい場合は、 //div[contains(text(), '品番')] と書くことができます。 さらに、『指定する文字を含むJavaScript』を取得する場合は、下記のように書くことができます。 //script[contains(text(), 'stock')] 要素の位置を指定: position 要素の位置を指定したい場合はpositionを使用します。 positionは、指定したノードから何番目のノードかを指定することができます。 < ul > < li > 色を選択 </ li > < li > ホワイト </ li > < li > レッド </ li > < li > ブルー </ li > </ ul > このHTMLでposition()を使ってみます。 position() = 上記のHTMLで『レッド』はli要素の3番目のなのでpositionを用いて //li[position()=3] と表すことができます。 またposition()=3を省略し //li[3] と書くこともできます。 position() > 『色を選択』以外のli要素を取得する場合はpositionを用いて下記のように表すことができます。 //li[position()>1] 『色を選択』はli要素の1番目であるため、position()>1は、『色を選択』以外のli要素を指定します。 テキストノードの取得 要素内のテキストを取得したい場合は、『text()』というテキストノードを用います。 < p > Sサイズ < span > レッド </ span ></ p > このHTMLから『Sサイズ』という文字列のみを取得したい場合は、text()を用いて //p/text() と書くことができます。 not notは述部にて否定を表します。 < img src = "http://sample.ne.jp/sample_main_image.jpg" > < img src = "http://sample.ne.jp/sample_sub_image.jpg" > < img src = "http://sample.ne.jp/sample_thumbnail.jpg" > このHTMLから http://sample.ne.jp/sample_main_image.jpg 以外の @srcを取得したい場合はnotを用いて //img[not(contains(@src, 'main'))]/@src と書くことができます。 or or条件をXPathで使うことができます。 < img src = "http://sample.ne.jp/sample_100.jpg" > < img src = "http://sample.ne.jp/sample_200.jpg" > < img src = "http://sample.ne.jp/sample_300.jpg" > < img src = "http://sample.ne.jp/sample_400.jpg" > < img src = "http://sample.ne.jp/sample_500.jpg" > < img src = "http://sample.ne.jp/sample_600.jpg" > このHTMLから、100か300を含むsrcを取得したい場合は、orを用いて下記のように書くことができます。 //td[contains(@src,'100') or contains(@src, '300')] また、100か300以外のsrcを取得したい場合は、notとorを組み合わせます。 //td[not(contains(@src,'100') or contains(@src, '300'))] and and条件も、XPathで使うことができます。 < img src = "http://sample.ne.jp/main_100.jpg" > < img src = "http://sample.ne.jp/main_300.jpg" > < img src = "http://sample.ne.jp/sub_100.jpg" > < img src = "http://sample.ne.jp/sub_300.jpg" > < img src = "http://sample.ne.jp/thumbnail_100.jpg" > < img src = "http://sample.ne.jp/thumbnail_300.jpg" > このHTMLから、『main』と『300』を含むsrcを取得したい場合は、 andを使って下記のように書くことができます。 //img[contains(@src, 'main') and contains(@src, '300')] 上級編 軸・ノードテスト・述部 ロケーションパス内で要素を表現する際、『軸・ノードテスト・述部』と呼ばれるものを用いて表現します。 名前 説明 軸 ツリー上の位置関係を指定する ノードテスト 選択するノードの型と名前を指定する 述部 選択するノードの集合を、任意の式を使用してさらに細かく指定する 先ほどの /html/body/h1 というXPathはノードテストのみで要素を表していました。 ノードテストだけでは欲しい要素を取得できない場合は、軸や述部を使用することで細かく要素を指定することができます。 述部について ツリー図に、classなどの属性の情報を加えたものが下記の図になります。 軸 軸は、ツリー上の位置関係を指定するものです。 軸の代表的なものとして、以下のような種類があります。 名前 説明 self ノード自身を表す child ノードの子ノードの集合 parent ノードの親ノードの集合 ancestor ノードから祖先ノードの集合(親も含む) descendant ノードから子孫ノード集合 following ノードの後に出てくるノードの集合 preceding ノードの前に出てくるノードの集合 following-sibling ノードと同じ階層にあり、かつ後に出てくる兄弟ノードの集合 preceding-sibling ノードと同じ階層にあり、かつ前に出てくる兄弟ノードの集合 軸を先ほどまでの図に加えてみます。 どこを起点として考えるかによって位置関係は変わりますが、今回は『div』を中心に軸を考えます。 今回はdivを中心に考えるため、div自身は起点となるノードつまり『self』になります。 bodyは、divから見て一つ上の階層つまり親となるので『parent』、 3つのspanは、一つ下の階層つまり子となるので『child』となります。 また、h1要素は、divと同じ階層かつdivより前に出現するので、軸は『preceding-sibling』となります。 childの省略について 明示的に軸を指定しない場合は、軸がchildとみなされます。 そのため基本的にchildは省略することができます。 軸::ノードテスト[述語] 軸・ノードテスト・述部を用いてXPathを書く場合、『軸::ノードテスト[述部]』という書き方で要素を指定します。 < html > ... < body > < h1 > ワンピース </ h1 > < div class = "item" > < span class = "brand" > iQON </ span > < span class = "regular_price" > 1,200円 </ span > < span class = "sale_price" > 1,000円 </ span > </ div > </ body > </ html > このHTMLから『1,200円』の要素を取り出すXPathは下記のように書くことができます。 /html/body/div/span[@class='regular_price']/self::text() また、省略した形で書くと、こうなります。 //span[@class='regular_price'] ここまでざっくりと軸・ノードテスト・述部を説明しました。 軸・ノードテスト・述部についてもっと詳しく知りたい方は、下記のページがすごくわかりやすいのでぜひ参考にしてみてください。 http://www.techscore.com/tech/XML/XPath/XPath3/xpath03.html/ 指定された要素より後の兄弟要素を持ってくる: following-sibling:: 軸のところで紹介しましたが『following-sibling::』は、起点となるノードと同じ階層にあり、かつ起点となるノードより『後』に出てくる兄弟ノードの集合を表す軸です。 この『following-sibling::』は、テーブル要素を指定するときに大活躍します。 < table > < tr > < td > 生産国 </ td > < td > 日本 </ td > </ tr > < tr > < td > 素材 </ td > < td > 綿 </ td > </ tr > </ table > このようなテーブルが用意されている場合、『綿』をどのように取得したらよいでしょうか。 //td[4] と書くこともできますが、td要素が増えたり減ったり変化がある場合、 //td[4] の指定する要素が変わり対応できません。 そこで、『following-sibling::』を使います。 綿を取得したい場合は、 //td[contains(*, '素材')]/following-sibling::td[1] と書くことができます。 指定された要素より前の兄弟要素を持ってくる: preceding-sibling:: 『preceding-sibling::』はfollowing-sibling::の対となる軸で、起点となるノードと同じ階層にあり、かつ起点となるノードより『前』に出てくる兄弟ノードの集合を表す軸です。 こちらもテーブル要素を指定するときに大活躍します。 < table > < tr > < td class = "title" > 22cm </ td > < td class = "title" > 23cm </ td > < td class = "title" > 24cm </ td > </ tr > < tr > < td class = "title" > レッド </ td > < td class = "inventory" > 在庫あり </ td > < td class = "inventory" > 在庫あり </ td > </ tr > < tr > < td class = "title" > ブルー </ td > < td class = "inventory" > 在庫あり </ td > < td class = "inventory" > 在庫なし </ td > </ tr > < tr > < td class = 'title' > グリーン </ td > < td class = 'inventory' > 在庫あり </ td > < td class = 'inventory' > 在庫あり </ td > </ tr > </ table > 上記のようなHTMLにおいて、色(レッド, ブルー, グリーン)をすべて取得したい場合、どのようなXPathで取得できるでしょうか。 単に、 //td[@class='title'] では色だけでなくサイズも取得してしまいます。 このようなときに、preceding-siblings::を用います。 //td[@class="inventory"][1]/preceding-sibling::td このように書くことで、class属性がinventoryのtd要素の1番目(td[@class='inventory'][1])の1つ前の要素(preceding-sibling::td)すなわち色を取得することができます。 重複なく抽出 < table > < tr > < td > レッド </ td > < td > レッド </ td > < td > レッド </ td > </ tr > < tr > < td > ブルー </ td > </ tr > < tr > < td > グリーン </ td > </ tr > </ table > このHTMLから重複なく色を取得したい場合は下記のように書けます。 //td[not(.=preceding::td)] このXPathは、td要素の中でprecedingすなわち前に出てくる要素と一致しないものを取得しています。 XPath関連便利サービス 最後に、XPathを取得する時にオススメの拡張機能を紹介します。 『XPath Helper』です!! XPath Helper XPath Helperは、ブラウザから要素をカーソルに合わせるだけでXPathを調べることができる超優れたchrome拡張機能です。 下記からダウンロードができます。 https://chrome.google.com/webstore/detail/XPath-helper/hgimnogjllphhhkhlmebbmlgjoejdpjl?hl=ja この拡張機能を使い方は、 chromeブックマークバーのxと書いてあるアイコンをクリック、またはショートカットキー[Ctrl + Shift + X]で起動します。 そして、取得したい要素をシフトを押しながら選択すると、要素のXPathが簡単に取得できます。 とても簡単にXPathが取得できるのでオススメです! まとめ 以上が、クローラーに便利なXPathまとめでした! XPathは比較的覚えやすく理解しやすい言語ですので、非エンジニアの方にもとてもオススメです。 ぜひXPath Helperを入れて、XPathを試してみてください! VASILYではiQONを一緒に開発してくれるエンジニアを募集しています。 興味がある方は以下のリンクをご覧ください。 https://www.wantedly.com/projects/61389 www.wantedly.com
こんにちは、VASILYバックエンドエンジニアの塩崎です。 VASILYでは様々なログデータの分析にBigQueryを使用しています。 インデックスについて何も考えなくても良いのが特に便利です。 さて、そんなBigQueryですが、数か月前にStandard SQLという新しい仕様のSQLがサポートされました。 BigQuery 1.11, now with Standard SQL, IAM, and partitioned tables! VASILYでも徐々にStandard SQLに移行をしているので、使い勝手や従来のSQLからの移行方法についてまとめておきます。 Standard SQLとは SQL:2011に準拠しつつ、配列や構造体等の構造化データを扱えるように拡張されたSQLです。 Standard SQLの登場によって、以前からあったSQLはLegacy SQLと呼ばれるようになりました。 発表された当初はほとんどの機能がβ版でしたが、最近ではDML機能を除いた全てがリリース版になっています。 すぐに移行する必要があるかどうか 現状ではLegacy SQLもStandard SQLもサポートされています。(2016/11/10現在) そのため、Legacy SQLで書かれたクエリがすぐに動かなくなるということはありません。 以下の公式ドキュメントでは緩やかな移行を推奨しています。 Do I have to migrate to standard SQL? Migration from legacy SQL to standard SQL is recommended but not required. https://cloud.google.com/bigquery/docs/reference/standard-sql/migrating-from-legacy-sql Standard SQLの使い方 Standard SQLでクエリを投げるためには、大きく分けて2つの方法があります。 クエリを投げる時のオプションで指定する方法と、SQL文中で指定する方法です。 クエリを投げる時のオプションで指定 Web UI Web UIからクエリを投げる時には以下のようにして、Standard SQLを指定します。 Show Optionをクリックして、オプション設定を開く。 Use Legacy SQLのチェックを外します。 bqコマンド bqコマンドを使いコマンドラインからクエリを投げる時には --use_legacy_sql=false オプションを付与することで、Standard SQLを指定します。 $ bq query --use_legacy_sql=false "SELECT ..." プログラムから呼ぶ場合 プログラムからクエリを呼ぶ場合は各言語のBigQuery Client Libraryのドキュメントを参考にしてください。 https://cloud.google.com/bigquery/docs/reference/libraries SQL文中で指定 SQL文の文頭に #standardSQL というコメント行を書くことで、Standard SQLを指定します。 この方法はWeb UI、bqコマンド、REST APIの全てで使うことができます。 Standard SQLの利点 その場でビューを作れるようになった WITH句を使うことで、その場でビューをつくることができるようになりました。 #standardSQL WITH T AS ( SELECT 1 AS x UNION ALL SELECT 2 AS x UNION ALL SELECT 3 AS x ) SELECT * FROM T 複数個のビューをその場で定義することも、自分よりも上で定義されたビューを参照することもできます。 #standardSQL WITH T1 AS ( SELECT 1 AS x UNION ALL SELECT 2 AS x UNION ALL SELECT 3 AS x ), T2 AS ( SELECT x + 1 FROM T1 ) SELECT * FROM T2 複雑な分析をする時に入れ子になったサブクエリを整理する時に使うことができそうです。 ビューとして保存することに比べて、一覧性の向上、ビュー数の削減によるデータセットの整理などの利点が考えられます。 もちろん、複数の分析で共通して必要な処理は今まで通り、ビューとして保存することが適しています。 WITH句は1箇所からしか使われない処理をまとめる時に使うのが良いと思います。 また、サンプルデータを定義するためにも使うことができます。 これ以降の説明でも、WITH句を利用したサンプルデータの定義を用いていきます。 SELECTとFROMの間にサブクエリが書けるようになった Legacy SQLと比べてサブクエリを書くことができる場所が増えました。 例えば、以下のクエリで、最高気温が20度以上の日の割合を計算することができます。 #standardSQL WITH T AS ( SELECT '2016/11/1' AS date , 16 AS max_temperature UNION ALL SELECT '2016/11/2' AS date , 12 AS max_temperature UNION ALL SELECT '2016/11/3' AS date , 19 AS max_temperature UNION ALL SELECT '2016/11/4' AS date , 19 AS max_temperature UNION ALL SELECT '2016/11/5' AS date , 20 AS max_temperature UNION ALL SELECT '2016/11/6' AS date , 21 AS max_temperature UNION ALL SELECT '2016/11/7' AS date , 14 AS max_temperature ) SELECT COUNTIF(max_temperature >= 20 ) / ( SELECT COUNT( 1 ) FROM T) AS warn_days_fraction FROM T 相関サブクエリが使えるようになった サブクエリの内側からサブクエリの外側のテーブルを参照できるようになりました。 従来ではJOINを利用せざるをえなかった処理をシンプルに書くことができそうです。 以下のクエリでは、都市ごとにその都市の平均気温以上の日を選択するクエリを相関サブクエリを用いて書いています。 where t1.city = t2.city の部分でサブクエリの外側のテーブルt1を参照しています。 #standardSQL WITH T AS ( SELECT '2016/11/1' AS date , 16 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/2' AS date , 12 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/3' AS date , 19 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/4' AS date , 19 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/5' AS date , 20 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/6' AS date , 21 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/7' AS date , 14 AS max_temperature, 'Tyokyo' AS city UNION ALL SELECT '2016/11/1' AS date , 4 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/2' AS date , 6 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/3' AS date , 4 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/4' AS date , 4 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/5' AS date , 2 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/6' AS date , 4 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/7' AS date , 4 AS max_temperature, 'Sapporo' AS city UNION ALL SELECT '2016/11/1' AS date , 28 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/2' AS date , 24 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/3' AS date , 25 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/4' AS date , 26 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/5' AS date , 28 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/6' AS date , 28 AS max_temperature, 'Naha' AS city UNION ALL SELECT '2016/11/7' AS date , 28 AS max_temperature, 'Naha' AS city ) SELECT * FROM T AS t1 WHERE max_temperature > ( SELECT AVG(max_temperature) FROM T AS t2 WHERE t1.city = t2.city GROUP BY city ) 配列と構造体が使えるようになった 配列と構造体を使うことで非正規化データを扱うこともできます。 それぞれについて見ていきましょう。 配列 配列は要素を[]で囲むことで、宣言することができます。 #standardSQL SELECT [ 1 , 2 , 3 , 4 , 5 ] AS numbers 配列の要素にアクセスするためには、0オリジンでインデックスを指定するOFFSET関数、1オリジンでインデックスを指定するORDINAL関数を通す必要があります。 numbers[1] といったアクセス方法は出来ません。 #standardSQL WITH T AS ( SELECT [ 1 , 2 , 3 , 4 , 5 ] AS numbers ) SELECT numbers[OFFSET( 1 )], numbers[ORDINAL( 1 )] FROM T 配列を扱う上での注意点が2点あります。 1点目は配列の要素にNULLを含むことはできない点です。 #standardSQL SELECT [ 1 , 2 , 3 , NULL ] AS numbers # ERROR!! 2点目は配列の配列を作ることはできない点です。 #standardSQL SELECT [[ 1 , 2 ], [ 3 , 4 ]] # ERROR!! 入れ子になった配列のようなデータ構造を作る場合は、この後に紹介する構造体と組み合わせて、配列を要素としてもつ構造体の配列を作る必要があります。 配列の使い方については以下のドキュメントが詳しくまとまっています。 Working with Arrays 構造体 構造体は要素を()で囲むことで宣言することができます。 #standardSQL SELECT ( 1 , 1 . 23 , 'str' ) また、ASと組み合わせることで、各要素に名前をつけることができます。 #standardSQL SELECT STRUCT( 1 AS int, 1 . 23 AS float , 'str' AS string) .を使うことで、構造体中の特定の要素にアクセスすることができます。 #standardSQL SELECT STRUCT( 1 AS int, 1 . 23 AS float , 'str' AS string).int COUNT(DISTINCT(x))が正確な数を返すようになった COUNT(DISTINCT(x))の処理がサンプリングされなくなりました。 Legacy SQLのCOUNT(DISTINCT(x))は、Standard SQLではAPPROX_COUNT_DISTINCT(x)に相当します。 TIMESTAMP型の関数にtime zoneを渡せるようになった TIMESTAMP型を引数に取る多くの関数にオプションとしてtime zoneを渡すことができるようになりました。 例えば、TIMESTAMP型をSTRING型にする関数であるFORMAT_TIMESTAMP関数に対して、以下のようにtime zoneを渡すと日本標準時での現在時刻が得られます。 #standardSQL select FORMAT_TIMESTAMP( '%Y-%m-%d %H-%M-%S' , CURRENT_TIMESTAMP, 'Asia/Tokyo' ) time zoneをしていするための文字列は2種類あります。 1つ目はUTCからのオフセットを表す (+|-)H[H][:M[M]] 形式の文字列です。 2つ目は tz database に登録されている名前です。 日本標準時を指定する時には、time zoneに +9:00 もしくは Asia/Tokyo を指定すれば良いです。 なお、time zoneを省略するとUTCとみなされます。 時刻型、日付型の種類が増えた 時刻、日付を表す型として、以下の3つの型が追加されました。 DATE TIME DATETIME DATA型は日付のみを保存する型、TIME型は時刻のみを保存する型、DATETIME型は日付と時刻の両方を保存する型です。 DATETIME型もTIMESTAMP型も日付と時刻を保持する型ですが、どのように使い分けたら良いのでしょうか。 BigQueryのドキュメントには以下のように書かれています。 Unlike Timestamps, a DATETIME object does not refer to an absolute instance in time. Instead, it is the civil time, or the time that a user would see on a watch or calendar. これだけですと、いまいち分かりづらいので具体例を考えます。 例として、ユーザーのアクションをログに残すことを考えます。 そのログの中には、いつユーザーがアクションを起こしたかを表す時刻情報を入れます。 さて、この時に日本のユーザーとイギリスのユーザが同時にアクションをしたとします。 この時刻情報をTIMESTAMP型で保存する場合は、日本で発生したアクションも、イギリスで発生したアクションも同じ時刻がUTCで記録されるべきです。 一方DATETIME型で保存する場合は、それぞれの国の標準時で時刻が記録されるべきです。 イギリスの標準時刻はUTC+00:00で、日本標準時はUTC+09:00なので、イベントがイギリスと日本で同時に起こった場合は、日本で起こったイベントの方が9時間進んだ時刻で記録されるべきです。 また、サマータイムを導入している場合には、DATETIME型のみ時刻がずれるべきです。 上記のように使い分けをする意図があるため、DATETIME型とTIMESTAMP型の間で暗黙の変換はできなくなっています。 ※ 本節で使用した世界地図のイラストは いらすとや さんの素材を利用させていただきました。 UDFが使いやすくなった User Define Function(UDF)機能自体はLegacy SQLの時からありましたが、Standard SQLではその仕様がシンプルになり、使いやすくなりました。 Legacy SQLでURIデコードを行うUDFを使う時には、以下のように書く必要がありました。 function urlDecode(row, emit) { emit( { uri: decodeHelper(row.uri) } ); } function decodeHelper(s) { try { return decodeURI(s); } catch (ex) { return s; } } bigquery.defineFunction( 'urlDecode' , [ 'uri' ] , [{ name: 'uri' , type: 'string' }] , urlDecode ); #lagecySQL SELECT uri AS decoded FROM urlDecode( SELECT '%e3%83%86%e3%82%b9%e3%83%88' AS uri) 行いたい処理はJavaScriptのdecodeURI関数を呼ぶだけなのに、記述量がかなり多いです。 入出力される列名の定義や、エラーハンドリングの処理があるためです。 また、UDFに入出力される列が変わった時の修正範囲が広範囲です。 Standard SQLでは同様の処理は以下のように書くことができます。 #standardSQL CREATE TEMPORARY FUNCTION urlDecode(s STRING) RETURNS STRING LANGUAGE js AS """ try { return decodeURI(s); } catch (ex) { return s; } """ ; SELECT urlDecode( '%e3%83%86%e3%82%b9%e3%83%88' ) AS decoded Legacy SQLのUDFと比べるとすっきりとした見栄えになっています。 エラーハンドリングをしなくても良い場合は、さらにすっきりとします。 #standardSQL CREATE TEMPORARY FUNCTION urlDecode(s STRING) RETURNS STRING LANGUAGE js AS "return decodeURI(s);" ; SELECT urlDecode( '%e3%83%86%e3%82%b9%e3%83%88' ) AS decoded Legacy SQLのUDFと比較して、以下の点が変わっています。 SQLとUDFを同じフォームに入力できるようになった 入出力の単位がテーブルからセルになった それに伴い、入出力される列名の指定が不要になった 関数のシグネチャはCREATE TEMPORARY FUNCTION文で宣言されるため、JavaScriptのコードでは{}の内側だけを書けば良くなった また、SQLでUDFを作る機能もサポートされました。 #standardSQL CREATE TEMPORARY FUNCTION ADD_ONE(i INT64) AS (i + 1 ); SELECT ADD_ONE( 1 ) [2016/11/10現在ではβ版] DML機能が使えるようになった テーブルに対する、INSERT、UPDATE、DELETE文が実行できるようになりました。 ですが、制限が多いのでまだまだ実運用は厳しそうな感じがします。 以下のような制限があります。 Quotaが少ない(1日あたり、48 UPDATE/DELETE文) REQUIRED型を含むテーブルに対して更新処理を行うことができない 暗黙的にトランザクションのCOMMITがされる(ROLLBACKができない) streaming insertをしているテーブルに対してのUPDATE/DELETEはバッファーが空になるまでできない Standard SQLの欠点 以上でStandard SQLの利点を色々と紹介しましたが、欠点もあります。 それらも合わせて紹介していきます。 Legacy SQLのビューを参照できない Legacy SQLで作成したビューに対してクエリを投げることができません。 Standard SQLでクエリを投げるためには、それらのビューをStandard SQLの仕様を満たすように書き換える必要があります。 #standardSQL SELECT * FROM `mydataset.legacy_sql_view` # ERROR! Legacy SQLで作られたビュー資産を多く持っているグループでは移行作業が大変になりそうです。 Standard SQLで作ったビューをLegacy SQLから参照できない 前節とは逆方向の操作も行うことができません。 Legacy SQLとStandard SQLを混ぜることはできません。 #legacySQL SELECT * FROM [mydataset.standard_sql_view] # ERROR! そのため、Legacy SQLのビューが別のビューを参照している場合には、一連のビューを全てStandard SQLに移行する必要があります。 デフォルトではLegacy SQLが選択されてしまう Web UIやbqコマンドからStandard SQLを使う時には、Standard SQLを有効にするオプションを明示的に設定する必要があります。 そのため、Standard SQLの布教活動がややし辛いです。 プロジェクト単位でStandard SQLとLegacy SQLのどちらをデフォルトにするかの機能があると移行をスムーズに行うことができるかと思います。 UDFをビューに保存できない UDFをビューに保存しようとすると、以下のようなエラーメッセージが表示されてしまいます。 Failed to save view. No support for CREATE TEMPORARY FUNCTION statements inside views Standard SQLではUDFが作りやすくなったので、ぜひビューにUDFも保存したいです。 Save Viewはできませんが、Save Queryは問題なく行うことができます。 UDF付きのSQLを他人と共有したいときには、とりあえずは、こちらの機能を使うのがいいでしょう。 日本語の資料が少ない BigQuery Standard SQLでGoogle検索をしても日本語の資料がほとんど見つかりません。 公式ドキュメントも一部の章しか翻訳されていません。 現時点で最もまとまっている日本語資料は @ryok0607 さんによる、以下のslideです。 SQLおじさん(自称)がBigQueryのStandard SQLを使ってみた 本記事を書く際にも多くのことを参考にさせていただきました。 Legacy SQLからStandard SQLへの移行方法 さて、Standard SQLの利点、欠点について色々と説明しましたが、ここからはLegacy SQLからStandard SQLに移行する方法について説明します。 FROM句の [] を``に、:を.に入れ替え テーブル名をエスケープするための文字が、 [ ] から``になりました。 また、プロジェクト名とデータセット名の間の区切り文字が:から.になりました。 #LegacySQL SELECT * FROM [bigquery- public -data:samples.shakespeare] LIMIT 1 #standardSQL SELECT * FROM `bigquery- public -data.samples.shakespeare` LIMIT 1 REQUIREDの列をNULLABLEにする REQUIREDの列があるテーブルを対象にして、UPDATE/DELETEを行うことができないため、それらの列をNULLABLEにする必要があります。 そのテーブルに対する操作がSELECTしかないのであれば、この作業は不要です。 列の属性はWeb UIのTable Detailsから行うことができます。 型名の変換 いくつかの型はStandard SQLで別の名前に変わっています。 その対応表を以下に示します。 Legacy SQL Standard SQL 備考 BOOL BOOL INTEGER INT64 FLOAT FLOAT64 STRING STRING BYTES BYTES RECORD STRUCT REPEATED ARRAY TIMESTAMP TIMESTAMP - DATE STRING型を使えば、Legacy SQLでも似た機能を使える - TIME STRING型を使えば、Legacy SQLでも似た機能を使える - DATETIME STRING型を使えば、Legacy SQLでも似た機能を使える DATE型、TIME型、DATETIME型はStandardSQLで新たに登場した型なので、Lagecy SQLには等価な型はありません。 似た機能が欲しい場合はSTRING型などで対応しましょう。 また、型のキャストの方法も変更されています。 #legacySQL SELECT INTEGER ( '1234' ) #standardSQL SELECT SAFE_CAST( '1234' AS INT64) SELECT句の最後の,を削除 細かな修正ですが、SELECT句の最終要素の後ろの,が許可されなくなりました。 #legacySQL SELECT word, corpus, FROM [bigquery- public -data:samples.shakespeare] LIMIT 1 #standardSQL SELECT word, corpus FROM `bigquery- public -data.samples.shakespeare` LIMIT 1 FROM句の,をUNION ALLに変換 FROM句の中でテーブルを縦方向に結合するために,区切りでテーブルを並べている場合は、それをUNION ALLに変換する必要があります。 #legacySQL SELECT x, y FROM ( SELECT 1 AS x, "foo" AS y), ( SELECT 2 AS x, "bar" AS y); #standardSQL SELECT x, y FROM ( SELECT 1 AS x, "foo" AS y) UNION ALL ( SELECT 2 AS x, "bar" AS y); なお、Standard SQLではFROM句の中での,はJOINの代わりに使用することができます。 #standardSQL WITH T1 AS ( ( SELECT 1 AS x, "foo" AS y) UNION ALL ( SELECT 2 AS x, "bar" AS y) ), T2 AS ( ( SELECT 1 AS x, "hoge" AS z) UNION ALL ( SELECT 2 AS x, "fuga" AS z) ) SELECT a.x, a.y, b.z FROM T1 AS a, T2 AS b WHERE a.x = b.x テーブルワイルドカード関数の変換 Standard SQLではTABLE_DATE_RANGE関数、TABLE_QUERY関数などのテーブルワイルドカード関数を使うことができません。 その代わりに、テーブル名中に*を入れ、_TABLE_SUFFIXに対する絞り込み条件をWHERE句に書きます。 #legacySQL SELECT * FROM TABLE_DATE_RANGE([mydataset.mytable_], TIMESTAMP( '2016-11-06' ), TIMESTAMP( '2016-11-08' )) #standardSQL SELECT * FROM `mydataset.mytable_*` WHERE _TABLE_SUFFIX BETWEEN '20161106' AND '20161108' これ以上に詳しい情報は以下のドキュメントによくまとまってます。 https://cloud.google.com/bigquery/docs/querying-wildcard-tables#migrating_legacy_sql_table_wildcard_functions 配列に対する関数の変換 Legacy SQLでのREPEATED型はStandard SQLではARRAY型に相当します。 しかし、それらに対する操作を行うための関数は大きく変化をしました。 配列を多用しているクエリをLegacy SQLからStandard SQLに移植する場合は一苦労ありそうです。 変更点が大きく、それだけで1本のブログ記事が書けそうな分量なので、詳細をここで説明することは割愛します。 詳細は以下のドキュメントをご覧ください。 https://cloud.google.com/bigquery/docs/reference/standard-sql/migrating-from-legacy-sql#differences_in_repeated_field_handling 関数の変換 それ以外の関数も変換が必要なものもあります。 弊社内の分析クエリでよく登場した関数についての対応表を以下に示します。 Legacy SQL Standard SQL INTEGER(x) SAFE_CAST(x AS INT64) NOW() CURRENT_TIMESTAMP() REGEXP_MATCH(str, pattern) REGEXP_CONTAINS(str, pattern) IS_NULL(x) x IS NULL str CONTAINS "foo" str LIKE '%foo%' COUNT(DISTINCT x) APPROX_COUNT_DISTINCT(x) EXACT_COUNT_DISTINCT(x) COUNT(DISTINCT x) JSON_EXTRACT UDFで対応 最後のJSON_EXTRACT関数はそれに対応するStandard SQLの関数がないので、UDFで対応しました。 #standardSQL CREATE TEMPORARY FUNCTION JSON_EXTRACT_HOGE(str STRING) RETURNS STRING LANGUAGE js AS "return JSON.parse(str).hoge"; SELECT JSON_EXTRACT_HOGE('{"hoge": "fuga"}') まとめ BigQuery Standard SQLは発表されてからまだ数ヶ月の新しいもので、一部の機能はβ版のままです。 また、日本語の資料がほとんどないため、移行するためのハードルはやや高いと感じます。 しかし、Legacy SQL、Standard SQLという名前の対比を考えるとLegacy SQL側のサポートが徐々になくなっていくことを感じさせられます。 今はこれらのSQLの移行期間なので、どちらとも読み書きできるようなエンジニアになるのがいいでしょう。 日本語資料が少なく移行を踏みとどまっている方々にとって、この記事が一助になれば幸いです。 VASILYではファッションに関する膨大なデータを保有しています。これらのデータを活用しユーザーに価値のあるプロダクトを届けられるメンバーを募集しています。
iOSエンジニアの庄司 ( @WorldDownTown ) です。 iOS 10.1 のリリースから遅れること3日、Xcode 8.1 がリリースされました。この Xcode 8.1 では Swift のバージョンが 3.0.1 にアップデートされています。 iQON の iOS アプリでは、Xcode 8 リリース後すぐに Swift 2.3 へのアップデートは済ませたのですが、最近 Swift のバージョンを 2.3 → 3.0.1 にアップデートしました。 本記事は、作業中に対応したエラー修正の記録のようなものです。とても長くなっていますが、Swift 2系 → 3系にアップデートするときの手助けになればと思います。 モチベーション 現在も引き続きSwift 2.3 で開発を続けることはできますが、いずれは Swift 3.x 系へアップデートすることになるでしょう。 一方、 Realm や RxSwift などのメジャーなライブラリは続々と Swift 3.0 の正式対応版をリリースしています。 このまま Swift 2.3 で開発を続けた場合、アップデート時の作業が増えます。 さらに、各OSSライブラリも今後のアップデートは Swift 3.x 系のみということもあるでしょう。 iQONでは、使用しているライブラリが全て Swift 3.0 対応版がリリースされたため、 iQON でも Swift 3.0.1 へのアップデートに踏み切りました。 前提条件 本記事の Swift 3.0.1 アップデート作業は下記の環境においてのものです。 MacBook Pro (Retina, 15-inch, Mid 2015) macOS Sierra 10.12.1 Swift (2.3 → 3.0.1) Xcode (8.0 → 8.1) Carthage 0.18.1 CocoaPods 1.0.1 サポートiOSバージョン : 9.0 以降 Objective-C / Swift のハイブリッド (比率は 40% : 60%) iPhoneのみ (not universal) 移行作業 大まかな流れは下記のようになっています。 Xcode 8 のコンバーターで Swift 2.3 の文法を 3.0.1 式に一括置換 CocoaPods, Carthage で管理するライブラリを Swift 3.0.1 に対応したバージョンに載せ替える ビルド時にXcodeに表示されるのエラーを取り除く
 〜ここでやっとビルドが通る〜 Xcodeに表示されるのワーニングを取り除く
- 表示崩れなどのバグ修正 & テストを繰り返す Swift 2.3 → 3.0.1 のコンバート Xcode画面上部メニューの Edit → Convert → To Current Swift Syntax... ライブラリの部分は後ほど対応するので、自分達で管理している部分だけチェックを入れてコンバートを実行します。 私の環境では、コンバートに10〜15分程度かかったと思います。 Carthage Cartfile 内の各ライブラリのバージョンを Swift 3.0 に対応したものに変更して、 carthage update するだけです。 CocoaPods 各ライブラリの Build Settings で Swift 3.0.1 を使うようにするために、Podfile に下記のような設定を追加します。 swift_version = ' 3.0.1 ' 各ライブラリ 公式に Swift 3.0 移行ガイドを用意してくれているものもあるので、まずは公式情報を確認しましょう。 例えば APIKit では、丁寧な移行ガイドがありました。 APIKit 3 Migration Guide コンパイルエラー対応 発生した多数のエラーとその対応方法を紹介します。 [Error] String(describing:) クラス名文字列を取得する処理 Swift 2.3 // Swift 2.3 String(SomeClass) Xcode 8でコンバート後 String(describing: SomeClass) // コンパイルエラー Swift 3.0.1 で正しくは String(describing : SomeClass.self ) [Error] Implicitly Unwrapped Optional が廃止になった影響 Objective-Cで、 nonnull , nullable の指定がないメソッドやプロパティの型は、Swift 2.3 から扱うとき、Implicitly Unwrapped Optional (IUO) として ! が付いていました。 Swift 3.0 以降では IUO は廃止され、Optional の派生型として扱われます。 Objective-C typedef void (^ViewHandler)(UIView *); Swift 2.3 let handler : ViewHanlder = { (view : UIView ! ) in view.alpha = 0.5 } Xcode 8でコンバート後 Swift 3.0.1 では、クロージャーの引数の型が Optional になります。 let handler : ViewHanlder = { (view : UIView ?) in view.alpha = 0.5 // コンパイルエラー。viewはOptionalなので直接alphaにはさわれません } Swift 3.0.1 で正しくは // Swift 3.0 let handler : ViewHanlder = { (view : UIView ?) in view?.alpha = 0.5 } ドキュメント [SE-0054] Abolish ImplicitlyUnwrappedOptional type [Error] NSErrorのプロパティにアクセスできない Objective-C のメソッドで、コールバックのクロージャーに NSError を返すメソッドがありました。 Swift 3では NSError が Error に変換されるため、 NSError クラスのプロパティが参照できません。 Swift 2.3 failure : { (error : NSError ! ) in print( "domain: \(error.domain) / code: \(error.code) " ) } Xcode 8でコンバート後 failure : { (error : Error ) in print( "domain: \(error.domain) / code: \(error.code) " ) // コンパイルエラー。NSErrorのプロパティにアクセスできない } Swift 3.0.1 で正しくは failure : { (error : Error ) in let nserror = error as NSError print( "domain: \(nserror.domain) / code: \(nserror.code) " ) } [Error] プロトコルに適合したのメソッド定義がエラーになってしまう Swift 2.3 下記のコードでも特に問題はありません class BrandPanelListDataSource : NSObject , UITableViewDataSource, UITableViewDelegate { ... } extension BrandPanelListDataSource { func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { Xcode 8 でコンバート後 Objective-C method 'tableView:cellForRowAt:' provided by method 'tableView(_:cellForRowAt:)' does not match the requirement's selector ('tableView:cellForRowAtIndexPath:') というエラーが発生。 class BrandPanelListDataSource : NSObject , UITableViewDataSource, UITableViewDelegate { ... } extension BrandPanelListDataSource { func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { // コンパイルエラー Swift 3.0.1 で正しくは Xcode のエラーメッセージをクリックすると自動で @objc(...) が挿入されて、エラーはなくなります。 class BrandPanelListDataSource : NSObject , UITableViewDataSource, UITableViewDelegate { ... } extension BrandPanelListDataSource { @objc (tableView : cellForRowAtIndexPath : ) func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { もしくは、 extension ごとにプロトコルを記述しても修正できます。 こちらの方が @objc(...) の記述が不要なうえ、コードの見通しがよくなると思います。 class BrandPanelListDataSource : NSObject { ... } extension BrandPanelListDataSource : UITableViewDataSource { func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { ... extension BrandPanelListDataSource : UITableViewDelegate { func tableView (_ tableView : UITableView , heightForRowAt indexPath : IndexPath ) -> CGFloat { ... . [Error] CGContextSetLineDash 点線を描くための CoreGraphics のメソッドが変更されていました。 Swift 2.3 CGContextSetLineDash(context, 0.0 , [ 4 ], 1 ) Xcode 8 でコンバート後 'CGContextSetLineDash' is unavailable: Use setLineDash(self:phase:lengths:) というエラーが発生します。 Swift 3.0.1 で正しくは CGContext のインスタンスメソッドを使います。 context.setLineDash(phase : 0.0 , length : [4.0] ) [Error] enumerated() の要素のキーワードが変更 enumerated() の要素にアクセスする際のキーワードが index → offset に変更になりました。 Swift 2.3 objects .enumerate() .forEach { print( $0 .index) } Xcode 8 でコンバート後 Value of tuple type '(offset: Int, element: Any)' has no member 'index' というエラーが発生します。 .enumerated() .forEach { print($0.index) } // コンパイルエラー Swift 3.0.1 で正しくは enumerated() ( EnumeratedSequence<Array<Element>> ) の各要素は offset でアクセスできます objects .enumerated() .forEach { print( $0 .offset) } ドキュメント Migrating to Swift 2.3 or Swift 3 from Swift 2.2 Users may need to manually rename the tuple element index to offset when accessing the result of Collection.enumerated() [Error] コンバーターのキャスト漏れ String の変数に NSString を代入するときなどに明示的なキャストが必要になりました。 Swift 2.3 var parameters : [String: AnyObject] = [ "key1" : "apple" , "key2" : "orange" , ] parameters[ "key3" ] = (someBool ? "banana" : "grape" ) Xcode 8 でコンバート後 parameters の値の型に合わせて as AnyObject を差し込んでくれますが、中途半端です。 var parameters : [String: AnyObject] = [ "key1" : "apple" as AnyObject, "key2" : "orange" as AnyObject, ] parameters[ "key3" ] = (someBool ? "banana" : "grape" as AnyObject) // as AnyObject は "grape" にしかかかっていないのでエラー Cannot convert value of type 'String' to expected argument type 'AnyObject' というエラーになります。 Swift 3.0.1 で正しくは as AnyObject のかかる範囲を変更します。 let parameters : [String: AnyObject] = [ "key1" : "apple" as AnyObject, "key2" : "orange" as AnyObject, ] parameters[ "key3" ] = (someBool ? "banana" : "grape" ) as AnyObject これでも良いのですが、多くのコードに as AnyObject が入ってしまい、可読性が著しく下がります。 parameters の型を [String: Any] に変更すると、 as AnyObject へのキャストが不要になります。 let parameters : [String: Any] = [ "key1" : "apple" , "key2" : "orange" , ] parameters[ "key3" ] = (someBool ? "banana" : "grape" ) ドキュメント [SE-0072] Fully eliminate implicit bridging conversions from Swift - swift-evolution [Error] コンバーターが @escaping を付けてくれない 『クロージャーを引数に持つクロージャー』を引数に持つメソッドにおいて、コンバーターが自動で @escaping を付けてくれない事がありました。 (コールバック地獄だということはさておき…) @escaping については、弊社ニコラスのブログも参考にしてみてください。 Swift 3の変更点の裏側 (アクセス制御 / @escaping) - VASILY DEVELOPERS BLOG Swift 2.3 private typealias CompletionType = Void -> Void ... private func runInBackground (task : CompletionType -> Void ) { Xcode 8 でコンバート後 fileprivate typealias CompletionType = () -> Void ... private func runInBackground (_ task : @escaping (CompletionType) -> Void ) { CompletionType のクロージャーも非同期で呼ばれるため、 @escaping が必要ですが、コンバーターは @escaping を付けてくれません。 そして、 CompletionType を実行するコードでは、下記のようなエラーが発生します。 Closure use of non-escaping parameter 'completeTask' may allow it to escape Swift 3.0.1 で正しくは typealias 宣言時に @escaping をつけるのはエラー // Error: "@escaping may only be applied to parameters of function type" fileprivate typealias CompletionType = @escaping () -> Void メソッド宣言時に @escaping を書くのが正しいようです。 fileprivate typealias CompletionType = () -> Void ... fileprivate func runInBackground (_ task : @escaping ( @escaping CompletionType) -> Void ) { ドキュメント [SE-0103] Make non-escaping closures the default [Error] UIntのプロパティには数字を直接代入できない UInt型のプロパティに直接数値リテラルを代入できなくなりました。 Swift 2.3 // maxCacheSize は UInt型 SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 // 何も問題ない Xcode 8 でコンバート後 SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 // エラー Cannot assign value of type 'Int' to type 'UInt' というエラーが発生します Swift 3.0.1 で正しくは 明示的に UInt のコンストラクタを使うか、 as UInt でキャストする必要があります。 SDImageCache.shared().maxCacheSize = UInt( 1024 * 1024 * 512 ) SDImageCache.shared().maxCacheSize = 1024 * 1024 * 512 as UInt ドキュメント [SE-0072] Fully eliminate implicit bridging conversions from Swift - swift-evolution ワーニング対応 発生したワーニングとその対応方法を紹介します。 [Warning] DispatchQueue.GlobalQueuePriority が deprecated Swift 3.0 から GCD の記述方法が変更になりました。 Xcode 8 のコンバーターが自動で変換してくれますが、変換されたものが deprecated でした。 Swift 2.3 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) { } Xcode 8でコンバート後 DispatchQueue.global(priority: DispatchQueue.GlobalQueuePriority.default).async { // ワーニング } しかし、下記のように2つのワーニングが発生します。 `default` was deprecated in iOS 8.0: Use qos attributes instead `global`(priority:)` was deprecated in iOS 8.0 Swift 3.0.1 で正しくは DispatchQueue.global(qos: .default).async { } // .defaultの場合、引数は省略可能 DispatchQueue.global().async { } [Warning] 戻り値があるメソッドの戻り値を使っていない場合はワーニングになる Swift 2.3 navigationController?.popViewControllerAnimated( true ) Xcode 8 でコンバート後 Expression of type 'UIViewController?' is unused というワーニングが発生する navigationController?.popViewController(animated : true ) Swift 3.0.1 で正しくは 明示的に戻り値を使わないように書きます。 _ = navigationController?.popViewController(animated : true ) ドキュメント [SE-0047] Defaulting non-Void functions so they warn on unused results その他特に苦労した話 Objective-C クラスのプロパティを文字列に組み込むときの不具合 IUO が廃止されたことで、 Objective-C のモデルクラスが持つプロパティを Swift 3.0 の String リテラル( "" ) に含めると "Optional(1)" のような文字列になってしまいます。 iQONでは、APIのURL文字列でこの問題が多く発生したため、多くのリクエスト処理がエラーになってしまい、対応に苦労しました。 Objective-C @interface User @property ( strong , nonatomic ) NSString *name; @end Swift 2.3 user.name = "Bob" print( "name: \(user.name) " ) // name: Bob Xcode 8でコンバート後 IUO ( ! ) は Optional の派生型なので、文字列リテラルに組み込むと "Optional" と表示されてしまいます。 user.name = "Bob" print( "name: \(user.name) " ) // name: Optional("Bob") Swift 3.0.1 で正しくは Optional変数として if let / guard let で明示的にアンラップして変数を扱います。 user.name = "Bob" if let name = user.name { print( "name: \(name) " ) // name: Bob } ドキュメント [SE-0054] Abolish ImplicitlyUnwrappedOptional type APIKit のリクエストパラメータ設定処理 iQONではAPIの通信処理に APIKit を使用しています。 Swift 3.0 に対応した APIKit 3.0.0 から リクエストパラメータを指定するための計算型プロパティの型が変更になりました。 public protocol Request { // APIKit 2.x var parameters : AnyObject ? { // APIKit 3.x var parameters : Any ? { リクエストするURLごとにこの計算型プロパティを実装するのですが、 Xcode 8 のコンバーターはこれを修正してくれないため、自分で対応するしかありません。 この計算型プロパティの型を変更しないと、 Request protocol の parameters は実装されていないことになるため、通信時にパラメータが無いことになってしまいます。 この変更に気付かなかったため、アイテム検索のページで検索条件を変更しても何も結果が変わらず、原因がわかるまでかなり苦労しました。 Optional の 大小比較 < , > 下記のような Optional の値の大小比較が Swift 3.0 ではできなくなりました。 let a : Int ? = nil let b : Int ? = 4 print(a < b) // true そのため、Xcode 8のコンバーターは Optional の大小比較をするコードが存在するファイルごとに 、下記のような不等号演算子を実装するコードを挿入します。 fileprivate func < <T: Comparable> (lhs : T ?, rhs : T ?) -> Bool { switch (lhs, rhs) { case let (l?, r?) : return l < r case ( nil , _?) : return true default : return false } } fileprivate func > <T: Comparable> (lhs : T ?, rhs : T ?) -> Bool { switch (lhs, rhs) { case let (l?, r?) : return l > r default : return rhs < lhs } } この演算子定義のおかげで既存のコードは同じように動作しますが、ファイルごとに宣言されているのは気持ちよくないですし、デフォルトの挙動が変わったのなら、 Optional の大小比較自体をやめるべきだと思います。 let a : Int ? = nil let b : Int ? = 4 let result : Bool if let a = a, let b = b { result = a < b } else { result = false } print(result) // false ドキュメント [SE-0121] Remove Optional Comparison Operators private → fileprivate Xcode 8 のコンバーターは private , fileprivate の使い分けが一切ないまますべて fileprivate に変換してしまいます どこかのタイミングでスコープが小さい物は private に変更する予定です。 プッシュ通知用のDevice Token Swift 3.0 にアップデートしてから、プッシュ通知用のDevice Tokenが今までの方法では取得できなくなってしまいました。 Swift 2.3 let deviceTokenString = deviceToken .description .stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString : "<>" )) .stringByReplacingOccurrencesOfString( " " , withString : "" ) // "a8f1ef4e27181279f3b60e2db043f1409211faf6b24382e1a0a1223b797fbc79" のような64文字の文字列 Xcode 8でコンバート後 Data の description のレスポンスが変更されたため、必ず 32bytes という文字列になってしまいます。 let token : String = deviceToken .description .trimmingCharacters( in : CharacterSet (charactersIn : "<>" )) .replacingOccurrences(of : " " , with : "" ) // "32bytes" Swift 3.0.1 で正しくは let token : String = deviceToken.map { String(format : "%.2hhx" , $0 ) }.joined() // "a8f1ef4e27181279f3b60e2db043f1409211faf6b24382e1a0a1223b797fbc79" のような64文字の文字列 ドキュメント こちらのQitaの記事が参考になりました。 Swift 3.0でのプッシュ通知用のDevice TokenのData型から16進数文字列への変換方法 最後に 今回は、Swift 3.0.1 にアップデートする際の対応方法を紹介しました。 これらの対応方法はあくまで、iQONで発生したエラーの対応方法ですので、そのほかのエラーについては、 swift-evolution を参考にするのが良いです。 本記事がSwift 3系へアップデートする際の参考になれば幸いです。 VASILYではiQONを一緒に開発してくれるiOSエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。 https://www.wantedly.com/projects/62340 www.wantedly.com
Androidエンジニアの @nissiy です。Androidでは、API Level 21からベクター画像をアニメーションさせる仕組みである AnimatedVectorDrawable が使えるようになりました。また、Support Libraryのv23.2.0からは AnimatedVectorDrawableCompat が提供され、API Level 11からも使えるようになりました。 これらは、1つのリソースファイルで拡大縮小に強いアニメーションが作れる優秀な仕組みになっていますが、使うためには animated-vector タグで囲った入り組んだリソースファイルを作成する必要があります。 しかし、先日Googleの Roman Nurik さんによって Android Icon Animator という、AnimatedVectorDrawableとAnimatedVectorDrawableCompatで使えるリソースファイルをWebブラウザ上で作成できるオーサリングツールが作られました。 現在はプレビューリリースということですが機能は十分に揃っているのでぜひともチェックしてみてください。 今回はiQON内の多くの箇所で使用しているアニメーション付きのLIKEボタンをAndroid Icon Animatorで作り直した工程を紹介したいと思います。 Android Icon Animatorの基本的な使い方 Android Icon Animatorの基本的な使い方を理解したい方は @takahirom さんの記事がスゴくオススメです。 qiita.com ぜひとも本稿とあわせて読んでみてください。 今までのアニメーション付きのLIKEボタンについて 今までのLIKEボタンは、17枚の画像を使用したFrameアニメーションになっています。 iQONでは、hdpi・xhdpi・xxhdpiのリソース画像を設置しているため51点のリソース画像と、下記のXMLと計52点のリソースファイルを使用してLIKEボタンのアニメーションを実現しています。 <? xml version = "1.0" encoding = "utf-8" ?> <animation-list xmlns : android = "http://schemas.android.com/apk/res/android" android : oneshot = "true" > <item android : drawable = "@drawable/like_animation_01" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_02" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_03" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_04" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_05" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_06" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_07" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_08" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_09" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_10" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_11" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_12" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_13" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_14" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_15" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_16" android : duration = "30" /> <item android : drawable = "@drawable/like_animation_17" android : duration = "30" /> </animation-list> アニメーション自体は 『背景』『効果』『ハート』『外枠』 と4つのレイヤーに分かれたものになっています。 各レイヤーの振る舞いは以下のようになっています。 レイヤー 振る舞い 背景 時間の経過とともに白からピンクに色が変化 効果 背景が変化するタイミングで白の円として出現し時間の経過とともに 透過率とスケールを変えながら消えていく このレイヤーのおかげで一瞬ボタンがキラっとするように見える ハート 時間の経過とともにピンクから白に色を変化しながら2回転 外枠 時間の経過とともに透過率が変わり消えていく 次にこれらの情報をAndroid Icon Animatorに展開させていきます。 Android Icon Animatorでリソースファイルを作成する 事前に元になるSVGファイルを作成しておいてください。レイヤーごとにアニメーションが付けられるので必要に応じてレイヤーを分けておくと後の工程が楽になります。 Android Icon Animator をWebブラウザで開いて、SVGファイルをそこにドラッグアンドドロップすると、左下に各要素が path や layer group として階層表示されます。 takahiromさんの記事 でもありますが、pathとlayer groupではできることが違っているため、必要に応じて階層を入れ替えたり、要素を追加したりして階層を整えます。 例えば、今回のLIKEボタンは以下のように階層を整えることができます。 レイヤー 配置方法 背景 色が変化するだけなのでpathをそのまま配置 効果 透過率とスケールを変化させるのでlayer groupの中にpathを配置 スケールを中心を基準に変化させるのでlayer groupのpivotを中心に設定 ハート 色とスケールを変化させるのでlayer groupの中にpathを配置 スケールを中心を基準に変化させるのでlayer groupのpivotを中心に設定 外枠 透過率が変化するだけなのでpathをそのまま配置 加えて、要素を管理しやすくするために各要素のidを整理してもいいと思います。 階層を入れ替えて、idを整理したものが以下になります。 ここから各要素の時計アイコンをクリックしてアニメーションを作成していきます。大まかに配置を行ってから右上のフォームを使って調整を行うとスムーズにいきます。 実際に元のLIKEボタンに似せてアニメーションを設置した画面が以下になります。 最後に画面の左側にある Export の Animated vector drawable(s) を選択するとリソースファイルが出力されます。AnimatedVectorDrawableやAnimatedVectorDrawableCompatを使用して読み込むと、以下のようにアニメーションが再生されます。 ベクター画像のアニメーションのため、画面いっぱいに表示させても問題ない見た目になります。 Android Icon Animatorを使う上で気をつけるポイント こまめにSaveを行う 現状のAndroid Icon Animatorには自動保存機能などは存在しないため、こまめに画面の左側にある File の Save を選択して、ローカルに .iconanim ファイルをバックアップしておくことをオススメします。 このファイルを File の Open で開くことでプロジェクトを復旧することができるので、Webブラウザが急に固まったときの惨事を軽減することができます。 私もアニメーションのチェックをしている際に過負荷で固まってしまったことがあり、Saveをしていなかったことでヒドく落ち込みました。 API Level 22以下をサポートしている場合はpathDataのアニメーションは使わない 手元で確認したところpathDataのアニメーションはAPI Level 22以下では動かないことがわかりました。 そのため、API Level 22以下をサポートしているアプリはpathDataを変更するアニメーションは使わない方が良さそうです。作りたいアニメーションが他の要素を使うことで実現可能であればそちらを使って対応するようにしましょう。 API Level 22以下で1つの要素に複数の異なるアニメーションを設定する場合には注意 API Level 22以下の場合、1つの要素に複数の異なるアニメーションを設定すると、2つ目以降のアニメーションが上手く動作しないことがわかりました。今回の場合だと 効果 の部分で行っている scaleX と scaleY がそれに当たります。 この問題に遭遇した場合は作成したリソースファイルを直接触って target を分けることで解決できます。 修正前 <target android : name = "effect_layout" > < aapt : attr name = "android:animation" > <set xmlns : android = "http://schemas.android.com/apk/res/android" > <objectAnimator android : name = "effect_layout" android : duration = "330" android : interpolator = "@android:anim/decelerate_interpolator" android : propertyName = "scaleX" android : startOffset = "60" android : valueFrom = "1" android : valueTo = "1.333" android : valueType = "floatType" /> <objectAnimator android : name = "effect_layout" android : duration = "330" android : interpolator = "@android:anim/decelerate_interpolator" android : propertyName = "scaleY" android : startOffset = "60" android : valueFrom = "1" android : valueTo = "1.333" android : valueType = "floatType" /> </set> </ aapt : attr> </target> 修正後 <target android : name = "effect_layout" > < aapt : attr name = "android:animation" > <objectAnimator android : name = "effect_layout" android : duration = "330" android : interpolator = "@android:anim/decelerate_interpolator" android : propertyName = "scaleX" android : startOffset = "60" android : valueFrom = "1" android : valueTo = "1.333" android : valueType = "floatType" /> </ aapt : attr> </target> <target android : name = "effect_layout" > < aapt : attr name = "android:animation" > <objectAnimator android : name = "effect_layout" android : duration = "330" android : interpolator = "@android:anim/decelerate_interpolator" android : propertyName = "scaleY" android : startOffset = "60" android : valueFrom = "1" android : valueTo = "1.333" android : valueType = "floatType" /> </ aapt : attr> </target> また ハート のように同一のアニメーションを複数実行する場合には問題がないことを確認しています。 今後Android Icon AnimatorやSupport Libraryで改善されると思うので、それまではこの問題に注意してください。 最後に 今回紹介したAndroid Icon Animatorの使い方を覚える際には、Android Icon Animatorのページ上部からダウンロードできるサンプルファイルを活用してみてください。 サンプルファイルを一通り触れば基本的なことはできるようになると思います。 AnimatedVectorDrawableやAnimatedVectorDrawableCompatを使用することでプロジェクトからリソースファイルを多く減らすことができます。 今回作り直したLIKEボタンだけでも52点のファイルを削減することができたので、アニメーションのクオリティを落とさずにリソースファイルの軽量化を進める際にはかなり有効な手段だと思うのでぜひとも試してみてください。 最後の最後に、VASILYではiQONを一緒に開発してくれるAndroidエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。
データサイエンティストの中村です。VASILYではファッションに特化した画像解析エンジンを開発しています。本記事では、スナップ写真からファッションアイテムを検出するシステムを紹介したいと思います。 概要 このシステムの入力はスナップ写真です。スナップ写真が入力されたとき、システムは以下のタスクを解きます。 写真中からファッションアイテムに該当する領域を検出する 検出したファッションアイテムのカテゴリを予測する 検出したファッションアイテムに似ているアイテムをDBから検索する 各タスクを解く方法は様々ありますが、弊社のシステムでは2種類のネットワークを使ってこれを達成しています。 ファッションアイテムの検出とカテゴリ予測 検出は画像認識の基本的なタスクで盛んに研究されていて様々な手法が提案されていますが、今回はSingle Shot MultiBox Detector (SSD) *1 と呼ばれる手法を使いました。 SSDでは検出、すなわち領域の予測とカテゴリ予測を単一のネットワークで同時に解きます。 シンプルなモデルなので学習や拡張が簡単になるというメリットがあります。また、処理速度も既存の手法に比べてだいぶ改善されています。 検出・カテゴリ予測問題のデファクトスタンダードといえばRegion-based Convolutional Neuralnetwork (R-CNN) *2 があげられると思いますが、R-CNNやFast R-CNN *3 はSelective Search *4 という前処理を必要としていて、この処理の重さが課題になっています。Faster RCNN *5 で速度の問題はある程度改善されていますが、SSDの処理速度はFaster RCNNを凌ぎます。 PASCAL VOC を用いた実験ではFaster RCNNが 7 FPS (frames per second) であるのに対し、SSDは 58 FPSを記録したそうです。 モデル SSDのネットワークは以下です。 前半の層はVGG *6 と同じアーキテクチャを採用しています。VGGの全結合層は不要なので削除し、代わりに畳み込み層を追加しています。 こうして何層にも積まれた畳み込み層ひとつひとつの出力 (feature map) を領域の予測とカテゴリの予測に使います。複数のfeature mapの出力を予測に使う点がSSDの強みだそうです。 学習時の入力は画像とオブジェクトの矩形 (ground truth) 及びカテゴリです。学習開始時にground truthと各feature map上の矩形 (default box) との対応付けを行います。 default boxはセルごとにアスペクト比を変えて複数個定義されていて、それぞれについて矩形のオフセットとカテゴリを予測します。 SSDに関しては著者がコードを公開しています( github )。SSDを試したい場合はこのコードをそのまま使うことをおすすめします。 ファインチューニングの実験はREADMEには書かれていない操作が必要になるかもしれませんが、よくある落とし穴はissueで著者が回答しているので、困ったらissueを探してみてください。 ファッションアイテムの検索 画像検索の基本的なアプローチは画像特徴量の類似度計算です。 画像から特徴抽出して得たベクトルについて何らかの関数で類似度を計算することで類似度を定量化します (コサイン類似度やユークリッド距離がよく使われます) 。定量化した類似度を降順にソートすることで画像検索が可能になります。 ところが今回の画像検索においては画像のドメインに注意する必要があります。 まず入力画像はスナップ写真からトリミングされたアイテムの画像です。トリミングされているとは言え、矩形のスケールによっては背景が写り込んでいますし、向きも正面から撮影されているとは限りません。照明の影響も受けます。すなわち入力画像にはノイズが大量に含まれていると考えられます。一方で検索対象である商品画像は、アイテム単体が目立つように正面から撮影されています。背景も単色であることがほとんどです。 このようにドメインが異なる画像を検索するタスクについては、それぞれの特徴量をコサイン類似度のような単純な手法を使って比較しても精度は期待できません。 そこで今回は、ノイズの大きさに関わらず同じアイテムであれば大きな値を返す関数を学習によって獲得するというアプローチを取りました。 線形な関数で解けそうな問題でもないと思ったので、非線形関数を仮定します。となるとやはりニューラルネットワークを試すのが手っ取り早いです。 類似度学習 ディープ系の類似度学習にはcontrastive lossやtriplet lossを使ったものがあります *7 *8 。今回はtriplet lossを採用しました。 トリミングされたスナップ写真の特徴量を 、写真と同じアイテムの特徴量を 、写真と異なるアイテムの特徴量を とした時、triplet loss関数には のtripletを入力します。 理想的な検索システムでは、 の任意の組み合わせについて となっているはずです。triplet lossによる学習はこの状態を目指します。 特徴抽出 特徴抽出にはVGGとSimo-Serra2016 *9 のアーキテクチャを参考に以下のネットワークを使いました。 import numpy as np import chainer import chainer.links as L import chainer.functions as F from chainer import Variable class StyleExtractorBN (chainer.Chain): def __init__ (self): self.train = False self._layers = {} self._layers[ 'conv1_1' ] = L.Convolution2D( 3 , 64 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 3 )) self._layers[ 'conv1_2' ] = L.Convolution2D( 64 , 64 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 64 )) self._layers[ 'conv2_1' ] = L.Convolution2D( 64 , 128 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 64 )) self._layers[ 'conv2_2' ] = L.Convolution2D( 128 , 128 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 128 )) self._layers[ 'conv3_1' ] = L.Convolution2D( 128 , 256 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 128 )) self._layers[ 'conv3_2' ] = L.Convolution2D( 256 , 256 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 256 )) self._layers[ 'conv4_1' ] = L.Convolution2D( 256 , 128 , 3 , stride= 1 , pad= 1 , wscale= 0.02 *np.sqrt( 3 * 3 * 256 )) self._layers[ 'fc5' ] = L.Linear( 3072 , 128 ) self._layers[ 'bn1_1' ] = L.BatchNormalization( 64 ) self._layers[ 'bn1_2' ] = L.BatchNormalization( 64 ) self._layers[ 'bn2_1' ] = L.BatchNormalization( 128 ) self._layers[ 'bn2_2' ] = L.BatchNormalization( 128 ) self._layers[ 'bn3_1' ] = L.BatchNormalization( 256 ) self._layers[ 'bn3_2' ] = L.BatchNormalization( 256 ) super (StyleExtractorBN, self).__init__(**self._layers) def __call__ (self, x): h = F.relu(self.bn1_1(self.conv1_1(x), test= not self.train)) h = F.relu(self.bn1_2(self.conv1_2(h), test= not self.train)) h = F.max_pooling_2d(h, 4 , stride= 4 ) h = F.dropout(h, train=self.train, ratio= 0.25 ) h = F.relu(self.bn2_1(self.conv2_1(h), test= not self.train)) h = F.relu(self.bn2_2(self.conv2_2(h), test= not self.train)) h = F.max_pooling_2d(h, 4 , stride= 4 ) h = F.dropout(h, train=self.train, ratio= 0.25 ) h = F.relu(self.bn3_1(self.conv3_1(h), test= not self.train)) h = F.relu(self.bn3_2(self.conv3_2(h), test= not self.train)) h = F.max_pooling_2d(h, 4 , stride= 4 ) h = F.dropout(h, train=self.train, ratio= 0.25 ) h = F.relu(self.conv4_1(h)) h = self.fc5(h) return h 結果 検出・カテゴリ予測・検索の結果を一度に表示します。 定性的な結果は概ね良好だと思います。 学習結果の可視化 学習した特徴抽出器で商品画像から抽出した特徴量を t-SNE で2次元に圧縮してみます。 これを見ると特徴の似たアイテムが近くに分布してることがわかります。 まとめ スナップ写真からファッションアイテムを検索する仕組みについて紹介しました。検出・判別・検索の3種類のタスクが要求されるシステムを2つのネットワークを組み合わせて実装しました。特に検索についてはチャンピオンモデルのようなものが存在していないので、手探りな状態から始めました。幾つか実験を重ねて今の実装になりましたが、まだまだ高度化の余地はあると感じています。より使いやすいシステムを目指して、各タスクの高精度化や検索対象の拡大に取り組んでいきたいと思います。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。 *1 : Liu, W., Anguelov, D., Erhan, D., Szegedy, C., Reed, S.: SSD: Single Shot MultiBox Detector. arXiv (2015) *2 : Girshick, R., Donahue, J., Darrell, T., Malik, J.: Rich feature hierarchies for accurate object detection and semantic segmentation. In: CVPR. (2014) *3 : Girshick, R.: Fast R-CNN. In: ICCV. (2015) *4 : Uijlings, J.R., van de Sande, K.E., Gevers, T., Smeulders, A.W.: Selective search for object recognition. In: IJCV. (2013) *5 : Ren, S., He, K., Girshick, R., Sun, J.: Faster R-CNN: Towards real-time object detection with region proposal networks. In: NIPS. (2015) *6 : Simonyan, K., Zisserman, A.: Very deep convolutional networks for large-scale image recog- nition. In: NIPS. (2015) *7 : Bell, S., Bala, K.: Learning visual similarity for product design with convolutional neural networks. In: SIGGRAPH. (2015) *8 : Hoffer, E., Ailon, N.: Deep metric learning using triplet network. In: ICLR. (2015) *9 : Simo-Serra, E., Ishikawa, H.: Fashion Style in 128 Floats: Joint Ranking and Classification using Weak Data for Feature Extraction. In: CVPR. (2016)
こんにちは、Webフロントエンドエンジニアの権守です。今回は弊社で開発中のサービスで実装した商品画像の拡大プレビュー機能の実装について紹介します。 概要 新サービスの商品詳細ページに次の動画のような拡大プレビュー機能を実装しました。 このように拡大プレビュー機能があることで、デザインの細部や生地感がわかり、ユーザにとってよりよい体験を与えることができると思います。 仕様 今回、機能を実装する上で満たさなければいけなかった仕様です。 画像をマウスオーバーした際に横に拡大プレビューを表示する 元の画像はカルーセルで表示されている 画像の大きさは予めわからない 対応ブラウザはPCのモダンブラウザ(IE11, Edge, Firefox最新版, Chrome最新版, Safari最新版) 実装方法 次の順序で実装について説明していきます。 ルーペの表示 拡大プレビューエリア 表示・非表示の切り替え カーソル移動に合わせたプレビュー箇所の変更 ルーペの表示実装 まずは、ルーペの表示部分を実装します。ルーペは商品画像に重なるように表示する必要があります。そのため、商品画像に対して相対的に位置を指定したいところです。しかし、imgタグはタグを内包できないので、画像と同じ大きさを持つdivで一段くくり、それに対して相対位置を指定します。 < div class = "m-lens-container" > < img alt = "商品タイトル" src = "https://dummyimage.com/600x400/000/fff" > < div class = "m-lens" ></ div > </ div > < div class = "m-lens-container" > < img src = "https://dummyimage.com/400x600/000/fff" > < div class = "m-lens" ></ div > </ div > .m-lens-container { display : inline-block ; position : relative ; margin - } .m-lens { position : absolute ; top : 50px ; /* JSで適切な値を設定する */ left : 30px ; /* JSで適切な値を設定する */ z-index : 2 ; background : #f57716 ; opacity : 0.3 ; height : 172px ; width : 172px ; } .m-lens-container img { max-height : 344px ; max-width : 344px ; } これでルーペ部分を画像に重ねて表示することはできました。しかし、完成形と比較してわかるように画像が縦横中央揃えになっていません。そこで次に display: table を使って画像の縦横中央揃えを実現します。 < ul class = "slides" > < li class = "slide" > < div class = "cell" > < div class = "m-lens-container" > < img src = "https://dummyimage.com/600x400/000/fff" > < div class = "m-lens" ></ div > </ div > </ div > </ li > < li class = "slide" > < div class = "cell" > < div class = "m-lens-container" > < img src = "https://dummyimage.com/400x600/000/fff" > < div class = "m-lens" ></ div > </ div > </ div > </ li > </ ul > .slides { display : flex; /* カルーセルで横並びにする必要があるため */ justify- content : space -between; /* 解説用 */ width : 700px ; /* 解説用 */ } .slide { display : table ; background : #fff ; /* 解説用 */ text-align : center ; /* 横に中央揃え */ height : 344px ; width : 344px ; } .cell { display : table-cell ; vertical-align : middle ; /* 縦に中央揃え */ } 上のように記述することで、画像を縦横中央揃えにすることができました(一部、結果が見えやすい用に解説用のCSSを加えています)。 拡大プレビューエリアの実装 次に、拡大プレビューエリアの実装について説明します。ここでのポイントは、拡大プレビューエリアを画像の右横に他の要素に重なるように表示することと、拡大した画像を切り抜く必要があることです。ここでは、 position: absolute を使って右横に配置し、 overflow: hidden を使って切り抜きます。 < div class = "images" > < div class = "zoom-area active" > <!-- JSでactiveを切り替える --> < img src = "https://dummyimage.com/600x400/000/fff" /> </ div > < div class = "slides-container" > < ul class = "slides" > ... </ ul > </ div > </ div > .images { position : relative ; } .slides-container { /* カルーセル表示領域 */ width : 344px ; overflow : hidden ; } .zoom-area { display : none ; position : absolute ; top : 0 ; left : 369px ; border : 1px solid #ccc ; height : 520px ; width : 520px ; overflow : hidden ; } .zoom-area.active { display : block ; } .zoom-area img { width : 1040px ; /* JSで適切な値を設定する */ margin-top : -30px ; /* JSで適切な値を設定する */ margin-left : -60px ; /* JSで適切な値を設定する */ } position: absolute を使って画像の右横に設置したいですが、slides-container内にzoom-areaを入れるのは不自然なので、ここでもdiv (images)で一段くくり、それに対し position: relative を設定し、プレビューエリアの表示位置の基準値とします。そして、zoom-areaに position: absolute を設定し、leftを使って必要な量のオフセットを水平方向に設定します。 次に、画像の切り抜きについてです。これは、zoom-area内に拡大後のサイズの画像を入れて、エリアから溢れた部分を非表示にすることで実現できます。溢れた部分の非表示は overflow: hidden で、表示位置の指定はネガティブマージンで実装できます。 後ほど解説しますが、拡大後の画像サイズやネガティブマージンの量の調節、zoom-areaのactiveの切り替えは、JS側で行う必要があります。 表示・非表示の切り替え ここまでで、CSSによるレイアウトなどの設定はできたので、いよいよ動きの部分を作っていきます。 まずは、ルーペと拡大エリアの表示・非表示の切り替えから説明していきます。 .m-lens { display : none ; } .m-lens-container : hover .m-lens { display : block ; } まず、CSSでm-lensに display: none を設定することでルーペをデフォルト非表示にします。そして、m-lens-containerのhover時に中のm-lensに display: block を設定することで、画像をホバーした際にルーペが表示されるようにします。 ( function () { var zoomArea = document .querySelector( '.zoom-area' ); var zoomImage = zoomArea.querySelector( 'img' ); var size = 172; var scale = 520 / size; Array .prototype.forEach.call( document .querySelectorAll( '.m-lens-container' ), function (container) { var lens = container.querySelector( '.m-lens' ); var img = container.querySelector( 'img' ); container.addEventListener( 'mouseenter' , function () { var image = container.querySelector( 'img' ); zoomArea.classList.add( 'active' ); zoomImage.setAttribute( 'src' , image.src); zoomImage.style.width = (image.offsetWidth * scale) + 'px' ; } ); container.addEventListener( 'mouseleave' , function () { zoomArea.classList.remove( 'active' ); } ); } ); } )(); 次に、JSで画像のホバー時に拡大エリアを表示するようにします。拡大エリアを表示するにはzoom-areaにactiveのクラスを追加すればよいので、m-lens-containerのmouseenterイベントに対して、 zoomArea.classList.add('active') の処理を結びつけます。このとき同時に拡大エリアに表示する画像のパスと拡大後のサイズの指定を行います。拡大後の画像のサイズは、左に実際に表示されているサイズに拡大率をかけることで求まります。また、拡大率は拡大エリアのサイズをルーペのサイズで割ることで求まります(ここでは簡単化のためにルーペと拡大エリアのサイズを直接指定していますが、汎用性を高めるならJSでサイズを取得してもよいと思います)。 そして、拡大エリアの非表示は、画像からカーソルが離れた際に、zoom-areaのactiveを解除すればよいので、m-lens-containerのmouseleaveイベントに対して、 zoomArea.classList.remove('active') の処理を結びつけることで実装できます。 (注)拡大プレビューエリアの実装の説明時の例では、zoom-areaに対し、activeをデフォルトで指定していましたが、本来はデフォルトでactiveは不要なので削除の必要があります。 カーソル移動に合わせたプレビュー箇所の変更 最後に、カーソル移動に合わせたプレビュー箇所の変更を実装します。 Array .prototype.forEach.call( document .querySelectorAll( '.m-lens-container' ), function (container) { ... container.addEventListener( 'mousemove' , function (e) { var rect = container.getBoundingClientRect() ; var mouseX = e.pageX; var mouseY = e.pageY; var positionX = rect.left + window .pageXOffset; var positionY = rect. top + window .pageYOffset; var offsetX = mouseX - positionX; var offsetY = mouseY - positionY; var left = offsetX - (size / 2); var top = offsetY - (size / 2); lens.style. top = top + 'px' ; lens.style.left = left + 'px' ; zoomImage.style.marginLeft = -(left * scale) + 'px' ; zoomImage.style.marginTop = -( top * scale) + 'px' ; } ); } ); 先程のJSにmouseenter, mouseleaveに加え、mousemoveのイベント処理を追加します。ここで必要な処理は、マウスカーソルの移動の度にその座標を取得し、ルーペの表示位置と拡大エリアのプレビュー箇所を計算し、指定することです。 座標の取得については、当初、 e.offsetX と e.offsetY を用いる予定でしたが、要素が重なっているせいなのか、m-lens-containerではなくルーペの基準値からの距離を取得してしまったため、pageXとpageYを使う実装に変更しました。 pageXとpageYはページの左上からの距離を取得するので、m-lens-containerの左上からの距離に変換する必要があります。そのために、getBoudingClientRectとwindow.pageXOffset, window.pageYOffsetを使う必要があります。まず、 rect = container.getBoudingClientRect() で、矩形オブジェクトを取得します。そして、 rect.left と rect.top でウィンドウの左上からの距離をそれぞれ取得します。これはあくまで、ウィンドウで表示されている領域からの距離なので、これらの値とpageX, pageYから座標を計算してしまうと、ページがスクロールされている場合に、スクロール量の分だけ計算がずれてしまいます。 そこで、 window.pageXOffset と window.pageYOffset を使って、スクロール量も計算に入れます。まとめると次のようになります。 コンテナの左上からの距離 = ページの左上からの距離 - スクロール量 - ウィンドウの左上からの距離 これでカーソル位置を計算することができましたが、このままの値をルーペの表示位置にしてしまうとカーソルがルーペの左上にきてしまいます。そこで、ルーペのサイズの半分を、計算結果から引いてあげます。これでカーソルを中心にルーペが表示できます。 まだ、拡大エリアのプレビューの表示箇所の変更の処理が残っていますが、ここまでくれば後は簡単です。コンテナの左上からのルーペまでの距離と拡大エリアで非表示にしたい左上の領域の比率は同じなので、先程計算したコンテナの左上からのルーペの距離を拡大比率でかけたものをネガティブマージンに設定するだけで処理は完了です。 ここまでで拡大機能は一通りできましたが、上の動画を見てもらうと画像外の範囲もプレビューしてしまって使いづらいことがわかると思います。 そこで、次に、プレビューエリアの限界値の設定を行います。 var xmax, ymax; img.addEventListener( 'load' , function () { xmax = img.offsetWidth - size; ymax = img.offsetHeight - size; } ); container.addEventListener( 'mousemove' , function (e) { ... var left = offsetX - (size / 2); var top = offsetY - (size / 2); if (left > xmax) { left = xmax; } if ( top > ymax) { top = ymax; } if (left < 0) { left = 0; } if ( top < 0) { top = 0; } lens.style. top = top + 'px' ; lens.style.left = left + 'px' ; zoomImage.style.marginLeft = -(left * scale) + 'px' ; zoomImage.style.marginTop = -( top * scale) + 'px' ; } ); 画像の読み込みが終わったタイミングで画像の表示サイズを取得します。そして、ルーペの座標は左上を基準としていることを考慮すると、表示サイズからルーペの大きさを引いた値がルーペの座標の最大値になることがわかります。 後は、ルーペの表示位置の計算結果が限界値を超えた際に、限界値にする処理を入れればルーペが画像をはみ出ることはなくなります。 以上が今回実装した拡大プレビュー機能の実装方法です。 今回、解説時に作ったサンプルをgistに公開してあるので、コード全体を見たい方は こちら を参照してください。 まとめ 今回は新サービスの商品詳細ページで実装した商品画像の拡大プレビュー機能の実装について紹介しました。ECサイト上での買い物では、商品の細部がわからず困るということがよくありますが、この機能を実装することによってそれを少し軽減でき、ユーザにとってより使いやすいサイトになったのではないかと思います。 最後に VASILYでは、ユーザがより気持ちよくサービスを使えるUIなどを作っていける仲間を募集しています。少しでもご興味のある方は以下のリンク先をご確認ください。 https://www.wantedly.com/projects/61388 www.wantedly.com
こんにちは。VASILYのiOSエンジニアの にこらす です。 2015年の12月からSwiftがオープンソースになり、 Swift Evolution (Swift言語の新しい仕様について提案する場所)で多くの開発者の提案が採用されました。 今回はSwift 3の アクセス制御 と @escaping についての変更点と、その背景について紹介します。 Swift 言語の変更点はすべて、 Swift Evolution で確認することができます。 さらに変更点だけでなく、決定に至る議論の内容は Swift Evolution Mailing List のメーリングリストで追うことができます。さらにここで アーカイブ も読めます。 アクセス制御 Swift 2 でのアクセス制御の概念は private , internal , public の3つでした。 Swift 3では、Swift Evolution の下記の2つの提案が承認されました。 Scoped Access Level SE-0025 Allow distinguishing between public access and public overridability SE-0117 この変更でアクセスコントロールのルールに変更があり、新しいキーワード fileprivate と open が追加されました。 fileprivate SE-0025 SE-0025 が承認されたことで、新しいアクセス修飾子 fileprivate が追加されました。 その結果、Swift 3のアクセス制御ルールは下記の表のようになりました。 アクセス修飾子 意味 SE-0025で変更があったか public モジュールの外からアクセス可能。クラスなら継承することもできる 変更なし internal 同一モジュール内のならアクセス可能 変更なし fileprivate 同一ファイル内ならアクセス可能 🆕 private 同一スコープ内のみアクセス可能 🆕 SE-0025では public , internal の意味は変わりませんが、新しく fileprivate という修飾子が追加されました。 fileprivate として定義されたものは同じファイルの中からどこでもそのメンバーにアクセスできます。これはSwift 2での private と同じ挙動です。 一方 private の方は、同一ファイル内でも、クラスやextensionをまたぐなどスコープを超えたアクセスができなくなりました。 なぜ fileprivate が必要だったのか Swift 3から private の意味はファイルに対してではなく、スコープに対して private になりました。 fileprivate は Swift 2 の private と同じです。 fileprivate と private の区別ができると、 extension の中でのみ使用できるメソッドを追加出来るようになります。 例えば Stack というデータを保持するクラスを作るとき、push処理とpop処理 を別々の extension に書きたいことがあります。 このときpop() するときだけ、pop直前のデータを print したい。こういう時に extension の中で private が使えます。 class Stack <T> { fileprivate var elements = [T]() } // push extension extension Stack { func push (_ element : T ) { printStack() // 呼び出せない elements.append(element) } } // pop extension extension Stack { private func printStack () { print( "Stack: \(elements) " ) } func pop () -> T ? { printStack() return elements.removeLast() } } printStack() は pop extension の中だけで使いたいので、 private にして定義された extension の外からは呼び出せないようにできます。 printStack() はちゃんと隠れています。 private extension と fileprivate Swift の extension を書くときに、 private extension とすると、その extension 内のメンバーは何も書かなければ fileprivate と同じアクセスレベルになります。 extension はそもそもファイルのトップレベルに宣言するもので、 private extension の private の有効範囲はファイル全体になります。 したがって プロパティ一つ一つに fileprivate を書くことと同じ動作をします。 下記のコードは一緒です。 fileprivate が付いてる書き方 class Hoge { } extension Hoge { fileprivate func methodA () { ... } fileprivate func methodB () { ... } fileprivate func methodC () { ... } } 暗黙的な書き方 class Hoge { } private extension Hoge { func methodA () { ... } func methodB () { ... } func methodC () { ... } } open SE-0117 SE-0117 が承認されたことで、新しいアクセス修飾子 oepn が追加されました。 その結果、Swift 3のアクセス制御ルールは下記の表のようになりました。 アクセス修飾子 意味 SE-0117で変更があったか open モジュールの外からアクセス可能。クラスなら継承することもできる (Swift 2 での public と同じ挙動) 🆕 public モジュールの外からアクセス可能。 外部からクラスの継承はできない 🆕 internal 同一モジュール内のならアクセス可能 変更なし fileprivate 同一ファイル内ならアクセス可能 変更なし private 同一スコープ内のみアクセス可能 変更なし なぜ open が必要だったのか 他の言語にある多彩なアクセスコントロールではできることが、Swift 2ではフルオープンな public しかありませんでした。 オープンソースライブラリを公開するときなど、クラスを公開はしても、継承はしてほしくないときに利用者に明示的にその意志を伝える事ができます。 Swift 2 の final public と Swift 3 の public Swift 2 の public final と Swift 3 の public は似たような振る舞いをしますが、厳密には下記のような違いがあります。 // Swift 2 public final class Mammal { } public class Dog : Mammal { } // 同一モジュール内でも継承できない // Swift 3 public class Mammal { } public class Dog : Mammal { } // 同一モジュール内では継承可能 @escaping SE-0103 Swift 3 から @noescape の言語キーワードが無くなり、メソッドのクロージャー引数はデフォルトで離脱しない @noescape と同じ挙動になりました。 一方、メソッドのクロージャー引数が離脱する「可能性がある」場合、 @escaping キーワードを使う必要があります。 UIKit にもアニメーション処理など @escaping が付いてるメソッドが多いので、ぜひとも理解しておきたい概念です。 ( @noescape : 「離脱しない」、 @escaping : 「離脱する」と表現しています) 離脱しないクロージャー Swift 3のデフォルトのクロージャー引数は、すぐに破棄されるため、 [weak hoge] を書かなくても循環参照することはありません。 class Hoge { func useIt (thisClosure : () -> Void ) { thisClosure() } } let hoge = Hoge() // デフォルトで離脱しないクロージャーなので、[weak hoge] を書かなくても循環参照にならない。 hoge.useIt() { print(hoge) } 離脱するクロージャー実例 下記のコードのように引数のクロージャー ( thisClosure ) をプロパティにコピーすると、メソッド実行後にも引数のクロージャーが破棄されない (離脱する) ため、コンパイルエラーが発生します。 // コンパイルエラー var myPrettyClosure : (() -> Void )? = nil func trapIt (thisClosure : () -> Void ) { thisClosure() myPrettyClosure = thisClosure } クロージャーの型宣言の前に @escaping を書くことでコンパイルが通るようになります。 // OK var myPrettyClosure : (() -> Void )? = nil func trapIt (thisClosure : @escaping () -> Void ) { thisClosure() myPrettyClosure = thisClosure } なぜクロージャーのデフォルトの挙動が変わったのか Swift 2以前のクロージャーでは、強く意識していないと容易に循環参照が発生しがちです。 Swift 3では、この循環参照が発生しにくくなるように、クロージャーはデフォルトで離脱しない ( @noescape ) ようになりました。 Swift でのクロージャーは、 map , filter , reduce に代表される関数型プログラミングのような離脱しない ( @noescape ) 使い方の方が多く存在します。 まとめ 今回、Swift 3のアクセス制御と @escaping について説明しました。 しかし、Swift 3にはまだ大きな破壊的変更が多く存在します。 fileprivate , private のおかげでもっと徹底的なモジュールアクセス制御ができるようになり、 open , public のおかげで外からアクセスできるクラスが継承可能かどうかを区別できるようになりました。 次回のブログでもまた Swift Evolution の他の変更点に紹介したいと思います。 ー にこらす https://www.wantedly.com/projects/62340 www.wantedly.com
こんにちは、データチームの後藤です。この記事では、一般物体認識で優秀な成績を収めた代表的なニューラルネットワークモデルを、ファッションアイテムの画像データに対して適用し、どのアーキテクチャが有用か、どれだけの精度を出せるのかを調べる実験を行います。 今回は、 AlexNet Network In Network GoogLeNet DenseNet の4つのアーキテクチャを試しました。 背景 iQONでは毎日500以上のECサイトをクロールし、一日平均1万点もの新着アイテムを追加しています。この過程で、新着アイテムがiQONのどのカテゴリに属するのかを決める必要がありますが、この作業を人手で行うと膨大なコストになってしまいます。この問題に対して我々は、アイテムの名前や説明文、画像データを活用してカテゴリを判別する仕組みを作りました。とくに画像データによる判別には、畳み込みニューラルネットワーク(CNN)を活用しています。その一例は過去のブログに書いていますので興味のある方は是非ご覧ください。 tech.vasily.jp 過去の例では、5層の比較的浅いCNNを利用しましたが、ニューラルネットワークの研究は日々進歩しており、カテゴリ判別問題に対して有効なアーキテクチャが次々と提案されています。そこで今回は、新しいアーキテクチャの能力をファッションアイテムのデータで試し、その精度や有用性を検討してみたいと思います。いずれのネットワークにも、ファッションアイテムの画像の入力に対して、カテゴリを予測する判別器を学習させます。 データ 今回はファッションアイテムの画像を34カテゴリに分け、各カテゴリ1000枚から5000枚程度の画像を用意しました。画像の数は合計で63348枚になりました。目視で正しいカテゴリであることを確認しています。全体のうち、9割を学習用に、1割をテストに充てます。 学習環境 ニューラルネットワークの学習には、以下の環境を用います。ディープラーニングのフレームワークとしてはPFNのChainerを用います。 開発機 OS: Ubuntu 14.04 GPU: NVIDIA GTX 1080 (初任給) ソフト cuda: 8.0 cuDNN: 5.0RC Python: 3.5.1 chainer: 1.16.0 Network 一般物体認識において優秀な成績を収めているチャンピオンモデルを試します。AlexNet、GoogLeNetなどはすでにChainerの examples に用意されていますが、 データだけ差し替えても動かなかったため、必要に応じて書き換えています。動かない原因はこのバグによるものだと考えています。 Imagenet example failed to evaluate the model #1691 AlexNet [pdf] ImageNet Classification with Deep Convolutional Neural Networks 2012年のILSVRCで一世を風靡したチャンピオンモデルです。以下のネットワークは、examplesのコードで省かれていた入力次元を記述したものです。入力次元をNoneではなく、数値で記述すると動きました。(2016年10月11日時点) import numpy as np import chainer import chainer.functions as F from chainer import initializers import chainer.links as L class Alex (chainer.Chain): """Single-GPU AlexNet without partition toward the channel axis.""" insize = 227 def __init__ (self): super (Alex, self).__init__( conv1=L.Convolution2D( 3 , 96 , 11 , stride= 4 ), conv2=L.Convolution2D( 96 , 256 , 5 , pad= 2 ), conv3=L.Convolution2D( 256 , 384 , 3 , pad= 1 ), conv4=L.Convolution2D( 384 , 384 , 3 , pad= 1 ), conv5=L.Convolution2D( 384 , 256 , 3 , pad= 1 ), fc6=L.Linear( 9216 , 4096 ), fc7=L.Linear( 4096 , 4096 ), fc8=L.Linear( 4096 , 34 ), ) self.train = True def __call__ (self, x, t): h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv1(x))), 3 , stride= 2 ) h = F.max_pooling_2d(F.local_response_normalization( F.relu(self.conv2(h))), 3 , stride= 2 ) h = F.relu(self.conv3(h)) h = F.relu(self.conv4(h)) h = F.max_pooling_2d(F.relu(self.conv5(h)), 3 , stride= 2 ) h = F.dropout(F.relu(self.fc6(h)), train=self.train) h = F.dropout(F.relu(self.fc7(h)), train=self.train) h = self.fc8(h) loss = F.softmax_cross_entropy(h, t) chainer.report({ 'loss' : loss, 'accuracy' : F.accuracy(h, t)}, self) return loss NIN [arXiv]Network In Network Network in Networkは疎性結合による特徴量抽出部分に畳み込みではなく多層パーセプトロンを使っているのが特徴です。 import math import chainer import chainer.functions as F import chainer.links as L class NIN (chainer.Chain): """Network-in-Network example model.""" insize = 227 def __init__ (self): w = math.sqrt( 2 ) # MSRA scaling super (NIN, self).__init__( mlpconv1=L.MLPConvolution2D( 3 , ( 96 , 96 , 96 ), 11 , stride= 4 , wscale=w), mlpconv2=L.MLPConvolution2D( 96 , ( 256 , 256 , 256 ), 5 , pad= 2 , wscale=w), mlpconv3=L.MLPConvolution2D( 256 , ( 384 , 384 , 384 ), 3 , pad= 1 , wscale=w), mlpconv4=L.MLPConvolution2D( 384 , ( 1024 , 1024 , 34 ), 3 , pad= 1 , wscale=w), ) self.train = True def __call__ (self, x, t): h = F.max_pooling_2d(F.relu(self.mlpconv1(x)), 3 , stride= 2 ) h = F.max_pooling_2d(F.relu(self.mlpconv2(h)), 3 , stride= 2 ) h = F.max_pooling_2d(F.relu(self.mlpconv3(h)), 3 , stride= 2 ) h = self.mlpconv4(F.dropout(h, train=self.train)) h = F.reshape(F.average_pooling_2d(h, 6 ), (x.data.shape[ 0 ], 34 )) loss = F.softmax_cross_entropy(h, t) chainer.report({ 'loss' : loss, 'accuracy' : F.accuracy(h, t)}, self) return loss GoogLeNet [pdf]Going Deeper with Convolution インセプションと呼ばれる複数の畳み込みフィルタを並列に用いるモジュールを積み上げることで、効率的にパラメータの数を減らすことに成功しています。 import numpy as np import chainer import chainer.functions as F from chainer import initializers import chainer.links as L class GoogLeNetBN (chainer.Chain): """New GoogLeNet of BatchNormalization version.""" insize = 224 def __init__ (self): super (GoogLeNetBN, self).__init__( conv1=L.Convolution2D( 3 , 64 , 7 , stride= 2 , pad= 3 , nobias= True ), norm1=L.BatchNormalization( 64 ), conv2=L.Convolution2D( 64 , 192 , 3 , pad= 1 , nobias= True ), norm2=L.BatchNormalization( 192 ), inc3a=L.InceptionBN( 192 , 64 , 64 , 64 , 64 , 96 , 'avg' , 32 ), inc3b=L.InceptionBN( 256 , 64 , 64 , 96 , 64 , 96 , 'avg' , 64 ), inc3c=L.InceptionBN( 320 , 0 , 128 , 160 , 64 , 96 , 'max' , stride= 2 ), inc4a=L.InceptionBN( 576 , 224 , 64 , 96 , 96 , 128 , 'avg' , 128 ), inc4b=L.InceptionBN( 576 , 192 , 96 , 128 , 96 , 128 , 'avg' , 128 ), inc4c=L.InceptionBN( 576 , 128 , 128 , 160 , 128 , 160 , 'avg' , 128 ), inc4d=L.InceptionBN( 576 , 64 , 128 , 192 , 160 , 192 , 'avg' , 128 ), inc4e=L.InceptionBN( 576 , 0 , 128 , 192 , 192 , 256 , 'max' , stride= 2 ), inc5a=L.InceptionBN( 1024 , 352 , 192 , 320 , 160 , 224 , 'avg' , 128 ), inc5b=L.InceptionBN( 1024 , 352 , 192 , 320 , 192 , 224 , 'max' , 128 ), out=L.Linear( 1024 , 34 ), conva=L.Convolution2D( 576 , 128 , 1 , nobias= True ), norma=L.BatchNormalization( 128 ), lina=L.Linear( 2048 , 1024 , nobias= True ), norma2=L.BatchNormalization( 1024 ), outa=L.Linear( 1024 , 34 ), convb=L.Convolution2D( 576 , 128 , 1 , nobias= True ), normb=L.BatchNormalization( 128 ), linb=L.Linear( 2048 , 1024 , nobias= True ), normb2=L.BatchNormalization( 1024 ), outb=L.Linear( 1024 , 34 ), ) self._train = True @ property def train (self): return self._train @ train.setter def train (self, value): self._train = value self.inc3a.train = value self.inc3b.train = value self.inc3c.train = value self.inc4a.train = value self.inc4b.train = value self.inc4c.train = value self.inc4d.train = value self.inc4e.train = value self.inc5a.train = value self.inc5b.train = value def __call__ (self, x, t): test = not self.train h = F.max_pooling_2d( F.relu(self.norm1(self.conv1(x), test=test)), 3 , stride= 2 , pad= 1 ) h = F.max_pooling_2d( F.relu(self.norm2(self.conv2(h), test=test)), 3 , stride= 2 , pad= 1 ) h = self.inc3a(h) h = self.inc3b(h) h = self.inc3c(h) h = self.inc4a(h) a = F.average_pooling_2d(h, 5 , stride= 3 ) a = F.relu(self.norma(self.conva(a), test=test)) a = F.relu(self.norma2(self.lina(a), test=test)) a = self.outa(a) loss1 = F.softmax_cross_entropy(a, t) h = self.inc4b(h) h = self.inc4c(h) h = self.inc4d(h) b = F.average_pooling_2d(h, 5 , stride= 3 ) b = F.relu(self.normb(self.convb(b), test=test)) b = F.relu(self.normb2(self.linb(b), test=test)) b = self.outb(b) loss2 = F.softmax_cross_entropy(b, t) h = self.inc4e(h) h = self.inc5a(h) h = F.average_pooling_2d(self.inc5b(h), 7 ) h = self.out(h) loss3 = F.softmax_cross_entropy(h, t) loss = 0.3 * (loss1 + loss2) + loss3 accuracy = F.accuracy(h, t) chainer.report({ 'loss' : loss, 'loss1' : loss1, 'loss2' : loss2, 'loss3' : loss3, 'accuracy' : accuracy, }, self) return loss DenseNet [arXiv] Densely Connected Convolutional Networks 今年の8月に登場したアーキテクチャです。各層のアウトプットをすべて次の層のインプットにするというシンプルなアーキテクチャですが、精度、パラメータ数削減、汎化性能などの点で優れているようです。前の3つのネットワークで使った画像サイズに合わせたネットワークを作ろうとすると、GPUメモリに収まらなかったため、論文と同様の32×32の画像をインプットとしました。 実装はいくつかありますが、以下のChainerによる実装を参考にしました。 https://github.com/yasunorikudo/chainer-DenseNet loss関数とinitializerの部分を変更しています。 今回は、growth_rateは12、DenseBlockの数は3、各DenseBlockは40層としています。 import chainer import chainer.functions as F import chainer.links as L from chainer import initializers from six import moves import numpy as np class DenseBlock (chainer.Chain): def __init__ (self, in_ch, growth_rate, n_layer): self.dtype = np.float32 self.n_layer = n_layer super (DenseBlock, self).__init__() for i in moves. range (self.n_layer): W = initializers.HeNormal( 1 / np.sqrt( 2 ), self.dtype) self.add_link( 'bn{}' . format (i + 1 ), L.BatchNormalization(in_ch + i * growth_rate)) self.add_link( 'conv{}' . format (i + 1 ), L.Convolution2D(in_ch + i * growth_rate, growth_rate, 3 , 1 , 1 , initialW=W)) def __call__ (self, x, dropout_ratio, train): for i in moves. range ( 1 , self.n_layer + 1 ): h = F.relu(self[ 'bn{}' . format (i)](x, test = not train)) h = F.dropout(self[ 'conv{}' . format (i)](h), dropout_ratio, train) x = F.concat((x, h)) return x class Transition (chainer.Chain): def __init__ (self, in_ch): self.dtype = np.float32 W = initializers.HeNormal( 1 / np.sqrt( 2 ), self.dtype) super (Transition, self).__init__( bn=L.BatchNormalization(in_ch), conv=L.Convolution2D(in_ch, in_ch, 1 , initialW=W)) def __call__ (self, x, dropout_ratio, train): h = F.relu(self.bn(x, test= not train)) h = F.dropout(self.conv(h), dropout_ratio, train) h = F.average_pooling_2d(h, 2 ) return h class DenseNet (chainer.Chain): def __init__ (self, n_layer= 12 , growth_rate= 12 , n_class= 34 , dropout_ratio= 0.2 , in_ch= 16 , block= 3 ): """DenseNet definition. Args: n_layer: Number of convolution layers in one dense block. If n_layer=12, the network is made out of 40 (12*3+4) layers. If n_layer=32, the network is made out of 100 (32*3+4) layers. growth_rate: Number of output feature maps of each convolution layer in dense blocks, which is difined as k in the paper. n_class: Output class. dropout_ratio: Dropout ratio. in_ch: Number of output feature maps of first convolution layer. block: Number of dense block. """ self.dtype = np.float32 self.insize = 32 self.dropout_ratio = dropout_ratio in_chs = moves. range ( in_ch, in_ch + (block + 1 ) * n_layer * growth_rate, n_layer * growth_rate) W = initializers.HeNormal( 1 / np.sqrt( 2 ), self.dtype) super (DenseNet, self).__init__() self.add_link( 'conv1' , L.Convolution2D( 3 , in_ch, 3 , 1 , 1 , initialW=W)) for i in moves. range (block): self.add_link( 'dense{}' . format (i + 2 ), DenseBlock(in_chs[i], growth_rate, n_layer)) if not i == block - 1 : self.add_link( 'trans{}' . format (i + 2 ), Transition(in_chs[i + 1 ])) self.add_link( 'bn{}' . format (block + 1 ), L.BatchNormalization(in_chs[block])) self.add_link( 'fc{}' . format (block + 2 ), L.Linear(in_chs[block], n_class)) self.train = True self.dropout_ratio = dropout_ratio self.block = block def __call__ (self, x, t): h = self.conv1(x) for i in moves. range ( 2 , self.block + 2 ): h = self[ 'dense{}' . format (i)](h , self.dropout_ratio, self.train) if not i == self.block + 1 : h = self[ 'trans{}' . format (i)](h, self.dropout_ratio, self.train) h = F.relu(self[ 'bn{}' . format (self.block + 1 )](h, test= not self.train)) h = F.average_pooling_2d(h, h.data.shape[ 2 ]) h = self[ 'fc{}' . format (self.block + 2 )](h) loss = F.softmax_cross_entropy(h, t) chainer.report({ 'loss' : loss, 'accuracy' : F.accuracy(h, t)}, self) return loss 学習 学習部分のコードは、 chainer/examaples/imagenet/train_imagenet.py が非常に有用です。必要なデータだけをその都度マルチプロセスで読み込み学習することでメモリの消費を抑えることができます。メモリに乗り切らない大規模なデータを学習させる際は必須です。以前のバージョンでは、feeder、logger、train_loopがコード内にきっちり記述されていましたが、最近のバージョンのコードを見直してみると、複雑で弄るのが難しい部分が上手く隠されており、読みやすいコードになっていました。 各ネットワークを公平な評価にするために、条件をできるだけ揃えます。OptimizerにはMomentumSGDを用い、learning rateは0.01から開始し、30epochを回った所で、0.001に下げ、50epochまで学習させます。平均画像を引く、cropするといった前処理は行っていません。 結果 network train acc test acc Layers input AlexNet 99.68% 72.49% 8 3×227×227 Network In Network 99.09% 75.70% 12 3×227×227 GoogLeNet 99.85% 81.14% 22 3×224×224 DenseNet 84.53% 75.20% 120 3×32×32 DenseNet以外のいずれのモデルも、学習データを入力とした場合の精度は99%を超えました。その中でも、未知のデータに対してもっとも精度が高かったのはGoogLeNetでした。一般に、層の数を増やすと過学習する恐れがありますが、GoogLeNetのアーキテクチャはうまく汎化してくれるようです。一方、DenseNetは小さなインプットデータを用いている分、他のネットワークより不利なはずですが、Accuracy 75.20%と健闘しています。学習データでも99%を出せてないことから、32×32は、227×227に比べて情報が落ちていると考えられますが、それでもAlexNetの成績を超え、NINと同等の成績を出しているという点で性能の高いアーキテクチャと言えるでしょう。GoogLeNetとDenseNetの条件を揃えた上での性能比較は、今回は難しかったので省きます。 議論 定義の曖昧なファションアイテムのカテゴリ分け もっとも精度の高かったGoogLeNetのモデルにカテゴリ判別をさせ、confusion matrixを図示してみます。 対角成分が赤いカテゴリは判別精度が高く、黄色や緑色の場合は判別精度が悪いことを意味します。対角成分が緑色のカテゴリに注目すると、「シューズその他」、「帽子その他」などの「その他」カテゴリの精度が弱いということがわかります。「その他」カテゴリというのは、例えばシューズ系であれば、「パンプス」、「スニーカー」、「サンダル」、「ブーツ」以外のシューズを含めるためのカテゴリです。代表的なカテゴリ以外のアイテムも何らかのカテゴリを与えるために、このようなカテゴリを設けています。定義の不明瞭なカテゴリであるため、様々な特徴をもつシューズ系のアイテムが集められ、判別が難しいカテゴリとなっていると考えられます。 判別が難しい例は「その他」カテゴリに限らず、例えば「ロングパンツ」の中に、「スカート」と見分けがつかないものがあったり、「トップス」と「ルームウェア」など意味が重複するカテゴリがあったりします。つまり、画像データだけから完璧なカテゴリ判別器をつくるのは難しそうです。 精度80%のモデルをどう使うか それでもiQONのサービス上、ユーザーはカテゴリ毎にアイテムを閲覧することが多いので、カテゴリをきっちりと判別できている必要があります。ブラウザでは1ページで30件のアイテムが表示されるので、20%も間違いがあるとかなりの違和感があります。よって、精度80%のモデルができたとしても、全てのアイテムの判別をこのモデルに委ねる訳にはいきません。 予測カテゴリをそのまま使うのではなく、一つ前の段階のsoftmax関数のアウトプットを使うという方法が考えられます。softmax関数のアウトプットはカテゴリに属する確率として捉えることができるので、softmaxのアウトプットが閾値を超えたときだけ判別結果を採用するといった方法や、判別を間違えないと分かったカテゴリのみ判別結果を採用するといった工夫をすることで誤判定を減らすことができそうです。 もう一つの使い方として、softmax関数の出力を新たなインプットとして、他の情報と組み合わせて判別につかうという方法も考えられます。画像データで数カテゴリに候補を絞りながら、ブランドやアイテム名などで情報を補えば、より正確にカテゴリの判別が行えます。実際に、iQONのカテゴリ判別器は、AlexNetのアウトプットを新たな特徴量として扱い、アイテムの名前やブランド、ドメインなど他の情報を合わせた判別器を学習させることで判別精度を上げています。 まとめ ファッションアイテムの画像データに対して、ニューラルネットワークによる一般物体認識モデルを学習させました。今回試した4つのアーキテクチャの中では、GoogLeNetがもっとも精度が高いことがわかりました。不利な条件で高い精度を誇ったDenseNetも条件を揃えることで、どんなパフォーマンスを発揮するか気になるところです。 また、ファッションアイテムのカテゴリの特徴上、画像だけでは判別できないアイテムも存在します。実際には、画像によるカテゴリ判別器単体で使うことは難しく、他の情報も併用しながら、精度を上げていくといった工夫が必要になりそうです。 最後に VASILYでは、最新の研究にアンテナを張りながら、同時にユーザーの課題解決を積極的に行うメンバーを募集しています。 興味のある方はこちらからご応募ください。
Androidエンジニアの堀江( @Horie1024 )です。Androidアプリのテストについて考えた時、UIの操作を含むActivity単位の結合テストをどう実行するか?が課題になります。 最近では、テスト基盤をクラウド上で提供するサービスが複数リリースされており、今年のGoogle I/Oで正式に公開された Firebase Test Lab もその一つです。本記事では、Firebase Test LabでのAndroidアプリのテストについてご紹介しようと思います。 サンプルアプリ 本記事を通してサンプルアプリとして以下を使用します。2Activityで構成される簡単なアプリで、テストコードをEspressoで書いています。 github.com Firebase Test Labとは? 以下の動画を観ていただければ概要は掴めるかと思います。 Firebase Test Lab はGoogleが提供しているクラウドベースのAndroidアプリのテスト基盤です。以前はCloud Test Labと呼ばれていましたが、Google I/O 2016で Firebase の一機能として正式にリリースされました。「for Android」と紹介されていますが、iOS版は(今のところ)ありません。 Firebase Test Labの特徴としては以下の3点が挙げられます。 実機でのテスト Googleのデータセンターに配置された実機でテストが実行される。デバイスの種類、OSのバージョン、Localeなどの組み合わせた幅広い条件でのテストをまとめて実行でき、特定の条件での不具合を発見しやすくなる。 テストコード不要なテスト Roboテスト と呼ばれるテストの実行モードがあり、テストコード無しでアプリのテストを実行可能。テストコードがあるのであれば、それをFirebase Test Labで実行可能。 開発フローへの統合 Android Studio、Firebase console、gcloudコマンドからテストを実行可能。gcloudコマンドを使用することでJenkinsやCircleCIといったCIシステムとの連携も可能。 Firebase Test Labでのテストの概要 テスト手法 Test Labで選択できるテスト手法は以下の2種類です。 Instrumentationテスト Roboテスト Instrumentationテスト Instrumentationテストで使用できるテスティングフレームワークは以下の3種類となっており、実機で30分、仮想端末で60分以内に完了したテストについてのみ結果を取得できます。 Espresso Robotium UI Automator 2.0 Roboテスト Roboテストは、Instrumentationテストのテストコードを書いていない場合でも、クローラーの様にアプリのUI構造を解析し探索することで、自動的にユーザーの操作をシミュレートすることでアプリの不具合を発見します。詳細の後ほど紹介します。 テストの実行方法 テストの実行方法は、3種類から選択可能です。 Firebase console Web UIによってAPKのアップロードとテストの実行があらゆる環境で可能 Android Studio 開発環境から開発中のアプリのテスト実行が可能 CLI gcloudコマンドによってCLIからの実行が可能になり、自動ビルドおよび自動テストプロセスへの組込みが可能 また、これら3つの方法とは別にpre-launch reportsという機能があり、これによってAPKをPlay Storeにα or βでアップロードした際に苞を自動で実行させることができます。pre-launch reportsの詳細は後ほど紹介します。 テストが実行される条件について Firebase Test Labでは作成した Test Matrix に従ってテストが実行されます。Test Matrixは、 Test Dimension と Test Execution と呼ばれる概念から決定されています。 Test Dimensionは、デバイスに関連する設定項目のことで、各Dimensionはデバイスタイプ、APIレベル、ロケール、画面の向きからなります。テストを実行する前に各Dimensionから項目を選択することでFirebase Test Labが有効な設定項目の組合せのリストを生成します。各有効なDimensionの組合せをTest Executionと呼びます。そして、Test ExecutionのリストがTest Matrixであり、選択したTest Dimensionの組合せです。 まとめると以下のようになります。 名称 概要 Test Dimension デバイスに関連する設定項目 Test Execution 有効なTest Dimensionの項目の組合せ Test Matrix Test Executionのリスト 例として次のような条件をTest DimensionとしてTest Labに与えた場合を考えてみます。 デバイス: Nexus 5, Nexus 6 APIレベル: 21, 22 ロケール: Japanese 画面の向き: 縦向き この条件から得られるTest Matrixは以下のようになります。 デバイス APIレベル ロケール 画面の向き Nexus 5 21 Japanese 縦向き Nexus 5 22 Japanese 縦向き Nexus 6 21 Japanese 縦向き Nexus 6 22 Japanese 縦向き この時選択した端末とAPIレベルによっては無効な組合せとなる場合がり、その場合テストの実行はスキップされます。また、Test Matrixの各Test Executionはpassかfailのいずれかの結果になり、もしTest Executionが1つでもfailならば、テスト全体がfailになります。 料金体系 Test Labを利用するには、FirebaseのBlazeプランを契約する必要があります。そして、利用料金は以下の通りです(2016/9/30現在)。 各実機に対して $5/1時間 各仮想端末に対して $1/1時間(2016/10/1まで無料) 課金処理は、分単位でされ、テストを実行するために使用した時間分のみ課金されます。アプリのインストールに掛かる時間やテスト結果を集計する時間は含まれません。 Firebaseプロジェクトの作成と課金 Firebaseプロジェクトの作成 Firebaseのプロジェクトをまだ作成していないのであれば Firebase console から作成します。「新規プロジェクトを作成」をクリックし、プロジェクト名と国を選択し、「プロジェクトを作成」をクリックします。これでプロジェクトが作成されます。 課金 作成したプロジェクトのメニューから「Test Lab」を選択します。 Test Labを利用するためには、「 Blazeプラン 」を選択する必要があるので選択します。 課金処理が完了すると以下のような画面になり、これでFirebase Test Labを利用する準備ができました。 テストの実行 前節まででFirebase Test Labを利用する準備ができたので、実際にテストを実行してみましょう。「最初のテストを実行する」をクリックすると以下のウィザード画面が表示されます。 テスト手法として「Roboテスト」、「Instrumentationテスト」の2種類から選択できます。その他のテスト方法として「gcloud」、「Android Studio」の2種類が紹介されていますが、これらは後ほど紹介します。 Roboテスト Roboテストとは、Firebase Test Labにデフォルトで組込まれているテストツールです。Roboテストは、クローラーの様にアプリのUI構造を解析し探索することで、自動的にユーザーの操作をシミュレートします。 Monkey test とは異なり、Roboテストでは、シミュレートするユーザーの操作(種類や順番)は、デバイスの設定などの条件が同じならば常に一定になります。 この特徴によって、Roboテストは、Monkey testでは不可能なバグフィックスの確認やリグレッションテストに利用できます。また、シミュレートしたユーザーの操作は、ログ、スクリーンショット、スクリーンショットから生成した動画によって確認でき、クラッシュの原因を把握するための手がかりとなります。 Roboテストで使われる用語について Roboテストのドキュメントで使用されている用語についてまとめます。 Root メインスクリーン UI Branch 枝分かれした画面遷移の分岐 Depth 画面の階層の深さ 画面A-Eまで存在し、A->B、A->C、B->D、B->Eと画面遷移する場合、以下のような図になります。 Roboテストの設定 Maximum depth RoboテストがアプリのUI階層のどの程度の深さまで探索するかをMaximum depthによって設定できます。Maximum depthは、テストが特定のUI BranchからRootに戻るまでどの程度の深さまで探索すべきかを指定します。デフォルト値は50です。値を2未満に指定すると探索はRootで止まります。 Timeout Roboテストの実行時間についてTimeoutを指定できます。アプリのUIの複雑さによってRoboテストが完了するまでに時間は変化します。推奨設定値は、通常のアプリで120秒、複雑なUIを持つアプリで300秒です。デフォルト値は、Android StudioやFirebase consoleから実行した場合300秒、gcloudツールから実行した場合1500秒となっています。 パラメータ設定についての補足 公式ドキュメント には、画面の分岐が多く、遷移の深さが深い場合、Roboテストがアプリを完全に探索できるよう以下の設定のうちいずれかを検討するべきであると書かれています。 timeoutを高い値に設定し、Roboテストが複数のUI Branchを探索できるようにする maximum depthを低い値に設定し、Roboテストに各UI Branchの探索を完了させる パラメータの設定については、アプリによって適切な値が異なるため、最初はデフォルト値で運用しその後パラメータを調整するのが良さそうです。 Google Play Developer Consoleとの連携 Pre-launch reportsという機能がFirebase Test Labリリース後から利用できるようになっています。この機能は、APKをalphaもしくはbetaでGoogle Play Developer Consoleにアップロードした際に自動的にRoboテストを実行する機能です。テストには、Googleが一般的によく使われていると判断した複数のデバイスで、OSのバージョンやlocationなどの設定が異なる複数の条件で実行されます。ただ、世界でよく使われている端末が選択されているようで日本製の端末は含まれていませんでした。 この機能を有効にする方法は こちら にまとまっています。Pre-launch reportsは無料で使用できます。Blazeプランを契約している場合、Roboテストの代わりに任意のテストを実行可能です。 以下は、iQONでPre-launch reportを有効にした際にRoboテストが実行された端末のリストです。 テスト結果は各端末毎に確認できるようになっています。 Roboテストの制限 Roboテストには、テストを実行する上での制限が3つ存在しています。 UI framework support Sign-in Scripting UI framework support Roboテストは、Android UI frameworkの要素(View、ViewGroupオブジェクトなど)が使用されたアプリにのみ対応しています。もしUnityのような別のUI frameworkを利用している場合、テストは何も実行されずRootから進まずに終了します。 Sign-in Roboテストは、サインイン画面を迂回できません。唯一サインイン画面を迂回できる例は、認証にGoogleアカウントを使用しかつ追加のアクション(ユーザー名を入力するような)を要求しない場合です。 以下は、Roboテストのテスト結果で確認できるアクティビティマップという図になります。RoboテストはiQONへのログインを試みていますが、ログイン、新規登録画面から先にRoboテストが進むことができていません。 解決方法として考えられるのは、 System Property でTest Labでアプリが実行中であることを判別し、その場合に認証をスキップする方法です。 Settings.System.getString メソッドで firebase.test.lab を参照することでTest Labでアプリが実行中かを判別できます。 String testLabSetting = Settings.System.getString(context.getContentResolver(), "firebase.test.lab" ); if ( "true" .equals(testLabSetting)) { // Test Labでの実行中に行う処理 } Scripting Roboテストでは、スクリプトを書いて任意の操作を行わせることはできません。アプリに対して行われる操作は全てTest Lab側で決定されます。 Roboテストの実行 「Roboテストを実行」をクリックすると以下の画面になるのでAPKをアップロードし、完了後に「続行」をクリックします。 ディメンションを選択という画面に遷移します。ここでは、Roboテストを実行する端末の種類やAPIレベルといったTest Dimensionを指定します。 詳細設定からタイムアウト(Timeout)と最大深度(Maximum depth)を指定可能です。 下部にある「X件のテストを開始」をクリックすることでテストを開始します。先程指定したTest DimensionをもとにTest Matrixが作成され、Test LabはそのTest Matrixに従ってテストを実行します。 Instrumentationテスト 基本的にRoboテスト場合と同様の手順になります。InstrumentationテストがRoboテストと異なるのは。通常のAPKとは別にテストAPKが必要になる点です。以下の様に「アプリAPK」と「テストAPK」をアップロードする必要があります。 テストAPKは以下のコマンドで作成できます。 $ ./gradlew assembleDebugTest 次にRoboテスト同様Test Dimensionを指定します。 そして、下部にある「X件のテストを開始」をクリックすることで、こちらもRoboテスト同様テストを開始します。 テスト結果の確認 テスト結果はアプリ毎にグルーピングされ過去のものも含めてFirebase consoleのTest Labタブから確認でき、テスト実行後90日間保存されます。 テスト結果は各Test Execution毎に表示されたアイコンで簡単に判別できます。 アイコン 意味 成功 1件以上失敗 Test Labのエラーにより実行不能 スキップ さらに、各Test Execution毎に詳しいテスト結果を確認できます。Instrumentationテストでは、テストケース、ログ、スクリーンショット、動画でテスト結果を確認できます。また、「ソースファイルを表示」をクリックするとCloud Storageへアクセスでき、元データを確認できます。 Roboテストでは、テストケースの代わりにアクティビティマップが表示されます。 Android Studioからの実行 Firebase Test Labでのテストは、Android Studioからも実行できます。実行出来るようにするには、Android StudioのTest Configurationを設定する必要があります。以下は、Android Studioの設定からテストを実行するまでの手順です。 メインメニューから「Run」> 「Edit Configurations」をクリックします。 Add New Configuration「+」をクリックしAndroid Testsを選択します。 Android Testのconfigurationダイアログが開いたら Test name、Module type、Test type、Test classを設定します。 Targetのドロップダウンから「Firebase Test Lab Device Matrix」を選択します。 Firebaseと未接続なら「Connect to Firebase」をクリックしログインします。 以下の赤枠で囲ったボタンをクリックし、Firebaseプロジェクトを選択します。 Test Matrixの作成と設定 Matrix Configurationの以下の赤枠で囲ったボタンをクリックしダイアログを開きます。 Add New Configuration (+)をクリックします。 Name欄にConfigurationの名前を記入します。 テストを実行したいDevice、Platform、Locale、Orientationにチェックを入れます。 OKをクリックし保存します。 Run/Debug ConfigurationsダイアログのOKをクリックし閉じます。 これでAndroid StudioからFirebase Test Labでのテストを実行できるようになったので、作成したConfigurationを選択してRunボタンをクリックしテストを実行することができます。テストの実行結果は以下のように表示されます。また、Firebase consoleでテスト結果の詳細を確認することも可能です。 CLIからの実行 Firebase Test Labは、 gcloud コマンドを使用してCLIからテストの実行をスケジューリング可能です。CLIでの実行は以下のQiitaの記事としてまとめています。 qiita.com CIシステムとの連携 CIシステムからFirebase Test Labのテストを実行することも可能です。CircleCIを例にCIシステムとFirebase Test Labの連携については以下のQiitaの記事としてまとめています。 qiita.com まとめ Firebase Test Labの概要と使用方法についてご紹介しました。Roboテストでの認証処理や、日本製端末が選択出来ないなどの問題はありますが、比較的簡単にテストを実行することができます。先日リリースされた Android Studio 2.2 では、 Espresso Test Recorder が搭載されるなどEspressoでテストを書くハードルは下がっていますし、Firebase Test Labと併せることでUIのテストを実行しやすくなるはずです。弊社でもiQONの開発フローに導入していこうと思います。 最後に VASILYではiQONを一緒に開発してくれるAndroidエンジニアを募集しています。ご興味がある方は以下のリンクをご覧ください。
2016年9月20日、第三回目となる Fashion Tech meetup を開催しました。前回に引き続き、 MERY を運営する株式会社peroli様、 FRIL を運営する株式会社Fablic様との共同開催となりました。 今回も増枠を設けるほどの申込みがあったのですが、イベント当日は台風16号が接近し、天候に恵まれない日となってしまいました。 そんな中、悪天候にも関わらず多くの方が足を運んだくださり、Fashion Tech meetupへの期待を感じられ嬉しく思います。 今回、弊社からも2名登壇しましたので、その資料を公開します。 残念ながら参加出来なかった方、Fashion Tech meetupを初めて知った方、是非ご一読ください。 メインセッション 『「もっと可愛いワンピースないの?」ディープラーニングで実現する、いままでにないアイテム検索』 新卒のデータサイエンティスト後藤が発表しました。 現在iQONでは、1000万点以上のアイテム情報や250万人の行動ログデータなど、膨大なファッションデータを保有しています。 アイテムに関しては日々増え続けており、1シーズンで約90万点ほどの商品追加が行われています。 このような状況下でも欲しいアイテムが発見できるように、機械学習などの研究開発を続けているのが弊社のデータサイエンティストです。 会場ではディープラーニングを活用した類似画像検索のデモを実施しました。画像に「フェミニン」などの属性を足すことで精度の高い類似画像検索を実現しています。 スライドだけでは物足りず、動いている様子を見たい方は是非弊社までお越しください。 弊社のデータサイエンティストは研究や分析だけでなく、実装を行いプロダクトに組み込むまで担っています。 きっと他では聞けない面白い話が聞けるはずです。 「もっと可愛いワンピースないの?」ディープラーニングで実現する、いままでにないアイテム検索 from Ryosuke Goto LT ES6を導入しようとして、古のRailsアプリの闇にハマった話 LTは新卒のエンジニア茨木が発表しました。 フロントエンドをモダンな環境にしていくため、iQONにES2015を導入しようとした内容となっています。 browserify-rails + browserify + babelといった環境で導入を試み、思いもよらぬ実装に苦戦を強いられましたが、無事原因を特定して改善が行われました。VASILYでは新卒エンジニアもフレームワークやアーキテクチャを変更に携わり、自ら率先してチャレンジしていく環境なのだと伝われば幸いです。 ES2015をRailsアプリに導入しようとして思わぬ闇にハマった話 from Nobuhito Ibaraki まとめ VASILYの資料を公開しましたが、いかがでしたでしょうか。 本記事で興味を持ち、Fashion Tech meetup #4に足を運んでくださる方が増えれば幸いです。 おわりに 弊社の資料を通して興味を持たれた方、是非一度オフィスまでお越しください。 一緒にFashionxTechnology盛り上げていきましょう! https://www.wantedly.com/projects/62047 www.wantedly.com https://www.wantedly.com/projects/61388 www.wantedly.com
iOSエンジニアの遠藤です。 先日iQONで、Xcodeのビルドパフォーマンス改善の一環としてEmbedded Frameworkの導入を行いました。 今回は、そのEmbedded Frameworkの導入について紹介したいと思います。 Embedded Frameworkとは? Embedded FrameworkはiOS 8・Xcode 6から追加された機能です。 アプリのコードを分割してFrameworkとして扱うことができます。 Embedded Frameworkを導入することのメリット コードを分割してFramework化することで以下のメリットがあります。 コードのターゲットが分かれるため、差分コンパイルされビルドパフォーマンスが向上する App Extentionsを持つアプリの場合、メインターゲットとExtension間でコード共有することができる Frameworkに分けることで、依存関係がシンプルになる Frameworkごとにテストを書くことができる 導入方法 Xcode 7.3.2 Swift 2.2での導入方法です。 Frameworkの作り方 Embedded Frameworkの導入は簡単で、Xcodeのツールバーから「File」 → 「New」 → 「Target」を選択すると、以下の画面が表示されます。 そこから「Framework & Library」 → 「Cocoa Touch Framework」を選択すると新しくターゲットが追加されます。 新しくできたTargetに分けたいコードを追加していきます。 Frameworkの使い方 Frameworkとして扱うメソッドやclass、変数にはメインターゲットからアクセスできるように public 修飾子をつけてください。 import Foundation import Alamofire public class Util { public var name : String ? public init () { } } 使用したいFrameworkをimportするだけで使えるようになります。 import UIKit import SampleFramework class ViewController : UIViewController { override func viewDidLoad () { super .viewDidLoad() let util = Util() util.name = "hoge" } } Embedded Frameworkの導入はすごく簡単です。 しかし、Framework内でライブラリを使う場合はいろいろとハマる部分がありました。 その対応方法をいくつか紹介したいと思います。 CocoaPodsでライブラリを使う場合 Embedded Frameworkだけでしか利用していないライブラリでも、メインターゲットにもインストールする必要があります。 Podfileにターゲットごとに同じライブラリ名を記述するのは手間なので、iQONでは abstract_target でまとめています。 // Podfile abstract_target ' All ' do # Targetとかぶらなければ、名前の文字列は何でも大丈夫です pod ' Alamofire ' target ' SampleApp ' do # AlamofireとSVProgressHUDがインストールされる pod ' SVProgressHUD ' end target ' SampleFramework ' do # Alamofireがインストールされる end end Carthageでライブラリを使う場合 CarthageもCocoaPodsと同様にFrameworkとメインターゲットどちらにも、ライブラリを設定する必要があります。 メインターゲットの Embedded Binaries にFrameworkで利用しているライブラリを設定します。 Library not loaded でクラッシュする場合 CocoaPodsでインストールしたライブラリが Library not loaded でクラッシュしてしまう時は「Build Phases」を確認してみてください。 おそらく、インストールしたライブラリをアプリ内に組み込むための Run Script が存在していないためにクラッシュしている可能性があります。 [CP] Embed Pods Frameworks という名前の Run Script は本来CocoaPodsが自動的に追加するもので、なぜそのRun Scriptが存在しないのかは詳しくは分かりませんが、追加すればクラッシュしなくなります。 Framework not found Pods_** でビルドが通らない場合 [Build Phases] -> [Link Binary With Libraries]を確認してみてください。 エラーで表示されているFrameworkを削除してください。 以前からCocoaPodsでライブラリを管理しているプロジェクトだと、 Pod_**.framework というものが Link Binary With Libraries に設定されています。 しかし、Podfileを abstract_target で書くようにした場合、新しく Pods-**-**.framework というものが設定されるので、 Pod_**.framework が不要になります。 不要なものが設定されているために、ビルドに失敗していました。 導入した結果 まだ、一部分のコードしかFrameworkにできていないので、差分コンパイルによるビルドパフォーマンスの向上はあまり感じることはできていません。 しかし、frameworokに分けることでコードの依存関係が明確化されたのは良かったと思います。 今までコード感の依存関係がごちゃごちゃになっていたのが明確化されたので、コードの設計を見直すきっかけになりました。 まとめ Embedded Frameworkを導入した話でした。 Frameworkを作ること自体はすごく簡単です。 Framework内でライブラリを使用する際にハマるポイントがいくつかありますが、慣れてしまえばすごく便利でメリットが多いのでおすすめです。 まだiQONは一部しか対応をできていませんが、随時Framework化してビルドパフォーマンスの向上を目指していきたいと思います。 最後に VASILYでは一緒にiQONを開発してくれる仲間を募集しています。少しでもご興味のある方は以下のリンク先を御覧ください。 https://www.wantedly.com/projects/62340 www.wantedly.com