0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【その3】【自然言語処理】単語はベクトルではなく“非線形関数” ― 複素テイラー単語関数言語モデル ― 意味を“観測可能な量子状態”として扱うNLPモデルを作ってみた

0
Last updated at Posted at 2026-06-15

この記事はシリーズものの3回目です。

1回目の記事はこちらです。
2回目の記事はこちらです。

4回目の記事はこちらです。
5回目の記事はこちらです。
6回目の記事はこちらです。

↓1回目の記事:

↓前回(2回目)の記事:

↓次回(4回目)の記事:

↓5回目の記事:

↓6回目の記事:

0. 前回までのあらすじ

$$\huge{「意味」とは「状態」だ。}$$

$$\huge{「単語」とは状態を変換する「非線形作用素」だ。}$$

こう考えると、「文」は「作用素の合成」として理解することが出来るし、
「意味を理解すること」とは「状態の観測」として説明することが出来る。

image.png

image.png

―こう考えて、量子力学の真似事をしながら、新しい自然言語処理モデル「複素テイラー単語関数言語モデル」の理論を組み立てたのが前回までだ。

前回までで、第3章まで書いた。
今回の記事は、第4章から開始する。

4. コーディング

複素テイラー単語関数言語モデルを学習させて実験してみるためのコードを、PyTorch で作ってみよう。

ディレクトリ構成は次のようにする。

ディレクトリ構成とステータス
(カレントディレクトリ)
├ main.py                               # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py        # 🛠️モデルクラス
├ ComplexTaylorWordFunctionLearner.py   # 😴モデルを学習するためのクラス
├ constants.py                          # 🛠️複数ファイルで共有する定数など (コーパス以外)
├ corpus.py                             # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # 😴学習結果や経過を評価するためのクラス
└ test_project.py                       # 😴テスト用コード

ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり

まずは 4-1節~4-3節で、モデルクラス ComplexTaylorWordFunctionLM.py をコーディングする。
次に 4-4節でモデルクラスをテストする。
その後、次回以降の記事でその他のファイルをコーディングしていく。

4-1. モデルクラスの設計

複素テイラー単語関数言語モデルを表す ComplexTaylorWordFunctionLMクラスを作ろう。
このクラスには torch.nn.Module クラスを継承させる。

まずは:

  • 属性 (メソッド及びデータ属性(Javaでいうフィールド)) 概要
  • モデルの入力と出力
  • 損失関数
  • (高速化のためのキャッシュ)

を決めよう。

4-1-1. メソッド一覧

ComplexTaylorWordFunctionLM クラスには次のメソッドを持たせる。
(すべてインスタンスメソッドである。)

メソッド 役割
__init__ (コンストラクタ) モデルのパラメータ(各単語及び3-2-1節で導入した仮想単語$\mathbb{next}$)、初期意味$\ket{0}$、POVM キャッシュ(4-1-3節で解説する) を初期化する
reset_null 仮想単語 $\mathbb{null}$ を恒等関数に戻す。4-2-2節で説明する。
何も受け取らず、何も返さない。
tensor_power 意味 $z$と、$0$以上$k$以下の整数$n$を受け取って 2-4節の $\displaystyle w(z)=\sum_{n=0}^k \left(\frac{A_{w,n} {\rm vec}(z^{\otimes n})}{n!} \right)$ における ${\rm vec}(z^{\otimes n})$ を返す
apply_word 意味 $z$ と単語$w$ を受け取って 意味 $w(z)$ を返す
sequence_meaning 文(単語列のこと。中途半端でもよい)を受け取って その意味 $\ket{文}$ を返す。
例えば「I love you」という文を受け取ったら $\rm you(love(I(\ket{0})))$ を返す
apply_next 意味 $z$ を受け取って $\mathbb{next}(z)$ を返す
(=意味 $\ket{z}$ を受け取って $\ket{\mathbb{next}(z)}$ を返す)
word_meaning 単語$w$を受け取って、$\ket{w}\ (= w(\ket{0}))$ を返す
update_povm_cache POVM キャッシュを更新する。何も受け取らず、何も返さない。
amplitudes_from_meaning 意味$\ket{z}$ を受け取って、単語 $w$ 毎に $\braket{w|G^{-\frac{1}{2}}|\mathbb{next}(z)}$ を求めたものを返す。
但し $\displaystyle G = \sum_{単語} \ketbra{単語}{単語}$である。
それぞれ絶対値を取って$2$乗すれば、各単語 $w$が 次単語として選ばれる確率を平方根POVMで求めた値 (今後「平均値POVMによる次単語予測確率」と呼ぶ)となる。
born_probs_from_meaning 意味$\ket{z}$ を受け取って、単語 $w$ 毎に $\Big|\braket{w|G^{-\frac{1}{2}}|\mathbb{next}(z)}\Big|^2$ を求めたものを返す。
平方根POVMによる次単語予測確率を表す。
log_probs_from_meaning 意味$\ket{z}$ を受け取って、平方根POVMによる次単語予測確率の対数を返す
forward 単語$w$を受け取って、平方根POVMによる次単語予測確率の対数を返す
forward_sequence 文を受け取って、平方根POVMによる次単語予測確率の対数を返す
例えば「急が ば」を受け取ったら、「回れ」の対数確率だけ高くなれば成功だな
povm_normalization_error 何も受け取らず、$\displaystyle \sum_{単語} \left(G^{-\frac{1}{2}}\ketbra{単語}{単語}G^{-\frac{1}{2}}\right)$ を実測し、単位行列$I$ に対する誤差を返す。
(理論的には $\displaystyle \sum_{単語} \left(G^{-\frac{1}{2}}\ketbra{単語}{単語}G^{-\frac{1}{2}}\right)=G^{-\frac{1}{2}}GG^{-\frac{1}{2}}=I$ である)
speak 自然数$n$などを受け取って、$n$語喋る。次単語として確率が高いものを連結する工程を$n$回繰り返す
flat_operator 単語$w$を受け取って、$\displaystyle w(z)=\sum_{n=0}^k \left(\frac{A_{w,n} {\rm vec}(z^{\otimes n})}{n!} \right)$における ${\rm vec}(A_{w,0}), {\rm vec}(A_{w,1}), \cdots, {\rm vec}(A_{w,k})$ を連結した $\displaystyle d\sum_{n=0}^k d^n$ 次元ベクトルを返す
operator_norm 単語$w$を受け取って、${\rm vec}(A_{w,0}), {\rm vec}(A_{w,1}), \cdots, {\rm vec}(A_{w,k})$ を連結した $\displaystyle d\sum_{n=0}^k d^n$ 次元ベクトルのノルムを返す

モデル全体の入出力(文→次単語予測確率の対数)に対応している、最重要メソッドはforward_sequence である。
(つまり、モデルの入力 (文) を受け取って、モデルの出力 (次単語予測確率の対数) を返しているのは、forward_sequence メソッドである。)

4-1-2. 損失関数

モデルの出力(つまりforward_sequence メソッドの出力)は、確率そのものではなく、確率の対数にしていることに注意。
「負の対数尤度損失」(nll_loss などとよく呼ばれる) に確率の対数を渡すと、交差エントロピー損失が返ってくる。

こうして得られる交差エントロピー損失を、今回の学習の損失関数として採用することにしよう。
とはいえ、学習の役割は ComplexTaylorWordFunctionLM クラスではなく、次回の記事で作る ComplexTaylorWordFunctionLearner クラスが担うため、損失関数はそちらで指定される。

4-1-3. POVM キャッシュ

コンストラクタおよび update_povm_cache メソッドに出てくる「POVM キャッシュ」というのは、平方根POVMに必要な $\displaystyle G = \sum_{単語} \ketbra{単語}{単語}$ や $G^{-\frac{1}{2}}$ を毎回(各バッチで)計算するのが重いため、何度か使い回して高速化することを可能にするために導入した仕組みである。
(その場合は当然、本来毎回更新すべき$G^{-\frac{1}{2}}$ の更新をサボることになるわけだから、学習の精度は多少落ちる。よって POVMキャッシュ の更新頻度を落とすことによる高速化と、学習の品質はトレードオフの関係になる)

4-1-4. データ属性一覧

ComplexTaylorWordFunctionLM クラスは次のデータ属性(メソッド以外の属性)を持つ。
(すべて インスタンスデータ属性である。)

変数 種類 内容
V 設定 語彙数
itos 設定 単語id(0,1,2,...) から 単語(文字列) への辞書
stoi 設定 単語(文字列) から 単語id(0,1,2,...) への辞書
d 設定 意味ベクトルの次元
k 設定 単語関数のテイラー展開の最高次数。モデルは単語関数をk次近似することになる
A 単語 単語関数のテイラー係数の行列のリスト。学習パラメータ
Next 仮想単語 仮想単語$\mathbb{next}$のテイラー係数の行列のリスト。学習パラメータ
z0 初期意味 初期意味$\ket{0}$。学習しない固定パラメータ
word_meanings_povm POVM キャッシュ 単語$w$ ごとに $\ket{w}$ を求めたもの。学習しないパラメータ。
学習はしないが update_povm_cacheメソッド で更新される
G_povm POVM キャッシュ $G$である。学習しないパラメータ。
学習はしないが update_povm_cacheメソッド で更新される
G_inv_sqrt_povm POVM キャッシュ $G^{-\frac{1}{2}}$である。学習しないパラメータ。
学習はしないが update_povm_cacheメソッド で更新される

POVMキャッシュであるword_meanings_povm,G_povm,G_inv_sqrt_povm 以外のデータ属性はすべてコンストラクタで初期化される。
POVMキャッシュはupdate_povm_cacheメソッドで初期化、更新される。

4-2. モデルクラスの各メソッドのコーディング

4-2-1. コンストラクタ

コンストラクタがやるべきことは、結局は4-1-4節で列挙したデータ属性(POVMキャッシュ除く)の初期化に過ぎない。
また、POVMキャッシュについても、変数そのものはコンストラクタで作っておいてよいだろう。


まずは引数。

コンストラクタの引数
def __init__(self, vocab, d=16, k=4, next_identity_init=True):

vocabには、単語のリスト (list[str])を指定する。
但し、単語のリストの0番目は必ず "<Null>" とする必要がある。
"<Null>" は簡単に言えばパディングに使う専用の仮想単語 $\mathbb{null}$ だ。
文の長さを揃えるため、短い文の右側に "<Null>" を詰める。

(まあ、$\mathbb{null}$ を表す文字列と対応する単語ID は constants.py というファイルを作ってそこで変更できるようにする。)


次に、親クラスの初期化。
これは torch.nn.Module の子クラスとしてモデルを作るときのお約束なので、特に考えずに行ってよい。

親クラスの初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    super().__init__()
    ...

次に「設定」のデータ属性を初期化しよう。

次のようにすればよい。

コンストラクタで「設定」用データ属性を初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    super().__init__() # さっきやったやつ

    # ここから追加
    self.V = len(vocab)
    self.itos = {i:s for (i,s) in enumerate(vocab)}
    self.stoi = {s:i for (i,s) in enumerate(vocab)}
    self.d = d
    self.k = k

    if self.itos[NULL_ID] != NULL_TOKEN:
        raise ValueError(f"{NULL_TOKEN} must have word ID {NULL_ID}.")

    if d > V:
        raise ValueError(
            "The frame-normalized POVM needs enough word meanings to span the space, "
            f"so this simple version assumes d <= V. Got d={d}, V={V}."
        )
    ...

語彙数が意味ベクトルの次元より小さい場合、平方根POVM が計算できなくなる。
具体的には $\displaystyle G = \sum_{単語} \ketbra{単語}{単語}$ が正則でなくなるので、
平方根POVMで使う $G^{-\frac{1}{2}}$ が存在しなくなってしまう。
そのため、その場合には ValueError を投げている。


次に「単語」のデータ属性を初期化しよう。

次のようにすればよい。

コンストラクタで単語のデータ属性を初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    ...
    # ここから追加
    self.A = nn.ParameterList()

    for n in range(k + 1):
        in_dim = d ** n
        scale = 0.02 / math.sqrt(max(1, in_dim))

        R = scale * torch.randn(V, d, in_dim)
        I = scale * torch.randn(V, d, in_dim)
        A = torch.complex(R, I)

        self.A.append(nn.Parameter(A)) 

    self.reset_null()
    ...

このコードで、self.A は 長さ ktorch.nn.ParameterList となる。
そして self.A[n] は 形状 $(語彙数)\times d \times d^n$ の$3$階ランダムテンソルとして初期化される。

つまり単語$w$のID を$i$ とするなら、self.A[n][i] は $d$行$d^n$列の行列となる。
これは $A_{w,n}$ を表すが、2-4節では確かに$A_{w,n}$ は$d$行$d^n$列の行列だった。
うん、確かに整合している。

ランダム初期値は複素数で、実部と虚部はそれぞれ平均$0$の正規分布に従うが、その標準偏差は $n$ が大きくなると指数関数に従って小さくなるようにしている。

これは、標準偏差をすべて$1$にしていると:

$$
\left|A_{w,n} {\rm vec}(z^{\otimes n})\right|
$$

がだいたい:

$$d^\frac{n}{2}$$

程度になることから逆算して、釣り合いを取るためだ。

また、最後の self.reset_null() は、仮想単語 $\mathbb{null}$ を初期化している。
$\mathbb{null}$ は単語ID 0 を持つので、 self.A[n][0] が更新される。
4-2-2節で説明するが、$\mathbb{null}$ は恒等関数として機能する。


次に仮想単語 $\mathbb{next}$のデータ属性を初期化しよう。

次のようにすればよい。

コンストラクタでnextのデータ属性を初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    ...
    # ここから追加
    self.Next = nn.ParameterList()

    for n in range(k + 1):
        in_dim = d ** n

        if next_identity_init:
            R = torch.zeros(d, in_dim)
            I = torch.zeros(d, in_dim)

            if n == 1:
                R[:, :] = torch.eye(d)
        else:
            scale = 0.02 / math.sqrt(max(1, in_dim))

            R = scale * torch.randn(d, in_dim)
            I = scale * torch.randn(d, in_dim)

        N = torch.complex(R, I)
        self.Next.append(nn.Parameter(torch.complex(R, I)))
    ...

このコードは、self.A のときとほぼ同じことをしている。
特に next_identity_initFalse のときは、
self.A[n] が 形状 $(語彙数)\times d \times d^n$ の$3$階テンソルであるのに対して
self.Next[n] が 形状 $d \times d^n$ の $2$階テンソルである点を除いて、まったく同じだ。

但し next_identity_initTrue の場合は、
self.Next はランダムではなくてほぼすべて $0$ で初期化している、という点も異なる。
但し、self.Next[1] のみ、 $0$でなく、単位行列で初期化する。
このようにすると、 next_identity_initTrue のとき、初期の$\mathbb{next}$ は恒等写像として振る舞う。


次に、「初期意味」のデータ属性を初期化しよう。

次のようにすればよい。

コンストラクタで初期意味のデータ属性を初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    ...
    # ここから追加
    value = (1.0 + 1.0j) / math.sqrt(2 * d)
    z0 = torch.full((d,), value, dtype=torch.cfloat)
    self.register_buffer("z0", z0)
    ...

これは2-6節に従い:

$$\ket{0}:=\frac{1+i}{\sqrt{2d}}\left(
\begin{matrix}
1\\1\\\vdots\\1
\end{matrix}
\right)$$

としている。
self.register_buffer("z0", z0) により、固定パラメータ self.z0 を登録している。


最後に、「POVMキャッシュ」のデータ属性を初期化しよう。

次のようにすればよい。

コンストラクタでPOVMキャッシュのデータ属性を初期化
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    ...
    # ここから追加
    eye = torch.eye(d, dtype=torch.cfloat)
    self.register_buffer(
        "word_meanings_povm", torch.zeros(V, d, dtype=torch.cfloat)
    )
    self.register_buffer("G_povm", eye.clone())
    self.register_buffer("G_inv_sqrt_povm", eye.clone())

eye.clone()clone() メソッドを呼び出しているのは、テンソルをコピーして別のものとして扱う必要があるためである。

ダメな例
self.register_buffer("G_povm", eye)
self.register_buffer("G_inv_sqrt_povm", eye)

のように clone() メソッドを呼ばずに直接渡すと、
self.G_povmself.G_inv_sqrt_povm が同じテンソルになってしまい、
値まで自動的に同じになってしまう恐れがある。

またここで eye を作るときに dtype=torch.cfloat としているのは、複素テンソルとして定義するためである。


コンストラクタ全体のコードは次のとおり。

コンストラクタ全体のコードを表示
ComplexTaylorWordFunctionLMクラスのコンストラクタ
def __init__(self, vocab, d=16, k=4, next_identity_init=True):
    # 1. torch.nn.Module を継承する場合のお約束
    super().__init__()

    # 2. 設定
    ## 2-1. データ属性の初期化
    self.V = len(vocab)
    self.itos = {i:s for (i,s) in enumerate(vocab)}
    self.stoi = {s:i for (i,s) in enumerate(vocab)}
    self.d = d
    self.k = k

    ## 2-2. Null チェック
    if self.itos[NULL_ID] != NULL_TOKEN:
        raise ValueError(f"{NULL_TOKEN} must have word ID {NULL_ID}.")

    ## 2-3. 語彙数と次元の大小関係をチェック
    if d > V:
        raise ValueError(
            "The frame-normalized POVM needs enough word meanings to span the space, "
            f"so this simple version assumes d <= V. Got d={d}, V={V}."
        )

    # 3. 作用素
    ## 3-1. 単語作用素
    self.A = nn.ParameterList()

    for n in range(k + 1):
        in_dim = d ** n
        scale = 0.02 / math.sqrt(max(1, in_dim))

        R = scale * torch.randn(V, d, in_dim)
        I = scale * torch.randn(V, d, in_dim)
        A = torch.complex(R, I)

        self.A.append(nn.Parameter(A))

    ## 3-2. Null作用素
    self.reset_null()

    ## 3-3. Next作用素
    self.Next = nn.ParameterList()

    for n in range(k + 1):
        in_dim = d ** n

        if next_identity_init:
            R = torch.zeros(d, in_dim)
            I = torch.zeros(d, in_dim)

            if n == 1:
                R[:, :] = torch.eye(d)
        else:
            scale = 0.02 / math.sqrt(max(1, in_dim))

            R = scale * torch.randn(d, in_dim)
            I = scale * torch.randn(d, in_dim)

        N = torch.complex(R, I)
        self.Next.append(nn.Parameter(N))

    # 4. 初期意味
    value = (1.0 + 1.0j) / math.sqrt(2 * d)
    z0 = torch.full((d,), value, dtype=torch.cfloat)
    self.register_buffer("z0", z0)

    # 5. POVMキャッシュ
    eye = torch.eye(d, dtype=torch.cfloat)
    self.register_buffer(
        "word_meanings_povm", torch.zeros(V, d, dtype=torch.cfloat)
    )
    self.register_buffer("G_povm", eye.clone())
    self.register_buffer("G_inv_sqrt_povm", eye.clone())

(コンストラクタ全体のコードを表示 ここまで)

4-2-2. reset_null メソッド

reset_null メソッドでは、引数を受け取らず、何も返さないが、
仮想単語 $\mathbb{null}$ を恒等関数に戻す役割を果たす。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのreset_nullメソッド
@torch.no_grad()
def reset_null(self):
    for n in range(self.k + 1):
        self.A[n][NULL_ID].zero_()
        if n == 1:
            self.A[n][NULL_ID, :, :] = torch.eye(
                self.d,
                device=self.A[n].device,
                dtype=self.A[n].dtype,
            )

仮想単語 $\mathbb{null}$ は 単語ID 0 なので、
self.A[(1以外)][0] をすべて $0$ に、
self.A[1][0] を単位行列にする。

@torch.no_grad デコレータは、勾配計算をしなくてよいことを宣言している。


そもそも $\mathbb{null}$ は、学習用のパディングのために用意された仮想単語である。

例えば「ジャイアン 男 ガキ大将 乱暴 悪」という長さ$5$の文と、
「ジャイアン 強い 乱暴」という長さ$3$の文をバッチにまとめて学習したい場合、
文の長さを揃える必要がある。
そこで、短いほうの文を $\mathbb{null}$ で埋めて (=パディングして)
「ジャイアン 強い 乱暴 $\mathbb{null}$ $\mathbb{null}$」
のようにして長さを揃える。

パディングのために入れたに過ぎない $\mathbb{null}$ が文の意味を変えては困るため、
$\mathbb{null}$ は恒等関数 $\ket{z} = \mathbb{null}(\ket{z})$ に固定する。

PyTorchの仕様上、テンソルの一部分だけ学習を停止することが出来ないようだったので、
学習してパラメータを更新した直後に毎回このメソッドを呼ぶことで、$\mathbb{null}$ を恒等関数に戻す必要がある。

4-2-3. tensor_power メソッド

tensor_power では、意味ベクトル $z$ をバッチにまとめた z_batch と、非負整数 n を受け取り:

$${\rm vec}(z^{\otimes n})$$ をバッチにまとめたものを返す。


このメソッドでは、意味ベクトル $z$ を受け取って:

$$1\otimes z \otimes z \otimes \cdots\ (n回繰り返し)$$

つまり:

$$1\underbrace{\otimes z}_{n回繰り返し}$$

を作り、これを flatten して $d^n$ 次元ベクトルにした:

$${\rm vec}(z^{\otimes n})$$

を返せばよい。

但し、バッチにまとめていっぺんに計算したい都合上、
引数はz, n ではなく、 z_batch, n とし、戻り値も ${\rm vec}(z^{\otimes n})$ をバッチにまとめたものとしよう。

次のようなコードにすればよい。

ComplexTaylorWordFunctionLMクラスのtensor_powerメソッド
def tensor_power(self, z_batch, n):
    B = z_batch.shape[0]
    if n == 0:
        return torch.ones(B, 1, dtype=torch.cfloat, device=z_batch.device)

    y_batch = z_batch
    for _ in range(n - 1):
        y_batch = torch.einsum(
            "bi,bj->bij",
            y_batch,
            z_batch
        ).reshape(B, -1)

    return y_batch

引数 z_batch は、$d$次元意味ベクトルを$B$個並べたもの、つまり $B行d列$ の行列(形状 $B\times d$ の$2$階テンソル) を想定している。
但し$B$は、バッチサイズを表す自然数。

引数 n は $0$以上$k$以下の自然数を想定している。

引数 n が $0$ の場合は、$1$次元ベクトル $(1)$ を $B$ 個並べた $B行1列$ の行列を返す。

それ以外の場合は、for文をn回繰り返して得られる$B$行幾列の行列を返す。

この処理を理解するためには torch.einsum について知っておく必要があるだろう。

4-2-2-1. アインシュタインの縮約記法

einsum とは「アインシュタインの縮約記法」といって、添字を使ってテンソルの掛け算を柔軟に表現するための仕組みである。

一般化した説明よりも、先に具体例を考えたほうが理解しやすいので、具体例を用いて説明する。

まず、行列積:

$$C =AB$$

とするとき、 $C_{i,j}$ は:

$$C_{i,j} = \sum_k A_{i,k}B_{k,j}$$

と書ける。これを einsum では:

einsumによる行列積
C = torch.einsum("ik,kj->ij", A, B)

と書く。
"ik,kj->ij" の、矢印の右にある "ij" は、C[i,j] を表している。
矢印の左にある、カンマで区切られた最初の文字列 "ik" は、A[i,k] を表している。
矢印の左にある、カンマで区切られた$2$番目の文字列 "kj" は、B[k,j] を表している。

矢印の右は、何らかの関数${\rm einsum}$の「出力の添字」、
矢印の左は、${\rm einsum}$の「入力の添字」を表す。

この時点で:

$$C_{i,j} = {\rm einsum}(A_{i,k}, B_{k,j})$$

ということが分かるだろう。

さらに${\rm einsum}$では、入力が複数ある場合、それらの積を取る約束になっている。

よって:

$$C_{i,j} = {\rm einsum}(A_{i,k}B_{k,j})$$

だ。

さて、矢印の左(入力)には"k" があるのに、右(出力)には "k" がない。
実は${\rm einsum}$は、このように「出力にしか出て来ない添字」に対して総和を取る関数として定義されている。

よって確かに:

$$C_{i,j} = \sum_k A_{i,k}B_{k,j}$$

となるわけだ。

これがeinsum のすべてである。


今度は逆に、内積 $x = a^\top b$ を表す einsum を考えてみよう。
(これはエルミート内積ではないので注意)

$x = a^\top b$ は:

$$x = \sum_i a_i b_i$$

である。

$\displaystyle \sum_i$ を取っているということは、
矢印の左にのみ"i" が出てきて、右には出て来ないことが分かる。

さらに、入力が $a_i$ と $b_i$ (の積) であることから:

torch.einsum("i,i->???", a, b)

まで分かる。
さらに出力が $x$ (添字なし) であることから、矢印の右の添字もないこと、つまり:

x = torch.einsum("i,i->", a, b)

までわかる。これは確かに「矢印の左にのみ"i" が出てきて、右には出て来ない」 になっている。
これで完成だ。

4-2-2-1. 今回のコードでのアインシュタインの縮約記法

今回:

y_batch = torch.einsum(
    "bi,bj->bij",
    y_batch,
    z_batch
).reshape(B, -1)

としている。

一旦これを:

tmp_batch = torch.einsum(
    "bi,bj->bij",
    y_batch,
    z_batch
)
y_batch = tmp.reshape(B, -1)

に分解して考えようか。

矢印の左に出てくる "b", "i", "j" はすべて矢印の右にも出てきている。
添字が消えないということは、総和を取らないということを意味する。

出力の添字から、出力は tmp_batch[b,i,j] を考えていることが分かる。
また入力の添字から、入力は y_batch[b,i] * z_batch[b,j] を考えていることが分かる。

よって結局は tmp_batch[b,i,j] = y_batch[b,i] * z_batch[b,j] を(b,i,j毎に)考えているに過ぎない。
b はバッチ内の各データを回しているに過ぎないから、tmp[b,:,:] = tmp のように考えて、
tmp[i,j] = y[i] * z[j] と捉えなおしてみようか。
そういえばコードでは最初に y_batch = z_batch、つまり y = z のようにしていたから、
最初は:

y = z
tmp[i,j] = y[i] * z[j]

のようなことが行われるはずだ。

これは:

$${\rm tmp} = z \otimes z$$

そのものだ。
そしてこれが:

y_batch = tmp_batch.reshape(B, -1)

つまり:

y = tmp.reshape(-1)

のように reshape されて、 yz.テンソル積(z).reshape(-1) のような $d^2$ 次元ベクトルになる。

これを $n$回繰り返すので、 y は最終的に z.テンソル積(z).テンソル積(z)...((n回繰り返す))....テンソル積(z).reshape(-1) のような $d^n$ 次元ベクトル${\rm vec}(z^{\otimes n})$になる。

だから、 y_batch は結局${\rm vec}(z^{\otimes n})$を バッチにまとめた(=$B$ 個集めた)、$B$行 $d^n$列の行列となる。

4-2-4. apply_word メソッド

apply_word メソッドでは、単語ID をバッチにまとめた word_id_batch と意味ベクトル $z$ をバッチにまとめた z_batch を受け取り:

$$w(z)$$

をバッチにまとめたものを返す。

($w$ は単語ID に対応する単語)


次のようにすればよい。

ComplexTaylorWordFunctionLMクラスのapply_wordメソッド
def apply_word(self, word_id_batch, z_batch):
    B = z_batch.shape[0]
    y_batch = torch.zeros(
        B, self.d, dtype=torch.cfloat, device=z_batch.device
    )

    for n in range(self.k + 1):
        z_batch_power_n = self.tensor_power(z_batch, n)
        A_batch_n = self.A[n][word_id_batch]
        y_batch = y_batch + torch.einsum(
            "bji,bi->bj", A_batch_n, z_batch_power_n
        ) / math.factorial(n)

    y_batch = y_batch / (
        torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
    )
    return y_batch

まず、戻り値 y_batch は $w(z)$ をバッチ(大きさ$B$) にまとめたものにしたいから、$B$行$d$列の行列になればよい。

だから:

B = z_batch.shape[0]
y_batch = torch.zeros(
    B, self.d, dtype=torch.cfloat, device=z_batch.device
)

ではまず、$B$行$d$列の零行列を作っている。
ここに for ループで足し算していけばよいだろう。

forループでは、 z_batch_power_n がまず ${\rm vec}(z^{\otimes n})$ をバッチにまとめたものになっている。
次に A_batch_n が、$A_{w,n}$ をバッチにまとめたものになっている。
(但し $w$ は word_id_batch 内の単語IDに対応する単語だ。)

そしてz_batch_power_nA_batch_n に対して:

einsum
tmp_batch_n = torch.einsum(
    "bji,bi->bj", A_batch_n, z_batch_power_n
)

を考える。

これはイメージとしては:

tmp_batch_n[b,j] = sum_i(A_batch_n[b,j,i] * z_batch_power_n[b,i])

つまり:

$$
{\rm tmp\_batch\_n}_{b,j} = \sum_i {\rm A\_batch\_n}_{b,j,i}
{\rm z\_batch\_power\_n}_{b,i}$$

ということだ。

ここで ${\rm A\_batch\_n}_b = A_{w,n}$ とみれるし、${\rm z\_batch\_power\_n}_{b} = {\rm vec}(z^{\otimes n})$ ともみれることから、
${\rm tmp\_batch\_n}_{b} = {\rm tmp\_n}$

と置けば:

$$\begin{array}{ll}
{\rm tmp\_n}_j &\displaystyle = \sum_i (A_{w,n})_{j,i}({\rm vec}(z^{\otimes n}))_i
\\
&= (A_{w,n} {\rm vec}(z^{\otimes n}))_j\\
{\rm tmp\_n} &= A_{w,n} {\rm vec}(z^{\otimes n})
\end{array}$$

となる。

よって tmp_batch_n は $A_{w,n} {\rm vec}(z^{\otimes n})$ をバッチにまとめたものといえる。

で、実際のコードでは:

y_batch = y_batch + torch.einsum(
    "bji,bi->bj", A_batch_n, z_batch_power_n
) / math.factorial(n)

としていて、これは:

y_batch = y_batch + tmp_batch_n / math.factorial(n)

のことだから:

$$\frac{
A_{w,n} {\rm vec}(z^{\otimes n})
}{n!}$$

をバッチにまとめたものだけ加算しているんだな。

これを for n in range(self.k + 1): のループでやっているんだから、
ループ終了直後の y_batch は:

$$\sum_{n=0}^k \frac{
A_{w,n} {\rm vec}(z^{\otimes n})
}{n!}$$

つまり:

$$w(z)$$

をバッチにまとめたものになる。

そして最後:

ループを終えた直後
y_batch = y_batch / (
    torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
)

とすることで、$w(z)$ をそれぞれ大きさ $1$ に強制して、これをバッチにまとめたものを y_batch として:

return y_batch

している。

4-2-5. sequence_meaning メソッド

sequence_meaning メソッド は 文を表す単語IDの列をバッチにまとめた word_seq_batch を受け取り、 $\ket{文}$ をバッチにまとめたものを返す。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのsequence_meaningメソッド
def sequence_meaning(self, word_seq_batch):
    B, T = word_seq_batch.shape
    z_batch = self.z0.unsqueeze(0).repeat(B, 1)

    for t in range(T):
        id_batch = word_seq_batch[:, t]
        z_batch = self.apply_word(id_batch, z_batch)

    return z_batch

まずB はバッチサイズ、 T は文の長さになる。
そして:

z_batch = self.z0.unsqueeze(0).repeat(B, 1)

z_batch は初期意味ベクトル self.z0 を複製してバッチ内の各データに初期意味ベクトル$\ket{0}$を割り当てている。
z_batch の形状は $B\times d$ である。

そしてfor文で順番に self.apply_word していくことで、最終的に z_batch
$\ket{文}$ をバッチにまとめたものになる。

for文で
id_batch = word_seq_batch[:, t] は $w_t$ をバッチにまとめたものを表す。
それを self.apply_word(id_batch, z_batch) することで $w_t(\ket{z})$ をバッチにまとめたものを求めてこれを新しい $\ket{z}$ としている。
これを繰り返すことで最終的に$w_{T-1}(\cdots(w_1(w_0(\ket{0}))))$, つまり$\ket{文}$をバッチにまとめたものが得られる。

4-2-6. apply_next メソッド

apply_next メソッドでは、意味ベクトル $z$ をバッチにまとめた z_batch を受け取り:

$$\mathbb{next}(z)$$

をバッチにまとめたものを返す。


次のようにすればよい。

ComplexTaylorWordFunctionLMクラスのapply_nextメソッド
def apply_next(self, z_batch):
    B = z_batch.shape[0]
    y_batch = torch.zeros(
        B, self.d, dtype=torch.cfloat, device=z_batch.device
    )

    for n in range(self.k + 1):
        z_batch_power_n = self.tensor_power(z_batch, n)
        Next_n = self.Next[n]
        y_batch = y_batch + torch.einsum(
            "ji,bi->bj", Next_n, z_batch_power_n
        ) / math.factorial(n)

    y_batch = y_batch / (
        torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
    )
    return y_batch

正直、apply_word メソッドとやっていることは大して変わらない。

B = z_batch.shape[0]
y_batch = torch.zeros(
    B, self.d, dtype=torch.cfloat, device=z_batch.device
)

ではまず、$B$行$d$列の零行列を作っている。
ここに for ループで足し算していけばよいだろう。

for ループでは、 z_batch_power_n がまず ${\rm vec}(z^{\otimes n})$ をバッチにまとめたものになっている。
次に Next_n が、$\mathbb{next}_n$ になっている。

そして z_batch_power_nNext_n に対して:

einsum
tmp_batch_n = torch.einsum(
    "ji,bi->bj", Next_n, z_batch_power_n
)

を考える。
これは:

$${\rm tmp\_batch\_n}_{b,j} = \sum_i {\rm Next\_n}_{j,i}{\rm z\_batch\_power\_n}_{b,i}$$

である。

ここで ${\rm Next\_n} = \mathbb{next}_n$ であるし、${\rm z\_batch\_power\_n}_{b} = {\rm vec}(z^{\otimes n})$ ともみれることから、
${\rm tmp\_batch\_n}_{b} = {\rm tmp\_n}$

と置けば:

$$\begin{array}{ll}
{\rm tmp\_n}_j &\displaystyle = \sum_i (\mathbb{next}_n)_{j,i}({\rm vec}(z^{\otimes n}))_i
\\
&= (\mathbb{next}_n {\rm vec}(z^{\otimes n}))_j\\
{\rm tmp\_n} &= \mathbb{next}_n {\rm vec}(z^{\otimes n})
\end{array}$$

となる。

よって、上記の einsum は$\mathbb{next}_n {\rm vec}(z^{\otimes n})$ をバッチにまとめたものを求めていると言える。

実際には:

y_batch = y_batch + torch.einsum(
    "ji,bi->bj", Next_n, z_batch_power_n
) / math.factorial(n)

としているから:

$$\frac{
\mathbb{next}_n {\rm vec}(z^{\otimes n})
}{n!}$$

をバッチにまとめたものだけ加算している。

これを for n in range(self.k + 1): のループでやっているんだから、
ループ終了直後の y_batch は:

$$\sum_{n=0}^k \frac{
\mathbb{next}_n {\rm vec}(z^{\otimes n})
}{n!}$$

つまり:

$$\mathbb{next}(z)$$

をバッチにまとめたものになる。

そして最後:

ループを終えた直後
y_batch = y_batch / (
    torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
)

とすることで、$\mathbb{next}(z)$ をそれぞれ大きさ $1$ に強制して、これをバッチにまとめたものを y_batch として:

return y_batch

している。

4-2-7. word_meaning メソッド

word_meaning メソッドは、単語ID をバッチにまとめた word_id_batch を受け取り、
各単語に対応する意味ベクトルをバッチにまとめたものを返す。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのword_meaningメソッド
def word_meaning(self, word_id_batch):
    z_batch = self.z0.unsqueeze(0).repeat(len(word_id_batch), 1)
    return self.apply_word(word_id_batch, z_batch)

4-2-8. update_povm_cache メソッド

update_povm_cache メソッドは、何も受け取らず、何も返さない。
POVMキャッシュを更新する。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのupdate_povm_cacheメソッド
@torch.no_grad()
def update_povm_cache(self, eps=1e-8):
    word_ids = torch.arange(self.V, device=self.z0.device)

    V_meanings = self.word_meaning(word_ids)
    V_meanings = V_meanings / (
        torch.linalg.norm(V_meanings, dim=1, keepdim=True) + 1e-12
    )

    G = torch.einsum(
        "vi,vj->ij",
        V_meanings,
        torch.conj(V_meanings)
    )
    G = 0.5 * (G + torch.conj(G.T))

    evals, evecs = torch.linalg.eigh(G)
    evals = torch.clamp(evals.real, min=eps)

    G_inv_sqrt = (
        evecs
        @ torch.diag(torch.rsqrt(evals)).to(torch.cfloat)
        @ torch.conj(evecs.T)
    )
    G_inv_sqrt = 0.5 * (G_inv_sqrt + torch.conj(G_inv_sqrt.T))

    self.word_meanings_povm.copy_(V_meanings.detach())
    self.G_povm.copy_(G.detach())
    self.G_inv_sqrt_povm.copy_(G_inv_sqrt.detach())

まず word_ids は 単語IDを順に並べた$1$階テンソルである。
それを self.word_meaning メソッドに入れてその後正規化することで、
V_meanings は各単語に対応する意味ベクトルをまとめたものになる。
言い換えれば、 V_meanings[i] が、$\ket{単語i}$ になる。

G = torch.einsum(
    "vi,vj->ij",
    V_meanings,
    torch.conj(V_meanings)
)

では:

G[i,j] = sum_v(V_meanings[v,i] * torch.conj(V_meanings)[v,j])

のようになっていることから:

$$G = \sum_{v} \ketbra{単語v}{単語v}$$

を求めていることが分かる。
理論上は $G = G^\dagger$ なのだが、数値上は誤差が出る場合があるので、この後コードでは:

G = 0.5 * (G + torch.conj(G.T))

として、$G = G^\dagger$ を保証している。

続いて:

evals, evecs = torch.linalg.eigh(G)

で $G$ を固有値分解している。
torch.linalg.eigh は、 $G = G^\dagger$ な行列専門で固有値分解する関数だ。

evals は固有値たち、 evecs は固有ベクトルたち。

固有値と固有ベクトルを求めるのは、 $G^{-\frac{1}{2}}$ を求めるためだ。

$G$ の 固有ベクトルを列に並べて作ったユニタリ行列を $U$,
固有値たちを対角成分とした対角行列を $\Lambda$ と書くなら:

$$G = U\Lambda U^\dagger$$

と出来る。

$\Lambda$ の各対角成分を$-\frac{1}{2}$ 乗したものを $\Lambda^{-\frac{1}{2}}$ と呼ぶなら:

$$G^{-\frac{1}{2}} = U\Lambda^{-\frac{1}{2}} U^\dagger$$

となる。
導出は:

$$\begin{array}{ll}
G &= U\Lambda U^\dagger\\
U^\dagger G U &=\Lambda\\
U^{-1} G^{-1} \cdot (U^\dagger)^{-1} &=\Lambda^{-1}\\
U^\dagger G^{-1} U &=\Lambda^{-1}\\
G^{-1} &= U\Lambda^{-1} U^\dagger\\
\end{array}$$

としてまず $G^{-1}$ が得られて:

$$G^{-1} = U\Lambda^{-1} U^\dagger = U\Lambda^{-\frac{1}{2}} U^\dagger U\Lambda^{-\frac{1}{2}} U^\dagger = G^{-\frac{1}{2}}G^{-\frac{1}{2}} $$

とすればよい。

実際:

G_inv_sqrt = (
    evecs
    @ torch.diag(torch.rsqrt(evals)).to(torch.cfloat)
    @ torch.conj(evecs.T)
)

で$G^{-\frac{1}{2}}$を求めている。

そして最後に:

self.word_meanings_povm.copy_(V_meanings.detach())
self.G_povm.copy_(G.detach())
self.G_inv_sqrt_povm.copy_(G_inv_sqrt.detach())

でPOVMキャッシュのデータ属性を更新している。

4-2-9. amplitudes_from_meaning メソッド

amplitudes_from_meaning メソッド は、 意味$\ket{z}$ をバッチにまとめた z_batch を受け取って、単語 $w$ 毎に $\braket{w|G^{-\frac{1}{2}}|\mathbb{next}(z)}$ を求めたリストをバッチにまとめたものを返す。


例えば:

# very: id 1
# thank: id 2
# you: id 3
# much: id 4

word_seq_batch = torch.Tensor([
  [2,3,1],
  [3,0,0]
]) #  [(thank you very), (you <Null>, <Null>)]. batch_size == 2
z_batch = model.sequence_meaning(word_seq_batch)
display(model.amplitudes_from_meaning(z_batch))

とした場合、model.amplitudes_from_meaning(z_batch) は:

$$\begin{array}{ll}
&
\left(\begin{matrix}
\bra{\mathbb{null}}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm very}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm thank}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm you}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm much}G\ket{\rm thank\_you\_very\_\mathbb{next}}\\
\bra{\mathbb{null}}G\ket{\rm you\_\mathbb{null}\_\mathbb{null}\_\mathbb{next}}
&\bra{\rm very}G\ket{\rm you\_\mathbb{null}\_\mathbb{null}\_\mathbb{next}}
&\bra{\rm thank}G\ket{\rm you\_\mathbb{null}\_\mathbb{null}\_\mathbb{next}}
&\bra{\rm you}G\ket{\rm you\_\mathbb{null}\_\mathbb{null}\_\mathbb{next}}
&\bra{\rm much}G\ket{\rm you\_\mathbb{null}\_\mathbb{null}\_\mathbb{next}}
\end{matrix}\right)\\
=&
\left(\begin{matrix}
\bra{\mathbb{null}}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm very}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm thank}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm you}G\ket{\rm thank\_you\_very\_\mathbb{next}}
&\bra{\rm much}G\ket{\rm thank\_you\_very\_\mathbb{next}}\\
\bra{\mathbb{null}}G\ket{\rm you\_\mathbb{next}}
&\bra{\rm very}G\ket{\rm you\_\mathbb{next}}
&\bra{\rm thank}G\ket{\rm you\_\mathbb{next}}
&\bra{\rm you}G\ket{\rm you\_\mathbb{next}}
&\bra{\rm much}G\ket{\rm you\_\mathbb{next}}
\end{matrix}\right)
\end{array}$$

になる


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのamplitudes_from_meaningメソッド
def amplitudes_from_meaning(self, z_batch):
    z_batch_next = self.apply_next(z_batch)

    z_batch_povm = torch.einsum(
        "ij,bj->bi",
        self.G_inv_sqrt_povm,
        z_batch_next
    )

    return torch.einsum(
        "vi,bi->bv",
        torch.conj(self.word_meanings_povm),
        z_batch_povm
    )

まず:

z_batch_povm = torch.einsum(
    "ij,bj->bi",
    self.G_inv_sqrt_povm,
    z_batch_next
)

は、バッチ内の各データに対して:

z_povm = torch.einsum(
    "ij,j->i",
    self.G_inv_sqrt_povm,
    z_next
)

のようなことをしている。
つまり:

$${\rm z\_povm}_i = \sum_j G_{i,j} \ket{\mathbb{next}(z)}_j = (G\ket{\mathbb{next}(z)})_i$$

であるから、z_batch_povm は $G\ket{\mathbb{next}(z)}$ をバッチにまとめたものになっている。

次に:

return torch.einsum(
    "vi,bi->bv",
    torch.conj(self.word_meanings_povm),
    z_batch_povm
)

は、バッチ内の各データに対して:

rtn_data = torch.einsum(
    "vi,i->v",
    torch.conj(self.word_meanings_povm),
    z_povm
)

のようなことをしている。
つまり:

$${\rm rtn\_data}_v = \sum_i \overline{\ket{単語v}}_i(G\ket{\mathbb{next}(z)})_i
=\bra{単語v}G\ket{\mathbb{next}(z)}$$

であるから、rtn_data は、「単語$v$ ごとに$\bra{単語v}G\ket{\mathbb{next}(z)}$ をまとめたリスト」になっている。

そして、その「単語$v$ ごとに$\bra{単語v}G\ket{\mathbb{next}(z)}$ をまとめたリスト」をバッチにまとめたものが戻り値になっている。

4-2-10. born_probs_from_meaning メソッド

born_probs_from_meaning メソッド は、 意味$\ket{z}$ をバッチにまとめた z_batch を受け取って、単語 $w$ 毎に $\mathbb{P}_{\rm next}[w|\ket{z}]$ (意味 $\ket{z}$ が与えられた時に次の単語が $w$ である確率; 平方根POVMによる次単語予測確率)を求めたリストをバッチにまとめたものを返す。


例えば:

# very: id 1
# thank: id 2
# you: id 3
# much: id 4

word_seq_batch = torch.Tensor([
  [2,3,1],
  [3,0,0]
]) #  [(thank you very), (you <Null>, <Null>)]. batch_size == 2
z_batch = model.sequence_meaning(word_seq_batch)
display(model.born_probs_from_meaning(z_batch))

とした場合、model.born_probs_from_meaning(z_batch) は:

$$\begin{array}{ll}
&
\left(\begin{matrix}
\mathbb{P}_{\rm next}[\mathbb{null}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm very}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm thank}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm you}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm much}|\ket{\rm thank\_you\_very}]\\
\mathbb{P}_{\rm next}[\mathbb{null}|\ket{\rm you\_\mathbb{null}\_\mathbb{null}}]
&\mathbb{P}_{\rm next}[{\rm very}|\ket{\rm you\_\mathbb{null}\_\mathbb{null}}]
&\mathbb{P}_{\rm next}[{\rm thank}|\ket{\rm you\_\mathbb{null}\_\mathbb{null}}]
&\mathbb{P}_{\rm next}[{\rm you}|\ket{\rm you\_\mathbb{null}\_\mathbb{null}}]
&\mathbb{P}_{\rm next}[{\rm much}|\ket{\rm you\_\mathbb{null}\_\mathbb{null}}]
\end{matrix}\right)\\
=&
\left(\begin{matrix}
\mathbb{P}_{\rm next}[\mathbb{null}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm very}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm thank}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm you}|\ket{\rm thank\_you\_very}]
&\mathbb{P}_{\rm next}[{\rm much}|\ket{\rm thank\_you\_very}]\\
\mathbb{P}_{\rm next}[\mathbb{null}|\ket{\rm you}]
&\mathbb{P}_{\rm next}[{\rm very}|\ket{\rm you}]
&\mathbb{P}_{\rm next}[{\rm thank}|\ket{\rm you}]
&\mathbb{P}_{\rm next}[{\rm you}|\ket{\rm you}]
&\mathbb{P}_{\rm next}[{\rm much}|\ket{\rm you}]
\end{matrix}\right)
\end{array}$$

になる


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのborn_probs_from_meaningメソッド
def born_probs_from_meaning(self, z_batch, eps=1e-12):
    amps_batch = self.amplitudes_from_meaning(z_batch)
    probs_batch = torch.abs(amps_batch).pow(2)

    return torch.clamp(probs_batch.real, min=eps)

単に、 amplitudes_from_meaning メソッドを呼んでその結果を$2$乗しているに過ぎない。

4-2-11. log_probs_from_meaning メソッド

log_probs_from_meaning メソッド は、 意味$\ket{z}$ をバッチにまとめた z_batch を受け取って、単語 $w$ 毎に $\ln \mathbb{P}_{\rm next}[w|\ket{z}]$ つまり平方根POVMによる次単語予測確率の対数を求めたリストをバッチにまとめたものを返す。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのlog_probs_from_meaningメソッド
def log_probs_from_meaning(self, z_batch, eps=1e-12):
    probs_batch = self.born_probs_from_meaning(z_batch, eps=eps)
    return torch.log(probs_batch + eps)

単に、 born_probs_from_meaning メソッドを呼んでその結果の自然対数を取っているに過ぎない。

4-2-12. forward メソッド

forward メソッド は、 単語ID をバッチにまとめた word_id_batch を受け取って、それぞれの平方根POVMによる次単語予測確率の対数を求めたリストをバッチにまとめたものを返す。


戻り値は:

$$\left(\begin{matrix}
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_id\_batch}_0}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_id\_batch}_0}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_id\_batch}_0}]
&\cdots\\
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_id\_batch}_1}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_id\_batch}_1}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_id\_batch}_1}]
&\cdots\\
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_id\_batch}_2}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_id\_batch}_2}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_id\_batch}_2}]
&\cdots\\
\vdots & \vdots & \vdots & \ddots
\end{matrix}\right)$$

のようになる。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのforwardメソッド
def forward(self, word_ids):
    z_batch = self.word_meaning(word_ids)
    return self.log_probs_from_meaning(z_batch)

4-2-13. forward_sequence メソッド

forward_sequence メソッド は、文を表す 単語ID の列をバッチにまとめた word_seq_batch を受け取って、それぞれの平方根POVMによる次単語予測確率の対数を求めたリストをバッチにまとめたものを返す。


戻り値は:

$$\left(\begin{matrix}
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_seq\_batch}_{0,:}}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_seq\_batch}_{0,:}}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_seq\_batch}_{0,:}}]
&\cdots\\
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_seq\_batch}_{1,:}}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_seq\_batch}_{1,:}}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_seq\_batch}_{1,:}}]
&\cdots\\
\ln \mathbb{P}_{\rm next}[単語0(\mathbb{null})|\ket{{\rm word\_seq\_batch}_{2,:}}]
& \ln \mathbb{P}_{\rm next}[単語1|\ket{{\rm word\_seq\_batch}_{2,:}}]
& \ln \mathbb{P}_{\rm next}[単語2|\ket{{\rm word\_seq\_batch}_{2,:}}]
&\cdots\\
\vdots & \vdots & \vdots & \ddots
\end{matrix}\right)$$

のようになる。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのforward_sequenceメソッド
def forward_sequence(self, word_seq_batch):
    z_batch = self.sequence_meaning(word_seq_batch)
    return self.log_probs_from_meaning(z_batch)

4-2-14. povm_normalization_error メソッド

povm_normalization_error メソッド は何も受け取らず:

$${\rm vec}(G^{-\frac{1}{2}}GG^{-\frac{1}{2}}-I)$$

のユークリッドノルムを返す。

但し $G$ と $G^{-\frac{1}{2}}$ は POVMキャッシュのものを利用する。

診断用メソッド。
戻り値が $0$ に十分近ければ正常。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのpovm_normalization_errorメソッド
def povm_normalization_error(self):
    G_inv_sqrt = self.G_inv_sqrt_povm
    Msum = G_inv_sqrt @ self.G_povm @ G_inv_sqrt
    I = torch.eye(self.d, dtype=torch.cfloat, device=G_inv_sqrt.device)
    return torch.linalg.norm(Msum - I).item()

4-2-15. speak メソッド

speak メソッドは 非負整数 n を受け取って、
確率的に生成された文を文字列として返す。

次単語予測確率に応じてランダムに文を作るわけだ。


学習に関係ないメソッドなので、詳しくは解説しない。

コードを表示する
ComplexTaylorWordFunctionLMクラスのspeakメソッド
@torch.no_grad()
def speak(
    self,
    n,
    prompt_words=None,
    temperature=1.0,
    top_k=None,
    greedy=False,
    return_list=False,
    update_cache=True,
    print_text=True,
):
    if not hasattr(self, "itos") or not hasattr(self, "stoi"):
        raise RuntimeError(
            "model.speak() needs model.itos and model.stoi. "
            "After creating the model, set: model.itos = itos; model.stoi = stoi"
        )

    if n < 0:
        raise ValueError("n must be non-negative")
    if temperature <= 0:
        raise ValueError("temperature must be positive")
    if top_k is not None:
        top_k = int(top_k)
        if top_k <= 0:
            raise ValueError("top_k must be positive or None")
        top_k = min(top_k, self.V)

    was_training = self.training
    self.eval()

    if update_cache:
        self.update_povm_cache()

    device = self.z0.device
    z = self.z0.unsqueeze(0)

    generated = []

    if prompt_words is not None:
        if isinstance(prompt_words, str):
            prompt_words = prompt_words.split()

        for w in prompt_words:
            if w not in self.stoi:
                raise ValueError(f"unknown word: {w}")
            generated.append(w)
            wid = torch.tensor([self.stoi[w]], dtype=torch.long, device=device)
            z = self.apply_word(wid, z)

    for _ in range(int(n)):
        probs = self.born_probs_from_meaning(z).squeeze(0)
        probs = torch.clamp(probs.real, min=1e-12)
        probs = probs / probs.sum()

        if greedy:
            next_id = int(torch.argmax(probs).item())
        else:
            if temperature != 1.0:
                logits = torch.log(probs + 1e-12) / temperature
                probs = torch.softmax(logits, dim=0)

            if top_k is not None:
                values, indices = torch.topk(probs, k=top_k)
                values = values / values.sum()
                sampled_pos = torch.multinomial(values, num_samples=1)
                next_id = int(indices[sampled_pos].item())
            else:
                next_id = int(torch.multinomial(probs, num_samples=1).item())

        next_word = self.itos[next_id]
        generated.append(next_word)

        wid = torch.tensor([next_id], dtype=torch.long, device=device)
        z = self.apply_word(wid, z)

    if was_training:
        self.train()

    if return_list:
        result = generated
    else:
        result = " ".join(generated)

    if print_text:
        print(result)

    return result

(コードを表示するここまで)

4-2-16. flat_operatorメソッド

flat_operatorメソッドは、デフォルトでは 単語ID word_id を受け取って、$\displaystyle w(z)=\sum_{n=0}^k \left(\frac{A_{w,n} {\rm vec}(z^{\otimes n})}{n!} \right)$における ${\rm vec}(A_{w,0}), {\rm vec}(A_{w,1}), \cdots, {\rm vec}(A_{w,k})$ を連結した $\displaystyle d\sum_{n=0}^k d^n$ 次元ベクトルを返すメソッドである。
但し$w$は単語ID word_id に対応する単語。

実際には引数として word_id のほかに adjust_n, normalize という二つのフラグをオプションとして受け取る。デフォルトではどちらも Falseである。

adjust_nTrue の場合:

$${\rm vec}(A_{w,0}), {\rm vec}(A_{w,1}), \cdots, {\rm vec}(A_{w,k})$$

の連結ではなく、これを係数で調整した:

$$\frac{{\rm vec}(A_{w,0})}{\sqrt{d^1}}, \frac{{\rm vec}(A_{w,1})}{\sqrt{d^2}}, \cdots, \frac{{\rm vec}(A_{w,k})}{\sqrt{d^{k+1}}}$$

の連結を返す。

(このようにすると、各次数の係数の「平均的な大きさ」を比較できるようになる。)

normalizeTrue の場合、連結したベクトルを正規化してから返す。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのflat_operatorメソッド
def flat_operator(self, word_id, adjust_n=False, normalize=False):
    chunks = []

    for n in range(self.k + 1):
        A = self.A[n][word_id]
        chunk = A.reshape(-1)

        if adjust_n:
            chunk = chunk / math.sqrt(chunk.numel())

        chunks.append(chunk)

    v = torch.cat(chunks)

    if normalize:
        v = v / (torch.linalg.norm(v) + 1e-12)

    return v

4-2-17. operator_norm メソッド

operator_normメソッドは、 単語ID word_id を受け取って、$\displaystyle w(z)=\sum_{n=0}^k \left(\frac{A_{w,n} {\rm vec}(z^{\otimes n})}{n!} \right)$における ${\rm vec}(A_{w,0}), {\rm vec}(A_{w,1}), \cdots, {\rm vec}(A_{w,k})$ を連結した $\displaystyle d\sum_{n=0}^k d^n$ 次元ベクトルのユークリッドノルムを返すメソッドである。
但し$w$は単語ID word_id に対応する単語。


コードは次のとおり。

ComplexTaylorWordFunctionLMクラスのoperator_normメソッド
def operator_norm(self, word_id):
    v = self.flat_operator(word_id, normalize=False)
    return torch.linalg.norm(v).item()

4-3. モデルクラス全体のコード

長いので、折りたたむ。
このコードを ComplexTaylorWordFunctionLM.py としてカレントディレクトリに保存する。

コードを表示
ComplexTaylorWordFunctionLM.py
import math
import torch
import torch.nn as nn

from constants import NULL_TOKEN, NULL_ID, MAKE_A

# =========================
# Complex Taylor word model
# =========================

class ComplexTaylorWordFunctionLM(nn.Module):
    """Complex Taylor Word Function Language Model.

    Model summary:

        Word        = nonlinear operator

        Meaning     = complex state vector

        Sentence meaning
                    = repeated operator composition

        Observation = frame-normalized (square-root) POVM

        Probability = Born rule

    Each word is represented as a nonlinear Taylor operator

        w(z) = Σ_{n=0}^{k} A_{w,n} z^{\\otimes n} / n!

    acting on semantic states (=meaning) z.

    Meanings are state vectors, while words are operators.
    Next-word prediction is performed
    through a frame-normalized (square-root) POVM.

    The philosophy of this model is described at Qiita:
    
        https://qiita.com/hiratatomotaka/items/3ff5603e5d511eeefc13

    Attributes:
        V (int): count of vocabulary include <Null>
        itos (dict[int, str]): word id to word str
        stoi (dict[str, int]): word str to word id
        d (int): dimension of the semantic state space
        k (int): maximum Taylor order
        
        A (torch.nn.ParameterList):
            Parameters of word operator.
            
            Length: k+1
            A[n] (torch.nn.Parameter):
                shape: (V, d, d ** n)
                dtype: torch.cfloat

        Next (torch.nn.ParameterList):
            Parameters of <Next> operator.

            Length: k+1
            Next[n] (torch.nn.Parameter):
                shape: (d, d ** n)
                dtype: torch.cfloat

        z0 (torch.Tensor): initial meaning; shape (d,), dtype torch.cfloat
        word_meanings_povm (torch.Tensor):
            one of the povm cache; shape (V, d), dtype torch.cfloat
        G_povm (torch.Tensor):
            one of the povm cache; shape (d, d), dtype torch.cfloat
        G_inv_sqrt_povm (torch.Tensor):
            one of the povm cache; shape (d, d), dtype torch.cfloat            
        
    
    """
    def __init__(self, vocab, d=16, k=4, next_identity_init=True):
        """Initialize the Complex Taylor Word Function Language Model.

        Args:
            vocab (list[str]):
                Vocabulary list.

                The first entry must be "<Null>", which is treated as
                a fixed identity operator used for padding.

            d (int):
                Dimension of the semantic state space.

                Meanings are represented as normalized complex vectors:

                    |z> \\in \\mathbb{C}^d

            k (int): maximum Taylor order

            next_identity_init (bool):
                Control initialization of virtual word operator:

                    next(z).

                If True:

                    next(z) = z

                at initialization.

                If False, starts from a random complex Taylor operator.
                
                The next operator transforms semantic states into states
                optimized for next-word prediction.

        Raises:
            ValueError: if vocab[(<Null> ID)] != <Null>
            ValueError:
                if d > V so that frame-normalized POVM couldn't span the space
        """
        # 1. torch.nn.Module を継承する場合のお約束
        super().__init__()

        # 2. 設定
        ## 2-1. データ属性の初期化
        self.V = len(vocab)
        self.itos = {i:s for (i,s) in enumerate(vocab)}
        self.stoi = {s:i for (i,s) in enumerate(vocab)}
        self.d = d
        self.k = k

        ## 2-2. Null チェック
        if self.itos[NULL_ID] != NULL_TOKEN:
            raise ValueError(f"{NULL_TOKEN} must have word ID {NULL_ID}.")

        ## 2-3. 語彙数と次元の大小関係をチェック
        if self.d > self.V:
            raise ValueError(
                "The frame-normalized POVM needs enough word meanings "
                "to span the space, "
                f"so this simple version assumes d <= V. Got d={d}, V={self.V}."
            )

        # 3. 作用素
        ## 3-1. 単語作用素
        self.A = nn.ParameterList()

        for n in range(k + 1):
            A = MAKE_A(self.V, d, n)
            self.A.append(nn.Parameter(A))

        ## 3-2. Null作用素
        self.reset_null()

        ## 3-3. Next作用素
        self.Next = nn.ParameterList()

        for n in range(k + 1):
            in_dim = d ** n

            if next_identity_init:
                R = torch.zeros(d, in_dim)
                I = torch.zeros(d, in_dim)

                if n == 1:
                    R[:, :] = torch.eye(d)
            else:
                scale = 0.02 / math.sqrt(max(1, in_dim))

                R = scale * torch.randn(d, in_dim)
                I = scale * torch.randn(d, in_dim)

            N = torch.complex(R, I)
            self.Next.append(nn.Parameter(N))

        # 4. 初期意味
        value = (1.0 + 1.0j) / math.sqrt(2 * d)
        z0 = torch.full((d,), value, dtype=torch.cfloat)
        self.register_buffer("z0", z0)

        # 5. POVMキャッシュ
        eye = torch.eye(d, dtype=torch.cfloat)
        self.register_buffer(
            "word_meanings_povm", torch.zeros(self.V, d, dtype=torch.cfloat)
        )
        self.register_buffer("G_povm", eye.clone())
        self.register_buffer("G_inv_sqrt_povm", eye.clone())

    @torch.no_grad()
    def reset_null(self):
        """Reset the virtual padding word <Null>.

        The null word is forced to act as the identity map:

            Null(z) = z.

        This constraint is re-applied after every optimizer step so that
        padding tokens never acquire semantic meaning.

        Mathematically:

            A_0(Null) = 0
            A_1(Null) = I
            A_n(Null) = 0   (n >= 2)
        """
        for n in range(self.k + 1):
            self.A[n][NULL_ID].zero_()
            if n == 1:
                self.A[n][NULL_ID, :, :] = torch.eye(
                    self.d,
                    device=self.A[n].device,
                    dtype=self.A[n].dtype,
                )

    def tensor_power(self, z_batch, n):
        """Compute the n-th tensor power of semantic states.

        Examples:

            n = 0: returns 1
            n = 1: returns z
            n = 2: returns vec(z \\otimes z)
            n = 3: returns vec(z \\otimes z \\otimes z)
            ...

        This is used to evaluate Taylor terms

            A_n vec(z^{\\otimes n}).

        Args:
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)
            n (int): order of tensor power

        Returns:
            torch.Tensor: shape (batch_size, meaning_vector_dim**n)
        """
        B = z_batch.shape[0]
        if n == 0:
            return torch.ones(B, 1, dtype=torch.cfloat, device=z_batch.device)

        y_batch = z_batch
        for _ in range(n - 1):
            y_batch = torch.einsum(
                "bi,bj->bij",
                y_batch,
                z_batch
            ).reshape(B, -1)

        return y_batch

    def apply_word(self, word_id_batch, z_batch):
        """Apply a word operator to semantic states.

        For each word w:

            w(z) = Σ_n A_{w,n} vec(z^{\\otimes n}) / n!

        followed by state normalization.

        Args:
            word_id_batch (torch.Tensor): shape (batch_size,)
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)

        Returns:
            torch.Tensor: shape (batch_size, meaning_vector_dim)
        """
        B = z_batch.shape[0]
        y_batch = torch.zeros(
            B, self.d, dtype=torch.cfloat, device=z_batch.device
        )

        for n in range(self.k + 1):
            z_batch_power_n = self.tensor_power(z_batch, n)
            A_batch_n = self.A[n][word_id_batch]
            y_batch = y_batch + torch.einsum(
                "bji,bi->bj", A_batch_n, z_batch_power_n
            ) / math.factorial(n)

        y_batch = y_batch / (
            torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
        )
        return y_batch

    def sequence_meaning(self, word_seq_batch):
        """Compute the meaning of a word sequence.

        Starting from

            |z_init>

        repeatedly apply word operators:

            |z_0> = w0(|z_init>)
            |z_1> = w1(|z_0>)
            ...
            |z_{T-1}> = w{T-1}(|z_{T-2}>)

        The final state |z_{T-1}> is interpreted as the meaning of the sequence.
        (|z_0> differs from self.z0, which means |z_init>)

        Args:
            word_seq_batch (torch.Tensor): shape (batch_size, seq_length)

        Returns:
            torch.Tensor: shape (batch_size, meaning_vector_dim)  
        """
        B, T = word_seq_batch.shape
        z_batch = self.z0.unsqueeze(0).repeat(B, 1)

        for t in range(T):
            id_batch = word_seq_batch[:, t]
            z_batch = self.apply_word(id_batch, z_batch)

        return z_batch

    def apply_next(self, z_batch):
        """Apply the virtual word operator <Next>.

        The next operator has the same Taylor structure as ordinary words:

            next(z) = Σ_n Next_n vec(z^{\\otimes n}) / n!

        Conceptually:
        
            z       = meaning of the current context

            next(z) = state optimized for predicting the next word

        If Next = I:

            next(z) = z / |z|
            
            (|z| should be 1, therefore you can consider that

                next(z) = z
            )

        then prediction is performed directly from semantic meaning.

        Args:
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)

        Returns:
            torch.Tensor: shape (batch_size, meaning_vector_dim)
        """
        B = z_batch.shape[0]
        y_batch = torch.zeros(
            B, self.d, dtype=torch.cfloat, device=z_batch.device
        )

        for n in range(self.k + 1):
            z_batch_power_n = self.tensor_power(z_batch, n)
            Next_n = self.Next[n]
            y_batch = y_batch + torch.einsum(
                "ji,bi->bj", Next_n, z_batch_power_n
            ) / math.factorial(n)

        y_batch = y_batch / (
            torch.linalg.norm(y_batch, dim=1, keepdim=True) + 1e-8
        )
        return y_batch

    def word_meaning(self, word_id_batch):
        """Compute canonical word meanings.

        A word meaning is defined as

            |w> = w(|z_init>)

        where |z_init> is the fixed initial semantic state.

        These word meanings are used to construct the POVM frame.

        Args:
            word_id_batch (torch.Tensor): shape (batch_size,)

        Returns:
            torch.Tensor: shape (batch_size, meaning_vector_dim)
        """
        z_batch = self.z0.unsqueeze(0).repeat(len(word_id_batch), 1)
        return self.apply_word(word_id_batch, z_batch)
    
    @torch.no_grad()
    def update_povm_cache(self, eps=1e-8):
        """Update the cached POVM frame.

        Current word kets are

            |v> = word_meaning(v) = v(|z_init>).

        Then

            G = sum_v |v><v|
            M_v = G^{-1/2}|v><v|G^{-1/2}.

        This cache is intentionally detached from autograd.
        It is typically refreshed before each optimization step
        in the training loop.

        Args:
            eps (float, optional): a small value to prevent 0 division
        """
        word_ids = torch.arange(self.V, device=self.z0.device)

        V_meanings = self.word_meaning(word_ids)
        V_meanings = V_meanings / (
            torch.linalg.norm(V_meanings, dim=1, keepdim=True) + 1e-12
        )

        G = torch.einsum(
            "vi,vj->ij",
            V_meanings,
            torch.conj(V_meanings)
        )
        G = 0.5 * (G + torch.conj(G.T))

        evals, evecs = torch.linalg.eigh(G)
        evals = torch.clamp(evals.real, min=eps)

        G_inv_sqrt = (
            evecs
            @ torch.diag(torch.rsqrt(evals)).to(torch.cfloat)
            @ torch.conj(evecs.T)
        )
        G_inv_sqrt = 0.5 * (G_inv_sqrt + torch.conj(G_inv_sqrt.T))

        self.word_meanings_povm.copy_(V_meanings.detach())
        self.G_povm.copy_(G.detach())
        self.G_inv_sqrt_povm.copy_(G_inv_sqrt.detach())

    def amplitudes_from_meaning(self, z_batch):
        """Compute POVM amplitudes using the cached frame.

            z_next = next(z)
            amp[v]  = <v | G^{-1/2} | z_next>

        where |v>, G, and G^{-1/2} are updated only when
        update_povm_cache() is called.

        Args:
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)

        Returns:
            torch.Tensor: shape (batch_size, vocab_size)
        """
        z_batch_next = self.apply_next(z_batch)

        z_batch_povm = torch.einsum(
            "ij,bj->bi",
            self.G_inv_sqrt_povm,
            z_batch_next
        )

        return torch.einsum(
            "vi,bi->bv",
            torch.conj(self.word_meanings_povm),
            z_batch_povm
        )

    def born_probs_from_meaning(self, z_batch, eps=1e-12):
        """POVM Born probabilities.

            G = sum_v |v><v|
            M_v = G^{-1/2}|v><v|G^{-1/2}
            P_next(v| |z>) = <next(z)|M_v|next(z)>
                   = |<v|G^{-1/2}|next(z)>|^2

        Since sum_v M_v = I, the probabilities are normalized by
        construction. No softmax-like division by sum_u scores is needed.

        Args:
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)
            eps (float, optional):
                minimum probability value used for numerical stability

        Returns:
            torch.Tensor: shape (batch_size, vocab_size)
        """
        amps_batch = self.amplitudes_from_meaning(z_batch)
        probs_batch = torch.abs(amps_batch).pow(2)

        return torch.clamp(probs_batch.real, min=eps)

    def log_probs_from_meaning(self, z_batch, eps=1e-12):
        """Natural logarithm of born_probs_from_meaning().

        Args:
            z_batch (torch.Tensor): shape (batch_size, meaning_vector_dim)
            eps (float, optional): a small value to prevent log(0)

        Returns:
            torch.Tensor: shape (batch_size, vocab_size)
        """
        probs_batch = self.born_probs_from_meaning(z_batch, eps=eps)
        return torch.log(probs_batch + eps)

    def forward(self, word_id_batch):
        """Predict next-word log probabilities from single words.
        Convenience wrapper for single-word inputs.

        This method is mainly useful for analysis and debugging.
        Training normally uses forward_sequence().

        For each word w:

            |w> = w(|z_init>)

        is first constructed as the canonical meaning of the word.

        Then

            log P_next(v| |w>)

        is computed through the POVM observation pipeline:

            |w>
                -> next(|w>)
                -> G^{-1/2}
                -> POVM Born probabilities.

        Args:
            word_id_batch (torch.Tensor): shape (batch_size,)

        Returns:
            torch.Tensor: shape (batch_size, vocab_size)            
        """
        z_batch = self.word_meaning(word_id_batch)
        return self.log_probs_from_meaning(z_batch)

    def forward_sequence(self, word_seq_batch):
        """Predict next-word log probabilities from word sequences.

        Given a sequence

            w0, w1, ..., w{T-1}

        the semantic state is constructed by repeated operator
        composition:

            |w0>               = w0(|z_init>)
            |w0 w1>            = w1(|w0>)
            ...
            |w0 w1 ... w{T-1}> = w{T-1}(|w0 w1 ... w{T-2}>)

        The final state |z> = |w0 w1 ... w{T-1}> is interpreted
        as the meaning of the whole context.

        Next-word probabilities are then computed by

            P_next(v| |z>) = <next(z)|M_v|next(z)>

        where

            M_v = G^{-1/2}|v><v|G^{-1/2}

        is the frame-normalized POVM associated with word meanings.

        Args:
            word_seq_batch (torch.Tensor): shape (batch_size, seq_length)

        Returns:
            torch.Tensor: shape (batch_size, vocab_size)  
        """
        z_batch = self.sequence_meaning(word_seq_batch)
        return self.log_probs_from_meaning(z_batch)

    def povm_normalization_error(self):
        """Perform a diagnostic.
        
            || sum_v M_v - I ||_F for
            M_v=G^{-1/2}|v><v|G^{-1/2}.

        Should be near 0 when G is numerically full rank.

        Returns:
            float: result
        """
        G_inv_sqrt = self.G_inv_sqrt_povm
        Msum = G_inv_sqrt @ self.G_povm @ G_inv_sqrt
        I = torch.eye(self.d, dtype=torch.cfloat, device=G_inv_sqrt.device)
        return torch.linalg.norm(Msum - I).item()

    @torch.no_grad()
    def speak(
        self,
        n,
        prompt_words=None,
        temperature=1.0,
        top_k=None,
        greedy=False,
        return_list=False,
        update_cache=True,
        print_text=True,
    ):
        """Generate n words autoregressively from the trained model.

        Usage after training:

            model.speak(20)
            model.speak(20, prompt_words=["ジャイアン"])
            model.speak(20, temperature=0.8, top_k=5)

        At each step, compute

            P_next(v| |z>) = <next(z)|M_v|next(z)>

        and sample the next word according to that distribution.
        Then update the semantic state (= meaning) by applying the sampled word
        operator v.

        Args:
            n (int): words length

        Returns:
            str | list[str]:
                Generated text, or generated token list if return_list=True.

        Raises:
            ValueError: if arguments are invalid
            RuntimeError: if itos, stoi attribute not found on model
        """
        if not hasattr(self, "itos") or not hasattr(self, "stoi"):
            raise RuntimeError(
                "model.speak() needs model.itos and model.stoi. "
                "After creating the model, set: model.itos = itos; "
                "model.stoi = stoi"
            )

        if n < 0:
            raise ValueError("n must be non-negative")
        if temperature <= 0:
            raise ValueError("temperature must be positive")
        if top_k is not None:
            top_k = int(top_k)
            if top_k <= 0:
                raise ValueError("top_k must be positive or None")
            top_k = min(top_k, self.V)

        was_training = self.training
        self.eval()

        if update_cache:
            self.update_povm_cache()

        device = self.z0.device
        z = self.z0.unsqueeze(0)

        generated = []

        if prompt_words is not None:
            if isinstance(prompt_words, str):
                prompt_words = prompt_words.split()

            for w in prompt_words:
                if w not in self.stoi:
                    raise ValueError(f"unknown word: {w}")
                generated.append(w)
                wid = torch.tensor(
                    [self.stoi[w]], dtype=torch.long, device=device
                )
                z = self.apply_word(wid, z)

        for _ in range(int(n)):
            probs = self.born_probs_from_meaning(z).squeeze(0)
            probs = torch.clamp(probs.real, min=1e-12)
            probs = probs / probs.sum()

            if greedy:
                next_id = int(torch.argmax(probs).item())
            else:
                if temperature != 1.0:
                    logits = torch.log(probs + 1e-12) / temperature
                    probs = torch.softmax(logits, dim=0)

                if top_k is not None:
                    values, indices = torch.topk(probs, k=top_k)
                    values = values / values.sum()
                    sampled_pos = torch.multinomial(values, num_samples=1)
                    next_id = int(indices[sampled_pos].item())
                else:
                    next_id = int(
                        torch.multinomial(probs, num_samples=1).item()
                    )

            next_word = self.itos[next_id]
            generated.append(next_word)

            wid = torch.tensor([next_id], dtype=torch.long, device=device)
            z = self.apply_word(wid, z)

        if was_training:
            self.train()

        if return_list:
            result = generated
        else:
            result = " ".join(generated)

        if print_text:
            print(result)

        return result

    def flat_operator(self, word_id, adjust_n=False, normalize=False):
        """Flatten all Taylor coefficients of a word operator.

        If adjust_n=True, each Taylor-order block is rescaled so that
        higher-order terms do not dominate merely because their parameter
        count grows as d**n.

            A_{w,n}.numel() = d * d**n

        Therefore, each block is divided by sqrt(numel), i.e. its RMS scale
        is compared rather than its raw L2 scale.

        Args:
            word_id (int): a word id
            adjust_n (bool): explained above
            normalize (bool): if should be normalized or not

        Returns:
            torch.Tensor:
                shape (d * sum_{n=0}^k d**n,)
                where d is meaning_vector_dim,
                and k is taylor order.
        """
        chunks = []

        for n in range(self.k + 1):
            A = self.A[n][word_id]
            chunk = A.reshape(-1)

            if adjust_n:
                chunk = chunk / math.sqrt(chunk.numel())

            chunks.append(chunk)

        v = torch.cat(chunks)

        if normalize:
            v = v / (torch.linalg.norm(v) + 1e-12)

        return v

    def operator_norm(self, word_id):
        """Operator vector norm.

        First, flatten all Taylor coefficients of a word operator,
        then get Euclid norm.

        Args:
            word_id (int): a word id

        Returns:
            float: operator norm
        """
        v = self.flat_operator(word_id, normalize=False)
        return torch.linalg.norm(v).item()

(コードを表示 ここまで)

さらに、次のコードを constants.py としてカレントディレクトリに保存する。

constants.py
import math
import torch

# =========================
# Constants
# =========================

NULL_TOKEN = "<Null>"
NULL_ID = 0

def MAKE_A(v, d, n):
    in_dim = d ** n
    scale = 0.02 / math.sqrt(max(1, in_dim))

    R = scale * torch.randn(v, d, in_dim)
    I = scale * torch.randn(v, d, in_dim)
    A = torch.complex(R, I)

    return A

4-4. モデルクラスのテスト

現状のステータスは次のとおり。

ディレクトリ構成とステータス
(カレントディレクトリ)
├ main.py                               # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py        # 👀モデルクラス
├ ComplexTaylorWordFunctionLearner.py   # 😴モデルを学習するためのクラス
├ constants.py                          # ✅複数ファイルで共有する定数など (コーパス以外)
├ corpus.py                             # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # 😴学習結果や経過を評価するためのクラス
└ test_project.py                       # 🛠️テスト用コード

ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり

今回は、 pytest を使って ComplexTaylorWordFunctionLM.py をテストしていこう。

この記事は 単体テストのやり方を解説する記事ではないので、
恐縮ながら pytest の紹介や、テストコードの解説は省略させていただきます。

テストコードが何をやっているのか分からない方は、 他の人が出している pytestの解説記事などを読むことをお勧めします。

pytestをある程度理解した上でこのテストコードを読むと、
テスト内容が理解できるだけでなく、
「モデルクラスの実際の利用方法」もイメージできるようになるはずです。

テストコード test_project.py は次のとおり。

テストコード test_project.py を表示
test_project.py
import pytest
from constants import NULL_TOKEN, NULL_ID
from ComplexTaylorWordFunctionLM import ComplexTaylorWordFunctionLM
import torch
import numpy as np
import math

# =========================
# Test
# =========================

# utils
def norm(tensor):
    try:
        return torch.linalg.norm(tensor)
    except TypeError:
        return np.linalg.norm(tensor)
normalize = lambda tensor: tensor / norm(tensor)    
assert_close = torch.testing.assert_close
tensor = lambda l, dtype=torch.cfloat: torch.tensor(l, dtype=dtype)
def assert_not_close(a, b, **kwargs):
    with pytest.raises(AssertionError) as exc_info:
        assert_close(a, b, **kwargs)

# Model
@pytest.fixture
def vocab():
    return [NULL_TOKEN, "I", "love", "you", "thank", "very", "much"]

@pytest.fixture
def model(vocab):
    return ComplexTaylorWordFunctionLM(vocab, d=4)

def test_Model_init_error(vocab):
    with pytest.raises(ValueError, match="must have word ID") as excinfo:
        vocab_illegal = list(vocab[1:])
        model = ComplexTaylorWordFunctionLM(vocab_illegal, d=4)

    with pytest.raises(
        ValueError, match="POVM needs enough word meanings"
    ) as excinfo:
        model = ComplexTaylorWordFunctionLM(vocab)

def test_Model_init(vocab, model):
    assert model.V == len(vocab)
    assert all(
        i == i_ and s == s_
        for ((i,s), (s_,i_))
        in zip(model.itos.items(), model.stoi.items())
    )
    assert len(vocab) == len(model.itos)
    assert len(model.itos) == len(model.stoi)
    assert model.d == 4
    assert model.k == 4

    A = model.A
    assert isinstance(A, torch.nn.ParameterList)
    assert len(A) == model.k + 1
    assert isinstance(A[0], torch.nn.Parameter)
    for n in range(model.k + 1):
        assert A[n].shape == (model.V, model.d, model.d ** n)
    print(f"\n{model.A[0]=}\n{model.A[1]=}")

    Next = model.Next
    assert isinstance(Next, torch.nn.ParameterList)
    assert len(Next) == model.k + 1
    assert isinstance(Next[0], torch.nn.Parameter)
    for n in range(model.k + 1):
        assert Next[n].shape == (model.d, model.d ** n)
    print(f"\n{model.Next[0]=}\n{model.Next[1]=}")

    z0 = model.z0
    assert isinstance(z0, torch.Tensor)
    assert z0.shape == (model.d,)
    print(f"\n{model.z0=}")
    
    assert isinstance(model.word_meanings_povm, torch.Tensor)
    assert model.word_meanings_povm.shape == (model.V, model.d)
    assert isinstance(model.G_povm, torch.Tensor)
    assert model.G_povm.shape == (model.d, model.d)
    assert isinstance(model.G_inv_sqrt_povm, torch.Tensor)
    assert model.G_inv_sqrt_povm.shape == (model.d, model.d)

def test_Model_reset_null(model):
    # 1. null を破壊する
    with torch.no_grad():
        model.A[0][NULL_ID,:,:].fill_(3.0+4.0j)

    # 2. null が恒等作用素でなくなったことを確認する
    null_meaning = model.word_meaning([NULL_ID])[0]
    assert torch.linalg.norm(model.z0 - null_meaning) > 1e-8

    # 3. reset_null する
    model.reset_null()

    # 4. null が恒等作用素に戻ったことを確認する
    null_meaning = model.word_meaning([NULL_ID])[0]
    assert norm(model.z0 - null_meaning) < 1e-8

def test_Model_tensor_power(model):
    z0 = [1, 2+3j, -4j, 5j-6]
    z1 = [7, 8, 9, 10]
    z_batch = tensor([z0, z1])

    # 0階冪
    assert_close(model.tensor_power(z_batch, 0), tensor([[1],[1]]))

    # 1階冪
    assert_close(model.tensor_power(z_batch, 1), tensor([z0,z1]))

    # 2階冪
    z0_otimes_z0_flatten = [x * y for x in z0 for y in z0]
    z1_otimes_z1_flatten = [x * y for x in z1 for y in z1]
    assert_close(
        model.tensor_power(z_batch, 2),
        tensor([z0_otimes_z0_flatten, z1_otimes_z1_flatten])
    )

    # 3階冪
    z0_otimes_z0_otimes_z0_flatten = [
        x * y for x in z0_otimes_z0_flatten for y in z0
    ]
    z1_otimes_z1_otimes_z1_flatten = [
        x * y for x in z1_otimes_z1_flatten for y in z1
    ]
    assert_close(
        model.tensor_power(z_batch, 3),
        tensor([z0_otimes_z0_otimes_z0_flatten, z1_otimes_z1_otimes_z1_flatten])
    )

def test_Model_apply_word(model):
    # 正規化されていないz
    z0 = [1, 2+3j, -4j, 5j-6]
    z1 = [7, 8, 9, 10]
    z_batch = tensor([z0, z1])
    
    # 正規化されるため、恒等作用素でも出力が変わるはず
    id_batch = tensor([NULL_ID], int)
    assert_not_close(model.apply_word(id_batch, z_batch), z_batch)

    # zを正規化して再試行
    z0 = normalize(tensor(z0)).tolist()
    z1 = normalize(tensor(z1)).tolist()
    z_batch = tensor([z0, z1])

    # <Null>
    id_batch = tensor([NULL_ID], int)
    assert_close(model.apply_word(id_batch, z_batch), z_batch)

    # <Null>, you
    you_id = model.stoi["you"]
    id_batch = tensor([NULL_ID, you_id], int)
    terms = []
    for n in range(model.k + 1):
        A_you_n = model.A[n][you_id].detach().numpy()
        terms.append(
            np.dot(
                A_you_n, model.tensor_power(z_batch, n)[1].numpy()
            ) / math.factorial(n)
        )
    null_z0 = z0
    you_z1 = normalize(np.sum(terms, axis=0)).tolist()
    y_batch = tensor([null_z0, you_z1])
    
    assert_close(model.apply_word(id_batch, z_batch), y_batch)

def test_Model_sequence_meaning(model):
    I_id     = model.stoi["I"]
    love_id  = model.stoi["love"]
    you_id   = model.stoi["you"]
    thank_id = model.stoi["thank"]
    word_seq_batch = tensor([
        [I_id, love_id, you_id],
        [thank_id, you_id, NULL_ID]
    ], int)

    meaning_batch = model.sequence_meaning(word_seq_batch)
    assert isinstance(meaning_batch, torch.Tensor)
    assert meaning_batch.shape == (2, model.d)
    assert meaning_batch.dtype == torch.cfloat

    meaning_I_love_you = meaning_batch[0]
    z_0_batch = model.z0.unsqueeze(0)
    z_I_batch          = model.apply_word(tensor([I_id],    int), z_0_batch)
    z_I_love_batch     = model.apply_word(tensor([love_id], int), z_I_batch)
    z_I_love_you_batch = model.apply_word(tensor([you_id],  int),
                                                             z_I_love_batch)
    z_I_love_you = z_I_love_you_batch[0]
    assert_close(meaning_I_love_you, z_I_love_you)

def test_Model_apply_next(model):
    z0 = normalize(tensor([1, 2+3j, -4j, 5j-6])).tolist()
    z1 = normalize(tensor([7, 8, 9, 10])).tolist()
    z_batch = tensor([z0, z1])

    assert_close(model.apply_next(z_batch), z_batch)

def test_Model_word_meaning(model):
    id_batch = tensor([1,2], int)
    word_meaning_batch = model.word_meaning(id_batch)

    z0_batch = model.z0.unsqueeze(0)
    word_meaning_batch_by_apply_word = model.apply_word(id_batch, z0_batch)

    assert_close(word_meaning_batch, word_meaning_batch_by_apply_word)

def test_Model_update_povm_cache(model):
    # 1. パラメータをランダムに書き換える
    with torch.no_grad():
        for a in model.A:
            a.normal_(mean=0.0, std=1.0)
    model.reset_null()

    # 2. POVMキャッシュが同期されていないことを確認する
    word_meanings = model.word_meaning(torch.arange(model.V))
    word_meanings_np = word_meanings.detach().numpy()
    G_np = np.dot(word_meanings_np.T, np.conj(word_meanings_np))
    G = torch.from_numpy(G_np)

    assert_not_close(word_meanings, model.word_meanings_povm)
    assert_not_close(G, model.G_povm)
    assert_not_close(
        torch.inverse(G), model.G_inv_sqrt_povm @ model.G_inv_sqrt_povm
    )

    # 3. POVM キャッシュを同期する
    model.update_povm_cache()

    # 4. 同期されたことを確認する
    assert_close(word_meanings, model.word_meanings_povm)
    assert_close(G, model.G_povm)
    assert_close(
        torch.inverse(G), model.G_inv_sqrt_povm @ model.G_inv_sqrt_povm
    )

def assert_probs_batch(probs_batch):
    B = probs_batch.shape[0]
    
    # どの値も0以上か
    assert torch.all(torch.min(probs_batch, dim=1).values >= 0)

    # 和はほとんど1か
    assert_close(
        torch.sum(probs_batch, dim=1), tensor([1] * B),
        check_dtype=False
    )
assert_log_probs_batch = lambda log_pb: assert_probs_batch(torch.exp(log_pb))
    

def test_Model_probs_3_methods(model):
    z0 = normalize(tensor([1, 2+3j, -4j, 5j-6])).tolist()
    z1 = normalize(tensor([7, 8, 9, 10])).tolist()
    z_batch = tensor([z0, z1])
    model.update_povm_cache()

    # 1. born_probs_from_meaning メソッド
    probs_batch = model.born_probs_from_meaning(z_batch)
    assert_probs_batch(probs_batch)

    # 2. amplitudes_from_meaning メソッド
    assert_close(
        torch.abs(model.amplitudes_from_meaning(z_batch))**2, probs_batch,
        check_dtype=False
    )

    # 3. log_probs_from_meaning メソッド
    log_probs_batch = model.log_probs_from_meaning(z_batch)
    assert_close(
        log_probs_batch, torch.log(probs_batch)
    )
    
def test_Model_forward(model):
    model.update_povm_cache()
    
    id_batch = tensor([1,2], int)
    assert_log_probs_batch(model.forward(id_batch))

def test_Model_forward_sequence(model):
    model.update_povm_cache()

    I_id     = model.stoi["I"]
    love_id  = model.stoi["love"]
    you_id   = model.stoi["you"]
    thank_id = model.stoi["thank"]
    word_seq_batch = tensor([
        [I_id, love_id, you_id],
        [thank_id, you_id, NULL_ID]
    ], int)

    log_probs_batch = model.forward_sequence(word_seq_batch)
    assert_log_probs_batch(log_probs_batch)

    log_probs_thank_you_null = log_probs_batch[1]
    z_0_batch = model.z0.unsqueeze(0)
    z_thank_batch          = model.apply_word(tensor([thank_id], int),z_0_batch)
    z_thank_you_batch      = model.apply_word(tensor([you_id],   int),
                                                                  z_thank_batch)
    z_thank_you_null_batch = model.apply_word(tensor([NULL_ID],  int),
                                                              z_thank_you_batch)
    assert_close(
        log_probs_thank_you_null,
        model.log_probs_from_meaning(z_thank_you_null_batch)[0]
    )

def test_Model_povm_normalization_error(model):
    model.update_povm_cache()

    err = model.povm_normalization_error()
    assert isinstance(err, float)
    assert torch.abs(tensor(err)) < 1e-4
    print(f"\n{err=}")

def test_Model_speak(model):
    """
    Only do a quick, basic test since this is not a learning related method.
    """
    model.update_povm_cache()

    assert isinstance(model.speak(8, print_text=False), str)
    for i in range(10):
        print(f"\n({i=},){model.speak(i, print_text=False)=}")

    assert isinstance(model.speak(8, return_list=True, print_text=False), list)
    for i in range(10):
        print(f"\n({i=},){model.speak(i, return_list=True, print_text=False)=}")

    with pytest.raises(ValueError, match="n must be non-negative") as exc_info:
        model.speak(-1, print_text=False)

def test_Model_operator_2_methods(model):
    A_one = model.flat_operator(1, adjust_n=False, normalize=False)
    assert A_one.shape == (model.d * sum(model.d**n for n in range(model.k+1)),)

    A_one_adjusted = model.flat_operator(1, adjust_n=True, normalize=False)
    assert_close(
        A_one / A_one_adjusted,
        tensor([
            (model.d * model.d**n)**0.5
            for n in range(model.k + 1)
            for _ in range(model.d *model.d**n)
        ])
    )

    A_one_normalized = model.flat_operator(1, adjust_n=False, normalize=True)
    assert_close(A_one, A_one_normalized * model.operator_norm(1))

(テストコード test_project.py を表示 ここまで)

上記のテストコードを用意した上で、カレントディレクトリにて pytest -s -v を実行した結果、次のようになった。

結果を表示
pytest -s -v
(cd)>pytest -s -v
================================================= test session starts =================================================
platform win32 -- Python 3.13.1, pytest-9.0.2, pluggy-1.6.0 -- C:\xxx\python.exe
cachedir: .pytest_cache
rootdir: (cd)
plugins: anyio-4.9.0
collected 15 items

test_project.py::test_Model_init_error PASSED
test_project.py::test_Model_init
model.A[0]=Parameter containing:
tensor([[[ 0.0000+0.0000j],
         [ 0.0000+0.0000j],
         [ 0.0000+0.0000j],
         [ 0.0000+0.0000j]],

        [[-0.0002-0.0049j],
         [ 0.0075-0.0140j],
         [ 0.0134+0.0010j],
         [-0.0215+0.0075j]],

        [[-0.0093+0.0256j],
         [-0.0065+0.0033j],
         [ 0.0248-0.0008j],
         [ 0.0078-0.0172j]],

        [[-0.0183-0.0102j],
         [-0.0184+0.0176j],
         [-0.0271+0.0134j],
         [ 0.0310-0.0094j]],

        [[-0.0643-0.0160j],
         [ 0.0266+0.0497j],
         [-0.0164-0.0185j],
         [-0.0257+0.0250j]],

        [[-0.0042-0.0058j],
         [-0.0164+0.0046j],
         [ 0.0034-0.0009j],
         [ 0.0059-0.0047j]],

        [[-0.0317-0.0073j],
         [ 0.0122-0.0220j],
         [-0.0644-0.0129j],
         [ 0.0129-0.0127j]]], requires_grad=True)
model.A[1]=Parameter containing:
tensor([[[ 1.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j,
           0.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j],
         [ 0.0000e+00+0.0000e+00j,  1.0000e+00+0.0000e+00j,
           0.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j],
         [ 0.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j,
           1.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j],
         [ 0.0000e+00+0.0000e+00j,  0.0000e+00+0.0000e+00j,
           0.0000e+00+0.0000e+00j,  1.0000e+00+0.0000e+00j]],

        [[-6.9171e-03-7.1732e-04j, -1.7274e-03-1.0874e-02j,
          -8.3198e-03-1.0945e-02j,  1.7975e-02-6.8287e-03j],
         [-1.0659e-02+1.4300e-02j, -1.5762e-02+4.9395e-03j,
          -6.5144e-03-1.6123e-02j, -7.7059e-04-3.5662e-03j],
         [-3.3527e-03+9.6975e-03j, -2.3078e-02+3.2349e-02j,
          -2.3709e-03+1.1124e-03j, -4.0613e-03+8.2611e-03j],
         [-3.4178e-03-9.5897e-03j, -1.1052e-02-2.0399e-03j,
           2.2146e-02-2.6187e-03j,  3.3472e-04-3.0316e-03j]],

        [[ 1.0263e-02+8.3255e-03j,  2.8505e-03+2.1744e-03j,
          -7.6300e-03+9.4689e-04j, -2.1400e-02-8.7484e-03j],
         [-2.8971e-03+2.1459e-03j, -2.7207e-03-3.1921e-03j,
          -4.6967e-03+1.2110e-03j,  5.1615e-04+3.0735e-03j],
         [ 2.3181e-02+1.0096e-03j, -5.0978e-03-2.1092e-03j,
           7.8256e-04-6.2067e-03j, -3.6946e-03-8.5231e-03j],
         [-7.2738e-03+9.4141e-03j, -2.1535e-02-8.4201e-04j,
           9.4962e-03+1.4321e-02j,  1.3336e-02-4.7964e-03j]],

        [[ 3.8623e-03+1.0188e-03j, -3.0513e-03-9.5333e-03j,
          -3.2301e-04-1.1038e-03j, -4.5440e-03+5.7638e-03j],
         [-7.9587e-04-9.2807e-03j,  1.4157e-02-1.3751e-02j,
           7.6451e-03+6.9191e-03j, -8.4987e-03-8.8684e-03j],
         [ 1.1537e-02+5.9793e-03j, -1.1127e-02-3.9855e-03j,
          -2.1556e-02-5.6082e-03j, -1.2150e-02-6.3976e-03j],
         [-1.4775e-02-1.4799e-02j, -9.3408e-03-7.8459e-03j,
           1.1235e-02-1.5518e-03j,  1.5033e-02+5.1866e-03j]],

        [[ 8.8523e-03+6.4374e-04j,  1.0901e-02+3.4195e-03j,
          -1.6920e-02-3.0354e-05j, -1.9878e-02-9.2765e-03j],
         [-2.4088e-03+1.2684e-02j, -1.8141e-02+1.1350e-02j,
          -5.2678e-03-5.0647e-03j, -1.9426e-04-8.5412e-03j],
         [-2.0992e-03-9.5961e-04j,  2.8851e-03+8.5175e-03j,
           3.0833e-04+9.4807e-03j, -3.7298e-04+1.1456e-03j],
         [-1.0446e-02-8.9207e-05j, -1.3453e-02+2.8660e-03j,
           2.1872e-02-2.8545e-03j,  7.6257e-03+9.5212e-03j]],

        [[ 1.3558e-02-4.3925e-03j,  8.6221e-03+8.9609e-03j,
           1.0205e-02+4.2352e-03j,  1.9139e-03+1.7722e-03j],
         [ 8.2847e-03-2.1896e-02j,  1.0950e-02+5.2911e-03j,
          -3.1118e-03+1.0639e-02j,  1.8011e-02+3.8317e-03j],
         [-2.6935e-02-9.0242e-03j, -5.9375e-03+1.7512e-02j,
           1.9096e-02-1.6767e-02j,  8.2719e-03-7.9622e-03j],
         [ 6.6897e-03+4.2753e-03j, -1.5379e-02-2.0450e-03j,
           7.9086e-03-1.4592e-02j,  7.9287e-03-3.7462e-03j]],

        [[-1.0010e-02-1.7333e-03j,  3.9771e-04-5.1298e-03j,
          -7.2023e-03-1.3922e-02j, -1.0909e-02-9.1501e-03j],
         [ 5.9597e-03+3.8783e-03j, -2.9292e-04-1.3476e-02j,
           2.7781e-04-1.5233e-02j, -9.1725e-03+1.4599e-02j],
         [-1.1139e-02+2.2522e-03j,  5.7539e-03-1.1065e-02j,
          -1.3220e-02-1.8145e-03j, -4.9993e-04+9.8062e-03j],
         [-2.4608e-02+4.3920e-03j, -2.1162e-02-6.0244e-03j,
           1.3092e-02-1.0064e-02j, -3.9071e-03+1.2819e-03j]]],
       requires_grad=True)

model.Next[0]=Parameter containing:
tensor([[0.+0.j],
        [0.+0.j],
        [0.+0.j],
        [0.+0.j]], requires_grad=True)
model.Next[1]=Parameter containing:
tensor([[1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
        [0.+0.j, 1.+0.j, 0.+0.j, 0.+0.j],
        [0.+0.j, 0.+0.j, 1.+0.j, 0.+0.j],
        [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]], requires_grad=True)

model.z0=tensor([0.3536+0.3536j, 0.3536+0.3536j, 0.3536+0.3536j, 0.3536+0.3536j])
PASSED
test_project.py::test_Model_reset_null PASSED
test_project.py::test_Model_tensor_power PASSED
test_project.py::test_Model_apply_word PASSED
test_project.py::test_Model_sequence_meaning PASSED
test_project.py::test_Model_apply_next PASSED
test_project.py::test_Model_word_meaning PASSED
test_project.py::test_Model_update_povm_cache PASSED
test_project.py::test_Model_probs_3_methods PASSED
test_project.py::test_Model_forward PASSED
test_project.py::test_Model_forward_sequence PASSED
test_project.py::test_Model_povm_normalization_error
err=7.151913337111182e-07
PASSED
test_project.py::test_Model_speak
(i=0,)model.speak(i, print_text=False)=''

(i=1,)model.speak(i, print_text=False)='<Null>'

(i=2,)model.speak(i, print_text=False)='much I'

(i=3,)model.speak(i, print_text=False)='love <Null> <Null>'

(i=4,)model.speak(i, print_text=False)='<Null> <Null> <Null> <Null>'

(i=5,)model.speak(i, print_text=False)='you much you you love'

(i=6,)model.speak(i, print_text=False)='<Null> much love very very <Null>'

(i=7,)model.speak(i, print_text=False)='love very love <Null> much you thank'

(i=8,)model.speak(i, print_text=False)='<Null> <Null> <Null> <Null> thank much <Null> very'

(i=9,)model.speak(i, print_text=False)='<Null> much much <Null> love very very thank I'

(i=0,)model.speak(i, return_list=True, print_text=False)=[]

(i=1,)model.speak(i, return_list=True, print_text=False)=['<Null>']

(i=2,)model.speak(i, return_list=True, print_text=False)=['<Null>', '<Null>']

(i=3,)model.speak(i, return_list=True, print_text=False)=['<Null>', '<Null>', '<Null>']

(i=4,)model.speak(i, return_list=True, print_text=False)=['<Null>', 'love', '<Null>', '<Null>']

(i=5,)model.speak(i, return_list=True, print_text=False)=['much', 'you', 'I', 'much', 'much']

(i=6,)model.speak(i, return_list=True, print_text=False)=['<Null>', 'love', '<Null>', 'very', 'very', 'very']

(i=7,)model.speak(i, return_list=True, print_text=False)=['<Null>', 'much', 'you', 'very', 'very', 'you', 'you']

(i=8,)model.speak(i, return_list=True, print_text=False)=['I', 'thank', 'very', 'much', 'love', 'much', 'much', 'much']

(i=9,)model.speak(i, return_list=True, print_text=False)=['<Null>', 'thank', '<Null>', 'very', 'love', 'love', 'love', 'very', 'very']
PASSED
test_project.py::test_Model_operator_2_methods PASSED

================================================= 15 passed in 2.09s ==================================================

(cd)>

(結果を表示ここまで)

全体のステータスが (テスト数) passed in (経過時間)s

となっており、x failed がないので、テストはすべて成功である。

改めて、現在のステータスを表示しておく。

ディレクトリ構成とステータス
(カレントディレクトリ)
├ main.py                               # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py        # ✅モデルクラス
├ ComplexTaylorWordFunctionLearner.py   # 😴モデルを学習するためのクラス
├ constants.py                          # ✅複数ファイルで共有する定数など (コーパス以外)
├ corpus.py                             # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # 😴学習結果や経過を評価するためのクラス
└ test_project.py                       # ✅テスト用コード

ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり

5. 次回について

次回は、4-5節以降を書いていく。
モデルの学習をするComplexTaylorWordFunctionLearnerクラスを書いて、実験の準備を進める。

↓次回の記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?