こんにちは、6年前に 入社エントリ を書いて以来の中野です!
あれから時が経ち、現在はBtoB領域で商品情報を管理するシステムを担当していて、去年から検索の改善にも取り組み始めています。取り組みの一環として機械学習を活用したランキング改善を検証しているのですが、当初はそもそも機械学習についての知識がゼロの状態でした。
この記事では、そんな私が検索×機械学習に入門した際の経験を共有させていただきます。
はじめの一歩
社内に機械学習をやっている部署はあるものの「検索×機械学習」は事例がなく、まず何から始めたらよいのか見当がつかないような状況でした。何か参考になる情報がないかと調べていたところ「機械学習による検索ランキング改善ガイド」を見つけました。
タイトルのとおり、ランキングの改善に機械学習のアプローチを導入することによって検索結果の質を高めるプロセスを解説する書籍です。2部構成になっていて、前半で検索の基本から機械学習検索ランキングに関連するトピックの解説があり、後半から実際に手を動かして検索システムに機械学習を導入して検索結果を改善するハンズオンになっています。
この書籍のおかげで検索における機械学習導入の流れを掴むことができ、改善に向けてはじめの一歩を踏み出すことができました。著者の皆様にこの場を借りて感謝申し上げます!
エラーに遭遇
次のステップとして、導入プロセスに沿ってアスクルのデータを使って学習データを作るところから、Elasticsearchにデプロイしたモデルを使って検索結果を取得し、その結果を評価するところまでひととおり検証しました。その最中、モデルをElasticsearchへアップロードする際に次のような「Unknown feature」というエラーに遭遇しました。
{ "error": { ... ... "caused_by": { "type": "parsing_exception", "reason": "Unknown feature [2]", "line": 10, "col": 14 } ... ... }, "status": 400 }
これは、次のようにモデルをファイル出力する際に特徴量名のマッピングを指定していないことが原因でした。
# モデルをファイル出力する ranker.get_booster().dump_model( "model.json", # fmap引数で特徴量名のマッピングファイルを指定しないと # Elasticsearchにアップロードする際に「Unknown feature」エラーになってしまう。 fmap="feature_map.txt", dump_format="json", )
ググったりAIに聞いたりすることですぐに解決できました!
補足
私が当時参考にしていた書籍が数年前の物でして、そのコードを参考にしつつ各種ライブラリ等は最新バージョンを使っていたためこのエラーに遭遇していました。 LTRプラグインのサンプルコードではしっかり fmapが指定されています。もっと早くにプラグインのサンプルコードを確認すべきだったと反省しています...!
出力されるモデルを見比べてみた
一応解決はしたのですが、これだけではイマイチ腹落ちしませんでした...。そこで、出力されたモデルの中身を見比べてみることにしました。fmapを指定した場合としなかった場合で出力されるモデルファイルの差分を比較した画像を次に示します。
たしかに比較してみると(細かい見方はわかりませんが)、fmapを指定することでsplit要素の値が特徴量名になることがわかりました。
LTRプラグインの実装を見てみた
また、せっかくエラーメッセージに出会ったので LTRプラグイン のソースコードを見てみることにしました。とはいえ私はElasticsearchについても習熟しているとはいえず、プラグインのソースコードを見るのも初めてですのでどこから見ていけばよいか見当もつきません。ですので、ひとまずエラーメッセージで検索してそれっぽいところを探すことにしました。
検索結果 を眺めていると「XGBoostJsonParser.java」といういかにもそれっぽいファイルがありました! どうやらここで実装されている SplitParserState::parse メソッド で、アップロードされたモデルをパースしているようです。次にメソッドのソースコードを引用します。
public static SplitParserState parse(XContentParser parser, FeatureSet set) { SplitParserState split = PARSER.apply(parser, set); if (split.isSplit()) { if (!split.splitHasAllFields()) { throw new ParsingException(parser.getTokenLocation(), "This split does not have all the required fields"); } if (!split.splitHasValidChildren()) { throw new ParsingException(parser.getTokenLocation(), "Split structure is invalid, yes, no and/or" + " missing branches does not point to the proper children."); } if (!set.hasFeature(split.split)) { throw new ParsingException(parser.getTokenLocation(), "Unknown feature [" + split.split + "]"); } } else if (!split.leafHasAllFields()) { throw new ParsingException(parser.getTokenLocation(), "This leaf does not have all the required fields"); } return split; }
アップロードされたファイルをパースした結果がsplit変数、あらかじめElasticsearchにアップロードしておいた特徴量セットの定義がset変数に格納されていることが読み取れます。
先述のとおり、モデルのファイル出力時にfmapを指定しないと "split" の値が特徴量名ではなく "0" などの数字になってしまいます。そのため、このメソッドでsplitとsetを突き合わせたときに一致せず「Unknown feature」エラーが発生していたと理解できました!
テストコードを書いてPRを出してみた
次にSplitParserState::parseメソッドのテストコードを眺めていたところ、一部のケースでテストが不足していることに気づきました。プルリクチャンス! ということで、すかさずPRを出しました。
現在、レビューをお願いしているところです(すでにお一人からはapproveをいただきました!)。メンテナの皆さんは 本業の傍らでこのプラグインのメンテをしてくださっている ようです。多忙な中でのご対応に本当に頭が下がります。ありがとうございます!
おわりに
私が検索×機械学習に入門した際の経験を共有させていただきました。
素晴らしい技術書に出会えたことで、身近に事例が無い中でも導入に向けて一歩を踏み出すことができました。また、エラーメッセージについてわからないなりに地道に調べてみることで、プラグインの中身についてほんの少しですが理解を深めることができました。今後もチャンスがあれば積極的にコントリビュートしたいと思います。
私自身いつも今回のようにソースコードまで確認しているわけではありませんが、「このライブラリ/ツールとは長い付き合いになりそうだな」という時には、エラーをきっかけにソースコードを覗いてみるのもよいかもしれません。この記事を読んでくださった皆様のご参考になれば幸いです!
仲間を募集中です!
我々のチームでは今後、社内の機械学習のプロフェッショナルと連携しながら検索の改善に取り組んでいく予定です。この記事を読んで検索やアスクルの開発に興味を持たれた方はぜひお気軽にご連絡ください。一緒に取り組める仲間をお待ちしています!