https://zh.d2l.ai/chapter_recurrent-neural-networks/language-models-and-dataset.html
import collections import re from d2l import torch as d2l draw_pic=0 #@save d2l.DATA_HUB['time_machine'] = (d2l.DATA_URL + 'timemachine.txt', '090b5e7e70c295757f55df93cb0a180b9691891a') def read_time_machine(): #@save """将时间机器数据集加载到文本行的列表中""" with open(d2l.download('time_machine'), 'r') as f: lines = f.readlines() #re.sub('[^A-Za-z]+', ' ', line):将line字符串中的 连续多个非字母的字符 变成 空格 #re.sub('[^A-Za-z]+', ' ', 'I\n') #Out[6]: 'I ' return [re.sub('[^A-Za-z]+', ' ', line).strip().lower() for line in lines] #lines = read_time_machine() #print(f'# 文本总行数: {len(lines)}') #print(lines[0]) #print(lines[10]) def tokenize(lines, token='word'): #@save """将文本行拆分为单词或字符词元""" if token == 'word': # 按照单词拆分 return [line.split() for line in lines] elif token == 'char': #按照字符拆分 return [list(line) for line in lines] else: print('错误:未知词元类型:' + token) #tokens = tokenize(lines) # for i in range(16): # print("tokens",i,tokens[i]) def count_corpus(tokens):# """统计词元的频率""" # 这里的tokens是1D列表或2D列表 if len(tokens) == 0 or isinstance(tokens[0], list): # 将词元列表展平成一个列表 # tokens1=[] # for line in tokens: # #print("line",line) # for token in line: # #print("token",token) # tokens1.append(token) # tokens=tokens1 tokens = [token for line in tokens for token in line] #print(tokens.shape) #最主要的作用是计算“可迭代序列”中各个元素(element)的数量 ''' #对字符串作用 temp=Counter('aabbcdeffgg') print(temp) #Counter({'a': 2, 'b': 2, 'f': 2, 'g': 2, 'c': 1, 'd': 1, 'e': 1}) ''' return collections.Counter(tokens) #count_corpus(tokens) #词表 #构建一个字典,通常也叫做词表(vocabulary),用来将字符串类型的词元映射到从开始的数字索引中. # 我们先将训练集中的所有文档合并在一起,对它们的唯一词元进行统计, 得到的统计结果称之为语料(corpus)。 # 然后根据每个唯一词元的出现频率,为其分配一个数字索引。 很少出现的词元通常被移除,这可以降低复杂性。 # 另外,语料库中不存在或已删除的任何词元都将映射到一个特定的未知词元“<unk>”。 # 我们可以选择增加一个列表,用于保存那些被保留的词元, 例如:填充词元(“<pad>”); 序列开始词元(“<bos>”); 序列结束词元 class Vocab: #@save """文本词表""" def __init__(self, tokens=None, min_freq=0, reserved_tokens=None): if tokens is None: tokens = [] if reserved_tokens is None: #reserved_tokens = ["a","b"] reserved_tokens = [] # 按出现频率排序 ''' #对字符串作用 temp=Counter('aabbcdeffgg') print(temp) #Counter({'a': 2, 'b': 2, 'f': 2, 'g': 2, 'c': 1, 'd': 1, 'e': 1}) ''' counter = count_corpus(tokens) #print("词频统计结果",counter) ''' 词频统计结果 Counter({' ': 29927, 'e': 17838, 't': 13515, 'a': 11704, 'i': 10138, 'n': 9917, 'o': 9758, 's': 8486, 'h': 8257, 'r': 7674, 'd': 6337, 'l': 6146, 'm': 4043, 'u': 3805, 'c': 3424, 'f': 3354, 'w': 3225, 'g': 3075, 'y': 2679, 'p': 2427, 'b': 1897, 'v': 1295, 'k': 1087, 'x': 236, 'z': 144, 'j': 97, 'q': 95}) ''' # 排序 已知词汇 reverse=True 从高到低 self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True) # 未知词元的索引为0 self.idx_to_token = ['<unk>'] + reserved_tokens #self.idx_to_token = ['<unk>'] + reserved_tokens #{'<unk>': 0} #{'<unk>': 0, 'a': 1, 'b': 2} self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)} #print(self.token_to_idx ) ''' 1最开头是 未知词元 假设是 词源 idx_to_token['<unk>', ‘a’ ,'b'] 词源ID token_to_idx[0,1,2] 2将文本中统计出来的有效词源 加进去 例如 b1 2-1 剔除小于阈值的 2-2 加入新词源 频率 从高到低 词源 idx_to_token['<unk>', ‘a’ ,'b','b1'] 词源ID token_to_idx[0,1,2,3] ''' # 最开头是 未知词元 假设是 idx_to_token['<unk>', ‘a’ ,'b'] # 将文本中统计出来的有效词源 加进去 例如 b1 # for token, freq in self._token_freqs: # 从高到低访问 if freq < min_freq:# 剔除小于阈值的词 break if token not in self.token_to_idx:# 未知词汇剔除 # 未知词元 添加 单词进入列表 # '<unk>' a b 加入 d1 self.idx_to_token.append(token) # token_to_idx【'd1'】= 4-1=3 self.token_to_idx[token] = len(self.idx_to_token) - 1 #从0开始 添加 def __len__(self): return len(self.idx_to_token) def __getitem__(self, tokens): if not isinstance(tokens, (list, tuple)): return self.token_to_idx.get(tokens, self.unk) return [self.__getitem__(token) for token in tokens] def to_tokens(self, indices): if not isinstance(indices, (list, tuple)): return self.idx_to_token[indices] return [self.idx_to_token[index] for index in indices] @property def unk(self): # 未知词元的索引为0 return 0 @property def token_freqs(self): return self._token_freqs # 整合所有功能 def load_corpus_time_machine(max_tokens=-1): #@save """返回时光机器数据集的词元索引列表和词表""" # 1 读取所有文本 逐行存 lines = read_time_machine() # 2 # 2-1将所有行的字母拆出来,汇总到一个列表里面 tokens = tokenize(lines, 'char') # print("tokens",tokens) ''' tokens= [['a', 'b'],['c' 'd'],...] ''' # 3 获取词汇列表 ''' (频率 从高到底 前面是未知词源) 词源 idx_to_token['<unk>', ‘a’ ,'b','b1'] 词源ID token_to_idx[0,1,2,3] ''' vocab = Vocab(tokens) # 因为时光机器数据集中的每个文本行不一定是一个句子或一个段落, # 所以将所有文本行拆分成一个个字符,然后展平到一个列表中 corpus=[] ''' tokens= [['a', 'b'],['c' 'd'],...] ''' for line in tokens: for token in line: corpus.append(vocab[token]) # vocab[token] 字母’x‘的编码ID #corpus = [vocab[token] for line in tokens for token in line] if max_tokens > 0: corpus = corpus[:max_tokens] #corpus 全文变成单独字符的列表结合 #vocab vocab[token] 字母’x‘的编码ID(非次数 ID就是从高到低按照次数排列到的) 集合 {“a”,1,"b","2",...} return corpus, vocab #===============================测试1 按照 字符拆分============================================== #在使用上述函数时,我们将所有功能打包到load_corpus_time_machine函数中, #该函数返回corpus(词元索引列表)和vocab(时光机器语料库的词表)。 ''' 为了简化后面章节中的训练,我们使用字符(而不是单词)实现文本词元化; 时光机器数据集中的每个文本行不一定是一个句子或一个段落,还可能是一个单词,因此返回的corpus仅处理为单个列表,而不是使用多词元列表构成的一个列表。 ''' #corpus, vocab = load_corpus_time_machine() #170580 28 一共有corpus 170580个字符,其中重复字符的有vocab 28个(ABCD-FG 28个英文字符) #print(len(corpus), len(vocab)) ''' 词频统计结果 Counter({' ': 29927, 'e': 17838, 't': 13515, 'a': 11704, 'i': 10138, 'n': 9917, 'o': 9758, 's': 8486, 'h': 8257, 'r': 7674, 'd': 6337, 'l': 6146, 'm': 4043, 'u': 3805, 'c': 3424, 'f': 3354, 'w': 3225, 'g': 3075, 'y': 2679, 'p': 2427, 'b': 1897, 'v': 1295, 'k': 1087, 'x': 236, 'z': 144, 'j': 97, 'q': 95}) ''' #===============================测试2 按照 单元 单词拆分============================================== import random import torch from d2l import torch as d2l #1 # 1 读取所有文本 逐行存 all_txt_word_list=read_time_machine() #2 根据单词统计词频 从高到低存放 最前面是未知词源 # 2-1将所有行的字母拆出来,汇总到一个列表里面 # 2-2将所有字母,对每一个字母统计词频 tokens = tokenize(all_txt_word_list, 'word') # print("tokens",tokens) ''' tokens= [['apple', 'bule'],['hey' 'hello'],...] ''' # 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起 corpus = [token for line in tokens for token in line] # 3 获取词汇列表 ''' (频率 从高到底 前面是未知词源) 词源 idx_to_token['<unk>', ‘a’ ,'b','b1'] 词源ID token_to_idx[0,1,2,3] ''' vocab = d2l.Vocab(corpus) print("单元打印前10个",vocab.token_freqs[:10]) token_freqs=vocab.token_freqs freqs=[] tokens=[] ''' ['hall', 13] ['sea', 13] ['course', 12] ['clearly', 12] ''' for token, freq in token_freqs: #freq=[token,freq] freqs.append(freq) #print(freq) #freqs = [freq for token, freq in token_freqs] # X 按单词的词频高-低排序的的id 1 2 3 4 Y 频率 ... if draw_pic: d2l.plot(freqs, xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log') if draw_pic: d2l.plt.show() #===============================测试3 按照 二元 单词拆分============================================== #我们来看看二元语法的频率是否与一元语法的频率表现出相同的行为方式。 #1 # 1 读取所有文本 逐行存 #all_txt_word_list=read_time_machine() #2 根据单词统计词频 从高到低存放 最前面是未知词源 # 2-1将所有行的字母拆出来,汇总到一个列表里面 # 2-2将所有字母,对每一个字母统计词频 #tokens = tokenize(all_txt_word_list, 'word') # print("tokens",tokens) ''' tokens= [['apple', 'bule'],['hey' 'hello'],...] ''' # 因为每个文本行不一定是一个句子或一个段落,因此我们把所有文本行拼接到一起 #corpus = [token for line in tokens for token in line] #bigram_tokens = [pair for pair in zip(corpus[:-1], corpus[1:])] bigram_tokens=[] for pair in zip(corpus[:-1], corpus[1:]): bigram_tokens.append(pair) #print(pair) #('when', 'mind') #('mind', 'and') #在十个最频繁的词对中,有九个是由两个停用词组成的, 只有一个与“the time”有关。 ''' [(('of', 'the'), 309), (('in', 'the'), 169), (('i', 'had'), 130), (('i', 'was'), 112), (('and', 'the'), 109), (('the', 'time'), 102), (('it', 'was'), 99), (('to', 'the'), 85), (('as', 'i'), 78), (('of', 'a'), 73)] ''' vocab = Vocab(bigram_tokens) print("双元打印前10个",vocab.token_freqs[:10]) token_freqs=vocab.token_freqs bigram_freqs =[] tokens=[] ''' ['hall', 13] ['sea', 13] ['course', 12] ['clearly', 12] ''' for token, freq in token_freqs: #freq=[token,freq] bigram_freqs .append(freq) #print(freq) #freqs = [freq for token, freq in token_freqs] # X 按单词的词频高-低排序的的id 1 2 3 4 Y 频率 ... if draw_pic: d2l.plot(bigram_freqs , xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log') if draw_pic: d2l.plt.show() #===============================测试4 按照 三元 单词拆分============================================== trigram_tokens=[] for pair in zip(corpus[:-2], corpus[:-2], corpus[2:]): ''' print(pair) ('mind', 'and', 'strength') ('and', 'strength', 'had') ''' trigram_tokens.append(pair) #trigram_tokens = [triple for triple in zip(corpus[:-2], corpus[1:], corpus[2:])] vocab = Vocab(trigram_tokens) print("三元打印前10个",vocab.token_freqs[:10]) token_freqs=vocab.token_freqs trigram_freqs =[] tokens=[] ''' ['hall', 13] ['sea', 13] ['course', 12] ['clearly', 12] ''' for token, freq in token_freqs: #freq=[token,freq] trigram_freqs .append(freq) #print(freq) #freqs = [freq for token, freq in token_freqs] # X 按单词的词频高-低排序的的id 1 2 3 4 Y 频率 ... if draw_pic: d2l.plot(trigram_freqs , xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log') if draw_pic: d2l.plt.show() ''' [(('the', 'time', 'traveller'), 59), (('the', 'time', 'machine'), 30), (('the', 'medical', 'man'), 24), (('it', 'seemed', 'to'), 16), (('it', 'was', 'a'), 15), (('here', 'and', 'there'), 15), (('seemed', 'to', 'me'), 14), (('i', 'did', 'not'), 14), (('i', 'saw', 'the'), 13), (('i', 'began', 'to'), 13)] ''' #==================最后,我们直观地对比三种模型中的词元频率:一元语法、二元语法和三元语法。============== # bigram_freqs = [freq for token, freq in bigram_vocab.token_freqs] # trigram_freqs = [freq for token, freq in trigram_vocab.token_freqs] if draw_pic: d2l.plot([freqs, bigram_freqs, trigram_freqs], xlabel='token: x', ylabel='frequency: n(x)', xscale='log', yscale='log', legend=['unigram', 'bigram', 'trigram']) if draw_pic: d2l.plt.show() #================== #当序列变得太长而不能被模型一次性全部处理时, 我们可能希望拆分这样的序列方便模型读取。 # 假设我们将使用神经网络来训练语言模型, 模型中的网络一次处理具有预定义长度 (例如个时间步)的一个小批量序列。 #现在的问题是如何随机生成一个小批量数据的特征和标签以供读取。 #首先,由于文本序列可以是任意长的 #于是任意长的序列可以被我们划分为具有相同时间步数的子序列。 #因为我们可以选择任意偏移量来指示初始位置,所以我们有相当大的自由度。 #如果我们只选择一个偏移量, 那么用于训练网络的、所有可能的子序列的覆盖范围将是有限的。 #因此,我们可以从随机偏移量开始划分序列, 以同时获得覆盖性(coverage)和随机性(randomness)。 def seq_data_iter_sequential(corpus, batch_size, num_steps): #@save """使用顺序分区生成一个小批量子序列""" # 从随机偏移量开始划分序列 #corpus= [0, 1, 2, 3, ...,33,34] #num_steps=5 #batch_size=2 offset = random.randint(0, num_steps)# 获取一个 0-num_steps 之间的整型随机数 #offset=1 print('offset',offset) ##offset 1 num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size #取整除 ,返回商的整数部分(向下取整) # (35-1-1)// 2 * 2 = 16 *2 = 32 print('num_tokens',num_tokens) #num_tokens 32 # [1 : 1+32=33] 最后一个不取==> [1 : 32] 总长度 num_tokens 偶数 Xs = torch.tensor(corpus[offset: offset + num_tokens]) print('Xs',Xs.shape,Xs) ''' Xs torch.Size([32]) tensor([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]) ''' # [2 : 2+32=34] Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens]) print('Ys',Ys.shape,Ys) ''' Ys torch.Size([32]) tensor([ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]) ''' # batch_size 批次 单词导入的样本数目 # Xs 32个 Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1) print('Xsreshape',Xs.shape,Xs) ''' Xsreshape torch.Size([2, 16]) tensor( [[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], [ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]]) ''' print('Ysreshape',Ys.shape,Ys) ''' Ysreshape torch.Size([2, 16]) tensor( [[ 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], [ 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]]) ''' # Xs.shape[1] 列 长度// num_steps num_batches = Xs.shape[1] // num_steps #num_batches= 16//5 = 15/5=3 # 0 - 5*3 - 5 for i in range(0, num_steps * num_batches, num_steps): # i 0 5 10 X = Xs[:, i: i + num_steps] Y = Ys[:, i: i + num_steps] ''' # i=0 Xs [:,0:5] Ys [:,0:5] # i=5 Xs [:,5:10] Ys [:,5:10] # i=15 Xs [:,10:15] Ys [:,10:15] ''' yield X, Y #my_seq [0, 1, 2, 3, ...,33,34] my_seq = list(range(35)) #print("my_seq",my_seq) for X, Y in seq_data_iter_sequential(my_seq, batch_size=2, num_steps=5): print('X: ', X, '\nY:', Y) #================== def seq_data_iter_random(corpus, batch_size, num_steps): #@save """使用随机抽样生成一个小批量子序列""" # 从随机偏移量开始对序列进行分区,随机范围包括num_steps-1 corpus = corpus[random.randint(0, num_steps - 1):] # 减去1,是因为我们需要考虑标签 num_subseqs = (len(corpus) - 1) // num_steps # 长度为num_steps的子序列的起始索引 initial_indices = list(range(0, num_subseqs * num_steps, num_steps)) # 在随机抽样的迭代过程中, # 来自两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻 random.shuffle(initial_indices) def data(pos): # 返回从pos位置开始的长度为num_steps的序列 return corpus[pos: pos + num_steps] num_batches = num_subseqs // batch_size for i in range(0, batch_size * num_batches, batch_size): # 在这里,initial_indices包含子序列的随机起始索引 initial_indices_per_batch = initial_indices[i: i + batch_size] X = [data(j) for j in initial_indices_per_batch] Y = [data(j + 1) for j in initial_indices_per_batch] yield torch.tensor(X), torch.tensor(Y) my_seq = list(range(35)) for X, Y in seq_data_iter_random(my_seq, batch_size=2, num_steps=5): print('X: ', X, '\nY:', Y)