🦾

ゼロからつくるDeepLearning 2 格闘日誌5/1 1-2章

に公開

ゼロから作るDeep Learning 2を友達と読んでいます.
自分用のまとめかつ,補充知識として重要なことを書いています.
読者対象はゼロから作るDeep Learning 実装が終わったくらいの方,Deep Learning 2を読んでいる方です.

ゼロつく2の読者にとっては1章は既習の内容だと思うので、飛ばして2章からぜひ.
(追記:2025/05/06)commonで関数を実装したほうが便利そうなので,やっぱり1章もさらっとやったほうがいいと思います.

1章 ニューラルネットワークの復習

1.1 数学とPythonの復習

1.1.1 ベクトルと行列

ベクトル:1次元の配列
行列:2次元の配列
テンソル:行列の一般化(n次元の配列)
3行2列
行:横方向の並び
列:縦方向の並び
[[1,2],
[3,4],
[5,6]]
このようになっているとき,行は3つ,列は2つあるので
3行2列の行列と呼ぶ
基本的に行ベクトルを使用する
Numpyを使用する

>>> import numpy as np
>>> x = np.array([1,2,3])
>>> x.__class__
<class 'numpy.ndarray'>
>>> x.shape
(3,)
>>> x.ndim
1
>>> W = np.array([[1,2,3],[4,5,6]])
>>> W.shape
(2, 3)
>>> W.ndim
2
>>>

1.1.2 行列の要素ごとの演算

>>> import numpy as np
>>> W = np.array([[1,2,3],[4,5,6]])
>>> X = np.array([[0,1,2],[3,4,5]])
>>> W+X
array([[ 1,  3,  5],
       [ 7,  9, 11]])
>>> W*X
array([[ 0,  2,  6],
       [12, 20, 30]])

1.1.3 ブロードキャスト

>>> import numpy as np
>>> A = np.array([[1,2],[3,4]])
>>> A*10
array([[10, 20],
       [30, 40]])

1.1.4 ベクトルの内積と行列の積

(復習)ベクトルの内積

x \cdot y = x_1 y_1+ x_2 y_2 + ... + x_n y_n
ベクトルの内積
>>> a = np.array([1,2,3])
>>> b = np.array([4,5,6])
>>> np.dot(a,b)
32
行列の積
>>> A =np.array([[10, 20],
...              [30, 40]])
>>> B =np.array([[5, 6],
...              [7, 8]])
>>> np.dot(A,B)
array([[190, 220],
       [430, 500]])

いずれにもnp.dotが使える

1.1.5 行列の形状チェック☑

3×2 2×4

1.2 ニューラルネットワーク(NN)の推論

学習フェーズと推論フェーズ

1.2.1 推論の全体図

入力層、隠れ層、出力層:NNの3つのニューロンの分類
重み:前層の入力と乗算
バイアス:加算される定数
全結合層:隣接するすべてのニューロン間に結びつきがある
xを入力,W1を重み,b1をバイアスとして中間層hを出力してみる.

>>> W1 = np.random.randn(2,4)
>>> b1 = np.random.randn(4)
>>> x = np.random.randn(10,2)
>>> h = np.dot(x,W1)+b1
>>> print(h)
[[ 0.79282566  1.53166272  0.29712265 -1.14731456]
 [-0.46965528 -0.27906834 -0.04877097  0.03823241]
 [ 0.65199268  0.36956256 -0.63019708  0.94641914]
 [-1.44234408 -2.94880093 -1.49515136  3.55571147]
 [ 1.39493232  2.72507638  0.76740143 -2.38657347]
 [-2.86676099 -4.65434314 -1.57305002  4.20392698]
 [ 0.98925817 -0.10853606 -1.42811797  2.59469878]
 [-2.47951038 -3.7469635  -1.14115592  3.12122749]
 [ 0.54950034  2.61490486  1.55621975 -3.84484521]
 [ 2.8612204   3.24965829 -0.29198621 -0.53873987]]

b1はブロードキャストが自動で適用される。b1の形をhの形に合わせて(10,4)としてしまうと、x10個のデータからの平均的な影響をパラメータ4つに与えるという話ではなくなってしまう(パラメータ40個になってしまうよね?)だから(4,)でよい。

1.2.1,5 シグモイド関数を追加して非線形化

import numpy as np
def sigmoid(x):
    return 1 / (1 + np.exp(-x))

x = np.random.randn(10,2)
W1 = np.random.randn(2,4)
b1 = np.random.randn(4)
W2 = np.random.randn(4,3)
b2 = np.random.randn(3)

h = np.dot(x,W1) + b1
a = sigmoid(h)
s = np.dot(a,W2) +b2
print(s)

1.2.2 レイヤとしてのクラス化と順伝播の実装

全結合層によつ変換をAffine Layer,Sigmoid関数による変換をSigmoid Layerとして実装
順伝播: forward()
逆伝播: backward()

本書におけるレイヤ実装ルール

  • すべてのレイヤは、メソッドとしてforward()とbackward()をもつ
  • すべてのレイヤは、インスタンス変数としてparamsとgradsをもつ
import numpy as np

class Sigmoid:
    def __init__(self):
        self.params = [] #パラメータなし
    def forward(self,x):
        return 1 / (1 +np.exp(-x))

class Affine:
    def __init__(self, W, b):
        self.params = [W, b]
    def forward(self, x):
        W, b = self.params
        out = np.dot(x,W) + b
        return  out

X → Affine → Sigmoid → Affine → b

2章 自然言語と単語の分散表現

(本質的問題) コンピュータに人の言葉を理解させる
本章は古典的手法(DL以前)

2.1 自然言語処理とは

NLP : 自然言語処理(Natural Langage Processing)

2.1.1 単語の意味

文字:言葉の最小単位
単語:意味の最小単位
(本章のテーマ):単語の最小単位
本章では

  • シソーラスによる手法(2.2)
  • カウントベースの手法(2.3)
  • 推論ベースの手法(3章)

2.2 シソーラス

シソーラス:単語の意味を表すための辞書を人の手で作成.

car = auto, automobile, machine, motorcar, 車

上位下位概念が設定されることも

2.2.1 WordNet

Wordnet:もっとも有名なシソーラス(1985,プリンストン大学)
類義語の取得,単語ネットワークの利用
時間があれば付録Bを参照

2.2.2 シソーラスの問題点

WordNetはコンピュータに(間接的に)単語の意味を理解させることができるか?
WordNet→抽象化すると→人の手によるラベル付け には大きな欠点が.

  • 時代の変化に対応困難(定義が変わるごとに更新...?)
  • 作業コストの高さ
  • 単語の細かいニュアンスが表現できない(私≠僕)

これらの問題から,シソーラスは死んだ

2.3 カウントベースの手法

corpus:大量のテキストデータ
corpusから効率よく、自動的に知識のエッセンスを抽出したい.

2.3.1 Pythonによるコーパスの下準備

textbase.py
from common.utils import preprocess
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)
#[0 1 2 3 4 1 5 6]
utils.py
import numpy as np
def preprocess(text):
    text = text.lower()
    text = text.replace('.',' .')
    words = text.split(' ')
    word_to_id = {}
    id_to_word = {}
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
    corpus = np.array([word_to_id[w] for w in words])
    return corpus,word_to_id,id_to_word

2.3.2 単語の分散表現

分散表現:自然言語処理分野において,単語をベクトルで表現すること
密なベクトル:各要素の多くが0でない実数値で表される
分散表現においては単語の分散表現は密なベクトルであることを期待される.

2.3.2 分布仮説

分布仮説:「単語の意味は,周囲の単語によって形成される」というアイデア
単語そのものに意味はなく,単語の意味は文脈に沿って決定される
ex.1) I drink beer./ I drink wine ←drinkの後には飲み物が現れやすい.
ex.2) I guzzle beer. We guzzle wine ←guzzleの意味はdrinkに近い
コンテキスト:その単語の周囲に存在する単語
ウィンドウサイズ:コンテキストのサイズ.

2.3.3 共起行列

共起行列:すべての単語に対して共起する(ここでは隣り合って出現する)単語をテーブルにまとめたもの(図は該当書参照.)
以下,手書きで作成した共起行列.

textbase.py
#you say goodbye and i (say) hello .
C = np.array([
    [0,1,0,0,0,0,0],#you
    [1,0,1,0,1,1,0],#say
    [0,1,0,1,0,0,0],#goodbye
    [0,0,1,0,1,0,0],#and
    [0,1,0,1,0,0,0],#i
    [0,1,0,0,0,0,1],#hello
    [0,0,0,0,0,1,0],#.
])
print(C[0])#youを表現するベクトル
#[0 1 0 0 0 0 0]

コーパスから共起行列を求める関数を作成

common/utils.py
def create_co_matrix(corpus,vocab_size,window_size=1):
    co_matrix = np.zeros((vocab_size,vocab_size),dtype=np.int32)
    corpus_size = len(corpus)
    for idx,word_id in enumerate(corpus):
        for i in range(1, window_size):
            left_idx = idx - i
            right_idx = idx + i
        
        if left_idx >= 0:
            left_word_id = corpus[left_idx]
            co_matrix[word_id,left_word_id]
    
        if right_idx < corpus_size:
            right_word_id = corpus[right_idx]
            co_matrix[word_id,right_word_id]
            
    return co_matrix

2.3.5 ベクトル間の類似度

共起行列で単語をベクトルで表せる.
コサイン類似度:ベクトルの近さを計測できる.

similarity(\bm{x},\bm{y}) = \frac{\bm{x} \cdot \bm{y}}{\| \bm{x} \|\| \bm{y} \|}=\frac{x_1 y+1+... +x_n y_n}{\sqrt{x_1^2+...+x_n^2} \sqrt{y_1^2+...+y_n^2}}

epsはゼロ除算を防ぐための小さな値.x,yそれぞれ正規化してから内積を計算する

common/utils.py
def cos_similarity(x, y, eps=1e-8):
    nx = x/np.sqrt(np.sum(x**2) + eps)
    ny = y/np.sqrt(np.sum(y**2) + eps)
    return np.dot(nx,ny)

np.dotの挙動:
numpyの標準データ型であるnp.array()に対して内積を計算できる
https://kakakakakku.hatenablog.com/entry/2021/05/31/104411

2.3.6 類似単語ランキング表示(most_similarの実装)

クエリ:問い合わせたい単語
クエリに対して類似した単語を上位から順に表示する.

utils.py
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
    '''
    Find most similar words
    query: word to find similar words
    word_to_id: dictionary of word to id
    id_to_word: dictionary of id to word
    word_matrix: word vector matrix
    top: number of similar words to find
    '''

    if query not in word_to_id:
        print('%s is not found' % query)
        return
    print('\n[query] '+query)
    query_id = word_to_id[query]
    query_vec = word_matrix[query_id]

    #cos similarity
    vocab_size = len(id_to_word)
    similarity = np.zeros(vocab_size)
    for i in range(vocab_size):
        similarity[i] = cos_similarity(word_matrix[i], query_vec)

    # result of the calc of cos similarity, return the words from top 
    count = 0
    for i in (-1*similarity).argsort():
        if id_to_word[i]==query:
            continue #queryについてはスキップする
        print(' %s: %s' % (id_to_word[i], similarity[i]))

        count += 1
        if count >= top:
            break
    return
[query] you
 goodbye: 0.7071067758832467
 hello: 0.7071067758832467
 i: 0.7071067758832467
 and: 0.0
 say: 0.0

以上より,コーパスを基にクエリに対する類似単語を表示するシステムが完成した.(コーパスが小さいのでうまくいかないが,より大きいコーパスで実験する)
実行を行う部分をipynb,utilsはpyで管理していたら,どうやらnotebookはutilsなどのimportする関数をキャッシュしてしまうようで,utilsの変更が反映されないという状況に出くわした.jupyterを再起動して治った.

2.4 カウントベースの手法の改善

前章の共起行列の改善に取り組む.

2.4.1 相互情報量

高頻度出現単語に対する問題点:"car"と"drive"については明確な関係があるが,"car"と"the"はその出現頻度の多さゆえに後者のほうが関係性が高いと評価されうる.
→相互情報量の活用
相互情報量:Pointwise Mutual Infomation(PMI)x,yは確率変数

PMI(x,y) = \log_2{\frac{P(x,y)}{P(x)P(y)}}

共起行列をCとして,単語x,yの共起する回数をC(x,y),単語xが共起する回数をC(x),単語yが共起する回数をC(y)とすると

PMI(x,y) = \log_2{\frac{P(x,y)}{P(x)P(y)}}=\log_2{\frac{\frac{C(x,y)}{N}}{\frac{C(x)}{N}\frac{C(y)}{N}}}=\log_2{\frac{C(x,y)\cdot N}{C(x)C(y)}}

PMIの問題点:2つの単語で共起する回数が0の場合,\log_2(0)=-\infty
→正の相互情報量(PPMI:Positive PMI)

\operatorname{PPMI}(x, y) = \max(0, \operatorname{PMI}(x, y))

以上の内容をutils.pyに実装.

utils.py
def ppmi(C,verbose=False,eps=1e-8):
    M = np.zeros_like(C,dtype=np.float32) #Mの初期化
    N = np.sum(C) #コーパスに含まれる全単語数
    S = np.sum(C,axis=0) #axis0について足し合わせ
    total = C.shape[0] * C.shape[1] #Cの要素数.0は行,1は列.
    cnt = 0

    for i in range(C.shape[0]):
        for j in range(C.shape[1]):
            pmi = np.log2(C[i,j] * N / (S[j]*S[i]+eps)) #pmiの定義.全ての(i,j)について計算.S(x)については行/列は関係ない
            M[i,j] = max(0,pmi) #PPMIとしてMに代入
            if verbose: #verbose==1なら,途中状況確認.
                cnt += 1
            if cnt % (total//100+1) == 0:
                print('%.1f%% done' % (100*cnt/total))
    
    return M

以下に共起行列をPPMI行列に変換した結果を示す.

[[0 1 0 0 0 0 0]
 [1 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]
--------------------------------------------------
PPMI
[[0.    1.807 0.    0.    0.    0.    0.   ]
 [1.807 0.    0.807 0.    0.807 0.807 0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.    1.807 0.    1.807 0.    0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.807 0.    0.    0.    0.    2.807]
 [0.    0.    0.    0.    0.    2.807 0.   ]]

PPMI行列での問題:コーパスの語彙数が増えるにつれ,各単語のベクトルの次元数も増えていく.
→次元削減

2.4.2 次元削減

次元削減:dimentionality reduction

特異値分解:SVD(Singular Value Dicomposition)次元削減を行う方法の一つ.cf)PCA(主成分分析)

\bm{X} = \bm{U} \bm{S} \bm{V}^{\top}

SVDとPCAの関係については,以下の記事を参照.

https://qiita.com/horiem/items/71380db4b659fb9307b4

SVDを実行し,2次元のベクトルで表現した結果を以下に示す.

プロット図を見ると,iとyouが近くにある(iとgoodbyeが重なっている.)が,コーパスが小さい関係で微妙.
SVDの計算はO(N^3)のため,とても微妙.Truncated SVDを使用する(特異値の小さなものは切り捨てる.)

2.4.4 PTBデータセット

Penn Treebank(PTB):提案手法の品質を評価するためのベンチマークとして使われる大規模コーパス
ptbdatasetを使うためのライブラリがあらかじめ用意されているので,それを作業ディレクトリに移動して使うことにした.

2_4_4ptbdataset.py
import sys
sys.path.append('..')
from dataset import ptb
corpus, word_to_id, id_to_word, = ptb.load_data('train')
print('corpus size:',len(corpus))
print('corpus[:30]: ',corpus[:30])
print('id_to_word[0]: ',id_to_word[0])
print('id_to_word[1]: ',id_to_word[1])
print('id_to_word[2]: ',id_to_word[2])
print()
print('word_to_id["car"]: ',word_to_id['car'])
print('word_to_id["world"]: ',word_to_id['world'])

2.4.5 PTBデータセットでの評価

自分のパソコンで試した結果...

なんと、私のパソコンではメモリ不足で持たなかった.
colabに場所を移し替えて再実験.

2_4_5ptbdataset.ipynb
import sys
sys.path.append('/content/drive/MyDrive')
import numpy as np
from dataset import ptb

pathの設定をこのように書き換えて,driveの最上位ディレクトリに入れておけば動くはずです.
以下のようにベクトルが得られます

[query] you
 i: 0.6835166811943054
 we: 0.6592429876327515
 anybody: 0.5824012160301208
 else: 0.532889187335968
 do: 0.5244526267051697

[query] year
 month: 0.7218987345695496
 quarter: 0.6709836721420288
 earlier: 0.6607603430747986
 last: 0.6445731520652771
 week: 0.6106088161468506

[query] car
 auto: 0.6572113037109375
 luxury: 0.6497750282287598
 vehicle: 0.579791784286499
 cars: 0.5315102338790894
 truck: 0.5082963705062866

[query] toyota
 nissan: 0.7144942283630371
 motor: 0.7038341164588928
 honda: 0.6742873787879944
 motors: 0.6348630785942078
 lexus: 0.5958027243614197

(追記)なぜか通常のSVDが先に入ってしまっていたようで,その行を削除したら普通に私のパソコンでも動きました.googledriveのマウント機能は便利なので残しておきます.

まとめ

今回はカウントベースの手法を取り扱った2章について学習しました.(時間があれば1章も追記します)
分布仮説に基づいたカウントベースの手法により,単語の分散表現を得ることに成功しました.コンテクストをカウントし,それをPPMI行列に変換(そもそもの生起回数が多いものを小さい値に),SVDによる次元削減を行うことでより密なベクトルを得たことになります.

Discussion