Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Slackの絵文字サジェストを機械学習でリバースエンジニアリング

 Slackの絵文字サジェストを機械学習でリバースエンジニアリング

Keisuke OGAKI

July 08, 2022
Tweet

More Decks by Keisuke OGAKI

Other Decks in Programming

Transcript

  1. Slackの絵文字サジェストを機械
    学習でリバースエンジニアリング

    View Slide

  2. 機械学習って、人間のつけたものだったり、どっ
    かのデータベースにあったり、 “ラベル”を学習す
    るというイメージを持つことが多いけれど、 どこか
    に実装があるアルゴリズム そのものを学習するこ
    とって... もちろんできらぁ
    def function(query, target):
    ……
    リバースエンジニアリング
    人間の模倣
    法則の学習
    港区

    View Slide

  3. minorと打つと、badminton_racquet_and_shuttlecockがサ
    ジェストされる
    slackの絵文字アルゴリズムって意外と不思議な挙
    動をする

    View Slide

  4. slackの絵文字サジェストのアルゴリズムを学習してみよう

    View Slide

  5. 問題設計

    View Slide

  6. 学習データ作成
    length = 3
    candidate_characters = string.ascii_lowercase + string.digits + ' '
    for query_characters in itertools.combinations_with_replacement(candidate_characters, length):
    query = ''.join(query_characters)
    editor = browser.find_element(By.CLASS_NAME, 'ql-editor')
    editor.send_keys(query)
    l = browser.find_element_by_id('chat_input_tab_ui')
    candidates = l.find_elements_by_tag_name('li')
    result = [candidate.text for candidate in candidates]
    f.write(json.dumps(dict(key=query, result=result))+'\n')
    3文字までの英数字の組み合わせの全通り
    seleniumで直接入力し、サジェスト結果をそのまま保存
    {'aaa': ['kaaba'],
    'aab': ['kaaba', 'tanabata_tree'],
    'aac': ['abacus', 'parachute'],
    'aad': ['green_salad'],
    'aae': [],
    'aaf': ['falafel'],
    'aag': [],

    View Slide

  7. アルゴリズムの一致度の評価はどうやる?
    query -> answerが1:1ではないので、[email protected],
    [email protected]ではなく、平均順位を評価指標とする
    {'aaa': ['kaaba'],
    'aab': ['kaaba', 'tanabata_tree'],
    'aac': ['abacus', 'parachute'],
    'aad': ['green_salad'],
    'aae': [],
    'aaf': ['falafel'],
    'aag': [],
    mean(1,2,3,5) = 2.75
    ● 最小値は2.5なので、0.25分間違えてる
    ○ oboに対してtoolboxが不正解なの
    なんで...謎アルゴリズム
    これを評価セットのkey全てで順位を取る
    (最小値)

    View Slide

  8. 一旦それっぽいアルゴリズムを考えてみる
    理論最小値: 7.70
    def distance(query, target):
    if not set(query).issubset(target):
    return 99999
    else:
    return target.find(query[0])
    文字列がサブセットになっているか
    59.65
    マッチ部の最小長さ+出現位置
    def distance2(query, target):
    if not set(query).issubset(target):
    return 9999999999
    else:
    pattern = '.*?'.join(query)
    matches = re.findall(pattern,
    target)
    if not matches:
    return 999999999
    else:
    return min(len(found) for
    found in
    matches)*100+target.find(query[0])
    17.29
    def distance4(query, target):
    if not set(query).issubset(target):
    return 9999999999
    else:
    pattern = '.*?'.join(query)
    matches = re.findall(pattern,
    target)
    if not matches:
    return 999999999
    else:
    return min(len(found) for
    found in
    matches)*1000+len(target)*10+target.find(
    matches[0])
    マッチ部の最小長さ+文字列長+出現
    位置
    14.94

    View Slide

  9. 識別器編

    View Slide

  10. 学習できる形に落とす
    やりたいこと: actualがTrueのものを上位に集める
    “obo/snowboarder”
    “obo/horse”
    9.998831e-01
    7.690672e-12
    なるべく1に近いスコア な
    るべく0に近いスコア
    になるように学習
    モデル
    (LSTM, Transformer)
    ● key/queryと、区切り文字を適当にきめて文字列で入れることで二変数・三変数関数を雑に表現できるの
    はNLPのおもしろいところ
    ○ もちろん別々にエンコードして明確に
    2変数にしてもいいですが
    ● 今回は1/0を当てる問題にしているが、ランキング学習をするという手もある
    ○ snowboarderはhorseより順位が上である、というのを学習させる
    ○ 特に上位の並びを当てるのはこちらの方がよくなるはず

    View Slide

  11. 結果: SoTA達成、勝利
    [ヒューリスティック]
    マッチ部の最小長さ+文字列長+出現位置
    14.94 (+7.24)
    [学習] LSTM, 2layer, 100D 10.49 (+2.79)
    理論最小値 7.70

    View Slide

  12. どういうものを間違える?
    modelワースト
    “7”のモデル予測値
    学習の方が大外ししない
    意味的に似てるもの”8”を捉え
    ちゃってる!なんで!!

    View Slide

  13. さらなる高みへ: 速度改善
    keyが変わるごとに、全てのcandidateと
    の組み合わせを実行しなきゃ行けない、
    手元のMacだと1891絵文字の探索で平
    均1秒、線形なので会社の全絵文字
    10000だと5,6秒かかりそう
    “obo/snowboarder”
    “obo/horse”
    9.998831e-01
    7.690672e-12
    モデル
    (LSTM, Transformer)
    for key in tqdm(keys_test):
    for candidate in more_itertools.chunked(candidates, n=100):
    xs = key + '/' + candidate
    pred = torch.nn.Sigmoid()(model(xs)).to('cpu').numpy()

    View Slide

  14. さらなる高みへ: 速度改善
    “obo”
    “horse”
    9.998831e-01
    7.690672e-12
    QueryEncoder
    モデル
    (LSTM, Transformer)
    TargetEncoder
    モデル
    (LSTM, Transformer)
    “snowboarder”
    内積
    なるべく1に近いスコア な
    るべく0に近いスコア
    になるように学習
    target_feature = model.model2(candidates).to('cpu').numpy() #事前計算
    query_feature = model.model1(keys_test).to('cpu').numpy()
    score=query_feature.dot(target_feature.T)
    ● targetは事前にエンコードしておけるので、検索時にはクエリのエンコー
    ドと行列積1回: 平均0.018s
    ● エンコード以外はベクトルの内積だけなので、例えば ESなど汎用的な
    検索エンジンを利用可能
    for文が消える

    View Slide

  15. “obo”
    “horse”
    9.998831e-01
    7.690672e-12
    QueryEncoder
    モデル
    (LSTM, Transformer)
    TargetEncoder
    モデル
    (LSTM, Transformer)
    “snowboarder”
    内積

    View Slide

  16. 結果
    平均順位 時間
    [ヒューリスティック]
    マッチ部の最小長さ+文字列長+
    出現位置
    14.94 (+7.24) 0.008s
    [学習] TwinModel 46.01(+) 0.018s
    [学習] LSTM, 2layer, 100D 10.49 (+2.79) 1s
    理論最小値 7.70

    View Slide

  17. どうして難しいのか?
    obo/snowboarder
    o.*?b.*?o
    match: owbo
    obo
    o.*?b.*?o ????
    snowboarder
    難1: 内積で表現できる形
    式にエンコード
    難2: どんなクエリにも対
    応可能なエンコード
    (例えば)1層目でクエリを
    組み立てて2層目でマッチ
    すればOK

    View Slide

  18. まとめ
    ● 機械学習でリバースエンジニアリングはできる
    ● 頑張れば速度も実用レベルまで詰められる
    ● 今回は速度と精度がトレードオフだったけど、もう少し頑張れば両取りもできるかな
    ○ もう少し学習は進められそう
    ○ Twinモデルで上位100個に絞ってから結合モデルで for loopというのも定石

    View Slide