Neural Networks-part2


神经网络-Chap.2

本系列神经网络课程根据Youtube上Andrej Karpathy的系列视频: Neural Networks: Zero to Hero整理而来.
应当可以作为对于LLM进行内部原理理解的入门视频之一.

这一节我们开始真正试着捣鼓出一些比较简单的模型. 依旧基于作者提供的一个GitHub库: https://github.com/karpathy/makemore

2.1 引子

我们的目的是捣鼓出来一个能生成一些很像名字的字符串的模型.
这个仓库的基本结构围绕着这个目的, 其结构非常简单:

makemore/
├── names.txt      # 【数据文件】约 32,000 个真实英文人名,每行一个名字
├── makemore.py    # 【源码】单文件包含了 Bigram / MLP / RNN / BoW / Transformer 5 种架构
├── README.md      # 项目说明与命令行运行指南
└── LICENSE        # MIT 开源协议

需要指出的是, 如果我们给了一个名字, 例如: adam. 我们其实能从这个名字里面获得的信息很多, like:

  • a有可能作为一个名字的起始字母
  • d有可能跟在a后面
  • a可能跟在ad后面
  • …
  • m有可能是名字的最后一个字母

为了更好地开始, 我们暂且不考虑过于复杂的信息, 如第二讲视频中所提及的, 我们这一节只是想做一个 ‘Bigram’ 模型. 即只根据前面一个字母来生成下一个字母.

2.2 探索相邻字符的关联

我们考虑从这段代码开始:

words = open('names.txt', 'r').read().splitlines()
for w in words:
    for ch1, ch2 in zip(w, w[1:]):
        print(ch1, ch2)

但就像我们上面所说的那样. 这样提取出的前后关联, 少了一步.
也就是开头的字母以及结尾的字母该怎么表示, 所以我们在字符串前后都加一个特殊字符.

words = open('names.txt', 'r').read().splitlines()
for w in words:
    chs = ['<S>'] + list(w) + ['<E>']
    for ch1, ch2 in zip(chs, chs[1:]):
        print(ch1, ch2)
Output
<S> e e m m m m a a <E> <S> o o l l i i v v i i a a <E> <S> a a v v a a <E> ...

So, 我们现在要找到这一对二元组的数据之间的关联, 最简单的方式是什么?
直接数, 对吧:

b = {}
words = open('names.txt', 'r').read().splitlines()
for w in words:
    chs = ['<S>'] + list(w) + ['<E>']
    for ch1, ch2 in zip(chs, chs[1:]):
        bigram = (ch1, ch2)
        b[bigram] = b.get(bigram, 0) + 1
# And we sorted the result(with backward order)
sorted(b.items(), key = lambda kv: -kv[1])
output
[ (('n', '<E>'), 6763), # 最常见的组合:以字母 'n' 结尾的名字(出现了 6763 次) (('a', '<E>'), 6640), # 第二常见:以字母 'a' 结尾的名字(出现了 6640 次) (('a', 'n'), 5438), # 第三常见:字母 'a' 后面紧接 'n'(出现了 5438 次) (('<S>', 'a'), 4410), # 第四常见:以字母 'a' 开头的名字(出现了 4410 次) (('e', '<E>'), 3983), (('a', 'r'), 3264), (('e', 'l'), 3248), (('r', 'i'), 3033), (('n', 'a'), 2977), # ... (省略中间项) ... (('q', 'r'), 1), (('z', 'x'), 1), ]

现在我们用的是dictionary这个数据结构. 虽然这不算低效, 但确实不适合计算. 有没有更合适的方式来存储这种数据结构?
矩阵.

我们用矩阵的行作为第一个字母, 列作为第二个字母, 行列对应位置的数字作为计数值.
所以我们应该需要一个 28 * 28 的矩阵, 即26个字母 + 两个特殊字符.

我们直接用Pytorch来进行矩阵的创建, 存储更新:

import torch
N = torch.zeros((28, 28), dtype = torch.int32) # 存储计数, 所以只需要int类型即可
words = open('names.txt', 'r').read().splitlines()

chars = sorted(list(set(''.join(words))))
stoi = {s:i for i, s in enumerate(chars)} # a dictionary to convert letters to number
stoi['<S>'] = 26
stoi['<E>'] = 27

for w in words:
    chs = ['<S>'] + list(w) + ['<E>']
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = stoi[ch1]
        ix2 = stoi[ch2]

        N[ix1, ix2] += 1

Expected Output:

ix1 \ ix20 (a)1 (b)2 (c)…25 (z)26 (<S>)27 (<E>)
0 (a)556541470…43506640
1 (b)321381…00114
2 (c)815042…4097
……………………
25 (z)86042…450160
26 (<S>)441013061542…92900
27 (<E>)000…000

显然, 这样的储存方式有点呆呆的, 我们浪费了一些空间, 因为结尾符号是不可能在起始符号之前的. 同时, 起始符号紧跟着结尾符号的情况也几乎不可能出现(我们一般管它叫无效数据)
优化一下:

import torch
N = torch.zeros((28, 28), dtype = torch.int32) # 存储计数, 所以只需要int类型即可
words = open('names.txt', 'r').read().splitlines()

chars = sorted(list(set(''.join(words))))
stoi = {s:i for i, s in enumerate(chars)} # a dictionary to convert letters to number
stoi['.'] = 0

for w in words:
    chs = ['.'] + list(w) + ['.']
    for ch1, ch2 in zip(chs, chs[1:]):
        ix1 = stoi[ch1]
        ix2 = stoi[ch2]

        N[ix1, ix2] += 1

Expected output:

ix1 \ ix20 (.)1 (a)2 (b)3 (c)…26 (z)
0 (.)0441013061542…929
1 (a)6640556541470…435
2 (b)114321381…0
3 (c)97815042…4
…………………
26 (z)16086042…45

这样好多了.


文章作者: MUG-chen
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 MUG-chen !
  目录