pytestメモ

これは?

単体テストやりたいよね〜 pytestの書き方メモっとこうか

pytestの準備

インストール

pip install pytest

toyコード作成

ディレクトリ構成

pytest_work/
  data/
    out.csv
  src/
    work.py
  tests/
    test_sample.py

work.py

# -*- coding: utf-8 -*-
"""
DFを二つ作り、結合してその件数をチェックする処理をpytestで実施してみる。

"""
import pandas as pd
from pathlib import Path

def main():
    a = pd.DataFrame(
        {
            'a': [1, 2, 3],
            'b': [12, 13, 14]
         }
    )
    b = pd.DataFrame(
        {
            'a': [3, 4, 5],
            'c': ['a', 'b', 'c']
        }
    )
    c = pd.merge(a, b, on='a')
    out_file = Path.cwd().parent / "data" / "out.csv"
    c.to_csv(out_file, index=False)


if __name__ == '__main__':
    main()

work.pyを実行すると、DFのaとbの'a'というカラムの重複分がDFとして作成されて、data/out.csvとして出力される。
これに対し、tests/test_sample.pyでpytestを実施するイメージでやってみる。

テスト

test_sample.py

# -*- coding: utf-8 -*-
import pytest
import pandas as pd
from pathlib import Path


class TestSample:
    def test_return(self):
        x = 'test'
        assert x.find('st') != -1

    def test_uniq(self):
        target_file = Path.cwd().parent / "data" / "out.csv"
        x = pd.read_csv(target_file)
        assert len(x) != 0


if __name__ == '__main__':
    a = TestSample()
    a.test_uniq()

classにTestから始まる名称を作成し、その中の関数にtest_で始まるように定義していく。
test_return関数は、固定の変数に対するテストでtest_uniqがout.csvが0件でなければエラーとなるように記載している。

この状態で、tests/に移動して以下を実行するとテストができる。

(pytest-work-P7cVJe5b-py3.9) bash-3.2$ pytest test_sample.py
========================================================== test session starts ==========================================================
platform darwin -- Python 3.9.5, pytest-7.2.1, pluggy-1.0.0
rootdir: /Users/shirai_y/work/tmp/pytest_work/tests
collected 2 items

test_sample.py ..                                                                                                                 [100%]

=========================================================== 2 passed in 0.57s ===========================================================

これは、2つのテストをpassしたよってこと。

特定のテストだけ走らせたい場合は以下のようにすると良い。

(pytest-work-P7cVJe5b-py3.9) bash-3.2$ pytest test_sample.py::TestSample::test_uniq
========================================================== test session starts ==========================================================
platform darwin -- Python 3.9.5, pytest-7.2.1, pluggy-1.0.0
rootdir: /Users/shirai_y/work/tmp/pytest_work/tests
collected 1 item

test_sample.py .                                                                                                                  [100%]

=========================================================== 1 passed in 0.54s ===========================================================

テストファイル名の後に「::」でクラス名やその後に「::」で関数名を記載すると、指定したテストだけが実施できる。

便利な機能

assertの第二引数にメッセージを書くと、エラーの内容がテスト時に出力される。

test_sample.py

# -*- coding: utf-8 -*-
import pytest
import pandas as pd
from pathlib import Path


class TestSample:
    def test_return(self):
        x = 'test'
        assert x.find('st') != -1

    def test_uniq(self):
        target_file = Path.cwd().parent / "data" / "out.csv"
        x = pd.read_csv(target_file)
        assert len(x) != 0, "重複しているレコードが無いよ!"  # こんな感じでエラーの内容を記載。


if __name__ == '__main__':
    a = TestSample()
    a.test_uniq()

pytest実行。

(pytest-work-P7cVJe5b-py3.9) bash-3.2$ pytest test_sample.py
========================================================== test session starts ==========================================================
platform darwin -- Python 3.9.5, pytest-7.2.1, pluggy-1.0.0
rootdir: /Users/shirai_y/work/tmp/pytest_work/tests
collected 2 items

test_sample.py .F                                                                                                                 [100%]

=============================================================== FAILURES ================================================================
_________________________________________________________ TestSample.test_uniq __________________________________________________________

self = <test_sample.TestSample object at 0x11c90f7f0>

    def test_uniq(self):
        target_file = Path.cwd().parent / "data" / "out.csv"
        x = pd.read_csv(target_file)
>       assert len(x) != 0, "重複しているレコードが無いよ!"
E       AssertionError: 重複しているレコードが無いよ!
E       assert 0 != 0
E        +  where 0 = len(Empty DataFrame\nColumns: [a, b, c]\nIndex: [])

test_sample.py:15: AssertionError
======================================================== short test summary info ========================================================
FAILED test_sample.py::TestSample::test_uniq - AssertionError: 重複しているレコードが無いよ!
====================================================== 1 failed, 1 passed in 0.42s ======================================================

エラーが出たときに何のエラーなのかがわかりやすくて良き。

fixture

conftest.pyにテストで共通で使うような関数を記述しておくと、これをpytestの実体コード上で扱うことができる。

fixture.py

# -*- coding: utf-8 -*-
import pytest
import pandas as pd
from pathlib import Path


@pytest.fixture(scope="class")
def df_load_output():
    return pd.read_csv(Path.cwd().parent / "data" / "out.csv")

test_sample.py

# -*- coding: utf-8 -*-
import pytest


class TestSample:
    def test_return(self):
        x = 'test'
        assert x.find('st') != -1

    def test_uniq(self, df_load_output):  # 引数にtextureで指定した関数名を追加して、
        x = df_load_output  # その名称で使うとfixtureの処理が実行できる。
        assert len(x) != 0, "重複しているレコードが無いよ!"


if __name__ == '__main__':
    a = TestSample()
    a.test_uniq()

重複チェック以外にも同じファイルに対する処理をする場合に、毎回load処理を記述しなくても良いのが良いね。
あとは、DBコネクションとか共通系の処理はfixtureに書いてあげると良いみたい。

lightGBMの学習データのweight指定を試してみる

これなーに

正解率が偏らないように、lightGBMのweightを指定したくなった。

以下を参考に実施してみる。
qiita.com

irisのデータで試す。notebookべたばりですいません。。

ここまでがノーマル。クラス2が苦手であることがわかる。
そこで、クラス2だけweightを上げてみる。

狙い通りクラス2上がったけど、クラス1も上がってるのはなんで??

注意が必要なのは、weightを付与するためにclassラベル付けると、それを削除しないとカンニングになる。
weight用のカラムもちゃんと削除して学習/予測しないとね。

しかし、validデータにweightを付与するのはなぜだろう??
学習時のパラメータ更新のためのものだと思うんだけど。。

kaggleで勝つデータ分析の技術 読了

これなーに

www.amazon.co.jp

上記書籍を読んだ。
面白かったポイントを列挙しておく。

不均衡データの評価指標はPR-AUCが良さそう

正例が1%、負例が99%のような2値分類におけるROC-AUCだと、True-Negative(負例を負として当てた)が多くなって正例が正しく当てられなくても数値が大きくなってしまう。
一方PR-AUCだとTrue-Positive(正例を正として当てた)を重視するPrecisionとRecallでの評価になるので、1%の発生をどれだけ的確に当てられたかの評価になるので良さそう。

PR-CURVE

TP/FN/TN/FP(いつも忘れる・・・)

PrecisionとRecall

この辺も読んでてわかりやすかった。
不均衡データとの向き合い方 - Qiita

GBDTに変数のスケール変換は不要

変数のスケール(変数の値が取りうる範囲)や分布をあまり気にしなくて良く、また欠損値やカテゴリ変数を扱いやすいことが、GBDTがよく使われている理由の1つです。

ですな〜 分岐が大小関係で行われるから、対数取ったりは特にしなくてよいのは楽だよね。

特徴量作成について

モデルに現在与えられている入力から読み取れない・読み取りづらい情報を追加で与えるというのが、特徴量を作るイメージになります。

「決定木の気持ちになって考える。」って言葉良いね。

ユーザの行動に大きく影響している要素が、購入額を購入数で除した平均購入単価だったとします。このとき、購入額と購入数のみが特徴量として入っていると、GBDTは購入額と購入数の相互作用として平均購入単価をある程度までは反映しますが、明示的に特徴量として加えた方がより適切に反映します。

すなわち、例えば購入金額と購入回数のカラムがあった場合、この二つを特徴量として利用すると、木の分岐で「購入平均単価」みたいな意味合いが一応表現はできるけど、直接的に購入金額/購入回数で新しい特徴量を作った方が良いわけね。
確かに木の分岐回数の最大値を超えちゃうと表現できなかったりするしね。

GBDTでは欠損値をそのまま使った方が良い

埋めたほうが良い場合もあるけど、そのままで良いみたい。欠損は欠損として扱う。

欠損値の埋め方

上と矛盾するけど埋め方の考え方でほ〜ってなった。

また、平均のとり方も、単純に全データの平均ではなく、別のカテゴリ変数の値でグループ分けし、そのグループごとの平均を代入する方法も考えられます。これは、欠損している変数の分布がグループごとに大きく変わることが想定される場合に有効です。カテゴリ変数の値ごとに平均をとる際に、データ数が極端に少ないカテゴリが存在する場合、その平均値にはあまり信用がおけませんし、そのカテゴリの値はすべて欠損しているかもしれません。そのような場合、以下の算式のように分子と分母に定数項を足して計算させるBayesianaverageという方法が

確かに、グループで大きく分かれる特徴量についてはこういうのが良いかも。
例えば、仕事の給与とかだと業界や職種で結構差がありそうで、そういうグループでの平均などで欠損を埋めてもいいかも。
欠損値そのまま利用で良いってあるけど、特徴量の重要度が高いなら埋め方を検討してみるのもいいかもね。

カテゴリ変数の場合は、欠損値を1つのカテゴリとみなして欠損を表すカテゴリを新たに作り置き換える方法や

カテゴリ変数の欠損は、欠損カテゴリにしても良いかもですって。

年齢や「最初に予約した日と年齢や「最初に予約した日とアカウント作成日の差をbinningしカテゴリ変数としたもの」などの重要と思われる特徴量について、予測による補完が行われています

なるほど〜 やっぱり重要な特徴量が欠損している場合は、予測してでも埋めた方が良いこともあるのね。

欠損として扱うかどうかを考える

最初の段階で変数の分布をヒストグラムなどで見て、欠損として認識すべき値がないかを確認しておくことが望ましいでしょう。

これは確かに大切そう。欠損扱いでルールで一定の値を入れている場合のデータがあるから、それを欠損とみなすのを発見しとくといいよねってことね。

binning

データに対する前提知識があり、どのような区間に分けるべきかの見当がついているとより有効です。

例えば、年齢を~18/19~22/23~とかの高校生/大学生/社会人とかにしとくと意味あるね的なのが分かっていたら、それを特徴量にしても良いってことか。

Kaggleの「CouponPurchasePrediction」の筆者(T)のソリューションでは、食事のクーポンの単価を1,500円以下、1,500円~3,000円、3,000円以上にbinningし、そのあとそれらの区間ごとに他の変数の値の集計を行っていました。同じ食事でも金額の範囲ごとに利用目的が異なることを反映しています。

具体例。

target encoding

カテゴリ変数を数値に変える方法。
CVのk-foldで自分のデータがないfoldで目的変数の確率を平均化して使う。

xfeafってのを使うと簡単にできるらしい。

from sklearn.model_selection import KFold

fold = KFold(n_splits=5, shuffle=False)
encoder = TargetEncoder(
    input_cols=["Cabin"], 
    target_col="Survived",
    fold=fold,
    output_suffix="_re"
    )

encoded_df = encoder.fit_transform(train_df)
encoded_df[["Survived", "Cabin", "Cabin_re"]].head(3)

acro-engineer.hatenablog.com

xfeat色々できて楽そう。さすがPFN...
Python: xfeat を使った特徴量エンジニアリング - CUBE SUGAR CONTAINER

レコード間の関係性に注目

一方で、レコード間に一部強い関係性が見られるデータもあります。分かりやすい例としては、同じユーザのレコードが複数ある場合が考えられます。
数個のレコードの関係性に注目することも、レコード全体としてどういうパターンがあるかに注目することもできます。例えば、上記の同じユーザのレコードが複数ある例で、出現している回数が違うことが何らかの性質を表していると考えて、ユーザごとのレコード数をカウントすることができます。

なるほど!全体でカウント取ってそれを特徴量とするのね。確かに熱度みたいな意味合いとかになるかも。

あるユーザの値とそのユーザの属するグループの平均値との差や比率をとるなど、他と比較したときの差や比率といった相対値に注目するのも有効です。

これもいいね。平均からの差で偏差値的なものが見えてくるかも。

トピックモデルの応用によるカテゴリ変数の変換

トピックモデルという文書分類の手法を応用して、他のカテゴリ変数との共起の情報から、カテゴリ変数を数値ベクトルに変換する手法があります。2つのカテゴリ変数の片方を文書、もう片方を単語とみなすと、各文書に各単語が何回現れたかという共起の情報から単語文書のカウント行列を作ることができます。これに対しLDA(latentDirichletallocation)を適用すると、前者の変数を文書が属するトピックに対する確率を表す数値ベクトルに変換できます。

なるほど、ちゃんと理解してない・・・。

Negative down sampling

これ良さそう!
正例1%、負例99%のようなデータなら、負例1%をランダムサンプリングして作っちゃえば良いじゃん。
そして負例のサンプリングはランダムに何回もできて、それらのモデルでバギング。

ユーザーの行動から特徴量作成

想像力が必要ですが、極めて有効な特徴量を抽出できることがあります。このコンペでは、以下のような特徴量などが作成されました。
1.ユーザの「真面目さ」を表現する:訪問日数や動画視聴数など、ユーザの「真面目さ」を表現する特徴量
2.ユーザの学習の進捗を表現する:アクセスログから、ユーザの進捗度合、平均的な進捗度合とのずれが算出できるため、それらを特徴量とする

こういうの大切だよね。

GBDTについて

label-encodingは必要だけど、one-hot encodingは不要。

例えばあるカテゴリ変数cが1から10まであるときに、cが5のときのみ効く特徴だった場合に、決定木の分岐を(c<5,5<=c)と(c<=5,5<c)と重ねることで、cが5であるという特徴が抽出されるためです。

なるほど・・・。

catboost

targetencodingを行い、数値に変換します。targetencodingは使い方を誤ると目的変数の情報を不適切に使ってしまうため、ランダムにデータを並べ変えながら適用するなどの工夫がされています。

マジか・・・

過学習を防ぐテクニック

データが少なすぎる葉を構成しない

決定木の深さの制限

adversarial validation

学習データとテストデータを結合し、テストデータか否かを目的変数とする二値分類を行うことで、学習データとテストデータの分布が同じかどうかを判断する手法があります。同じ分布であればそれらの見分けはできないので、その二値分類でのAUCは0.5に近くなります。一方、AUCが1に近くなった場合は、それらをほぼ確実に見分けられる情報があることになります。上記の二値分類でAUCが0.5を十分上回るような、学習データとテストデータが違う分布の場合を考えます。このとき、「テストデータらしい」学習データをバリデーションデータとすることで、テストデータを良く模したデータでの評価を期待できます。

ほ〜〜〜ほ〜〜〜って感じ。なるほど、今の実務では使わなさそうだけど、覚えておきたいテクニックかも。

ハイパラチューニング

1.以下のパラメータを初期値に設定する
・eta:0.1or0.05(データ量に依存する)
・max_depth:最初にチューニングするので決めない
・colsample_bytree:1.0
・colsample_bylevel:0.3
・subsample:0.9
・gamma:0
・lambda:1
・alpha:0
・min_child_weight:1
2.depthの最適化
・5~8ぐらいを試す。さらに浅いor深い方が改善しそうなら広げる
3.colsample_levelの最適化
・0.5~0.1を0.1刻みで試す
4.min_child_weightの最適化
・1,2,4,8,16,32,...と2倍ごとに試す
5.lambda,alphaの最適化
・両者のバランスなのでいろいろ試す(初期ではやらないこともある)

ありがテェ・・・・

feature importance

ここで、(Pythonの)デフォルトでは頻度が出力されますが、ゲインを出力した方が良いでしょう。ゲインの方が、特徴量が重要かどうかをより表現していると考えられるためです。

デフォルトは頻度らしいけど、ゲインは目的変数の精度への寄与なので、ゲイン指定で見ること。

以上! 面白いなぁ

kaggleでの表データテクニック調べていくよ〜

今更だけどkaggleの表データのチップスみたいなの調べていく。

完全にメモの殴り書きになる感じがするけど、とりあえずやってみよう。

気になるのは以下あたり。 この中からピックアップしつつ調べていこう。

https://www.kaggle.com/code/gpreda/santander-eda-and-prediction

https://www.slideshare.net/mlm_kansai/kaggle-138546659

http://kaggler-ja-wiki.herokuapp.com/%E3%81%AA%E3%82%93%E3%81%A7%E3%82%82kaggle%E9%96%A2%E9%80%A3%E3%83%AA%E3%83%B3%E3%82%AF

qiita.com

気になるあれこれ target encoding OOF ENSEMBLE

https://www.kaggle.com/code/cdeotte/forward-selection-oof-ensemble-0-942-private/notebook

RFM分析という顧客分析の手法を用いてユーザの分類や特徴量を作成する(Recency:最新購入日、Frequency:購入頻度、Monetary:購入金額) 門脇 大輔,阪田 隆司,保坂 桂佑,平松 雄司. Kaggleで勝つデータ分析の技術 (Japanese Edition) (p.337). Kindle 版.

lightGBMの初期パラーメータはあんまり良くない。

https://alphaimpact.co.jp/downloads/pydata20190927.pdf

こういうのもっと見つけて手を動かしてみたい https://www.kaggle.com/code/tushiro/03-cdle

時系列データのcloss validation

カテゴリ変数のラベルエンコーディングについて https://qiita.com/sinchir0/items/b038757e578b790ec96a

カテゴリ変数の扱い

時間のデータの特徴量作成

GCPチップス

これなーに?

GCPを久しぶりに使うからメモしていく。

プロジェクト作成

VPCの作成

ダッシュボード→サイドバー→VPCネットワークを選択。

VPCネットワークを作成を押す。

ファイヤウォールの作成

サイドバー→ファイヤウォール。 ファイアウォールルールの作成を選択。

GCEを立てる

サイドバー→Compute Engine→VMインスタンスインスタンスの作成を選択。

※ 最後の方の自動再起動はオフ。

sshアクセス

gcloupでのアクセスを実施。

gcloud compute ssh --zone "asia-northeast1-b" "baseball-instance"  --project "baseball-364204"

上記実行時、gcloud用のsshのキーが自動で作成される。これを使ってログインができる。

dockerインストール

sudo apt-get update
sudo apt-get install \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

## ここまででdockerインストール完了。
sudo docker info

# user nameは変えること。rootじゃなくてもdockerコマンドが打てるようにする。実行後はexitで再ログインが必要。
sudo usermod -a -G docker [user name]

dockerで簡易動作

mkdir test
cd test

# index.htmlを作成
vi index.html
# 適当に文字を入れる

vi Dockerfile
FROM nginx
COPY ./hello-world.html /usr/share/nginx/html/

docker build -t hello-world-docker .

http://外部IPアドレス/index.htmlでアクセスし、アクセスできたらOK。

GCS

サイドバー→クラウドストレージ→バケットを作成を選択。 バケット名を指定して作成。

GCEからGCSのファイルを確認。

gsutil ls gs://baseball-bucket1/servent_data/

バケット作成

gsutil mb gs://uoiuweoiuouaiuoifkjsal/

IAMロールにcloud strageの管理者をつける必要がある。インスタンスAPI許可を全て付与した。

Big Query

プロジェクトの右の3点をクリックして、データセットを作成を選択。 データセット名を選択し、テーブルを作成を選択。

スキーマ:自動検出をすると、カラム名がheaderで作成される。

データポータル

https://marketingplatform.google.com/intl/ja/about/data-studio/ から利用。

グラフを追加から折れ線グラフなどを追加できる。 ディメンションが横軸。指標が縦軸。内訳ディメンションがグループ化項目。 並び替えは横軸。日付で並べる場合は昇順で。

gitでプルリクしたけど特定のコミットに戻したい!

結論

# 特定のコミットに戻りたいとき
git checkout commit-id

# 最新に戻ってきたいとき
git checkout master

以下だらだらワーク

適当に以下のようなpushをした。

git log
commit 7427ae19e41593e15f8030c8fe285fde8577c2b9 (HEAD -> master, origin/master)
Date:   Fri Jul 22 11:48:55 2022 +0900

    [add] 4回目のpush

commit 232a26ad2859c003686098eb21e38dd77da340ee
Date:   Fri Jul 22 11:48:21 2022 +0900

    [add] 3回目のpush

commit 42c07ae5ded7030c7d7e217b928569c78eba416e
Date:   Fri Jul 22 11:47:20 2022 +0900

    [add] commitに戻すテスト追加

commit 9435a5cd68a692456a30ed3ce9d2a5d608fe97fb
Date:   Fri Jul 22 11:44:12 2022 +0900

    first commit

次にブランチを切った。

bash-3.2$ git co -b feature_1
Switched to a new branch 'feature_1'
bash-3.2$ git br
* feature_1
  master

ブランチで以下を実施。

touch add_5.txt
bash-3.2$ git add add
add_3.txt  add_4.txt  add_5.txt
bash-3.2$ git add add_5.txt && git commit -m '[add] ブランチでの追加1' && git push origin feature_1

これで、masterよりorigin feature_1はadd_5.txtだけ追加されている状態。

次にプルリク出してマージした。 マージした際、ブランチは削除した。

ローカルでmasterに変更した。

bash-3.2$ git log
commit 7427ae19e41593e15f8030c8fe285fde8577c2b9 (HEAD -> master, origin/master)
Date:   Fri Jul 22 11:48:55 2022 +0900

    [add] 4回目のpush

commit 232a26ad2859c003686098eb21e38dd77da340ee
Date:   Fri Jul 22 11:48:21 2022 +0900

    [add] 3回目のpush

commit 42c07ae5ded7030c7d7e217b928569c78eba416e
Date:   Fri Jul 22 11:47:20 2022 +0900

    [add] commitに戻すテスト追加

commit 9435a5cd68a692456a30ed3ce9d2a5d608fe97fb
Date:   Fri Jul 22 11:44:12 2022 +0900

    first commit

まだブランチの変更は反映されてない。

masterを最新化

git pull

git log
bash-3.2$ git log
commit 85c0537361a036865c3fa5ff6b0864d859a516fd (HEAD -> master, origin/master, origin/feature_1, feature_1)
Date:   Fri Jul 22 11:53:26 2022 +0900

    [add] ブランチでの追加1

commit 7427ae19e41593e15f8030c8fe285fde8577c2b9
Date:   Fri Jul 22 11:48:55 2022 +0900

    [add] 4回目のpush

commit 232a26ad2859c003686098eb21e38dd77da340ee
Date:   Fri Jul 22 11:48:21 2022 +0900

    [add] 3回目のpush

commit 42c07ae5ded7030c7d7e217b928569c78eba416e
Date:   Fri Jul 22 11:47:20 2022 +0900

    [add] commitに戻すテスト追加

commit 9435a5cd68a692456a30ed3ce9d2a5d608fe97fb
Date:   Fri Jul 22 11:44:12 2022 +0900

    first commit

ブランチでの変更が反映されている。

ここからがやりたいこと。

特定のコミットに戻す。

git co 232a26ad2859c003686098eb21e38dd77da340ee

git log
bash-3.2$ git log
commit 232a26ad2859c003686098eb21e38dd77da340ee (HEAD)
Date:   Fri Jul 22 11:48:21 2022 +0900

    [add] 3回目のpush

commit 42c07ae5ded7030c7d7e217b928569c78eba416e
Date:   Fri Jul 22 11:47:20 2022 +0900

    [add] commitに戻すテスト追加

commit 9435a5cd68a692456a30ed3ce9d2a5d608fe97fb
Date:   Fri Jul 22 11:44:12 2022 +0900

    first commit

確かに戻ってる。

最新に戻ってくる

git co master

bash-3.2$ git log
commit 85c0537361a036865c3fa5ff6b0864d859a516fd (HEAD -> master, origin/master, origin/feature_1)
Date:   Fri Jul 22 11:53:26 2022 +0900

    [add] ブランチでの追加1

commit 7427ae19e41593e15f8030c8fe285fde8577c2b9
Date:   Fri Jul 22 11:48:55 2022 +0900

    [add] 4回目のpush

commit 232a26ad2859c003686098eb21e38dd77da340ee
Date:   Fri Jul 22 11:48:21 2022 +0900

    [add] 3回目のpush

commit 42c07ae5ded7030c7d7e217b928569c78eba416e
Date:   Fri Jul 22 11:47:20 2022 +0900

    [add] commitに戻すテスト追加

commit 9435a5cd68a692456a30ed3ce9d2a5d608fe97fb
Date:   Fri Jul 22 11:44:12 2022 +0900

    first commit

戻ってきた!

conSultantBERT: Fine-tuned Siamese Sentence-BERT for Matching Jobs and Job Seekersをまとめる

書いてる理由

転職サービスにおけるテキストデータの活用方法が知りたい。

参考

conSultantBERT: Fine-tuned Siamese Sentence-BERT for Matching Jobs and Job Seekers

目次

  • 調べた理由詳細
  • conSultantBERTの論文の紹介
  • Abstruct
  • Introduction
  • Related Work

詳細

調べた理由詳細

転職サービスの求人のレコメンド などに用いるデータは、行動履歴や人/求人の属性情報がメインだが、 テキストデータを用いてマッチング精度向上ができないか?? 他のサービスはどう使ってるんだろう?? 調べたろ。 Recsys2021のHR workshopから持ってきた。

conSultantBERTの論文の紹介

Abstruct

求人と候補者のマッチングを目的としたモデルにテキストデータを特徴量として追加することを狙う。 パース済みの履歴書データを用いる・異なるデータソースのデータを用いる・多言語を扱いそれぞれを相互に変換するタスクになっている。 これを実現するため、27万を超える候補者のレジュメと求人情報のペアへの親和性を転職スタッフがアノテーションし、Seamise Sentence-Bertをファインチューニングした。 この取り組みで、TF-IDFやbertへの組み込みで従来を上回るパフォーマンスを発揮した。 また多言語データの扱いや言語の変換も可能であった。

1. Introduction

著者らが所属するRandstadは、グローバル展開するHRカンパニー。 2020年では200万人を超える候補者と、23万6千の企業を取り扱う。 世界で38のマーケットを展開し、その半分でTOP3のサービスとなっている。 2020年時点で、34680人の従業員がおり、2兆円を超える売り上げがある。

候補者に求人を紹介する仕事と、逆に企業に候補者を紹介する仕事をしている。 その中でレコメンド をしており、候補者のレジュメ・求人のテキスト・両方の属性情報を混ぜて利用している。 最大目的は、ポディションが開いている候補者に最適な求人を推薦することである。 本論文では、候補者のレジュメや求人のテキストをレコメンド に組み込むことを検証する。

これをするために、以下を要件として検証する。 1) グローバル展開しているため、多言語を扱うことができること。 2)大量のテキストを効率よく利用できること。

最終的には、これをレコメンドシステムに組み込みより良い推薦をすることを狙う。

1-1. Problem setting

候補者と求人のマッチングにテキストデータを活用するためには、以下の課題がある。 1) データがフォーマット化されていない。 レジュメは候補者が自由に作成し、基本的にPDFであることあ多いがいつもそうであるとは限らない。 またレジュメのテキストをパースする必要があるが、これも難易度が高い。本論文ではパースの方法については対象外とする。 求人のテキストはフォーマット化されていることが多い。

2) データの性質が多様。 テキスト解析でよくあるのは、二つの文章が意味的に近いかどうかを取り扱うことが多い。 しかし転職におけるテキストは同じものを説明するためのテキストではなく、相互に補完する(マッチする)かどうかが興味対象となる。 そのため、我々が扱う正解データはマッチするかや似ているかが焦点となる。

3) 多言語を扱う難しさ。 グローバル展開をしている場合、複数の言語を取り扱わなければいけない。 言語ごとのモデルを作る方法もあるが、ビジネスを考えるとスケールが難しくできれば一度に扱いたい。 それゆえ、一つのアルゴリズムで複数言語を扱いたい。 またレジュメと求人のテキストの言語が異なるケースも多数ある。 例えば、レジュメは一般的に英語で作成されることが多いが、求人は各国の言語で記載されることが多い。

本論では、以下の構成で進める。 2章で、HRドメインにおけるテキストデータの活用の関連研究を述べる。 3章で、レジュメと求人のペアへのアノテーションについてと、これを用いたファインチューニングについて述べる。 4章で、上記で挙げた課題を私たちのアプローチで解決したことを示し、マッチングシステムに組み込めることを述べる。

2. Related Work

自然言語データをレコメンド に組み込むことは近年増えておりデファクトになりつつある。 そこで我々も自然言語データをレコメンド に組み込むことを目的とする。 レコメンド に組み込むとは、所在地などの属性情報と一緒にレコメンド に利用できるようにすることを指す。 そのためにはドメインに特化した文書の組み込みを模索する必要がある。 テキストデータの活用には、文脈を考慮した埋め込みが研究としては進んでいるが、産業界ではまだBag Of Wordsでの利用がメインになっている。

Bianらは、2つのモデルを組み合わせた手法を提案している。 1つ目のモデルでレジュメと求人のテキストの関係をエンコードし、2つ目のモデルでレジュメと求人をエンコードする。 この際文章は1文ごとに解析され、BERTのCLSトークンが全ての文の先頭に付与する。 文章を1文ずつBertでベクトル化してそれらをFull Connectすることで文章のベクトルとして扱う。

Bhatiaらは、文章のペアが同じ候補者や求人から発生した文章かを学習することで、一人の人の持つ経験を距離で表現する学習を提案している。 この方法は、レジュメ・求人のテキスト間のラベルが不要であり、これで作成したモデルを通して利用することで、テキストのベクトル化及びレコメンド への組み込みを実現している。

Zhaoらは、W2Vでレジュメと求人のテキストを学習し、文章を単語ごとにベクトル化してそれをConvlution層の入力としている。 Convolutionのアウトプットをattentionに流し込み、最終的にFCで一つのベクトルにする。 これを候補者と求人のマッチ度と比較することで、マッチング率を計算している。

Ramanathらは、企業からの問合せに対して候補者を推薦する際のランキングに、教師無しの埋め込みと教師有り埋め込みを利用している。 教師無しの埋め込みは、テキストではなくリンクトインのグラフデータを用いている。 グラフデータは、スキル、教育機関、雇用者、従業員の関係が表現され、これをグラフニューラルネットを用いて候補者やクエリを埋め込む。 教師ありの埋め込みは、テキストデータを用いており、企業の問合せテキストと候補者のプロフィールのテキストをDSSM(Deep Structured Semantic Models)で扱っている。 DSSMはテキストをトリグラムで扱い、候補者のプロフィール/企業の問い合わせをそれぞれ別のモデルに通して埋め込むベクトルを獲得する。 DSSMは企業からの問い合わせとそれにどの候補者を推薦したかで学習される。

Zhuらは、skip-gramを用いてレジュメを64次元、求人のテキストを256次元で表現し、Convolutionの入力に利用している。 Convolutionに通して、レジュメはmax pooling、仕事のテキストはmean poolingをしてベクトルをそれぞれ作成している。 これを用いてレジュメと求人のテキストの親和性をcos類似度で表現する。

Qinらは、テキストを求人に必要な経験とレジュメの経験を分割して取り扱っている。 それぞれを双方向のLSTMに入力しベクトルを作成する。 これを二つのモデルで扱うパイプラインで処理する。 LSTMの出力をアテンションにかけ、その出力を用いて再度双方向LSTMに入力している。 さらに分野ごとに特定のスキルを際立たせるための仕組みも提案している。

本論の取り組みは、取り扱うデータが異なるため、これらの取り組みと直接比較はできない。 例えばBhatiaやQinはレジュメの構造化を重視しており、Ramanathらは企業からの問い合わせに対するレコメンド であり、Bhatiaらは制限された範囲内でのアプローチとなっている。 一方本論では、異なる言語で書かれたレジュメや仕事のテキストを扱うことを目的としており、異なる目的となっている。 また複数のモデルを繋げるパイプラインでのアプローチもしておらずレジュメと仕事のテキストを同じモデルで扱うアプローチとなっている。 私たちはグローバルリーダーとして、既存のデータセットを用いることをせず自前でデータを構築した。

3. Method

3-1でデータをどう作成したかを述べ、3-2でこれの利用について述べる。

3-1. Dataset creation

筆者らは、企業と候補者の履歴を膨大に持っている。 その企業と候補者のやりとりの中で、電話や面接や内定をポジティブな信号を定義する。 逆に候補者がレジュメを送ったが企業からリジェクトされたらそれをネガティブな信号と定義する。 またデータが不均衡であるため、ネガティブデータとして企業ごとにポジティブではない候補者をランダムにサンプリングしてネガティブデータとして利用した。 結果的に、274407の候補者求人のペアのデータを作成し、うち126679がポジティブなペア、109724がネガティブなペアである。 ネガティブなペアの中の38004はネガティブサンプリングで生成した。 156256のユニークな候補者のレジュメ、23080のユニークな求人のテキストが含まれ、求人のテキストと候補者のレジュメは1:Nの関係にある。

求人ごとの候補者のペア数の分布は以下。 f:id:raishi12:20220220160454p:plain

10.5%の求人が一つの候補者しかペアがおらず、また30%の求人でも3つの候補者ペアしかいないような分布になっている。

レジュメは候補者が作成したPDFからApach Tikaを用いてパースした。 レジュメの構造は多様で、個人情報・学歴・職歴・趣味などの欄があり、その順序性や表の形式等も異なる。 一方求人のテキストは構造化されている。 大体、2100のトークン(単語)で作られ、その構造は求人情報、仕事の内容、必要なスキルを含む仕事の条件、報酬などを含む仕事の利点、会社の説明が記述されている。

3-2. Architecture

我々が抱えるコンサルタントが付与したデータを用いてプレトレインをしたため、consultantBERTと名付け、wikipediaで100カ国の文字で書かれた記事をクローリングしてプレトレインをしている。 プレトレインはsiamese networksを使っていて、マッチングに対してこのネットワークは効果的である。

オリジナルのSBERTは入力をテキストのペアとし、文章を単語に区切って扱い、それらをpoolingして統合する。 この出力をcos類似度で計算し同じ/異なるという結果との差異をMSEで計算して最小化したり、ラベルとみなしてCloss Entropyを計算して最小化する。

このどちらもレジュメと求人の親和性の計算には利用できるため、seamiseとClassificationの両方の実験をした。

3-2-1. Document representation

SBERTを含むトランスフォーマーの大半は文をベクトル化することを狙っているが、我々は文書をベクトル化したい。 上述したプレトレイン済みのモデルを用いて様々な文書のベクトル化を実験した。 まず文書から文に分割し、それをベクトル化することを試みた。 文は文書に複数あるため、文のベクトルを単純に平均する方法や、文の長さで重み付けする方法をとった。

次に各文章に付与した[CLS]トークンを最後の4文の平均を取得してこれを文書のベクトルとした。 この時の平均化も単純な平均や加重平均なども試した。

このように色々試したが最終的には、文書の最初の512単語をSBERTの入力に使うことに落ち着いた。(文で切らずに連続したものとして扱った) パースした結果が入力となり、これを過剰に削らないように前処理して扱った。

3-2-2. Fine-tuning method

全データの80%を学習に利用し、epoch:5、バッチサイズ:4にして学習をした。 レジュメと求人のテキストは平均プーリングがよかったため、これを用いた。

BERTのモデルのファインチューニングは512単語で実施した。(wikipediaのデータ)

4. Expetimental Setup

4章では、学習用・検証用・テスト用に利用したデータセットを4.1で説明し、ベースラインとそれをベースラインに利用する理由を4.2で説明し、consultantBERTを4.3で説明し、評価を最後にする。

4-1. Dataset

3章で説明した通り、274407の候補者求人ペアを持っている。 これを80%を学習、10%を検証、10%をテストに分割した。 学習データは埋め込みのためのファインチューニングとランダムフォレストの作成のために利用し、検証データはハイパーパラメータの探索、テストデータはこの論文での精度結果を確認するために利用した。

4-2 Baselines

本論では、テキストを有用な形で組み込める特徴量を作成することが目的であるため、比較するためのいくつかのアプローチも試す。

4-2-1 教師無し まず教師無しの手法での特徴量作成をした。 具体的には、TFIDFとBERTのプレトレーニングで特徴量を作成する方法をとった。 これらの手法でベクトル化し、候補者と求人をcos類似度で距離を計算することでこれをマッチングスコアとみなす。 TFIDFは768次元で表現し、BERTでの次元数と合わせた。 学習データでTFIDFのベクトル変換器を作成し、テスト用の候補者と求人のテキストをベクトル化した。一方BERTを用いる方は、FuggingFaceのwikipediaのデータで学習された事前学習済みモデルを用いた。

この試行は、語彙のギャップの評価に利用できました。例えば候補者と求人の利用単語が完全に異なる場合、TFIDFで得る結果からのcos類似度は低くなります。 逆に語彙の重なりが多ければ、TFIDFでも十分良いベクトルを得ることができる。

4-2-2 教師あり ここではTFIDFとBERTでベクトル化したデータをRandom Forestに利用した実験をした。 学習データの80%を用いて、Random Forestのデフォルトパラメータで学習した。 これを教師無しと比較することで、語彙のギャップだけで親和性を予測するのと、そのギャップを親和性の正解と比較しながら学習するのではどちらが良いかを判断する。

4-3. conSultantBERT

最後に、consultantBERTを比較し、Classificationのタスクとして学習するかRegressionのタスクとして学習するかを比較した。

分類問題として解く理由は、マッチングが親和性有り or 無しの二値分類とみなせるからです。 また同時に、本研究は、マッチングに利用できるテキストの埋め込みの探索であるため、分類タスクだけでなく、Regressionとして距離を出すことにも意味があるため、Regressionタスクとしても実施する。

4-4 Evaluation

上記の方法を比較するため、ROC-AUCのスコア及びマクロPrecisoin/Recall/F1-scoreを用いて比較する。 最後に有意差を独立性のあるt検定で検定する。

5. RESULTS

表1に結果を示す。

f:id:raishi12:20220222153306p:plain

5-1. Overall Performance

予想通り、BERTとTF-IDFを教師なしで使用した場合、あまり良い結果は得られなかった。 これは、転職において候補者と求人のテキストに利用されるテキストが一定ではなく、異なる傾向にあるという性質の推測を裏付ける。 3,4行目は教師あり学習の結果で、1,2よりも大きくスコアが増えていることから、教師あり学習をした方が良い精度が得られることがわかり、学習可能なタスクであることを意味する。

5-1-2 conSultantBERT

5~8行目にconSultantBERTの結果を示す。 5,6行目はclassificationのタスクとして学習した結果で、1と5の比較から、私たちが用意したデータが大きな改善をもたらしていることがわかる。

また3,4との結果の比較から、consultantBERTを用いたベクトルを利用したほうが精度の上昇が大きく、3と6の比較ではROC-AUCが12%も上昇している。 私たちの目的は、レコメンドに有用な特徴表現を生成することであり、教師ありで良い精度を出すのも重要であるが、cos類似度をとった時にどうかという教師無しのアプローチの結果が重要となる。

最後にRegressionタスクとして学習した結果を7,8行目に示す。 ここでは、Random Forestを使うより、単純にcos類似度での判別をしたほうが精度が良いという結果になった。 しかし統計的な有意差があるほどの差異ではない。

注目すべきは、cos類似度を用いた方法が最も良い結果となったことである。 これがRFを後続に持つアーキテクチャよりも精度が良いということは、最終層で分類を実施するよりもこれがない状態で予測する方が良いということであり、分類箇所の学習がなくとも親和性を取得できる良い表現ができていることを示している。

5-2 Precision & Recall

PrecisionとRecallに着目すると、1,2行目ではTFIDFの方がBERTに比べてrecallを保ちつつprecisionが少し高いという結果となった。 3,4行目の結果はほぼ変わらず、TFIDFもBERTもベクトル化をした後に分類タスクをかけるとこの二つのベクトルには差異が大きく存在しないという結果となった。 5~8のconsultantBERTを用いた方法は、1~4に比べて精度が向上しており、特に6,7,8はprecision/recall/F1-scoreがバランス良く高い結果となっている。

6 Analysis and Discussion

前のセクションで結果を分析した後、このセクションでは、ポジティブマッチとネガティブマッチにまたがる予測スコアの分布を調べることで、さまざまな手法を詳しく見る。 また、埋め込み空間に求められるもう一つの特性である多言語性について言及する。

6-1. Distibutions

各手法におけるポジティブデータとネガティブデータのスコア(cos類似度は距離、RFは親和性ありの確率)の分布を示す。オレンジがポジティブデータで青がネガティブデータ。

f:id:raishi12:20220223011906p:plain

consultantBERTを用いた方がポジとネガを綺麗に分割できそうである。 用いない左二つは、cos類似度/RFの両方で分布の大部分が重なってしまっている。 また、cos類似度では、BERTはTFIDFに比べて全体的に類似度が高く出力されている。

最もよく分離できていそうなのは右上のconsultantBERTRegressorのCosine類似度をとったものである。

6-2 Multilinguality

私達のグローバル展開を考えると、多言語対応できることが望ましい。 サービスを展開する国では、候補者が求人の企業の国籍が母国ではないケースが増えている。 例えばオランダでは求人のほとんどがオランダ語ですが、候補者のレジュメの10%が英語を使って作られている。 テキスト解析でよく用いられるTFIDFやw2vは一つの言語を扱うのが普通であり、言語を跨いだ解析は困難である。

例えば英語での「logistics」はオランダ語では「logistiek」となるが、これは異なる単語として扱われることが普通である。 これをconsultantBERTで学習した結果、異なる単語を利用しているが文章の類似度がとれたことを以下に示す。

f:id:raishi12:20220223014054p:plain

左側が英語の文章で、下がオランダ語の文章。 各手法で、それぞれの文章の類似度をとった結果が各箱のスコア。 TFIDFでは単語の重なりから計算するので、スコアは0となるが、BERTは単語が異なっていても意味が近しいと判断されればその類似度が計算できるため、通常のBERTでもスコアが算出されている。 これと比較し、consaltantBERTを用いると通常のBERTに比べて似た文章はさらにスコアが高く、似ていない文章はスコアが低くなるようになっている。

7 Conclusion

本論では、様々な方法で候補者のレジュメと求人のテキストを埋め込むための実験をしました。

我々は、コンサルタントがラベル付した、レジュメと求人のテキストの大規模な実世界データセットを用意し、これをSiamese SBERT フレームワークを使用してBERT モデルをプレトレインした。 このモデルは、TF-IDF特徴と事前に訓練されたBERTでの結果よりも良い精度を出すことが明らかとなった。 さらに、このモデルが多言語を同時に扱える可能性を示した。 最後に、私達のモデルに通したベクトルをコサイン類似度をとる方法で最大の精度となったことから、レコメンドへの組み込みが可能なベクトル化モデルを構築したと考える。

以上。