ML_BearのKaggleな日常

元WEBマーケターのMLエンジニアがKaggleに挑戦する日々を綴ります

Shopeeコンペ解法を読んで勉強になったことの雑なまとめ

前置き

  • Shopeeコンペの解法を読んで、勉強になることが多かったので雑にまとめたものです。
  • Shopeeコンペには参加しておらず、エアプなので実際に使うときには色んな工夫が必要だとは思います。
  • 参考資料からほぼ抜粋させていただいたところも多々あります。
    • 問題あれば何なりとおっしゃってください。

参考資料

Solutions

  • 1st / 2nd を中心に読んでまとめています
  • 2ndのコードは穴が開くほど拝見させていただきました
    • が、GCNのあたりはまだ全然理解できてない…orz

ブログ記事やYoutubeなど

  • shimacosさん記事
    • コンペ概要や解法の丁寧な解説に加えて、何故そういう解法を思いついたのかという思考の流れや、工夫してやったけど結局多分効かなかったことも書かれていました
    • この記事読まれたことがない方は、僕の記事なんか閉じてまずはこちらを読まれることを強くお勧めしますw
  • Kaggle ShopeeコンペPrivate LB待機枠&プチ反省会
    • コンペ終了直後に行われた日本人上位陣の方の解法解説のアーカイブ動画
    • 生で聞いてる時は半分もわかりませんでしたが、その後色々調べてから再度聞くと理解が深まりとても勉強になりました
  • asteriamさん記事
    • 上位解法を一通りまとめてくださっています
    • 英日見比べながら上位解法を読むときの理解の助けにさせていただきました

コンペ概要

  • shimacosさんが書かれている記事の内容が非常に簡潔でわかりやすかったので引用させていただきます。
東南アジア最大級のECプラットフォームであるShopeeが開催したもので、データとしてはユーザが登録した商品画像と商品のタイトルが与えられます。

また、ラベルとしてはユーザが登録した商品の種別が与えられています。このラベルは、ユーザが登録したものなので、ノイズが多く載っているものになっており、同じ画像や同じタイトルでも違うラベルがついていたりします。また、この種別というのは思った以上に細かく、同じ化粧品でも50mlのものと100mlのもので違うラベルになっていたりします。

このようなユーザがつけたラベルを教師データとして、画像とタイトルのテキスト情報を用いて商品セットの中から同じ商品を抽出するモデルを作成することが今回のお題となっています。
  • 与えられた画像や文書をNNでうまくベクトル化した後、それを用いて検索を行うコンペだったようですが、1st solution曰く、(embeddingを獲得するための)モデルを究極まで改善することはあまり本質的ではなく、抽出したベクトルをいかに上手く検索に利用するかが肝だったようです。

解法例 (2nd)

  • 1st stage: Train metric learning models to obtain cosine similarities of image, text, and image+text data
    • timm/huggingfaceをベースにベクトル取得
    • それをconcatenate
    • faissでインデックス化、近傍探索
    • query expansion して concat (以下同じ)
      • weightはsimilarityのsqrt (αQEっぽい)
  • 2nd stage: Train “meta” models to classify whether a pair of items belong to the same label group or not.
    • Used LightGBM and GAT (Graph Attention Networks)

勉強になったことの箇条書き

pre-train models の活用

  • 画像ではtimm、NLP: transformersがほぼデファクトスタンダードっぽい。
  • 使い古されてるモデルから最新のモデルまで、古今東西の様々なモデルが手軽に利用できる。
  • まずはこれらのモデルのfine-tuningをどう行うか、を考えるのが常套手段っぽい。
    • 1st, 2nd が共にこの構成だった。
    • 使っているモデルも比較的似通っていた。

timm (Github)

  • 画像系のNNモデルがめちゃくちゃ頻繁に更新されている
  • 上位解法で利用されていたmodelの例
    • 1st
      • eca_nfnet_l1
        • nfnetを軽量化したやつ?
    • 2nd
      • vit_deit_base_distilled_patch16_384
        • 画像のtransformer
      • dm_nfnet_f0
        • batch normalizationを利用しない / 2021/02に登場した新しいやつ
  • inference-code の例
# https://www.kaggle.com/lyakaap/2nd-place-solution

import timm

backbone = timm.create_model(
    model_name='vit_deit_base_distilled_patch16_384',
    pretrained=False)
model1 = ShopeeNet(
    backbone, num_classes=0, fc_dim=768)
model1 = model1.to('cuda')
model1.load_state_dict(checkpoint1['model'], strict=False)
model1.train(False)
model1.p = 6.0

huggingface (ref)

  • NLP界の超有名ライブラリ
  • AutoModelという機構(?)を使えば読込む事前学習モデルのパスを変えるだけで使いまわせる。
    • めちゃくちゃ便利そうなのに今まで全然知らんかった…
  • マイナー言語や多言語モデルも多数存在
    • 今回のECサイトはインドネシア語だったので、インドネシア語のBERTが強かったとのこと。
  • models
    • 1st
      • xlm-roberta-large
      • xlm-roberta-base
      • cahya/bert-base-indonesian-1.5G
      • indobenchmark/indobert-large-p1
      • bert-base-multilingual-uncased
    • 2nd
      • cahya/bert-base-indonesian-522M
      • Multilingual-BERT (huggingfaceのモデル名調べてない)
      • Paraphrase-XLM embeddings (同上)
  • inference-code の例
# https://www.kaggle.com/lyakaap/2nd-place-solution

from transformers import AutoTokenizer, AutoModel, AutoConfig

model_name = params_bert2['model_name']
tokenizer = AutoTokenizer.from_pretrained('../input/bertmultilingual/')
bert_config = AutoConfig.from_pretrained('../input/bertmultilingual/')
bert_model = AutoModel.from_config(bert_config)
model2 = BertNet(
    bert_model, num_classes=0, tokenizer=tokenizer, 
    max_len=params_bert['max_len'], simple_mean=False,
    fc_dim=params_bert['fc_dim'], s=params_bert['s'],
    margin=params_bert['margin'], loss=params_bert['loss']
)
model2 = model2.to('cuda')
model2.load_state_dict(checkpoint2['model'], strict=False)
model2.train(False)

深層距離学習

  • 概要はyu4uさんの記事に詳しい
    • 以下の点が嬉しいとのこと
      • 通常のクラス分類問題を学習させるだけで距離学習が実現できる
      • 学習が容易なクラス分類モデルに1層独自のレイヤを追加するだけで、通常のクラス分類問題として学習が可能、ロスもcross entropyのままで良い。
    • 上位のチームは大体使ってそう
  • チューニングに手こずることも
    • 1stのチームはArcFaceのチューニングに相当手こずったそうで、以下の工夫をしたとのこと。
      • increase margin gradually while training
      • use large warmup steps
      • use larger learning rate for cosinehead
      • use gradient clipping
    • 4thのチーム
      • we also saw batch size to matter during training
      • Some models seemed to be very sensitive to the learning rate
      • gradient clipping may have also helped to stabilize the training.
    • shimacosさん
      • 序盤はなかなか学習が進まなかったりしてパラメータの調整が難しかった
      • 学習率を大きくし、warmupを大きめに行うことで学習が進みやすくなった
      • 学習の序盤だと普通のsoftmaxよりもクラス間の予測値の差が顕著に出ないため、学習が難しいのではないか
  • 2ndはCurricularFaceを利用
    • 多くのチームが使っていたArcFaceを超える性能だったとのこと
    • 学習ステージに応じて、イージーサンプルとハードサンプルの相対的な重要性を自動調整?
      • ArcFace に学習サンプルを賢く選ぶような機能をつけたイメージ?
  • 1st: class-size-adaptive margin もある程度は使えたのこと

QueryExpansion / DataBase-side feature Augmentation

  • IRにおいてクエリ及びDBを拡張するための手法
  • QueryExpansion: 検索した結果を元にクエリをどんどん拡張していく
    • 元々のベクトルで検索
    • 検索で引っかかったアイテムのベクトルを重み付けして元のベクトルに加算
# https://www.kaggle.com/lyakaap/2nd-place-solution

def query_expansion(feats, sims, topk_idx, alpha=0.5, k=2):
    # 引っかかった似ているアイテムへのウェイトを決める式(論文)
    weights = np.expand_dims(sims[:, :k] ** alpha, axis=-1).astype(np.float32)
    # ウェイトに応じてベクトルを加算して新しいベクトルを求める
    feats = (feats[topk_idx[:, :k]] * weights).sum(axis=1)
    return feats

# img_D / img_I は一段目の検索で引っかかった画像のベクトル、インデックス
img_feats_qe = query_expansion(img_feats, img_D, img_I)
  • DBA(DataBase-side feature Augmentation)
    • lyakaapさんのmemoより
      • データベースのサンプルを、そのサンプルに対する近傍のdescriptorによる重み付き平均を取ることでrefineする。
      • QEと似ている。QEはquery側をrefineするけどDBAはDB側をrefineするイメージ。そのためDBAはオフラインで一回やるだけで良くて、query searchのときには速度に影響しないのが強み。
      • 何故効くのか? → descriptorがよりクラス中心に近づくから。DBAは同一クラス同士(近傍のサンプルは同一クラスに大体属しているという仮定)でよりクラス中心に引きつけ合うようなことをしている。

embeddingをアンサンブルするときの工夫

* 一番良かったのは、各種EmbeddingをそれぞれL2 Normalizeしてからconcatするという方法
* モデルによってEmbeddingのスケールが違うので当たり前と言えば当たり前ですが、L2 normalizeせずにconcatしてしまうとそこまで改善が得られませんでした。
* このような細かい技術は、過去コンペの解法でもしれっと書かれているだけなので覚えておくと良いかもしれません。

faiss

  • Facebook Resarchが提供する近傍探索ライブラリ (Github)
  • faissはGPUをフル活用して検索を高速化することもできるそうなので、このコンペとの相性が良かったのだろうか。
  • 日本語のキャッチーな記事
    • メルカリ社でも使われてるとのこと、ふーん。
  • ベクトルの追加とそれを用いた検索のコード例
# https://www.kaggle.com/lyakaap/2nd-place-solution

import faiss

res = faiss.StandardGpuResources()
index_img = faiss.IndexFlatIP(params1['fc_dim'] + params2['fc_dim'])
index_img = faiss.index_cpu_to_gpu(res, 0, index_img)
index_img.add(img_feats)
similarities_img, indexes_img = index_img.search(img_feats, k)

Forest Inference

  • GPUでGBMの推論を爆速にしてくれるやつ
  • RAPIDS公式
    • Using FIL (Forest Inference Library), a single V100 GPU can deliver up to 35x more inference throughput than a CPU-only node with 40 cores.
# 多分こんな感じで使う

import treelite
from cuml import ForestInference

clf = ForestInference()
clf.load_from_treelite_model(
    treelite.Model.load(
        '/tmp/tmp.lgb',
        model_format='lightgbm'
    )
)
clf.predict(X_test).get()

その他細かな点

Generalized Mean (GeM) Pooling

tokenizer

  • TweetTokenizer
    • カジュアルな文章のtokenizeに向いている?

NVIDIA DALI

LightGBM特徴量 (2nd)

  • 各商品のTOP50の組み合わせに対して類似度や編集距離を付与 → 同じカテゴリだったかどうかを予測するように学習
    • 特徴量
      • 商品同士の類似度
      • 編集距離
      • 各商品のタイトルの長さ、ワード数
      • 各商品のtop-N類似商品のsimilarityの平均
        • (これ効くのどういうお気持ちなんだろう)
      • 各商品の画像サイズ

punctuationの前処理

  • 複数の文字(長さ1の文字列)を指定して置換する場合は文字列(str型)のtranslate()メソッドを使う。translate()に指定する変換テーブルはstr.maketrans()関数で作成する。
title.translate(str.maketrans({_: ' ' for _ in string.punctuation}))
  • string: https://docs.python.org/ja/3/library/string.html
    • string.punctuation: String of ASCII characters which are considered punctuation characters in the C locale
  • TfidfVectorizer: token_pattern=u'(?u)\\b\\w+\\b' とかやると一文字のトークンを除外しなくなる

編集距離を一撃で出すライブラリ

  • 色々あるらしい
    • editdistance
    • Levenshtein
# https://github.com/roy-ht/editdistance

import editdistance
editdistance.eval('banana', 'bahama') ## 2

# https://qiita.com/inouet/items/709eca4d8172fec85c31
import Levenshtein

string1 = "井上泰治"
string2 = "井上泰次"

string1 = string1.decode('utf-8')
string2 = string2.decode('utf-8')

print Levenshtein.distance(string1, string2)

stemmer

import Stemmer

stemmer = Stemmer.Stemmer('indonesian')

LangID

言語を特定してくれる

import langid

result = langid.classify('これは日本語です')
print(result)  # => ('ja', -197.7628321647644)