您现在的位置是:首页 >技术杂谈 >神经网络:Zero2Hero 1网站首页技术杂谈

神经网络:Zero2Hero 1

今晚打佬虎 2023-05-24 08:00:04
简介神经网络:Zero2Hero 1

Zero → o Hero : 1

 实现了一个字符级中文语言模型,数据采用的是开源中文姓名数据集中的一部分,主要内容如下:

  1. 字符的预处理
    • 统计频次
    • 计算字符对频次矩阵
  2. 实现一个简单的先验概率模型
    • 从训练数据中计算字符的先验概率
    • 根据先验概率通过采样生成字符串
    • 根据先验概率计算字符串的平均负对数似然度
  3. 优化语言模型,简单神经网络模型
    • 模拟一个简单语言模型的预测过程
    • 计算模型预测结果的负对数似然度
    • 通过梯度优化模型的参数,最小化模型的负对数似然度
import torch
import matplotlib.pyplot as plt 
import torch.nn.functional as F
from matplotlib.font_manager import FontProperties
font = FontProperties(fname='../chinese_pop.ttf', size=10)

数据集

数据是一个中文名数据集

  • 名字最小长度为 2:
  • 名字最大长度为 3:
words = open('../Chinese_Names_Corpus.txt', 'r').read().splitlines()
# 数据包含100多万个姓名,过滤出一个姓氏用来测试
names = [name for name in words if name[0] == '王' and len(name) == 3]
len(names)
52127
names[:10]
['王阿宝', '王阿炳', '王阿成', '王阿达', '王阿大', '王阿迪', '王阿多', '王阿芳', '王阿凤', '王阿福']

字符对统计:

把数据拆分为简单的上下文对,并统计在数据集中出现的频次:

b = {}
for w in names:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        bigram = (ch1, ch2)
        b[bigram] = b.get(bigram, 0) + 1
# top 100
sorted(b.items(), key = lambda kv: -kv[1])[:10]
[(('.', '王'), 52127),
 (('王', '文'), 599),
 (('华', '.'), 555),
 (('王', '晓'), 538),
 (('王', '子'), 500),
 (('平', '.'), 481),
 (('明', '.'), 472),
 (('文', '.'), 450),
 (('林', '.'), 432),
 (('王', '建'), 431)]

从统计结果可以看到,以(‘平’, ‘明’, ‘文’,‘林’)结尾的名字最多,因为数据只包含王姓,所以('.', '王')频次最高。

构建词汇表到索引,索引到词汇表的映射,词汇表大小为:1561(加上开始和结束填充字符):

chars = sorted(list(set(''.join(names))))
char2i = {s:i+1 for i,s in enumerate(chars)}
char2i['.'] = 0               # 填充字符
i2char = {i:s for s,i in char2i.items()}
len(chars)
1650

计算字符频次矩阵:

N = torch.zeros((1651, 1651), dtype=torch.int32)
for w in names:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        N[ix1, ix2] += 1

部分字符组合和可视化:

  • 行和列是全部的字符共1650+1(‘.’)
  • 每行统计数据中,所有两两字符组合的频次
plt.figure(figsize=(20, 20))
plt.imshow(N[:27,:54], cmap='Blues')
for i in range(27):
    for j in range(54):
        chstr = i2char[i] + i2char[j]
        plt.text(j, i, chstr, ha="center", va="bottom", color='gray', fontproperties=font)
        plt.text(j, i, N[i, j].item(), ha="center", va="top", color='gray')
plt.axis('off')

在这里插入图片描述

构建简单的语言模型

根据词频矩阵对频次进行行方向的归一化,得到先验概率矩阵,根据概率矩阵预测下一个词,过程如下:

  1. 根据矩阵的第一行,选择概率最高的词组(.x),(.王)被选中的概率最高。
  2. 然后根据对应的索引,查询矩阵的对应行,选择(王x)概率最高的组合。
  3. 如此这般,…直到遇到(x.)或者字符达到最大长度结束。

第一行是.和所有字符的组合的概率分布,由于数据是过滤了全部的王姓,所以的概率是最高的,这个结果是正常的。

# 计算每个字符组合的先验概率
p = N[0].float()
p = p / p.sum()
# 根据先验概率采样
g = torch.Generator().manual_seed(2147483647)
ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
i2char[ix]
'王'

下面计算先验概率矩阵,根据先验概率生成10个名字:

# 字符对儿的先验概率矩阵
P = (N+1).float()
P /= P.sum(1, keepdims=True)
P.shape
torch.Size([1651, 1651])
g = torch.Generator().manual_seed(2147483647)

for i in range(10):
    out = []
    ix = 0
    while True:
        p = P[ix]
        ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
        out.append(i2char[ix])
        if ix == 0 or len(out) == 3:
            break
    print(''.join(out))
王静修
王映凤
王定罗
戈佶次
王安.
王永仰
王娆拥
王牡右
王友缨
王梓集

以上就是最简单的概率模型,通过查表的方式生成名字字符串,生成过程都是基于可观察数据中的真实概率。

概率模型的量化评估

如何量化概率模型的生成结果?

答案是:likelihood,也叫做可能性或似然度,计算方式简单就是把所有词的概率相乘即可。例如,王阿宝:

l i k e l i h o o d = p ( . 王 ) × p ( 王阿 ) × p ( 阿宝 ) × p ( 宝 . ) likelihood = p(.王) imes p(王阿) imes p(阿宝) imes p(宝.) likelihood=p(.)×p(王阿)×p(阿宝)×p(.)

但是这会出现一个问题,就是概率都是小于1的值,只会越乘越小,所以可以计算log_likelihood,更利于观察:

l o g l i k e l i h o o d = l o g ( p ( . 王 ) ) + l o g ( p ( 王阿 ) ) + l o g ( p ( 阿宝 ) ) + l o g ( p ( 宝 . ) ) {log}_{likelihood} = log(p(.王)) + log(p(王阿)) + log(p(阿宝)) + log(p(宝.)) loglikelihood=log(p(.))+log(p(王阿))+log(p(阿宝))+log(p(.))

对数变换后,在增加一些常用的变换,取负值和计算平均似然度:

M e a n N e g a t i v e L o g L i k e l i h o o d = − l o g l i k e l i h o o d n = ( l o g ( p ( . 王 ) ) + l o g ( p ( 王阿 ) ) + l o g ( p ( 阿宝 ) ) + l o g ( p ( 宝 . ) ) b i g ) ∗ − 1 n MeanNegativeLogLikelihood = frac{-{log}_{likelihood}}{n} = frac{ig(log(p(.王)) + log(p(王阿)) + log(p(阿宝)) + log(p(宝.))\big)*-1}{n} MeanNegativeLogLikelihood=nloglikelihood=n(log(p(.))+log(p(王阿))+log(p(阿宝))+log(p(.))big)∗−1

这意味着,如果生成的结果越好,平均负对数似然度就应该越小,反之越大。

下面计算部分模型生成结果的MNLL=MeanNegativeLogLikelihood:

# 计算字符串的对数似然度
log_likelihood = 0.0
n = 0

for w in ["王静修","王映凤","戈佶次"]:
    chs = ['.'] + list(w) + ['.']        
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        prob = P[ix1, ix2]
        logprob = torch.log(prob)
        log_likelihood += logprob
        n += 1
        print(f'{ch1}{ch2}: {prob:.4f} {logprob:.4f}')
print(f'{log_likelihood=}')
print('negative log likelihood =',-log_likelihood)
mean_nll = -log_likelihood/n
print(f'{mean_nll=}')
.王: 0.9693 -0.0312
王静: 0.0028 -5.8954
静修: 0.0005 -7.5648
修.: 0.0143 -4.2474
.王: 0.9693 -0.0312
王映: 0.0005 -7.5968
映凤: 0.0006 -7.4260
凤.: 0.1019 -2.2840
.戈: 0.0000 -10.8926
戈佶: 0.0006 -7.4146
佶次: 0.0006 -7.4097
次.: 0.0006 -7.4103
log_likelihood=tensor(-68.2039)
negative log likelihood = tensor(68.2039)
mean_nll=tensor(5.6837)

下面根据先验概率计算出整个数据的MNLL:

# 计算全部样本的平均负对数似然度
log_likelihood = 0.0
n = 0

for w in names:
    chs = ['.'] + list(w) + ['.']        
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        prob = P[ix1, ix2]
        logprob = torch.log(prob)
        log_likelihood += logprob
        n += 1

nll = -log_likelihood
mean_nll = nll/n
print(f'{mean_nll=}')
mean_nll=tensor(3.9716)

下面随机测试一些名字:

# 计算字符串的对数似然度
log_likelihood = 0.0
n = 0

for w in ["一乃乃","乃乃乃","丁丫丫"]:
    chs = ['.'] + list(w) + ['.']        
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        prob = P[ix1, ix2]
        logprob = torch.log(prob)
        log_likelihood += logprob
        n += 1
        print(f'{ch1}{ch2}: {prob:.4f} {logprob:.4f}')
print(f'{log_likelihood=}')
print('negative log likelihood =',-log_likelihood)
mean_nll = -log_likelihood/n
print(f'{mean_nll=}')
.一: 0.0000 -10.8926
一乃: 0.0005 -7.6406
乃乃: 0.0006 -7.4559
乃.: 0.0006 -7.4559
.乃: 0.0000 -10.8926
乃乃: 0.0006 -7.4559
乃乃: 0.0006 -7.4559
乃.: 0.0006 -7.4559
.丁: 0.0000 -10.8926
丁丫: 0.0006 -7.4236
丫丫: 0.0006 -7.4103
丫.: 0.0012 -6.7172
log_likelihood=tensor(-99.1490)
negative log likelihood = tensor(99.1490)
mean_nll=tensor(8.2624)

通过上面的测试结果,可以发现数据中真实正常的名字MNLL = 3.97,根据先验概率生成的结果MNLL = 5.68,随机编的奇葩名字MNLL = 8.26。这也验证生成效果越差,MNLL值越大。

优化模型:

接下就是通过神经网络模型来替代之前的简单模型,通过找到一组参数可以实现以下目标:

  • 最大化生成结果的对数似然度 = = = 最小化负对数似然度 = = = 最小化平均负对数似然度,生成更加符合数据分布的结果。

在这里插入图片描述

x n ∗ 1651 × W 1651 ∗ 1651 = l o g i t s n ∗ 1651 x_{n*1651} imes W_{1651*1651} = {logits}_{n*1651} xn1651×W16511651=logitsn1651

M N L L = − ∑ i = 1 n l o g ( f p ( l o g i t s ) [ i ] ) n MNLL = frac{-sum_{i=1}^{n} log(f_p(logits)[i])}{n} MNLL=ni=1nlog(fp(logits)[i])

  • W W W : 模型的参数矩阵
  • 1651 : 词汇表的大小,也是输入向量化后的长度
  • n :输入输出序列的长度
  • f p f_p fp : 转换函数把logits缩放为0~1之间概率值
  • MNLL : 平均负对数似然度

创建训练数据:(x,y)

xs, ys = [], []

for w in names[:1]:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        print(ch1, ch2)
        xs.append(ix1)
        ys.append(ix2)
    
xs = torch.tensor(xs)
ys = torch.tensor(ys)
. 王
王 阿
阿 宝
宝 .
xs(char)ys(char)xs(index)ys(index)
.0993
9931539
1539409
.4090

下面我们模拟神经网络模型的推理过程,简单起见,模型只有一个权重矩阵(输入 → o 输出),计算模型输出:

  1. 对输入进行向量化,one-hot编吗:
xenc = F.one_hot(xs, num_classes=1651).float()
xenc
tensor([[1., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.],
        [0., 0., 0.,  ..., 0., 0., 0.]])

模型输入向量的可视化:
在这里插入图片描述

  1. 随机初始化的输入到输出之间的权重矩阵 W W W,计算模型的输出:预测ys的概率分布,即xs每个词对应下个词的概率分布。
W = torch.randn((1651, 1651))
W.shape
torch.Size([1651, 1651])

权重矩阵的可视化:

在这里插入图片描述

  1. 计算模型的输出:矩阵乘法
logits = xenc @ W # log-counts
counts = logits.exp() # equivalent N
probs = counts / counts.sum(1, keepdims=True) # scale to 0~1
probs
tensor([[2.6660e-04, 3.3742e-03, 1.3357e-03,  ..., 3.4182e-04, 2.3816e-04,
         3.4352e-04],
        [2.1228e-04, 1.5516e-03, 5.3819e-05,  ..., 1.5116e-03, 1.8045e-04,
         1.7484e-04],
        [7.3022e-04, 8.9569e-04, 1.9453e-04,  ..., 1.0783e-04, 2.8304e-03,
         9.5034e-05],
        [2.9773e-04, 6.6036e-04, 1.1865e-03,  ..., 1.6132e-04, 1.7862e-04,
         4.8288e-04]])

模型输出的可视化:

在这里插入图片描述

  1. 下面根据最终的输出,每一行概率最大元素对应的索引即模型的预测值:
for i in torch.argmax(probs, axis=1):
    print(i.item(),i2char[i.item()])
1316 萱
30 乐
869 济
573 惜
  1. 计算随机模型预测结果的MNLL:
xs,ys
(tensor([   0,  993, 1539,  409]), tensor([ 993, 1539,  409,    0]))

nlls = torch.zeros(4)
for i in range(4):
    # i-th bigram:
    x = xs[i].item() # input character index
    y = ys[i].item() # label character index
    print('--------')
    print(f'example {i+1}: {i2char[x]}{i2char[y]} (indexes {x},{y})')
    print('input to the neural net:', x)
    print('label (next  character):', y)
    p = probs[i, y]
    print('probability assigned by net:', p.item())
    logp = torch.log(p)
    print('log likelihood:', logp.item())
    nll = -logp
    print('negative log likelihood:', nll.item())
    nlls[i] = nll

print('=========')
print('average negative log likelihood, i.e. loss =', nlls.mean().item())
--------
example 1: .王 (indexes 0,993)
input to the neural net: 0
label (next  character): 993
probability assigned by net: 0.00024021849094424397
log likelihood: -8.333961486816406
negative log likelihood: 8.333961486816406
--------
example 2: 王阿 (indexes 993,1539)
input to the neural net: 993
label (next  character): 1539
probability assigned by net: 0.0004108154389541596
log likelihood: -7.797366619110107
negative log likelihood: 7.797366619110107
--------
example 3: 阿宝 (indexes 1539,409)
input to the neural net: 1539
label (next  character): 409
probability assigned by net: 0.002926822518929839
log likelihood: -5.833837985992432
negative log likelihood: 5.833837985992432
--------
example 4: 宝. (indexes 409,0)
input to the neural net: 409
label (next  character): 0
probability assigned by net: 0.0002977268013637513
log likelihood: -8.11933422088623
negative log likelihood: 8.11933422088623
=========
average negative log likelihood, i.e. loss = 7.521124839782715

对比之前的测试结果,随机初始化的模型预测结果和随机生成的名字差不多。

梯度下降

通过MNLL(loss)对参数 W W W求导计算梯度,根据梯度更新模型。

# 使用全量数据
xs, ys = [], []
for w in names:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = char2i[ch1]
        ix2 = char2i[ch2]
        xs.append(ix1)
        ys.append(ix2)
xs = torch.tensor(xs)
ys = torch.tensor(ys)
num = xs.nelement()
print('number of examples: ', num)
number of examples:  208508
# 随机初始化
g = torch.Generator().manual_seed(202304191424)
W = torch.randn((1651, 1651), generator=g, requires_grad=True)
# gradient descent
history = {'loss':[]}
for k in range(100):
    # forward pass
    xenc = F.one_hot(xs, num_classes=1651).float()  # one-hot encoding
    logits = xenc @ W                             # predict log-counts
    counts = logits.exp()                         # counts, equivalent to N
    probs = counts / counts.sum(1, keepdims=True) # probabilities for next character
    loss = -probs[torch.arange(num), ys].log().mean() + 0.01*(W**2).mean() # Add a regular
    #print(loss.item())
    history['loss'].append(loss.item())
    # backward pass
    W.grad = None                                 # set to zero the gradient
    loss.backward()
    # update
    W.data += -300 * W.grad
plt.figure(figsize=(8,3))
plt.plot(history['loss'], label='NN model')
plt.axhline(y=3.9716, c='red')
plt.grid()

在这里插入图片描述

经过100多轮的训练,模型的预测结果已经达到了真实数据的水平,下面是测试结果:

# finally, sample from the 'neural net' model
g = torch.Generator().manual_seed(202304191533)

for i in range(10):
    out = []
    ix = 0
    while True:
        xenc = F.one_hot(torch.tensor([ix]), num_classes=1651).float()
        logits = xenc @ W # predict log-counts
        counts = logits.exp() # counts, equivalent to N
        p = counts / counts.sum(1, keepdims=True) # probabilities for next character
        ix = torch.multinomial(p, num_samples=1, replacement=True, generator=g).item()
        out.append(i2char[ix])
        if ix == 0 or len(out) == 3:
            break
    print(''.join(out))
王骏杭
王桂普
王晓薪
王亦麒
王英.
王长萍
王丙能
王中.
王占郢
王光皓
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。