見出し画像

単語埋め込みのジェンダーバイアスを取り除く ~日本語単語埋め込みのDebiasハンズオン~

今回のテックブログは、AIの公平性がテーマ。機械学習モデルに使われる「単語埋め込み」に潜むジェンダーバイアスと、それらを取り除く手法について解説します。AmazonやGoogleが開発したシステムに差別的なバイアスがあることが発覚し話題となりましたが、日本語ではどうなのかを検証しました。

はじめに

こんにちは、メディア研究開発センターに所属している新妻です。
普段は、自然言語処理やデータジャーナリズムに関する研究開発に携わっています。

修士課程では自然言語処理の分野で機械学習モデルからバイアスを取り除くための手法を研究していました。
研究の場ではこうした手法は英語で学習されたモデルに対して適用されることが多く、そのほかの言語への適用は置き去りにされがちな傾向があります。
そこで、日本語の単語埋め込みが持つジェンダーバイアスを検出し取り除く手法を試してみようと思い、それをハンズオンにしてみました。

本稿のハンズオンは、自然言語処理におけるバイアスに関する初期の研究とされているBolukubasiら(2016)の”Man is to Computer Programmer as Woman is to Homemaker? Debiasing Word Embeddings”で提案された手法を用いております。

機械学習におけるバイアス

統計的差別という言葉はご存知でしょうか。
とりあえず、Wikipediaの定義を置いておきます。

統計的差別(英: statistical discrimination)とは、統計に基づいた合理的な判断によって、差別が生じるというメカニズム(理論)である。統計による差別とも言う。

例えば、企業が採用段階において、労働者の能力を個人の実際の能力ではなく、「学歴」や「性別」などといった、労働者が所属する属性ごとの統計的な平均値に基づいて推測し、採用の判断をする結果、属性ごとの賃金格差が拡大するなどの差別的状態が生じることを言う[1]。

統計的差別 - フリー百科事典『ウィキペディア(Wikipedia)』

ある人間に対して何らかの判断を行う必要があるとき、その人の属性(人種や性別)の統計値に基づいて合理的に判断を行うことで差別が生じることを統計的差別と呼びます。
その属性の差異(ex. 男性と女性)は、歴史的・構造的要因によって引き起こされている可能性があり、その個人を代表しうる値ではないはずです。
それにも関わらず、能力やその人への共感などではなく個人の属性を抽出してその統計値で判断することは、不当な差別と言えるでしょう。

機械学習モデルにおいても、統計的差別を意図せず引き起こしてしまう可能性があります。
それは、多くの機械学習モデルが統計的な理論に基づいてパラメータを学習するモデルであることと、学習に使われるデータもそれを作った人間の主観やデータが発生する社会の構造によってバイアスが含まれうるからです。

実際に次のような機械学習モデルが意図せずに差別をしてしまったという事例もあります。

そして、バイアスを持った機械学習モデルを使ってしまうことは、さらに社会構造を固定化させてしまい、統計的差別を強化してしまう可能性があるのです。

とはいえ、機械学習は我々に与えてくれるメリットは非常に大きく、現代においては手放せない存在となりつつあります。

そんな機械学習モデルを、可能な限り特定の属性を持つ人々を差別しないような安全なものにしていこうという研究がAIの公平性という分野で、本稿のハンズオンもその試みを広めようという一歩です。
だいぶ前置きが長くなってしまいましたが、はじめていきましょう。

実際に日本語単語埋め込みでバイアスを取り除いてみましょう

ライブラリをインストールして単語埋め込みを読み込む

まずは次のコマンドを実行して、ハンズオンで利用するライブラリをインストールしてください。

pip install scikit-learn==1.0.2
pip install gensim==4.1.2
pip install matplotlib==3.5.1
pip install seaborn==0.11.2
pip install japanize-matplotlib==1.1.3

※もし、Google Colabで本ハンズオンを動かしている場合は `pip install` が終わったらランタイムの再起動をしてください。

インストールが終わったら、とりあえずimportをしちゃいましょう。

from matplotlib import pyplot as plt
import seaborn as sns
import japanize_matplotlib
import pandas as pd
import numpy as np
import gensim
from sklearn.decomposition import PCA

次は、バイアスを取り除く単語埋め込みを用意しましょう。
本稿では、日本語の学習済み単語埋め込みとして、chiVeを利用します。
chiVeはワークスアプリケーションが開発しているオープンソースの単語埋め込みで、学習済み単語埋め込みも配布されています。

日本語のトークナイズに同社によって開発されているSudachiを用いており、Skip-gramを元に学習されています。
また、chiVe同社の研究員によって言語処理学会の年次大会で論文が発表されており、詳しい詳細はこちらで読めます。

それでは、chiVeをダウンロードして解凍しましょう。

wget https://sudachi.s3-ap-northeast-1.amazonaws.com/chive/chive-1.1-mc30_gensim.tar.gz
tar vxzf ./chive-1.1-mc30_gensim.tar.gz
# Google Colabならば…
# tar vxzf /content/chive-1.1-mc30_gensim.tar.gz

うまく解凍できたら、gemsimを使ってchiVeを読み込みます。

vecs = gensim.models.KeyedVectors.load("./chive-1.1-mc30_gensim/chive-1.1-mc30.kv")

単語埋め込みのジェンダーバイアス

それでは、本題である単語埋め込みのバイアスの話に入ります。
そもそも、単語埋め込みにおけるジェンダーバイアスとは、どういうものなのかが想像つきますでしょうか。

まずは単語埋め込みが何を表しているのかに立ち返ってみましょう。単語埋め込みは言語学において分布仮説と呼ばれる「単語はその周辺によって特徴付けられる」という考えを統計モデルにして特徴量学習したものです。
そのため、「ある単語が同じ文の中で共起するかどうか」を多次元なベクトル空間における距離として表現できるように学習されます。
その結果として、次式のような演算処理によるアナロジーが成立します。

$$
\overrightarrow{\text { king }} - \overrightarrow{\text { man }}+\overrightarrow{\text { woman }} \approx \overrightarrow{\text { queen }}
$$

これはkingが表しているベクトルからmanの成分を取り除いてwomanの成分を足すことによって、queenが表しているベクトルに近似するという意味です。
せっかくなので、chiVeでも試してみましょう。
gensimを使って$${\overrightarrow{\text { 王様 }} - \overrightarrow{\text { 男 }}+\overrightarrow{\text { 女 }}}$$の近傍にある単語を並べてみましょう。

# most_similarは、演算結果のベクトルに近い単語とそのコサイン類似度の組のリストを返すメソッド
vecs.most_similar(positive=["王様", "女"], negative=["男"])
# [('王女', 0.5798832774162292),
#  ('女王', 0.5600255727767944),
#  ('王妃', 0.5432600378990173),
#  ('姫', 0.5153658390045166),
#  ('王', 0.5093510150909424),
#  ('妃', 0.49332189559936523),
#  ('プリンセス', 0.47179174423217773),
#  ('大様', 0.457807332277298),
#  ('国王', 0.4548933506011963),
#  ('王族', 0.45409610867500305)]

いかがでしたでしょうか。おそらく、最も近い単語が「王女」となったのではないでしょうか。
このようにして単語埋め込みは四則演算によって、単語のアナロジーを表現できるわけです。

そして、Bolukubasiらはこのアナロジーにバイアスが成立しうると主張しました。
それが論文のタイトル `Man is to Computer Programmer as Woman is to Homemaker?` に現れているバイアスで、次式のようなアナロジーが表現されてしまっていると指摘しています。

$$
\overrightarrow{\text { Computer Programmer }} - \overrightarrow{\text { man }}+\overrightarrow{\text { woman }} \approx \overrightarrow{\text { Homemaker }}
$$

これはつまり「コンピュータープログラマから男性を引いて女性を足すと主婦に近似する」という偏見が表出してしまっているというわけです。

では、このアナロジーを実際にchiVeで試すとどうなるでしょうか。

vecs.most_similar(positive=["プログラマー", "女"], negative=["男"])
# [('プログラミング', 0.6557545065879822),
#  ('システムエンジニア', 0.6493301391601562),
#  ('グラミング', 0.640878438949585),
#  ('エンジニア', 0.6008622646331787),
#  ('プログラム開発', 0.5877535939216614),
#  ('Java', 0.5777055621147156),
#  ('コーディング', 0.5564300417900085),
#  ('開発者', 0.5539030432701111),
#  ('グラフィッカー', 0.5515120625495911),
#  ('SIer', 0.5484914183616638)]

幸い、chiVeにはそのようなバイアスがないようです。
これは言語の違いやBolukbasiらは単語埋め込みの学習にGoogle Newsコーパスを使っているのに対してchiVeはWikipediaを使っており、コーパスの性質や書き手が異なっているというのがありそうです。

しかし、次のアナロジーにしてみるとどうでしょうか。

vecs.most_similar(positive=["総合職", "女"], negative=["男"])
# [('一般職', 0.7393637299537659),
#  ('事務職', 0.6642106771469116),
#  ('短大卒', 0.6537709832191467),
#  ('新卒', 0.6327295899391174),
#  ('事務系', 0.616508424282074),
#  ('大卒', 0.6164991855621338),
#  ('新卒者', 0.5834293365478516),
#  ('研究職', 0.583089292049408),
#  ('採用枠', 0.5824562907218933),
#  ('契約社員', 0.5760298371315002)]

すると、「一般職」という単語が非常に類似しているという結果になり、2番目の単語である「事務職」よりも大幅に近いようです。
これが性別特有の結果なのかを確認するために、性別を入れ替えてパターンをみてみましょう。

vecs.most_similar(positive=["総合職", "男"], negative=["女"])
# [('営業職', 0.6846187114715576),
#  ('一般職', 0.6735339164733887),
#  ('事務職', 0.6306720972061157),
#  ('技術職', 0.630662202835083),
#  ('大卒', 0.6098009347915649),
#  ('事務系', 0.5949766635894775),
#  ('契約社員', 0.5865164399147034),
#  ('技術系', 0.5645288228988647),
#  ('現業', 0.5596275925636292),
#  ('新卒', 0.5582292675971985)]

男性の方でも「一般職」という単語は2番目にあるため、それなりに近い単語であるようですが類似度は女性よりも低めとなっています。
この結果から、単語埋め込みは「一般職は女性がなりやすい」というジェンダーロールを学習してしまっていると言えるかもしれません。

これは、Wikipediaにおいて「一般職」が女性に関わる単語との共起しやすいためだと考えられます。
つまり、このような単語埋め込みのバイアスは学習に使われたコーパスの中で共起する単語に偏りがあることによって、引き起こされていると考えられております。
そもそも、この単語の共起の偏り自体がコーパスに含まれる文章の書き手の偏見や想起からくる報告バイアス、そもそもその書き手自体の偏りによって内(外)集団バイアスが反映されてしまうことによって引き起こされています。

そして、そのようなバイアスが含まれたままのモデルを用いて機械学習アプリケーションを作ってしまうことで、モデルが意図しない差別的な分類をしてしまう危険もあります。
例えば、このようなバイアスを持ったモデルが履歴書の分類を行うとどうなるでしょうか。
そのため、こうしたバイアスを単語埋め込みから取り除くために提案されたのが本稿で紹介する手法となります。

Hard-debiasing

Bolukubasiらは、提案手法をHard-debiasingと呼んでおり、次の三つの工程から成ります。

  1. 単語埋め込みのベクトル空間上で属性を表す部分空間を特定する

  2. バイアスを取り除きたい単語から属性を表す部分空間を減算する

  3. 属性を表す単語の組から位置の偏りをなくす

この属性を表す部分空間は、軸としてPCA(主成分分析)を用いて取り出します。
PCAは与えられた特徴量の間で最も分散が大きくなるようなベクトルを取り出す手法です。そのベクトルを主成分と呼び、与えられた特徴量の間にある何らかの情報を軸として表現できるというものです。

本手法では、このPCAによって、ジェンダーを表す単語ペアの集合(ex. 男性と女性、父と母)から、ジェンダーを表す軸を抽出します。

手法を理解しやすくするためにも、手順を図解してみました。
簡単にするため、単語埋め込みの次元数を三次元とし、埋め込みの単語数も6個とし、PCAの細かい説明は省きます。

バイアスを取り除く手順の図解

①ジェンダー部分空間を表す軸を検出
PCAの特徴量として、「男性」と「女性」および「彼」と「彼女」のペアをそれぞれ平均して得られるベクトルを入力し、その第一主成分を取り出します。
そして、その第一主成分をジェンダー部分空間とし、その主成分と埋め込みとの内積(主成分スコア)からなる軸(図中の赤矢印)が得られます。

②ジェンダーに関係ない単語を中立化
次に、「技術」や「家事」などの本来は性別から独立しているべき単語を、軸と平行でかつ主成分スコアが0となる点へと移動させます。

③ジェンダーを表す単語は軸に対して等距離に変換
そして、最後に「男性」と「女性」のような対となる単語のベクトルを移動させます。ペア同士が軸と平行で、かつ軸上の主成分スコアが0となる点から等距離となるように配置し直します。

これによって③で操作された単語ペアは、②で0に操作された単語から互いに等距離となり性別間のバイアスが解消されることになります。

バイアスの検出・除去

それでは、実際に単語埋め込みバイアスを取り除いていきましょう。
Bolukubasiらは、すべての単語に対してバイアスを取り除いているのですが、本稿では簡単にするため一部の単語を対象にすることにします。

まず、単語埋め込みが持つバイアスを検出します。
以下のような非常に簡潔なコードで計算できちゃいます。

#  単語埋め込みと単語のペアのリストを受け取って、主成分分析をおこなう
def performPCA(wv, pairs, n_components=10):
    matrix = []
    for a, b in pairs:
        center = wv[[a, b]].mean(axis=0)
        matrix.append(wv[a] - center)
        matrix.append(wv[b] - center)
    matrix = np.array(matrix)
    pca = PCA(n_components=n_components)
    pca.fit(matrix)
    return pca

# 単語埋め込みと単語のペアのリストを受け取って、第一主成分をジェンダー部分空間として返す
def compute_bias_direction(wv, pairs):
    pca = performPCA(wv, pairs)
    bias_direction = pca.components_[0]

    return bias_direction

実際にジェンダー部分空間を取得してみましょう。
今回はジェンダー部分空間を計算するための元となる単語(以下、ベースワードと呼ぶ)は、論文中ではWordNetを使って選んだりしているのですが、本稿では簡単にするため論文で使われているベースワードを日本語にしたものから選びました。

# ベースワード
men_words = ["男", "男性", "男子" , "父", "父親", "彼"]
women_words = [ "女",  "女性", "女子" , "母",  "母親", "彼女"]
# debiasのための計算の都合上、埋め込みの空間のnormalizeをする
vecs.unit_normalize_all()
# ジェンダー部分空間の取得
direction = compute_bias_direction(vecs, list(zip(men_words, women_words)))

ジェンダー部分空間から計算されるジェンダースコアによってバイアスを可視化するために、いくつかの単語の部分空間上の位置を計算してみましょう。
位置を可視化する単語は、ベースワードに加えてわかりやすさのため「技術」と「家事」を選びました。
単語のジェンダー部分空上の位置は、主成分スコアによって表されるため単語のベクトルと主成分(ジェンダー部分空間)の内積を取ることで得られます。

df = pd.DataFrame({
    "gender": ["男" for w in men_words] + ["女" for w in women_words] + ["女", "男"],
    "score": (vecs[men_words + women_words + ["家事", "技術"]] @ direction)
    })

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6,4))
fig.suptitle("単語のジェンダースコア")
sns.stripplot(data=df, x="gender", y="score", ax=ax,  jitter=False)
ax.text(0.05, df.loc[len(df)-1, "score"], "技術")
ax.text(1.05, df.loc[len(df)-2, "score"], "家事")
単語埋め込みがそれぞれの単語をどっちのジェンダーだと思ってるか?

「技術」と「家事」は本来は性別からは独立しているべき単語ですが、ジェンダー部分空間上の位置を見ると「技術」は男性寄りにあって「家事」は女性寄りにあり、単語埋め込みによって暗黙的に性別間における偏りを学習してしまっていることがわかります。

それでは、ここからバイアスを取り除いてみましょう。
まずはバイアスを取り除くための関数を定義します。

# 単語群のバイアスを取り除く計算をする
def debias(wv, words, bias_direction):
    vs = wv[words]
    return vs - (bias_direction * ((vs @ bias_direction).T / (bias_direction @ bias_direction))[:, np.newaxis])

# 距離が部分空間の軸から等価であるべきペア(ex. 「男性」と「女性」のようなペア)を等距離になるように再配置する
def equalize_pair_words(wv, word_pair, bias_direction):
    u = wv[word_pair].mean(axis=0)
    y = u - bias_direction * (u @ bias_direction).T / (bias_direction @ bias_direction)

    z = np.sqrt(1 - np.linalg.norm(y)**2)

    a, b = word_pair
    if (wv[a] - wv[b]) @ bias_direction < 0:
        z = -z
    return np.stack((z * bias_direction + y, -z * bias_direction + y))

そして、上記の関数を使ってバイアスを取り除いてみます。

# まずは「技術」と「家事」だけのバイアスを取り除く
debiased_vecs = vecs.vectors_for_all(vecs.key_to_index)
debiased_vecs[["技術", "家事"]] = debias(debiased_vecs, ["技術", "家事"], direction)

# 本来はここで対象全ての単語のバイアスを取り除いて、
# normalizeする必要があるんですが都合によりスキップ
# (大きな影響はなかった)

# 性別に関わる語彙はジェンダー部分空間の軸から等距離になるように再配置する
for pair in zip(men_words, women_words):
    debiased_vecs[list(pair)] = equalize_pair_words(debiased_vecs, pair, direction)

# 本来は最後にまたnormalizeする必要があるんですが都合によりスキップ
#(ここも大きな影響はなかった)

それでは、バイアスを取り除いたベースワードと「技術」と「家事」を再度可視化してみましょう。

debiased_direction = compute_bias_direction(vecs, list(zip(men_words, women_words)))

debiased_df = pd.DataFrame({
    "gender": ["男" for w in men_words] + ["女" for w in women_words] + ["女", "男"],
    "score": (debiased_vecs[men_words + women_words + ["家事", "技術"]] @ debiased_direction)
    })

fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(6,4))
fig.suptitle("単語のジェンダースコア")
sns.stripplot(data=debiased_df, x="gender", y="score", ax=ax,  jitter=False)
ax.text(0.05, debiased_df.loc[len(debiased_df)-1, "score"], "技術")
ax.text(1.05, debiased_df.loc[len(debiased_df)-2, "score"], "家事")
「技術」と「家事」のスコアが0になって、
ジェンダーを表す単語は軸から等距離になるように再配置されている。

バイアスの除去は、このような処理をすることでジェンダーから独立しているべき単語はジェンダーを表す成分が含まれないように軸に配置し、各ジェンダーは互いに軸から等距離になるように再配置されます。

次に、実際に「総合職」と「一般職」のバイアスを取り除いてみましょう。

# debiasの対象となる単語は次の通り
# 総合職, 一般職, 事務職, 営業職, 大卒, 事務系, 技術職, 新卒, 短大卒, 契約社員, 研究職, キャリヤ採用, 管理職, 新卒者, 正社員, 大卒者, 中途入社, 中途採用者, 採用枠, 技術系, 販売職, 学卒, 国家公務員, 新卒採用, 経験者採用, 第二新卒, 高専卒, 経理職, 新卒入社, 転職組, 入社
target_words = ["総合職"] + [w for w, _ in vecs.most_similar("総合職", topn=30)]
debiased_vecs[["総合職", "一般職"]] = debias(debiased_vecs, ["総合職", "一般職"], direction)
debiased_vecs.most_similar(positive=["総合職", "女"], negative=["男"])
# [('一般職', 0.6886234283447266),
#  ('事務職', 0.6284332871437073),
#  ('営業職', 0.6104720234870911),
#  ('大卒', 0.6058101654052734),
#  ('技術職', 0.5969399213790894),
#  ('事務系', 0.5890994071960449),
#  ('現業', 0.5870155692100525),
#  ('新卒', 0.5791255831718445),
#  ('短大卒', 0.5731465220451355),
#  ('契約社員', 0.5634496212005615)]
debiased_vecs.most_similar(positive=["総合職", "男"], negative=["女"])
# [('一般職', 0.6886234283447266),
#  ('事務職', 0.6284332871437073),
#  ('営業職', 0.6104720234870911),
#  ('大卒', 0.6058101654052734),
#  ('技術職', 0.5969399213790894),
#  ('事務系', 0.5890993475914001),
#  ('新卒', 0.5791255831718445),
#  ('短大卒', 0.5731465220451355),
 # ('契約社員', 0.5634497404098511),
#  ('既卒', 0.5617469549179077)]

バイアスを取り除いた単語埋め込みでそれぞれのベクトルに近い単語を見てみると、性別の間に単語の並びやその類似度との間に大きな差異がなくなっていることがわかると思います。
このような処理を全ての単語に対して、適切におこなっていくことで単語埋め込みからバイアスを取り除くことができるというわけです。

余談ですが、「総合職」と「男」「女」の間のコサイン距離も大きく変わっています。

# バイアスを取り除く前
vecs.distance("男", "総合職")
# 0.8463258147239685
vecs.distance("女",  "総合職")
# 0.7694417536258698

# バイアスを取り除いた後
debiased_vecs.distance("男", "総合職")
# 0.82374607026577
debiased_vecs.distance("女", "総合職")
# 0.8237460851669312

最後に

本稿は以上となります。
詳しく知りたい方やより正しい実装について知りたい方は参考文献に元の論文と著者実装のGitHubリポジトリのURLを記載しておきます。
ぜひご覧ください!

この研究が発表されてから、本手法を元に非常に多くの研究が提案されています。例えば、本手法をBERTに適用してみるといった研究です。
筆者も修士課程に所属していたときは、本分野の中心に研究をしていました。本分野はまだまだ発展途上ですが、盛り上がりを見せている分野でもあり、今後ももっと研究は増えていくことでしょう。
これを機に興味を持たれたら、いろんな論文を読んで探してみていただければと思います。

参考文献