d2l从零与简洁实现RNN

详解一下该章的代码

目录

1.从零实现RNN

1.1加载数据

1.2One-hot独热向量

1.3初始化参数

1.4.1tuple补充

1.5封装一下上面的函数

1.6预测

1.6.1函数里面lambada的探索

1.7梯度剪裁

1.8训练

2.简洁版RNN

2.1同样的数据载入

2.2模型定义

2.3训练

3.rnn总结一下输入输出维度


1.从零实现RNN

1.1加载数据

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

  既然是从0实现我就一块都讲了,看一看这个load_data函数长什么样:

def load_data_time_machine(batch_size, num_steps, #@save
    use_random_iter=False, max_tokens=10000):
    """返回时光机器数据集的迭代器和词表"""
    data_iter = SeqDataLoader(
        batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab

看看套的这个类是什么:

class SeqDataLoader: #@save
    """加载序列数据的迭代器"""
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = d2l.seq_data_iter_random
        else:
            self.data_iter_fn = d2l.seq_data_iter_sequential
        self.corpus, self.vocab = d2l.load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps
    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

   其中,d2l.load_corpus_time_machine(max_tokens)是获得corpus和vocab,这两个参数是为了调用data_corpus_fn用的。
   进入data_iter_fn是顺序或随机获得每个iter的X和Y,均为(bs,T)--详情见上一节: 语言模型

  总结一下得到了什么:train_iter为原txt的X,Y,尺寸均为(bs,T);vocab为该txt对应的vocab(char)

1.2One-hot独热向量

  每次采样的小批量数据的形状是(bs,时间步数T)
  经过one_hot转变成(T,bs,len),将X转置实现的,为了方便后续通过外层维度更新小批量数据的隐状态。

举个例子:这个例子中,bs=2;T=5

X = torch.arange(10).reshape((2, 5))
F.one_hot(X.T, 28).shape

'''
torch.Size([5, 2, 28])
'''

1.3初始化参数

  输入输出都是从Vocab里面来的(label就是features的后一个),所以要保证输入输出尺寸相同
  这里面num_hiddens隐藏单元数是超参数

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size
    
    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01
    
    # 隐藏层参数
    W_xh = normal((num_inputs, num_hiddens))
    W_hh = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)
    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # 附加梯度
    params = [W_xh, W_hh, b_h, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params

初始化H0

def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )

1.4定义RNN计算块fn

  更新为(T,bs,len)后,看成3维矩阵,T为行,bs为列,len为厚度,通过最外层T提取的每一行,为第一个时间部对应的bs个单词,其中每个单词为len长的独热向量表示。通过计算的H
  X这里为(bs,len),每次迭代通过T迭代出的是每一个时间步对应的单词位置,比如T=5,则共迭代5次,每一次迭代一个位置,每个位置X吐出bs个单词(前面可得),每个单词用len(vocab)长度的one-hot表示。
  计算得到的Y尺寸为(bs,v),与X一致,后续有进行了cat操作,所以最终返回的为(bs*T,len);另一个是当前的隐藏状态H

def rnn(inputs, state, params):
    # inputs的形状:(时间步数量,批量⼤⼩,词表⼤⼩)
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # X的形状:(批量⼤⼩,词表⼤⼩)
    for X in inputs:
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)

1.4.1tuple补充

  补充以下关于tuple的取值,如果把tuple里面的值取出来,则后面加‘,’,如果想把单一值存tuple,括号里面也要加',',否则就是int了

a = (5,)
b, = a
b

'''
5
'''

1.5封装一下上面的函数

class RNNModelScratch: #@save
    """从零开始实现的循环神经⽹络模型"""
    def __init__(self, vocab_size, num_hiddens, device,
                get_params, init_state, forward_fn):
        
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
        self.params = get_params(vocab_size, num_hiddens, device)
        self.init_state, self.forward_fn = init_state, forward_fn
        
    def __call__(self, X, state):
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
        return self.forward_fn(X, state, self.params)
    
    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)

  检查以下输出维度
  这里设定的X中,bs为2,T为5
  可看出Y为(T*bs,len);隐藏状态不变(bs,hiddens)
  隐藏状态尺寸:Xt×Wxh (bs,v)(v,h)--(bs,h)

num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(),
                      get_params,
                      init_rnn_state, rnn)

state = net.begin_state(X.shape[0], d2l.try_gpu())
Y, new_state = net(X.to(d2l.try_gpu()), state) # 此时送进去的X是(bs,T)
Y.shape, len(new_state), new_state[0].shape

'''
(torch.Size([10, 28]), 1, torch.Size([2, 512]))
'''

1.6预测

  perfix是提供的包含多个字符的字符串。
  output是每个字符串在vocab里面的下标,将prefix的第一个char放进vocab拿到该char对应下标,output一开始就是一个idx
  不要开始的net输出,因为有label,只更新H。训练完label后,再输出预测
  再使用idx转成token

def predict_ch8(prefix, num_preds, net, vocab, device): #@save
    """在prefix后⾯⽣成新字符"""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]: # 预热期
        _, state = net(get_input(), state)
        outputs.append(vocab[y])
    for _ in range(num_preds): # 预测num_preds步
        y, state = net(get_input(), state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])

predict_ch8('time traveller ', 10, net, vocab, d2l.try_gpu())

'''
'time traveller waqdsaqdsa'
'''

1.6.1函数里面lambada的探索

  lambda的作用:将outputs动态的带入循环中,如下代码所示更明晰,这里get_inputs传入out的参数为outputs,然后将传入参数的最后一个拿出来变成(1,1)的tensor再送入net中进行输出。
  原函数中没有设置传入参数,是因为直接将需要迭代的参数写到里面去了,这样函数再for loop迭代过程中自动更新outputs。
  但是如果不写lambda的话,则outputs不会产生变化,迭代过程中input都是一样的,进而产生的输出也都是一样的了。

  outputs在for loop中有append当前y的操作,vocab[i]=5,prefix[1:]取到的第一个char也是i,故经过第一轮后outputs为[3,5]。

  前向预测的时候,state是经过prefix计算后的H,这里y得到的是(1,len),表示的是每个可能的char的各个类别的预测分数,再取argmax得到最大可能char的idx,然后再并到outputs中,前面的vocab[y]中y为char,通过getitem得到idx,这里直接返回的也是idx。
  第一个字母返回idx是20,idx_to_token一下是p,在最后的输出中验证了这一点(见下图):

去掉lambda与补全lambda形式:

def predict_ch81(prefix, num_preds, net, vocab, device): #@save
    """在prefix后⾯⽣成新字符"""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    get_input = lambda out: torch.tensor([out[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]: # 预热期
        _, state = net(get_input(outputs), state)
        outputs.append(vocab[y])
    for _ in range(num_preds): # 预测num_preds步
        y, state = net(get_input(outputs), state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])
def predict_ch82(prefix, num_preds, net, vocab, device): #@save
    """在prefix后⾯⽣成新字符"""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    get_input = torch.tensor([outputs[-1]], device=device).reshape((1, 1))
#     get_input2 = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]: # 预热期
        _, state = net(get_input, state)
        outputs.append(vocab[y])
    for _ in range(num_preds): # 预测num_preds步
        y, state = net(get_input, state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return ''.join([vocab.idx_to_token[i] for i in outputs])

补全:(添加了自变量定义,并在后续传入)

predict_ch81('time traveller ', 10, net, vocab, d2l.try_gpu())

'''
'time traveller waqdsaqdsa'
'''

去掉lambda:input不会变,导致输出的都是一个东西。

predict_ch82('time traveller ', 10, net, vocab, d2l.try_gpu())

'''
'time traveller llllllllll'
'''

1.7梯度剪裁

  实现目标:若para的L2norm之和大于theta,则每个para* theta/norm
  sum是py自带函数;torch.sum仅能对tensor进行操作,可以实现在不同维度上求和,可部署GPU

def grad_clipping(net, theta): #@save
    """裁剪梯度"""
    if isinstance(net, nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

1.8训练

  iter=total/bs..也就是bs的总个数
  如果是random的iter,则每个bs之间的X不再上下关联,是随机的(前面有Sequential是前后顺序的)。所以前面一个iter的state对于后面的不再适用,要清零。
  backward只在一个iteraion里面做,所以对前面的state要进行.detach_()分离梯度操作,使得隐状态的梯度计算总是限制在一个小批量数据的时间步内。
  y为什么转置再reshape(-1)见下面
  注意使用了梯度裁剪后,在算完backward后加一个clipping
  别忘了最后要用困惑度,加exp

#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练⽹络⼀个迭代周期(定义⻅第8章)"""
    state, timer = None, d2l.Timer()
    metric = d2l.Accumulator(2) # 训练损失之和,词元数量
    for X, Y in train_iter:
        if state is None or use_random_iter:
            # 在第⼀次迭代或使⽤随机抽样时初始化state
            state = net.begin_state(batch_size=X.shape[0], device=device)
    else:
        if isinstance(net, nn.Module) and not isinstance(state, tuple):
            # state对于nn.GRU是个张量
            state.detach_()
        else:
            # state对于nn.LSTM或对于我们从零开始实现的模型是个张量
            for s in state:
                s.detach_()
    y = Y.T.reshape(-1)
    X, y = X.to(device), y.to(device)
    y_hat, state = net(X, state)
    l = loss(y_hat, y.long()).mean()
    if isinstance(updater, torch.optim.Optimizer):
        updater.zero_grad()
        l.backward()
        grad_clipping(net, 1)
        updater.step()
    else:
        l.backward()
        grad_clipping(net, 1)
        # 因为已经调⽤了mean函数
        updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()

对于y为什么也要使Y进行转置,原因:

  省事版:因为X再送进net中进行了转置操作,所以y也要。
  详细版: 1.虽然reshape(-1)后维度一样,但是内容可不一样,原y_hat是(bs* T,len),是由T个bs按dim=0螺起来的,每个bs为T对应位置的bs个预测。(第一个bs即为对T[0]的bs个预测,不同bs的T[0]对应的char不一样,这里T[0]只表示位置)
### 2.对于reshape(-1),会先将第一行展开,后再将第二行展开放到第一行后面...从而降成一维。Y本来是(bs,T),如果不转置则展开为按T展开,则第一行中的元素为各个T[i]对应的编号,而转置后wei(T,bs),每一行都对应着同一个T[i],与y_hat是对应一致的!!

for X, Y in train_iter:
    print(X.shape, Y.shape)
y = Y.T.reshape(-1)
y2 = Y.reshape(-1)
print(y)
print(y2)


'''
torch.Size([32, 35]) torch.Size([32, 35])
torch.Size([32, 35]) torch.Size([32, 35])
...
tensor([ 6,  1,  9,  ...,  7, 10, 21])
tensor([ 6,  3, 12,  ...,  8,  1, 21])
'''

  本质还是多分类,所以loss为交叉熵;每个bs中由T个单词,所以单bs送入net中其本质使进行了bs*T次的分类cls任务。bs是一个批量,里面有T个char。
  没有val,直接用predict预测后面50个char看看长什么样子

#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
                use_random_iter=False):
    """训练模型(定义⻅第8章)"""
    loss = nn.CrossEntropyLoss()
    animator = d2l.Animator(xlabel='epoch', ylabel='perplexity',
                            legend=['train'], xlim=[10, num_epochs])
    # 初始化
    if isinstance(net, nn.Module):
        updater = torch.optim.SGD(net.parameters(), lr)
    else:
        updater = lambda batch_size: d2l.sgd(net.params, lr, batch_size)
    predict = lambda prefix: predict_ch8(prefix, 50, net, vocab, device)
    # 训练和预测
    for epoch in range(num_epochs):
        ppl, speed = train_epoch_ch8(
                    net, train_iter, loss, updater, device, use_random_iter)
        if (epoch + 1) % 10 == 0:
            print(predict('time traveller'))
            animator.add(epoch + 1, [ppl])
    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('time traveller'))
    print(predict('traveller'))
num_epochs, lr = 500, 1
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu())

'''
困惑度 1.2, 63602.5 词元/秒 cuda:0
time travellerit s against reason said filbywhat thas se sho ls 
travellerit s against reason said filbywhat thas se sho ls 
'''

2.简洁版RNN

2.1同样的数据载入

batch_size, num_steps = 32, 35
train_iter, vocab = d2l.load_data_time_machine(batch_size, num_steps)

2.2模型定义

一层循环神经网络的输出被用作下一层循环神经网络的输入

num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)
state = torch.zeros((1, batch_size, num_hiddens))
state.shape

'''
torch.Size([1, 32, 256])
'''

  这里的X经过rnn得到的Y,输出的是(T,bs,hiddens),不涉及层的运算,指每个时间步的隐状态。
  state尺寸为(隐藏层数,bs,hidden),是最后一个时间步的隐状态Ht。

X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape

'''
(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))
'''

  Y是所有每一个时间步的隐状态H,类似于从0实现里面rnn中的全部的H!!!

  state是最后一个时间步的。

  时间步T,在图中表现为输入的X有几个。

  torch里面的rnnlayer只包括隐藏层,不包括输出层。所以调用rnn_layer的时候,要构造一个输出层Linear.

#@save
class RNNModel(nn.Module):
    """循环神经⽹络模型"""
    def __init__(self, rnn_layer, vocab_size, **kwargs):
        super(RNNModel, self).__init__(**kwargs)
        self.rnn = rnn_layer
        self.vocab_size = vocab_size
        self.num_hiddens = self.rnn.hidden_size
        # 如果RNN是双向的(之后将介绍),num_directions应该是2,否则应该是1
        if not self.rnn.bidirectional:
            self.num_directions = 1
            self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
        else:
            self.num_directions = 2
            self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)
            
    def forward(self, inputs, state):
        X = F.one_hot(inputs.T.long(), self.vocab_size)
        X = X.to(torch.float32)
        Y, state = self.rnn(X, state)
        # 全连接层⾸先将Y的形状改为(时间步数*批量⼤⼩,隐藏单元数)
        # 它的输出形状是(时间步数*批量⼤⼩,词表⼤⼩)。即(bs*T,len(v))
        output = self.linear(Y.reshape((-1, Y.shape[-1])))
        return output, state
    
    def begin_state(self, device, batch_size=1):
        if not isinstance(self.rnn, nn.LSTM):
            # nn.GRU以张量作为隐状态
            return torch.zeros((self.num_directions * self.rnn.num_layers,
                                batch_size, self.num_hiddens),
                                device=device)
        else:
            # nn.LSTM以元组作为隐状态
            return (torch.zeros((
                self.num_directions * self.rnn.num_layers,
                batch_size, self.num_hiddens), device=device),
                torch.zeros((
                    self.num_directions * self.rnn.num_layers,
                    batch_size, self.num_hiddens), device=device))

2.3训练

device = d2l.try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
num_epochs, lr = 500, 1
d2l.train_ch8(net, train_iter, vocab, lr, num_epochs, device)

'''
perplexity 1.3, 259910.4 tokens/sec on cuda:0
time travellerisus aits a berthat this is so extensivel is suld 
traveller came back andfilby s anecdote cowe prengtwercare 
'''

总结:简洁版代替了Rnn的定义与初始权重的定义。预测与训练是一致的。

3.rnn总结一下输入输出维度

  送入for X,T in train_iter里面的X,Y均为(bs,T)
  X直接送入net会先经过one-hot变成(T,bs,V)
  经过net后得到的y_hat为(T*bs,V)
  在net中,如果是调用nn.RNN(len(vocab),num_hiddens),则通过RNN得到的Y尺寸为(T,bs,hiddens),本质上是所有时间步的隐层,需要再接一个LInear(h,V)得到输出y_hat为(T*bs,V)
  如果是从零实现,则得到的每一个Y为(bs,V),再通过dim=0的累加得到y_hat为(T*bs,V)
  然后在于y(经过转置为(T*bs))进行交叉熵计算loss。

你可能感兴趣的:(文件处理,rnn,人工智能,深度学习)