1. 要旨
Word2Vec や fasttext による単語ベクトル では、 $\overrightarrow{王} - \overrightarrow{男} + \overrightarrow{女} \simeq \overrightarrow{女王}$ というような、「意味の足し引き算」が出来る。
このことを利用し、例えば「北(0°)を正義、東(90°)を悪 としたとき、警察は北北東(32°) を向き、ヒトラーは北東やや東(47°)を向く」として、「正義と悪の角度」による定量評価を行うことに成功した手法を提案する。
この手法は、マスコミの偏向報道の定量評価に役立てることが出来る。
例えば次のような時、A新聞はB新聞よりも警察を悪く報道していることが客観的に理解できる。
新聞 | 警察の「正義と悪の角度」 |
---|---|
A | |
B |
2. 理論
ここでは、「警察」の「正義と悪の角度」を測る場合を考える。
2-1. 角度の定義
2-1-1. シンプルな定義
$\overrightarrow{警察}$ は次のように書ける。
\overrightarrow{警察} = a \overrightarrow{正義} + b \overrightarrow{悪} +
\overrightarrow{その他}
ここで、 $\overrightarrow{その他}$ は $\overrightarrow{正義}$ と$\overrightarrow{悪}$ が貼る平面 (正義・悪 平面)と 垂直である。
このような実数の組 $a,b$ は 一つしかない。
証明
次のような直交行列 $R$ を考える。
- $R \overrightarrow{正義}$ が $x$軸と平行になる。つまり $R \overrightarrow{正義} = \begin{pmatrix}
\alpha & 0 & 0 & \cdots & 0
\end{pmatrix}$ となる - $R \overrightarrow{正義}$ と $R \overrightarrow{悪}$ が $xy$平面を貼る。つまり $R \overrightarrow{悪} = \begin{pmatrix}
\beta & \gamma & 0 & \cdots & 0
\end{pmatrix}$ となる
このとき、$R \overrightarrow{その他}$ は、$x$成分と$y$成分が共に0 の任意のベクトルとなる。
よって、$R (a \overrightarrow{正義} + b \overrightarrow{悪} +
\overrightarrow{その他}) = \begin{pmatrix}
a\alpha + b\beta & b\gamma & \cdots
\end{pmatrix}$ となり、これが $R \overrightarrow{警察}$ と等しいので、
$a\alpha + b\beta = (R\overrightarrow{警察})_x$
$b\gamma = (R\overrightarrow{警察})_y$
となり、2つの連立2元1次方程式となるので、変数 $a,b$は一意に決まる
(証明終わり)
$a,b$ により、「正義・悪 平面」内で、「警察」の角度を特定することが出来る。
上の図でいえば、「正義を0°、悪を60°としたとき、警察は20°である」といえる。
しかし、この定義では、悪の角度が一通りに決まらない。
そこで、次の節で悪を90°に固定する方法を議論する。
2-1-2. 直交性を保証する定義
前節での定義では、正義が常に0°となった一方、悪が90°とは限らなかった。
ここでは、正義を0°、悪を90°に固定する方法を考える。
アイデアとしては、「正義と警察のcos類似度」を実部、「悪と警察のcos類似度」を虚部とした複素数の偏角を角度として採用するものである。
$\overrightarrow{正義}$ 方向の単位ベクトルを$\vec{u}$ とし、$\overrightarrow{正義}$と$\overrightarrow{悪}$ が貼る平面上にあり、かつ $\overrightarrow{u}$ に垂直な単位ベクトル $\pm\vec{v}$ のうち、$\overrightarrow{悪}$ と為す角度が小さいものを$\vec{v}$ とする。
$\overrightarrow{正義}$ と $\overrightarrow{悪}$ の為す角を $\theta$ とする。
\vec{s} = a \overrightarrow{正義} + b \overrightarrow{悪}
= \alpha \vec{u} + \beta \vec{v}
とすると、
\overrightarrow{警察} = \vec{s} + \overrightarrow{その他}
と表せる。
cos類似度を $\rm{sim}$ とすると、
{\rm{arg}}\left(
{\rm{sim}} \left( \overrightarrow{警察}, \overrightarrow{正義} \right)
+
i{\rm{sim}} \left( \overrightarrow{警察}, \overrightarrow{悪} \right)
\right)
={\rm{arg}} \left(\alpha + i (\alpha \cos\theta+ \beta \sin\theta )
\right)
である。
導出
\begin{align}
&{\rm{sim}} \left( \overrightarrow{警察}, \overrightarrow{正義} \right)
+
i{\rm{sim}} \left( \overrightarrow{警察}, \overrightarrow{悪} \right)
\\
=&
\frac{
\left(
\vec{s} + \overrightarrow{その他}
\right) \cdot \overrightarrow{正義}
}{\left|\overrightarrow{警察}\right|\left|\overrightarrow{正義}\right|}
+
i \frac{
\left(
\vec{s} + \overrightarrow{その他}
\right) \cdot \overrightarrow{悪}
}{\left|\overrightarrow{警察}\right|\left|\overrightarrow{悪}\right|}
\end{align}
$\overrightarrow{その他}$ は $\overrightarrow{正義}$ や $\overrightarrow{悪}$ と垂直なので
\begin{align}
=&
\frac{
\vec{s} \cdot \overrightarrow{正義}
}{\left|\overrightarrow{警察}\right|\left|\overrightarrow{正義}\right|}
+
i\frac{
\vec{s} \cdot \overrightarrow{悪}
}{\left|\overrightarrow{警察}\right|\left|\overrightarrow{悪}\right|} \\
=&
\frac{1}{\left|\overrightarrow{警察}\right|}
\left\{
\frac{
\left(\alpha \vec{u}+\beta \vec{v}\right) \cdot \overrightarrow{正義}
}{\left|\overrightarrow{正義}\right|}
+i
\frac{
\left(\alpha \vec{u}+\beta \vec{v}\right) \cdot \overrightarrow{悪}
}{\left|\overrightarrow{悪}\right|
}
\right\}\\
=&
\frac{1}{\left|\overrightarrow{警察}\right|}
\left\{
\frac{
\alpha \vec{u} \cdot \overrightarrow{正義}
+\beta \vec{v} \cdot \overrightarrow{正義}
}{\left|\overrightarrow{正義}\right|}
+i
\frac{
\alpha \vec{u} \cdot \overrightarrow{悪}
+\beta \vec{v} \cdot \overrightarrow{悪}
}{\left|\overrightarrow{悪}\right|
}
\right\}
\end{align}
ここで、$\vec{u}$ と $\overrightarrow{正義}$ は同じ方向なので、
$\vec{u} \cdot \overrightarrow{正義} = \left|\vec{u}\right| \left|\overrightarrow{正義}\right|$
$\vec{v}$ と $\overrightarrow{正義}$ は直交するので
$\vec{v} \cdot \overrightarrow{正義} = 0$
$\vec{u}$ と $\overrightarrow{悪}$ は $\theta$ の角度を作るので、
$\vec{u} \cdot \overrightarrow{悪} = \left|\vec{u}\right| \left|\overrightarrow{悪}\right|\cos\theta$
$\vec{v}$ と $\overrightarrow{悪}$ は $\theta - \frac{\pi}{4}$ の角度を作るので、
$\vec{v} \cdot \overrightarrow{悪} = \left|\vec{v}\right| \left|\overrightarrow{悪}\right|\cos\left(\theta - \frac{\pi}{4}\right)$
なので
\begin{align}
=&
\frac{1}{\left|\overrightarrow{警察}\right|}
\left\{
\frac{
\alpha \left|\vec{u}\right| \left|\overrightarrow{正義}\right|
}{
\left|\overrightarrow{正義}\right|
}
+i \frac{
\alpha \left|\vec{u}\right| \left|\overrightarrow{悪}\right|\cos\theta
+\beta\left|\vec{v}\right| \left|\overrightarrow{悪}\right| \cos \left(
\theta-\frac{\pi}{4}
\right)
}{
\left|\overrightarrow{悪}\right|
}
\right\}\\
\end{align}
単位ベクトルなので $\left|\vec{u}\right|=1, \left|\vec{v}\right|=1$ なので
\begin{align}
=&
\frac{1}{\left|\overrightarrow{警察}\right|}
\left\{
\alpha + i (\alpha \cos\theta+ \beta \sin\theta )
\right\}
\end{align}
(導出終わり)
よって、次の通りいえる。
$\overrightarrow{警察} = \alpha \overrightarrow{正義_\rm{unit}} +
\beta \overrightarrow{悪'_{\rm{unit}}} + \overrightarrow{その他}$
と書けるとき、
正義は0°、
悪は90°、
警察は ${\rm{atan2}}(\alpha,\alpha \cos\theta+ \beta \sin\theta)$
の角度である。
但し $\overrightarrow{正義_\rm{unit}}$ は 正義方向の単位ベクトルであり、
$\overrightarrow{悪'_{\rm{unit}}}$ は 正義・悪 平面 上で 正義と垂直な2方向のうち、悪に近い方向の単位ベクトルであり、
$\theta$ は 正義と悪の為す角度である。
2-1-3. シンプルな定義から直交性を保証する定義への変換
「正義を0°、悪を角度$\theta$としたとき、警察は角度$\gamma$である」という場合は、
$\alpha = k, \beta = k \tan \gamma$
といえる。
${\rm{atan2}}(k,k \cos\theta+ k\tan\gamma \sin\theta)$
は $k$ の正負が入れ替わらない限り一定であるから、
$\gamma$ が-90°超え90°未満のときは、
${\rm{atan2}}(1,\cos\theta+ \tan\gamma \sin\theta)$
$\gamma$ が 90°超え270°未満の時は
${\rm{atan2}}(-1,-\cos\theta- \tan\gamma \sin\theta)$
となる。
尚、この変換は直感に反するので注意が必要である。
例えば $\theta=\gamma$ の場合でも、変換後の角度は 90°とは限らない。
また、 $\gamma=90°$ のときは $\theta$ に拘わらず変換後の角度が90°となる。
2-2. 角度の意味
ここでは、正義・悪 平面を $xy$平面とみなし、また正義が0° ($x$軸方向)、悪が90° ($y$軸方向)の場合を考える。
通常は、正義と悪は相反する概念であると考えるが、正義と悪の角度を考えるときは、「共存可能な、別の概念」とみなす。
例えば $\overrightarrow{警察}$ の角度が 0°から 90°の場合、$x$成分も$y$成分も共に正であり、「警察は、正義の成分も悪の成分も含む」ということになる。これはすなわち、「必要悪」を意味するだろう。
90°から 180°の場合は $x$ 成分が負で、$y$ 成分が正であるということになる。これは「警察は、正義と逆の成分を含み、悪の成分も含む」ということになる。「正義と逆」とは厳密にいえば「正義との類似度や関連性が負である」ということであるから、「正義と無関係」ということである。したがってこの場合は、「警察は正義と無関係な悪である」ということになる。これを「完全悪」と呼ぼう。
180°から 270°の場合は $x$成分も$y$成分も共に負である。これは「警察は正義とも悪とも無関係」ということになる。
270°から 360°の場合は、先ほどと同様に考えて「完全正義」と呼ぼう。
3. 実験
3-1. 学習済みモデルの入手
今回は、モデルの学習が目的ではないため、自分で学習は行わず、gensimの学習済みモデルを借りてくることにした。
こちらを使った。
https://qiita.com/Hironsan/items/513b9f93752ecee9e670
コーパス: 日本語版 Wikipedia 2017/01/01 現在
dim: 300
epoch: 10
minCount: 20
ここから入手した model.vec
をカレントディレクトリに保存する。
3-2. 角度の評価
正義を0°、悪を90° (または任意の角度) とした評価を行うにあたっては、
- ベクトルを回転させて扱いやすくする
- matplotlib を準備する
- matplotlib で図示する
といった手順を踏むのが良いだろう。
3-2-1. ベクトルを回転させて扱いやすくする
正義と悪に対する各単語の角度を求めるにあたって、
次の回転を行いたい。
- 正義・悪 平面 が $xy$平面に重なる
- $\overrightarrow{正義}$が $x$方向を向く
これは、 $R\overrightarrow{正義}=k \vec{e_x}, R\overrightarrow{悪}=l\vec{e_x}+m\vec{e_y}$ を満たす実数 $k,l,m (k,m>0)$ があるような直交行列(回転行列積) $R$ を求めることである。
これは、次のようなコード (tool/get_orthogonal_matrix.py
)で実現できる。
import numpy as np
def get_orthogonal_matrix(a, b):
"""
a を x 軸に、 b を xy 平面に重ねる直交行列を求める
"""
a = np.array(a).reshape(-1)
b = np.array(b).reshape(-1)
R1 = rotate_a_to_x(a)
R2 = rotate_b_to_xy(R1 @ b)
return R2 @ R1
def rotate_a_to_x(a):
"""
a を x 軸に重ねる直交行列を求める
"""
dim = len(a)
R = np.eye(dim)
for i in range(1, dim):
angle = get_angle(R @ a, 0, i)
R = rotate(R @ a, 0, i, -angle) @ R
return R
def get_angle(vec, axis_0, axis_1):
"""
vec の 第axis_0軸 と 第axis_1軸 が作る平面における角度を求める。
"""
return np.arctan2(vec[axis_1], vec[axis_0])
def rotate(vec, axis_0, axis_1, angle):
"""
vec を 第axis_0軸 と 第axis_1軸 が作る平面において、 angle だけ回転させる
"""
R = np.eye(len(vec))
c = np.cos(angle)
s = np.sin(angle)
R[axis_0, axis_0] = c
R[axis_1, axis_1] = c
R[axis_0, axis_1] = -s
R[axis_1, axis_0] = s
return R
def rotate_b_to_xy(b):
"""
x 方向を動かさず、b を xy平面に重ねる直交行列を求める
"""
dim = len(b)
R = np.eye(dim)
for i in range(2, dim):
angle = get_angle(R @ b, 1, i)
R = rotate(R @ b, 1, i, -angle) @ R
return R
このようにして求められる直交行列 $R$ は、ベクトルのノルムと内積を保存する。
つまり、任意の $\vec{a}, \vec{b}$ について、
$|R\vec{a}|=|\vec{a}|$
$R\vec{a}\cdot R\vec{b} = \vec{a}\cdot\vec{b}$
が成り立つため、単語ベクトルを「壊さない」といえる。
(ベクトルたちを一様に回転させているだけなので当然である)
3-2-2. matplotlib を準備する
matplotlib では、そのままだと日本語が表示できなかったり、
3次元プロットでベクトルを「いい感じ」に表示できなかったりする。
そこで次の github の力を借りる。
結局、次のようなコード (tool/arrow3d.py
)を用意する。
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.proj3d import proj_transform
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = "MS Gothic" # 日本語対応
class Arrow3D(FancyArrowPatch):
"""いい感じの3次元ベクトルを用意する"""
def __init__(self, x, y, z, dx, dy, dz, *args, **kwargs):
super().__init__((0, 0), (0, 0), *args, **kwargs)
self._xyz = (x, y, z)
self._dxdydz = (dx, dy, dz)
def draw(self, renderer):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
super().draw(renderer)
def do_3d_projection(self, renderer=None):
x1, y1, z1 = self._xyz
dx, dy, dz = self._dxdydz
x2, y2, z2 = (x1 + dx, y1 + dy, z1 + dz)
xs, ys, zs = proj_transform((x1, x2), (y1, y2), (z1, z2), self.axes.M)
self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
return np.min(zs)
def _arrow3D(ax, x, y, z, dx, dy, dz, *args, **kwargs):
'''Add an 3d arrow to an `Axes3D` instance.'''
arrow = Arrow3D(x, y, z, dx, dy, dz, *args, **kwargs)
ax.add_artist(arrow)
setattr(Axes3D, 'arrow3D', _arrow3D)
3-2-3. matplotlib で図示する
3Dグラフと2Dグラフを左右に並べるコードを作成する。
3Dグラフでは、 3-2-1 節で求めた直交行列を利用し、
$\overrightarrow{正義}$を$x$軸、
$\overrightarrow{正義}$と$\overrightarrow{悪}$ を$xy$平面、
に重ねたグラフを表示する。
但し、 $z$ 軸には、その他の成分を集約して、正方向にまとめる。
$(\overrightarrow{vec3d})_z = \sqrt{|\overrightarrow{vec}|^2 - \{(\overrightarrow{vec})_x\}^2- \{(\overrightarrow{vec})_y\}^2}$
2Dグラフでは、2-1-2節の直交性を保証する定義により、角度を求めて表示する。
from gensim.models import Word2Vec
import matplotlib.pyplot as plt
from tool.arrow3d import Arrow3D
from tool.get_orthogonal_matrix import get_orthogonal_matrix as get_R
import numpy as np
def eval(model, x_word, xy_word, words):
"""
単語ベクトルを 3D および 2D 空間に プロットする
"""
R = get_R(model.wv[x_word], model.wv[xy_word])
word_vecs = {
word: get_vec_3d(R @ model.wv[word])
for word in (words + [x_word, xy_word])
}
fig = plt.figure()
ax3d = fig.add_subplot(121, projection='3d')
ax2d = fig.add_subplot(122)
# 3D
# ベクトルをプロット
x = word_vecs[ x_word]
xy = word_vecs[xy_word]
ax3d.arrow3D(-x[0], -x[1], -x[2], x[0]*2, x[1]*2, x[2]*2,
mutation_scale=20, arrowstyle="-|>")
ax3d.arrow3D(-xy[0], -xy[1], -xy[2], xy[0]*2, xy[1]*2, xy[2]*2,
mutation_scale=20, arrowstyle="-|>")
for word in words:
plot_3d(ax3d, word_vecs[word], word)
ax3d.set_box_aspect((1,1,1))
# 軸ラベルの設定
ax3d.set_xlabel(x_word)
ax3d.set_ylabel(x_word +"," + xy_word)
ax3d.set_zlabel('その他')
# 軸の範囲の設定
lim = max(x[0], xy[0], xy[1])
ax3d.set_xlim([-lim, lim])
ax3d.set_ylim([-lim, lim])
ax3d.set_zlim([0, lim*2])
# 2D (直交性を保証)
# ベクトルをプロット
ax2d.arrow(-1, 0, 2, 0,
head_width=0.05, length_includes_head=True)
ax2d.arrow(0, -1, 0, 2,
head_width=0.05, length_includes_head=True)
for word in words:
alpha = word_vecs[word][0]
beta = word_vecs[word][1]
theta = np.arccos(np.dot(x, xy) / (np.linalg.norm(x) * np.linalg.norm(xy)))
angle = np.arctan2(alpha * np.cos(theta) + beta * np.sin(theta), alpha)
plot_2d(ax2d, [np.cos(angle), np.sin(angle)], word)
ax2d.set_box_aspect(1)
# 軸ラベルの設定
ax2d.set_xlabel(x_word)
ax2d.set_ylabel(xy_word)
# 軸の範囲の設定
vecs = np.array(list(word_vecs.values()))[:-2,0:2]
lim = np.max(np.abs(vecs))
ax2d.set_xlim([-1, 1])
ax2d.set_ylim([-1, 1])
# グラフの表示
plt.legend()
plt.show()
def get_vec_3d(vec):
"""高次元のベクトルを3次元に投影する"""
[x,y] = vec[0:2]
zz = np.linalg.norm(vec)**2 - x**2 - y**2
if zz < 0:
# 非常に小さいとき、計算誤差でzzが負になる場合がある
z = 0
else:
z = (zz)**0.5
return np.array([x, y, z])
def plot_3d(ax, vec, text):
ax.arrow3D(
0,0,0,
vec[0], vec[1], vec[2],
mutation_scale = 20,
color = "#000000",
arrowstyle = "-|>"
)
ax.text(vec[0], vec[1], vec[2], text)
def plot_2d(ax, vec, text):
ax.arrow(
0,0, vec[0],vec[1],
color="#000000",
head_width=0.1, length_includes_head=True
)
ax.text(vec[0], vec[1], text)
3-1 節で入手した model.vec
の単語ベクトルについて表示するには、
次のコードを実行する。
("左派", "右派", "右翼", "左翼", "ヒトラー", "麻原", "裁判", "警察" という単語に対して、"正義"・"悪"に対する角度を求めている)
import gensim
from eval import eval
class Dummy_model:
pass
model = Dummy_model()
model.wv = gensim.models.KeyedVectors.load_word2vec_format('model.vec', binary=False)
eval(model, "正義", "悪", ["左派", "右派", "右翼", "左翼", "ヒトラー", "麻原", "裁判", "警察"])
4. 結果 (「警察」や「ヒトラー」などの角度を求められた!)
次のような結果を得た。
左の3Dグラフは、悪が60°ほどになっている。
右の2Dグラフでは、悪を90°に補正している。
「ヒトラー」(2Dグラフでおよそ50°)が悪に近い角度をしていて、「裁判」や「警察」(2Dグラフでおよそ30°)が正義に近い角度をしているのは、直感的にも妥当そうである。
また、「左翼」「右翼」という強い言葉が悪に近づき、「左派」、「右派」という比較的穏やかな言葉は正義に近づいている点も興味深い。
但し、「麻原」が「裁判」や「警察」と同じくらい正義に近い角度をしている点は直感に反していると言えるだろう。
また、今回調査した8単語がすべて「必要悪」に分類されたことも特筆すべき点だろう。
さらに、 3Dグラフを別の角度から見てみると、次のように、「その他」の成分が非常に強く、8単語すべてが 正義・悪 平面に対してほぼ直交していることも確認できた。
5. 考察
今回の実験では、学習済みモデルを1通りしか検証していないため、再現性が確認できていない。今後は複数のコーパスや学習済みモデルにおいて同様の検証を行う必要があるだろう。
また、それと並行して、Doc2Vec のように文章をベクトルにする技術において、同様の角度測定を行うことも考えたい。これはマスコミの報道記事における偏向性を定量評価することに繋がる可能性がある。例えば「リベラルを0°、保守を90°としたときに、マスコミの文章はどのような角度になるか」といった具合である。