はじめに
以前の記事「Juliaで日本語Word2vecを使ってみた」でWord2vec(FastText)を使ってみました。FastTextには2つのデータが用意されています。二つの違いは学習元のテキストです。
Embeddings.jlのデフォルトの設定では Common Crawlのデータ(cc.ja.300.vec)が使われます。以前使ったのはこちらです。
Wikipediaのデータ(wiki.ja.vec)を使う設定もあるので、今回試してみました。
※実行環境は、SageMaker Studio Lab です。
FastTextでWikipediaのデータを読み込もうとすると…
まず、必要に応じてパッケージを追加してください。すでに行っているなら不要です。
using Pkg; Pkg.add("Embeddings")
次に、対応している単語ベクトルのデータを確認してみましょう。
language_files(FastText_Text{:ja})
2-element Vector{String}: "FastText ja CommonCrawl Text/cc.ja.300.vec" "FastText ja Wiki Text/wiki.ja.vec"
2つのファイルを扱うことができるのがわかります。この時点では、データはダウンロードされていません。
次に、FastTextのWikipediaデータのダウンロードです。今回は、ファイル番号を指定します。ファイル番号は、上記のファイルをそれぞれ1,2と割り振ります。デフォルト値1は省略できたのですが、今回は、2番目のファイルを取得したいので、明示的に指定します。以前と同じく、ダウンロード済みかどうかを判定して、まだの場合には自動的にダウンロードします。
using Embeddings
const embtable = load_embeddings(FastText_Text{:ja}, 2)
ダウンロードがまだなら、「stdin> 」のようなプロンプトが出ます。半角英字の「y」を入力し、Enterを押下してください。
この後は、少し時間がかかってメッセージが表示されます。前の方には、ダウンロード関連の情報です。
問題は後半です。ダウンロードの後にエラーメッセージが出ています。
どうやら、ダウンロードは成功したものの、Juliaでの読み込みに失敗したようです。
~デバッグしてみました(原因究明の過程は長くなるので略)~
デバッグの結果、行から表記文字列とベクトルを切り出す箇所が問題だということがわかりました。問題のコードは、[Embeddings.jl/src/fasttext.jl 26行目]の「tok = split(line)」です。
split()の話については、前回の記事「Julia:関数split()と日本語文字列で困ったこと」で書いたのですが、FastTextのWikipediaデータに、Unicode カテゴリ Zs に含まれる文字を表記文字列としている行があり、そこに関数split() が区切り指定なしで呼び出されているので、表記文字ごと区切り扱いで消えてしまい、その行のデータが不正扱いになって、エラーになっています。
さて、どうしたものでしょうか。
前回の記事にも書いた通り、そこを修正するので、他の言語圏に影響を与えない確証が得られないので、プログラム修正以外の方法での解決を考えます。
つまり、FastTextのWikipediaデータを加工してしまいましょう。そもそも、一般的な日本語の自然言語処理の際に、空白文字を扱う必要性は感じません。つまり、FastTextのWikipediaデータから表記文字列が、Unicode カテゴリ Zs に含まれる文字であるものを削除してしまいましょう。というか、関数split() で適切に区切られないものを削除してしまえばよいようですね。
先ほどの、load_embeddings() の実行で、ファイル自体はダウンロードされています。次の場所に格納されています。(SageMaker Studio Labの場合です)
/home/studio-lab-user/.julia/datadeps/FastText ja Wiki Text/wiki.ja.vec
そのファイルを加工します。コードは次になります。
embedding_file = "/home/studio-lab-user/.julia/datadeps/FastText ja Wiki Text/wiki.ja.vec"
save_file = "/home/studio-lab-user/.julia/datadeps/FastText ja Wiki Text/wiki.ja.vec.org"
# 別名で保存
mv(embedding_file, save_file)
# 元のファイル名に正常データを書き出し
open(save_file,"r") do fi
# 1行目は、データサイズが入っているので、そのまま書き込み
line = readline(fi)
vocab_size, vector_size = parse.(Int64, split(line))
# 表記+ベクトルで301件のデータ
real_vocab_size = 0
lines = []
for no in 1:vocab_size
line = readline(fi)
toks = split(line)
# 規定のデータサイズの場合のみ書き込み
if length(toks) == 301
push!(lines, line)
real_vocab_size += 1
end
end
open(embedding_file,"w") do fo
# 1行目は、単語数・ベクトルの要素数
println(fo, "$real_vocab_size $vector_size")
# 表記+ベクトルで301件のデータ
for line in lines
println(fo, line)
end
end
end
オリジナルは「wiki.ja.vec.org」として残しておいて、エラーが出ない行だけを書き込んだファイルを新しく「wiki.ja.vec」として作成します。20~30秒くらいかかりました。ちなみに7行が除去されたのですが、すべて空白文字でした。
FastTextでWikipediaのデータを読み込む!
再度読み込みを実行してみます。ダウンロード済みのファイルは存在しているので、あとはJuliaでの読み込みだけが行われます。読み込みだけでも30~40秒くらいかかります。
const embtable = load_embeddings(FastText_Text{:ja}, 2)
Embeddings.EmbeddingTable{Matrix{Float32}, Vector{String}}(Float32[2.4405 1.0757 … 0.46393 0.37888; -2.4237 -2.3466 … -0.25479 0.38643; … ; 4.833 5.9771 … 0.95672 0.38441; -5.1077 -5.3925 … -0.86273 -0.41518], ["、", "</s>", "の", "。", "に", "年", "-", "を", "(", ")" … "cuznsod", "系統だったが", "ツェンワン", "むかさ", "アディグ", "ジャイアントケルプ", "ジブラル", "ryuco", "人の男が逮捕された", "orimattila"])
エラーなく読み込むことができました!
関数を定義
Embeddings.jlがやってくれるのはここまでです。
単語ベクトルの各種操作は、それぞれ定義する必要があります。とりあえず、3つの関数を定義します。(vec、cosine、closest)
# 単語文字列からインデックスを取得するテーブル
const get_word_index = Dict(word=>ii for (ii,word) in enumerate(embtable.vocab))
# 単語文字列をベクトルに変換
function vec(word)
ind = get_word_index[word]
emb = embtable.embeddings[:,ind]
return emb
end
using Distances
cosine(x,y)=1-cosine_dist(x, y) # ベクトル間のcos類似度
# 指定ベクトルに最も近い単語文字列をn件表示する
function closest(v, n=20)
list=[(x,cosine(embtable.embeddings'[x,:], v)) for x in 1:size(embtable.embeddings)[2]]
topn_idx=sort(list, by = x -> x[2], rev=true)[1:n]
return [embtable.vocab[a] for (a,_) in topn_idx]
end
Word2vecを試してみます
1. 定番の「王様-男性+女性」
closest(vec("王様")-vec("男性")+vec("女性"), 5)
5-element Vector{String}: "王様" "!" "遊々" "私" "』"
ダメですね。
2. MathWorks社公式のword2vecの例題を日本語にして試してみます
italy = vec("イタリア")
rome = vec("ローマ")
paris = vec("パリ")
word = closest(italy - rome + paris, 5)
5-element Vector{String}: "パリ" "チリエ" "ルゾ" "ルボ" "ブエ"
ダメダメでした。
3. いろいろな単語間の類似度を計算してみます
cosine(vec("科学"), vec("技術"))
# 0.9988841f0
cosine(vec("米国"), vec("アメリカ"))
# 0.9979304f0
cosine(vec("英国"), vec("イギリス"))
# 0.99799067f0
cosine(vec("東京"), vec("大阪"))
# 0.9986091f0
cosine(vec("英国"), vec("タピオカ"))
# 0.99262315f0
cosine(vec("日本"), vec("ブッシュ"))
# 0.9938446f0
これは大丈夫なのでしょうか…大小関係を見る限りでは、それっぽい値と言えなくもないのですが…
まとめ
FastTextの単語ベクトルのWikipediaデータを利用してみました。使用するにあたってのトラブルなどありましたが、何とか使うことができました。
そして、いくつかを試行してみました。
ベクトルの加算ができなくても、大きな問題ではありません。
しかし、単語間の比較は単語ベクトルを使う場合の重要なポイントになります。Common Crawlデータではそれなりの値だったものが、Wikipediaデータでは微妙な値になっているように見えます。
Common CrawlデータがあるのにWikipediaデータを積極的に採用する意味は見出せませんでした。
苦労してWikipediaデータを使えるようにしたのですが、Embeddings.jlでCommon Crawlデータがデフォルトになっているのは、それなりの意味があるということなのかもしれません。
コメント