300次元の単語ベクトルを1次元に圧縮する
Word2Vecに代表される単語分散表現を、1次元で表すことを目的とした単語埋め込み手法を日本語データで追試したので手順と結果をまとめます。
今回参考にしたのは京都大学から発表されたWordTourです。著者ご本人の解説資料が以下に公開されています。
単語ベクトルは、昨今の大規模言語モデルの発展を見るに非常に有効であることは明らかです。一方で、資料でも紹介されている通り、潤沢な計算資源が無い状況でのシステム運用を考えると、上記の資料でも言及されている通り「メモリを食う」、「時間を食う」、「解釈が困難」という課題が存在します。
上記の課題を解決するため、ピクセルが1次元連続であることに着想を受け、テキストも1次元連続にできないか試みた、という研究です。
動作環境
- Ubuntu20.04
- Python3.8.10
環境構築
著者ご本人が実装を公開されています。
How to Build WordTour by Yourself
の項目を参考に環境構築を進めます。今回は日本語の単語分散表現も用意するため追加の環境構築も行います。
setup.sh
sudo apt install wget unzip build-essential
git clone https://github.com/joisino/wordtour.git
./download.sh
make
pip3 install sudachipy sudachidict_core
日本語単語分散表現の準備
今回はWorksApplicationsの方々が公開している日本語単語分散表現chiveを使用します。版はv1.2 mc90、gensim版のファイル(0.6GB)をダウンロードして任意のパスに配置しておきます。
WordTourとは
テキストを1次元連続にすることを検討された研究です。テキストは離散データですから、「両隣の単語」といった連続データのような扱いはできません。単語ベクトルに変換すれば、単語ベクトルの両隣のベクトルや、ある単語ベクトルの摂動は表現できますが、具体的にどんな単語が対応するかは表現することができません。
そこで、高次元の単語ベクトル空間上から、1次元の単語埋め込みを出力するような問題を考えます。その際、近くに埋め込めれた単語の意味が近くなるように単語埋め込みを出力する、という「健全性」という条件を定義して定式化します。また、単語埋め込みの最初と最後は一致するような条件も追加することで、パスではなく環っかで単語埋込みを定式化します。
後は、この問題を解くのみですが、この定式化は巡回セールスマン問題でありNP困難な問題です。しかし、最近のソルバなら100000頂点程の巡回セールスマン問題ならば厳密に解けることが知られています。従って、後はソルバに入力することでこの問題の解である1次元単語埋め込みを出力します。
実装
今回は名詞の単語ベクトルに絞って検証します。また、WordTourで使用する巡回セールスマン問題のソルバーが最大約10万点の頂点までであれば厳密に解けることを考慮して、今回は10000単語に語彙数を制限します。まず始めに、chiveの単語ベクトルから名詞だけ抽出するクラスVectorDumperを実装します。以下の実装で出力されるデータはchiveのテキスト版と同じフォーマットになりますが、テキスト版は容量が大きいのでgensim版から名詞の単語ベクトルのみ出力します。
実装内容はchiveの単語ベクトルの品詞情報をsudachipyで出力し、10000単語分の名詞と対応した単語ベクトルをtour.txtに出力しています。詳細は実装をご確認ください。
全体実装 VectorDumper (60行)
import yaml
import gensim
from sudachipy import tokenizer
from sudachipy import dictionary
class VectorDumper:
def __init__(self, opt):
self.opt = opt
self.word2vec = gensim.models.KeyedVectors.load(opt["path"]["chive"])
self.vocabs = self.word2vec.index_to_key
self.tokenizer = dictionary.Dictionary().create()
self.mode = tokenizer.Tokenizer.SplitMode.C
self.target_pos = ["名詞"]
return
def tokenize(self, token):
return self.tokenizer.tokenize(token, self.mode)
def dump_controlled_vocab(self):
with open(self.opt["path"]["out"], mode="w", encoding="utf-8") as o:
counter = 0
for idx, vocab in enumerate(self.vocabs):
if counter > 10000:
break
pos = self.tokenize(vocab)[0].part_of_speech()[0]
if pos in self.target_pos:
counter += 1
vector = self.word2vec[vocab]
target = list(map(str, vector.tolist()))
line = " ".join([vocab] + target) + "\n"
o.write(line)
return
def run(self):
# self.dump_raw_vocab()
self.dump_controlled_vocab()
return
if __name__ == "__main__":
# config.yamlの読込
with open('config.yaml', 'r') as yml:
opt = yaml.safe_load(yml)
vd = VectorDumper(opt)
vd.run()
上記のクラスを実行すると以下のフォーマットで単語と単語ベクトルを1行にまとめたファイルが10000行分出力されます。自身の環境では./dev/tour.txt
に出力しました。
映画 0.02665202133357525 0.026810171082615852 ...
先生 -0.07289369404315948 0.08926983177661896 ...
メール 0.07681874185800552 0.12336508184671402 ...
場所 0.019091689959168434 -0.08042081445455551 ...
次に上記のファイルをWordTourに入力します。
WordTourのリポジトリをクローンしたディレクトリを開いたターミナルで以下のコマンドを実行します。
./make_LKH_file ./dev/tour.txt 10000 > ./LKH-3.0.6/wordtour.tsp
cp wordtour.par ./LKH-3.0.6/wordtour.par
cd LKH-3.0.6
make
./LKH wordtour.par
cd ..
python3 generate_order_file.py
cat wordtour.txt
出力されたwordtour.txtが1次元に埋め込まれた単語埋め込みです。
実装結果
wordtour.txtの出力の一部を抜粋します。
こと
物
...
はじめ
中心
メイン
テーマ
タイトル
...
同時
瞬間
一気
急
途中
何度
一度
度
熱
空気
匂い
香り
味
酒
ビール
コーヒー
茶
黒
白
赤
色
カラー
...
週
週間
箇月
年間
年
以来
振り
久々
久し振り
先日
昨日
今日
本日
明日
楽
対応
サポート
プログラム
実行
設定
モード
クリア
プレー
ゲーム
アニメ
漫画
小説
物語
ストーリー
人物
キャラ
キャラクター
...
00
0
1
2
3
4
5
6
7
8
9
11
12
14
13
16
18
17
19
21
22
23
26
24
25
15
10
20
30
40
60
50
100
1000
円
ドル
米
金
お金
価値
...
事
実装した感想
単語の移り変わりを確認しましょう。上記の一部をさらに抜粋します。
同時
瞬間
一気
急
途中
何度
一度
------ここから上は期間やタイミングに関する単語
度 ← 期間やタイミングの「一度」から温度の「度」へ接続
------ここから下は気体に関する単語
熱
空気
匂い
香り
------
味 ← 五感で感じる「香り」から味覚の「味」へ接続、味から飲み物へ遷移
------
酒
ビール
コーヒー
------
茶 ← 飲み物から飲み物の「色」へ遷移
黒
白
赤
色
カラー
1次元単語埋め込みの直感的な理解
人間が見るとかなり直感的につながっているように思いますが、全ての単語を網羅している訳ではないことが見て取れます。これは著者ご本人の解説資料で言及されている通り、WordTourでは単語埋め込みに満たしていて欲しい性質を健全性と完全性に分けています。WordTourは上記の内健全性のみ(正解だけがとってこられるが取り残しの可能性はあり)を満たす単語埋め込みを作ることを目指していますから、このchiveを基にした1次元単語埋め込みの出力傾向は正しいと言えそうです。
1次元単語埋め込みから見る単語の学習状況
他にも出力結果の抜粋を見ると序数の埋め込みがあることも確認できます。こうやってchiveの単語ベクトルを眺めているだけでいろんな知見が得られそうです。
例えば、こうして連続している単語が得られていることから、chiveの単語埋め込みは意味の近い単語を上手く学習できていることが見て取れます。仮に単語ベクトルの学習が上手く行っていないWord2vecモデルや学習途中のモデルであれば、このような意味の繋がりは見られないでしょう。
次元削減の観点から見た1次元単語埋め込み
従来の次元削減といえば、元のデータをなるべく少ない次元で表す主成分分析が挙げられます。強力な手法ですが、単語ベクトルに当てはめて考えてみると、せっかく高い表現を持ち得るベクトル(例えば300次元)を学習したのに、その次元を解釈できていない状態で削減してしまうと、筆者はもはや何を見せられているのかよく分からなくなってしまいます。通常の主成分分析では、解釈できるが冗長な入力が多くある際に、必要な次元が何かを見定めるために主成分に分解する事例が多いように感じます。
今回は、単語埋め込みの各次元には明確な解釈は与えられていないため、そのまま主成分分析を適用するのは難儀に感じます。一方で、健全性を満たす単語埋め込みを、単語ベクトルの全ての次元を使って求めている1次元単語埋め込みは、学習した単語ベクトルの次元に関する情報は失わせず(完全性については失っています)、実装結果で述べたような解釈可能な次元削減済み単語埋め込みを出力しています。また、冒頭で述べた「メモリを食う」、「時間を食う」という課題も、1次元に圧縮されていますので単語ベクトルをそのまま使うよりも十分省メモリ・省コストになっています。
完全性を満たすために必要な要素
1次元の単語埋め込みは解釈可能で有用そうなことがわかりましたが、課題もあります。例えば、著者ご本人の解説資料でも述べられていますが、「動物」という単語の隣における単語は両隣の2つだけです。「動物」は2種類以上いますから、そもそも1次元では類似単語を抜けもれなく表現することはできません。この課題に対して、例えば単語の構造関係を獲得するような手法が求められます。
単語間の上位関係が保持されている日本語Wordnetはありますが、可能であれば教師無しで大量の単言語コーパスから流行りの言葉を含めて、階層構造を学習できるようにしたいですよね。
まとめ
省メモリ・省コスト・解釈可能な1次元単語埋め込みを獲得する手法WordTourを日本語単語埋め込みベクトルchiveに適用しました。実際に健全性が満たされた単語埋め込みであることを出力から確認し、次元圧縮や完全性を満たすために必要な要素について感想レベルですが述べました。
自然言語処理では大規模言語モデルの開発が破竹の勢いで進んでいますが、企業で生成モデルを使うにはリスクや壁がまだまだ多いです。そういった開発を企業で行うのも重要ではあるのですが、個人的にはもうすこし解釈しやすい、かつ、扱いやすくコンパクトで従来より強力なアルゴリズムやモデルを開発して従来のエンジニアリング業務に少しづつ浸透させていけるよう奮闘していきます。
Discussion
一つ質問があるのですが、kaeru39さんが作成した日本語版のプログラムはgitなどに記載されていますか?
日本語版の同じものを再現したいと考えていて、どこから手をつければいいのか分からない状況です。
@ggpさん、ご質問ありがとうございます。
大変恐縮ですが、今回はwordtourのリポジトリの入力ファイルのみを入れ替えただけですので、日本語版のプログラムの公開は行っておりません。
私が行った手順は以下です。
1)wordtourのリポジトリをreadme.mdに従って実行する
2)実行ファイルが英語データだったため、chiVeの日本語分散表現をダウンロードしてきて記事に記載のVectorDumperクラスを実装する、英語データと同じフォーマットであることを確認する
3)入力ファイルを日本語データに差し替えてwordtourを再度実行する
ただ、今回コメントいただきましたので、時間を作ってgithubでの公開も検討したいと思います。ありがとうございます!