人工知能とか犬とか

人工知能と犬に興味があります。しょぼしょぼ更新してゆきます。

Word2Vecと多義性

概要

Word2Vecは、単語をベクトルとして表現する手法ですが、「ダウンタウン」のような語は、多義性を持っています。 事実、word2vecにおける「ダウンタウン」は「ウッチャンナンチャン」のようなお笑い芸人コンビよりも、「シーサイド」などの地理的な用語とのほうが類似度が高くなることがあります。 SVDを使って、ベクトル表現された「ダウンタウン」からのお笑い芸人要素を抽出してみたいのです。

準備

ダウンタウン」の多義性

ダウンタウンに類似している単語を、学習済みword2vecモデルを使って調べてみましょう。

from gensim.models.word2vec import Word2Vec
model_path = 'word2vec.gensim.model'
model = Word2Vec.load(model_path)

target_word = "ダウンタウン"
model.wv.most_similar(target_word, topn=10)

きっとこんな出力が得られるはずです。

[('ミッドタウン', 0.8537717461585999),
 ('中心街', 0.8283530473709106),
 ('アップタウン', 0.8175398111343384),
 ('ストリート', 0.8096662759780884),
 ('マンハッタン', 0.8070244193077087),
 ('セントラル・パーク', 0.7880061864852905),
 ('チャイナタウン', 0.7842386364936829),
 ('セントラルパーク', 0.7788770198822021),
 ('メインストリート', 0.772649884223938),
 ('リバーサイド', 0.7724398374557495)]

都市や町を表すような横文字の単語がたくさん出てきます。ダウンタウンはお笑いコンビであると同時に、「繁華街」を表す単語でもあります。 ちなみに、自分がぱっと思いつくお笑い芸人がどこに出てくるかというと、、、

owarai_words = ['タモリ', 'さんま', '明石家さんま', 'ビートたけし', 'サンドウィッチマン',
                'ウッチャンナンチャン', 'とんねるず', 'ナインティナイン', 'バナナマン']

for i, (word, score) in enumerate(model.wv.most_similar("ダウンタウン", topn=100000)):
    if word in owarai_words:
        print(i+1, word, score)
7737 ウッチャンナンチャン 0.37757110595703125
9201 とんねるず 0.36377257108688354
15844 ナインティナイン 0.3186175227165222
16347 サンドウィッチマン 0.31598663330078125
21048 バナナマン 0.29402637481689453
22260 タモリ 0.28919389843940735
27073 明石家さんま 0.27145278453826904
34131 ビートたけし 0.2494104504585266
45667 さんま 0.2202756255865097

なんと7737番目に「ウッチャンナンチャン」が出てきます。類似度も0.38と、そんなに高くありません。 この学習済みモデル内では、ダウンタウンはお笑い芸人というよりは、「繁華街」の意味が強いみたいです。

多義性を抽出する

ダウンタウンのベクトル表現の中に含まれているであろうお笑い芸人要素をなんとかして抽出してみましょう。以下のような戦略を取ります。

  1. とりあえず類似している単語を検索してそこそこの数を集める
  2. あつめた類似語の中で支配的なベクトル成分をSVDによって特定する
  3. SVDで得られた変換によって、元のword vectorから、支配的な成分を差し引く
  4. 差し引いたベクトルで検索をかけてみる

類似している単語を集める

とりあえず30単語くらい集めてみましょう。

model.wv.most_similar(positive=[target_word], topn=30)
[('ミッドタウン', 0.8537717461585999),
 ('中心街', 0.8283530473709106),
 ('アップタウン', 0.8175398111343384),
 ('ストリート', 0.8096662759780884),
 ('マンハッタン', 0.8070244193077087),
 ('セントラル・パーク', 0.7880061864852905),
 ('チャイナタウン', 0.7842386364936829),
 ('セントラルパーク', 0.7788770198822021),
 ('メインストリート', 0.772649884223938),
 ('リバーサイド', 0.7724398374557495),
 ('ベイエリア', 0.7706623077392578),
 ('ロウアー・マンハッタン', 0.7596601247787476),
 ('シュガーランド', 0.7591447234153748),
 ('アヴェニュー', 0.7567015290260315),
 ('ナッシュビル', 0.7514258623123169),
 ('アッパー・イースト・サイド', 0.7497652769088745),
 ('ヴィレッジ', 0.7473465204238892),
 ('タイムズスクエア', 0.7472085952758789),
 ('マンハッタン区', 0.7465125918388367),
 ('スコッツデール', 0.7446001172065735),
 ('大通り', 0.7414740920066833),
 ('ハイウェイ', 0.7391560077667236),
 ('ハイツ', 0.7379716634750366),
 ('ビーチ', 0.7375644445419312),
 ('イースト', 0.7367238998413086),
 ('タウン', 0.7340238094329834),
 ('メイン・ストリート', 0.7318717241287231),
 ('セントポール', 0.7312244176864624),
 ('アベニュー', 0.7290509939193726),
 ('ビレッジ', 0.727695643901825)]

トップ30の単語に含まれる強い成分を抽出してみましょう。

from sklearn.decomposition import TruncatedSVD
similar_words = [x[0] for x in model.wv.most_similar(positive=[target_word], topn=30)]
x = np.array([model.wv[target_word]] + [model.wv[word] for word in similar_words])

# SVDで最も強い成分を取り出す
svd = TruncatedSVD(n_components=1)
transformed_x = svd.fit_transform(x)
extracted_x = svd.inverse_transform(transformed_x)

# プロットしてみる
fig, (ax1, ax2, ax3) = plt.subplots(1, 3)
fig.set_figheight(4)
fig.set_figwidth(16)
ax1.bar(np.linspace(0, 49, 50), x[0])
ax2.bar(np.linspace(0, 49, 50), extracted_x[0])
ax3.bar(np.linspace(0, 49, 50), x[0] - extracted_x[0])

ax1.set_ylim(-0.4, 0.4)
ax2.set_ylim(-0.4, 0.4)
ax3.set_ylim(-0.4, 0.4)

ax1.set_title("ダウンタウン")
ax2.set_title("強い成分")
ax3.set_title("残りの部分")

f:id:wanchan-daisuki:20180617220242p:plain

もともとの「ダウンタウンのベクトル」から「強い成分」を引いて、検索してみましょう。 gensimでは、most_similarメソッドの引数のnegativeに差し引きたい成分を入れることでやりたいことが実現できます。

model.wv.most_similar(positive=[target_word], negative=[extracted_x[0]], topn=30)
[('毎回', 0.6686477661132812),
 ('トーク', 0.6482667922973633),
 ('バラエティ', 0.6234453916549683),
 ('ギャグ', 0.6228033304214478),
 ('番組内容', 0.6177021265029907),
 ('視聴者', 0.6139947175979614),
 ('リスナー', 0.6131852269172668),
 ('笑い', 0.6000913977622986),
 ('ネタ', 0.5957863330841064),
 ('出演者', 0.5891598463058472),
 ('下ネタ', 0.5873293280601501),
 ('人気番組', 0.5856291651725769),
 ('とんねるず', 0.5822530388832092),
 ('面白い', 0.581822395324707),
 ('コント', 0.5800341367721558),
 ('タモリ', 0.575900137424469),
 ('お笑い', 0.5756425857543945),
 ('楽しく', 0.574203610420227),
 ('本音', 0.5713927745819092),
 ('お色気', 0.5688245892524719),
 ('芸人', 0.5651636719703674),
 ('お笑い番組', 0.5635770559310913),
 ('ナレーション', 0.5626435279846191),
 ('喋り', 0.5624589323997498),
 ('聴取者', 0.5620508790016174),
 ('ファン', 0.561782956123352),
 ('深夜番組', 0.5606791377067566),
 ('フリートーク', 0.5589464902877808),
 ('雰囲気', 0.5542364120483398),
 ('トーク番組', 0.5526514053344727)]

お笑いや芸能関係の単語がたくさん出てきましたね。お笑い芸人の名前もちらほら見えます。

繰り返し抽出する

以下のような手順を何度か繰り返すことで、「ダウンタウン」に関わる多義性を炙り出せるのではなかろうか、と考えられます。

  1. negativeリストを用意する
  2. 単語とnegativeリストで検索する
  3. トップn個の類似単語から、強い成分を抽出する
  4. 得られた強い成分で検索し、類似している語を得る
  5. 強い成分をnegativeリストに加え、2.に戻る
from ipywidgets import interact

def extract_similar_words(word, n_iter=10):
    wordvec = model.wv[word]
    negative = []
    
    vectors = [wordvec]
    top_words = [[(word, 1.0)],]
    
    for i in range(n_iter):
        # topnを変えることで、少し結果が変わります。
        similar_words = [x[0] for x in model.wv.most_similar(positive=[word], negative=negative, topn=10)]  # 類似した単語を集める
        x = np.array([wordvec] + [model.wv[word] for word in similar_words])  # 特異値分解の行列を作成する
        svd = TruncatedSVD(n_components=1)
        transformed_x = svd.fit_transform(x)
        extracted_x = svd.inverse_transform(transformed_x)
        
        vectors.append(extracted_x[0])
        top_words.append(model.wv.most_similar(positive=[extracted_x[0]], topn=10)) 
        
        negative.append(extracted_x[0])  # negativeな成分を追加
        
    # interactiveなwidgetを使っています。
    @interact(factor=(0, n_iter))
    def wordvec_plot(factor=0):
        x = np.arange(50)
        plt.bar(x,vectors[factor])
        plt.title("{}つ目の成分".format(factor))
        plt.ylim((-0.5, 0.5))
        plt.text(60, -0.4, '\n'.join(["{}: {}".format(x[0], x[1]) for x in top_words[factor]]))
        
    return wordvec_plot
extract_similar_words("ダウンタウン")

1つ目の成分で検索してみると、街区や地区に関する、主に横文字が出てきます。 f:id:wanchan-daisuki:20180617220646p:plain

2つ目の成分で検索してみると、お笑い芸人が出てきます。 f:id:wanchan-daisuki:20180617220650p:plain

3つ目の成分で検索してみると、よくわからない結果になります。残されたベクトルの成分も平坦になってきているので、多義性を抽出し尽くした、と言えるのかもしれません。 f:id:wanchan-daisuki:20180617220654p:plain

ウマやイヌで試す

ダウンタウン」には、はっきりと違う2つの意味=多義性がありました。 しかし、例えば「馬」は人間と様々かたちで関わる動物です。 それぞれの観点によって類似している単語は異なってくるはずです。

  • 哺乳類としての馬 → 様々な哺乳類
  • 食べ物としての馬 → 様々な食用動物
  • 役畜としての馬 → 牛
  • 乗り物としての馬 → 車、自転車
  • 競争するものとしての馬 → フォーミュラカー
  • 戦争の道具としての馬 → 戦車

などということが想像できます。

ウマ

extract_similar_words("ウマ")

1つ目の成分で検索してみると、草食動物や家畜っぽい動物が出てきます。 f:id:wanchan-daisuki:20180617222913p:plain

2つ目の成分で検索してみると、競馬っぽい単語が出てきます。 f:id:wanchan-daisuki:20180617222555p:plain

ちなみに、「ウマ」ではなく「馬」で検索してみると、当然ですが、全く違う結果が得られます。

1つ目の成分では、競馬系の単語が並びます。 f:id:wanchan-daisuki:20180618212030p:plain

2つ目の成分で検索してみると、乗り物としての馬や戦(いくさ)を意識させるような単語が出てきましたね。 f:id:wanchan-daisuki:20180618212037p:plain

イヌ

ついでに犬でもやってみましょう。

extract_similar_words("イヌ")

1つ目の成分で検索してみると、動物や昆虫が出てきました。「イヌ」自身も類似後のトップ10に出現しています。 f:id:wanchan-daisuki:20180618212236p:plain

2つ目の成分で検索してみると、、、なぜか法律や病気に関する単語が出てきます。1つ目の成分の類似語に「イヌ」そのものが含まれていたことを考えると、1つ目の成分で「イヌ」の意味はほとんど説明できている、と言えるのかもしれません。 f:id:wanchan-daisuki:20180618212242p:plain

まとめ

Word2Vecで得られた埋め込みベクトルを分析し、単語の中に含まれている多義性ごとに類似単語を得ることができることを確認しました。 また、現実において多様な扱われ方をする「馬」にも、多義性のようなものが含まれていることがわかりました。

Word2Vecを含めたEmbeddingは様々な自然言語処理の深層学習に必須のテクニックですが、単体で見ても分析しがいのある題材ですね。