Skip-Gram#

[発展課題]skip_gram.py#

(簡易的な)Skip-Gramを実装し,max_epochs=100, minibatch_size=512として訓練し,「サッカー」,「日本」,「女王」,「機械学習」について類似単語を類似度の高い順に上位5個表示するプログラムを作成してください.

  • cbow.pyを参考にしてください.

  • 学習にはja.text8を利用してください.

雛形:

class SkipGram(nn.Module):
    def __init__(self):
        super().__init__()
        ...

    def forward(self,x):
        ...

実装#

実行結果#

Usage#

argparserのdescriptionやhelpに説明を書き込んで,--help オプションで使い方が表示できるようにしてください.

(datasci) mriki@RikinoMac _prml % python skipgram.py -h
usage: skipgram.py [-h] [--learning_rate LEARNING_RATE] [--batch_size BATCH_SIZE] [--embedding_dim EMBEDDING_DIM] [--seed SEED] [--max_epochs MAX_EPOCHS] [--char_limit CHAR_LIMIT] [--device DEVICE] [--data_path DATA_PATH] [--save_path SAVE_PATH] [--window_size WINDOW_SIZE] [--query QUERY] [--topn TOPN]

Skip-Gramの訓練をja.text8で行う

options:
  -h, --help            show this help message and exit
  --learning_rate LEARNING_RATE
  --batch_size BATCH_SIZE
  --embedding_dim EMBEDDING_DIM
  --seed SEED
  --max_epochs MAX_EPOCHS
  --char_limit CHAR_LIMIT
                        ja.text8の先頭から何文字を利用するか.Noneの場合は全てを使う. ex. 1_000_000
  --device DEVICE
  --data_path DATA_PATH
                        訓練用コーパスの保存場所
  --save_path SAVE_PATH
                        学習済みモデルのファイル名.すでに存在していた場合はそれを読み込んで利用する
  --window_size WINDOW_SIZE
  --query QUERY         文字列を渡すと類似する単語をtopn個検索する
  --topn TOPN           検索単語数

実行#

初回学習時:

(datasci) mriki@RikinoMac _prml % python skipgram.py --char_limit 1000000 --seed 7012 --save_path ./skipgram.pkl --max_epochs 2
全文書の文字数が46507793あり,その内1000000だけを利用します.
前処理...
363003it [00:16, 21819.80it/s]
100%|███████████████████████| 80264/80264 [00:00<00:00, 900528.08it/s]
contextsのshape: (80264, 17871)
訓練開始...
  epoch    train_loss    train_ppl    valid_loss    valid_ppl      dur
-------  ------------  -----------  ------------  -----------  -------
      1        9.8043   18111.2683       10.1251   24961.8040  14.5564
      2        8.4743    4789.8352       10.6640   42786.7327  14.5319

学習済みの場合:

(datasci) mriki@RikinoMac _prml % python skipgram.py --char_limit 1000000 --seed 7012 --save_path ./skipgram.pkl --max_epochs 2
./skipgram.pklから学習済みモデルを読み込みます...

学習済みでクエリを検索する場合:

(datasci) mriki@RikinoMac _prml % python skipgram.py --save_path ./skipgram.pkl --query 日本
./skipgram.pklから学習済みモデルを読み込みます...
>>> 日本
1:古代  0.9472917318344116
2:文明  0.9328379034996033
3:社会  0.931919515132904
4:文化  0.9224883317947388
5:天皇  0.9139895439147949
(datasci) mriki@RikinoMac _prml % python skipgram.py --save_path ./skipgram.pkl --query ロボット
./skipgram.pklから学習済みモデルを読み込みます...
>>> ロボット
1:ロボティックス        0.8361095190048218
2:ステーション  0.8090811967849731
3:ぼう  0.8085721135139465
4:ロケット      0.7877843976020813
5:地球  0.7545166611671448

(簡易的な)Skip-Gram実装のヒント#

Note

このノートは Skip-Gram実装課題 のヒントになるように書かれています.

CBoWと遂になる単語埋め込みベクトル作成手法である Skip-Gram を実装します.ここでは,Negative Samplingのような技術を使わず,出力層でsoftmax関数を利用することで実装を簡単にしています.そのため計算コストが膨大になる傾向があり,大規模なコーパスに適用することはお勧めしません.

Skip-Gramの計算コストの大きさには出力層のSoftmax活性化関数が大きな影響を与えます.そのため,高速化を行うためにはSoftmaxを Negative Sampling と呼ばれるアルゴリズムで代用することになります.これについてはこのブログが実装の助けになります.また,直接Skip-Gramを紹介しているわけではないのですが,CBoWの説明の中でこれを説明しているゼロから作るDeep Learning ❷ ―自然言語処理編も非常に参考になるでしょう.

# packageのimport
import re
import math 
from typing import Any
from tqdm.std import trange,tqdm
import numpy as np 
import matplotlib.pyplot as plt 
import seaborn as sns
from scipy.sparse import lil_matrix

# pytorch関連のimport
import torch
import torch.nn as nn 
import torch.nn.functional as F 
import torch.optim as optim 
import skorch
from skorch import NeuralNetClassifier, NeuralNetRegressor
from skorch.callbacks import Callback, EpochScoring
from torch.utils.data import Dataset

from janome.tokenizer import Tokenizer

データの読み込み#

コーパスにはja.text8のサブセットを利用します.発展課題に取り組む場合も,指定されているハイパーパラメータやコーパスのサイズが実行困難である場合は適宜修正してください.ただし,その場合はskip_gram.pyの先頭行に,docstringを用意してその旨を書いてください.あるいはCLIのオプションにしてもいいかもしれません.

with open("./input/ja.text8") as f:
    text8 = f.read()
print(text8[:200])

#LIMIT = math.floor(len(text8)*0.1)
LIMIT = 100_0000
print(f"{LIMIT}/ {len(text8)}")
text8 = text8[:LIMIT]
ちょん 掛け ( ちょん がけ 、 丁 斧 掛け ・ 手斧 掛け と も 表記 ) と は 、 相撲 の 決まり 手 の ひとつ で ある 。 自分 の 右 ( 左 ) 足 の 踵 を 相手 の 右 ( 左 ) 足 の 踵 に 掛け 、 後方 に 捻っ て 倒す 技 。 手斧 ( ちょう な ) を かける 仕草 に 似 て いる こと から 、 ちょう な が 訛っ て ちょん 掛け と なっ 
1000000/ 46507793

形態素解析#

コーパス内の単語(トークン)全てを利用すると語彙が多くなりすぎるので,ここでは名詞(それも一般名詞と固有名詞)のみを利用します.そのために形態素解析を行う必要があるので,python製の形態素解析器であるjanomeを利用しています.形態素解析器はこれ以外にもMecabなどが有名です.

形態素解析器 janome#

ja.text8の一部に品詞分解を行なった結果を以下に示します.

t = Tokenizer()
sample_text = "".join(text8[:50].split())
for token in t.tokenize(sample_text):
    print(token.surface, "\t", token.part_of_speech.split(","))
ちょん 	 ['名詞', '一般', '*', '*']
掛け 	 ['名詞', '接尾', '一般', '*']
( 	 ['記号', '括弧開', '*', '*']
ちょん 	 ['名詞', '一般', '*', '*']
がけ 	 ['名詞', '接尾', '一般', '*']
、 	 ['記号', '読点', '*', '*']
丁 	 ['名詞', '固有名詞', '人名', '姓']
斧 	 ['名詞', '一般', '*', '*']
掛け 	 ['名詞', '接尾', '一般', '*']
・ 	 ['記号', '一般', '*', '*']
手斧 	 ['名詞', '一般', '*', '*']
掛け 	 ['名詞', '接尾', '一般', '*']
と 	 ['助詞', '格助詞', '引用', '*']
も 	 ['助詞', '係助詞', '*', '*']
表記 	 ['名詞', 'サ変接続', '*', '*']
) 	 ['記号', '括弧閉', '*', '*']
と 	 ['助詞', '格助詞', '引用', '*']
は 	 ['助詞', '係助詞', '*', '*']
、 	 ['記号', '読点', '*', '*']
相撲 	 ['名詞', '一般', '*', '*']

janomeを使った語彙辞書作成#

活用する語彙をまとめた辞書(word2id, id2word)を作成します.この実装はダーティなので,実際に自然言語処理を行う場合は参考にしないでください.

def my_analyzer(text):
    #text = code_regex.sub('', text)
    #tokens = text.split()
    #tokens = filter(lambda token: re.search(r'[ぁ-ん]+|[ァ-ヴー]+|[一-龠]+', token), tokens)
    tokens = []
    for token in tqdm(t.tokenize(text)):
        pos = token.part_of_speech.split(",")
        if "名詞" == pos[0]:
            if "一般" == pos[1] or "固有名詞" == pos[1]:
                tokens.append(token.surface)
    tokens = filter(lambda token: re.search(r'[ぁ-ん]+|[ァ-ヴー]+|[一-龠]+', token), tokens)
    return tokens 

def build_contexts_and_target(corpus, window_size:int=5)->tuple[np.ndarray,np.ndarray]:
    contexts = []
    target = []
    vocab = set()
    _window_size = window_size//2
    # 文ごとに分割
    preprocessed_corpus = corpus.replace(" ","")
    # posを見て単語ごとに分割
    tokens = list(my_analyzer(preprocessed_corpus))

    # 新しい語彙を追加
    vocab = vocab | set(tokens)

    # スライディングウィンドウ
    for i in trange(_window_size, len(tokens)-_window_size):
        # ウィンドウの真ん中をtargetにする
        target.append(tokens[i])
        # 真ん中以外の単語をcontextsへ
        tmp = tokens[i-_window_size:i]
        tmp += tokens[i+1:i+1+_window_size]
        contexts.append(tmp)

    # 辞書作成
    id2word = list(vocab)
    word2id = {word:id for id,word in enumerate(id2word)}
    vocab_size = len(word2id)


    # contextsとtargetを単語id配列へ置き換え
    contexts_id_list = [[word2id[word] for word in doc] for doc in contexts]
    target_id_list = [word2id[word] for word in target]


    contexts = lil_matrix((len(contexts_id_list), vocab_size),dtype=np.float32)
    for index, _contexts_id_list in enumerate(contexts_id_list):
        #tmp = np.eye(vocab_size)[np.array(_contexts_id_list)]
        for word_id in _contexts_id_list:
            contexts[index, word_id] +=1.

    target = np.array(target_id_list)
    return contexts.tocsr().astype(np.float32), target, word2id, id2word

WINDOW_SIZE = 11
contexts, target, word2id, id2word = build_contexts_and_target(text8, window_size=WINDOW_SIZE)
print(f"contextsのshape: {contexts.shape}")
Hide code cell output
0it [00:00, ?it/s]
2067it [00:00, 16788.10it/s]
6114it [00:00, 28602.12it/s]
9054it [00:00, 27513.13it/s]
13070it [00:00, 31596.71it/s]
16274it [00:00, 29259.19it/s]
20240it [00:00, 31985.91it/s]
23487it [00:00, 29714.19it/s]
26975it [00:00, 30285.42it/s]
30041it [00:01, 24175.57it/s]
33531it [00:01, 26601.94it/s]
36383it [00:01, 25186.02it/s]
40080it [00:01, 28102.24it/s]
43038it [00:01, 27185.48it/s]
47181it [00:01, 30772.49it/s]
50371it [00:01, 28219.07it/s]
54610it [00:01, 31350.79it/s]
57850it [00:02, 27924.15it/s]
60768it [00:02, 27947.62it/s]
64296it [00:02, 26403.68it/s]
68418it [00:02, 29675.01it/s]
71494it [00:02, 28336.15it/s]
74629it [00:02, 28300.20it/s]
77557it [00:02, 28215.65it/s]
80415it [00:02, 24705.54it/s]
83979it [00:03, 26913.42it/s]
86758it [00:03, 21413.99it/s]
89659it [00:03, 22960.89it/s]
92782it [00:03, 23040.60it/s]
96758it [00:03, 26982.49it/s]
99808it [00:03, 24583.65it/s]
103248it [00:03, 26746.48it/s]
106075it [00:03, 26002.27it/s]
108779it [00:04, 23805.48it/s]
112098it [00:04, 25576.92it/s]
114741it [00:04, 23626.72it/s]
117534it [00:04, 24421.70it/s]
120039it [00:04, 22662.13it/s]
122905it [00:04, 23829.15it/s]
125894it [00:04, 24982.43it/s]
128438it [00:04, 22667.08it/s]
131015it [00:05, 23264.20it/s]
133783it [00:05, 21041.74it/s]
137202it [00:05, 23884.01it/s]
140052it [00:05, 24930.75it/s]
142628it [00:05, 23719.98it/s]
146774it [00:05, 27978.05it/s]
149650it [00:05, 25320.49it/s]
153314it [00:05, 27742.60it/s]
156177it [00:05, 24780.47it/s]
159299it [00:06, 26038.11it/s]
161994it [00:06, 24371.58it/s]
165754it [00:06, 27730.18it/s]
168631it [00:06, 27250.74it/s]
172467it [00:06, 29755.64it/s]
175506it [00:06, 28448.40it/s]
179451it [00:06, 30959.44it/s]
182596it [00:06, 30136.05it/s]
187017it [00:06, 33470.57it/s]
190400it [00:07, 26255.65it/s]
193278it [00:07, 26309.55it/s]
196302it [00:07, 23741.03it/s]
200093it [00:07, 26879.51it/s]
203555it [00:07, 26480.24it/s]
207394it [00:07, 29410.45it/s]
210487it [00:07, 29166.32it/s]
213508it [00:08, 26895.29it/s]
217925it [00:08, 26725.94it/s]
221964it [00:08, 29842.69it/s]
225310it [00:08, 28545.33it/s]
229410it [00:08, 31307.23it/s]
232644it [00:08, 27619.69it/s]
235745it [00:08, 28328.65it/s]
239002it [00:08, 28759.53it/s]
241955it [00:09, 25132.97it/s]
245702it [00:09, 27826.15it/s]
248611it [00:09, 25966.52it/s]
252677it [00:09, 29245.89it/s]
255712it [00:09, 26169.03it/s]
258896it [00:09, 27460.17it/s]
261750it [00:09, 24834.35it/s]
265072it [00:09, 26905.70it/s]
267876it [00:10, 23138.73it/s]
271067it [00:10, 25050.50it/s]
274540it [00:10, 26974.20it/s]
277361it [00:10, 25489.65it/s]
281061it [00:10, 28046.84it/s]
283957it [00:10, 26710.19it/s]
288063it [00:10, 30090.33it/s]
291150it [00:10, 28279.04it/s]
294920it [00:10, 30672.90it/s]
298061it [00:11, 24372.09it/s]
300857it [00:11, 25179.12it/s]
303564it [00:11, 22856.82it/s]
306920it [00:11, 24987.33it/s]
309564it [00:11, 23933.32it/s]
312976it [00:11, 26180.60it/s]
315885it [00:11, 22397.85it/s]
318766it [00:12, 23857.26it/s]
322056it [00:12, 26019.05it/s]
324795it [00:12, 24036.28it/s]
328838it [00:12, 28206.75it/s]
331808it [00:12, 25551.70it/s]
335425it [00:12, 27766.25it/s]
338326it [00:12, 25687.79it/s]
342551it [00:12, 29755.42it/s]
345662it [00:13, 23158.86it/s]
348279it [00:13, 23823.99it/s]
350893it [00:13, 23223.21it/s]
355084it [00:13, 27424.62it/s]
357999it [00:13, 27282.78it/s]
361440it [00:13, 29060.77it/s]
363003it [00:13, 26577.71it/s]

  0%|                                                                                                                                                                                                                                                                                                             | 0/80264 [00:00<?, ?it/s]
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 80264/80264 [00:00<00:00, 1031989.90it/s]

contextsのshape: (80264, 17871)

クラスの作成#

Skip-gramをnn.Moduleのサブクラスとして実装します.

クラスの実装には上のskip-gramアーキテクチャ図を参考にしてください.高速化のテクニックなどは不要です.(もちろん実装できる人は実装してもOK)

class SkipGram(nn.Module):
    def __init__(self, vocab_size:int, embedding_dim:int)->None:
        super().__init__()
        ...

    def forward(self, input:torch.Tensor)->torch.Tensor:
        ...

損失関数の作成#

Skip-Gramはクラス分類の体裁をとっているので,損失関数にはCross Entropyを用います.ただしPyTorchで用意されているnn.CrossEntropyを用いることは(おそらく)できないので,自作しましょう.

Hint

条件:

  • batch_size=128, vocab_size=11342のとき,以下が損失関数に入力されると仮定して実装してください.

    • SkipGramがforwardメソッドから出力するtensor.shapeは「torch.Size([128, 11342])」,

    • 正解データとして利用するtensor.shapeは「torch.Size([128, 11342])」

  • callbackにおいて,ここで実装したcross entropyを使ってperplexityを計算します.

class BowCrossEntropy(nn.Module):
    def forward(self, input, target):
        """
        inputはSkip-gramの出力です.
        targetは予測したいcontextsです.
        """
        ...

trainerの準備と訓練#

ここまでの実装が終わったら,あとは訓練用のプログラムを書くだけです.この解説ではskorchを利用して楽をします.Skip-Gramはクラス分類の体裁を取っていると言いましたが,出力はcategoricalではなくmultinomialです.つまり 一つのデータに対して正解ラベルが複数あります .これはskorchのNeuralNetClassifierでは上手く扱えないので,NeuralNetRegressor を使っています.

Note

  • NeuralNetClassifierは主に1データ1ラベルの場合に利用します.今回の例でも使えないわけではないのですが,標準で設定された「正答率を表示するコールバック」が動作してしまうので利用を見送りました.

  • EpochScoring(lambda net,X=None,y=None: np.exp(net.history_[-1, "valid_loss"]), name="valid_ppl"), はエポックの終わりに呼び出されるコールバック関数の雛形であるEpochScoringを利用して,Perplexityを計算します.

  • targetもcontextsもnp.ndarrayのままでfitに渡します.

    • trainerが中でdatasetやdataloaderを用意してくれます.

    • contextsはscipy.sparse.lil_matrix or scipy.sparse.csr_matrixになっているので,toarrayメソッドでnp.ndarrayに戻しています.

trainer = NeuralNetRegressor(
    SkipGram(len(word2id), 50),
    optimizer=optim.Adam,
    criterion=BowCrossEntropy,
    max_epochs=20,
    batch_size=128,
    lr=0.01,
    callbacks=[
        EpochScoring(lambda net,X=None,y=None: np.exp(net.history_[-1, "valid_loss"]), name="valid_ppl"), 
        EpochScoring(lambda net,X=None,y=None: np.exp(net.history_[-1, "train_loss"]), name="train_ppl", on_train=True,)
    ],
    device="cpu", # 適宜変更
)

trainer.fit(target, contexts.toarray())
Hide code cell output
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[7], line 15
      1 trainer = NeuralNetRegressor(
      2     SkipGram(len(word2id), 50),
      3     optimizer=optim.Adam,
   (...)
     12     device="cpu", # 適宜変更
     13 )
---> 15 trainer.fit(target, contexts.toarray())

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/skorch/regressor.py:82, in NeuralNetRegressor.fit(self, X, y, **fit_params)
     71 """See ``NeuralNet.fit``.
     72 
     73 In contrast to ``NeuralNet.fit``, ``y`` is non-optional to
   (...)
     77 
     78 """
     79 # pylint: disable=useless-super-delegation
     80 # this is actually a pylint bug:
     81 # https://github.com/PyCQA/pylint/issues/1085
---> 82 return super(NeuralNetRegressor, self).fit(X, y, **fit_params)

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/skorch/net.py:1317, in NeuralNet.fit(self, X, y, **fit_params)
   1285 """Initialize and fit the module.
   1286 
   1287 If the module was already initialized, by calling fit, the
   (...)
   1314 
   1315 """
   1316 if not self.warm_start or not self.initialized_:
-> 1317     self.initialize()
   1319 self.partial_fit(X, y, **fit_params)
   1320 return self

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/skorch/net.py:905, in NeuralNet.initialize(self)
    903 self._initialize_module()
    904 self._initialize_criterion()
--> 905 self._initialize_optimizer()
    906 self._initialize_history()
    908 self._validate_params()

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/skorch/net.py:876, in NeuralNet._initialize_optimizer(self, reason)
    873         msg = self._format_reinit_msg("optimizer", triggered_directly=False)
    874     print(msg)
--> 876 self.initialize_optimizer()
    878 # register the virtual params for all optimizers
    879 for name in self._optimizers:

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/skorch/net.py:642, in NeuralNet.initialize_optimizer(self, triggered_directly)
    638 args, kwargs = self.get_params_for_optimizer(
    639     'optimizer', named_parameters)
    641 # pylint: disable=attribute-defined-outside-init
--> 642 self.optimizer_ = self.optimizer(*args, **kwargs)
    643 return self

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/torch/optim/adam.py:45, in Adam.__init__(self, params, lr, betas, eps, weight_decay, amsgrad, foreach, maximize, capturable, differentiable, fused)
     39     raise ValueError(f"Invalid weight_decay value: {weight_decay}")
     41 defaults = dict(lr=lr, betas=betas, eps=eps,
     42                 weight_decay=weight_decay, amsgrad=amsgrad,
     43                 maximize=maximize, foreach=foreach, capturable=capturable,
     44                 differentiable=differentiable, fused=fused)
---> 45 super().__init__(params, defaults)
     47 if fused:
     48     if differentiable:

File ~/workspace/prpy/.venv/lib/python3.11/site-packages/torch/optim/optimizer.py:279, in Optimizer.__init__(self, params, defaults)
    277 param_groups = list(params)
    278 if len(param_groups) == 0:
--> 279     raise ValueError("optimizer got an empty parameter list")
    280 if not isinstance(param_groups[0], dict):
    281     param_groups = [{'params': param_groups}]

ValueError: optimizer got an empty parameter list

類似単語検索#

cbowと同様に単語埋め込みベクトルを使って,類似単語の検索を行います.

def get_similar_words(query, word_embeddings, topn=5, word2id=word2id, ):
    """単語埋め込みベクトルを使って似た単語を検索する

    Args:
        query (str): 類似単語を検索したい単語
        topn (int, optional): 検索結果の表示個数. Defaults to 5.
        word2id (dict[str,int], optional): 単語→単語idの辞書. Defaults to word2id.
        word_embeddings (np.ndarray, optional): 単語埋め込み行列.必ず(語彙数x埋め込み次元数)の行列であること. Defaults to word_embeddings.
    """
    id=word2id[query]
    E = (word_embeddings.T / np.linalg.norm(word_embeddings,ord=2, axis=1)).T # {(V,L).T / (V)}.T = (V,L)
    target_vector = E[id]
    cossim = E @ target_vector # (V,L)@(L)=(V)
    sorted_index = np.argsort(cossim)[::-1][1:topn+1] # 最も似たベクトルは自分自身なので先頭を除外

    print(f">>> {query}")
    _id2word = list(word2id.keys())
    for rank, i in enumerate(sorted_index):
        print(f"{rank+1}:{_id2word[i]} \t{cossim[i]}")

word_embeddings = trainer.module_.embedding.weight.detach().cpu().numpy()

get_similar_words("ロボット", word_embeddings, )
>>> ロボット
1:ユニバーサル 	0.898608386516571
2:ポルト 	0.7893995642662048
3:ロボティックス 	0.763614296913147
4:テラ 	0.742680013179779
5:関節 	0.7259170413017273
get_similar_words("サッカー", word_embeddings, )
get_similar_words("日本", word_embeddings, )
get_similar_words("女王", word_embeddings, )
get_similar_words("機械学習", word_embeddings, )
>>> サッカー
1:リーグ 	0.734089195728302
2:専業 	0.7245967388153076
3:ヴァンフォーレ 	0.6850863695144653
4:選手 	0.6845436692237854
5:アルビレックス 	0.6741206645965576
>>> 日本
1:ほん 	0.6705817580223083
2:米国 	0.6255179047584534
3:王者 	0.6063108444213867
4:社団 	0.5765134692192078
5:蓄音機 	0.5684884786605835
>>> 女王
1:ヴィクトリアシリーズ 	0.6750556826591492
2:後塵 	0.649889349937439
3:ティアラカップ 	0.641579806804657
4:ボウラー 	0.6231715083122253
5:シェクター 	0.6060587763786316
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[11], line 4
      2 get_similar_words("日本", word_embeddings, )
      3 get_similar_words("女王", word_embeddings, )
----> 4 get_similar_words("機械学習", word_embeddings, )

Cell In[9], line 10, in get_similar_words(query, word_embeddings, topn, word2id)
      1 def get_similar_words(query, word_embeddings, topn=5, word2id=word2id, ):
      2     """単語埋め込みベクトルを使って似た単語を検索する
      3 
      4     Args:
   (...)
      8         word_embeddings (np.ndarray, optional): 単語埋め込み行列.必ず(語彙数x埋め込み次元数)の行列であること. Defaults to word_embeddings.
      9     """
---> 10     id=word2id[query]
     11     E = (word_embeddings.T / np.linalg.norm(word_embeddings,ord=2, axis=1)).T # {(V,L).T / (V)}.T = (V,L)
     12     target_vector = E[id]

KeyError: '機械学習'

今回の解説ではja.text8のサブセットを利用しているせいで,この単語埋め込みがカバーしている語彙に「機械学習」は含まれていないようです.