mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Char2Vec で文字の特性について調べてみた

ミクシィ Vantage スタジオのAI・ロボットチームで自然言語処理関連の研究開発に関わっている原(@toohsk)です.

Vantage スタジオでは人の感情に寄り添った会話ができるAIの研究開発を通じて,新しいコミュニケーションサービスを生み出そうとしています. 今回, Char2Vec を用いた,文字毎の特性について実験を行いましたので,紹介したいと思います.

Word2Vec とは

Word2Vec は単語をベクトル表現に変換する方法です. これまでは自然言語処理の分野では単語を扱う場合, one-hot の形式で文章内の単語を表現することが多かったです.
しかし,自然言語を機械学習で扱う場合や論文では,最近では必ずといっていいほど Embedding された状態,すなわち単語をベクトルに変換してから機械学習のアルゴリズムに与えています.
ではなぜ one-hot の形式ではなく, Embedding された状態に変換するのでしょうか? それは one-hot の形式では,単語同士の関係性や意味が考慮されていないためです. そこで単語同士の関係性や意味を学習するために Mikolov らにより, Word2Vec の手法が提案されました.
Word2Vec には大きく2つの手法があり, cbow と skipgram の2つが提案 1 されています. この2つの手法での大きな違いは, cbow は着目している単語の周辺語を入力に与え, skipgram は着目している単語を入力に与えるといったものがあります. これらの違いにより, cbow はたくさんの分布情報をなだらかにする効果があり,小さなデータセットでよく使われます. 一方, skipgram は着目する単語と文脈を新しい観点で扱うため,大きなデータセットに有効であると一般的に言われています.
また,Word2Vec 以降も GloVe 2 や FastText 3 というアルゴリズムが提案されています.

Char2Vecとは

Word2Vecを学習させる対象言語が英語の場合,各単語がスペースで区切られているため,スペースやピリオドなどで単語区切りのデータセットを作成します. 一方,日本語の場合は文章中に区切り文字がないため,一般的には分かち書きや形態素解析を行い,必要な品詞だけを抽出して学習させます. しかし,Word2Vec にはアルゴリズムの違いだけでなく,言語モデルに与えるデータセットの種類を変えた Doc2Vec や Paragraph2Vec, Char2Vec があります.
今回,私達は対話文を文字単位に分割し,文字のベクトル表現を学習させる Char2Vec に取り組みました.また,学習は Gensim4 の Word2Vec ライブラリを利用し, cbow として学習させました.

学習結果

学習した Char2Vec の結果を文字分布図とアナロジータスクの2つで見てみたいと思います. 今回の実験では,全・半角や数字,アルファベット,平仮名,カタカナ,漢字など3,731種類の文字を対象としています.

文字分布図

文字分布図はある文字に近い文字を可視化する分布図です.
今回は,'1','a','あ','悲'の4文字についてそれぞれに近い上位50個の文字を表示しました. すこしウォーリーを探せ感があるのですが,頑張って探してみてください.笑

まずは,数字をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183859p:plain

数字が近くに集まっているのがわかるかと思います.図だとわかりにくいのですが,図の中に同じ数字が出てきています.その理由は全角と半角の違いによるものです. しかし,全角は全角同士,半角は半角同士で集まっているので,区別して学習していることが分かります.

次に,アルファベットをベクトル表現にした場合です.

f:id:mixi_engineers:20171005183920p:plain

アルファベットは単体で使われることもありますが,文章においてはアルファベット同士が組み合わさって意味が表現されます.したがって,アルファベットはアルファベットとして集まって表現されているのだと分かります.

そして,平仮名をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183937p:plain

'あ' の近くには平仮名は集まっていないことがわかります.平仮名は単体では意味を表さず,文章中においては漢字と組み合わせて意味を表すので,分布図として漢字の中に紛れているのだと思われます.

最後に,漢字をベクトル表現にした場合です.

f:id:mixi_engineers:20171005183951p:plain

今回は'悲'という漢字を選択しました.目的として,感情や形容する言葉に使われる漢字なので,その近くには同様の漢字がある場合,漢字の意味をベクトルとして表現できているのではないかと考えられます.実際に図を見ると,'悲'という漢字の近くには,'楽'や'優','寂'など似た使われ方をする漢字が近くに集まることが分かりました.

アナロジータスク

アナロジータスクとは,単語の性質を演算として計算できるタスクのことです.
Word2Vec の魅力はたくさんありますが,単語をベクトル表現にできたことで演算が可能になったことは,その一つだと思います. このアナロジータスクの例としてよくあげられるのが, king - man + woman = queen というものです. 今回の Char2Vec でも同様のものができるか確認してみます.
Gensim でアナロジータスクを解く場合, most_similar メソッドを利用します. most_similar メソッドの引数に positive として与えているものは加算, negative として与えているものは減算するものです.
それでは,実験結果を見てみましょう.

まずは,数字でのアナロジータスクを見てみましょう. 今回の例として, 5 + 1 -2 を計算してみました.

model.most_similar(positive=["5", "1"],["2"]) 

[('4', 0.9669662117958069),
 ('3', 0.9577144980430603),
 ('6', 0.9576835036277771),
 ('7', 0.952031672000885),
 ('8', 0.9488403797149658),
 ('0', 0.9165981411933899),
 ('9', 0.8589234352111816),
 ('5', 0.8185327053070068),
 ('7', 0.7915964722633362),
 ('8', 0.7892774939537048)]

驚くことに,正しく計算されています.
数字の組み合わせということもあり,以下の条件でありうる440件分のデータセットを作成し,正答率がどの程度か計算しました.

  • "positive" と "negative" の中の数字は一致しない
  • Char2Vec のため "positive" と "negative" の計算結果は 0~9 までの値域とする
def create_analogy_sets():
    
    analogy_sets = []
    answer_sets = []
    
    for p1 in range(0, 10):
        for p2 in range(0, 10):
            for n1 in range(0, 10):
                
                # p1, p2, n1 が同じ数字になる場合 skip する
                if p1 == p2 or p1 == n1 or n1 == p2:
                    continue
                    
                # p1+p2-n1 が負の整数の場合 skip する
                answer = p1+p2-n1
                if answer < 0:
                    continue
                
                # p1+p2-n1 が10より大きい正の整数の場合 skip する
                if answer > 9:
                    continue
                    
                answer_sets.append(answer)
                analogy_sets.append([p1,p2,n1])
        
    return analogy_sets, answer_sets

def precision(analogy_sets, answer_sets, topn=1):
    correct = 0
    not_num = 0

    for i in range(len(analogy_sets)):
        p1, p2, n1 = analogy_sets[i]
        for predict in model.most_similar([str(p1), str(p2)], [str(n1)], topn=topn):
            try:
                if int(predict[0]) == answer_sets[i]:
                    correct += 1
            except:
                print(predict)
                not_num += 1
                continue

    return correct, not_num

モデルの戻り値を top1 と top3 の2パターンで評価しました.
まず,戻り値の種類についてですが, top1 と top3 の両方において,半角数字が返された率は100\%でした.
次に,計算結果についてですが, top1 での計算結果では,26.8\%の正答率となり, top3 での計算結果では,56.8\%の正答率となりました.
ちなみにですが,数字に限定した上でランダムに回答した場合, top1 では10\%, top3 では 1 - 9/10 * 8/9 * 7/8 = 0.3,すなわち30\%となるはずなので,ランダムで回答するよりも良い結果になっていることが分かります.
学習データが対話データなのでほとんど数字の関係性を教えられておらず, また,文字として数を与え,候補として3,000文字以上ある中から正解できていることに驚きを感じます.

ちなみに,漢数字の場合は,

model.most_similar_cosmul(["五", "一"],["二"]) 

[('四', 0.7392055988311768),
 ('染', 0.7125757932662964),
 ('1', 0.7093852758407593),
 ('先', 0.709115743637085),
 ('三', 0.7078965306282043),
 ('4', 0.706843912601471),
 ('3', 0.7037045955657959),
 ('6', 0.7036187052726746),
 ('九', 0.7021587491035461),
 ('敏', 0.6988415718078613)]

これも回答できています.近しいアナロジーとして漢数字や全角の数字が挙がっていることは驚くべき結果です.ちなみに先程の計算用のデータセットの場合,漢数字が返答されている確率は55.9\%,計算結果の正答率は 9.5\% でした.

最後に,漢字同士によるアナロジータスクです.
漢字は偏や旁などの部首とそれ以外の部分から構成されます. 今回は,その知識を活かしてアナロジータスクをしてみたいと思います.

model.most_similar(["木", "林"])

[('森', 0.6414894461631775),
 ('谷', 0.6174930334091187),
 ('雲', 0.6121721267700195),
 ('川', 0.6072961688041687),
 ('松', 0.6014201045036316),
 ('崎', 0.5943500995635986),
 ('熊', 0.5926244258880615),
 ('村', 0.5882901549339294),
 ('峰', 0.5817473530769348),
 ('冠', 0.5709230303764343)]
model.most_similar(["山", "石"])

[('岩', 0.6268459558486938),
 ('熊', 0.6220624446868896),
 ('松', 0.6124925017356873),
 ('芹', 0.610385000705719),
 ('藍', 0.5979625582695007),
 ('木', 0.5863777995109558),
 ('滝', 0.5861614346504211),
 ('韮', 0.5789919495582581),
 ('井', 0.5788655281066895),
 ('森', 0.5647031664848328)]

驚くことに漢字の構成を組み合わせると意図する漢字が返されました. しかし,必ずしもそうなるわけではありません.

model.most_similar(positive=["草", "楽"])

[('悲', 0.5380357503890991),
 ('憎', 0.47937360405921936),
 ('惜', 0.4682950973510742),
 ('苦', 0.4587554335594177),
 ('休', 0.44919806718826294),
 ('寂', 0.44706428050994873),
 ('忙', 0.4312727451324463),
 ('欲', 0.4239286184310913),
 ('鼻', 0.42341917753219604),
 ('嬉', 0.4128988981246948)]

残念なことに,'草'+'楽'='薬'を期待したのですが, '悲','憎','苦'などの漢字が抽出されました. (薬で辛い経験をしたニューラルネットワークなのでしょうか...)

まとめ

今回は, Char2Vec についてと学習させて得られた興味深い知見を紹介させていただきました.
全体として,概ね数字やアルファベットなどの種類や漢字が持つ意味はある程度近い距離にまとまっており,うまく Enbedding されているようでした. 今回のデータセットは,対話データをベースとしていましたが,データセットを変えることでまた異なる知見が得られるかもしれません.

最後に,弊社では一緒に人の感情に寄り添った会話ができる AI を創出したい自然言語処理 エンジニアを募集しています.ご応募お待ちしております.