目錄
- 統計 Bigram 語言模型
- 質量評價方法
- 神經網絡語言模型
【系列筆記】
【Andrej Karpathy 神經網絡從Zero到Hero】–1. 自動微分autograd實踐要點
本文主要參考 大神Andrej Karpathy 大模型講座 | 構建makemore 系列之一:講解語言建模的明確入門,演示
- 如何利用統計數值構建一個簡單的 Bigram 語言模型
- 如何用一個神經網絡來復現前面 Bigram 語言模型的結果,以此來展示神經網絡相對于傳統 n-gram 模型的拓展性。
統計 Bigram 語言模型
首先給定一批數據,每個數據是一個英文名字,例如:
['emma','olivia','ava','isabella','sophia','charlotte','mia','amelia','harper','evelyn']
Bigram語言模型的做法很簡單,首先將數據中的英文名字都做成一個個bigram的數據

其中每個格子中是對應的二元組,eg: “rh” ,在所有數據中出現的次數。那么一個自然的想法是對于給定的字母,取其對應的行,將次數歸一化轉成概率值,然后根據概率分布抽取下一個可能的字母:
g = torch.Generator().manual_seed(2147483647)
P = N.float() # N 即為上述 counts 矩陣
P = P / P.sum(1, keepdims=True) # P是每行歸一化后的概率值for i in range(5):out = []ix = 0 ## start符和end符都用 id=0 表示,這里是startwhile True:p = P[ix] # 當前字符為 ix 時,預測下一個字符的概率分布,實質是一個多項分布(即可能抽到的值有多個,eg: 擲色子是六項分布)ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()out.append(itos[ix])if ix == 0: ## 當運行到end符,停止生成breakprint(''.join(out))
輸出類似于:
mor.
axx.
minaymoryles.
kondlaisah.
anchshizarie.
質量評價方法
我們還需要方法來評估語言模型的質量,一個直觀的想法是:
P ( s 1 s 2 . . . s n ) = P ( s 1 ) P ( s 2 ∣ s 1 ) ? P ( s n ∣ s n ? 1 ) P(s_1s_2...s_n) = P(s_1)P(s_2|s_1)\cdots P(s_n|s_{n-1}) P(s1?s2?...sn?)=P(s1?)P(s2?∣s1?)?P(sn?∣sn?1?)
但上述計算方式有一個問題,概率值都是小于1的,當序列的長度比較長時,上述數值會趨于0,計算時容易下溢。因此實踐中往往使用 l o g ( P ) log(P) log(P)來代替,為了可以對比不同長度的序列的預測效果,再進一步使用 l o g ( P ) / n log(P)/n log(P)/n 表示一個序列平均的質量。
上述統計 Bigram 模型在訓練數據上的平均質量為:
log_likelihood = 0.0
n = 0for w in words: # 所有word里的二元組概率疊加chs = ['.'] + list(w) + ['.']for ch1, ch2 in zip(chs, chs[1:]):ix1 = stoi[ch1]ix2 = stoi[ch2]prob = P[ix1, ix2]logprob = torch.log(prob)log_likelihood += logprobn += 1 # 所有word里的二元組數量之和nll = -log_likelihood
print(f'{nll/n}') ## 值為 2.4764,表示前面做的bigram模型,對現有訓練數據的置信度## 這個值越低表示當前模型越認可訓練數據的質量,而由于訓練數據是我們認為“好”的數據,因此反過來就說明這個模型好
但這里有一個問題是,例如:
log_likelihood = 0.0
n = 0#for w in words:
for w in ["andrejz"]:chs = ['.'] + list(w) + ['.']for ch1, ch2 in zip(chs, chs[1:]):ix1 = stoi[ch1]ix2 = stoi[ch2]prob = P[ix1, ix2]logprob = torch.log(prob)log_likelihood += logprobn += 1print(f'{ch1}{ch2}: {prob:.4f} {logprob:.4f}')print(f'{log_likelihood=}')
nll = -log_likelihood
print(f'{nll=}')
print(f'{nll/n}')
輸出是
.a: 0.1377 -1.9829
an: 0.1605 -1.8296
nd: 0.0384 -3.2594
dr: 0.0771 -2.5620
re: 0.1336 -2.0127
ej: 0.0027 -5.9171
jz: 0.0000 -inf
z.: 0.0667 -2.7072
log_likelihood=tensor(-inf)
nll=tensor(inf)
inf
可以發現由于,jz 在計數矩陣 N 中為0,即數據中沒有出現過,導致 log(loss) 變成了負無窮,這里為了避免這樣的情況,需要做 平滑處理,即 P = N.float()
改成 P = (N+1).float()
,這樣上述代碼輸出變成:
.a: 0.1376 -1.9835
an: 0.1604 -1.8302
nd: 0.0384 -3.2594
dr: 0.0770 -2.5646
re: 0.1334 -2.0143
ej: 0.0027 -5.9004
jz: 0.0003 -7.9817
z.: 0.0664 -2.7122
log_likelihood=tensor(-28.2463)
nll=tensor(28.2463)
3.5307815074920654
避免了出現 inf
這種數據溢出問題。
神經網絡語言模型
接下來嘗試用神經網絡的方式構建上述bigram語言模型:
# 構建訓練數據
xs, ys = [], [] # 分別是前一個字符和要預測的下一個字符的id
for w in words[:5]:chs = ['.'] + list(w) + ['.']for ch1, ch2 in zip(chs, chs[1:]):ix1 = stoi[ch1]ix2 = stoi[ch2]print(ch1, ch2)xs.append(ix1)ys.append(ix2) xs = torch.tensor(xs)
ys = torch.tensor(ys)
# 輸出示例:. e
# e m
# m m
# m a
# a .
# xs: tensor([ 0, 5, 13, 13, 1])
# ys: tensor([ 5, 13, 13, 1, 0])# 隨機初始化一個 27*27 的參數矩陣
g = torch.Generator().manual_seed(2147483647)
W = torch.randn((27, 27), generator=g, requires_grad=True) # 基于正態分布隨機初始化
# 前向傳播
import torch.nn.functional as F
xenc = F.one_hot(xs, num_classes=27).float() # 將輸入數據xs做成one-hot embedding
logits = xenc @ W # 用于模擬統計模型中的統計數值矩陣,由于 W 是基于正態分布采樣,logits 并非直接是計數值,可以認為是 log(counts)
## tensor([[-0.5288, -0.5967, -0.7431, ..., 0.5990, -1.5881, 1.1731],
## [-0.3065, -0.1569, -0.8672, ..., 0.0821, 0.0672, -0.3943],
## [ 0.4942, 1.5439, -0.2300, ..., -2.0636, -0.8923, -1.6962],
## ...,
## [-0.1936, -0.2342, 0.5450, ..., -0.0578, 0.7762, 1.9665],
## [-0.4965, -1.5579, 2.6435, ..., 0.9274, 0.3591, -0.3198],
## [ 1.5803, -1.1465, -1.2724, ..., 0.8207, 0.0131, 0.4530]])
counts = logits.exp() # 將 log(counts) 還原成可以看作是 counts 的矩陣
## tensor([[ 0.5893, 0.5507, 0.4756, ..., 1.8203, 0.2043, 3.2321],
## [ 0.7360, 0.8548, 0.4201, ..., 1.0856, 1.0695, 0.6741],
## [ 1.6391, 4.6828, 0.7945, ..., 0.1270, 0.4097, 0.1834],
## ...,
## [ 0.8240, 0.7912, 1.7245, ..., 0.9438, 2.1732, 7.1459],
## [ 0.6086, 0.2106, 14.0621, ..., 2.5279, 1.4320, 0.7263],
## [ 4.8566, 0.3177, 0.2802, ..., 2.2722, 1.0132, 1.5730]])
probs = counts / counts.sum(1, keepdims=True) # 用于模擬統計模型中的概率矩陣,這其實即是 softmax 的實現
loss = -probs[torch.arange(5), ys].log().mean() # loss = log(P)/n, 這其實即是 cross-entropy 的實現
接下來可以通過loss.backward()
來更新參數 W:
for k in range(100):# forward passxenc = F.one_hot(xs, num_classes=27).float() logits = xenc @ W # predict log-countscounts = logits.exp()probs = counts / counts.sum(1, keepdims=True) loss = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean() ## 這里加上了L2正則,防止過擬合print(loss.item())# backward passW.grad = None # 每次反向傳播前置為Noneloss.backward()# updateW.data += -50 * W.grad
注意這里 logits = xenc @ W
由于 xenc
是 one-hot 向量,因此這里 logits
相當于是抽出了 W 中的某一行,而結合 bigram 模型中,loss 實際上是在計算實際的 log(P[x_i, y_i]),那么可以認為這里 W 其實是在擬合 bigram 中的計數矩陣 N(不過實際是 logW 在擬合 N)。
另外上述神經網絡的 loss 最終也是達到差不多 2.47 的最低 loss。這是合理的,因為從上面的分析可知,這個神經網絡是完全在擬合 bigram 計數矩陣的,沒有使用更復雜的特征提取方法,因此效果最終也會差不多。
這里 loss 中還加了一個 L2 正則,主要目的是壓縮 W,使得它向全 0 靠近,這里的效果非常類似于 bigram 中的平滑手段,想象給一個極大的平滑:P = (N+10000).float()`,那么 P 會趨于一個均勻分布,而 W 全為 0 會導致 counts = logits.exp() 全為 1,即也在擬合一個均勻分布。這里前面的參數 0.01 即是用來調整平滑強度的,如果這個給的太大,那么平滑太大了,就會學成一個均勻分布(當然實際不會希望這樣,所以不會給很大)