神经网络-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] += 1Expected Output:
ix1 \ ix2 | 0 (a) | 1 (b) | 2 (c) | … | 25 (z) | 26 (<S>) | 27 (<E>) |
|---|---|---|---|---|---|---|---|
0 (a) | 556 | 541 | 470 | … | 435 | 0 | 6640 |
1 (b) | 321 | 38 | 1 | … | 0 | 0 | 114 |
2 (c) | 815 | 0 | 42 | … | 4 | 0 | 97 |
| … | … | … | … | … | … | … | … |
25 (z) | 860 | 4 | 2 | … | 45 | 0 | 160 |
26 (<S>) | 4410 | 1306 | 1542 | … | 929 | 0 | 0 |
27 (<E>) | 0 | 0 | 0 | … | 0 | 0 | 0 |
显然, 这样的储存方式有点呆呆的, 我们浪费了一些空间, 因为结尾符号是不可能在起始符号之前的. 同时, 起始符号紧跟着结尾符号的情况也几乎不可能出现(我们一般管它叫无效数据)
优化一下:
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] += 1Expected output:
ix1 \ ix2 | 0 (.) | 1 (a) | 2 (b) | 3 (c) | … | 26 (z) |
|---|---|---|---|---|---|---|
0 (.) | 0 | 4410 | 1306 | 1542 | … | 929 |
1 (a) | 6640 | 556 | 541 | 470 | … | 435 |
2 (b) | 114 | 321 | 38 | 1 | … | 0 |
3 (c) | 97 | 815 | 0 | 42 | … | 4 |
| … | … | … | … | … | … | … |
26 (z) | 160 | 860 | 4 | 2 | … | 45 |
这样好多了.