↓1回目の記事:
↓前回(2回目)の記事:
↓次回(4回目)の記事:
↓5回目の記事:
↓6回目の記事:
0. 前回までのあらすじ
$$\huge{「意味」とは「状態」だ。}$$
$$\huge{「単語」とは状態を変換する「非線形作用素」だ。}$$
こう考えると、「文」は「作用素の合成」として理解することが出来るし、
「意味を理解すること」とは「状態の観測」として説明することが出来る。
―こう考えて、量子力学の真似事をしながら、新しい自然言語処理モデル「複素テイラー単語関数言語モデル」の理論を組み立てたのが前回までだ。
前回までで、第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 は 長さ k の torch.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}$のデータ属性を初期化しよう。
次のようにすればよい。
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_init が False のときは、
self.A[n] が 形状 $(語彙数)\times d \times d^n$ の$3$階テンソルであるのに対して
self.Next[n] が 形状 $d \times d^n$ の $2$階テンソルである点を除いて、まったく同じだ。
但し next_identity_init が True の場合は、
self.Next はランダムではなくてほぼすべて $0$ で初期化している、という点も異なる。
但し、self.Next[1] のみ、 $0$でなく、単位行列で初期化する。
このようにすると、 next_identity_init が True のとき、初期の$\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キャッシュ」のデータ属性を初期化しよう。
次のようにすればよい。
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_povm と self.G_inv_sqrt_povm が同じテンソルになってしまい、
値まで自動的に同じになってしまう恐れがある。
またここで eye を作るときに dtype=torch.cfloat としているのは、複素テンソルとして定義するためである。
コンストラクタ全体のコードは次のとおり。
コンストラクタ全体のコードを表示
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}$ を恒等関数に戻す役割を果たす。
コードは次のとおり。
@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})$ をバッチにまとめたものとしよう。
次のようなコードにすればよい。
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 では:
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 されて、 y は z.テンソル積(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 に対応する単語)
次のようにすればよい。
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_n と A_batch_n に対して:
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{文}$ をバッチにまとめたものを返す。
コードは次のとおり。
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)$$
をバッチにまとめたものを返す。
次のようにすればよい。
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_n と Next_n に対して:
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 を受け取り、
各単語に対応する意味ベクトルをバッチにまとめたものを返す。
コードは次のとおり。
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キャッシュを更新する。
コードは次のとおり。
@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}$$
になる
コードは次のとおり。
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}$$
になる
コードは次のとおり。
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による次単語予測確率の対数を求めたリストをバッチにまとめたものを返す。
コードは次のとおり。
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)$$
のようになる。
コードは次のとおり。
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)$$
のようになる。
コードは次のとおり。
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$ に十分近ければ正常。
コードは次のとおり。
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 を受け取って、
確率的に生成された文を文字列として返す。
次単語予測確率に応じてランダムに文を作るわけだ。
学習に関係ないメソッドなので、詳しくは解説しない。
コードを表示する
@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_n が True の場合:
$${\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}}}$$
の連結を返す。
(このようにすると、各次数の係数の「平均的な大きさ」を比較できるようになる。)
normalize が True の場合、連結したベクトルを正規化してから返す。
コードは次のとおり。
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 に対応する単語。
コードは次のとおり。
def operator_norm(self, word_id):
v = self.flat_operator(word_id, normalize=False)
return torch.linalg.norm(v).item()
4-3. モデルクラス全体のコード
長いので、折りたたむ。
このコードを 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 としてカレントディレクトリに保存する。
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 を表示
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 を実行した結果、次のようになった。
結果を表示
(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クラスを書いて、実験の準備を進める。
↓次回の記事

