经典循环神经网络(一)RNN及其在歌词数据集上的应用

news2024/11/28 4:44:56

经典循环神经网络(一)RNN及其在歌词数据集上的应用

1 RNN概述

在深度学习兴起之前,NLP领域一直是统计模型的天下,例如词对齐算法GIZA++,统计机器翻译开源框架MOSES等等。在语言模型方向,n-gram是当时最为流行的语言模型方法。n-gram的问题是其捕捉句子中长期依赖的能力非常有限。另外n-gram算法过于简单,其是否有能力取得令人信服的效果的确要打一个大的问号。

循环神经网络是为更好地处理时序信息而设计的。它引入状态变量来存储过去的信息,并⽤其与当前的输入共同决定当前的输出
循环神经网络常用于处理序列数据,如⼀段文字或声音、购物或观影的顺序,甚⾄是图像中的⼀行或⼀列像素。因此,循环神经网络有着极为广泛的实际应用,如语言模型、文本分类、机器翻译、语音识别、图像分析等。

1.1 RNN理解

下面图像,来自台大李宏毅老师课件

1.1.1 RNN的引入

对于2句话,都有Taipei这个词,但是一个是目的地一个是出发地

如果神经网络有记忆力,能够根据上下文对同样的input词汇产生不同的输出,我们就能解决这个问题

像下面两句话,同样输入Taipei,一个输出“目的地”,一个输出“出发地”

arrive Taipei on November 2

leave Taipei on November 2

在这里插入图片描述

1.1.2 结合例子理解

在这里插入图片描述

首先,网络接收第一个单词(arrive)的输入,经过网络得到一个输出,并保存隐藏层的输出。

然后接收第二个单词(Taipei)的输入,经过网络得到输出,……,用同样的网络结构不断重复这个行为

所以当两句不同的话输入的时候,一个Taipei前面是leave,一个是arrive,而这两个的vector是不一样的,所以存在memory中的值不同,这样就会得到不同的输出。

在这里插入图片描述

更一般地,网络可以不止有一个隐藏层,而是有许多个隐藏层,每个单词输入的时候,各个隐藏层都会考虑之前存在memory中的值。

在这里插入图片描述

1.2 RNN网络架构

在这里插入图片描述

上图展示了循环神经网络在三个相邻时间步的计算逻辑。

其中:
隐状态中 X t W x h + H t − 1 W h h 的计算,相当于 X t 和 H t − 1 的拼接与 W x h 和 W h h 的拼接的矩阵乘法。 隐状态中 X_tW_{xh} + H_{t-1}W_{hh}的计算,相当于X_t和H_{t-1}的拼接与W_{xh}和W_{hh}的拼接的矩阵乘法。 隐状态中XtWxh+Ht1Whh的计算,相当于XtHt1的拼接与WxhWhh的拼接的矩阵乘法。
这里使用代码验证一下:

在这里插入图片描述

RNN的实现非常简单,如下:

'''
定义了如何在【⼀个时间步内】计算隐状态和输出。

循环神经⽹络模型通过inputs最外层的维度实现循环,以便逐时间步更新⼩批量数据的隐状态H。
此外,这⾥使⽤tanh函数作为激活函数。
'''
def rnn(inputs, state, params):
    """
    :param inputs: inputs的形状:(时间步数量,批量⼤⼩,词表⼤⼩)
    :param state:  隐状态
    :param params: 初始化的权重及偏置参数
    :return:
    """
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # X的形状:(批量⼤⼩,词表⼤⼩)
    for X in inputs:
        # 隐藏层激活函数是tanh。
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)  # 隐变量state
        # 输出层没有激活函数,就是隐变量的线性变换
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)

2 RNN在歌词数据集上的应用

2.1 歌词数据集的预处理

歌词数据集下载地址:https://gitee.com/inkiinki/data20201205/blob/master/Data20201205/jaychou_lyrics.txt.zip

2.1.1 读取歌词数据集

这是⼀个相当小的语料库,但足够我们测试,现实中的文档集合可能会包含数十亿个单词。

import collections
import random
import torch
import re

# 读取歌词数据集
def read_jaychou(file_loc = '../data/jaychou_lyrics.txt'):
    with open(file=file_loc, mode='r', encoding='utf8') as f:
        lines = f.readlines()
        return lines
lines = read_jaychou()
print(f'⽂本总⾏数: {len(lines)}')
print(lines[0])
print(lines[1])
⽂本总⾏数: 5819
想要有直升机
想要和你飞到宇宙去

2.1.2 词元化

tokenize函数将文本行列表(lines)作为输入,列表中的每个元素是⼀个文本序列(如⼀条文本

行)。每个文本序列又被拆分成⼀个词元列表,词元(token)是文本的基本单位。最后,返回⼀个由词元列

表组成的列表。这里为了方便,一个中文字为一个词元。

def tokenize(lines):
    tokens = []
    for line in lines:
        line = line.replace("\n", " ").replace("\r", " ")
        fileters = ['!', '"', '#', '$', '%', '&', '\(', '\)', '\*', '\+', ',', '-', '\.', '/', ':', ';', '<', '=', '>',
                    '\?', '@', '\[', '\\', '\]', '^', '_', '`', '\{', '\|', '\}', '~', '\t', '\n', '\x97', '\x96', '”',
                    '“', ]
        line = re.sub("|".join(fileters), "", line)
        tokens.append(list(line))
    return tokens

在这里插入图片描述

2.1.3 词表

  • 我们先将训练集中的所有文档合并在⼀起,对它们的唯⼀词元进行统计,得到的统计结果称之为语料(corpus)。

  • 然后根据每个唯⼀词元的出现频率,为其分配⼀个数字索引。很少出现的词元通常被移除,这可以降低复杂性。

  • 另外,语料库中不存在或已删除的任何词元都将映射到⼀个特定的未知词元“”。

  • 我们可以选择增加⼀个列表,用于保存那些被保留的词元,例如:填充词元(“”);序列开始词元(“”);序列结束词元(“”)。

def count_corpus(tokens):
    """统计词元的频率"""
    # 这⾥的tokens是1D列表或2D列表
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # 将词元列表展平成⼀个列表
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)


class Vocab:
    """⽂本词表类"""
    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # 按出现频率排序
        counter = count_corpus(tokens)
        self._token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True)
        # 未知词元的索引为0
        self.idx_to_token = ['<unk>'] + reserved_tokens
        self.token_to_idx = {token: idx for idx, token in enumerate(self.idx_to_token)}

        for token, freq in self._token_freqs:
            if freq < min_freq:
                break
            if token not in self.token_to_idx:
                self.idx_to_token.append(token)
                self.token_to_idx[token] = len(self.idx_to_token) - 1

        self.vocab_size = len(self.idx_to_token)

    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
vocab = Vocab(tokens)

print(vocab.vocab_size)
print(vocab.idx_to_token[:10])
print(list(vocab.token_to_idx.items())[:10])

在这里插入图片描述

最后,将上面3步进行封装

def load_corpus_jaychou(max_tokens=-1):
    """返回歌词数据集的词元索引列表和词表"""
    lines = read_jaychou()
    tokens = tokenize(lines)
    vocab = Vocab(tokens)
    # 将所有文本行展平到一个列表中
    corpus = [vocab[token] for line in tokens for token in line]
    if max_tokens > 0:
        corpus = corpus[:max_tokens]
    return corpus, vocab

在这里插入图片描述

2.1.4 读取序列数据

随机采样

'''
随机采样:

在随机采样中,每个样本都是在原始的⻓序列上任意捕获的⼦序列。

1、在迭代过程中,来⾃两个相邻的、随机的、⼩批量中的⼦序列不⼀定在原始序列上相邻。
例如:
[15., 16., 17., 18., 19.] 和  [5., 6., 7., 8., 9.]

2、对于语⾔建模,⽬标是基于到⽬前为⽌我们看到的词元来预测下⼀个词元,因此标签label是移位了⼀个词元的原始序列。
例如:
[15., 16., 17., 18., 19.]对应的标签label是[16., 17., 18., 19., 20.]


下⾯的代码每次可以从数据中随机⽣成⼀个⼩批量。
参数batch_size指定了每个⼩批量中⼦序列样本的数⽬
参数num_steps是每个⼦序列中预定义的时间步数
'''
def seq_data_iter_random(corpus, batch_size, num_steps):
    """使⽤随机抽样⽣成⼀个⼩批量⼦序列"""
    # 从随机偏移量开始对序列进⾏分区,随机范围包括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)

在这里插入图片描述

顺序分区

'''
顺序分区:

在迭代过程中,除了对原始序列可以随机抽样外,我们还可以保证
两个相邻的⼩批量中的⼦序列在原始序列上也是相邻的。

例如:
[ 0.,  1.,  2.,  3.,  4.] 和 [ 5.,  6.,  7.,  8.,  9.]
'''
def seq_data_iter_sequential(corpus, batch_size, num_steps):
    """使用顺序分区生成⼀个小批量子序列"""
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = torch.tensor(corpus[offset: offset + num_tokens])
    Ys = torch.tensor(corpus[offset + 1: offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i: i + num_steps]
        Y = Ys[:, i: i + num_steps]
        yield X, Y

在这里插入图片描述

将上⾯的两个采样函数包装到⼀个类中,以便稍后可以将其用作数据迭代器

class SeqDataLoader:
    """加载序列数据的迭代器"""
    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = seq_data_iter_random
        else:
            self.data_iter_fn = seq_data_iter_sequential

        self.corpus, self.vocab = load_corpus_jaychou(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)

将预处理全部进行封装

def load_data_jaychou(batch_size, num_steps, 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

2.2 RNN的简单实现

2.2.1 加载歌词数据集

import torch
from torch import nn
import math
from _0_Vocab import load_data_jaychou
batch_size, num_steps = 32, 35
train_iter, vocab = load_data_jaychou(batch_size, num_steps)

2.2.2 封装通用的RNN模型

import torch.nn as nn
import torch
from torch.nn import functional as F


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):
        # inputs的shape为(batch_size,num_steps)
        # one-hot编码后,shape变为 (num_steps, batch_size, 词表大小)
        X = F.one_hot(inputs.T.long(), self.vocab_size)
        X = X.to(torch.float32)

        # Y的shape(num_steps, batch_size, 隐藏单元数)
        # state的shape(num_layers, batch_size, 隐藏单元数)
        Y, state = self.rnn(X, state)
        # 全连接层
        # 首先,将Y的形状改为(时间步数*批量⼤⼩, 隐藏单元数)
        # 它的输出形状是(时间步数*批量⼤⼩,词表大小)。
        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)
                    )


if __name__ == '__main__':
    # 1、构造⼀个具有256个隐藏单元的单隐藏层的循环神经⽹络层rnn_layer
    num_hiddens = 256
    rnn_layer = nn.RNN(input_size=28, hidden_size=num_hiddens,num_layers=1,bidirectional=True)
    # 2、构建rnn模型,词表大小为28
    net = RNNModel(rnn_layer, vocab_size=28)
    print(net)
    # 3、构建测试数据
    num_steps = 10
    batch_size = 5
    x = torch.rand(size=(batch_size, num_steps))
    state = net.begin_state(batch_size=x.shape[0], device='cpu')

    print(net(x, state)[0].shape)
    print(net(x, state)[1].shape)

RNNModel(
  (rnn): RNN(28, 256, bidirectional=True)
  (linear): Linear(in_features=512, out_features=28, bias=True)
)
torch.Size([50, 28])
torch.Size([2, 5, 256])

2.2.3 定义rnn模型

num_hiddens = 256

# 构造⼀个具有256个隐藏单元的单隐藏层的循环神经网络层rnn_layer
rnn_layer = nn.RNN(len(vocab), num_hiddens)
# 使用张量来初始化隐状态,
# 它的形状是(隐藏层数,批量大小,隐藏单元数)
state = torch.zeros((1, batch_size, num_hiddens))
print(state.shape)


'''
通过⼀个隐状态和⼀个输⼊,我们就可以⽤更新后的隐状态计算输出。

需要强调的是,rnn_layer的“输出”(Y)不涉及输出层的计算:
    它是指每个时间步的隐状态,这些隐状态可以⽤作后续输出层的输⼊。
'''
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape
torch.Size([1, 32, 256])

(torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))
from _0_RNNModel import RNNModel

def try_gpu(i=0):
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f'cuda:{i}')
    return torch.device('cpu')

device = try_gpu()

net = RNNModel(rnn_layer, vocab_size=vocab.vocab_size)
net = net.to(device)

在训练前,先预测一波

'''
首先,net(get_input(), state)目的就是用【前一次预测得到的输出】和【前一次时刻的隐藏状态】 得到 【当前时刻的输出和当前时刻的隐藏状态】。


然后第一次for循环,意在得到一个比较准确的,可供进行prefix这句话以后的字的预测的一个初始时刻隐藏状态,所以使用了完整的一句话去得到。
比如说 `你好不好` 是一句话,传入作为 `prefix` ,然后用 `你` 初始化 `outputs` 作为第0时刻的输出,
    首先预热整句话,从第1时刻开始,循环 `好不好` 来获得初始隐藏状态 `state` ,这期间连续的更新state直至最后一个 `好` 字;
    第1时刻默认用0时刻输出 `你` 和0时刻默认的 `state` 输入模型得到预测结果和1时刻的隐藏状态 `state` ,
    不用接收模型使用 `你` 预测得到的结果,因为标准答案 `好` 可以直接作为下一时刻的输入。
    剩下的循环以此类推。


所以第二个for循环,才是正式做预测,预测 `你好不好` 这句话的后面 `num_preds` 个词是什么,
  所以初始时刻就传入 `你好不好` 的最后一个词 `好` 和之前预热的 `state` 隐藏状态,得到第4时刻的预测结果 `y` 和当前时刻隐藏状态。
'''
def predict_ch(prefix, num_preds, net, vocab, device):
    """在prefix后⽣成新字符"""
    state = net.begin_state(batch_size=1, device=device)

    outputs = [ vocab[str(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_ch('分开', 10, net, vocab, device)
'分开万枯幅枯幅掉氧枯氧枯'

2.2.4 模型的训练

梯度裁剪

对于长度为T的序列,我们在迭代中计算这T个时间步上的梯度,将会在反向传播过程中产生长度为O(T)的

矩阵乘法链。当T较大时,它可能导致数值不稳定,例如可能导致梯度爆炸或梯度消失。因此,

循环神经网络模型往往需要额外的方式来支持稳定训练。梯度裁剪提供了⼀个快速修复梯度爆炸的方法。

在这里插入图片描述

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

    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm

评价指标——困惑度

我们可以通过⼀个序列中所有的n个词元的交叉熵损失的平均值来衡量。

由于历史原因,自然语言处理的科学家更喜欢使用⼀个叫做困惑度(perplexity)的量。其实,就是交叉熵损失的平均值的指数。

• 在最好的情况下,模型总是完美地估计标签词元的概率为1。在这种情况下,模型的困惑度为1。

• 在最坏的情况下,模型总是预测标签词元的概率为0。在这种情况下,困惑度是正无穷大。

from AccumulatorClass import Accumulator
from AnimatorClass import Animator
from TimerClass import Timer

'''
循环神经⽹络模型的训练函数既⽀持从零开始实现,也可以使⽤⾼级API来实现。

1. 序列数据的不同采样⽅法(随机采样和顺序分区)将导致隐状态初始化的差异。
2. 在更新模型参数之前裁剪梯度。这样的操作的⽬的是:即使训练过程中某个点上发⽣了梯度爆炸,也能保证模型不会发散。
3. ⽤困惑度来评价模型。
'''
def train_epoch(net, train_iter, loss, updater, device, use_random_iter):
    """训练网络⼀个迭代周期"""
    state, timer = None, Timer()
    metric = 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:
            # 在训练函数中,如果没有用torch提供的优化器,就先不进行梯度清零
            # 自定义的updater。在更新参数后,也清零了。
            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 sgd(params, lr, batch_size):
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()

def train(net, train_iter, vocab, lr, num_epochs, device,use_random_iter=False):
    """训练模型"""
    loss = nn.CrossEntropyLoss()
    animator = 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: sgd(net.params, lr, batch_size)


    predict = lambda prefix: predict_ch(prefix, 50, net, vocab, device)
    # 训练和预测
    for epoch in range(num_epochs):
        ppl, speed = train_epoch(net, train_iter, loss, updater, device, use_random_iter)

        if (epoch + 1) % 10 == 0:
            print(predict('分开'))
            animator.add(epoch + 1, [ppl])

    print(f'困惑度 {ppl:.1f}, {speed:.1f} 词元/秒 {str(device)}')
    print(predict('分开'))
    print(predict('不分开'))
num_epochs, lr = 1000, 1
train(net, train_iter, vocab, lr, num_epochs, device)

在这里插入图片描述

2.3 RNN的手动实现

2.3.1 加载歌词数据集

import torch
from torch import nn
import math
from _0_Vocab import load_data_jaychou
from torch.nn import functional as F
batch_size, num_steps = 32, 35
train_iter, vocab = load_data_jaychou(batch_size, num_steps)

2.3.2 初始化模型参数

'''
隐藏单元数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)  # 随机初始化为0

    # 输出层参数
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)  # 随机初始化为0

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

2.3.3 创建rnn模型

'''
init_rnn_state函数在初始化时返回隐状态。

这个函数的返回是⼀个张量,张量全⽤0填充,形状为(批量⼤⼩,隐藏单元数)
'''
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )


'''
定义了如何在【⼀个时间步内】计算隐状态和输出。

循环神经⽹络模型通过inputs最外层的维度实现循环,以便逐时间步更新⼩批量数据的隐状态H。
此外,这⾥使⽤tanh函数作为激活函数。
'''
def rnn(inputs, state, params):
    """
    :param inputs: inputs的形状:(时间步数量,批量⼤⼩,词表⼤⼩)
    :param state:  隐状态
    :param params: 初始化的权重及偏置参数
    :return:
    """
    W_xh, W_hh, b_h, W_hq, b_q = params
    H, = state
    outputs = []
    # X的形状:(批量⼤⼩,词表⼤⼩)
    for X in inputs:
        # 隐藏层激活函数是tanh。
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)  # 隐变量state
        # 输出层没有激活函数,就是隐变量的线性变换
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)

封装通用函数

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):
        '''
         如果第0维是批量大小,那么假如每次取一个样本 即(x1 x2 x3...)
         这样根据rnn计算规则,先算预测的x2 然后后才能算x3, 这样只能一个个算,不便于程序的并行执行。

         如果第0纬换成了时间序列,那么每拿一个出来,得到的就是(第一个x1 第二个x1...)这些是可以并行计算的,
         转置一下的好处就是把能够并行计算的东西 使得他们在空间存储上尽量相邻 提高计算效率。
        '''
        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)
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, try_gpu(), get_params,init_rnn_state, rnn)

2.3.4 预测

预测函数和2.2一致

# 鉴于我们还没有训练⽹络,它会⽣成荒谬的预测结果。
predict_ch('分开', 10, net, vocab, try_gpu())

2.3.5 模型的训练

训练函数和2.2一致

# 因为我们在数据集中只使⽤了10000个词元,所以模型需要更多的迭代周期来更好地收敛。
num_epochs, lr = 1000, 1
train(net, train_iter, vocab, lr, num_epochs, try_gpu())

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1055757.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YoloV8改进策略:SPD-Conv加入到YoloV8中,让小目标无处遁形

摘要 SPD-Conv是一种新的构建块,用于替代现有的CNN体系结构中的步长卷积和池化层。它由一个空间到深度(SPD)层和一个非步长卷积(Conv)层组成。 空间到深度(SPD)层的作用是将输入特征图的每个空间维度降低到通道维度,同时保留通道内的信息。这可以通过将输入特征图的每…

关于日志系统

目录 日志落地类&#xff08;工厂模式&#xff09;双缓冲区异步处理器缓冲区异步工作器 日志器类同步日志器异步日志器构造日志器构造局部日志器构造全局日志器日志器管理器&#xff08;单例模式&#xff09; 日志宏&全局接口&#xff08;代理模式&#xff09; 源码&#x…

使用 cURL 发送 HTTP 请求: 深入探讨与示例

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Vue3之Suspense

<Suspense> 是一个内置组件&#xff0c;用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成&#xff0c;并可以在等待时渲染一个加载状态。 我们可以看到官网并不推荐我们使用它&#xff0c;目前仍处于测试中。 他用于加…

【算法系列篇】哈希表

文章目录 前言1. 两数之和1.1 题目要求1.2 做题思路1.3 Java代码实现 2. 判断是否为字符重排2.1 题目要求2.2 做题思路2.3 Java代码实现 3. 存在重复元素3.1 题目要求3.2 做题思路3.3 Java代码实现 4. 存在重复元素II4.2 题目要求4.2 做题思路4.3 Java代码实现 5. 字母异位词分…

Guitar Pro 8 .1全新功能介绍及2023官方特惠优惠券

《中国好声音》节目诞生10年多热度不减&#xff0c;每一季都有籍籍无名的学员成为万众瞩目的新星。怎么像他们一样把爱好变成事业&#xff1f;带着这个问题在不断的探寻中找到了答案&#xff0c;那就是要在有限的时间里比别人做效率更高的事。所谓“工欲善其事&#xff0c;必先…

超百家上市公司抛出回购、增持计划

9月以来&#xff0c;多家上市公司披露回购方案或增持计划&#xff0c;持续向市场传递积极信号&#xff0c;以真金白银提振市场信心。 Wind统计显示&#xff0c;截至9月28日&#xff0c;9月以来已有77家上市公司披露回购预案&#xff0c;其中多家公司发布超过亿元的回购计划。 …

WINDOWS与LINUX的文件文件共享

打开VMware: 点击虚拟机->点击设置 出来虚拟机设置&#xff0c;咱们点击选项->有个共享文件夹点击->选择总是启用->点击添加 下一步 此处选择一个windows下与虚拟机共享的一个目录 最后确定就ok了 那怎么在虚拟机Linux中访问共享文件呐 在文件的其他位置选择计…

SpringMVC如何处理表单提交与文件上传

SpringMVC处理表单提交与文件上传 SpringMVC是一个流行的Java框架&#xff0c;用于构建Web应用程序。它提供了强大的功能来处理表单提交和文件上传操作。本文将深入探讨SpringMVC如何处理这些常见的Web任务&#xff0c;以及如何使用示例代码来实现它们。 表单提交处理 表单提…

基于 SpringBoot+Vue 的企业人事管理系统

1 简介 本文讲解的是 Java基于 SpringBoot 的人事管理系统。本系统涉到的功能主要有&#xff1a;首页&#xff0c;个人中心&#xff0c;员工管理&#xff0c;部门管理&#xff0c;员工考勤管理&#xff0c;请假申请管理&#xff0c;加班申请管理&#xff0c;员工工资管理&…

cesium 鹰眼图2

cesium 鹰眼图2 1、实现方法 本文采用cesium 和 leaflet 来时实现,鹰眼采用leaflet来实现 2、示例代码 2.1 <!DOCTYPE html> <html><head><meta charset="utf-8">

【图像融合】差异的高斯:一种简单有效的通用图像融合方法[用于融合红外和可见光图像、多焦点图像、多模态医学图像和多曝光图像](Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

mathtype如何嵌入到word中?详细mathtype安装步骤教程

mathtype是一款功能特别强大的数学方式编辑软件&#xff0c;为用户提供各种强大的数学公式符号帮助用户进行计算&#xff0c;并且速度很快。有小伙伴知道mathtype如何嵌入到word中吗&#xff0c;这里小编就给大家详细介绍一下mathtype嵌入到word中的方法&#xff0c;有需要的小…

第81步 时间序列建模实战:Adaboost回归建模

基于WIN10的64位系统演示 一、写在前面 这一期&#xff0c;我们介绍AdaBoost回归。 同样&#xff0c;这里使用这个数据&#xff1a; 《PLoS One》2015年一篇题目为《Comparison of Two Hybrid Models for Forecasting the Incidence of Hemorrhagic Fever with Renal Syndr…

循环语句

章节目录&#xff1a; 一、while 循环1.1 句式与基本使用1.2 while...else1.3 单行语句 二、for 循环2.1 句式与基本使用2.2 for...else2.3 range() 函数 三、退出循环3.1 break3.2 continue 四、pass 语句五、结束语 一、while 循环 1.1 句式与基本使用 句式&#xff1a; w…

【进阶C语言】自定义类型

本节内容大致目录如下&#xff1a; 1.结构体 2.位段 3.枚举 4.联合&#xff08;共用体&#xff09; 以上都是C语言中的自定义类型&#xff0c;可以根据我们的需要去定义。 一、结构体 一些基础知识在初阶C语言的时候已经介绍过&#xff0c;在这里粗略概括&#xff1b;重…

C++基于Qt中QOpenGLWidget模块实现的画图板源码+可执行文件

基于Qt中QOpenGLWidget模块实现的画图板 一、系统概述 本系统拟完成一个画图板&#xff0c;对多种常见图形进行基本操作系统功能 二维图形的输入&#xff1a;可输入或全部清除直线、矩形、圆、椭圆、多边形、文本等二维图形的变换&#xff1a;在直线、矩形、圆、椭圆、多边形…

(七)Flask之路由转换器

引子&#xff1a; from flask import Flaskapp Flask(__name__)# 通过使用<int>转换器&#xff0c;可以捕获URL中的整数值&#xff0c;并将其作为参数传递给视图函数。 app.route(/index/<int:nid>, methods[GET, POST]) def index(nid):print(nid)return Indexi…

基于SpringBoot的知识管理系统

目录 前言 一、技术栈 二、系统功能介绍 用户管理 文章分类 资料分类 文章信息 论坛交流 资料下载 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#xff0c;针对这个问题开发一个…