The post 最大内積探索(MIPS)のライブラリを公開しました first appeared on カメリオ開発者ブログ.
]]>最大内積探索問題(Maximum Inner Product Search, 以下MIPS)ってご存知でしょうか?データベースに登録された多くのアイテムのベクトルのうち、クエリのベクトルとの内積を最大化するアイテムを探す問題です。行列分解を用いてユーザにアイテムをレコメンドするときなど、この探索が問題になってくることがあります。
MIPSを解く単純な方法は、与えられたクエリに対してデータベースのすべてのアイテムとの内積を計算することですが、これにはアイテム数に比例した時間がかかります。そのため、アイテムが何千万や何億といった数にのぼると、ベストなアイテムを返す処理を遅延なくリアルタイムで行うのは厳しくなってしまいます。
これを解決するために、局所性鋭敏型ハッシュ(Locality Sensitive Hashing, 以下LSH)という手法をもとにしたアルゴリズムが提案されてきました。LSHは実ベクトルから小さい次元数の2値ベクトル(ハッシュ値)を計算する手法で、類似したアイテムからは同じハッシュ値が得られやすいという特徴があります。ALSHやSimple-LSHなどの手法では、入力するベクトルを変換することで、コサイン類似度のためのLSHを用いて内積を最大化するアイテムの近似探索を高速に行えることが示されました。
しかし実際にこれらの手法を使ってみると、アイテムに対して出力されるハッシュ値に偏りがあるなどして、期待する性能が得られないことがあります。もともとのLSHでも同様の問題があり、これを解決するためにLearning to Hashと総称されるハッシュ関数を学習する手法が提案されてきました。ITQ(ITerative Quantization)もそのうちの手法のひとつで、データベースのアイテムからハッシュ関数を学習して、より上手にハッシュ値を計算できるようにします。
そこで、ALSHやSimple-LSHとITQを組み合わせることで、MIPSに対してもより良いハッシュ値の計算を行い、近似探索の性能を向上させることができるのではないかと考えたのが、今回の本題であるライブラリのアルゴリズムです。SITQと名付けたこのアルゴリズムでは、Simple-LSHと同様の方法でアイテムやクエリのベクトルを変換し、その上でITQを用いてハッシュ関数を最適化します。探索では、クエリのハッシュ値と似ているハッシュ値を持つアイテムを取ってきて、そのなかで実際に内積を計算して最も大きい値となったアイテムを取得します。実際に内積を計算するアイテム数はデータベースのすべてのアイテムに比べて遥かに少ないので、探索にかかる時間を短くすることが期待できます。
このアルゴリズムをPythonのライブラリとして公開しました。pip install sitq
でインストールして使うことができます。ハッシュ関数の学習およびハッシュ値の計算を行うSitq
と、内積を最大化するアイテムの近似探索を行うMips
のふたつのクラスがあります。
Sitq
では次のようにしてハッシュ値を計算することができます。
import numpy as np
from sitq import Sitq
# サンプルデータを作成
items = np.random.rand(10000, 50)
query = np.random.rand(50)
sitq = Sitq(signature_size=8)
# ハッシュ関数を学習
sitq.fit(items)
# アイテムのハッシュ値を取得
item_sigs = sitq.get_item_signatures(items)
# クエリのハッシュ値を取得
query_sig = sitq.get_query_signatures([query])[0]
Mips
では次のようにしてクエリに対してアイテムを探索することができます。
import numpy as np
from sitq import Mips
# サンプルデータを作成
items = np.random.rand(10000, 50)
query = np.random.rand(50)
mips = Mips(signature_size=8)
# アイテムを登録
mips.fit(items)
# クエリに対して内積を最大化しそうなアイテムを探索
item_indexes, scores = mips.search(query, limit=10, distance=1)
ベンチマークでは、評価した他の近似探索アルゴリズムと比べてハッシュ値の偏りが小さく精度も高いという結果が得られました。実験結果はGitHubリポジトリに載せています。
また、MovieLens 20M Datasetデータセットに対してレコメンドを行うKaggle Kernelも作りました。レコメンドにはALSで学習したベクトルを使い、ブルートフォースとSITQを用いた場合の性能を出しています。
The post 最大内積探索(MIPS)のライブラリを公開しました first appeared on カメリオ開発者ブログ.
]]>The post 「推薦」の定式化から推薦システムを理解する first appeared on カメリオ開発者ブログ.
]]>近年の多くの推薦システムでは,機械学習手法を用いて,ユーザの行動履歴から推薦モデルを構築するようになってきています.これにより,単にデータを集計して人気のアイテムを出すようなアプローチと比べて,よりユーザの趣向に合ったアイテムを推薦することが可能になります.ここでは,そのような推薦モデルの構築(学習)方法について,簡単に紹介したいと思います.
この記事では,ユーザの行動履歴をフィードバックと呼びます.一言にユーザからのフィードバックと言っても様々な種類があります.例えば,ユーザがアイテムに対して点数(rating)をつけたものや,like・dislikeのようにそのアイテムを好きか嫌いかを表すものがあります.このタイプは,ユーザがそのアイテムを好きかどうかがはっきりとわかるので,明示的フィードバック(explicit feedback)と呼ばれます.一方で,単なる閲覧・購入履歴などのデータも存在します.このデータからは,閲覧したけど好きだったかどうかはわかりませんし,購入したけど満足したかどうかはわかりません.したがって,このタイプは暗黙的フィードバック(implicit feedback)と呼ばれます.大きく分けて,この二つのタイプのフィードバックを元に推薦モデルを構築するわけですが,この二つでは,学習手法が異なってきます.
学習方法に入る前に,「推薦」という問題の定式化をしていきます.\(m\)個のアイテム,\(n\)人のユーザを考え,アイテムを\(i \in \mathcal{I} = \{1, \ldots, m\}\),ユーザを\(u \in \mathcal{U} = \{1, \ldots, n\}\)とします.そして,推薦モデルへの入力を\((i, u)\),出力を\(y \in \mathbb{R}\)とします.最後に,推薦モデルを\(\hat{y}: \mathcal{I} \times \mathcal{U} \rightarrow \mathbb{R}\)とします.こうすると,ユーザ\(u\)への推薦は,\(\hat{y}(i, u)\)の高い順にアイテム\(i\)を推薦すれば良いことになります.よって,ここでの目的はフィードバックから\(\hat{y}\)を学習することになります.
ユーザから与えられるフィードバックを\((i, u, y, \alpha)\)とし,その集合を\(S\)とします.ここで,ユーザ\(u\)がアイテム\(i\)を(どのくらい)好きか(どのくらい)嫌いかというのを\(y\)で表していると考えます.\(\alpha\)は,そのフィードバックの強さを表す重みです.例えば,新しいフィードバックが古いフィードバックより重要な場合は,前者の重みを後者の重みに比べて大きくしたりします.もちろん,このような設定を考えない場合は全ての\((i, u, y, \alpha) \in S\)について\(\alpha=1\)としても構いません.このフィードバックを用いて\(\hat{y}\)を学習します.ここで,どんな最適化問題を定義して,どのように解くかは自由ですが,この記事ではl2正則化最小二乗学習の枠組みで考えたいと思います.
明示的フィードバックでは,二乗誤差に正則化項を加えた以下のような最適化問題を考えます.
$$
\begin{aligned}
\min_{\mathbf{\Theta}} \ \sum_{(i, u, y, \alpha) \in S} \alpha \left(y – \hat{y}(i, u; \mathbf{\Theta})\right)^2 + \sum_{\theta \in \mathbf{\Theta}} \lambda_{\theta} \theta^2
\end{aligned}
$$
ただし,\(\lambda_{\theta}\)は\(\theta\)に対する正則化パラメータです.さらに,\(\mathbf{\Theta}\)は,\(\hat{y}\)がもつパラメータ集合で,これを学習することになります.この最適化問題は,通常の勾配法に加えて,\(\hat{y}\)が多重線型性をもつ場合はより効率的に解を見つける方法が提案されています.多重線型性とは,いくつかのパラメータを固定すると,注目しているパラメータに対しては線形になるような性質を指します.
一方で,暗黙的フィードバックではもう少し工夫が必要です.先ほど,\(y\)はユーザ\(u\)がアイテム\(i\)を(どのくらい)好きかを表していると考えましたが,暗黙的フィードバックではこれは不確かなものです.よって,\(y\)に対して,何らかの仮定が必要になってきます.最も単純な仮定として,「フィードバックは全てポジティブなものである」というのがあり得るでしょう.例えば,「閲覧したから興味があるのだろう」と言った具合です.これは,ポジティブな場合は\(y\)を1と,ネガティブな場合は\(y\)を0とし,全ての\((i, u, y, \alpha) \in S\)に対して\(y=1\)とすることで表現できます.しかし,明示的フィードバックの時のような最適化問題を考えてしまうと,全ての\((i, u)\)に対して\(1\)を出力するような\(\hat{y}\)が最適なものとなってしまいます.そこで,\(S\)に含まれないアイテム・ユーザの組み合わせについては弱い重み\(\alpha_0\)で\(y=0\)とすることにし,以下のような最適化問題を考えます.
$$
\begin{aligned}
\min_{\mathbf{\Theta}} \ \sum_{(i, u, y, \alpha) \in S} \alpha \left(y – \hat{y}(i, u; \mathbf{\Theta})\right)^2 + \alpha_0 \sum_{(i, u) \in S_0} \hat{y}(i, u; \mathbf{\Theta})^2 + \sum_{\theta \in \mathbf{\Theta}} \lambda_{\theta} \theta^2
\end{aligned}
$$
ただし,\(S_0\)は\(S\)に含まれていないアイテム・ユーザペアで,以下のように定義しました.
$$
\begin{aligned}
S_0 = (\mathcal{I} \times \mathcal{U}) \setminus \left\{(i, u) \vert (i, u, y, \alpha) \in S\right\}
\end{aligned}
$$
以上のような最適化問題を解くことで,明示的・暗黙的フィードバックから推薦モデル\(\hat{y}\)を学習することになります.この段階では\(\hat{y}\)の形については一切触れていません.様々な\(\hat{y}\)を考えることができ,それに対して様々な最適化アルゴリズムが提案されています.次回以降,余力があればそれらについてもいくつか紹介したいと思います.
読んでいただきありがとうございました.
The post 「推薦」の定式化から推薦システムを理解する first appeared on カメリオ開発者ブログ.
]]>The post Go言語で書かれたダブル配列ライブラリを公開します first appeared on カメリオ開発者ブログ.
]]>この度、Go言語で実装されたダブル配列ライブラリを公開しましたのでお知らせします。
https://github.com/shiroyagicorp/double_array
ある文字列があらかじめ定義した集合に含まれているかどうか判定するときに使います。
Trieの効率的な実装方法です。
詳細は https://www.slideshare.net/s5yata/dsirnlp-04s5yata によくまとめられています。
私の趣味です。
…というのは半分冗談ですが、本当の所はGoで書かれたtrie実装でプロダクションで使えそうなものを見つけられませんでした。
(「ここにあるよ!」というのをご存知の方がいらっしゃいましたら教えて下さい。)
いくつかのリポジトリを拝見しましたがリポジトリによっては数年放置されていたり、
起動しなかったり、実装の基礎(だと私が思っているもの)が押さえられていなかったりしていました。
今回の実装は「静的なTrieをGoで扱いたいんだけど…(でも自前実装したくない)」という需要に応えるものです。
中身はごくごく普通のダブル配列実装です。特に凝ったことや難しいこと、新しい手法の開発などはしていません。
とはいえ他の実装とどう違うのか確認しておきましょう。
比較対象は godarts です。
(https://github.com/awsong/go-darts の方が実装の筋は良いような気がしましたが使い方を間違えているのか起動しませんでした)
実験には全国に存在する法人のうち、5文字以上の法人名の集合を使いました。
このデータセットには 2,109,909種類の法人名が含まれています。
このデータセットでビルドすると、ダブル配列の配列長は以下のようになります。
go-darts: 12,516,574
弊社実装: 6,803,126
約半分です。
ただし構築時間はgo-dartsの方が速いです。
go-darts: 19秒
弊社実装: 24秒
exampleディレクトリに解説つきの例を入れていますのでご覧ください。
弊社のユースケースでは不要だったため未実装です。ごめんなさい。
多くの高速なビルド法は256分岐trieを想定しており文字ベースtrieでの実装は結構大変です。
また、業務上、数十秒でビルドできるならそれで十分でした。
未対応です。
PRお待ちしております。
The post Go言語で書かれたダブル配列ライブラリを公開します first appeared on カメリオ開発者ブログ.
]]>The post try! Swift Tokyo 2017とその後 first appeared on カメリオ開発者ブログ.
]]>前回サーバーサイドSwiftでの記事を書いてから、しばらくぶりになりましたが、近況を簡単に報告することができました。
Swiftのオープンソース化から1年半ほどたちましたが、状況も様々変わりました。
スライドはこちらです。
ビデオもRealmのサイトでのちほど公開される予定です。
スライドで触れたライブラリなどは以下で公開しています。
また、カンファレンス当日は、多くの方にオフィスアワーに来ていただきましたが、
ラズベリーパイ上でのSwiftの反響が予想よりも大きかったように思います。
このSwiftは現在は自分でビルドするか、以下のようなビルドされたものを使うことができます。
また、私の作っているI2CをSwiftから扱うライブラリは以下にありますので、試してみてください。
最近は湿度センサー(SHT21)のモジュールを追加しました。
近日、ラズベリーパイZero WiFi付きモデルが安価に出るということで、そちらも楽しみです。
Q/Aなどを頂きました、サーバーサイドSwiftのHTTPクライアントについて、私たちはlibcurlをベースに使っています。
今はFoundationのURLSessionも実装がある程度されていますが、まだmacOSと同じというようではないようです。
そこで、社内で使っているlibcurlベースのHTTPクライアントを公開したいと思います。
リポジトリはこちら shiroyagicorp/swift-seeurl
libcurl自体が同期I/Oになりますので、I/O処理はブロックします。(最長タイムアウト時間)
必要に応じてDispatch.asyncすることもできます。
それ以外にもHTTPを扱えるサーバーサイドのライブラリとしては、Prorsumがあります。
noppoMan/Prorsum
こちらはTokyo Server Side Swift Meetupの@noppoManさんが作成・メンテナンスされています。
4月(予定)にはサーバーサイドSwift Meetupを行いますので、ご興味のある方は見てみてください。
https://tokyo-ss-swift.connpass.com
過去のハッシュタグは#tsssmeetupです。
最終日のハッカソンでは、numsw(numpy for Swift) のチームに参加しました。
実はハッカソンが終わったあとも実装を進めていて、iPadやサーバーサイドでの活用を考えています。
最後に、発表の提案をくださった、@k_katsumiさん、
Natashaさん、運営・ボランティアの皆様、参加者の皆様、アドバイス頂いた方々、ハッカソンのチームメイトのみなさん、どうもありがとうございました。
今後もSwiftに関することをはじめ、技術的なトピックをこちらのブログで発信していきたいと思います。
The post try! Swift Tokyo 2017とその後 first appeared on カメリオ開発者ブログ.
]]>The post PyCon JP 2016 に参加してきました #pyconjp first appeared on カメリオ開発者ブログ.
]]>今週の中日 (2016-09-20から09-24) から PyCon JP 2016 が開催されました。白ヤギコーポレーションは例年スポンサーをしていますが、「シルバースポンサーではうちの会社が目立たないだろう」的なノリで今年は初めてゴールドスポンサーとして参加しました。普段なら私は個人としてチケットを購入してカンファレンスに参加していましたが、ゴールドスポンサーは招待チケット枠が3つあったこともあり、スポンサーチケットを使って参加してきました。
今年のテーマは Everyone’s different, all are wonderful.「みんなちがって、みんないい」 でした。早稲田大学 さんの施設で行われ、正にテーマを表した5トラックという、例年よりも発表枠を拡大し幅広い分野における Python を使った事例や開発について様々な発表が行われていたように思います。
ゴールドスポンサープランの1つに ジョブフェア への出展があります。白ヤギはパネルディスカッションのパネラーの1社として登壇することになりました。今年のパネルディスカッションのテーマは 『エンジニアが語る サービス・プロダクトとの関わり方』 でした。私はパネルディスカッションに初めて登壇したのでしどろもどろでしたが、カメリオ API というサービスの開発を1年以上やってきたことの経験や、前職の組織や開発体制、働き方といった経験も踏まえて答えたりしていました。
Hunza様、いい生活様、HDE様、白ヤギコーポレーション様、ブレインパッド様によるジョブフェア開催中ですよ#pyconjp pic.twitter.com/RUaeXVNvHH
— PyCon JP 2016 (@PyConJ) September 22, 2016
パネルディスカッションの開始前に顔合わせしてどんな話し合いにしようかと他企業のパネラーの方々とお話したときの話題の方が私にとっては記憶に残っています。事前にモデレーター・パネラー同士での顔合わせができたことで緊張感が解けて本番に挑むことができたようにも思います。もしかしたら本番で話していないこともあるかもしれませんが、話題に関して覚えている範囲でいくつかあげてみます。
リモートワークの賛否は両方の意見が出ておもしろかったと会場で聞いていた同僚が話していました。
これが正解という答えはなく、それぞれの企業の考え方、事業規模や提供しているビジネスやサービスの形態によっても求められる体制の在り方が変わってきます。パネルディスカッションを聞いて頂いた方それぞれの勤め先に近い企業の話が1つのモデルケースとして参考になったのではないかと思います。白ヤギもこれから組織や開発体制を増強していく段階なので、白ヤギよりもずっと成長されている他企業のパネラーの方々のお話を伺えてとても参考になりました。
今年も CFP を送ってみたものの、残念ながら私は落選しました。たまたまビギナーセッションで講師を募集していたようなのでそのセッションの1つを担当しました。
イチからコードを書けない人を対象とし、プログラミングの始め方としてどんな風にコードを書いていけばいいかを実際に見てもらうことを意図したライブコーディングでした。
1時間という時間があったものの、普段ライブコーディングなどはしないためか、コードを書いていると時間がどんどん過ぎてしまって用意した教材の1/3ぐらいしか消化できませんでした。反省点として、コードを書きながら話さないといけないので画面ばかりをみてしまうため、参加者の様子を伺う余裕がなく、うまく配慮できなかった点が多かったのではないかと思います。ある程度はサンプルコードは用意しておいた上でコードを直したり書き加えたりといったことをした方が効率的に説明できたのではないかなと、終わってみて感じました。とはいえ、イチからコードを書くこと自体はお見せできました。その様子が何かしらの参加者の参考になっていれば嬉しいなぁと思う限りです。
金子です。
今年は「Python を支える技術: モジュール・ インポートシステム編」というタイトルで発表しました。発表者の声が聞こえづらかったり、時間をオーバーしてしまうなど個人的に課題の残る発表でしたが、発表後には Python のインポートの仕組みが分かったという感想も頂けました。
発表内容は以下のリンク先を見てください。
再び森本です。
既に flickr にたくさん写真がアップされているのでいくつか紹介します。
すっかり PyCon JP に馴染んだ感のあるモノタロウ侍ですね。今年も多くの参加者と記念写真を撮っていました。
本日のSnacksでお配りします!
カラフルですー!#pyconjp pic.twitter.com/4bKY3VDL1j— PyCon JP 2016 (@PyConJ) September 22, 2016
おやつのカップケーキです。Python のロゴがついたものもあり、写真の撮りがいのあるおやつでした。
みんなで集合写真。
今年もとても楽しいイベントに参加できました。PyCon JP 2016 スタッフの方々、ありがとうございました!
PyCon JP座長交代です#pyconjp pic.twitter.com/a4z1aGqJ8F
— PyCon JP 2016 (@PyConJ) September 22, 2016
来年は座長を交代してあらたな体制で臨むようです。また来年も楽しみにしています!
The post PyCon JP 2016 に参加してきました #pyconjp first appeared on カメリオ開発者ブログ.
]]>The post Elasticsearch を 1.7.5 から 2.3.5 へ移行しました first appeared on カメリオ開発者ブログ.
]]>白ヤギではニュース記事のキュレーションをする カメリオ API というサービスを開発していますが、検索バックエンドとして Elasticsearch を使っています。
カメリオ API は約1年3ヶ月前に開発を始めたのですが、当時は 1.7 系を使っていました。昨年の夏から秋ごろにかけて 2.0.0 のベータ版がリリースされ、2015-10-28 に GA がリリースされました。現時点の最新バージョン (安定版) は 2.3.5 になります。
Elasticsearch 1.x と 2.x は非互換となるため、検索インデックスやアプリケーションの変更なしに移行することはできません。2.0.0 リリース後の翌日に早速 ITS にチケットを作りました。その後どうなったかというと、いまこの記事を書いていることから察して頂けると思いますが、そのときに作ったチケットの変遷を一部抜粋しながら眺めてみましょう。
長かった。。。
このチケットは実に約10ヶ月間にわたり生存していました。「elasticsearch 2.0 対応」から「elasticsearch 2.x 対応」にチケットタイトルを変更してからも3ヶ月を要しました。作業を始めようとして割り込みが入ることも何度もありました。結果的にはいろいろあって8月中旬ぐらいから移行作業に取り掛かっていました。実際に移行に要した開発/作業期間は2週間ほどでした。終わってみてから振り返ると、想定していたよりはトラブルなく簡単に移行できました。
何かの役に立つかもしれないので Elasticsearch 1.7.5 から 2.3.5 へ移行したときの変更/作業内容についてまとめておきます。あくまで当社での利用における差異なので全ての変更内容を網羅しているわけではありません。互換性のない変更については以下のドキュメントを参照してください。
白ヤギでは Go 言語で API サーバーを開発しています。Go 言語向けの Elasticsearch クライアントとして elastic というライブラリを使っています。以前にも Elasticsearch Advent Calendar 2015 向けに記事を書きました。
Elastic ライブラリは、Elasticsearch 1.x と 2.x 向けでそれぞれライブラリのバージョンが違います。そのため、まずやることはこのライブラリのバージョンを上げます。ちょっと紛らわしいですが、1.x が elastic.v2、2.x が elastic.v3 となっています。
- "gopkg.in/olivere/elastic.v2" + "gopkg.in/olivere/elastic.v3"
ちなみに静的型付き言語の利点として、こういった非互換のバージョンアップに対して型エラーを直していくことで影響範囲を把握するのが容易だという点があげられます。
Elastic ライブラリのソースコードには Elasticsearch のドキュメントへのリンクも記載されています。例えば elastic/search_queries_bool.go をみると、以下のように Bool Query ドキュメントへのリンクがあります。
// Copyright 2012-2015 Oliver Eilhard. All rights reserved. // Use of this source code is governed by a MIT-license. // See http://olivere.mit-license.org/license.txt for details. package elastic import "fmt" // A bool query matches documents matching boolean // combinations of other queries. // For more details, see: // http://www.elasticsearch.org/guide/reference/query-dsl/bool-query.html type BoolQuery struct { Query ...
実際の変更作業の過程としては、型エラーが発生したクエリのソースコードからドキュメントを辿り、ドキュメントの内容を確認しながら修正していきました。
おそらくこの変更に影響を受けるアプリケーションが多いのではないかと推測します。クエリ DSL で Filter していた処理を全て Query に書き換えないといけません。この変更に関するドキュメントは以下になります。
使っている Query の種類によって4つの移行方法が書かれています。白ヤギでは基本的に Constant Score Query で Filter を移行しました。Constant Score Query については Combining Filters のドキュメントの例が分かりやすいと思います。ドキュメントとしてはこちらの方がお勧めです。
当初、Bool Query に追加された filter 句を使って移行していました。しかし、Not Filter が Not Query に置き換わったものの、それも 2.1.0 で非推奨扱いとなっています。移行方法としては、Bool Query の must_not 句を使うように記載されていますが、ここで1つ落とし穴があります。
Bool Query の filter 句はスコアに影響を与えないのでそのまま移行できますが、must_not はスコアに影響が出てしまいます。そのため、Not Filter の代替として Bool Query の must_not 句を使うと、フィルターとしての機能は実現できますが、従来の Not Filter の検索結果と比較するとスコアに差異が生じます。Bool Query の filter 句にネストした Bool Query の must_not 句を定義してみたり、他にもいくつかの方法を試してみたりしたのですが、検索結果かスコアのどちらが意図したものにはなりませんでした。
この課題を解決する方法が Constant Score Query です。例えば、以下のようなクエリを生成することで従来と同様の機能とスコア算出ができました。
"filter": { "constant_score": { "filter": { "bool": { "must_not": { ... }, "filter": { ... } } } } }
あと1点 Filter から Query に変更された副作用として filter 句のみにマッチするとき、スコアの値がゼロで検索結果に現れるようになってしまいました。検索結果が少ないときにノイズとなってしまうため、min_score を 0.001 などと指定してスコアの値がゼロのものを取り除くようにしました。
Constant Score Query のもう一つの用途として名前の通りスコアを一意にすることもできます。例えば、以下のように Bool Query の must 句で ID にマッチするものを取得したい場合、そのときの取得されるスコアは指定した ID の数によってばらばらになります。
"bool": { "must": { "terms": { "id": [ ... ] } } }
このとき min_score で基準値のスコアを指定していたりすると、それより小さいスコアの検索結果を取得できなくなってしまいます。そういったときに Constant Score Query でラップすることでマッチした ID の数に依らず一定値のスコアで取得できるようになります。デフォルトではスコアの値が1になるようです。
"bool": { "must": { "constant_score": { "filter": { "terms": { "id": [ ... ] } } } } }
このような過程で従来の 1.7.5 のときの機能/スコアを 2.3.5 の環境でも実現できました。実際は生成するクエリがもっと複雑で変更箇所も多岐に渡りましたが、原理的にはこれで大丈夫なはずです。
カメリオ API では自然言語処理技術を使った機能をいくつか実装していますが、その前処理として文章を形態素解析して単語やその品詞を判別する必要があります。
これまでは @johtani さんの elasticsearch-extended-analyze プラグインを使っていました。この機能が 2.2.0 から Explain Analyze として標準機能として取り込まれています。これは素晴らしいですね。
例えば「この先生き残れる」という文章に tokenizer と explain フラグを指定して解析すると以下のレスポンスが返されます。
$ curl -s -X POST localhost:9200/_analyze -d '{ "text": "この先生き残れる", "tokenizer": "kuromoji_tokenizer", "explain": true }'
{ "detail": { "tokenfilters": [], "tokenizer": { "tokens": [ { "reading (en)": "kono", "reading": "コノ", "inflectionForm": null, "bytes": "[e3 81 93 e3 81 ae]", "baseForm": null, "position": 0, "type": "word", "end_offset": 2, "start_offset": 0, "token": "この", "inflectionForm (en)": null, "inflectionType": null, "inflectionType (en)": null, "partOfSpeech": "連体詞", "partOfSpeech (en)": "adnominal", "positionLength": 1, "pronunciation": "コノ", "pronunciation (en)": "kono" }, { "reading (en)": "saki", "reading": "サキ", "inflectionForm": null, "bytes": "[e5 85 88]", "baseForm": null, "position": 1, "type": "word", "end_offset": 3, "start_offset": 2, "token": "先", "inflectionForm (en)": null, "inflectionType": null, "inflectionType (en)": null, "partOfSpeech": "名詞-一般", "partOfSpeech (en)": "noun-common", "positionLength": 1, "pronunciation": "サキ", "pronunciation (en)": "saki" }, { "reading (en)": "ikinokoreru", "reading": "イキノコレル", "inflectionForm": "基本形", "bytes": "[e7 94 9f e3 81 8d e6 ae 8b e3 82 8c e3 82 8b]", "baseForm": null, "position": 2, "type": "word", "end_offset": 8, "start_offset": 3, "token": "生き残れる", "inflectionForm (en)": "base", "inflectionType": "一段", "inflectionType (en)": "1-row", "partOfSpeech": "動詞-自立", "partOfSpeech (en)": "verb-main", "positionLength": 1, "pronunciation": "イキノコレル", "pronunciation (en)": "ikinokoreru" } ], "name": "kuromoji_tokenizer" }, "charfilters": [], "custom_analyzer": true } }
attributes を指定することで任意の属性のみを返すように制御できます。
$ curl -s -X POST localhost:9200/_analyze -d '{ "text": "この先生き残れる", "tokenizer": "kuromoji_tokenizer", "attributes": ["partOfSpeech", "pronunciation"], "explain": true }'
{ "detail": { "tokenfilters": [], "tokenizer": { "tokens": [ { "pronunciation": "コノ", "partOfSpeech": "連体詞", "position": 0, "type": "word", "end_offset": 2, "start_offset": 0, "token": "この" }, { "pronunciation": "サキ", "partOfSpeech": "名詞-一般", "position": 1, "type": "word", "end_offset": 3, "start_offset": 2, "token": "先" }, { "pronunciation": "イキノコレル", "partOfSpeech": "動詞-自立", "position": 2, "type": "word", "end_offset": 8, "start_offset": 3, "token": "生き残れる" } ], "name": "kuromoji_tokenizer" }, "charfilters": [], "custom_analyzer": true } }
extended-analyze プラグインとは多少の API パラメーターやレスポンスのデータ構造に変更はありますが、その処理は単体テストがあったのでそう苦もなくテストが通るように修正して対応できました。
これも小さな変更ですが、Scripting のパラメーターの指定方法が少し変わっていました。Groovy スクリプトを使っているのですが、そのスクリプト自体は変更することなくそのまま動作しました。
Elasticsearch 1.x と 2.x では検索インデックスの互換性がありません。そのため、検索インデックスを移行するか再作成しなくてはいけません。
をざっと読む限りは、クラスター全体を再起動することで (2.x で廃止された仕様を使っていない場合は) 検索インデックスの移行も行われるように読めます。白ヤギでは既存環境と新環境との比較検証もしたかったのでクラスターそのものを新規に再作成する方針で移行しました。そのため、このドキュメントにあるアップグレードの方法は試していません。
日々の記事を取り込むバッチ処理や検索インデックステンプレートの変更に伴うメンテナンスなど、運用向けには elasticsearch-py を使って CLI の社内ツールで運用しています。以下はそのツールのヘルプですが、サブコマンドをみれば何となく雰囲気は分かるかと思います。
$ esc conf_dev.ini -h usage: esc [-h] [-v] conf_path {delete,update_user_attributes,termvector,list, alias,bulk_sourcegroups,bulk_sources,template,bulk,bulk_topics} ... positional arguments: conf_path set config.ini {delete,update_user_attributes,termvector,list, alias,bulk_sourcegroups,bulk_sources,template,bulk,bulk_topics} commands delete delete index update_user_attributes update user attributes termvector handle termvector action list list index and alias alias handle alias action bulk_sourcegroups bulk sourcegroups indexing bulk_sources bulk sources indexing template handle template action bulk bulk indexing bulk_topics bulk sources indexing optional arguments: -h, --help show this help message and exit -v, --verbose set verbose mode
簡単に言うと、検索インデックステンプレートの操作や検索インデックスそのもののメンテナンスをするものです。特別なことはしていないせいか、基本的には elasticsearch-py のバージョンをアップデートするのみでそのまま動きました。
たまたま検証環境でいろいろ作業していて以下のエラーが発生したのでその仕様変更に気付きました。
Failed to parse mapping [index-name]: Mapper for [_all] conflicts with existing mapping in other types:
[mapper [_all] has different [analyzer], mapper [_all] is used by multiple types. Set update_all_types to true to update [search_analyzer] across all types.,
mapper [_all] is used by multiple types. Set update_all_types to true to update [search_quote_analyzer] across all types.]
このエラーメッセージでググってみると、以下の issue とドキュメントが回答になります。
Elasticsearch 2.x から同一インデックスでは異なる type であっても同名フィールドは共有されるようになっているようです。そのため、異なる type で同名フィールドのパラメーターを更新しようとするとエラーが発生します。ここでは _all をインデクシングしようとした際にエラーが発生しました。特に複数の type を使い分ける必要がないのであれば気にしなくても良いです。もし複数の type をまとめて更新したいときは update_all_types を指定して更新すれば良いそうです。
社内で作っていたプラグインをインストールしようとして以下のエラーが出ました。
ERROR: Could not find plugin descriptor ‘plugin-descriptor.properties’ in plugin zip
プラグインの作り方が変わったようで plugin-descriptor.properties というメタデータのファイルを作成しないといけないようです。より厳密にプラグインの種別や対応する Elasticsearch のバージョンを定義してインストール時にチェックできるようになったようです。詳しくは以下のドキュメントを参照してください。
ansible でサーバーのデプロイを行っています。
これまでは RPM ファイルをダウンロードしてインストールしていましたが、Elastic 社が Repositories を公開しているのを知って yum リポジトリ経由でインストールするように変更しました。
- name: import gpg key for elasticsearch become: yes rpm_key: state=present key={{ es_gpg_key_url }} - name: copy yum repository file for elasticsearch become: yes copy: src=elasticsearch.repo dest=/etc/yum.repos.d - name: install the latest elasticsearch rpm become: yes yum: name={{ item }} enablerepo=elasticsearch state=latest with_items: - elasticsearch
また ansible 2.0 から elasticsearch_plugin – Manage Elasticsearch plugins モジュールが提供されているのでそれを使ってプラグインのインストールも行うように変更しました。
- name: install {{ es_plugin_kuromoji }} plugins become: yes elasticsearch_plugin: state=present name={{ es_plugin_kuromoji }} - name: install {{ es_plugin_kuromoji_neologd }}/{{ es_plugin_kuromoji_neologd_version }} plugins become: yes elasticsearch_plugin: state=present name={{ es_plugin_kuromoji_neologd }} version={{ es_plugin_kuromoji_neologd_version }}
あとは既存環境と新環境を用意して様々なテストをしたり、動作を比較したりして検証するだけです。本当は結合テストが充実していてその結果を比較できると格好が良いのですが、まだまだそこまでには至っていないため、人間が管理画面で実行しながら結果を検証しました。
とはいえ API サーバーは Elasticsearch クラスターを任意に切り替えられる仕組みになっているので既存環境と新環境を用意して比較検証すること自体は容易でした。
白ヤギで Elasticsearch を 1.7.5 から 2.3.5 へ移行するにあたり、行った変更内容や作業について紹介しました。
OSS を本番環境で使うにあたり、その OSS のバージョンアップに追随できる開発体制を維持することはとても重要です。私の経験だと、バージョンアップに伴う開発や作業そのものよりも、安定した環境を変更することや互換性を崩してしまうリスクがあるといった心理的な抵抗の方が大きいように思います。本稿の Elasticsearch のアップグレードも正に後者でした。
良いプロダクト/アプリケーションを作る秘訣の1つは、常に開発を継続してメンテナンスし続けることだと私は考えています。そのためには重要なアプリケーション/ミドルウェアのメンテナンスをしやすい開発体制を築く必要があります。具体的にはアプリケーションの個々の機能のモジュール性を高める、テストを整備する、リファクタリングして良い設計を保つといったことなどです。冒頭で紹介したように10ヶ月前から検討していながらも対応できなかったという事実からそういった開発体制の未熟さにも気付くことができました。
最後に白ヤギコーポレーションでは一緒に API サービスを開発/運用してくれるエンジニアを募集しています。Elasticsearch を使ったプロダクト開発をしてみたい方、ぜひご応募ください。
The post Elasticsearch を 1.7.5 から 2.3.5 へ移行しました first appeared on カメリオ開発者ブログ.
]]>The post Go 1.7 Release Party in Tokyo で発表しました first appeared on カメリオ開発者ブログ.
]]>発表のスライドは以下になります。発表全体として (前回のブログのフォローする発表でもあったため) Go そのものは関係なく設計の話が中心になってしまいました。
※ How to Include Clickable Links on Slideshare Presentation によると、スライドの最初の3枚はリンクをクリックして遷移できないそうです (4枚目以降は遷移できる) 。最初の3枚のリンク先に興味がある方は PDF をダウンロードしてください。
スライドで紹介した WEB+DB PRESS Vol.74 の特集記事なのですが、正式には「Web 開発1年目に身につけたい 良い設計の基礎知識 変化に強い構造・読みやすいコード・適切な分割」というタイトルが付いています。当時、私はこの特集の著者が CTO を務める会社で働いていたわけですが、これらの基礎知識を身につけてはいませんでした。その会社での開発を通してそういった基礎知識が身につきました。さらに余談なんですが、当時この特集を読んだ後に個人ブログに所感を書いて本雑誌を紹介しようと草稿はほとんど完成していたものの、なぜか公開するのを数カ月ほど忘れてしまい、公開するタイミングを逃して結果的にその草稿を削除してしまいました。
あれから3年以上経ってしまいましたが、記事を紹介して知識も身につける (実践に活かす) ことができました。義理も果たせて義務も果たしたようで気分が良いです。
全体の発表が終わった後に @deeeet さんと話していてテストはどうしてます?といった質問を受けました。私は net/http のテストコードを参考にしながら TableDrivenTests を書いています。例えば、以下のようにしてレスポンスやリクエストを生成できます。net/http/httptest というテスト向けのユーティリティパッケージもあるので使えるものはそのまま使うと良いと思います。
res := httptest.NewRecorder() req := new(http.Request) req.URL = &url.URL{Path: "/api/ping"} req.Method = "GET" req.Header = make(http.Header) req.Header.Set("Content-Type", "application/json")
そうやって話していて気付いたのですが、レイヤ化して context を API 処理に渡すことのもう1つのメリットとしては単体テストが容易になることです。例えば、PingAPI というものがあって、その実装は以下のようになっているわけですが、API 処理は context としかやり取りしないので引数で渡している ApplicationContext を操作することでテストが書けます。
func PingAPI(c *types.ApplicationContext) { r := &PingAPIResponse{ CommonResponse: types.CommonResponse{Status: types.VALUE_OK}, Message: "pong", } c.SetResponse(r) }
私は Go 1.6 Release Party のときも参加していたのですが、リリースされた機会に新機能や変更点などを聞けるのは良いものですね。あまり他の言語コミュニティで見かけない気がします。今後もこのイベントが続いていくと嬉しいです。
最後に白ヤギコーポレーションでは一緒に API サービスを開発/運用してくれるエンジニアを募集しています。Go でプロダクト開発をしてみたい方、ぜひご応募ください。
The post Go 1.7 Release Party in Tokyo で発表しました first appeared on カメリオ開発者ブログ.
]]>The post Go で API サーバーを開発してきて1年が過ぎました first appeared on カメリオ開発者ブログ.
]]>白ヤギでは Go 言語でニュース記事のキュレーションをする カメリオ API というサービスを開発しています。約1年2ヶ月前、Go を使って開発し始めたときに当時調べた内容を整理して以下の記事を書きました。
1年以上に渡り開発を継続してきて変わったこと、変わってないことなどをざっくばらんにまとめてみます。たまたま過去の記事のはてブコメントを見返していて 以下のコメント を見つけました。
最近 golang 導入事例増えて来たけど、導入後一年くらいのメンテナンスフェーズな事例について聞いてみたい。継続的デリバリーみたいなの。まだ早いのかな?
まだまだメンテナンスフェーズにはなっていなくて現在も活発に開発中ですが、継続的デリバリーについて白ヤギでは特別なことをしてなく、ansible を使ってデプロイしているのみです。Go 1.6 からパッケージ管理の代替?となる vendoring 機能がデフォルトになっていて、いずれ対応しようとは考えていますが、白ヤギの用途ではパッケージ管理に困っていないので優先度は低いです。
API サービスも順調でここ半年ほどで数社の顧客に導入されています。導入事例をいくつか紹介します。
開発期間が1年以上になるにつれ、どのように開発が変わってきたかの変遷について書いてみます。コードの行数を数えてみると1万5千行 (テストコードを含めると2万5千行) ほどでした。開発規模は小さいのでちょっとした API サービスだと捉えてください。
開発環境は以前と変わらず全く同じです。
補足すると、Go のバージョンは新しいバージョンが出るごとに上げ続けています。1年前は 1.4.2 を使っていましたが、現在は 1.6.2 (8月上旬に 1.7 がリリース予定なので 1.6.3 は飛ばす) です。いつも新しいバージョンが出ると、ローカル環境で動作確認した後、すぐステージング環境に新しいバージョンをデプロイします。ステージング環境で1週間ほど動作させた後、問題がなければ本番環境にデプロイするようにしています。そうやってこの1年バージョンを上げ続けてきましたが、白ヤギの用途では Go のバージョンアップが原因で問題が発生したことは1度もありませんでした。
いま API サーバーを開発するのに使っているライブラリです。
以下は以前に使っていたライブラリですが、別のライブラリに移行しました。
簡単に移行したライブラリとその理由について紹介します。
ini ファイルを読むライブラリを configparser から ini というライブラリに変更しました。ini ファイルの設定と Go の構造体とのマッピングを簡潔に実装できるので設定項目が増えたときのメンテナンスが容易になるという利点から変更しました。以下のサンプルコードは [Note] セクションを構造体にマッピングしています。例えば、この [Note] セクションに新しい項目が追加されても Note 構造体にメンバーを増やすのみで対応できます。
// Just map a section? Fine. n := new(Note) err = cfg.Section("Note").MapTo(n)
昨年の秋頃に net/http の薄いラッパーライブラリであった Negroni から Goji に移行しました。
当時 Go のような若い言語で特定の Web フレームワークを使うのはリスクだという同僚からの反論もあったのですが、私の動機付けとしては Context オブジェクトは Web フレームワークと一緒に管理すべきだという設計方針がありました。もちろん Negroni でその仕組みを独自実装するという案もありましたが、それは Goji 相当のものを再発明するだけじゃないかという懸念もあり、Goji の採用に至りました。また Goji のサードパーティライブラリが使えるという利点もあります。白ヤギでも glogrus や cors などを使っています。
余談ですが、先月ユーリエの池内さん (@iktakahiro) に Go言語とReactで考える「いい感じなURL設計」 と題して 白ヤギが主催している勉強会 で発表して頂きました。
池内さんは Echo という軽量 Web フレームワークを採用していると発表されていました。私が Goji を採用した当時に Echo を見つけられていなかったのですが、これから採用するなら Echo も検討して良さそうです。パフォーマンスに重点を置くなら fasthttp に対応している数少ない Web フレームワークとしてもおもしろい存在です。
Goji も新しい機能は別リポジトリである github.com/goji/goji で開発を行うようです。Context オブジェクトに x/net/context を使っています。いずれ go 1.7 から標準ライブラリになる context パッケージに置き換えられるでしょう。また古い方の github.com/zenazn/goji もしばらくはメンテナンスを継続するとあります。
以前はカメリオのシステムを共有して使っていましたが、クラスタリング環境の管理/スケールの容易さ、コミュニティの盛り上がりなどから Elasticsearch に移行しました。それに伴い Elasticsearch クライアントとして Elastic というライブラリを採用しました。Elastic ライブラリについては以前にも Elasticsearch Advent Calendar 2015 向けに記事を書きました。
このライブラリについてもいくつか知見が溜まってきているのでいろいろ書きたいのですが、また別の記事にします。と言い続けてなかなか書かなかったりもするのですが、、、。
1年以上、API サービスを開発してきて大きなアーキテクチャの変更が2回ありました。1つは上述した Negroni から Goji への移行です。もう1つは Goji のフレームワーク上にアプリケーション層というレイヤを構築したことです。以下はアーキテクチャの概念図です。
余談ですが、このアーキテクチャにおけるアプリケーション層は Java の Spring フレームワーク での開発経験から影響を受けています。DI (Dependency Injection) までは作り込んでいませんが、AOP (Aspect-oriented programming) の概念を取り入れています。
この階層構造により、例えば、認証やキャッシュの処理などといった API を横断的に行う処理は Goji 層のミドルウェアで行い、顧客=アプリケーションと見立て、顧客別のビジネスロジックはアプリケーション層のインターセプターで実現しています。Goji 層とアプリケーション層を分離したことで安定性や保守性を維持しつつ、機能の拡張性や柔軟性も確保しています。API サービスを利用している顧客が急増した今でもこのアーキテクチャは機能していてしばらくは大丈夫そうに考えています。
サービスのアーキテクチャを考えた場合、次に目立つのはモデルコンポーネントの貧弱さです。OR マーパー的な gorp と、squirrel という SQL ビルダーを組み合わせて実装していますが、言わば SQL を発行して Go の構造体にマッピングする薄いラッパーでしかありません。もし次に大きな変更をするとしたら、おそらく gorp と squirrel を別のものに置き換えて、モデルコンポーネントを作り直すことになると思います。
多くの Web フレームワークではミドルウェアの仕組みを提供していると思います。Goji では Mux 単位にミドルウェアを設定する仕組みを提供していて、自分たちのアプリの用途として Mux 単位に任意の URL や URL 群を管理できるのであれば、標準のミドルウェア機能だけでも十分です。
とはいえ、例えばリソースを管理する CRUD な API を提供しようとしたとき、リソースの Create (POST メソッド) と Update (PUT メソッド) には検索インデックスの更新処理を、Retrieve (GET メソッド) は何もせず、Delete (DELETE メソッド) は削除処理を、といった要件に対してミドルウェアを適用するのは、がんばればそういった単位に Mux を分割して設定することもできますが、やや煩雑になりがちであるし、さらに他のミドルウェアとの連携を考慮するとすぐに破綻しそうにみえます。
白ヤギでは独自に MiddlewareDispatcher を定義して、ディスパッチ機能をもつミドルウェアを生成して登録します。以下はミドルウェアの生成メソッドです。
func (self *MiddlewareDispatcher) GetMiddleware() func(c *web.C, h http.Handler) http.Handler { return func(c *web.C, h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { isDispatch, handlersKey := self.Dispatch(c, r) if isDispatch { for _, before := range self.GroupBeforeHandlers[handlersKey] { if !before(*c, w, r) { break } } } // must call regardless of MiddlewareDispatcher.Dispatch // since http handler chain would be stopped h.ServeHTTP(w, r) if isDispatch { for _, after := range self.GroupAfterHandlers[handlersKey] { after(*c, w, r) } } } return http.HandlerFunc(fn) } }
MiddlewareDispatcher にはパスや HTTP メソッド、ハンドラー関数などセットした上で上記のメソッドを使って Mux にミドルウェアを登録します。
createTopicsDispatcher := NewMiddlewareDispatcher() createTopicsDispatcher.SetHandlers( PATH_TOPICS, "POST", handlers.CreateTopicsAPI, []BeforeHandler{}, []AfterHandler{ createOrUpdateSearchIndex, }, ) self.Mux.Use(createTopicsDispatcher.GetMiddleware())
この仕組み自体は非効率ではあるものの、このミドルウェア生成処理自体は安定しているので1年近く運用している中では保守や機能面で困ったことはありません。
その他に、私が知っている Goji のミドルウェア拡張としては kami があります。kami においても上述したミドルウェアグループによる管理は相互依存や組み合わせが煩雑になるという懸念から URL 階層に従うミドルウェアの仕組みを提供しています。また kami のミドルウェアは次に呼び出すハンドラーを返すのではなく、Context オブジェクトを返すかどうかでミドルウェアの実行制御をしています。
2016-08-09 追記
設計に関してイベントで発表したスライドを以下で公開しています。
テストの仕組みは1年前と変わっていません。
基本的には TableDrivenTests で単体テストを書いています。それでも何とかなっていると言えば何とかなっていますし、テストコードという特性上、優先度が低いので単に変えていないだけだったりもします。勉強会などでは testify を使っている事例をちょくちょく聞くのでいずれはこのライブラリを使おうかと検討していたりもします。
前回の記事でも書きました。あれから1年経ってどんな雰囲気になってきたかをいくつかの記事を紹介しながら考えてみます。と、いろんな記事を紹介しながら書き始めたらいろんな話題がごちゃ混ぜになっています。あくまで私個人が見聞きした記事から紹介しているのである程度は偏っているという前提で興味のあるところだけ参考にしてください。
ちょくちょく見かけるようになってきたと思います。それだけ Go 言語が多くの開発者に普及したという証拠でもあると私は思っています。
github に “Go はそれほど良くないよ” と銘打ったまとめ一覧があります。
このまとめ一覧から接頭辞が no で始まるものを抜き出してみます。
no constructors
no user-type iteration
no OOP
no language interoperability (only C)
no versioning model
no function/operator overloading
no exceptions
no generics
no immutables
no pattern matching
no decent IDE
no map/reduce/filter
no subpackages
no first-class support of interfaces
no unused imports
no ternary operator
no macros or templates
no virtual functions
こうやって抜き出してみると、いろいろあるもんですね。
まずジェネリクスは既に提案がされていて、もしかしたら次のメジャーバージョン Go 2.0 のときに採用されるかもしれません。
私の用途だと、ジェネリクス以外のものはなくても開発に困っていなかったりはします。強いて言えば、三項演算子があると嬉しいですが、それはコーディングスタイルの好みです。
ジェネリクスの代替として interface を使うサンプルが紹介されている記事を見かけた人も多いでしょう。例えば、sort パッケージのソースをみると、sort.Interface が以下のように定義されていて、一部のソート処理のサンプルを抜き出すとこんな感じに実装しています。
type Interface interface { // Len is the number of elements in the collection. Len() int // Less reports whether the element with // index i should sort before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) } func Sort(data Interface) { // Switch to heapsort if depth of 2*ceil(lg(n+1)) is reached. n := data.Len() maxDepth := 0 for i := n; i > 0; i >>= 1 { maxDepth++ } maxDepth *= 2 quickSort(data, 0, n, maxDepth) } // Insertion sort func insertionSort(data Interface, a, b int) { for i := a + 1; i < b; i++ { for j := i; j > a && data.Less(j, j-1); j-- { data.Swap(j, j-1) } } }
確かに interface で実装はできますが、数値型のサイズ違いのとき、プリミティブ型のエイリアスとして独自型を定義したときなど、毎回 interface を定義してそれぞれにほとんど同じようなコードを実装するのはちょっとうんざりしますし、DRY 原則にも違反してしまいます。
たまたま調べていて見つけたのですが、Go 言語の開発者である Russ Cox 氏の過去のブログ記事 The Generic Dilemma に、ジェネリクスにまつわるプログラマーの生産性、言語の複雑性、コンパイル速度、実行速度のそれぞれのトレードオフのような葛藤が書かれていておもしろかったです。興味があればそちらも参考にしてください。
批判を見かけるようになった一方でプラクティスも洗練されてきたように思います。
開発しているものがバイナリとして使うものか、ライブラリとして使うものかでリポジトリ構造を変えるというプラクティスが私は参考になりました。白ヤギでは API サーバーという単一リポジトリのみで開発しているのですが、そろそろライブラリ類を分割しても良いかもしれないと考えている頃です。Go のコンパイルは速いと言っても、やはりコードベースが大きくなるにつれて徐々に遅くはなっていくのでライブラリとしてリポジトリ分割する際に役立ちそうです。
またテストフレームワークやライブラリに関しては以下のようにばっさりですね。
2014年、私は様々なテスト用フレームワークとヘルパーライブラリを使った経験を振り返り、1つとして使えるものはないという結論に達して、stdlibのアプローチであるテーブルベースの平易なパッケージテストを薦めることにしました。
と、この記事の著者は主張していますが、私はここで言うテーブルベースの平易なテストのみをずっと書いてきてもう疲れました。また記事の中ではテストしやすいコードのための設計として関数型のスタイルで書くことが最善である (と思われる) と推奨しています。これは Go 言語に限ったものではなく、他の言語でも共通のプラクティスな気がします。
あまり見かけないもので、私が開発してきて思うプラクティスの1つは、Named result parameters (名前付き結果パラメーター) を多用することです。CodeReviewComments#named-result-parameters から簡単に抜粋してみます。以下はこの wiki で名前付き結果パラメーターの使用例として紹介されているサンプルコードです。
// Location returns f's latitude and longitude. // Negative values mean south and west, respectively. func (f *Foo) Location() (lat, long float64, err error)
return
と書きたいだけならその価値はないと、このコメントを読む限りでは多用することを推奨してはいません。私も明示するコードを書くスタイルを好むので当初は名前付き結果パラメーターを使っていませんでした。開発をしていて徐々に名前付き結果パラメーターを多用した方が良いと考えるように至った理由は以下になります。
名前付き結果パラメーターを多用するというのは私の主観なのでご参考まで。
GolangRdyJp を眺めていて興味深かったので紹介します。
白ヤギではまだ機能優先の段階であり、パフォーマンスの考慮は後回しになっていたりします。アプリに応じた最適化をするには適切なテストとプロファイルを取ることが必要になります。こういった Tips は、どこから最適化していくかの取っ掛かりになるのでとても参考になります。
私はイベントの抽選に外れて参加できなかったので発表そのものは聞いていないのですが、キーノートスピーカーの Dave Cheney 氏のスライドをみると Go のエラー処理について発表されていたようです。余談ですが、先日 『プログラミング言語Go』刊行記念イベント に参加してきたのですが、そのトークセッションの中でも鵜飼氏が「Go はエラーの扱いが特徴的だ」と話されていたのが私の記憶に残っています。
さて、Dave 氏のスライドでは Go のエラー処理を3つの種別に分けて考察しています。
error の値により処理を続行するか中止するかといったエラー処理を行う方法です。
io.EOF や syscall.ENOENT などが例として紹介されています。また error interface の Error() メソッドは、コード上で使うものではなく人間向けにログ出力や画面に出力するための文字列なので決して検査しようとしてはいけないとあります。結論としてこのようなエラー処理は避けるべきだと主張しています。
独自エラー型を定義して型アサーションや型 switch を使ってエラー処理を行う方法です。
os.PathError が標準の error を内包して付加情報を返す良い例だと紹介しています。
// PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
Sentinel errors よりはこちらの方が良いが、エラー値の多くの情報を共有してしまうところに懸念があるため、結論としてこれも避けるべきだと主張しています。
エラーの中身を知る必要はなく、エラーが発生したという事実をもってエラー処理を行う方法です。
import "github.com/quux/bar" func fn() error { x, err := bar.Foo() if err != nil { return err } // use x }
Go で開発しているとこのエラー処理の方法が一般的であるように思います。呼び出し側はエラーが発生したとかどうかのみを判別し、Foo() メソッドはどんなエラーを返すのかを保証しなくて済むというのが利点だとあります。
これらから Go の格言を紹介されていました。
Don’t just check errors, handle them gracefully.
Dave 氏の発表の後半に出てくる pkg/errors というライブラリについては Golangのエラー処理とpkg/errors の記事で詳しく解説されていて参考になります。
pkg/errors ライブラリでは、Wrap() と Cause() というメソッドでエラーを扱います。
_, err := ioutil.ReadAll(r) if err != nil { return errors.Wrap(err, "read failed") }
type causer interface { Cause() error } switch err := errors.Cause(err).(type) { case *MyError: // handle specifically default: // unknown error }
Java の例外チェーン機能 を知っていれば、この概念は受け入れやすいものだと思います。Python においても PEP-3134 で提案され、Python3 からこの機能が提供されています。Java/Python の場合、これらが言語機能に組み込まれている利点としては、スタックトレース情報を自動的に保持してくれたり、標準化されていることで例外情報の扱いがライブラリ依存にならなくて済むといったところでしょうか。
Stack Overflow でも Go 言語で例外チェーンはどうやるの?といった質問を見かけた (そういう機能はないという回答) のですが、言語機能 (もしくは標準ライブラリ) として提供するといった議論はあるのでしょうかね?私が簡単に調べた限りでは分かりませんでした。
1年前は Python から Go への移行が目立っていましたが、最近はその逆もたまに見かけるようになってきました。Python と Go、どちらの言語を選択するかの最も大きな要素は並行性や実行速度に関するものだと思います。Go を試してみた上で実行速度が絶対要件ではないものは Python の方が嬉しい場合があると表明する開発者が出てきました。
このスライドのパフォーマンスのところを抜き出してみます。
ちゃんと調べていないのですが、SSL の話題は Go crypto: bridging the performance gap の記事を指していると思います。比較対象が Go 1.4.2 なのでやや古い記事であるのに注意してください。
閑話休題。PyPy の存在により Go と比べて2-10倍遅い程度なら許容できる場合が多くあると書かれています。PyPy は良い選択肢だと思うのですが、1つ懸念なのは Python 3 互換である PyPy3 の開発がやや停滞気味にみえるところです。現在は 3.3.5 互換のアルファバージョンが公開されています。
こちらの記事は asciinema というターミナルセッションを記録するアプリ開発で Go 実装から Python 実装に変更したというものです。Go の良さも認めつつ Python に切り替えた理由として以下が挙げられています。
if err != nil {
と書くのにすぐ飽きるこのアプリの開発者にとって、アプリの特性を考慮した結果、Go よりも Python の方に開発上のメリットがあるというように読めます。
PyCon 2016 のスケジュールを眺めていて以下の発表を見つけました。
Go と Python はそれぞれ C 言語とやり取りできるのに着目し、C 言語を経由してそれぞれの言語のランタイムバリアを超えられるのではないかという考察です。Go から C 言語とやり取りするためのパッケージとして cgo があります。一方、Python から C 言語とやり取りする方法はいくつかありますが、ここでは CFFI (C Foreign Function Interface) を使っています。
実用性はともかく、技術的な興味としておもしろいです。Python の開発者はその文化的な類似性から Go の開発に馴染みやすいと思います。私のように Python でちょっとした Web アプリやツールを作ったりしつつ、Go でサーバーサイドの開発をしているプログラマーはたくさんいるように思います。実際に動くサンプルモジュールを作って Go の net/http と比べて遜色ないパフォーマンスのベンチマーク結果も掲載されています。
この読み難いテンプレートの、読み難い構成の、だらだら長い記事を最後まで読んで頂いてありがとうございます。
私はもう1年以上 Go 言語で API サーバーを開発しています。サービスが1年分大きくなった今でも特に困ることなく開発を継続しています。本稿の中でいくつもプラクティスを紹介したのですが、ビルドやデプロイも含め、まだまだ自分たちのプロダクトに活かせていないところがあるのが現状です。私自身 Go の機能や設計哲学、その開発文化などを学びながら、まだまだサービスを成長させる余地があるように考えています。
最後に白ヤギコーポレーションでは一緒に API サービスを開発/運用してくれるエンジニアを募集しています。Go でプロダクト開発をしてみたい方、ぜひご応募ください。
The post Go で API サーバーを開発してきて1年が過ぎました first appeared on カメリオ開発者ブログ.
]]>The post カメリオで使われている機械学習 first appeared on カメリオ開発者ブログ.
]]>カメリオでは、テーマに合ったニュース記事を提供するために、機械学習を応用した新しいアプローチを最近こっそり導入しました。この記事では、カメリオがどのようにニュース記事がテーマに合っていると判断しているのか、そのアルゴリズムの概要を解説してみたいと思います。
カメリオでは新しく入ってきたニュース記事を、何万もあるテーマの中から良く当てはまるものに自動的に振り分けています。これまでカメリオでは、記事があるテーマに振り分けられるためのさまざまな条件を半自動的に導出して、テーマと記事とのマッチングを行っていました。しかしこの従来の方法では、テーマ名の単語が記事中にたくさん出てきたりした場合に、実際にはあまりテーマに関係が無かったり、あるいはユーザの興味を引かないような記事が混ざってしまうことがありました。
新しく導入した機械学習を用いたアプローチは、従来の手法で選ばれたニュース記事を受け取ると、ユーザーが興味を持ちそうな記事だけを残す一種のフィルタとして実装しています。言い換えると、一見テーマにマッチしていそうなニュース記事の中から、実際には内容がテーマに関係していないものを取り除くことを目指しています。
新しい手法によってアプリで表示される記事がどのように変わったのか、まずは例を見てみましょう。新しい手法を導入する以前、ある時に「カメラ」テーマで出ていた記事は次のようなものでした。
– 3Kクラスの360度撮影! 約1.7万円で買える、コスパ抜群な全天球カメラ
– NIPPO/ホイールローダー自動停止システム開発/ステレオカメラで障害物対応
– OpenCV-Python Tutorialsの「カメラ校正」への補足
– TSIHD、店頭状況をカメラで分析 来客の購入率など
– スマートデバイス向け業務カメラアプリケーションKAITO セキュアカメラを住友生命保険相互会社に提供開始
– 世界初 中判ミラーレスデジカメがなんだかすごそう【倶楽部】
– iPhone7のカメラ・モジュールが激写される!7 Plusじゃなても一眼レフ並に?
– 富士フイルムがX-Pro2やX-T1など6機種の新ファームをリリース(デジカメinfo)
カメラに関する記事が並んでいますね。しかし、よく見てみるとカメラという単語はタイトルに含まれているものの、カメラそれ自体についての記事でないものもあるようです。そういった記事は「カメラ」テーマをフォローしているひとの関心は薄いと思われるので、一覧から取り除かれているほうが良さそうです。さて、一覧で色がついている記事がありますが、それらは新しい手法によって取り除かれるものです。カメラ自体にあまり主題が置かれていない記事が省かれていると思うのですが、いかがでしょうか。
もうひとつ、「自動車」テーマの例も見てみます。
– BMW/MINIブランドを体験できる複合販売店「BMWグループ東京ベイ」がオープン
– Official: 世界最速記録を競い合う「ボンネビル・スピードウィーク」が3年ぶりに開催へ
– トロロッソ「1台でもトップ10に入れて良かった」/F1イギリスGP2日目
– レッドブル「フェラーリに勝って2列目を独占」/F1イギリスGP2日目
– アルピナ B10 ビターボをじっくりと
– Official: 北米日産、マイナーチェンジした「パスファインダー」を発表
– ウィリアムズ「7番手と12番手、これが今の実力だ」/F1イギリスGP2日目
– メルセデスAMG「最後の最後までシビレる展開に」/F1イギリスGP2日目
一覧の記事を眺めると、大きく分けて乗用車に関する記事とF1に関する記事の二種類が混ざっていることが見てとれます。F1に関する記事については「フォーミュラ1」テーマが用意されているので、「自動車」テーマとしては乗用車の記事がメインになっていると良さそうです。新しい手法を適用することで、一覧に色付きで示されたF1に関する記事がフィルタリングされます。
今回導入した手法は、ざっくり言うとCBOW+SVMです。CBOWというのは文書をベクトル化する方法で、SVMはベクトル化された文書を分類する手法です。新しいニュース記事が入ってきたとき、その記事を表示するか除外するか判別するまでの流れを順を追って見ていきたいと思います。
まず記事が入力されると、その文書に含まれる単語を抽出します。抽出された各単語はWord2Vecという単語の意味を表現する手法によってベクトルに変換します。そして変換された各単語ベクトルの平均をとり、その結果得られたベクトルを記事のベクトルとします。このように記事のベクトルを単語の分散表現(ベクトル)の足し合わせで表現する手法をCBOWと呼びます。
記事のベクトルはテーマごとに用意されたSVMに入力として与えられ、出力としてテーマに対する記事の良さのスコアが得られます。SVMは機械学習でよく用いられる分類器のひとつで、前もって正解データを与えて学習させておきます。正解データとして用いられるのは”良い”記事と”悪い”記事ですが、それらの記事の”良さ”はカメリオでの人気度などから判断しています。SVMから得られたスコアはテーマごとに設けたしきい値と比較され、最終的に記事を表示するか除外するかを決定します。
この方法は比較的シンプルなので、テーマごとの学習にかかる時間が短くすみます。それだけではなく、いろいろ試した方法のなかでも分類性能が高くて意外なほどでした。そのほか試してみた方法では、文書ベクトルを得るのにParagraph Vectorという手法を用いてみたり。あるいは、テーマごとに単語の生成モデルを推定して、記事の良さをWMD(Word Mover’s Distance)という手法で求めてみたりしました。しかしそれらの方法では、より時間がかかるのにも関わらず、得られた性能は今回導入した方法に及びませんでした。
このようにして動作するフィルタリングを前述のマッチングと組み合わせて、カメリオではテーマごとに表示する記事を選んでいます。大ざっぱな説明だったかもしれませんが、雰囲気だけでも感じていただけていれば幸いです。
The post カメリオで使われている機械学習 first appeared on カメリオ開発者ブログ.
]]>The post ランダムフォレストを使った初期分析例 first appeared on カメリオ開発者ブログ.
]]>まずデータの前処理では、難なくRのrandomForestに突っ込むための加工を施します。主なステップは以下の4つです。
初期分析の段階で全データを使う必要はないと考えます。変数のサイズにもよりますが、私は10%くらいのデータでまずやるようにしています。
RのrandomForestのデフォルト設定では、欠損値があると実行できません。そのため、欠損値を補完する必要があります。
数値データの欠損は、本来は理由に応じて処理すべきですが、理由がわからなければ中央値で補完してしまいます。しかし、ただ単に補完してしまうと、数値が入っていたのか、欠損値だったかという情報が失われてしまいますので、欠損の有無をTRUE/FALSEで表現し、新たな変数として用意します。
Rにはfactor(因子)と呼ばれるデータ型があり、カテゴリ変数を表現するのに使われます。
RのrandomForestパッケージは、因子数が多すぎるとエラーになるので、4つ以上の因子数があるfactor型の変数は、整数にしてしまいます。カテゴリ変数を整数に直してしまうと違う意味になってしまいますが、もしある特定の因子が精度に効いてくるのであれば、決定木の中でうまく探し当ててくれるはず!ということで乱暴ですが、整数にしてしまいます。
日付はそのままだとランダムフォレストに入れにくいので、年、月、週数、1月1日からの経過日数、曜日といった数値に変換しておきます。
さて、ここまでデータの前処理をしたら下のような感じで、データをランダムフォレストに突っ込みます。
randomForest(ind, dept, ntree=30, sampsize=5000, nodesize=20, do.trace=10))
#indが説明変数 deptは被説明変数です
ポイントは、sampsize、ntree、nodesizeを大きくしすぎないことです。
sampsizeは各決定木を作るときのサンプリング数ですが、これが大きいと学習に時間がかかります。
また、ntreeは決定木の個数を指定するパラメーターも大きくすると精度は上がりますが、時間もかかります。忙しい人にはそんなに待っていられません。そのため、木の数を30とか50とか小さくても良いと割りきったほうが学習は短時間で済みます。
最後のnodesizeは決定木のターミナルノードに残す最小レコード数を指定するパラメータですが、大きい数字にすると結果的には木は浅くなります。そのため、RのrandomForestパッケージのデフォルト設定では、分類問題であれば1、回帰問題であれば5ですが、これを比較的大きい数値(ここでは20)にすると木が伸び過ぎるのを抑えることができ、学習時間が短くなります。
RのrandomForestにはvarImpPlotという関数が用意されており、変数の重要度を簡単に確認することができます。
下の例は、Kaggleのbluebook-for-bulldozersという中古ブルドーザーのオークション落札価格を予測するというコンペのデータを使って、上記のデータ前処理を行い、varImpPlot関数で、変数の重要度を可視化したものです。
これを見ると、一目瞭然で重要度が高い変数がわかります。
例えば、一番上に出ている”YearMade”、つまり何年に作られたブルドーザーかを示す変数が重要であることがわかります。次いで、”ProductSize”、ブルドーザーの大きさも重要な変数のようです。
一方、グラフのしたのほうにある変数は重要度が低い変数です。このような変数は予測モデルを作るうえではあまり効かないので、モデルからはカットしてしまいます。
時間があるなら、分析者自身でどのような変数が効きそうかをイメージし、変数を新たに作ってモデリングするというのが正攻法ですし、そのほうが楽しいです。
しかし、時間がないとなるとそうも言ってられません。そこで、2つの変数を総当たりで割り算して新しい変数を作ってしまいます。この方法をとると、膨大な新しい変数が作られますが、ランダムフォレストに突っ込み、変数の重要度を見て、効いていない変数はモデルからどんどん除外していきます。そうすることで、モデルの精度を上げていくと同時に、どのような変数が重要なのかを理解していきます。
分析コンペであればともかく、最終的にはデータから何がわかるのかを理解する必要があります。
そんなときは、RのrandomForestパッケージで用意されているpartialPlotという関数が役立ちます。
partialPlotとは、一言で言えば、偏微分です。興味ある特定の変数のみを動かし、残りの変数を固定したときの結果に与えるmarginal effect(限界効果)を可視化したものになります。
下の例は、再びKaggleのbluebook-for-bulldozersという中古ブルドーザーのオークション落札価格の予測コンペのデータを使ったものです。このコンペのトレーニングデータには、中古ブルドーザーの型式や製造年やスペックなど52の説明変数が用意されており、その中にはオークションの実施日付もありました。その日付から年だけを取り出した変数が、下のpartialPlotで用いた「saledate_year」という変数です。つまり、何年に行われたオークションかということですね。
さて、partialPlotの見方ですが、横軸が対象の変数、ここでは「saledate_year」です。縦軸が限界効用になります。このpartialPlotをみると、1990年代は高く、1990年代の最後になると価格が下がる傾向にあるようです。(1990年代は新品のブルドーザーが少なかった?1990年代前半は建築業界で需要が多かった?等いろいろな仮説が考えられますが、実際のところ調べていません。。。ご存知の方いましたら教えてください。)
The post ランダムフォレストを使った初期分析例 first appeared on カメリオ開発者ブログ.
]]>