AIって結局何なのかよく分からないので、とりあえず100日間勉強してみた Day79
経緯についてはこちらをご参照ください。
■本日の進捗
- CBoWの前処理を理解
■はじめに
今回も「ゼロから作るDeep Learning② 自然言語処理編(オライリー・ジャパン)」から学んでいきます。
今回は、カウントベース手法に代わる高効率で高性能な推論ベース手法を学んでいきます。
■カウントベース手法の問題点
カウントベース手法で処理していたデータはコーパスサイズが92万語、ユニークな語彙数が1万語の簡易版PTBデータセットでした。
これまで行ってきたように、コーパス全体を用いてカウント(統計的な処理)を行い分散表現を得る必要があり、最終的には次元削減するものの、やはり1万行列の行列計算を行う必要があります。
このような中規模なデータセットにおいてもそれなりに重かったですが、語彙数が100万語を超えるような大規模なデータセットに同様の処理を行うには相応の計算環境が必要になります。
■推論ベース手法
これらのカウントベース手法の問題点を克服する手法として、推論ベース手法があります。カウントベース手法ではターゲットの隣(あるいは周囲)の単語をカウントしていき、出現確率に落とし込むことで分散表現を得るものでしたが、推論ベース手法はその名前の通り、周囲の単語から特定の場所にどの単語が入るのかを推論していく手法になります。
この推論にはニューラルネットワークが使えます。ニューラルネットワークはコーパスをミニバッチにして学習していくことができるので、常に総語彙数を相手にしていたカウントベース手法より効率的に処理をすることができます。
■CBoW
CBoW(Continuous Bag of Words)とは、単語埋め込みを生成するプログラムであるword2vecのアルゴリズムの一つで、周囲の単語(コンテキスト)からターゲットである単語を予測して単語の分散表現を生成するアルゴリズムです。
今回はその前処理として、行列積を行う層、one-hot処理関数、コーパスからコンテキストとターゲットを切り分ける関数を用意します。
■MatMul層
まずは行列積を行う層であるMatMul(Matrix Multiply)層を用意しておきます。これは画像認識でも使用していた全結合層(Affine層)のバイアスを除いたものと考えることができます。
この層を下記のようにクラスとして定義して変数を初期化しておきます。
class MatMul: def __init__(self, W): self.params = [W] self.grads = [np.zeros_like(W)] self.x = None
●順伝播
全結合層の順伝播は下記のように基本的な線形変換として記述できました。
$$ y = \displaystyle \sum_{i}^n w_i x_i+ b $$
ここではバイアスを用いないので、バイアス項をなくせば、
$$ y = \displaystyle \sum_{i}^n w_i x_i $$
これを重み行列W、入力ベクトルX、バイアスベクトルBを用いれば下記のように記述できます。
$$ \boldsymbol{ Y } = \boldsymbol{ W } \cdot \boldsymbol{ X } $$
これを実装するのはNumPyのdotを用いればとても簡単です。
def forward(self, x): W, = self.params out = np.dot(x, W) self.x = x return out
●逆伝播
順伝播の出力を入力ベクトルXで偏微分すれば、
$$ \frac{\partial \boldsymbol{Y} }{\partial \boldsymbol{X} } = \frac{ \partial }{ \partial \boldsymbol{X} } ( \boldsymbol{W} \cdot \boldsymbol{X} ) = \boldsymbol{W}^{\mathrm{T}} $$
損失の入力勾配も連鎖律を用いれば、
$$ \frac{\partial L }{\partial \boldsymbol{X} } = \frac{\partial L}{\partial \boldsymbol{Y}} \cdot \frac{\partial \boldsymbol{Y} }{\partial \boldsymbol{X} } $$
以上より、
$$ \frac{\partial L }{\partial \boldsymbol{X} } = \frac{\partial L}{\partial \boldsymbol{Y}} \cdot \boldsymbol{W}^{\mathrm{T}} $$
これを実装すれば下記のようになります。
def backward(self, dout): W, = self.params dx = np.dot(dout, W.T) dW = np.dot(self.x.T, dout) self.grads[0][...] = dW return dx
■one-hot encoding
ニューラルネットワーク(というより全結合層は、という方が正しい)では入力データの次元数を固定する必要があるので、コーパスをone-hot表現に変換するための関数を用意します。
まず最初に引数として入力されているcorpusの単語数を変数Nに格納しておきます。
def convert_one_hot(corpus, vocab_size): N = corpus.shape[0]
次に、返り値であるone_hot用の行列を初期化してから、corpusの中身をone_hot行列に格納していきます。
corpusがIDリストだけなどの1次元配列だった場合は下記の通り2次元配列を作成してから、Python標準のenumerate関数でcorpusを分離してから格納していきます。
if corpus.ndim == 1: one_hot = np.zeros((N, vocab_size), dtype=np.int32) for idx, word_id in enumerate(corpus): one_hot[idx, word_id] = 1
corpusが複数のデータからなる2次元配列だった場合には、3次元配列を初期化してから同様に処理をしていきます。
elif corpus.ndim == 2: C = corpus.shape[1] one_hot = np.zeros((N, C, vocab_size), dtype=np.int32) for idx_0, word_id in enumerate(corpus): for idx_1, word_id in enumerate(word_id): one_hot[idx_0, idx_1, word_id] = 1
最後にone_hot変数を返して終了です。
■contextsとtargetの生成
CBoWで扱う形にするために、コーパスからターゲットとその周囲をコンテキストとして抽出する関数を用意しておきます。
まずはターゲットとなる単語を抽出しますが、指定したwindow_sizeの分だけ両端の単語を除外することで、コンテキストがない状況を避けるようにします。ここでコンテキスト用の配列も初期化しておきます。
def create_contexts_target(corpus, window_size=1): target = corpus[window_size:-window_size] contexts = []
外側のループでは両端を除外してターゲットである単語を選択し、csリスト(コンテキストの単語を一時的に保持しておくための変数)を都度初期化しておきます。
内側のループでは、ターゲットが自分自身であればスキップして、そうでない場合にcsリストに追記していきます。
for idx in range(window_size, len(corpus)-window_size): cs = [] for t in range(-window_size, window_size + 1): if t == 0: continue cs.append(corpus[idx + t]) contexts.append(cs)
最後にコンテキストとターゲットを返り値として返せば終了です。
■おわりに
最後にこれまでの関数をまとめておきます。
これらの関数を用いてCBoWを実装していくことになります。
import numpy as np class MatMul: def __init__(self, W): self.params = [W] self.grads = [np.zeros_like(W)] self.x = None def forward(self, x): W, = self.params out = np.dot(x, W) self.x = x return out def backward(self, dout): W, = self.params dx = np.dot(dout, W.T) dW = np.dot(self.x.T, dout) self.grads[0][...] = dW return dx def convert_one_hot(corpus, vocab_size): N = corpus.shape[0] if corpus.ndim == 1: one_hot = np.zeros((N, vocab_size), dtype=np.int32) for idx, word_id in enumerate(corpus): one_hot[idx, word_id] = 1 elif corpus.ndim == 2: C = corpus.shape[1] one_hot = np.zeros((N, C, vocab_size), dtype=np.int32) for idx_0, word_id in enumerate(corpus): for idx_1, word_id in enumerate(word_id): one_hot[idx_0, idx_1, word_id] = 1 return one_hot def create_contexts_target(corpus, window_size=1): target = corpus[window_size:-window_size] contexts = [] for idx in range(window_size, len(corpus)-window_size): cs = [] for t in range(-window_size, window_size + 1): if t == 0: continue cs.append(corpus[idx + t]) contexts.append(cs) return np.array(contexts), np.array(target)
■参考文献
- Andreas C. Muller, Sarah Guido. Pythonではじめる機械学習. 中田 秀基 訳. オライリー・ジャパン. 2017. 392p.
- 斎藤 康毅. ゼロから作るDeep Learning Pythonで学ぶディープラーニングの理論と実装. オライリー・ジャパン. 2016. 320p.
- 斎藤 康毅. ゼロから作るDeep Learning② 自然言語処理編. オライリー・ジャパン. 2018. 432p.
- ChatGPT. 4o mini. OpenAI. 2024. https://chatgpt.com/
- API Reference. scikit-learn.org. https://scikit-learn.org/stable/api/index.html
- PyTorch documentation. pytorch.org. https://pytorch.org/docs/stable/index.html
- Keiron O’Shea, Ryan Nash. An Introduction to Convolutional Neural Networks. https://ar5iv.labs.arxiv.org/html/1511.08458
- API Reference. scipy.org. 2024. https://docs.scipy.org/doc/scipy/reference/index.html