为了小论文之跟着李沐学AI(十七)

从0开始实现RNN模型

F.one_hot(torch.tensor([0,2]),len(vocab))

对于一个one-hont编码理解
他会返回一个 字典长度的向量,每一个元素表示一个词,如果是这个词,就表示为1

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

X的转置,表示,这个minibatch是一个5句话(5个样本点),每一个样本点有两个词,需要的就是把这两个词映射成one-hot编码

def get_params(vocab_size,num_hiddens,device):
  # 获得参数
  def normal(shape):
    return torch.randn(size=shape,device=deivce) * 0.01
  
  #隐藏层参数
  W_xh = normal((num_inputs,num_hiddens))
  W_hh = normal((num_hiddens,numhiddens))   #这一行就RNN最牛逼的地方,记录了上一层的隐藏变量
  b_h = torch.zeros(num_hiddens,device=device)

  #输出参数
  W_hq = normal((num_hiddens,num_outputs))
  b_q = otrch.zeros(num_outputs,device=deivce)

  #附加梯度
  params = [W_xh,W_hh,b_h,W_hq,b_q]
  
  for param in params:
    param.requires_grad_(True)

  return params


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

这个函数还是帮助我们初始化RNN的参数的
定义的normal函数就是一个辅助函数,给定shape,就给出一个shape

num_inputs = num_outputs = vocab_size,我们理解一下,对于我们的输入虽然是一个句子,但是都被one-hot映射成了vocab size大小的向量,而且我们的输出也是预测,这个词是字典中的某个词,做一个多分类的问题,所以我的输出维度还是一个长度为vocab_size的向量

如果光看W_xh和 b_h,那么这个就算是一个最最最简单,最最最基础的多层感知机,什么都不是,RNN不一样的地方就是加上了W_hh,这个是记录隐藏层的信息

W_hq 输出层的参数 b_q
对于输出层,可以理解为一个全连接层一个Linear,输入输出的维度是(vocabsize,vocabsize)

最后对于参数都要开启梯度记录,用于反向传播

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

初始化一个隐藏状态

def rnn(inputs,state,params):
  # input的形状(时间步数量,批量大小,vocab的大小)
  W_xh,W_hh,b_h,W_hq,b_q = get_params
  H, = state 
  outputs = []
  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,)

进行一个rnn的函数
第一步,先把参数展开
第二步,获得一个初始状态,这个状态记录了之前的时序信息
对于这个循环,然后你你看一看到,我们是先更新H
在这里得出一个结论 Yn = output(Hn),就是我发现 Yn 和 Hn竟然是这样的关系,两者的差距就是隔了一个全连接层,啊这

class RNNModelScratch:  #现在只是实现一个类去包装它而已
    """从零开始实现的循环神经网络模型"""
    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) #定义call () 会自己调用这个函数,就是线转换成one-hot编码,然后再用RNN
        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的转置?
拿本例来说,我们的输入是一个 2528的矩阵。2是batchsize可以理解为有几句话,5是timestep可以理解为有几个词,28是vocab_size
再进入rnn这个函数里面看
如果不转置(首先我们先忽略H0是一个228的矩阵)
5
28 点乘 28* 28 + H0*Whh = H1,我们可以看到这个H1记录的是第一句话的信息,但是第一句话和第二句话是独立的,这个和我们的需求是违背的。我们的需求是,拿一句话的第n个词预测以及第n个词之前的历史信息,预测n+1个词。
如果转置 2 * 28,那记录的就是词于词的关系而不是句子与句子的关系了

初始化的没什么好说的
begin_state会初始化隐藏层的状态

def predict_ch8(prefix,num_preds,net,vocab,device):
  # prefix 预测的起始句
  # num_preds 需要预测字符的数量
  # net 传入我们的网络
  # vocab 字典的长度
  # device 就是装置
  state = net.begin_state(batch_size=1,device=device)
  outputs = [vocab[prefix[0]]]  #有待商榷
  print([outputs[-1]])
  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])

prefix 预测的起始句
num_preds 需要预测字符的数量
net 传入我们的网络
vocab 字典的长度
device 就是装置
第一步更新隐藏状态
第二步装入待预测字符的第一个
第三步,这是一个匿名函数,自动获取output中最后一个字符
第四步,进行一个预热期,这个预热期主要预热的是隐藏状态,让隐藏状态达到一个更好的标准
第五步开始预测,每次传进去一个字符,然后预测下一个字符
最后把output中的索引转换成字符串

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) # 把y拉成一条向量
        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()
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'))

RNN的简洁实现

class RNNModel(nn.Module):
  #循环神经网络模型
  def __init__(self,rnn_layer,vocab_size,**kwargs):
    super().__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:  #这一块是为了RNN是不是双向设计的
        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)

    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:
      return (torch.zeros((self.num_directions * self.rnn.num_layers,atch_size, self.num_hiddens), device=device),torch.zeros((
                  self.num_directions * self.rnn.num_layers,
                  batch_size, self.num_hiddens), device=device))
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值