TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは、バックエンドエンジニアの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
アバター
フロントエンドエンジニアのnibaです。 先日、iQONのスマホページでviewportの改善を行いました。 その際の技術選定や工夫について述べていきたいと思います。 viewportについて まず初めにviewportに関して説明します。 viewportはHTMLメタ要素の一つです。これを指定することにより、スマホ/タブレットで表示される際の描画領域幅やスケールを決定できます。 viewportは以下のようなタグで指定できます。 < meta name = 'viewport' content = 'width=device-width,initial-scale=1' > 上の例では、描画領域幅widthにデバイス幅を意味するdevice-width、スケールinitial-scaleに1が指定されています。widthには980のようなピクセル固定値やdevice-widthを指定できます。固定値の場合には描画領域が画面幅に合うように自動でスケーリングされます。 昔は幅320pxのデバイスが多かったため、widthに320を指定するケースが多くありました。 しかし、レスポンシブデザインがスタンダードになってきている今、widthに固定値ではなくdevice-widthを指定することをGoogleも推奨しています。 Google Adsenseをはじめとした多くの広告もdevice-widthを前提として作られています。 今回やったこと iQONでは、1年前のリニューアル時にiPhone6のRetinaディスプレイの実ピクセル数に合わせて viewportのwidthに750を指定して実装しました。 しかし、これは現在のスタンダードにそぐわない上にデバイス幅基準の広告にも合いません。 そのため、8月にviewportのwidthにdevice-widthを指定する改修を行いました。 次に実際にどのように改修を行ったかについて述べていきます。 具体的にどのように改修を行ったか 実装で用いる単位の検討 今回の改修では、device-widthによらずレイアウトは一定にすることにしました。 そのため、絶対値のpxを相対値の単位に変更する必要があったので、CSSの相対単位の比較検討を行いました。 以下にCSSの相対単位の代表例を示します。 単位名 定義 対応デバイス rem htmlタグに指定されたfont-sizeを1remとする iOS5~ Android2.1~ em 親要素のfont-sizeを1emとする 全て vw ビューポートの幅に対する百分率。 iOS6.1~ Android4.4~ vh ビューポートの高さに対する百分率。 iOS6.1~ Android4.4~ 最終的に、iQONでは以下の理由でremを採用することにしました。 1remの大きさを自由に調整できる。 要素の包含関係に依存しないので、pxと同様に扱える。 iQONでサポートしているAndroid4.0~で使用できる。 1remの大きさの設定 1remの大きさを決定するにあたり、以下の2つを満たすような実装を考えました。 各デバイスで表示が変わらないようにする 幅750pxのデザインデータから直感的にコーディング出来るようにする ( デザイン上の100px=1rem など) 1.の条件を満たすために、画面幅基準の相対単位vwで1remの大きさを指定するようにしました。 さらに、1remに13.33vwを指定することで デザイン上の100px=1rem の関係になり、2.の条件を満たすことができました。 改修前 h1 { font-size : 20px ; width : 700px ; } 改修後 html { font-size : 13.33 vw; /* 1[vw] = 7.5[px]; 100[px] / 7.5[px/vw] = 13.33vw; */ } h1 { font-size : 0.2rem ; width : 7rem ; } デザイン上の1px=1rem や デザイン上の10px=1rem がより直感的ですが、実際にブラウザでレンダリングされる際に1remが10px未満になってしまいます。10px未満のフォントサイズはブラウザで正しくレンダリングされない為、 デザイン上の100px=1rem にしています。 ここまでvwを用いて1remの大きさを設定してきましたが、vwはAndroid4.4以上しか対応していません。 しかし、Android4.3以前の端末からのアクセスの場合でも、以下のJavaScriptコードで対応できます。 document .querySelector( 'html' ).style.fontSize = 100 * window .innerWidth + MOCKUP_VIEWPORT + 'px' これで、CSSの改修を単純な置換作業に落とし込むことができました。 幾つかハマった点もありましたが、単純な置換作業のみで大方問題なく改修出来ました。 次に、ハマった点をいくつかピックアップしたいと思います。 改修でハマった点 remはレンダリングの過程でpxに変換され、この際に小数が発生してしまいます。小数の処理はブラウザに大きく依存します。そのため、小さな値をremで指定した場合に不具合が発生することがあります。 ここでは実例を3つ紹介したいと思います。 ボーダーが消えてしまうことがある border-widthをremで指定するとデバイスによって1px未満の値になってしまい、 最悪の場合ボーダーが消えます。iQONでは、デバイスごとに表示が変わってしまうことを前提にborder-widthはpx指定を残しました。 border-radiusで描いた円が歪む ブラウザの小数点以下の処理により正円で無くなってしまうことがあります。 デバイスごとに表示が変わってしまうことを前提にpx指定を残しました。 子要素が微妙に隠れてしまう HTMLでは親要素は子要素に合わせて拡大されます。 しかし、子要素のサイズが小数点を含む場合、親要素が小数点以下を切り捨てられたサイズまでしか 広がらず、子要素の一部が隠れることがあります。対応方法はケースバイケースですが、子要素に指定していたスタイルを親要素に移すなどして対応しました。 子要素 親要素 まとめ CSSの相対単位は、レンダリングの過程で小数のpxに変換された際に表示がおかしくなる場合があるので 注意が必要な場合があります。 しかし、remを工夫して用いることによりviewportのdevice-widthへの改修を最小限のコストで行うことができました。 最後に VASILYでは、Webサービスを自らの手で創りたいハングリーな仲間を募集しています。 ご興味のある方は以下のリンク先をご覧ください。 https://www.wantedly.com/projects/61388 www.wantedly.com
アバター
こんにちは、神崎( @tknzk ) です。先日公開した ブログ からアップデートがありましたので、まとめておきます。 変更点としては、EBのBase Platformの変更と mackerel-agentの alpineベース化、ECRのセカンダリDocker Registryとしてのセットアップになります。 EB Base platformの変更 Elastic Beanstalkのbase platformを最新の 64bit Amazon Linux 2016.03 v2.1.3 running Multi-container Docker 1.11.1 へ移行しました。 これにより、container側のdocker も 1.11系を利用することができるようになりました。hostとcontainerのdocker versionの差異により、containerが起動できない問題が改善出来ました。 しかし、先日 1.12 がリリースされているので、またhostとcontainerのversionがずれる状況が起きる可能性がありますが、適宜最新にアップデートをしていきたいと思います。。 mackerel-agent のalpineベース化 課題であった host側のdocker versionとの組み合わせ問題と、alpine:3.3で起きていたmackerel-agent-plugin-linuxが起動できない問題が alpine:3.4で解決されたことから mackerel-agentのimageもalpineをベースにしたものをbuildしてproductionへ投入しています。 動かしているpluginは下記のとおりで、他のpluginでは問題が起きる可能性があるので、alpine linuxベースでmackerel-agentを動かす場合は、検証の上設定してください。 mackerel-agent mackerel-plugin-linux mackerel-plugin-nginx mackerel-plugin-docker Docker RegistryのSPOF問題への対応 以前のブログや、先日登壇した AWSUGコンテナ支部 でも発表させていただきましたが、過去に一度オペレーションミスによる障害をおこしており、オペレーションミスはできるだけ防ぐとともに、利用しているDocker Registryのquay.ioが万が一ダウンしてしまった場合に備えての冗長化を行いました。 セカンダリのDocker RegistryとしてAmason ECRを採用し、適当なタイミングで Docker imageをpushしておく仕組みを作りました。 以下のリンクにもあるとおり、東京リージョンにもECRがやってきたので、採用するには良いタイミングかと思います。 https://aws.amazon.com/jp/about-aws/whats-new/2016/08/amazon-ec2-container-registry-region-expansion/ 必要なdocker imageは EBの設定ファイルである Dockerrun.aws.jsonに集約してあるので、そこから tagを含めたimage名を取得し docker pullして imageを取得 docker tag コマンドで tagを付け替えを行い、 docker push で Amazon ECRへpushする簡易的なscriptを作りました。 # Docker image sync quay.io to Amazon ECR # require docker login over aws cli # command here : # aws ecr get-login --region ap-northeast-1 # and exec display docker login command # $(aws ecr get-login --region ap-northeast-1) # require ' json ' def targets # Dockerrun.aws.json json = open( ' ./Dockerrun.aws.json ' ) do | file | JSON .load(file) end images = [] json[ ' containerDefinitions ' ].each do | caontainer | images << caontainer[ ' image ' ] end images end def docker_pull (image) puts image ` docker pull #{ image }` end def tag_change (image) new_tag = replace_repository(image) puts new_tag ` docker tag #{ image } #{ new_tag }` end def docker_push (image) new_tag = replace_repository(image) ` docker push #{ new_tag }` end def replace_repository (image) repository, tag = image.split( ' : ' ) repository.gsub!( %r{ quay . io/vasilyjp } , ecr_host) return "#{ repository } :latest " if ARGV [ 0 ] == ' latest ' "#{ repository } : #{ tag }" end def ecr_host ' xxxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com ' end targets.each do | image | docker_pull(image) tag_change(image) docker_push(image) end 上記のscriptを適当なタイミング(imageに変更があったタイミング)で手動で実行して Amazon ECRに docker imageをpushしておき、プライマリなDocker Registryに事故が発生した際には、 Dockerrun.aws.jsonの 各ContainerDefinitionsのimageの部分と、認証情報の部分を書き換えるだけで、ElasticBeanstalkをデプロイすることできる状態を確保することができました。 切り替えのデプロイは手動による対応が必要になりますが、最低限の冗長化を行うことができました。 参考までにテストでECRのimageからデプロイした時の状態です [ec2-user@ip-xx-xx-xx-xx ~]$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 33783cbab565 xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/mackerel-agent:latest "/startup.sh" About a minute ago Up About a minute ecs-awseb-ad-server-stg-ammpxnj9ag-362-mackerel-agent-f6f3b99ddbb2baddee01 cc1018ff8e35 xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/nginx:latest "nginx -g 'daemon off" About a minute ago Up About a minute 0.0.0.0:80->80/tcp, 443/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-nginx-proxy-f28e9092c5d9dba2ee01 f65afdb19efb xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/ad_server:latest "supervisord" About a minute ago Up About a minute 3000/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-ad-server-c4e7d395e5d28abe9c01 94b3bf57a71a xxxxxxxxxxxx.dkr.ecr.us-east-1.amazonaws.com/td-agent:latest "td-agent --log /var/" About a minute ago Up About a minute 0.0.0.0:24224->24224/tcp ecs-awseb-ad-server-stg-ammpxnj9ag-362-fluentd-84e2a5e6b58c84e36400 aa66d70ad211 amazon/amazon-ecs-agent:latest "/agent" About a minute ago Up About a minute 127.0.0.1:51678->51678/tcp ecs-agent まとめ EB Base platformが docker 1.11 に対応したことで、docker imageの alpineベース化をすすめ、ECRを使うことで、Docker RegistryのSPOFの改善をおこいました。 以上、 ElasticBeanstalk w/ multi-container docker の最新事情でした。 最後に VASILYでは、一緒に開発をしてくれる仲間を募集しています。 Dockerを使った開発/運用をしてみたい方は以下のリンクをご確認ください!
アバター
はじめまして。サーバーサイドエンジニアのrihoです。 私は、とあるweb系大企業に新卒として入社し、1年働いた後にVASILYに転職してきました。 VASILYで働いて3ヶ月が経ち、同じweb業界でも職場環境が大きく違うことを実感しています。 そこで大企業とベンチャー企業の2社で働いた私から見た、それぞれの企業の特徴をまとめてみました! web業界での就職を考えている方や、現在就活中の方の参考になれば幸いです! 大企業編 専門分野に特化して学べる 前職では効率的に業務を進めるため、部署や業務が細分化されていました。そのおかげで、自分が担当している業務や技術に集中して学ぶことができ、業務に対する深い知識やスキルを身につけることができたと思います。 また、部署内にその分野のスペシャリストがいて普段から接することにより、スキルを高めやすい環境であることもよかったと思います。 大規模サービスの裏側を知ることができる 多くの人が利用しているサービスや、トラフィックが膨大なサービスをどのように運用して支えているのかを知り、携わることができるのはすごく良い経験となりました。 また大規模サービスには、それを支える素晴らしいフレームワークやノウハウが存在し、それらを学ぶことができるのは大企業ならではのことです。 知名度がある やはり知名度があることは大企業の魅力の1つです。 私の場合、インターネット業界に疎い親族でも会社を知っているということが、大企業に就職を決めた理由の1つでもありました。 やはり親元を離れて就職する方には、大企業の場合の方が両親の理解も得られやすいと思います。 また社内のエンジニアの方から、大企業は社会的信用の高さから、ローン、クレジットカードの審査や賃貸契約も通りやすいとも伺いました。 ベンチャー企業が信用度が低いというわけではないですが、やはり知名度のある大企業の方が審査が通りやすいそうです。 大きなプロジェクトに関わることができる ビジネスの規模が大きく、世間に影響力のあるプロジェクトに参加することができたことは、前職で良い経験になったと思っています。 大きなプロジェクトに参加することで、職種の違う様々な部署の人達とどうやってプロジェクトを進めるのかを知ることができますし、異職種の人々と連携する力がつきます。 ベンチャー編 スピードが速い 転職して感じた大企業との一番の違いはスピード感です。 前職の場合、仕方ないことだとは思うのですが、他部署の処理待ち時間が存在してしまい、ストレスを感じることが多々ありました。 その点ベンチャー企業は社員数が多くなく役職がフラットなので、他部署の待ち時間はあまり発生せず自分のペースで開発が行えます。 また意思決定のスピードも早く、事業の方向性や戦略が変わることもあり、変化への対応力も身につきます。 役員との距離が近い VASILYでは同じフロアに全社員が在籍しているので、必然と役員との距離も近くなります。 したがって前職に比べ、圧倒的に企業の方向性や役員の考えが伝わってきやすいです。 私の場合、隣の隣がCTOの席なのですが、やはり尊敬するCTOが近くにいると、開発のやる気が出て身が引き締まりますね! 勤務時間が柔軟 会社によるかもしれませんが、大企業の場合、始業時刻が決まっていることが多いです。 VASILYでは始業時刻が厳密には決まっておらず、個人が一番パフォーマンスを発揮出来る時間帯に働くことができます。 例えば、子育てのために、朝早くに出社して夕方早めに退社する人もいれば、昼過ぎに出勤して深夜まで勤務するエンジニアもいます。 通勤時間が長い私にとっては、満員電車を避けて通勤できることがすごく嬉しいです。 新しい技術に意欲的 前職では依存関係や制約により、新しい技術を取り入れたり挑戦することが難しい環境でした。 そして新しい技術に対して意欲的な人が周囲に少なかったように思います。 ベンチャー企業の方が、新しい技術に対して意欲的です。 またVASILYでは、会社の社風として技術的挑戦を挙げているので、社内エンジニアが日々新しい技術に貪欲です。 週に一度、社内のエンジニアで集まって、興味を持った技術の共有を行う会があるのですが、どこから見つけてくるんだろうと思うくらいたくさんの情報が共有され、知見がたまります。 まとめ web業界の大企業とベンチャー企業、全ての企業が同様の特徴を持っているわけではありません。 しかし、私が働いた経験が参考になればと思い今回の記事を書かせていただきました。 私の場合、大企業で安定して働くのではなく、スピード感ある会社でエンジニアとして多くを学び成長したい、と思い転職を決めました。 また、VASILYを志望したのは、 ”HIPSTER” という会社独自の理念に共感したからです。 今回は大きく大企業とベンチャー企業と分けましたが、やはり1社1社環境が違うので、納得のいくまで企業や人に会って会社理解して決断することが大事だと思います。 そして自分の納得のいく就活をしてください!!! そして現在、VASILYではiQONを一緒にガッツリ開発してくれるエンジニアを募集しています! ご興味のある方は以下のリンクからご応募ください。
アバター
Androidエンジニアのnissiyです。 iQONではアプリの収益化のために、ページによってネイティブ広告や動画広告の掲載を行っています。 先日アプリのアップデートで動画広告に関してのアップデートを行いましたが、案件の都合で広告再生の仕組みを外部アドネットワークのSDKを使用せずに自作することにしました。 しかし、実装に多くの時間を割くことができなかったため、Androidの標準の仕組みをベースにポイントを押さえながら最短で実装を行いました。 実装した動画広告は以下のようなインタースティシャルの動画広告になります。 アプリのOSバージョンと導入スピードからVideoViewを選択 現在Androidには動画や音楽を提供するための仕組みがいくつか存在します。 代表的なものだと、APIレベル1から存在する標準の仕組みである MediaPlayer 、2014年に登場したGoogle製の外部ライブラリである ExoPlayer があります。 本当は新しい仕組みであるExoPlayerを使用して実装したかったのですが、ExoPlayerは内部で MediaCodec を使用しているのでAPIレベル16以上でないと動作しないため、ExoPlayerの導入を諦めました(iQONはminSdkVersionが15で、APIレベル15の端末のユーザーが多くいる)。 そこでMediaPlayerを利用することに決めましたが、導入スピードを考えてMediaPlayerとSurfaceViewを内包して作られている VideoView を使用することにしました。 VideoViewはAPIレベル1から存在する標準の仕組みであり、レイアウトファイルに記述すれば最小で以下のコードだけで動画を再生することができます。 ネットワーク上の動画を再生する場合 VideoView videoView = (VideoView) findViewById(R.id.video_view); videoView.setVideoURI(Uri.parse(<動画ファイルのURL>)); videoView.start(); ローカルの動画を再生する場合 VideoView videoView = (VideoView) findViewById(R.id.video_view); videoView.setVideoPath(<動画ファイルのパス>); videoView.start(); 動画広告が再生されても、他のメディアプレイヤーの再生を止めないようにする VideoViewをそのまま使用する場合、Google Play Musicなどの音楽プレイヤーを使用している状態で動画広告の再生をスタートしてしまうと音楽プレイヤーの再生が止まってしまいます。 動画がメインのアプリの場合は全く問題ありませんが、動画広告や無音動画しか再生されないアプリの場合にこの挙動は非常に不便です。 なぜこのようなことが起こるかというと、VideoView内で明示的に他のプレイヤーの音を止める処理が書かれているからです。 // VideoView.java private void openVideo() { if (mUri == null || mSurfaceHolder == null ) { // not ready for playback just yet, will try again later return ; } // we shouldn't clear the target state, because somebody might have // called start() previously release( false ); // この2行が他のプレイヤーの音を止める処理 AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); am.requestAudioFocus( null , AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); // 以下省略... この2行を無効にするために今回は こちらのGist を参考にVideoViewをカスタマイズしました。 カスタマイズしたViewを使用することで、動画広告が再生されても他のプレイヤーの音が止まることは無くなりました。 VideoViewをラップしたCustomViewで広告画面を実装 透過したマスクをかけた上にインタースティシャルの動画広告を再生させるために画面全体をCustomViewで実装しました。 レイアウトは以下のような構成になります。 <jp . vasily . iqon . ui . InterstitialMovieLayout xmlns : android = "http://schemas.android.com/apk/res/android" android : id = "@+id/interstitial_movie_layout" android : layout_width = "match_parent" android : layout_height = "match_parent" android : background = "#b2000000" android : clickable = "true" android : paddingLeft = "16dp" android : paddingRight = "16dp" android : visibility = "gone" > <jp . vasily . iqon . ui . CustomVideoView android : id = "@+id/video_view" android : layout_width = "match_parent" android : layout_height = "match_parent" android : layout_gravity = "center" /> </jp . vasily . iqon . ui . InterstitialMovieLayout> 最初は広告画面全体を別Activityとして実装していましたが、VideoView内のSurfaceViewが厄介で広告の表示・非表示の制御が上手くいきませんでした。 そのためCustomViewで全体を覆うようにして、Visibilityの変更をトリガーに広告画面の表示・非表示と、動画の再生・停止を制御するようにしました。 最後に 動画広告を再生するための仕組みを紹介しましたが、広告は案件次第で使うAPIやLibraryに制限が出る場合があるため、今回のVideoViewを使った実装はあくまで一例に過ぎないことを覚えておいてください。 また、今回の内容は動画広告だけでなくライトな動画を再生するためにも有効な方法だと思いますので、別の用途でも参考にしていただければと思います。 VASILYではiQONを一緒に開発してくれるAndroidエンジニアを募集しています。興味がある方は以下のリンクをご覧ください。
アバター
こんにちは、VASILYバックエンドエンジニアの塩崎です。 今回はApache Solr(以下、Solr)で商品検索のサジェスターを作ったので、それを紹介します。 サジェスターを作るにあたり、どのようにスキーマやサーチコンポーネントを定義すれば良いのかを説明します。 なお、この記事はsolr 4.10.4を対象にした記事です。 それ以外のバージョンでは設定項目が変わってくる場合があります。 サジェスターとは サジェスターとは、ユーザーが検索用のフォームに単語を入力している途中に、その入力途中の単語を補完する機能です。 例えば、Google検索でサジェスターについて調べようとした時に、「さじぇ」と入力した時点で以下のように「さじぇ」に続く単語が候補として現れます。 このような機能を実装することによって、ユーザーがテキストを入力する手間が省けたり、入力間違いをした単語で検索をしてしまうことを防げたりする効果があります。 日本語のサジェスターの難しいところ サジェスターの基本動作は、検索対象のドキュメント中の単語リストを保持し、ユーザーの入力途中の単語で前方一致検索をかけることです。 しかし、日本語に対応したサジェスターを作る場合は、英語などの他の言語にはない、日本語特有の難しさがあります。 主な課題は「単語の抽出」と「漢字の読み」です。 単語の抽出 英語の文章は単語と単語との間がスペースや記号などで区切られています。 そのため、文章を単語に分解する処理を簡単に書くことができます。 しかし他方で日本語の文章にはそのような機械的に単語分割できるような目印はありません。 そのため、日本語の文章を単語分割するためには形態素解析という処理を行う必要があります。 Solrにはkuromojiという形態素解析エンジンが組み込まれているため、これを利用して文章を単語に分割します。 漢字の読み さらに、単語に分割した後には「漢字の読み」の問題もあります。 例えば、「とう」と入力した時に「東京」という単語を返すためには、「東京」という単語の読みが「とうきょう」であるという情報が必要です。 kuromojiで形態素解析を行うと単語の読みの情報も取得できるために、これを利用します。 また、未確定の子音やひらがなにも対応する必要があります。 「とうky」と入力した時には、「東京」や「東急」といった単語を候補として表示する必要があります。 これらのことを解決するために検索対象のドキュメント及び、ユーザーが入力中のテキストをローマ字読みに変換した後に前方一致検索をかける必要があります。 それと同時にローマ字読みに変換せずに前方一致検索をかける必要もあります。 これは「東」というテキストを入力した時に「東京」を候補として表示するためです。 「東」のローマ字読みは「higashi」なので、「tokyo」に前方一致しないからです。 日本語に対応したサジェスターの処理フロー 上記のような点を考慮してサジェスターを作る場合は次のような処理が必要です。 検索対象のドキュメントを単語単位に分割し単語リストを作る。 その時には各々の単語に対して、その単語のローマ字での読みを併せて保持する。 ユーザーがテキストを入力する毎に入力されたテキストをローマ字読みに変換し、上記のリストに対して前方一致検索を行う。 同時に、ローマ字読みに変換せずに前方一致検索も行う。 Solrの設定ファイル さて、サジェスターの作り方が分かりましたので、それをSolrの設定ファイルに落とし込みます。 schema.xml まずは、スキーマ定義です。 ここではtext_ja_for_suggestとtext_ja_romajiという2つの型を定義しています。 text_ja_for_suggestはサジェスター用に文章を単語に分割するための型です。 検索対象のドキュメントに対して、以下に定義されるanalyzerを通すことでサジェスト用の単語リストを作ります。 検索対象のドキュメントの英数字やカタカナの全角半角の表記揺れをICUNormalizer2CharFilterFactoryで統一した後に、JapaneseTokenizerFactoryで形態素解析を行っています。 検索用にこのtokenizerを使用するときには mode=search で使用することが多いですが、この設定をサジェストで使ってしまうと、単語が細かくなりすぎてしまうために mode=normal で使用しています。 分割後の単語に対しては、単語単位での表記揺れの統一や、不要な単語のフィルタリングなどを行っています。 <fieldType name = "text_ja_for_suggest" class = "solr.TextField" positionIncrementGap = "100" autoGeneratePhraseQueries = "false" > <analyzer> <!-- ICU正規化(NFKC) --> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <!-- 形態素解析(normal mode) --> <tokenizer class = "solr.JapaneseTokenizerFactory" mode = "normal" /> <!-- 品詞によるフィルタリング --> <filter class = "solr.JapanesePartOfSpeechStopFilterFactory" tags = "lang/stoptags_ja_for_suggest.txt" /> <!-- 同義語 --> <filter class = "solr.SynonymFilterFactory" synonyms = "synonyms_for_suggest.txt" ignoreCase = "true" expand = "true" /> <!-- カタカナ長音の正規化 --> <filter class = "solr.JapaneseKatakanaStemFilterFactory" minimumLength = "4" /> <!-- アルファベットを小文字に正規化 --> <filter class = "solr.LowerCaseFilterFactory" /> <!-- 単語によるフィルタリング --> <filter class = "solr.StopFilterFactory" ignoreCase = "true" words = "lang/stopwords_ja_for_suggest.txt" /> </analyzer> </fieldType> text_ja_romajiは単語をローマ字読みに変換するための型です。 上の型で分割された単語のローマ字読み変換に使用するのに加えて、ユーザーが入力途中のテキストをローマ字読み変換するためにも使用します。 前半部分はtext_ja_for_suggestと同じです。 JapaneseReadingFormFilterFactoryを mode=romaji で使用することによって、単語のローマ字読みを取得します。 ShingleFilterFactoryはユーザーが入力中のテキストが複数単語に分割された時に、それらを結合してから前方一致検索をするために使用しています。 <fieldType name = "text_ja_romaji" class = "solr.TextField" positionIncrementGap = "100" autoGeneratePhraseQueries = "false" > <analyzer> <!-- ICU正規化(NFKC) --> <charFilter class = "solr.ICUNormalizer2CharFilterFactory" name = "nfkc" /> <!--形態素解析(normal mode) --> <tokenizer class = "solr.JapaneseTokenizerFactory" mode = "normal" /> <!-- ローマ字の読みに変換 --> <filter class = "solr.JapaneseReadingFormFilterFactory" useRomaji = "true" /> <!-- トークンNGramの生成 --> <filter class = "solr.ShingleFilterFactory" minShingleSize = "2" maxShingleSize = "99" outputUnigrams = "false" outputUnigramsIfNoShingles = "true" tokenSeparator = "" /> <!-- アルファベットを小文字に正規化 --> <filter class = "solr.LowerCaseFilterFactory" /> </analyzer> </fieldType> そして、サジェスト用の単語が格納されるフィールドである、suggestフィールドを作ります。 copyFieldを使い、商品名が入るフィールドであるtitleフィールドの内容をsuggestフィールドにコピーしています。 <fields> <field name = "suggest" type = "text_ja_for_suggest" indexed = "true" stored = "true" /> </fields> <copyField source = "title" dest = "suggest" /> solrconfig.xml 上記のschema.xmlでフィールド及び型の定義が完了したので、次に、それらを使って検索を行うためのモジュールの設定を書きます。 Solrでは検索を行うためのモジュールはSearchComponentと呼ばれています。 ここでは2つのSearchComponentを併用しています。 SpellCheckComponent 1つ目のSearchComponentがSpellCheckComponentです。 このSearchComponentはローマ字読みに変換した後の単語に対して前方一致検索を行い、その結果を返すSearchComponentです。 suggestAnalyzerFieldTypeとqueryAnalyzerFieldTypeにtext_ja_romajiを指定することでローマ字読みに変換することを指定しています。 <searchComponent class = "solr.SpellCheckComponent" name = "suggest_ja" > <lst name = "spellchecker" > <str name = "name" > suggest_ja </str> <str name = "classname" > org.apache.solr.spelling.suggest.Suggester </str> <str name = "lookupImpl" > org.apache.solr.spelling.suggest.fst.AnalyzingLookupFactory </str> <str name = "storeDir" > suggest_ja </str> <str name = "buildOnStartup" > true </str> <str name = "buildOnCommit" > true </str> <str name = "comparatorClass" > freq </str> <str name = "field" > suggest </str> <str name = "suggestAnalyzerFieldType" > text_ja_romaji </str> <bool name = "exactMatchFirst" > true </bool> </lst> <str name = "queryAnalyzerFieldType" > text_ja_romaji </str> </searchComponent> TermsComponent もう1つのSearchConponentはTermsComponentです。 このSearchComponentはローマ字読みに変換せずに、単純な前方一致検索を行います。 <searchComponent name = "terms" class = "solr.TermsComponent" /> requestHandler 上で定義した2つのSearchComponentをrequestHandlerに登録して、HTTPで叩けるようにします。 サジェスターはユーザーが1文字入力する毎に呼び出されるため、高速に応答する必要があります。 そのため、1回のHTTPリクエストで2つのSearchComponentに対して同時に処理を投げられるように、1つのrequestHanderに2つのSearchComponentをひも付けます。 <requestHandler name = "/suggest_ja" class = "org.apache.solr.handler.component.SearchHandler" startup = "lazy" > <lst name = "defaults" > <str name = "spellcheck" > true </str> <str name = "spellcheck.dictionary" > suggest_ja </str> <str name = "spellcheck.collate" > false </str> <str name = "spellcheck.count" > 10 </str> <str name = "spellcheck.onlyMorePopular" > true </str> <bool name = "terms" > true </bool> <bool name = "terms.distrib" > false </bool> <str name = "terms.fl" > suggest </str> </lst> <arr name = "components" > <str> suggest_ja </str> <str> terms </str> </arr> </requestHandler> 未確定のテキストに対応する 上記設定をそのまま使うと、日本語入力の未確定テキストに対しては正しく結果を返さないことがあります。 text_ja_romaji型は一部の単語に対しては正しくローマ字の読みを返すことができないからです。 例えば、ユーザーが「きゃみ」と入力したときには、「kiゃみ」という結果になってしまいます。 これは「きゃみ」という単語が形態素解析エンジンの辞書にないために起こる現象です。 先ほどschame.xmlで定義したCharFilterをさらに追加して解決してもいいですが、アプリケーション側で行う方が簡単でしたので、一部のテキストに対してはSolrにリクエストを投げる前にアプリケーション側でローマ字変換を行いました。 以下の正規表現にマッチするテキストの場合は形態素解析をせずにローマ字読みに変換することが可能ですので、アプリケーション側でローマ字変換を行います。 ローマ字変換にはromajiというgemを使いました。 require ' romaji ' if word =~ /^( \p{InHiragana} | \p{InKatakana} )+[ a-zA-Za-zA-Z ]{0,3}$/ word = Romaji .kana2romaji(word) end # きゃみ -> kyami # 靴 -> 靴 まとめ Solrを使って日本語に対応したサジェスターを作ることができました。 日本語特有の問題である、単語抽出と漢字の読みの問題も解決し、入力途中のテキストに対してもサジェストを行うことができるようになりました。 参考 Apache Solr Apache Solrの公式サイトです。 http://lucene.apache.org/solr/ kuromoji Javaで形態素解析を行うためのライブラリです。 http://www.atilika.org/ [改訂新版] Apache Solr入門 ~オープンソース全文検索エンジン Solrを扱うならば必読書です。 この記事で紹介したサジェスターの基礎的な部分はこの本を参考にしました。 https://www.amazon.co.jp/dp/4774161632 romaji rubyでひらがな→ローマ字変換を行うgemです。 https://github.com/makimoto/romaji 最後に VASILYではiQONを一緒に作っていく仲間を募集中です。 興味のある方は以下のリンクをご覧ください。
アバター
2016年8月9日、スタートアップで活躍したいエンジニア向けのトークイベント、CAREER TALK for Engineerを開催しました。 イベント風景 MERY を運営する株式会社peroli様の開発部長 水島様と、弊社CTOの今村が、大手とベンチャーを比較した自身の考えを、経験に基づいて話す場がありました。大手とベンチャー両方を経験している両者だからこそ、「大手の安定」や「ベンチャーの不安定」などについて本音を皆さんに伝えられたのではないかと思います。 また、両者の若手エンジニアが「ベンチャーで働くメリット/デメリット」など若手視点から話すセッションがありました。 新卒でベンチャー企業を選ぶ人の考え方を表に出す機会はあまり無いため、若手の考え方が伝えられる珍しい場になったのではないかと思います。 おわりに ご参加して下さった皆様、ありがとうございました。 VASILYでは引き続きエンジニアに関するイベントを実施していきます。 今回はベンチャーについてお伝えするイベントを実施しましたが、次回は技術満載なイベント「Fashion Tech Meetup Vol.3」を開催する予定です。 3回目の開催となるFashion Tech Meetupですが、今回もFashion x Technologyに関する会社が それぞれの技術をお伝えします。前回までの内容は下記からご確認いただけますので、 興味を持たれた方は是非Fashion Tech Meetup Vol.3にご参加ください。 fashion-tech.connpass.com fashion-tech.connpass.com
アバター