PR:このページには広告(アフィリエイトリンク)が含まれます。リンク経由の応募で当サイトに紹介料が入ることがありますが、掲載順は各社から受け取る金額では決めていません。掲載基準について

リゾートソート > 記事

ショウジョウバエの嗅覚回路を使って、派遣会社をまたいだ求人の時給差を出す

リゾートソート(ZAX)/ 求人データの実測にもとづく記事です

※この記事は執筆時点のデータセット(14,328件)にもとづく調査記録です。 数字をあとから書き換えると調査の記録として成立しなくなるため、本文はそのままにしています。 現在の集計値は調査データのページでご確認ください。

リゾートソートには、掲載中の求人から「同じエリア・同じ職種なのに、派遣会社によって時給が違う組み合わせ」を自動で拾い出す仕組みがあります。ショウジョウバエの脳にある嗅覚回路の構造をそのまま計算に使っていて、実装は229行です。この記事では、その中身と、この方法で何が言えて何が言えないのかを書きます。

先に結論を書きます。

なぜ普通の文字列比較では足りなかったか

派遣会社は、勤務先の施設名をほとんど公開していません。求人票に出てくるのは「北海道/ニセコ/調理補助」「長野県/白馬/ホール」といった粒度です。掲載している12,497件のうち、施設名(workplace_name)が入っているものはごく一部です。

そのため、A社の求人とB社の求人が同じ現場なのかを、文字列の一致で判定することができません。表記も揃っていません。「賀茂郡河津町」と書く会社もあれば「河津」と書く会社もあります。

さらに、総当たりで比較すると 12,497 の2乗で約7,800万通りになります。素朴に回すと時間がかかります。

キノコ体がやっていること

ショウジョウバエは、数千種類の匂いを嗅ぎ分けます。その処理をしているのがキノコ体という部位で、構造がかなり変わっています。

入力側の射影ニューロン(PN)は数十次元しかないのに、その先のケニオン細胞(KC)は2,000個以上あります。情報を圧縮するのではなく、いったん高次元に広げます。 そして各KCは、全PNの中からランダムに数本だけ枝を伸ばして結合しています。最後にAPLという抑制性ニューロンが全体を見張っていて、強く反応した上位数%のKCだけを残し、残りを黙らせます。

結果として、1つの匂いは「2,000個のうち100個くらいだけが光っているパターン」になります。似た匂いは似たパターンになり、違う匂いは重ならないパターンになります。

これを「似ているものを似たハッシュ値にする」アルゴリズムとして定式化したのが FlyHash です(Dasgupta, Stevens, Navlakha, *Science*, 2017)。普通のハッシュ関数は、入力が少し違えば全く違う値を返すように設計されています。FlyHash はその逆で、似た入力を意図的に似た値に落とします。

実装

求人1件を、こういう素性の集合に変換します。


def extract_features(job):
    tokens = []
    if job.get("prefecture"): tokens.append(f"pref:{job['prefecture']}")
    if job.get("city"):       tokens.append(f"city:{job['city']}")
    if job.get("job_category"): tokens.append(f"cat:{job['job_category']}")

    text = f"{job.get('workplace_name') or ''} {job.get('workplace_key') or ''} {job.get('city') or ''}"
    clean = re.sub(r"[【】\[\]\s★♪◆◇◎※・\-_/0-9]", "", text)
    for i in range(len(clean) - 2):
        tokens.append(f"ng3:{clean[i:i+3]}")   # 3文字ずつの部分文字列
    return tokens

全求人を通すと、素性の語彙は 7,588種類 になりました。これが入力側(PN層)の次元数です。

ここから先が回路の再現です。


class FlyConnectomeEngine:
    def __init__(self, n_kc=2048, syn_per_kc=6, top_k_ratio=0.05):
        ...

こうして各求人が「2,048ビットのうち102ビットが立ったパターン」になります。あとは2つのパターンのXORを取って立っているビットを数えれば、似ている度合いが出ます(ハミング距離)。


dist = int(np.sum(hashes[idx1] ^ hashes[idx2]))
if dist <= max_hamming:      # max_hamming = 18
    ...

総当たりを避けるため、比較は(都道府県, 職種)で区切ったブロックの中だけで行っています。

何が出てくるか

実際に抽出されたものを、そのまま出します。


距離5  北海道定山渓/洗い場    ワクトリ 1,300円  vs  ヒューマニック 1,120円
距離8  北海道ニセコ/調理補助  ワクトリ 1,500円  vs  ヒューマニック 1,200円
距離6  長野県白馬/ホール      ヒューマニック 1,400円  vs  ワクトリ 1,100円
距離5  静岡県賀茂郡河津町/調理補助  グッドマン 1,400円  vs  アルファ 1,280円

時給差が30円以上あるものだけを残して、85,377ペアになりました。時給以外(月給・日給)は混ぜていません。極端な値を除くため、800円〜3,500円の範囲外も外しています。

ここからが正直な話

この記事を書くにあたって、自分の実装を読み直しました。説明できないことが2つ見つかったので、先に書いておきます。

これは「同じホテル」を特定していません

素性を見てもらうと分かる通り、使っているのは都道府県・市区町村・職種と、市区町村名の3-gramです。施設名はほとんどのデータに入っていないので、実質的に効いているのは地名と職種だけです。

つまり距離5で一致した「北海道定山渓/洗い場」の2件は、定山渓にある別々の旅館かもしれません。 定山渓には宿が何十軒もあります。

なので、この数字を「同じ職場なのに時給が違う」と読むのは正しくありません。正しくは「同じ温泉地の同じ職種で、会社によって出している時給が違う」です。求職者にとっては十分に意味のある情報ですが、言葉は変えるべきだと考えています。

厳密な「同じ施設」の比較は、別の分析で行っています。そちらは各社のJSON-LDに入っている郵便番号と番地が一致するものだけを同じ勤務地とみなす方法で、確認できたのは15か所です。85,377と15。この差が、そのまま「手法の緩さ」の差です。

精度を測っていません

ハミング距離の閾値18は、出力を眺めて決めた値です。正解データを用意して精度を測る作業を、まだやっていません。「信頼度98%」のような数字は、出せません。

閾値を8まで厳しくすると36,192ペア(42.4%)に減ります。どちらが妥当かを判断する根拠が、今は手元にありません。

想定される指摘

「それ、FlyHashである必要ある?」

正直、この用途ではありません。素性が地名と職種しかないので、(都道府県, 市区町村, 職種)で辞書を引けば同じことができます。FlyHash が効いてくるのは、施設名や求人本文のような表記揺れのあるテキストを素性に入れたときです。そこまでデータが揃っていないので、現状は回路の性能を使い切れていません。 施設名の取得率を上げるのが次の課題です。

「ランダムな投影で意味のある比較ができるの?」

できます。Johnson–Lindenstrauss の補題が保証しているのは「ランダム射影は距離をおおむね保つ」ことで、FlyHash はその疎・二値版にあたります。元論文でも、局所性鋭敏型ハッシュの標準手法と比べて同等以上の近傍検索性能が示されています。ただし乱数シードを固定しないと結果が変わるので、np.random.seed(42) で固定しています。

「毎回2,048×7,588の行列を作るの?」

疎行列(scipy.sparse.csr_matrix)です。結合は 2,048 × 6 = 12,288 本だけなので、密行列の 0.08% です。ここは生体の構造がそのまま計算量の節約になっています。

まだできていないこと

施設名が取れていないのが、すべての制約の根っこです。ここが埋まれば、FlyHash は本来の使い方になり、「同じ施設」の判定も15か所から大きく増やせます。精度の検証も、それができてから手作業で正解を作るのが順当だと考えています。

現時点の出力は時給差の一覧で公開しています。上に書いた制約を踏まえて見ていただければと思います。

この条件の求人

おすすめ順の上位12件です。時給・寮の種類・寮費・食事・ 勤務期間の記載を合わせて並べています。提携先の求人を少し上に表示しています。 リンク先は各社の公式求人ページです。

勤務地・職種給与会社
長野県下高井郡
調理(資格不要)
¥1,900
時給
完全個室
寮費無料 3食
グッドマンサービス詳細(公式)
長野県下高井郡
調理(要資格)
¥1,850
時給
完全個室
寮費無料 2食
グッドマンサービス詳細(公式)
長野県下高井郡
調理(資格不要)
¥1,800
時給
完全個室
寮費無料 3食
グッドマンサービス詳細(公式)
長野県下高井郡山ノ内町
調理
¥1,800
時給
完全個室
寮費無料 2食
株式会社ダイブ詳細(公式)
長野県下高井郡山ノ内町
調理
¥1,800
時給
完全個室
寮費無料 2食
株式会社ダイブ詳細(公式)
神奈川県柄下郡
調理(資格不要)
¥1,800
時給
完全個室
寮費無料 2食
グッドマンサービス詳細(公式)
静岡県伊東市
レストランサービス
¥1,700
時給
完全個室
寮費無料 2食
グッドマンサービス詳細(公式)
長野県下高井郡野沢温泉村
調理
¥1,900
時給
完全個室
寮費無料 2食
株式会社アルファスタッフ詳細(公式)
長野県下高井郡
調理補助
¥1,600
時給
完全個室
寮費無料 3食
グッドマンサービス詳細(公式)
長野県下高井郡
調理補助
¥1,600
時給
完全個室
寮費無料 3食
グッドマンサービス詳細(公式)
群馬県吾妻郡
調理(資格不要)
¥1,700
時給
完全個室
寮費無料 2食
グッドマンサービス詳細(公式)
神奈川県足柄下郡箱根町
調理(資格不要)
¥1,650
時給
完全個室
寮費無料 2食
グッドマンサービス詳細(公式)

条件を決めて求人を探す →

求人を時給順で比較する