🤖

Char2Matrixという概念の提案

2022/07/13に公開

はじめに

機械学習というか自然言語処理というか、その辺りの内容です。自分は全く専門家とかではなく、ただのポエマーなんですが、日々機械的な知能の表現について妄想するにつれて、ある発想が固まってきたので記事にしてみます。検証不足の妄想全開で行くので興味のある方だけ読んでください。

発想の根源は、「自然言語による文って行列積で表現できるのでは?」 という着想にあります。

そうだ、行列にしよう

まず、行列積って魅力的じゃないですか?逆行列が定義できれば元の状態が復元できる(AB(B^-1)=A)し、精度が許す限り、行列積以外の余計な処理を挟まなければ結果をひとつの行列にストレートに押し込める(F=ABCDE)んですよね。

私は常々不満に思ってました。可変長のデータをニューラルネットなんかに食べさせるとき、ただ一次元方向に連結した単語ベクトルを渡すとか、大きめの固定長にして余りを0で埋めるとか、不定サイズの特殊なテンソルとして扱うとか、何か気持ち悪いなと。

しかしですね、これって入力の単位となるデータを行列にしてしまえば解決するんじゃないかな?とふと思ったんですよね。自然言語の文を入力にする場合、その最小単位である文字を行列にしてしまえば、「文字列は文字行列の行列積の結果の行列一つ」で表わせるんじゃないかと。

もちろん、精度上の問題はあるでしょうし、長文になると消失や発散の危険が伴いますが、そこは何とかしましょう…。

固有の文を示す文字のような何か

文字を行列データとして表現するメリットとしては、まず先にも述べたように、ある程度長い文字列から成る文を、文字と同じサイズの正方行列に押し込めることができるという点が挙げられます。それは例えば単純に、

お・前・は・も・う・死・ん・で・い・る (各 n x n) -> 
お前はもう死んでいる (n x n)

というような事なのですが、これはよく考えれば、新しい「お前はもう死んでいる」という一つの文字を発明したようなイメージになります…わかりますかね?

「お前はもう死んでいる」という言葉に対して、固定サイズのデータが作れるわけです。

しかも、その構成要素となった文字が固有の行列データを持っている前提で語るなら、「お前はもう死んでいる」は他の文字、他の並び方で作ったあらゆるデータとは異なる、これまたほぼ固有のデータになりますよね?

「お前はもう死んでいる」は「死んでいるもうお前は」とは明らかに異なるそれぞれ固有の行列データを示します。

しかしながら、行列の構成要素が似ている場合、各文字のデータが単位行列に近い値を持つ行列なら、違う順序でかけあわせた二つの文行列は似た値を取るんです。

行列積は関係を壊さない変形を施す行列

少し想像しづらいかもしれないので詳しく説明すると、行列積というものは、その行列にどんなデータが入っていたとしても、何次元に拡張されたとしても、アフィン変換(拡大縮小、回転、移動、せん断の組み合わせ)の域を出ません(多分)。

たとえば入力Xが画像であるなら、X・W の Wは、ニューラルネット的には重み行列を掛けるとも取れるし、内積の定義を考えれば相関を取っているとも考えられますが、変形の視点から見れば
まさにWが回転なり拡大縮小なりを組み合わせた変形行列であり、WがXを変形させていると見ることができます。

ところで、アフィン変換は、画像全体を回転させたり、拡大縮小させたり、斜めらせたりしますが、ゴムのように部分的に歪ませたり、コーヒーに落としたミルクのようにグルグルかき混ぜて消すような操作はできません。

そこから何が言えるかというと、行列の値が単位行列に近ければ、その積はごくごく微細な、データを殆んど壊さない変形にしかならないということです。

例として、値が極端な回転を示すような行列の場合で、次元も低く3次元くらいで一度考えてみましょうか。X軸を基準に90度回転する行列が、(0, 1, 0)のY向き矢印にかかると、矢印はZか-Zの方向を向きます。

しかしここで、回転の度合いが0.1度くらいだと、どうですかね?Y向き矢印は、若干Zに寄るでしょうけど、ほぼそのままです。でも確かに(0, 1, 0)のままでは居られないので、「変形が施された痕跡は確実に残ります」。

そしてこのような微細な変形を組み合わせたとき何が起こるかというと、例えば大きな変形が二度施されたとき、(0, 1, 0)・X軸基準で90度回転・Y軸基準で90度回転、は(1, 0, 0)か(-1, 0, 0)になるのに対して、(0, 1, 0)・X軸基準で0.1度回転・Y軸基準で0.1度回転はほぼ(0, 1, 0)のままです。

X軸基準で0.1度回転はY軸基準で0.1度回転をほぼ単位行列とみなすので、「積をとっても自分がそのまま残ってX軸基準で0.1度回転の性質が受け継がれます」。Y軸基準で0.1度回転もまた、X軸基準で0.1度回転をほぼ単位行列とみなすので、「積をとっても自分がそのまま残ってY軸基準で0.1度回転の性質が受け継がれます」。

順序が逆になれば当然結果も変わりますが、互いが互いを単位行列程度にしか思っていないという関係性は変わらないので、X軸回転->Y軸回転もY軸回転->X軸回転も、回転量がごくごく小さければ、割と似た(けれども確実に違う)結果を作るはずです。

「概念」の形成

これまで述べたように、単位行列に似たデータを持つ文字行列の積はそれぞれの構成文字に似た、しかし確実に他の文とは違う固有の文行列を作ります。

ところで、話は若干飛びますが、これって「概念」という言葉で表す概念にかなり近いものではないですか?

文行列の性質は、
・文として表わすことができるが、それ自体を一言で述べられないデータ
・いくらでも組み合わせられて、無闇に伸びたり縮んだりしない(固定長)
・表現(構成要素の並び)が変わると意味が変わるが、どこか似ている

概念の意味を辞書(Weblio)で引くと、
・思考において把握される、物事の「何たるか」という部分
・抽象的かつ普遍的に捉えられた、そのものが示す性質
・対象を総括して概括した内容。 あるいは、物事についての大まかな知識や理解

共通点は
・それが何であるかを言葉にできない一言で表したもの
・表現が変わってもどこか似ている=抽象性・普遍性がある

どうですかね。

実装について

それで、このChar2Matrixの考え方を使って何ができるかというと…まだ何も思いついていないに等しいので何とも言えないんですが、役に立つかはともかく実装はできるという検証をしたコードを最後に乗せます。

一番面倒なのが、Char2Matrixなんていう同じ発想をしている人が(恐らく)殆ど居ない(居たら教えてください)という状況なので、例えばWord2Vecに相当するような処理をwikipediaからコーパス作って云々みたいにやらないといけないわけで、非常に面倒です。

ところが、Char2"Vec"という概念自体は存在していて、gensim用のモデルを配布してくれている方が居らっしゃいましたので、有り難く利用させてもらいます。

直リンクは場所が変わってしまうかもなので、ファイルの配布アドレスが書かれた設定ファイルへのリンクを貼らせてもらいます。該当ファイルは下の方のchar2vecってやつです。

https://github.com/lhideki/text-vectorian/blob/master/text_vectorian/config.yml

ここからは自分で書いたコード Python3系・Google Colab(2022/07/14) で動作

# Google Colab用
!pip install gensim==4.2.0
!wget https://dl.dropboxusercontent.com/s/l6nlont4jh7xry3/wikija_char2vec.model

from gensim.models.word2vec import Word2Vec
import numpy as np

if __name__ == '__main__':
    model_filename = 'wikija_char2vec.model'
    model = Word2Vec.load(model_filename)
    size = 48

    np.random.seed(1234)
    mother_matrix = np.random.randn(30, size * size)
    char_matrices = model.wv.vectors @ mother_matrix

    # サイズのみ調整して正規化する = コサイン類似度を変えずに一文字当たりの行列積の影響度を低める
    char_matrices = char_matrices / np.linalg.norm(char_matrices, axis=-1, keepdims=True)
    char_matrices = char_matrices.reshape((-1, size, size))

    def similarity_mats(mat_a, mat_b):
        mat_a = mat_a.reshape(1, size * size)
        mat_a = mat_a / np.linalg.norm(mat_a)
        mat_b = mat_b.reshape(size * size, 1)
        mat_b = mat_b / np.linalg.norm(mat_b)
        return (mat_a @ mat_b)[0, 0]

    def similarity_chars(a, b):
        ia = model.wv.key_to_index[a]
        ib = model.wv.key_to_index[b]
        mat_a = char_matrices[ia]
        mat_b = char_matrices[ib]
        return similarity_mats(mat_a, mat_b)

    def sentence_matrix(s):
        I = np.eye(size)
        mat_s = I
        for c in s:
            ic = model.wv.key_to_index[c]
            mat_ic = char_matrices[ic]
            mat_s = mat_s @ (I + mat_ic)  # 一文字の影響を小さく留めるため単位行列に近づける
        return mat_s - I  # 結果は単位行列に近くなりすぎているので最後に抜く

    def similarity_sentences(a, b):
        mat_a = sentence_matrix(a)
        mat_b = sentence_matrix(b)
        return similarity_mats(mat_a, mat_b)

    print(similarity_chars('犬', '猫'))  # 0.8013464579584988 ノイズ行列で変換したが大きいサイズへの変換なので情報は失われていない

    print(similarity_sentences('私の好物はカレーです', '私の好きな食べ物はカレーです'))  # 0.9323679938482615 字面が似ていれば似る
    print(similarity_sentences('私の好物はカレーです', '私の嫌いな食べ物はカレーです'))  # 0.9187917546859964 同上、しかしちゃんと一致度が低い

    print(similarity_sentences('私は鳥になりたい', '僕はカブトムシになりたい'))  # 0.585239961167018 半分似ているらしい
    print(similarity_sentences('東京特許許可局', '明日は晴れになるでしょう'))  # -0.08350791173806493 出鱈目な組み合わせは無相関を示す場合も

    print(similarity_sentences('お前はもう死んでいる', '貴方は既にお亡くなりになっています'))  # 0.7786918808928232 無相関ではないらしい
    print(similarity_sentences('お前はもう死んでいる', '私の好きな食べ物はカレーです'))  # 0.7192674541997371 ↑よりは似ていないらしい

微妙に意味ありそうな、なさそうな結果が得られています。少なくとも、固有の可変長文字列について固有のベクトルを固定長で得るという点には成功しているのではないでしょうか。

Google Colabで動くように最初にコマンド打ってる(gensimの新しいバージョン入れないとコケる)ので、実際試すときはコード分けて事前に実行するなり、ローカルで試すときは省くなりしてくださいね。

おわりに

実用的に使えるかどうかは未検証なんですが、発想として面白いかなと思い記事にしてみました。それ既に有るよとか有名な手法だよとかだったらこっそり教えてください…。

今回は以上です。

Discussion

ログインするとコメントできます