金融と工学のあいだ

興味関心に関するメモ(機械学習、検索エンジン、プログラミングなど)

単語の分散表現のチュートリアル

単語の意味の表現

なぜ単語の意味を表現する必要があるのか

  1. 表現することで機械に処理させることができるから
    • 機械に処理を行わせるためには、処理したい対象を数値列として表現しなくてはなりません。例えば、私たちが普段見ている画像も各画素のRGBの値として表現しており、その数値列を用いて記録を行ったり、描画したりしています。
    • 自然言語においても、同様のことが言えます。自然言語の意味を機械に処理させたい場合には、その意味を数値列で表現しなくてはなりません。
  2. 単語が意味を成す最小の構成単位だから
    • 自然言語の場合、意味を成す最小の構成単位は「単語」です。意味の最小構成単位である単語を表現できれば、その組み合わせである文やパラグラフ、文章の意味を表現することはそこまで難しくないのでは?(本当は難しいのですが…)と思えるでしょう。
    • 上記の理由から、単語の意味を表現するモチベーションは高いです。

機械による単語と意味の表現方法

  • ここで今すぐに単語の意味の表現の話にうつるべきですが、そもそもどのように単語を機械的に扱ってきたか振り返ろうと思います。
  • 例として、「動物」、「食べ物」、「犬」、「猫」、「猿」、「人間」、「トマト」、「りんご」、「バナナ」、「肉」の計10単語を扱います。
    1. idによる表現
      • これは最もシンプルな方法であり、単純に単語それぞれにユニークな番号を割り当てていくだけです。

        • 例:左から右にidを0から割り当てていくと、id=0は動物、id=1は食べ物、・・・、id=9は肉となる。

            words = [
                    "animal",
                    "food",
                    "dog",
                    "cat",
                    "monkey",
                    "human",
                    "tomato",
                    "apple",
                    "banana",
                    "meat"
                    ]
          
            print(words[0])  # animal
            print(words[2])  # dog
          
      • この方法のメリットは、誰でもすぐに思いつく方法であり、とても単純の方法であることです。

        1. 1つの数字(id)でどの単語か表現ができる
        2. 機械としても、1つの数値で単語を表現できるのでメモリ効率が良い
      • しかし、この方法には大きな問題があります。それはここで単語に割り当てられるidと単語の意味には何の関係もないことです。
        • 例:「動物(id=0)」に対して「食べ物(id=1)」と「犬(id=2)」だと食べ物の方がidが近いが、それは動物という単語に対して「食べ物」の方が「犬」より意味が近いことを表現しているわけではない
      • 特にこの問題は機械学習などの数値解析的手法を適用するときに明らかになります。(例えば、数直線状に上記単語がidの位置に配置されている図を想像して、それを意味のまとまりごとに分断する線を引けるか、考えてみるといいかもしれません。)
    2. 局所表現(local representation)
      • idによる表現は確かに単純ですが、大きな問題を抱えています。この問題を解決するために局所表現、特にその代表的な表現方法「one-hot表現」があります。
      • 全単語数の長さのvectorをそれぞれの単語に用意し、idの位置の値だけ1にし、残りを0にします。

        • 例:[1 0 0 0 0 0 0 0 0 0]は動物、[0 1 0 0 0 0 0 0 0 0]は食べ物、・・・、[0 0 0 0 0 0 0 0 0 1]は肉となります。

            words = [
                    "animal",
                    "food",
                    "dog",
                    "cat",
                    "monkey",
                    "human",
                    "tomato",
                    "apple",
                    "banana",
                    "meat"
                    ]
          
            representations = {
                    "animal":[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                    "food":[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
                    "dog":[0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
                    "cat":[0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
                    "monkey":[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
                    "human":[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
                    "tomato":[0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
                    "apple":[0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
                    "banana":[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
                    "meat":[0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
                    }
          
            print(representations[words[0]])  # [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
            print(representations[words[2]])  # [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
          
      • この方法のメリットは以下になります。

        1. idによる表現よりやや複雑になったが、全単語数を数えた後、順に1つずつone-hot表現を単語に割り当てるだけで良いので単純に生成できる
        2. 全ての単語が空間上で等距離に配置され、単語の距離が近い・遠いという状態がなくなり、idによる表現で問題になっていたことが無視できる
      • しかし、この方法も依然として大きな問題を抱えています。
        1. 全ての単語を等距離に配置してしまったため、似たような意味の単語も全く異なった単語として扱われる
          • 例:動物は[1 0 0 0 0 0 0 0 0 0]、犬は[0 0 1 0 0 0 0 0 0 0]であるが、この2単語の距離は他の単語と較べても変わらず、その他の単語と同様完全に数値的に独立した単語として扱われてしまう。
        2. 全単語数に応じてone-hot表現の次元数が線形に増えてしまう
        3. あらかじめ全単語数を知っていないとone-hot表現を作成できない
          • 例:1度one-hot表現を作った後、単語を追加すると新たにone-hot表現を全単語に対してつくりなおさないといけない
    3. 分散表現(distributed representation)、またな単語埋め込み(word embeddings)
      • idによる表現も局所表現も問題を抱えています。その共通する問題として、単語の意味を適切に表現できていないということが挙げられると思います。
      • この問題に対応するため、分散表現、もしくは単語埋め込みと呼ばれる手法が生まれました。
      • 分散表現は、単語を高次元の実数ベクトルで表現する手法です。この時、それぞれの単語は意味が近いほど距離が近くなるように実数ベクトルが割り当てられていきます。

        • 例:今回はグラフ化できるように各単語の次元数を2にしました。

            import matplotlib.pyplot as plt
          
            words = [
                    "animal",
                    "food",
                    "dog",
                    "cat",
                    "monkey",
                    "human",
                    "tomato",
                    "apple",
                    "banana",
                    "meat"
                    ]
          
            representations = {
                    "animal":[-0.3, 0.2],
                    "food":[0.4, -0.1],
                    "dog":[-0.6, 0.4],
                    "cat":[-0.5, 0.5],
                    "monkey":[-0.5, 0.1],
                    "human":[0.0, 0.2],
                    "tomato":[0.6, -0.3],
                    "apple":[0.5, -0.5],
                    "banana":[0.3, -0.5],
                    "meat":[0.1, -0.6]
                    }
          
            print(representations[words[0]])  # [-0.3, 0.2]
            print(representations[words[2]])  # [-0.6, 0.4]
          
            for w in words:
                plt.scatter(representations[w][0], representations[w][1])
                plt.annotate(w, xy=(representations[w][0], representations[w][1]))
            plt.show()
          
      • f:id:kumechann:20170626182224p:plain

      • この方法のメリットは以下が挙げられます
        1. もし単語の意味にしたがって正しく実数ベクトルが割り当てられていた場合、別々の単語でも意味が近い単語であれば近い実数ベクトルを持つことになり、数値計算機械学習アルゴリズムが同じように扱ってくれることです。
          • 例:グラフを見てみるとおそらく簡単に意味ごとのグループを作ることができるでしょう。k-means等のアルゴリズムを使えば機械も同様にグループを作ることができます。
        2. 実ベクトルの次元数は全単語数とは関係ないので、全単語数に対して線形に実ベクトルの次元数が増えることもないし、あとから単語を追加しても実ベクトルの次元数が増えることもない
      • しかし、この方法にも実は決定的な問題があります。
        • それは単語の意味にしたがって実数ベクトルを単語に割り当てることです。
        • 一見するとそんな方法は簡単に思いつかないような気がしますし、最近まで実応用ができるほど精度が高い、かつ高速な方法はありませんでした。
      • そこをブレイクスルーしたのがword2vecです。次の記事からはword2vecの詳細について扱います。
      • Chainerによる単語の分散表現のチュートリアル - 金融と工学のあいだ