↓1回目の記事:
↓2回目の記事:
↓3回目の記事:
↓前回(4回目)の記事:
↓次回(6回目)の記事:
0. 前回までのあらすじ
$$\huge{「意味」とは「状態」だ。}$$
$$\huge{「単語」とは状態を変換する「非線形作用素」だ。}$$
こう考えると、「文」は「作用素の合成」として理解することが出来るし、
「意味を理解すること」とは「状態の観測」として説明することが出来る。
―こう考えて、量子力学の真似事をしながら、新しい自然言語処理モデル「複素テイラー単語関数言語モデル」の理論を組み立てたのが前々々回(2回目)までだ。
そして、モデルクラス ComplexTaylorWordFunctionLM.py を書いたのが前々回(3回目)。
学習器クラス ComplexTaylorWordFunctionLearner.py を書いたのが前回(4回目)だ。
前回までで、4-8節まで書いた。
今回の記事は、4-9節から開始する。
4. コーディング
複素テイラー単語関数言語モデルを学習させて実験してみるためのコードを、PyTorch で作ってみよう。
(カレントディレクトリ)
├ main.py # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py # ✅モデルクラス
├ ComplexTaylorWordFunctionLearner.py # ✅モデルを学習するためのクラス
├ constants.py # ✅複数ファイルで共有する定数など (コーパス以外)
├ corpus.py # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # 🛠️学習結果や経過を評価するためのクラス
└ test_project.py # 📌テスト用コード
ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり
今回は、4-9節~4-11節で、評価器クラス ComplexTaylorWordFunctionEvaluator.py をコーディングする。
次に、4-12節で評価器クラスをテストする。
その後、次回の記事でmain.py と corpus.py をコーディングする。
4-9. 評価器クラスの設計
前回までで、モデルクラス ComplexTaylorWordFunctionLM と、学習器クラス ComplexTaylorWordFunctionLearner を作った。
これで、モデルを作り、コーパスさえ用意すればそこから訓練例を作り、次単語予測で学習するところまでは出来るようになった。
しかし、学習したモデルを眺めるためには、まだ足りないものがある。
例えば:
- 次単語予測の精度はどれくらいか?
- 単語に関連するベクトルの間で
「$正恩 - ジャイアン + ジャイ子 \simeq 与正$」
のようなアナロジーは成立するか?- word2vec では単語ベクトル間でこのような演算が成立する
- 単語を「作用素」と見なしたとき、逆作用素っぽいことは出来るか?
- $ジャイ子\big(ジャイアン^{-1}(\ket{正恩})\big) \simeq \ket{与正}$ だと嬉しい
- 「正恩」はどれくらい「正義」で、どれくらい「悪」なのか?
のようなことを調べたい。
そこで今回は、学習済みモデルを分析するための評価器クラス:
ComplexTaylorWordFunctionEvaluator
を作る。
モデル本体は ComplexTaylorWordFunctionLM。
学習まわりは ComplexTaylorWordFunctionLearner。
そして、分析まわりは ComplexTaylorWordFunctionEvaluator に分ける。
うん、責務を分けるとかなり見通しが良くなる。
まずは:
- 属性(メソッド, データ属性) 概要
- 次単語予測の評価
- アナロジー
- 逆作用素
- 意味の量子的分解
を決めよう。
4-9-1. メソッド一覧
ComplexTaylorWordFunctionEvaluator クラスには、次のメソッドを持たせる。
(quantum_decompose だけクラスメソッド、それ以外はインスタンスメソッドである)
| メソッド | 役割 |
|---|---|
__init__(コンストラクタ) |
学習器 learner を受け取り、評価用キャッシュを初期化する |
update_cache |
評価用キャッシュを再計算する。 何も受け取らず、何も返さない。 |
eval_accuracy |
何も受け取らず、学習器が持っている訓練データに対する次単語予測精度を計算して返す |
predict_next |
文を受け取って、次に来そうな単語を上位 topk 個を返す |
analogy_nearest |
アナロジー計算されたベクトルと、(単語からベクトルへの辞書)を受け取って、アナロジー計算の結果に近い順に単語を並び替えたものを返す |
apply_word_operator |
単語と意味ベクトルを受け取って、単語作用素を意味ベクトルに作用させたものを返す |
invert_word_operator_numerically |
単語と意味ベクトルを受け取って、単語作用素の逆作用を数値的に求めたものを返す |
quantum_decompose(クラスメソッド) |
複素ベクトルと非直交基底を受け取り、分解結果を返す |
quantum_prob |
分解対象単語と基底となる単語たちを受け取り、分解対象単語を基底で分解し、確率・干渉項・位相を返す |
4-9-2. データ属性一覧と評価用キャッシュ
ComplexTaylorWordFunctionEvaluator クラスは、次のデータ属性を持つ。
(すべてインスタンスデータ属性である)
| データ属性 | 内容 |
|---|---|
learner |
評価対象の学習器 |
auto_update_learner_cache |
update_cacheメソッド実行時にlearnerのキャッシュを更新するか否か |
vec |
評価用キャッシュ。単語から意味ベクトルへの辞書 |
D |
評価用キャッシュ。単語から次単語観測分布への辞書 |
A |
評価用キャッシュ。単語から作用素係数ベクトルへの辞書 |
A_adjusted |
評価用キャッシュ。単語から次数調整済み作用素係数ベクトルへの辞書 |
評価用キャッシュは、vec, D, A, A_adjusted が呼ばれるたびに毎回再計算していては効率が悪いので、 update_cache() メソッドが呼ばれた時のみに更新する。
ここで、vec は単語の意味ベクトルである。
$$
w \mapsto \ket{w}
$$
である。
D は、その単語の意味から次単語を観測したときの確率分布である。
$$
w \mapsto \mathbb{P}_{\rm next}[\cdot|\ket{w}]
$$
である。
A は、単語作用素そのものを平坦化したベクトルである。
$$
w \mapsto {\rm vec}(A_{w,0},A_{w,1},\cdots,A_{w,k})
$$
である。
A_adjusted は、A とほぼ同じだが、テイラー次数ごとのパラメータ数の違いを補正したものである。
たとえば、$n$ 次の項のパラメータ数は、
$$
d\cdot d^n
$$
である。
そのため、高次の項ほど単純にパラメータ数が多くなる。
すると、何も補正しない場合、高次の項が「重要だから大きい」のか、「単にパラメータ数が多いから大きい」のか分かりにくい。
そこで、A_adjusted では、各次数ブロックをそのブロックサイズの平方根で割る。
$$
(A_{w,n})_{\rm adjusted}
=
\frac{A_{w,n}}{\sqrt{d\cdot d^n}}
$$
これにより、作用素係数を比較しやすくする。
4-9-3. 次単語予測の評価
次単語予測の評価としては、eval_accuracy メソッド と predict_next メソッドを用意する。
eval_accuracy メソッド は、学習器が持つ各訓練データム:
(問題, 正解)
=
(context_words, target_word)
に対して、モデルが問題 context_words から 正解 target_word を当てられるかを調べる。
predict_next メソッドは、例えば:
["ジャイアン", "男", "妹"]
を入力すると、
("ジャイ子", 0.5123)
("与正", 0.2628)
("女", 0.1155)
...
のように、次に来そうな単語を確率付きで返す。
つまり、この評価器クラスは単なる accuracy 計算だけではなく、
モデル内部の意味構造を眺めるための道具であるといえるだろう。
4-9-4. アナロジー
次に、アナロジーを考える。
word2vec では、よく:
$$
{\rm king}-{\rm man}+{\rm woman}\approx{\rm queen}
$$
のような例が出てくる。
今回のトイコーパスでは、例えば次のようなアナロジーを見たい。
$$
{\rm 正恩}-{\rm ジャイアン}+{\rm ジャイ子}\approx{\rm 与正}
$$
「ジャイアン」と「正恩」の関係を、「ジャイ子」に適用したら「与正」になるか、ということである。
ただし、今回のモデルでは、単語の表現が1種類ではない。
少なくとも、次の4種類の空間でアナロジーを試すことが出来る。
| 表現 | 意味 |
|---|---|
vec |
意味ベクトル空間 |
D |
次単語観測分布空間 |
A |
作用素係数空間 |
A_adjusted |
次数調整済み作用素係数空間 |
意味ベクトル空間 vec であれば:
$$
\ket{q}=\ket{正恩}-\ket{ジャイアン}+\ket{ジャイ子}
$$
を作り、この $\ket{q}$ に近い単語を探す。
観測分布空間 Dであれば:
$$
\mathbb{P}_{\rm next}[\cdot|\ket{q}]
=
\mathbb{P}_{\rm next}[\cdot|\ket{正恩}]
-
\mathbb{P}_{\rm next}[\cdot|\ket{ジャイアン}]
+
\mathbb{P}_{\rm next}[\cdot|\ket{ジャイ子}]
$$
を作り、この$\mathbb{P}_{\rm next}[\cdot|\ket{q}]$に近い単語を探す。
作用素空間A, A_adjusted であれば、
$$
A(q)
=
A(正恩)
-
A(ジャイアン)
+
A(ジャイ子)
$$
を作り、この$A(q)$に近い単語を探す。
ここが、通常の word2vec との違いである。
普通の word2vec なら、単語はベクトルなので、アナロジーは基本的にベクトル空間上で行う。
しかし、このモデルでは、単語は本来「意味状態を変換する作用素」である。
したがって、
- 意味ベクトルとして似ているのか?
- 観測された次単語分布として似ているのか?
- 作用素として似ているのか?
を分けて調べることが出来る。
これはかなり重要である。
例えば、「ジャイアン」と「正恩」は、意味ベクトルとしては違うかもしれない。
しかし、「乱暴な男性キャラクター」から「独裁者」へ写すような関係が、作用素空間では似た方向として表れているかもしれない。
逆に、意味ベクトル空間ではうまくいくが、作用素空間ではうまくいかない、ということもあり得る。
その違いを見るために、analogy_nearest メソッドを作る。
イメージとしては、次のように使う。
evaler = ComplexTaylorWordFunctionEvaluator(...)
normalized = lambda v: v / (torch.linalg.norm(v) + 1e-12)
z = {w: normalized(v) for (w,v) in evaler.vec.items()}
query = z["正恩"] - z["ジャイアン"] + z["ジャイ子"]
evaler.analogy_nearest(query, z, mode="abs")
また、複素ベクトルの内積は一般に複素数になる。
そのため、類似度(cos類似度)の取り方も1種類ではない。
| mode | スコア |
|---|---|
"abs" |
$\left|\braket{\rm query|vector\_of\_word}\right|$ |
"re" |
${\rm Re}\braket{\rm query|vector\_of\_word}$ |
"abs with sign" |
${\rm sign}({\rm Re}\braket{\rm query|vector\_of\_word})\left|\braket{\rm query|vector\_of\_word}\right|$ |
複素ベクトルでは、位相をどう扱うかが重要になる。
単純に実部だけを見ると、複素位相の情報を捨ててしまう。
しかし、絶対値だけを見ると、向きの符号が分からなくなる。
このあたりも、実験しながら見ていく。
4-9-5. 逆作用素
次に、逆作用素を考える。
今回のモデルでは、単語はベクトルではなく作用素である。
つまり、単語 $w$ は、意味状態 $z$ に作用して、別の意味状態を作る。
$$
z' = w(z)
$$
である。
例えば:
$$
\ket{ジャイアン}
=
{\rm ジャイアン}(\ket{0})
$$
であり:
$$
\ket{正恩}
=
{\rm 正恩}(\ket{0})
$$
である。
ここで、次のようなことを考えたい。
$$
{\rm ジャイアン}(x)\approx\ket{正恩}
$$
を満たす $x$ を探す。
これは、形式的には:
$$
x\approx {\rm ジャイアン}^{-1}(\ket{正恩})
$$
である。
そして、その $x$ に「ジャイ子」作用素を作用させる。
$$
{\rm ジャイ子}(x)
\approx
{\rm ジャイ子}
\left(
{\rm ジャイアン}^{-1}(\ket{正恩})
\right)
$$
これが「与正」に近ければ、
$$
{\rm ジャイ子}
\circ
{\rm ジャイアン}^{-1}
\circ
{\rm 正恩}
\approx
{\rm 与正}
$$
のような、作用素的なアナロジーが出来たことになる。
もちろん、今回の単語作用素は非線形である。
$$
w(z)=
\sum_{n=0}^{k}
\frac{1}{n!}
A_{w,n}{\rm vec}(z^{\otimes n})
$$
のような形をしている。
したがって、一般には解析的な逆関数を簡単には書けない。
そこで、invert_word_operator_numerically メソッドでは、入力ベクトル $x$ を直接最適化する。
目的関数は、ざっくり言えば、
$$
\left|
w(x)-\rm target
\right|^2
$$
である。
つまり、
$$
w(x)
$$
が target に近くなるように、$x$ を Adam で更新する。
注意すべきなのは、このとき更新するのはモデルのパラメータではない、ということである。
更新するのは、一時的に作った入力ベクトル $x$ だけである。
モデル本体の
$$
A_{w,n}
$$
や
$$
N_n
$$
は固定する。
これは、評価のための逆問題であって、学習ではない。
ここが少しややこしいが、単語を「ベクトル」ではなく「作用素」として扱うなら、
このような逆作用素的な分析はかなり自然に出てくる。
4-9-6. 量子的な分解
最後に、この評価器クラスで一番やりたい分析を考える。
今回のモデルでは、意味は複素ヒルベルト空間上の状態ベクトルである。
つまり、単語「正恩」の意味は、
$$
\ket{正恩}
$$
のような状態ベクトルとして表される。
ここで、「正義」や「悪」も同じ意味空間上の状態ベクトルである。
$$
\ket{正義},\quad \ket{悪}
$$
である。
すると、次のような分解を考えることが出来る。
$$
\ket{正恩}
=
\alpha\ket{正義}
+
\beta\ket{悪}
+
\gamma\ket{other}
$$
この式の意味は、かなり直感的である。
「正恩」という意味を、「正義」方向と「悪」方向と、それ以外の成分に分解している。
つまり、
- $\alpha$ が大きければ、「正義」成分が強い
- $\beta$ が大きければ、「悪」成分が強い
- $\gamma$ が大きければ、「正義」「悪」だけでは説明しきれない成分が大きい
と見なすことが出来る。
もちろん、厳密には注意が必要である。
なぜなら、
$$
\ket{正義}
$$
と
$$
\ket{悪}
$$
は、一般には直交していないからである。
もし完全に直交していれば、単純に
$$
|\alpha|^2,\quad |\beta|^2
$$
をそのまま確率のように見てもよい。
しかし、非直交の場合、基底同士に重なりがある。
そのため、
$$
|\alpha|^2
$$
と
$$
|\beta|^2
$$
だけを独立した古典確率として解釈するのは危険である。
そこで、quantum_prob では、係数だけでなく、次の量も一緒に返す。
| 値 | 意味 |
|---|---|
alpha |
各基底方向の係数 |
beta |
residual 成分の係数 |
P |
各基底との重なり確率 |
Pother |
residual 成分との重なり確率 |
interference |
基底同士の干渉項 |
phase |
基底間の相対位相 |
other |
残差方向のベクトル |
特に、重なり確率は次のように定義する。
$$
\mathbb{P}[正義|正恩]
=
|\braket{正義|正恩}|^2
$$
$$
\mathbb{P}[悪|正恩]
=
|\braket{悪|正恩}|^2
$$
これにより、「正恩」がどれくらい「正義」に近いか、
どれくらい「悪」に近いかを数値化できる。
また、非直交基底で分解すると、干渉項も出てくる。
例えば、
$$
2{\rm Re}
\left(
\overline{\alpha}\beta
\braket{正義|悪}
\right)
$$
のような項である。
これは、「正義」成分と「悪」成分が、どのように強め合ったり打ち消し合ったりしているかを見るための量である。
さらに、相対位相も見る。
$$
\angle
\left(
\frac{\beta}{\alpha+\varepsilon}
\braket{正義|悪}
\right)
$$
位相が $0^\circ$ に近ければ、干渉は強め合う方向に強い。
位相が $\pm 180^\circ$ に近ければ、打ち消し合う方向に強い。
位相が $\pm 90^\circ$ に近ければ、干渉は弱い。
ここで重要なのは、意味を普通の実ベクトルではなく、複素ヒルベルト空間上の状態ベクトルとして扱っていることである。
そのおかげで、
$$
\ket{正恩}
=
\alpha\ket{正義}
+
\beta\ket{悪}
+
\gamma\ket{\rm (other)}
$$
のように、意味を「重ね合わせ」として分析できる。
これは、単語を単なるラベルや点として見るのではなく、
意味を方向・重なり・位相・干渉を持つ状態として見る、ということである。
今回のモデルの一番面白いところは、たぶんここである。
単語は作用素。
意味は状態ベクトル。
文は作用素の合成。
観測すると単語の確率分布が出る。そして、状態ベクトルとしての意味は:
$$
\ket{x}
=
\sum_i \alpha_i\ket{b_i}
+
\beta\ket{\rm (other)}
$$のように分解できる。
この構造により、
「この単語はどれくらい正義なのか?」
「どれくらい悪なのか?」
「正義と悪の中間なのか?」
「それとも、正義・悪では説明できない別方向の意味なのか?」
を、ある程度数値として眺めることが出来る。
というかぶっちゃけ、これをやるために意味を状態ベクトルとして持たせていると言ってもよい。
4-10. 評価器クラスの各メソッドのコーディング
4-10-1. コンストラクタ
コンストラクタがやるべきことは、結局は4-9-2節で列挙したデータ属性の初期化に過ぎない。
コードは次のとおり。
def __init__(
self, learner, auto_update_learner_cache=True, update_learner_cache=None
):
self.learner = learner
self.auto_update_learner_cache = bool(auto_update_learner_cache)
self.vec = {}
self.D = {}
self.A = {}
self.A_adjusted = {}
self.update_cache(update_learner_cache)
learner, auto_update_learner_cache 以外のデータ属性、つまり評価用キャッシュの初期化は update_cache メソッドに譲った。
4-10-2. update_cache メソッド
update_cache メソッドでは、評価用キャッシュを再計算する。
基本的には何も受け取らず、何も返さない。
(但し、オプション引数で、再計算の前に学習器の update_cache メソッドを呼ぶか否かを制御できる)
コードは次のとおり。
def update_cache(self, update_learner_cache=None):
update_learner_cache = \
self.auto_update_learner_cache if update_learner_cache is None \
else update_learner_cache
if update_learner_cache:
self.learner.update_cache()
model = self.learner.model
# vec
V_meanings = model.word_meanings_povm.detach().clone()
self.vec = {model.itos[i]:m for (i,m) in enumerate(V_meanings)}
# D
probss = model.born_probs_from_meaning(V_meanings)
self.D = {model.itos[i]:probs for (i,probs) in enumerate(probss)}
# A
chunks_False = []
chunks_True = []
for n in range(model.k + 1):
A = model.A[n] # shape (V, d, d**n)
chunk_False = A.reshape(A.shape[0], -1)
chunk_True = chunk_False / math.sqrt(chunk_False.shape[1])
chunks_False.append(chunk_False)
chunks_True .append(chunk_True)
flats_False = torch.cat(chunks_False, dim=1)
flats_True = torch.cat(chunks_True, dim=1)
self.A = {model.itos[i]:vec for (i,vec) in enumerate(flats_False)}
self.A_adjusted = {
model.itos[i]:vec for (i,vec) in enumerate(flats_True)
}
意味ベクトル self.vec は、 self.learner が持つ model の word_meanings_povm 属性から求められている。
例えば evaler.vec["ジャイアン"] のようにすれば、 $\ket{ジャイアン}$ を表す意味ベクトルが得られるようになる。
次単語確率分布 self.D は、 model の born_probs_from_meaning メソッドから求められている。
作用素係数ベクトル self.A, self.A_adjusted は、 model のA 属性、つまりパラメータから求められている。
4-10-3. eval_accuracy メソッド
eval_accuracy メソッド では、何も受け取らず、学習器が持っている訓練データに対する次単語予測精度を計算して返す。
(但し、オプション引数で、一度に処理するバッチサイズを指定できる)
やることは単純である。
訓練データ self.learner.examples の各データム:
(context_words, target_word)
について、context_words から次単語を予測し、
一番確率が高い単語が target_word と一致するかを調べる。
コードは次のとおり。
def eval_accuracy(self, batch_size=1024):
model = self.learner.model
correct = 0
total = 0
with torch.no_grad():
examples = self.learner.examples
for i in range(0, len(examples), batch_size):
batch = examples[i:i + batch_size]
(x, targets) = self.learner.make_batch_tensor(
batch, strange="error"
)
log_probs = model.forward_sequence(x)
preds = log_probs.argmax(dim=1)
correct += (preds == targets).sum().item()
total += targets.numel()
return correct / total
ここでは torch.no_grad() を使っている。
評価では勾配を計算する必要がないからである。
4-10-4. predict_next メソッド
predict_next メソッド では、文 list[str] と、候補数 topk を受け取って、次に来そうな単語上位 topk 個についてのlist[tuple[str, float]]:
[
(候補{0}, 確率),
(候補{1}, 確率),
...,
(候補{topk-1}, 確率)
]
を返す。
但し、 文はまとめて list[list[str]] で渡すこともでき、その場合はlist[list[tuple[str, float]]]:
[
[
(候補{0}, 確率),
(候補{1}, 確率),
...,
(候補{topk-1}, 確率)
],
...
]
を返す。
例えば:
evaler = ComplexTaylorWordFunctionEvaluator(...)
assert evaler.predict_next(["ジャイアン", "妹"]) == [
("ジャイ子", 0.521),
("与正", 0.212),
("女", 0.103),
...
]
assert evaler.predict_next([["ジャイアン", "妹"]]) == [[
("ジャイ子", 0.521),
("与正", 0.212),
("女", 0.103),
...
]]
みたいなイメージだ。
コードは次のとおり。
def predict_next(self, test_contexts, topk=5):
if test_contexts and isinstance(test_contexts[0], str):
return self.predict_next([test_contexts], topk)[0]
topk = min(topk or torch.inf, self.learner.model.V)
with torch.no_grad():
batch = [(c, NULL_TOKEN) for c in test_contexts]
(x, _) = self.learner.make_batch_tensor(batch, strange="error")
tops = [
zip(*(
[
self.learner.model.itos[i.item()]
for i in torch.topk(probs, k=topk).indices
],
[
p.item()
for p in torch.topk(probs, k=topk).values
]
))
for probs in torch.exp(self.learner.model.forward_sequence(x))
]
return tops
まず、入出力を
list[str] -> list[tuple[str, float]] なのか、それとも
list[list[str]] -> list[list[tuple[str, float]]] なのかはっきりさせたい。
そこで:
if test_contexts and isinstance(test_contexts[0], str):
return self.predict_next([test_contexts], topk)[0]
にて、前者の場合に引数をリストに入れなおして再帰呼び出しを行い、後者に統一して扱う工夫をしている。
よって、以後は 入出力は list[list[str]] -> list[list[tuple[str, float]]] である前提でコードを書ける。
self.learner.model の forward_sequence メソッドして対数確率を得て、
それを torch.exp に入れることで次単語確率を得ている。
そしてこれを torch.topk を利用してランク付けしている。
4-10-5. analogy_nearest メソッド
analogy_nearest メソッドでは、アナロジー計算されたベクトル query と、単語からベクトルへの辞書word_to_vec を受け取って、query に近い順に単語を並び替えたもの:
[
(1番 近い単語, cos類似度(query, word_to_vec[1番 近い単語])),
(2番目に近い単語, cos類似度(query, word_to_vec[2番目に近い単語])),
...
]
を返す。
また、ベクトルは複素ベクトルであるため、 cos類似度の計算方法は一意に決まらない。
そこで、オプション引数 mode で計算方法を選べる。
mode は次の3通りから選べる。
| mode | スコア |
|---|---|
"abs"(デフォルト) |
$\left|\braket{\rm query|vector\_of\_word}\right|$ |
"re" |
${\rm Re}\braket{\rm query|vector\_of\_word}$ |
"abs with sign" |
${\rm sign}({\rm Re}\braket{\rm query|vector\_of\_word})\left|\braket{\rm query|vector\_of\_word}\right|$ |
実ベクトル同士で analogy_nearest メソッドを使う場合は、 mode="re" とするのがよい。
例えば、意味ベクトル空間でアナロジーを試すなら:
evaler = ComplexTaylorWordFunctionEvaluator(...)
vec = evaler.vec
assert evaler.analogy_nearest(
query = vec["正恩"] - vec["ジャイアン"] + vec["ジャイ子"],
word_to_vec = vec
) == [
("与正", 0.521),
("ジャイ子", 0.212),
("女", 0.103),
...
]
みたいなイメージだ。
コードは次のとおり。
def analogy_nearest(self, query, word_to_vec, mode="abs", eps=1e-12):
first_vec = list(word_to_vec.values())[0]
query = (query / (torch.linalg.norm(query) + eps)).to(first_vec.device)
word_to_vec = {
word: vec / (torch.linalg.norm(vec) + eps)
for (word, vec) in word_to_vec.items()
}
mat = torch.empty(
(0, first_vec.shape[0]),
dtype=first_vec.dtype,
device=first_vec.device
)
model = self.learner.model
itos = model.itos
for i in range(model.V):
word = itos[i]
vec = word_to_vec[word]
mat = torch.cat([mat, vec.unsqueeze(0)], dim=0)
raw = (query.conj().unsqueeze(0) @ mat.T)[0]
if "abs" in mode.lower() and "sign" in mode.lower():
sign = torch.where(raw.real >= 0, 1, -1)
scores = sign * torch.abs(raw)
elif "abs" in mode.lower():
scores = torch.abs(raw)
elif mode.lower() == "re":
scores = raw.real
else:
raise ValueError("mode")
word_to_score = {
itos[i]:score.item() for (i,score) in enumerate(scores)
}
sorted_words = sorted(
word_to_vec.keys(), key=lambda w: -word_to_score[w]
)
word_and_scores = [
(word, word_to_score[word])
for word in sorted_words
]
return word_and_scores
まず:
first_vec = list(word_to_vec.values())[0]
query = (query / (torch.linalg.norm(query) + eps)).to(first_vec.device)
word_to_vec = {
word: vec / (torch.linalg.norm(vec) + eps)
for (word, vec) in word_to_vec.items()
}
にて、 query と word_to_vec は正規化される。
また query については、デバイスも合わせている。
次に mat 変数に、 word_to_vec内のベクトルを行ベクトルとして見て列方向に並べた行列を入れる。
そして:
raw = (query.conj().unsqueeze(0) @ mat.T)[0]
つまり:
$${\rm raw} = \bra{\rm query}\cdot({\rm mat}^\top) = \bra{\rm query}\cdot\left(\begin{matrix}
\ket{\rm vector\_of\_word0}\\
\ket{\rm vector\_of\_word1}\\
\vdots\\
\end{matrix}\right)$$
とすることで、 raw 変数にエルミート内積を並べたベクトルが得られる。
各ベクトルは正規化されているので、エルミート内積たちはそのまま「複素数版のcos類似度」とみなせる。
これを、 mode で指定された方法で実cos類似度に変換したものが scores である。
そして、 scores から辞書を作って返すわけだ。
4-10-6. apply_word_operator メソッド
apply_word_operator メソッドでは、単語 str と意味ベクトルtorch.Tensor(shape=(d,))を受け取って、単語作用素を意味ベクトルに作用させたtorch.Tensor(shape=(d,))を返す。
但し、 単語をまとめて list[str] で渡したり、
意味ベクトルをまとめて list[torch.Tensor(shape=(d,))] または torch.Tensor(shape=(batch_size, d)) で渡すこともでき、その場合はtorch.Tensor(shape=(batch_size, d))を返す。
コードは次のとおり。
def apply_word_operator(self, words, zs):
words_is_single = isinstance(words, str)
zs_is_single = isinstance(zs, torch.Tensor) and zs.dim() == 1
# 1. words と zs が single なら single を返す
if words_is_single and zs_is_single:
return self.apply_word_operator([words], [zs])[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if words_is_single: return self.apply_word_operator([words]*len(zs), zs)
if zs_is_single: return self.apply_word_operator(words, [zs]*len(words))
# 3. zs が listかtuple なら torch.Tensor にする
if isinstance(zs, (list, tuple)):
return self.apply_word_operator(words, torch.stack(zs, dim=0))
# 4. 単語作用素を適用する
model = self.learner.model
stoi = model.stoi
return model.apply_word([stoi[w] for w in words],zs.to(model.z0.device))
これは内部的には、self.learner.model の apply_word メソッドを呼んでいるだけである。
4-10-7. invert_word_operator_numerically メソッド
invert_word_operator_numerically メソッドでは、単語 str と意味ベクトルtorch.Tensor(shape=(d,))を受け取って、単語作用素を意味ベクトルに逆作用させたようなtorch.Tensor(shape=(d,))を返す。
但し、 単語をまとめて list[str] で渡したり、
意味ベクトルをまとめて list[torch.Tensor(shape=(d,))] または torch.Tensor(shape=(batch_size, d)) で渡すこともでき、その場合はtorch.Tensor(shape=(batch_size, d))を返す。
逆作用(のようなもの)は数値的に求められるので、そのための steps と lr をオプション引数で指定できる。
複素テイラー単語関数モデルの単語作用素は非線形関数である。
したがって、一般には逆関数を解析的に書けない。
しかし、次のような問題なら数値的に解ける。
$$
w(x)\approx \rm target
$$
を満たす $x$ を探す。
例えば:
$$
{\rm ジャイアン}(x)\approx \ket{正恩}
$$
を満たす $x$ を探す。
その後:
$$
{\rm ジャイ子}(x)
$$
を計算すれば、
$$
{\rm ジャイ子}({\rm ジャイアン}^{-1}(\ket{正恩}))\ \ \ \big(\overset{?}{\approx} \ket{与正}\big)
$$
に近い、作用素としてのアナロジーっぽいことができる。
まずは、引数を再帰的に整理しよう。
def invert_word_operator_numerically(
self, words, targets, steps=300, lr=0.05, eps=1e-12
):
words_is_single = isinstance(words, str)
targets_is_single = \
isinstance(targets, torch.Tensor) and targets.dim() == 1
# 1. words と targets が single なら single を返す
if words_is_single and targets_is_single:
return self.invert_word_operator_numerically(
[words], [targets], steps, lr, eps
)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if words_is_single:
return self.invert_word_operator_numerically(
[words]*len(targets), targets, steps, lr, eps
)
if targets_is_single:
return self.invert_word_operator_numerically(
words, [targets]*len(words), steps, lr, eps
)
# 3. targets が listかtuple なら torch.Tensor にする
if isinstance(targets, (list, tuple)):
return self.invert_word_operator_numerically(
words, torch.stack(targets, dim=0), steps, lr, eps
)
...
このコード以降、words 引数は list[str], targets 引数は torch.Tensor(shape=(batch_size, d)) に統一される。
次に、逆作用(のようなもの)を求める処理を書こう。
def invert_word_operator_numerically(
self, words, targets, steps=300, lr=0.05, eps=1e-12
):
...
# ここから追加
# 4. 逆作用のようなものを適用する
norm = torch.linalg.norm
normalize = lambda vecs: vecs / (norm(vecs, dim=-1, keepdim=True) + eps)
model = self.learner.model
requires_grads = [p for p in model.parameters() if p.requires_grad]
try:
for p in requires_grads: p.requires_grad_(False)
with torch.enable_grad():
targets = normalize(targets).detach().to(model.z0.device)
# optimize x only
x = targets.clone().detach().requires_grad_(True)
opt_inv = torch.optim.Adam([x], lr=lr)
wids = torch.tensor(
[model.stoi[w] for w in words],
dtype=torch.long, device=targets.device
)
for _ in range(steps):
opt_inv.zero_grad()
y = normalize(model.apply_word(wids, x))
loss = (y - targets).abs().pow(2).sum(dim=1).mean()
loss = loss + 0.01 * (norm(x, dim=1) - 1.0).pow(2).mean()
loss.backward()
opt_inv.step()
with torch.no_grad():
x[:] = normalize(x)
return normalize(x.detach())
finally:
for p in requires_grads: p.requires_grad_(True)
ここで最適化しているのは、モデルのパラメータではない。
最適化しているのは、一時的に作った入力ベクトル x だけである。
x = targets.clone().detach().requires_grad_(True)
そして:
opt_inv = torch.optim.Adam([x], lr=lr)
としている。
つまり、self.learner.model.parameters() は最適化対象に入っていない。
これは重要である。
逆作用素を求めたいだけなのに、モデル本体まで変わってしまったら困るからだ。
また最適化対象に入らないだけでなく、勾配再計算もしないようにしている。
そのために:
requires_grads = [p for p in model.parameters() if p.requires_grad]
try:
for p in requires_grads: p.requires_grad_(False)
# ここで x だけ最適化する
finally:
for p in requires_grads: p.requires_grad_(True)
としている。
invert_word_operator_numerically メソッド全体のコードは次のとおり。
invert_word_operator_numerically メソッド全体のコードを表示
def invert_word_operator_numerically(
self, words, targets, steps=300, lr=0.05, eps=1e-12
):
words_is_single = isinstance(words, str)
targets_is_single = \
isinstance(targets, torch.Tensor) and targets.dim() == 1
# 1. words と targets が single なら single を返す
if words_is_single and targets_is_single:
return self.invert_word_operator_numerically(
[words], [targets], steps, lr, eps
)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if words_is_single:
return self.invert_word_operator_numerically(
[words]*len(targets), targets, steps, lr, eps
)
if targets_is_single:
return self.invert_word_operator_numerically(
words, [targets]*len(words), steps, lr, eps
)
# 3. targets が listかtuple なら torch.Tensor にする
if isinstance(targets, (list, tuple)):
return self.invert_word_operator_numerically(
words, torch.stack(targets, dim=0), steps, lr, eps
)
# 4. 逆作用のようなものを適用する
norm = torch.linalg.norm
normalize = lambda vecs: vecs / (norm(vecs, dim=-1, keepdim=True) + eps)
model = self.learner.model
requires_grads = [p for p in model.parameters() if p.requires_grad]
try:
for p in requires_grads:
p.requires_grad_(False)
with torch.enable_grad():
targets = normalize(targets).detach().to(model.z0.device)
# optimize x only
x = targets.clone().detach().requires_grad_(True)
opt_inv = torch.optim.Adam([x], lr=lr)
wids = torch.tensor(
[model.stoi[w] for w in words],
dtype=torch.long, device=targets.device
)
for _ in range(steps):
opt_inv.zero_grad()
y = normalize(model.apply_word(wids, x))
loss = (y - targets).abs().pow(2).sum(dim=1).mean()
loss = loss + 0.01 * (norm(x, dim=1) - 1.0).pow(2).mean()
loss.backward()
opt_inv.step()
with torch.no_grad():
x[:] = normalize(x)
return normalize(x.detach())
finally:
for p in requires_grads:
p.requires_grad_(True)
(invert_word_operator_numerically メソッド全体のコードを表示 ここまで)
4-10-8. quantum_decompose メソッド (クラスメソッド)
quantum_decompose メソッドでは 複素ベクトル:
torch.Tensor(shape=(d,))
と非直交基底:
torch.Tensor(shape=(num_base, d))
| list[torch.Tensor(shape=(d,))]
を受け取り、分解結果 dict[str, torch.Tensor(dtype=torch.cfloat)]:
{
"alphas": shape (num_base,),
"beta": shape (),
"other": shape (meaning_vector_dim,)
}
を返す。
但し、複素ベクトルや非直交基底はリスト、または$1$階上のテンソルにまとめて渡すこともでき、その場合は:
{
"alphass": shape (batch_size, num_base),
"betas": shape (batch_size,),
"others": shape (batch_size, meaning_vector_dim)
}
を返す。
このメソッドは量子力学っぽい分析の中心的な役割を果たす。
やりたいことは、あるベクトル $\ket{x}$ を、いくつかの基底ベクトルで分解することである。
例えば:
evaler = ComplexTaylorWordFunctionEvaluator(...)
vec = evaler.vec
decomposed = ComplexTaylorWordFunctionEvaluator.quantum_decompose(
vec["正恩"], [vec["正義"], vec["悪"]]
)
assert set(decomposed.keys()) == {"alphas", "beta", "other"}
みたいにしたときに:
$$
\ket{正恩}
=
\alpha_0\ket{正義}
+
\alpha_1\ket{悪}
+
\beta\ket{\rm other}
$$
のような分解をしたい。
ただし $\ket{\rm other}$ は $
\ket{正義}
$ にも $\ket{悪}$ にも直交する複素単位ベクトル。
$\alpha_i$ と $\beta$ は複素数。
そして
decomposed["alphas"] が$\left(\begin{matrix}
\alpha_0\\\alpha_1
\end{matrix}\right)$ を表し、
decomposed["beta"] が $\beta$ を表し、
decomposed["other"] が $\ket{\rm other}$を表す。
まずは引数を再帰的に整理しよう。
@classmethod
def quantum_decompose(cls, zs, basess, eps=1e-12):
zs_is_single = isinstance(zs, torch.Tensor) and zs.dim() == 1
basess_is_single = (
isinstance(basess, torch.Tensor) and basess.dim() == 2
) or (
isinstance(basess, (list, tuple)) \
and len(basess) > 0 \
and isinstance(basess[0], torch.Tensor) and basess[0].dim() == 1
)
# 1. zs と basess が single なら single用のdictを返す
if zs_is_single and basess_is_single:
result_dict = cls.quantum_decompose([zs], [basess], eps)
return {key[:-1]: value[0] for (key, value) in result_dict.items()}
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if zs_is_single:
return cls.quantum_decompose([zs]*len(basess), basess, eps)
if basess_is_single:
return cls.quantum_decompose(zs, [basess]*len(zs), eps)
# 3. list や tuple を torch.Tensor にする
if isinstance(zs, (list, tuple)):
return cls.quantum_decompose(torch.stack(zs, dim=0), basess, eps)
if isinstance(basess, (list, tuple)):
return cls.quantum_decompose(
zs,
torch.stack([
torch.stack(list(bases), dim=0) for bases in basess
], dim=0),
eps
)
...
このコード以降、zs 引数は torch.Tensor(shape=(batch_size, d)), basess 引数は torch.Tensor(shape=(batch_size, num_base, d)) に統一される。
次に、分解処理を書こう。
@classmethod
def quantum_decompose(cls, zs, basess, eps=1e-12):
...
# ここから追加
# 4. 分解する
norm = lambda vecs: (
torch.linalg.norm(vecs, dim=-1, keepdim=True) + eps
)
normalize = lambda vecs: vecs / norm(vecs)
zs = normalize(zs.to(torch.cfloat))
basess = normalize(basess.to(torch.cfloat))
B = basess
## 4-1. Gram[b,i,j] = <bases[i]|bases[j]>
Gram = torch.einsum("bik,bjk->bij", B.conj(), B)
## 4-2. rhs[b,i] = <bases[i]|z>
rhs = torch.einsum("bik,bk->bi", B.conj(), zs)
## 4-3. (alphas = ) alphass[b] = Gram[b]^{-1} rhs[b]
alphass = torch.einsum("bij,bj->bi", torch.linalg.pinv(Gram), rhs)
## 4-4. projections[b] = Σ_i alphas[i] |bases[i]>
projections = torch.einsum("bi,bik->bk", alphass, B)
## 4-5. betas と others を求める
residuals = zs - projections
betas = norm(residuals).squeeze(-1).to(torch.cfloat)
others = normalize(residuals)
# 5. 返す
return {
"alphass": alphass,
"betas": betas,
"others": others
}
分解対象の意味ベクトルを:
$$\ket{z}$$
として、また基底ベクトルを:
$$
\ket{b_0},\ket{b_1},\cdots,\ket{b_{m-1}}
$$
として、Gram 行列:
$$
G_{i,j}=\braket{b_i|b_j}
$$
を考える(コメント4-1)。
コメント4-2 で:
$$
r_i=\braket{b_i|z}
$$
とすると、係数 $\alpha_i$ は:
$$
G\alpha=r
$$
を解くことで求められる(コメント4-3)。
そしてコメント4-4 で:
$$p = \sum_i \alpha_i \ket{b_i}$$
を作り、コメント4-5 で:
$$\begin{array}{l}
\beta = \big|\ket{z} - p\big|,\\
\displaystyle
\ket{\rm other} = \frac{\ket{z} - p}{\beta}
\end{array}$$
を求めている。
quantum_decompose メソッド全体のコードは次のとおり。
quantum_decompose メソッド全体のコードを表示
@classmethod
def quantum_decompose(cls, zs, basess, eps=1e-12):
zs_is_single = isinstance(zs, torch.Tensor) and zs.dim() == 1
basess_is_single = (
isinstance(basess, torch.Tensor) and basess.dim() == 2
) or (
isinstance(basess, (list, tuple)) \
and len(basess) > 0 \
and isinstance(basess[0], torch.Tensor) and basess[0].dim() == 1
)
# 1. zs と basess が single なら single用のdictを返す
if zs_is_single and basess_is_single:
result_dict = cls.quantum_decompose([zs], [basess], eps)
return {key[:-1]: value[0] for (key, value) in result_dict.items()}
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if zs_is_single:
return cls.quantum_decompose([zs]*len(basess), basess, eps)
if basess_is_single:
return cls.quantum_decompose(zs, [basess]*len(zs), eps)
# 3. list や tuple を torch.Tensor にする
if isinstance(zs, (list, tuple)):
return cls.quantum_decompose(torch.stack(zs, dim=0), basess, eps)
if isinstance(basess, (list, tuple)):
return cls.quantum_decompose(
zs,
torch.stack([
torch.stack(list(bases), dim=0) for bases in basess
], dim=0),
eps
)
# 4. 分解する
norm = lambda vecs: (
torch.linalg.norm(vecs, dim=-1, keepdim=True) + eps
)
normalize = lambda vecs: vecs / norm(vecs)
zs = normalize(zs.to(torch.cfloat))
basess = normalize(basess.to(torch.cfloat))
B = basess
## 4-1. Gram[b,i,j] = <bases[i]|bases[j]>
Gram = torch.einsum("bik,bjk->bij", B.conj(), B)
## 4-2. rhs[b,i] = <bases[i]|z>
rhs = torch.einsum("bik,bk->bi", B.conj(), zs)
## 4-3. (alphas = ) alphass[b] = Gram[b]^{-1} rhs[b]
alphass = torch.einsum("bij,bj->bi", torch.linalg.pinv(Gram), rhs)
## 4-4. projections[b] = Σ_i alphas[i] |bases[i]>
projections = torch.einsum("bi,bik->bk", alphass, B)
## 4-5. betas と others を求める
residuals = zs - projections
betas = norm(residuals).squeeze(-1).to(torch.cfloat)
others = normalize(residuals)
# 5. 返す
return {
"alphass": alphass,
"betas": betas,
"others": others
}
(quantum_decompose メソッド全体のコードを表示 ここまで)
4-10-9. quantum_prob メソッド
quantum_prob メソッド では 分解対象単語 str と基底となる単語たち list[str] を受け取り、
分解対象単語を基底で分解し、
分解結果 dict[str, object]:
{
"target": str,
"alpha": dict[str, 複素数],
"beta": 複素数,
"interference": dict[tuple[str, str], 実数],
"phase": dict[tuple[str, str], 実数],
"P": dict[str, 実数],
"Pother": 実数,
"other": 複素ベクトル
}
複素数: torch.Tensor(shape=(), dtype=torch.cfloat)
実数: torch.Tensor(shape=(), dtype=torch.float)
複素ベクトル: torch.Tensor(shape=(d,), dtype=torch.cfloat)
を返す。
但し分解対象単語をまとめて list[str] で渡したり、
基底単語たちをまとめて list[list[str]] で渡すこともでき、
その場合は list[dict[str, object]] が返る。
このメソッドは、 quantum_decompose メソッドと共に量子力学っぽい分析の中心的な役割を果たす。
例えば:
evaler = ComplexTaylorWordFunctionEvaluator(...)
prob_dict = evaler.quantum_prob("正恩", ["正義", "悪"])
assert set(prob_dict.keys()) == {
"target", "alpha", "beta", "interference", "phase", "P", "Pother", "other"
}
みたいにしたときに:
$$
\ket{正恩}
=
\alpha_{正義}\ket{正義}
+
\alpha_{悪}\ket{悪}
+
\beta\ket{\rm other}
$$
のような分解をしたい。
ただし $\ket{\rm other}$ は $
\ket{正義}
$ にも $\ket{悪}$ にも直交する複素単位ベクトル。
$\alpha_w$ と $\beta$ は複素数。
そして
prob_dict["target"] は "正恩" で、
prob_dict["alpha"] は:
-
prob_dict["alpha"]["正義"]が $\alpha_{正義}$を表し、 -
prob_dict["alpha"]["悪"]が $\alpha_{悪}$を表し、
prob_dict["beta"] が $\beta$ を表し、
prob_dict["interference"] は:
-
prob_dict["interference"]["正義", "悪"]が干渉項 $2 {\rm Re}(
\overline{\alpha_{正義}} \alpha_{悪} \braket{正義|悪}
)$ を表し、 -
prob_dict["interference"]["悪", "正義"]が干渉項 $2 {\rm Re}(
\overline{\alpha_{悪}} \alpha_{正義} \braket{悪|正義}
)$ を表し、
prob_dict["phase"] は:
-
prob_dict["phase"]["正義", "悪"]が相対位相 $\angle\left(
\dfrac{\alpha_{悪}}{\alpha_{正義}}
\braket{正義|悪}
\right)$ を表し、 -
prob_dict["phase"]["悪", "正義"]が相対位相 $\angle\left(
\dfrac{\alpha_{正義}}{\alpha_{悪}}
\braket{悪|正義}
\right)$ を表し、
prod_dict["P"] は:
-
prod_dict["P"]["正義"]が 正恩が正義である確率 $\big|\braket{正義|正恩}\big|^2$ を表し、 -
prod_dict["P"]["悪"]が 正恩が悪である確率 $\big|\braket{悪|正恩}\big|^2$ を表し、
prod_dict["Pother"] が 正恩が正義でも悪でも説明つかない確率 $\big|\braket{\rm other|正恩}\big|^2$ を表し、
prod_dict["other"] が $\ket{\rm other}$を表す。
まずは引数を再帰的に整理しよう。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
target_is_single = isinstance(target_words, str)
base_is_single = base_wordss and isinstance(base_wordss[0], str)
# 1. target_words と base_wordss が single なら single を返す
if target_is_single and base_is_single:
return self.quantum_prob([target_words], [base_wordss], eps)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if target_is_single:
return self.quantum_prob(
[target_words]*len(base_wordss), base_wordss, eps
)
if base_is_single:
return self.quantum_prob(
target_words, [base_wordss]*len(target_words), eps
)
...
このコード以降、分解対象単語 target_words引数 はlist[str] に、基底単語たち base_wordss 引数はlist[list[str]] に統一される。
次に、分解処理の概要を書こう。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
# ここから追加
# 3. 分解する
## 3-1. 単語をベクトル化する
zs = torch.stack([self.vec[w] for w in target_words], dim=0)
basess = torch.stack(
[
torch.stack([self.vec[w] for w in base_words], dim=0)
for base_words in base_wordss
],
dim=0
)
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
betas = decomped["betas"] # (batch_size,)
others = decomped["others"] # (batch_size, d)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
...
results.append({
"target": target_word,
"alpha": alpha_dict,
"beta": beta,
"interference": interference_dict,
"phase": phase_dict,
"P": P_dict,
"Pother": Pother,
"other": other
})
# 4. 返す
return results
コメント3-3 における for文内の処理は、コードが長くなるのでいったん省略した。
省略部分では、 self.quantum_decompose メソッドで得られた分解結果をもとに、本メソッドで返却するための辞書の部品 alpha_dict, beta, interference_dict, phase_dict, P_dict, Pother, other を作っている。
次に、コメント3-3 における for文内の処理を書こう。
まずは$\alpha$値の辞書 alpha_dict を作る。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
...
alphas = alphass[b]
...
alpha_dict = {w: alphas[i] for (i,w) in enumerate(base_words)}
...
results.append({..., "alpha": alpha_dict, ...})
# 4. 返す
return results
次に$\beta$ を表す beta を作る。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
betas = decomped["betas"] # (batch_size,)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
beta = betas[b]
...
results.append({..., "beta": beta, ...})
# 4. 返す
return results
次に干渉項$2 {\rm Re}(
\overline{\alpha_{i}} \alpha_{j} \braket{基底単語i|基底単語j}
)$の辞書 interference_dict を作る。
この辞書は:
$$(基底単語i, 基底単語j) \mapsto 2 {\rm Re}(
\overline{\alpha_{i}} \alpha_{j} \braket{基底単語i|基底単語j}
)$$
である。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
bases = basess[b]
alphas = alphass[b]
...
Gram = bases.conj() @ bases.T
...
interference_dict = {
(base_words[i], base_words[j]):
2 * torch.real(alphas[i].conj() * alphas[j] * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
...
results.append({..., "interference": interference_dict, ...})
# 4. 返す
return results
次に相対位相$\angle\left(
\dfrac{\alpha_{j}}{\alpha_{i}}
\braket{基底単語i|基底単語j}
\right)$の辞書 phase_dict を作る。
この辞書は:
$$(基底単語i, 基底単語j) \mapsto \angle\left(
\dfrac{\alpha_{j}}{\alpha_{i}}
\braket{基底単語i|基底単語j}
\right)$$
である。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
bases = basess[b]
alphas = alphass[b]
...
Gram = bases.conj() @ bases.T
...
phase_dict = {
(base_words[i], base_words[j]):
torch.angle((alphas[j] / (alphas[i] + eps)) * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
...
results.append({..., "phase": phase_dict, ...})
# 4. 返す
return results
次に確率の辞書 P_dict を作る。
この辞書は:
$$基底単語 \mapsto \left|\braket{基底単語|分解対象単語}\right|^2$$
つまり:
$$基底単語 \mapsto (対象単語が基底単語の意味を持つ確率)$$
である。
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
z = zs[b]
bases = basess[b]
...
base_overlaps = bases.conj() @ z
...
P_dict = {
w: base_overlaps[i].abs().pow(2)
for (i,w) in enumerate(base_words)
}
...
results.append({..., "P": P_dict, ...})
# 4. 返す
return results
最後に、$\ket{\rm other}$ およびその確率$\left|\braket{\rm other|分解対象単語}\right|^2$を表す other, Pother を作る
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
...
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
others = decomped["others"] # (batch_size, d)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
z = zs[b]
other = others[b]
...
other_overlap = other.conj() @ z
...
Pother = other_overlap.abs().pow(2)
...
results.append({..., "Pother": Pother, "other": other})
# 4. 返す
return results
quantum_prob メソッド全体のコードは次のとおり。
quantum_prob メソッド全体のコードを表示
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
target_is_single = isinstance(target_words, str)
base_is_single = base_wordss and isinstance(base_wordss[0], str)
# 1. target_words と base_wordss が single なら single を返す
if target_is_single and base_is_single:
return self.quantum_prob([target_words], [base_wordss], eps)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if target_is_single:
return self.quantum_prob(
[target_words]*len(base_wordss), base_wordss, eps
)
if base_is_single:
return self.quantum_prob(
target_words, [base_wordss]*len(target_words), eps
)
# 3. 分解する
## 3-1. 単語をベクトル化する
zs = torch.stack([self.vec[w] for w in target_words], dim=0)
basess = torch.stack(
[
torch.stack([self.vec[w] for w in base_words], dim=0)
for base_words in base_wordss
],
dim=0
)
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
betas = decomped["betas"] # (batch_size,)
others = decomped["others"] # (batch_size, d)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
z = zs[b]
bases = basess[b]
alphas = alphass[b]
beta = betas[b]
other = others[b]
Gram = bases.conj() @ bases.T # 行列 @ 行列 → 行列
base_overlaps = bases.conj() @ z # 行列 @ ベクトル → ベクトル
other_overlap = other.conj() @ z # ベクトル @ ベクトル → スカラ
### 3-3-1. alpha
alpha_dict = {w: alphas[i] for (i,w) in enumerate(base_words)}
### 3-3-2. (beta は既に求めてある)
### 3-3-3. interference
interference_dict = {
(base_words[i], base_words[j]):
2 * torch.real(alphas[i].conj() * alphas[j] * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
### 3-3-4. phase
phase_dict = {
(base_words[i], base_words[j]):
torch.angle((alphas[j] / (alphas[i] + eps)) * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
### 3-3-5. P, Pother
P_dict = {
w: base_overlaps[i].abs().pow(2)
for (i,w) in enumerate(base_words)
}
Pother = other_overlap.abs().pow(2)
### 3-3-6. 結果に追加する
results.append({
"target": target_word,
"alpha": alpha_dict,
"beta": beta,
"interference": interference_dict,
"phase": phase_dict,
"P": P_dict,
"Pother": Pother,
"other": other
})
# 4. 返す
return results
( quantum_prob メソッド全体のコードを表示 ここまで)
4-11. 評価器クラス全体のコード
長いので、折りたたむ。
このコードを ComplexTaylorWordFunctionEvaluator.py としてカレントディレクトリに保存する。
コードを表示
import math
import torch
from constants import NULL_TOKEN
# =========================
# Evaluation class
# =========================
class ComplexTaylorWordFunctionEvaluator:
"""Evaluation helper for ComplexTaylorWordFunctionLearner.
This class provides analysis utilities around a trained or training
ComplexTaylorWordFunctionLearner.
It keeps several cached representations of words:
- word meanings
- next-word observation distributions
- flattened word-operator coefficient vectors
- Taylor-order-adjusted flattened operator vectors
These caches are useful for analogy experiments, nearest-neighbor search,
quantum-like decomposition, and next-word prediction evaluation.
Attributes:
learner (ComplexTaylorWordFunctionLearner):
Learner to be evaluated.
auto_update_learner_cache (bool):
Whether self.learner.update_cache() should be called
when self.update_cache().
vec (dict[str, torch.Tensor]):
Word to word meaning.
Each value has shape (meaning_vector_dim,)
and dtype torch.cfloat.
D (dict[str, torch.Tensor]):
Word to Born-style next-word observation distribution.
Each value has shape (vocab_size,)
and dtype torch.float.
A (dict[str, torch.Tensor]):
Word to flattened word-operator coefficient vector.
Each value has shape (d * sum(d ** n for n in range(k + 1)),)
and dtype torch.cfloat.
A_adjusted (dict[str, torch.Tensor]):
Word to flattened word-operator coefficient vector,
where each Taylor-order block is divided by
the square root of its block size.
This is useful when comparing operator coefficients
across Taylor orders with different parameter counts.
"""
def __init__(
self, learner, auto_update_learner_cache=True, update_learner_cache=None
):
"""Initialize the evaluator.
self.update_cache() is called during initialization,
so cached representations are immediately available after construction.
Args:
learner (ComplexTaylorWordFunctionLearner):
Learner to be evaluated.
The learner must have a model compatible
with ComplexTaylorWordFunctionLM.
auto_update_learner_cache (bool, optional):
Whether self.learner.update_cache() should be called
whenever self.update_cache() runs in future.
Default value is True.
update_learner_cache (bool|None, optional):
If bool, whether self.learner.update_cache() should be called
at the time __init__ runs.
If None, self.auto_update_learner_cache will be adopted.
Default value is None.
"""
self.learner = learner
self.auto_update_learner_cache = bool(auto_update_learner_cache)
self.vec = {}
self.D = {}
self.A = {}
self.A_adjusted = {}
self.update_cache(update_learner_cache)
def update_cache(self, update_learner_cache=None):
"""Recalculate cached evaluation representations.
This method refreshes:
self.vec
word meanings stored in self.learner.model.word_meanings_povm
self.D
Born-style next-word distributions computed from self.vec
self.A
flattened word-operator coefficient vectors
self.A_adjusted
flattened word-operator coefficient vectors
whose Taylor-order blocks are normalized by block size
This should be called after training steps, vocabulary changes,
or POVM-cache updates if the evaluator cache should reflect
the latest model state.
Args:
update_learner_cache (bool|None, optional):
If bool, whether self.learner.update_cache() should be called.
If None, self.auto_update_learner_cache will be adopted.
Default value is None.
"""
update_learner_cache = \
self.auto_update_learner_cache if update_learner_cache is None \
else update_learner_cache
if update_learner_cache:
self.learner.update_cache()
model = self.learner.model
# vec
V_meanings = model.word_meanings_povm.detach().clone()
self.vec = {model.itos[i]:m for (i,m) in enumerate(V_meanings)}
# D
probss = model.born_probs_from_meaning(V_meanings)
self.D = {model.itos[i]:probs for (i,probs) in enumerate(probss)}
# A
chunks_False = []
chunks_True = []
for n in range(model.k + 1):
A = model.A[n] # shape (V, d, d**n)
chunk_False = A.reshape(A.shape[0], -1)
chunk_True = chunk_False / math.sqrt(chunk_False.shape[1])
chunks_False.append(chunk_False)
chunks_True .append(chunk_True)
flats_False = torch.cat(chunks_False, dim=1)
flats_True = torch.cat(chunks_True, dim=1)
self.A = {model.itos[i]:vec for (i,vec) in enumerate(flats_False)}
self.A_adjusted = {
model.itos[i]:vec for (i,vec) in enumerate(flats_True)
}
def eval_accuracy(self, batch_size=1024):
"""Evaluate next-word prediction accuracy on self.learner.examples.
For each training example
(context_words, target_word)
this method predicts the next word from context_words and checks
whether the argmax prediction equals target_word.
Args:
batch_size (int, optional):
Number of examples processed at once.
Default value is 1024.
Returns:
float:
Next-word prediction accuracy.
The value is
correct_predictions / total_examples.
Raises:
ValueError:
Propagated from self.learner.make_batch_tensor()
if an unknown word is found under strange="error".
"""
model = self.learner.model
correct = 0
total = 0
with torch.no_grad():
examples = self.learner.examples
for i in range(0, len(examples), batch_size):
batch = examples[i:i + batch_size]
(x, targets) = self.learner.make_batch_tensor(
batch, strange="error"
)
log_probs = model.forward_sequence(x)
preds = log_probs.argmax(dim=1)
correct += (preds == targets).sum().item()
total += targets.numel()
return correct / total
def predict_next(self, test_contexts, topk=5):
"""Predict likely next words for given contexts.
Args:
test_contexts (list[str] | list[list[str]]):
Context words.
If a single context is given as
["ジャイアン", "男"],
this method returns the top-k candidates
for that single context.
If multiple contexts are given as
[
["ジャイアン"],
["正恩", "男", "妹"]
],
this method returns the top-k candidates for each context.
topk (int | None, optional):
Number of candidates returned for each context.
If None or larger than vocabulary size, vocabulary size is used.
Default value is 5.
Returns:
list[tuple[str, float]] | list[list[tuple[str, float]]]:
If test_contexts is a single context list[str], returns
[
(word_0, probability_0),
(word_1, probability_1),
...
]
for that context.
If test_contexts is a list of contexts list[list[str]], returns
[
[
(word_0, probability_0),
(word_1, probability_1),
...
],
[
(word_0, probability_0),
(word_1, probability_1),
...
],
...
]
where each inner list corresponds to one context.
probability is obtained by exponentiating
self.learner.model.forward_sequence() output.
Raises:
ValueError:
Propagated from self.learner.make_batch_tensor()
if an unknown word is found under strange="error".
"""
if test_contexts and isinstance(test_contexts[0], str):
return self.predict_next([test_contexts], topk)[0]
topk = min(topk or torch.inf, self.learner.model.V)
with torch.no_grad():
batch = [(c, NULL_TOKEN) for c in test_contexts]
(x, _) = self.learner.make_batch_tensor(batch, strange="error")
tops = [
zip(*(
[
self.learner.model.itos[i.item()]
for i in torch.topk(probs, k=topk).indices
],
[
p.item()
for p in torch.topk(probs, k=topk).values
]
))
for probs in torch.exp(self.learner.model.forward_sequence(x))
]
return tops
def analogy_nearest(self, query, word_to_vec, mode="abs", eps=1e-12):
"""Sort words by similarity to an analogy query vector.
This method normalizes the query and all candidate vectors,
then computes complex inner products between them.
The score definition depends on mode:
"abs" : abs(<query|word>)
"re" : Re(<query|word>)
"abs with sign": sign(Re(<query|word>)) * abs(<query|word>)
Args:
query (torch.Tensor):
Query vector.
Shape must be compatible with vectors in word_to_vec.
word_to_vec (dict[str, torch.Tensor]):
Mapping from word to vector representation.
Typical inputs are:
self.vec
self.D
self.A
self.A_adjusted
mode (str, optional):
Similarity score mode.
Valid values are "abs", "re", or "abs with sign".
Default value is "abs".
eps (float, optional):
Small value used to avoid division by zero during normalization.
Default value is 1e-12.
Returns:
list[tuple[str, float]]:
Words sorted by descending similarity score.
Each element is
(word, score).
Raises:
ValueError: if mode is illegal.
"""
first_vec = list(word_to_vec.values())[0]
query = (query / (torch.linalg.norm(query) + eps)).to(first_vec.device)
word_to_vec = {
word: vec / (torch.linalg.norm(vec) + eps)
for (word, vec) in word_to_vec.items()
}
mat = torch.empty(
(0, first_vec.shape[0]),
dtype=first_vec.dtype,
device=first_vec.device
)
model = self.learner.model
itos = model.itos
for i in range(model.V):
word = itos[i]
vec = word_to_vec[word]
mat = torch.cat([mat, vec.unsqueeze(0)], dim=0)
raw = (query.conj().unsqueeze(0) @ mat.T)[0]
if "abs" in mode.lower() and "sign" in mode.lower():
sign = torch.where(raw.real >= 0, 1, -1)
scores = sign * torch.abs(raw)
elif "abs" in mode.lower():
scores = torch.abs(raw)
elif mode.lower() == "re":
scores = raw.real
else:
raise ValueError("mode")
word_to_score = {
itos[i]:score.item() for (i,score) in enumerate(scores)
}
sorted_words = sorted(
word_to_vec.keys(), key=lambda w: -word_to_score[w]
)
word_and_scores = [
(word, word_to_score[word])
for word in sorted_words
]
return word_and_scores
def apply_word_operator(self, words, zs):
"""Apply word operators to semantic states.
For each pair
(word, z),
this method computes
word(z)
using self.learner.model.apply_word().
words and zs should represent the same batch size
when batched input is used.
Args:
words (str | list[str]):
Word or words whose operators are applied.
zs (torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor]):
Input semantic state or states.
For batched input, expected shape is
(batch_size, meaning_vector_dim).
For single input, expected shape is
(meaning_vector_dim,)
Returns:
torch.Tensor:
If both words and zs are single:
shape (meaning_vector_dim,)
If else:
shape (batch_size, meaning_vector_dim)
Dtype is torch.cfloat.
Raises:
KeyError: If a word is not found in self.learner.model.stoi.
"""
words_is_single = isinstance(words, str)
zs_is_single = isinstance(zs, torch.Tensor) and zs.dim() == 1
# 1. words と zs が single なら single を返す
if words_is_single and zs_is_single:
return self.apply_word_operator([words], [zs])[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if words_is_single: return self.apply_word_operator([words]*len(zs), zs)
if zs_is_single: return self.apply_word_operator(words, [zs]*len(words))
# 3. zs が listかtuple なら torch.Tensor にする
if isinstance(zs, (list, tuple)):
return self.apply_word_operator(words, torch.stack(zs, dim=0))
# 4. 単語作用素を適用する
model = self.learner.model
stoi = model.stoi
return model.apply_word([stoi[w] for w in words],zs.to(model.z0.device))
def invert_word_operator_numerically(
self, words, targets, steps=300, lr=0.05, eps=1e-12
):
"""Numerically invert word operators.
For each pair
(word, target),
this method searches for a normalized vector x such that
word(x) ≈ target
by optimizing x with Adam while keeping model parameters fixed.
This is useful for nonlinear inverse-operator analogy experiments,
for example:
Find x such that
ジャイアン(x) ≈ 正恩,
then evaluate
ジャイ子(x),
this is practically the same as
ジャイ子(ジャイアン^{-1}(正恩)),
hoped to be close to
与正.
Args:
words (str | list[str]):
Word or words whose operators are inverted.
targets (torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor]):
Target semantic state or states.
For batched input, expected shape is
(batch_size, meaning_vector_dim).
For single input, expected shape is
(meaning_vector_dim,).
steps (int, optional):
Number of optimization steps.
Default value is 300.
lr (float, optional):
Learning rate of the temporary Adam optimizer.
Default value is 0.05.
eps (float, optional):
Small value used to avoid division by zero during normalization.
Default value is 1e-12.
Returns:
torch.Tensor:
Approximate inverse input vector or vectors.
If both words and targets are single:
shape (meaning_vector_dim,)
If else:
shape (batch_size, meaning_vector_dim)
Dtype is torch.cfloat.
Raises:
KeyError: If a word is not found in self.learner.model.stoi.
"""
words_is_single = isinstance(words, str)
targets_is_single = \
isinstance(targets, torch.Tensor) and targets.dim() == 1
# 1. words と targets が single なら single を返す
if words_is_single and targets_is_single:
return self.invert_word_operator_numerically(
[words], [targets], steps, lr, eps
)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if words_is_single:
return self.invert_word_operator_numerically(
[words]*len(targets), targets, steps, lr, eps
)
if targets_is_single:
return self.invert_word_operator_numerically(
words, [targets]*len(words), steps, lr, eps
)
# 3. targets が listかtuple なら torch.Tensor にする
if isinstance(targets, (list, tuple)):
return self.invert_word_operator_numerically(
words, torch.stack(targets, dim=0), steps, lr, eps
)
# 4. 逆作用のようなものを適用する
norm = torch.linalg.norm
normalize = lambda vecs: vecs / (norm(vecs, dim=-1, keepdim=True) + eps)
model = self.learner.model
requires_grads = [p for p in model.parameters() if p.requires_grad]
try:
for p in requires_grads:
p.requires_grad_(False)
with torch.enable_grad():
targets = normalize(targets).detach().to(model.z0.device)
# optimize x only
x = targets.clone().detach().requires_grad_(True)
opt_inv = torch.optim.Adam([x], lr=lr)
wids = torch.tensor(
[model.stoi[w] for w in words],
dtype=torch.long, device=targets.device
)
for _ in range(steps):
opt_inv.zero_grad()
y = normalize(model.apply_word(wids, x))
loss = (y - targets).abs().pow(2).sum(dim=1).mean()
loss = loss + 0.01 * (norm(x, dim=1) - 1.0).pow(2).mean()
loss.backward()
opt_inv.step()
with torch.no_grad():
x[:] = normalize(x)
return normalize(x.detach())
finally:
for p in requires_grads:
p.requires_grad_(True)
@classmethod
def quantum_decompose(cls, zs, basess, eps=1e-12):
"""Decompose vectors into non-orthogonal basis components.
Given normalized vectors
|z>,
|base_0>, |base_1>, ..., |base_{m-1}>,
this method finds
alphas (complex scalars),
beta (complex scalar),
|other> (complex vector)
satisfying
|z> = Σ_i alpha_i |base_i> + beta |other>
where
|other>
is normalized and orthogonal to the subspace spanned by the bases.
The basis vectors do not have to be mutually orthogonal.
Coefficients are computed using the Gram matrix pseudo-inverse.
Inputs are normalized internally, so callers do not need
to pre-normalize zs or basess.
Args:
zs (torch.Tensor | list[torch.Tensor] | tuple[torch.Tensor]):
Target vector or vectors.
If torch.Tensor, supported shapes are:
(meaning_vector_dim,)
(batch_size, meaning_vector_dim)
If list or tuple, supported component torch.Tensor shape is:
(meaning_vector_dim,)
basess (
torch.Tensor
| list[torch.Tensor]
| tuple[torch.Tensor]
| (list|tuple)[(list|tuple)[torch.Tensor]]
):
Basis vectors.
If torch.Tensor, supported shapes are:
(num_base, meaning_vector_dim)
(batch_size, num_base, meaning_vector_dim)
If list or tuple and components are torch.Tensor,
supported shape is:
(num_base, meaning_vector_dim)
If list or tuple and components are list or tuple,
supported component component torch.Tensor shape is:
(meaning_vector_dim,)
eps (float, optional):
Small value used to avoid division by zero during normalization.
Default value is 1e-12.
Returns:
dict[str, torch.Tensor]:
If both zs and basess are single, returns:
{
"alphas": shape (num_base,),
"beta": shape (),
"other": shape (meaning_vector_dim,)
}
If else, returns:
{
"alphass": shape (batch_size, num_base),
"betas": shape (batch_size,),
"others": shape (batch_size, meaning_vector_dim)
}
All returned tensors use dtype torch.cfloat.
"""
zs_is_single = isinstance(zs, torch.Tensor) and zs.dim() == 1
basess_is_single = (
isinstance(basess, torch.Tensor) and basess.dim() == 2
) or (
isinstance(basess, (list, tuple)) \
and len(basess) > 0 \
and isinstance(basess[0], torch.Tensor) and basess[0].dim() == 1
)
# 1. zs と basess が single なら single用のdictを返す
if zs_is_single and basess_is_single:
result_dict = cls.quantum_decompose([zs], [basess], eps)
return {key[:-1]: value[0] for (key, value) in result_dict.items()}
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if zs_is_single:
return cls.quantum_decompose([zs]*len(basess), basess, eps)
if basess_is_single:
return cls.quantum_decompose(zs, [basess]*len(zs), eps)
# 3. list や tuple を torch.Tensor にする
if isinstance(zs, (list, tuple)):
return cls.quantum_decompose(torch.stack(zs, dim=0), basess, eps)
if isinstance(basess, (list, tuple)):
return cls.quantum_decompose(
zs,
torch.stack([
torch.stack(list(bases), dim=0) for bases in basess
], dim=0),
eps
)
# 4. 分解する
norm = lambda vecs: (
torch.linalg.norm(vecs, dim=-1, keepdim=True) + eps
)
normalize = lambda vecs: vecs / norm(vecs)
zs = normalize(zs.to(torch.cfloat))
basess = normalize(basess.to(torch.cfloat))
B = basess
## 4-1. Gram[b,i,j] = <bases[i]|bases[j]>
Gram = torch.einsum("bik,bjk->bij", B.conj(), B)
## 4-2. rhs[b,i] = <bases[i]|z>
rhs = torch.einsum("bik,bk->bi", B.conj(), zs)
## 4-3. (alphas = ) alphass[b] = Gram[b]^{-1} rhs[b]
alphass = torch.einsum("bij,bj->bi", torch.linalg.pinv(Gram), rhs)
## 4-4. projections[b] = Σ_i alphas[i] |bases[i]>
projections = torch.einsum("bi,bik->bk", alphass, B)
## 4-5. betas と others を求める
residuals = zs - projections
betas = norm(residuals).squeeze(-1).to(torch.cfloat)
others = normalize(residuals)
# 5. 返す
return {
"alphass": alphass,
"betas": betas,
"others": others
}
def quantum_prob(self, target_words, base_wordss, eps=1e-12):
"""Compute quantum-like decomposition statistics for words.
For each target word x and basis words b_i, this method decomposes
|x> = Σ_i alpha_i |b_i> + beta |other>
using quantum_decompose(), then returns amplitudes, probabilities,
interference terms, relative phases, and the residual component.
Basis words are allowed to be non-orthogonal.
Therefore, |alpha_i|**2 should not be interpreted
as an independent classical probability by itself.
Example:
If
target_word = "ドラえもん",
base_words = ["正義", "悪"],
then this method analyzes
|ドラえもん>
= alpha_0 |正義>
+ alpha_1 |悪>
+ beta |other>.
This example will be referred to as
"the justice-evil decomposition of ドラえもん".
Args:
target_words (str | list[str]):
Target word or target words.
If str, a single result dict is returned.
base_wordss (list[str] | list[list[str]]):
Basis words.
If target_words is str, this may be a single list
of basis words (list[str]).
If target_words is list[str], this may be either:
- list[str]:
same basis words are used for all target words
- list[list[str]]:
each target word has its own basis word list
eps (float, optional):
Small value used to avoid division by zero.
Default value is 1e-12.
Returns:
dict[str, object] | list[dict[str, object]]:
If target_words is str, returns one dict.
Otherwise, returns a list of dicts.
Each result dict has keys:
"target" (str):
Target word.
In the justice-evil decomposition of ドラえもん:
"ドラえもん"
"alpha" (dict[str, torch.Tensor]):
Basis word to decomposition coefficient.
In the justice-evil decomposition of ドラえもん:
{
"正義": alpha_0,
"悪": alpha_1
}
"beta" (torch.Tensor):
Residual amplitude.
Shape is () and dtype is torch.cfloat.
In the justice-evil decomposition of ドラえもん:
beta
"interference" (dict[tuple[str, str], torch.Tensor]):
Ordered basis-word pair to interference term.
For pair (i, j), the value is
2 * Re(conj(alpha_i) * alpha_j * <base_i|base_j>).
In the justice-evil decomposition of ドラえもん:
{
("正義", "悪"):
2 Re(
conj(alpha_0) alpha_1 <正義|悪>
),
("悪", "正義"):
2 Re(
conj(alpha_1) alpha_0 <悪|正義>
)
}
"phase" (dict[tuple[str, str], torch.Tensor]):
Ordered basis-word pair to relative phase.
For pair (i, j), the value is
angle(
(alpha_j / (alpha_i + eps))
* <base_i|base_j>
)
In the justice-evil decomposition of ドラえもん:
{
("正義", "悪"):
angle(
(alpha_1 / alpha_0) <正義|悪>
),
("悪", "正義"):
angle(
(alpha_0 / alpha_1) <悪|正義>
)
}
"P" (dict[str, torch.Tensor]):
Basis word to Born-style overlap probability.
For basis word b_i, the value is
|<b_i|target>| ** 2.
In the justice-evil decomposition of ドラえもん:
{
"正義": |<正義|ドラえもん>|**2,
"悪": |<悪|ドラえもん>|**2
}
"Pother" (torch.Tensor):
Residual overlap probability.
|<other|target>| ** 2
In the justice-evil decomposition of ドラえもん:
|<other|ドラえもん>|**2
"other" (torch.Tensor):
Normalized residual vector.
Shape is (meaning_vector_dim,)
and dtype is torch.cfloat.
In the justice-evil decomposition of ドラえもん:
|other>
Raises:
KeyError:
If target_words or base_wordss contain words
not found in self.vec.
"""
target_is_single = isinstance(target_words, str)
base_is_single = base_wordss and isinstance(base_wordss[0], str)
# 1. target_words と base_wordss が single なら single を返す
if target_is_single and base_is_single:
return self.quantum_prob([target_words], [base_wordss], eps)[0]
# 2. 片方だけが single なら もう片方のバッチサイズに合わせる
if target_is_single:
return self.quantum_prob(
[target_words]*len(base_wordss), base_wordss, eps
)
if base_is_single:
return self.quantum_prob(
target_words, [base_wordss]*len(target_words), eps
)
# 3. 分解する
## 3-1. 単語をベクトル化する
zs = torch.stack([self.vec[w] for w in target_words], dim=0)
basess = torch.stack(
[
torch.stack([self.vec[w] for w in base_words], dim=0)
for base_words in base_wordss
],
dim=0
)
## 3-2. 分解する
decomped = self.quantum_decompose(zs, basess, eps=eps)
alphass = decomped["alphass"] # (batch_size, num_base)
betas = decomped["betas"] # (batch_size,)
others = decomped["others"] # (batch_size, d)
## 3-3. 各値を求める
results = []
for (b, (target_word, base_words)) in enumerate(
zip(target_words, base_wordss)
):
z = zs[b]
bases = basess[b]
alphas = alphass[b]
beta = betas[b]
other = others[b]
Gram = bases.conj() @ bases.T # 行列 @ 行列 → 行列
base_overlaps = bases.conj() @ z # 行列 @ ベクトル → ベクトル
other_overlap = other.conj() @ z # ベクトル @ ベクトル → スカラ
### 3-3-1. alpha
alpha_dict = {w: alphas[i] for (i,w) in enumerate(base_words)}
### 3-3-2. (beta は既に求めてある)
### 3-3-3. interference
interference_dict = {
(base_words[i], base_words[j]):
2 * torch.real(alphas[i].conj() * alphas[j] * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
### 3-3-4. phase
phase_dict = {
(base_words[i], base_words[j]):
torch.angle((alphas[j] / (alphas[i] + eps)) * Gram[i,j])
for i in range(len(base_words))
for j in range(len(base_words))
if i != j
}
### 3-3-5. P, Pother
P_dict = {
w: base_overlaps[i].abs().pow(2)
for (i,w) in enumerate(base_words)
}
Pother = other_overlap.abs().pow(2)
### 3-3-6. 結果に追加する
results.append({
"target": target_word,
"alpha": alpha_dict,
"beta": beta,
"interference": interference_dict,
"phase": phase_dict,
"P": P_dict,
"Pother": Pother,
"other": other
})
# 4. 返す
return results
(コードを表示 ここまで)
4-12. 評価器クラスのテスト
現状のステータスは次のとおり。
(カレントディレクトリ)
├ main.py # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py # ✅モデルクラス
├ ComplexTaylorWordFunctionLearner.py # ✅モデルを学習するためのクラス
├ constants.py # ✅複数ファイルで共有する定数など (コーパス以外)
├ corpus.py # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # 👀学習結果や経過を評価するためのクラス
└ test_project.py # 🛠️テスト用コード
ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり
今回は、 pytest を使って ComplexTaylorWordFunctionEvaluator.py をテストしていこう。
テストコード test_project.py は次のとおり。
テストコード test_project.py を表示
import pytest
from constants import NULL_TOKEN, NULL_ID
from ComplexTaylorWordFunctionLM import ComplexTaylorWordFunctionLM
from ComplexTaylorWordFunctionLearner import ComplexTaylorWordFunctionLearner
from ComplexTaylorWordFunctionEvaluator\
import ComplexTaylorWordFunctionEvaluator
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))
# Learner
@pytest.fixture
def sentences():
return [
["I", "love", "you", "very", "much"],
["you", "love", "you"],
["thank", "you", "very", "much"]
]
@pytest.fixture
def learner(sentences):
return ComplexTaylorWordFunctionLearner(sentences, d=4)
def test_Learner_init_error(sentences):
with pytest.raises(ValueError, match="strange must be") as exc_info:
learner = ComplexTaylorWordFunctionLearner(
sentences, strange="illegal_strange"
)
with pytest.raises(ValueError, match="auto_update_cache") as exc_info:
learner = ComplexTaylorWordFunctionLearner(
sentences, auto_update_cache="illegal_auto_update_cache"
)
with pytest.raises(ValueError, match="norm_reg_mode") as exc_info:
learner = ComplexTaylorWordFunctionLearner(
sentences, norm_reg_mode="illegal_norm_reg_mode"
)
with pytest.raises(ValueError, match="POVM needs enough word meanings")\
as exc_info:
learner = ComplexTaylorWordFunctionLearner(sentences)
with pytest.raises(Exception) as exc_info:
learner = ComplexTaylorWordFunctionLearner(
sentences, d=4, optimizer="illegal_optimizer"
)
def test_Learner_init(learner, sentences):
# strange
assert learner.strange == "error"
# auto_update_cache
assert not learner.auto_update_cache_before_learning
assert learner.auto_update_cache_after_learning
learner1 = ComplexTaylorWordFunctionLearner(
sentences, d=4, auto_update_cache=True
)
assert learner1.auto_update_cache_before_learning
assert learner1.auto_update_cache_after_learning
learner2 = ComplexTaylorWordFunctionLearner(
sentences, d=4, auto_update_cache=False
)
assert not learner2.auto_update_cache_before_learning
assert not learner2.auto_update_cache_after_learning
assert learner.model.d == 4
assert learner.model.k == 4
assert (['<Null>', 'I', 'love', 'you', 'very', 'much'], '<Null>') \
in learner.examples
learner3 = ComplexTaylorWordFunctionLearner(
sentences, d=4, context_len=4
)
assert (['<Null>', 'I', 'love', 'you', 'very', 'much'], '<Null>') \
not in learner3.examples
assert (['love', 'you', 'very', 'much'], '<Null>') \
in learner3.examples
assert learner.opt.__class__ == torch.optim.AdamW
def test_Learner_set_optimizer(learner):
# None
learner.set_optimizer(None)
assert learner.opt is None
# (error)
with pytest.raises(ValueError, match="unknown optimizer") as exc_info:
learner.set_optimizer("illegal_optim")
# "SGD"
learner.set_optimizer("SGD")
assert learner.opt.__class__ == torch.optim.SGD
# Adam
learner.set_optimizer(torch.optim.Adam)
assert learner.opt.__class__ == torch.optim.Adam
# kwargs
learner.set_optimizer(eps=2.0)
assert learner.opt.defaults["eps"] == 2.0
def test_Learner_add_words(learner):
V_old = learner.model.V
# optimizer_rebuilt flag (1/2)
assert learner.optimizer_rebuilt
learner.add_words(["strange0"], rebuild_optimizer=False)
assert not learner.optimizer_rebuilt
learner.set_optimizer()
assert learner.optimizer_rebuilt
# optimizer_rebuilt flag (2/2)
result = learner.add_words(["strange1"])
assert learner.optimizer_rebuilt
assert result == ["strange1"]
# hook
learner.added_words = []
def hook(added_words):
learner.added_words += added_words
learner.on_before_word_added = hook
learner.add_words(["strange2"])
assert learner.added_words == ["strange2"]
# V
assert learner.model.V == V_old + 3
result = learner.add_words(["strange2"]) # 既に追加済みの単語
assert learner.model.V == V_old + 3
assert result == []
def test_Learner_remove_words(learner):
# 存在しない単語を削除しても何も起きない
V_old = learner.model.V
result = learner.remove_words(["strange0"])
assert result == []
assert learner.model.V == V_old
# example_policy の異常検知
with pytest.raises(ValueError, match="example_policy") as exc_info:
learner.remove_words(["I"], example_policy="illegal_value")
# example_policy = "keep"
old_examples = list([e for e in learner.examples])
learner.remove_words(["I"], example_policy="keep")
assert learner.examples == old_examples
learner.add_words(["I"])
# example_policy = "null"
assert any(["love" in e for e in old_examples])
result = learner.remove_words(["love"], example_policy="null")
assert result == ["love"]
assert not any(["love" in e for e in learner.examples])
assert len(learner.examples) == len(old_examples)
assert (['<Null>', 'I', '<Null>', 'you', 'very', 'much'], '<Null>') \
in learner.examples
learner.add_words(["love"])
learner.examples = list([e for e in old_examples])
# example_policy = "drop"
assert any(["you" in e for e in old_examples])
learner.remove_words(["you"])
assert not any(["you" in e for e in learner.examples])
assert len(learner.examples) < len(old_examples)
learner.add_words(["you"])
learner.examples = list([e for e in old_examples])
# hook
learner.removed_words = []
def hook(removed_words):
learner.removed_words += removed_words
learner.on_word_removed = hook
learner.remove_words(["very", "much"])
assert set(learner.removed_words) == {"very", "much"}
# null 削除拒否
with pytest.raises(ValueError, match="cannot be removed") as exc_info:
learner.remove_words([NULL_TOKEN])
# 語彙が小さくなりすぎる
with pytest.raises(RuntimeError, match="assumes d <= V") as exc_info:
while learner.model.V:
id1 = learner.model.itos[1]
learner.remove_words([id1])
def test_Learner_make_examples(learner):
sentences = [
["I", "love", "you", "very", "much"],
["thank", NULL_TOKEN, "you"]
]
examples = learner.make_examples(sentences, context_len=4)
assert sorted(examples) == sorted([
([NULL_TOKEN], "I"),
([NULL_TOKEN, "I"], "love"),
([NULL_TOKEN, "I", "love"], "you"),
([NULL_TOKEN, "I", "love", "you"], "very"),
([ "I", "love", "you", "very"], "much"),
([ "love", "you", "very", "much"], NULL_TOKEN),
([NULL_TOKEN], "thank"),
([NULL_TOKEN, "thank"], "you"),
([NULL_TOKEN, "thank", "you"], NULL_TOKEN)
])
def test_Learner_make_batchs(learner):
assert max([len(batch) for batch in learner.make_batchs(8)]) == 8
# シャッフルする場合
examples = []
for batch in learner.make_batchs(6):
examples += batch
assert sorted(learner.examples) == sorted(examples)
assert learner.examples != examples
# シャッフルしない場合
examples = []
for batch in learner.make_batchs(6, shuffle=False):
examples += batch
assert learner.examples == examples
def test_Learner_make_batch_tensor(learner):
batch = learner.make_batchs(6)[0]
# empty batch
with pytest.raises(ValueError, match="empty batch") as exc_info:
learner.make_batch_tensor([])
# strange illegal
with pytest.raises(ValueError, match="strange") as exc_info:
learner.make_batch_tensor(batch, strange="illegal_strange")
# strange error
with pytest.raises(ValueError, match="unknown words") as exc_info:
learner.make_batch_tensor([(["未知語0"], "未知語1")], strange="error")
# strange add
V_old = learner.model.V
result = learner.make_batch_tensor(
[(["未知語0"], "未知語1")] + batch, strange="add"
)
assert learner.model.V == V_old + 2
assert isinstance(result, tuple)
(問題tensor, 正解tensor) = result
assert 問題tensor[0][0] == learner.model.stoi["未知語0"]
assert 正解tensor[0] == learner.model.stoi["未知語1"]
assert 問題tensor[1][0] == learner.model.stoi[batch[0][0][0]]
assert 正解tensor[2] == learner.model.stoi[batch[1][1]]
def test_Learner_update_cache(learner):
pass
def test_Learner_operator_l2_regularization(learner):
assert learner.operator_l2_regularization(mode=None) == \
learner.operator_l2_regularization(mode="both")
with pytest.raises(ValueError) as exc_info:
learner.operator_l2_regularization(mode="illegal_mode")
assert abs(
learner.operator_l2_regularization(mode="A").item()
- sum(
torch.abs(learner.model.A[n]).pow(2).mean()
for n in range(learner.model.k + 1)
)
) < 1e-8
def test_Learner_normalize_batch(learner):
batch = learner.make_batchs(6)[0]
assert learner.normalize_batch(batch) == batch
stoi = learner.model.stoi
batch_by_id = [([stoi[s] for s in q], stoi[a]) for (q, a) in batch]
assert learner.normalize_batch(batch_by_id) == batch
batch_as_tensor = learner.make_batch_tensor(batch)
# padding があるため元には戻らない
assert learner.normalize_batch(batch_as_tensor) != batch
# padding をちゃんとすれば元に戻る
q_len = batch_as_tensor[0].shape[1]
assert learner.normalize_batch(batch_as_tensor) == [
(
q + [NULL_TOKEN] * (q_len-len(q)),
a
)
for (q, a) in batch
]
def test_Learner_learn(learner):
with pytest.raises(RuntimeError, match="opt is None") as exc_info:
learner.set_optimizer(None)
learner.learn()
learner.set_optimizer("AdamW")
with pytest.raises(RuntimeError, match="expired") as exc_info:
learner.add_words(["strange"], rebuild_optimizer=False)
learner.learn()
learner.remove_words(["strange"], rebuild_optimizer=True)
batch = learner.make_batchs(4)[0]
learner.learn(batch)
# 勾配蓄積モードなら勾配が変化しない (パラメータは変化する)
A_grad_old = learner.model.A[0].grad.detach().clone()
A_old = learner.model.A[0] .detach().clone()
learner.learn()
A_grad_new = learner.model.A[0].grad
A_new = learner.model.A[0]
assert_close(A_grad_old, A_grad_new)
assert_not_close(A_old, A_new)
# 通常モードなら勾配が変化する (当然パラメータも変化する)
A_grad_old = learner.model.A[0].grad.detach().clone()
A_old = learner.model.A[0] .detach().clone()
learner.learn(batch)
A_grad_new = learner.model.A[0].grad
A_new = learner.model.A[0]
assert_not_close(A_grad_old, A_grad_new)
assert_not_close(A_old, A_new)
# 戻り値
assert learner.learn() is None
assert set(learner.learn (batch).keys()) == \
set(learner.accumulate_grad(batch).keys())
# 通常モードで、バッチに未知語があれば、未知語が処理される
learner.words_added = False
def hook(*args, **kwargs):
learner.words_added = True
learner.on_word_added = hook
learner.learn(strange="add")
assert not learner.words_added
learner.learn([(["未知語0"], "未知語1")], strange="add")
assert learner.words_added
def test_Learner_zero_grad(learner):
pass
def test_Learner_accumulate_grad(learner):
batch = learner.make_batchs(5)[0]
learner.learn(batch)
# 勾配が変化する (パラメータは変化しない)
A_grad_old = learner.model.A[0].grad.detach().clone()
A_old = learner.model.A[0] .detach().clone()
learner.accumulate_grad(batch)
A_grad_new = learner.model.A[0].grad
A_new = learner.model.A[0]
assert_not_close(A_grad_old, A_grad_new)
assert_close(A_old, A_new)
# 戻り値
assert set(learner.accumulate_grad(batch)) == {
"loss", "ce_loss", "norm_reg", "batch_size"
}
# 未知語が処理される
learner.words_ignored = False
def hook(*args, **kwargs):
learner.words_ignored = True
learner.on_strange_ignored = hook
learner.accumulate_grad([(["未知語0"], "未知語1")], strange="ignore")
assert learner.words_ignored
# Evaluator
@pytest.fixture
def evaler(learner):
return ComplexTaylorWordFunctionEvaluator(learner)
def test_Evaluator_init(evaler, learner):
model = learner.model
assert evaler.learner is learner
assert evaler.auto_update_learner_cache
assert set(evaler.vec) == set(model.stoi)
assert set(evaler.D) == set(model.stoi)
assert set(evaler.A) == set(model.stoi)
assert set(evaler.A_adjusted) == set(model.stoi)
flat_dim = model.d * sum(model.d ** n for n in range(model.k + 1))
for word, word_id in model.stoi.items():
assert evaler.vec[word].shape == (model.d,)
assert evaler.vec[word].dtype == torch.cfloat
assert evaler.D[word].shape == (model.V,)
assert evaler.D[word].dtype in {torch.float32, torch.float64}
assert evaler.A[word].shape == (flat_dim,)
assert evaler.A[word].dtype == torch.cfloat
assert evaler.A_adjusted[word].shape == (flat_dim,)
assert evaler.A_adjusted[word].dtype == torch.cfloat
assert_close(
evaler.A[word],
model.flat_operator(word_id, adjust_n=False, normalize=False),
)
assert_close(
evaler.A_adjusted[word],
model.flat_operator(word_id, adjust_n=True, normalize=False),
)
def test_Evaluator_update_cache(evaler, learner):
model = learner.model
old = evaler.vec["I"].detach().clone()
with torch.no_grad():
model.A[0][model.stoi["I"]].fill_(1.0 + 0.0j)
model.reset_null()
evaler.update_cache()
assert_not_close(old, evaler.vec["I"])
assert_close(evaler.vec["I"], model.word_meanings_povm[model.stoi["I"]])
def test_Evaluator_eval_accuracy(evaler, learner):
acc = evaler.eval_accuracy(batch_size=3)
correct = 0
total = 0
for batch in learner.make_batchs(3, shuffle=False):
x, y = learner.make_batch_tensor(batch)
pred = learner.model.forward_sequence(x).argmax(dim=1)
correct += (pred == y).sum().item()
total += y.numel()
assert acc == correct / total
def test_Evaluator_predict_next(evaler, learner):
top = list(evaler.predict_next(["I"], topk=3))
assert len(top) == 3
assert all(isinstance(w, str) for (w, p) in top)
assert all(isinstance(p, float) for (w, p) in top)
assert all(0.0 <= p <= 1.0 for (w, p) in top)
tops = evaler.predict_next([["I"], ["thank", "you"]], topk=2)
tops = [list(t) for t in tops]
assert len(tops) == 2
assert all(len(t) == 2 for t in tops)
top_all = list(evaler.predict_next(["I"], topk=None))
assert len(top_all) == learner.model.V
def test_Evaluator_analogy_nearest(evaler):
z = {
w: v / (torch.linalg.norm(v) + 1e-12)
for (w, v) in evaler.vec.items()
}
for mode in ["abs", "re", "abs with sign"]:
result = evaler.analogy_nearest(z["I"], z, mode=mode)
assert result[0][0] == "I"
assert abs(result[0][1] - 1.0) < 1e-5
with pytest.raises(ValueError):
evaler.analogy_nearest(z["I"], z, mode="illegal")
def test_Evaluator_apply_word_operator(evaler, learner):
model = learner.model
z = model.z0
assert_close(
evaler.apply_word_operator("I", z),
model.apply_word([model.stoi["I"]], z.unsqueeze(0))[0],
)
zs = torch.stack([model.z0, model.z0], dim=0)
assert_close(
evaler.apply_word_operator("I", zs),
model.apply_word([model.stoi["I"], model.stoi["I"]], zs),
)
assert_close(
evaler.apply_word_operator(["I", "you"], z),
model.apply_word([model.stoi["I"], model.stoi["you"]], zs),
)
assert_close(
evaler.apply_word_operator(["I", "you"], [z, z]),
model.apply_word([model.stoi["I"], model.stoi["you"]], zs),
)
def test_Evaluator_invert_word_operator_numerically(evaler, learner):
model = learner.model
target = evaler.vec["I"]
batch = learner.make_batchs(4, shuffle=False)[0]
learner.learn(batch)
grad_old = model.A[0].grad.detach().clone()
requires_grad_old = [p.requires_grad for p in model.parameters()]
x = evaler.invert_word_operator_numerically(
"I", target, steps=2, lr=0.01
)
assert x.shape == target.shape
assert x.dtype == torch.cfloat
assert abs(torch.linalg.norm(x).item() - 1.0) < 1e-5
assert_close(model.A[0].grad, grad_old)
assert [p.requires_grad for p in model.parameters()] == requires_grad_old
targets = torch.stack([evaler.vec["I"], evaler.vec["you"]], dim=0)
xs = evaler.invert_word_operator_numerically(
["I", "you"], targets, steps=2, lr=0.01
)
assert xs.shape == (2, model.d)
assert_close(
torch.linalg.norm(xs, dim=1),
torch.ones(2),
atol=1e-5,
rtol=1e-5,
)
def test_Evaluator_quantum_decompose_single():
e0 = tensor([1, 0, 0, 0])
e1 = tensor([0, 1, 0, 0])
e2 = tensor([0, 0, 1, 0])
z = normalize(e0 + e2)
result = ComplexTaylorWordFunctionEvaluator.quantum_decompose(
z, [e0, e1]
)
assert set(result) == {"alphas", "beta", "other"}
assert_close(
result["alphas"],
tensor([1 / math.sqrt(2), 0]),
atol=1e-6,
rtol=1e-6,
)
assert_close(
result["beta"],
tensor(1 / math.sqrt(2)),
atol=1e-6,
rtol=1e-6,
)
assert_close(
torch.stack([e0, e1]).conj() @ result["other"],
tensor([0, 0]),
atol=1e-6,
rtol=1e-6,
)
assert_close(
torch.linalg.norm(result["other"]),
torch.tensor(1.0),
atol=1e-6,
rtol=1e-6,
)
def test_Evaluator_quantum_decompose_batched_and_broadcast():
e0 = tensor([1, 0, 0, 0])
e1 = tensor([0, 1, 0, 0])
e2 = tensor([0, 0, 1, 0])
z0 = normalize(e0 + e2)
z1 = normalize(e1 + e2)
bases = [e0, e1]
result = ComplexTaylorWordFunctionEvaluator.quantum_decompose(
[z0, z1], bases
)
assert set(result) == {"alphass", "betas", "others"}
assert result["alphass"].shape == (2, 2)
assert result["betas"].shape == (2,)
assert result["others"].shape == (2, 4)
assert_close(
result["alphass"],
tensor([
[1 / math.sqrt(2), 0],
[0, 1 / math.sqrt(2)],
]),
atol=1e-6,
rtol=1e-6,
)
bases_tensor = torch.stack([e0, e1], dim=0)
for other in result["others"]:
assert_close(
bases_tensor.conj() @ other,
tensor([0, 0]),
atol=1e-6,
rtol=1e-6,
)
basess = [bases, bases]
result2 = ComplexTaylorWordFunctionEvaluator.quantum_decompose(
z0, basess
)
assert result2["alphass"].shape == (2, 2)
def test_Evaluator_quantum_prob(evaler):
info = evaler.quantum_prob("I", ["love", "you"])
assert info["target"] == "I"
assert set(info["alpha"]) == {"love", "you"}
assert set(info["P"]) == {"love", "you"}
assert ("love", "you") in info["interference"]
assert ("you", "love") in info["interference"]
assert ("love", "you") in info["phase"]
assert info["other"].shape == evaler.vec["I"].shape
expected_p = (evaler.vec["love"].conj() @ evaler.vec["I"]).abs().pow(2)
assert_close(info["P"]["love"], expected_p)
bases = torch.stack([evaler.vec["love"], evaler.vec["you"]], dim=0)
assert_close(
bases.conj() @ info["other"],
tensor([0, 0]),
atol=1e-5,
rtol=1e-5,
)
infos = evaler.quantum_prob(["I", "you"], ["love", "very"])
assert len(infos) == 2
assert [x["target"] for x in infos] == ["I", "you"]
(テストコード test_project.py を表示 ここまで)
ComplexTaylorWordFunctionLearner.py をテストしたときのテストコードに加筆して、
from ComplexTaylorWordFunctionEvaluator import ComplexTaylorWordFunctionEvaluator と # Evaluator 以降を追加した。
上記のテストコードを用意した上で、カレントディレクトリにて 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 39 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.0094-0.0039j],
[-0.0221+0.0095j],
[-0.0265-0.0115j],
[ 0.0085-0.0082j]],
[[ 0.0156-0.0275j],
[-0.0228-0.0021j],
[-0.0318+0.0124j],
[ 0.0356-0.0345j]],
[[-0.0045+0.0116j],
[-0.0223+0.0021j],
[-0.0081+0.0063j],
[-0.0009+0.0335j]],
[[-0.0038+0.0139j],
[ 0.0005-0.0001j],
[ 0.0119-0.0119j],
[-0.0027-0.0017j]],
[[ 0.0422+0.0217j],
[-0.0119-0.0021j],
[ 0.0130-0.0096j],
[-0.0192-0.0379j]],
[[ 0.0180-0.0428j],
[-0.0133+0.0222j],
[ 0.0491+0.0038j],
[ 0.0360-0.0184j]]], 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]],
[[-7.9731e-03+1.5460e-02j, -1.5893e-02+2.5456e-03j,
-1.6312e-02+9.1565e-03j, -6.5215e-03+8.5229e-03j],
[ 8.4448e-03-8.4403e-03j, -1.4173e-02-1.1990e-03j,
7.3808e-03+1.0966e-02j, 6.5279e-04-1.0357e-02j],
[ 2.3636e-04-6.1832e-03j, -1.0755e-02-4.5151e-03j,
-1.5321e-02+4.8395e-03j, 1.2390e-03-2.3122e-02j],
[ 5.8188e-03+8.4287e-03j, 1.2066e-02+5.4432e-03j,
2.1044e-02+4.3305e-03j, 9.7193e-03-1.0680e-02j]],
[[ 1.1490e-02-2.0584e-02j, -1.5540e-02+1.4849e-02j,
-3.2577e-03+9.6463e-03j, 1.3989e-02+1.2522e-02j],
[ 7.1794e-03+6.8052e-05j, 8.4614e-03-2.7502e-03j,
-9.1446e-04+1.3397e-03j, -1.3502e-02+4.4802e-03j],
[-1.9656e-03+1.3648e-02j, 2.0687e-03-2.1871e-02j,
3.9596e-03-2.9706e-03j, -5.1367e-03+2.2707e-03j],
[ 1.2574e-02+1.2423e-03j, -8.1226e-03-1.6177e-02j,
-4.8724e-03-7.0679e-03j, 1.7332e-02-1.5048e-02j]],
[[ 1.1497e-02+8.2498e-03j, 7.9322e-03+9.6483e-03j,
-2.8749e-03-1.1754e-02j, -9.8417e-03-9.4701e-04j],
[ 1.1493e-02+2.0230e-02j, 1.8672e-02-1.3409e-02j,
1.9481e-02+2.9011e-03j, 2.6465e-03-1.9939e-02j],
[-6.3537e-03+8.0289e-03j, -1.3728e-02+1.4274e-02j,
1.1982e-04+4.4552e-03j, 9.0519e-03-2.1334e-03j],
[ 6.3886e-03+1.6137e-02j, -1.9638e-02-1.4477e-02j,
7.8268e-03+4.8046e-03j, -1.6054e-02+5.2812e-03j]],
[[ 1.0995e-02+3.5657e-03j, -6.5974e-03+5.2467e-03j,
1.1357e-02+9.2005e-03j, 1.3989e-03-2.5275e-03j],
[ 5.4663e-04+1.8476e-02j, -9.5399e-03-1.1775e-02j,
-1.0529e-02-1.6394e-03j, 2.9738e-03-5.7987e-03j],
[ 3.9861e-03-5.8886e-04j, -1.8021e-02+5.8050e-03j,
-5.4025e-03+5.5702e-04j, -6.7833e-03-9.6988e-03j],
[-3.9581e-03-9.4786e-03j, -2.7289e-03-9.9295e-03j,
6.5361e-03-1.0331e-02j, -1.1323e-02-4.9100e-03j]],
[[-9.3081e-03-1.2493e-03j, 2.3469e-02+6.3860e-03j,
7.1693e-03-1.4716e-02j, 2.1952e-03-1.2243e-02j],
[ 1.7742e-04-4.1985e-03j, 1.1895e-03+4.8096e-03j,
-1.1788e-02-4.4777e-04j, 7.1163e-03+6.5105e-03j],
[-3.9372e-04-2.1229e-03j, 2.9399e-03+8.1919e-03j,
-4.8041e-03+7.7311e-03j, 4.5157e-03-5.1640e-03j],
[ 3.6883e-03-8.5270e-03j, -2.6823e-03+4.8685e-03j,
1.2664e-02-1.1895e-02j, -7.7926e-03-1.3942e-02j]],
[[-2.5733e-02+1.3827e-02j, 4.2786e-03+8.2278e-03j,
-1.7604e-03+4.0415e-03j, -2.5668e-02-2.2885e-03j],
[-1.2204e-02-3.8125e-03j, -5.4722e-03-2.6231e-02j,
9.1910e-04-2.3613e-02j, -1.0290e-02+1.1150e-02j],
[-9.8706e-03-3.5213e-03j, -3.7959e-03-1.4023e-02j,
1.2373e-02-1.0795e-02j, 1.2655e-03-1.7319e-02j],
[-4.6889e-04+1.9876e-03j, -6.8645e-03+1.6420e-04j,
2.1517e-03-8.6205e-03j, -1.4686e-03+2.0761e-02j]]],
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=2.3260904526978265e-06
PASSED
test_project.py::test_Model_speak
(i=0,)model.speak(i, print_text=False)=''
(i=1,)model.speak(i, print_text=False)='love'
(i=2,)model.speak(i, print_text=False)='love very'
(i=3,)model.speak(i, print_text=False)='love love love'
(i=4,)model.speak(i, print_text=False)='<Null> much love <Null>'
(i=5,)model.speak(i, print_text=False)='you you very <Null> very'
(i=6,)model.speak(i, print_text=False)='<Null> very very love thank I'
(i=7,)model.speak(i, print_text=False)='thank thank love very very you I'
(i=8,)model.speak(i, print_text=False)='love I you thank thank much very love'
(i=9,)model.speak(i, print_text=False)='much much you thank thank love very very much'
(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)=['love', 'love']
(i=3,)model.speak(i, return_list=True, print_text=False)=['much', '<Null>', 'much']
(i=4,)model.speak(i, return_list=True, print_text=False)=['I', 'much', '<Null>', 'you']
(i=5,)model.speak(i, return_list=True, print_text=False)=['<Null>', 'thank', '<Null>', 'thank', 'thank']
(i=6,)model.speak(i, return_list=True, print_text=False)=['love', 'love', 'thank', 'thank', 'love', 'much']
(i=7,)model.speak(i, return_list=True, print_text=False)=['<Null>', '<Null>', 'very', '<Null>', 'very', 'very', 'very']
(i=8,)model.speak(i, return_list=True, print_text=False)=['<Null>', '<Null>', 'much', 'thank', 'thank', '<Null>', 'I', 'very']
(i=9,)model.speak(i, return_list=True, print_text=False)=['very', 'thank', 'thank', 'I', '<Null>', 'much', '<Null>', 'much', 'I']
PASSED
test_project.py::test_Model_operator_2_methods PASSED
test_project.py::test_Learner_init_error PASSED
test_project.py::test_Learner_init PASSED
test_project.py::test_Learner_set_optimizer PASSED
test_project.py::test_Learner_add_words PASSED
test_project.py::test_Learner_remove_words PASSED
test_project.py::test_Learner_make_examples PASSED
test_project.py::test_Learner_make_batchs PASSED
test_project.py::test_Learner_make_batch_tensor PASSED
test_project.py::test_Learner_update_cache PASSED
test_project.py::test_Learner_operator_l2_regularization PASSED
test_project.py::test_Learner_normalize_batch PASSED
test_project.py::test_Learner_learn PASSED
test_project.py::test_Learner_zero_grad PASSED
test_project.py::test_Learner_accumulate_grad PASSED
test_project.py::test_Evaluator_init PASSED
test_project.py::test_Evaluator_update_cache PASSED
test_project.py::test_Evaluator_eval_accuracy PASSED
test_project.py::test_Evaluator_predict_next PASSED
test_project.py::test_Evaluator_analogy_nearest PASSED
test_project.py::test_Evaluator_apply_word_operator PASSED
test_project.py::test_Evaluator_invert_word_operator_numerically PASSED
test_project.py::test_Evaluator_quantum_decompose_single PASSED
test_project.py::test_Evaluator_quantum_decompose_batched_and_broadcast PASSED
test_project.py::test_Evaluator_quantum_prob PASSED
================================================= 39 passed in 3.65s ==================================================
(cd)>
(結果を表示 ここまで)
全体のステータスが (テスト数) passed in (経過時間)s
となっており、x failed がないので、テストはすべて成功である。
改めて、現在のステータスを表示しておく。
(カレントディレクトリ)
├ main.py # 😴メインプログラム
├ ComplexTaylorWordFunctionLM.py # ✅モデルクラス
├ ComplexTaylorWordFunctionLearner.py # ✅モデルを学習するためのクラス
├ constants.py # ✅複数ファイルで共有する定数など (コーパス以外)
├ corpus.py # 😴トイコーパス
├ ComplexTaylorWordFunctionEvaluator.py # ✅学習結果や経過を評価するためのクラス
└ test_project.py # ✅テスト用コード
ステータスの凡例
😴: 未着手
🛠️: 今から作成・編集する
📌: 新たに編集が必要になる
👀: 作成済み(未テスト)
✅: 作成済み (テスト成功済み or テストを実施しない)
⚠️: 問題あり
5. 次回予告
次回は、4-13節以降を書いていく。いよいよ実験の準備の最終段階だ。
モデルを実際に動かす main.py と、モデルに学習させるための corpus.py を書いて、実験の準備を終える予定だ。
↓次回の記事

