深度学习(二):CNN & RNN

深度学习(二):CNN & RNN

卷积神经网络

从全连接层到卷积(引入卷积的必要性)

计算机视觉的神经网络架构需要满足两个特性:

  1. 平移不变性:神经网络的前面几层应该对相同的图像区域具有相似的反应。
  2. 局部性:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。

对于输入X和其隐藏表示H,用\([X]_{i,j}\)\([H]_{i,j}\)来表示在位置\((i,j)\)处的像素。在原来的多层感知机中,\([H]_{i,j}\)会依赖于X中的所有元素值,即:

image-20230225163905639

其中k和l可以看做是遍历整个二维输入矩阵X。可以等价地写成i+a和j+b表示距离(i,j)的偏移。

那么为了满足平移不变性,意味着检测对象在输入X中的平移,应该仅导致隐藏表示H中的平移。因此有:

image-20230225163702199

这就是卷积,其中a和b分别是偏移量,\(V\)可以看做是卷积层的权重矩阵。

为了满足局部性原则,则应该给偏移a和b划定范围,即:

image-20230225164117463

这就是一个卷积层(convolutional layer),而卷积神经网络是包含卷积层的一类特殊的神经网络。\(V\)被称为卷积核(convolution kernel)或者滤波器(filter),亦或简单地称之为该卷积层的权重,通常该权重是可学习的参数。

由于图像一般具有颜色,那么它就是一个由高度、宽度和颜色组成的三维张量。因此X表示为\([X]_{i,j,k}\),卷积也表示成\([V]_{a,b,c}\)。由于输入图像是三维的,我们的隐藏表示H也最好采用三维张量。 换句话说,对于每一个空间位置,我们想要采用一组而不是一个隐藏表示。这样一组隐藏表示可以想象成一些互相堆叠的二维网格。 因此,我们可以把隐藏表示想象为一系列具有二维张量的通道(channel)。

为了支持输入X和隐藏表示H中的多个通道,由于H的每个值是由X的所有值来得到的,我们可以在V中添加第四个坐标,即\([V]_{a,b,c,d}\)。d表示H的输出通道。

image-20230225165707987

图像卷积

卷积运算(互相关运算)的图例:

image-20230225170855838

假设输入大小为\((in_h,in_w)\),卷积核大小为\((k_h,k_w)\),则卷积运算后输出的大小为\((in_h-k_h-1, in_w-k_w-1)\)。因为必须有足够的空间才能保证卷积运算,所以输出大小会改变。

卷积运算代码:

import torch
from torch import nn
from d2l import torch as d2l

def corr2d(X, K):  #@save
    """计算二维互相关运算"""
    h, w = K.shape
    Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1)) 
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
    return Y

卷积层

卷积层中的两个被训练的参数是卷积核权重和标量偏置。

class Conv2D(nn.Module):
    def __init__(self, kernel_size):
        super().__init__()
        self.weight = nn.Parameter(torch.rand(kernel_size))
        self.bias = nn.Parameter(torch.zeros(1))

    def forward(self, x):
        return corr2d(x, self.weight) + self.bias

卷积核的学习

# 构造一个二维卷积层,它具有1个输入通道,1个输出通道和形状为(1,2)的卷积核
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)

# 这个二维卷积层使用四维输入和输出格式(批量大小、通道、高度、宽度),
# 其中批量大小和通道数都为1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2  # 学习率

for i in range(10):
    Y_hat = conv2d(X)
    l = (Y_hat - Y) ** 2
    conv2d.zero_grad() #设置conv2d的所有模型参数的梯度为0
    l.sum().backward()
    # 迭代卷积核
    conv2d.weight.data[:] -= lr * conv2d.weight.grad
    if (i + 1) % 2 == 0:
        print(f'epoch {i+1}, loss {l.sum():.3f}')

填充和步幅

填充

在应用多层卷积时,我们常常丢失边缘像素。 解决这个问题的简单方法即为填充(padding):在输入图像的边界填充元素(通常填充元素是0)。 一般设置填充的行数和列数为\(k_h-1\)\(k_w-1\),保证输出与输入大小一致。

对于任何二维张量X,当满足:

  1. 卷积核的大小是奇数;

  2. 所有边的填充行数和列数相同

  3. 输出与输入具有相同高度和宽度

则可以得出:输出Y[i, j]是通过以输入X[i, j]为中心,与卷积核进行互相关计算得到的。

# 请注意,这里每边都填充了1行或1列,因此总共添加了2行或2列
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))

步幅

conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)

多输入多输出通道

多输入单输出

一个具有两个输入通道的二维互相关运算的示例:

image-20230225192802937

上图中卷积核的形状为\(c_i\times k_h\times k_w\),其中\(c_i\)是输入通道数,每个通道各自执行互相关操作,然后将结果相加。

def corr2d_multi_in(X, K):
    # 先遍历“X”和“K”的第0个维度(通道维度),再把它们加在一起
    return sum(d2l.corr2d(x, k) for x, k in zip(X, K))

多输入多输出

多个输出通道,和多个输入通道的做法并不相同。它的卷积核的形状为\(c_o\times c_i\times k_h\times k_w\),其中\(c_o\)为输出通道数。该卷积核的每个形状为\(c_i\times k_h\times k_w\)的子卷积核分别与输入张量做 多输入单输出的卷积运算,得到一系列输出张量,然后将这些输出张量沿着第0维拼接,得到最终的输出。

代码实例:

def corr2d_multi_in_out(X, K):
    # 迭代“K”的第0个维度,每次都对输入“X”执行互相关运算。
    # 最后将所有结果都叠加在一起
    return torch.stack([corr2d_multi_in(X, k) for k in K], 0)

K = torch.stack((K, K + 1, K + 2), 0) #torch.Size([3, 2, 2, 2])
corr2d_multi_in_out(X, K)
#结果为:
tensor([[[ 56.,  72.],
         [104., 120.]],

        [[ 76., 100.],
         [148., 172.]],

        [[ 96., 128.],
         [192., 224.]]])

1$\times$1卷积层

主要用于通道上,使得输出中的每个元素都是从输入图像中同一位置的元素的线性组合。

image-20230225194159742

上图输入通道数为3,输出通道数为2,则卷积核的形状为\(2\times 3\times 1\times 1\)

代码实现:

def corr2d_multi_in_out_1x1(X, K):
    c_i, h, w = X.shape
    c_o = K.shape[0]
    X = X.reshape((c_i, h * w))
    K = K.reshape((c_o, c_i))
    # 全连接层中的矩阵乘法
    Y = torch.matmul(K, X)
    return Y.reshape((c_o, h, w))
    
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)

汇聚层

汇聚(pooling)层,它具有双重目的:降低卷积层对位置的敏感性,同时降低对空间降采样表示的敏感性。

汇聚层的运算和卷积运算类似,也是滑动求值。但它通常计算汇聚窗口中所有元素的最大值或平均值。这些操作分别称为最大汇聚层(maximum pooling)和平均汇聚层(average pooling)。

手动代码实例:

import torch
from torch import nn
from d2l import torch as d2l

def pool2d(X, pool_size, mode='max'):
    p_h, p_w = pool_size
    Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
    for i in range(Y.shape[0]):
        for j in range(Y.shape[1]):
            if mode == 'max':
                Y[i, j] = X[i: i + p_h, j: j + p_w].max()
            elif mode == 'avg':
                Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
    return Y

默认情况下,深度学习框架中的步幅与汇聚窗口的大小相同

pool2d = nn.MaxPool2d(3) #步幅默认为3
pool2d = nn.MaxPool2d(3, padding=1, stride=2) 
pool2d = nn.MaxPool2d((2, 3), stride=(2, 3), padding=(0, 1))

在处理多通道输入数据时,汇聚层在每个输入通道上单独运算,而不是像卷积层一样在通道上对输入进行汇总。 这意味着汇聚层的输出通道数与输入通道数相同。

LeNet

总体来看,LeNet(LeNet-5)由两个部分组成:

  • 卷积编码器:由两个卷积层组成;
  • 全连接层密集块:由三个全连接层组成。
net = nn.Sequential(
    nn.Conv2d(1, 6, kernel_size=5, padding=2), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Conv2d(6, 16, kernel_size=5), nn.Sigmoid(),
    nn.AvgPool2d(kernel_size=2, stride=2),
    nn.Flatten(),
    nn.Linear(16 * 5 * 5, 120), nn.Sigmoid(),
    nn.Linear(120, 84), nn.Sigmoid(),
    nn.Linear(84, 10))

X = torch.rand(size=(1, 1, 28, 28), dtype=torch.float32)
for layer in net:
    X = layer(X)
    print(layer.__class__.__name__,'output shape: \t',X.shape)
  • 为了构造高性能的卷积神经网络,我们通常对卷积层进行排列,逐渐降低其表示的空间分辨率,同时增加通道数。

循环神经网络

文本处理

词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。让我们构建一个字典,通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从0开始的数字索引中。

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 = []
        # 按出现频率排序
        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

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):  #vocab[tokens],返回该token对应的索引
        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 count_corpus(tokens):  #@save
    """统计词元的频率,返回一个字典,键为词元,值为词元出现的频次"""
    # 这里的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)

下面的函数返回corpus(文本中按顺序的词元对应的索引组成的列表)和vocab(时光机器语料库的词表)。

def load_corpus_time_machine(max_tokens=-1):  #@save
    """返回时光机器数据集的词元索引列表和词表"""
    lines = read_time_machine()
    tokens = tokenize(lines, 'char') #一个个词元
    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

语言模型和数据集

语言模型的目标就是,给定一个序列数据,去预测它的下一个时间步的词元。

很长的文本序列需要划分,假设模型可以一次处理长度为n(时间步为n)的序列,每个时间步对应一个词元,则划分一个长的文本序列有两种策略:随机采样和顺序分区策略。

随机采样

在随机采样中,每个样本都是在原始的长序列上任意捕获的子序列。 对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。

下面的代码每次可以从数据中随机生成一个小批量。 在这里,参数batch_size指定了每个小批量中子序列样本的数目, 参数num_steps是每个子序列中预定义的时间步数。

def seq_data_iter_random(corpus, batch_size, num_steps):  #@save
    """使用随机抽样生成一个小批量子序列,一个小批量里有batch_size个子序列,每个子序列中有num_steps步"""
    corpus = corpus[random.randint(0, num_steps - 1):] #随机找个偏移量先,重置corpus。corpus是文本中一个个按顺序排列的词元对应的索引列表
    # 减去1,是因为我们需要考虑标签
    num_subseqs = (len(corpus) - 1) // num_steps #可以划分的子序列数量
    # 长度为num_steps的子序列的起始索引,由于corpus已经被重置了,corpus[0]对应的就是第一个子序列的第一个词元
    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 #能有多少个batch
    for i in range(0, batch_size * num_batches, batch_size):
        # 在这里,initial_indices包含一个batch的子序列们的随机起始索引
        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)

随机采样的随机体现在:小批量内、小批量间的子序列排列都是没有顺序的。

顺序分区

顺序分区使得每个小批量间的对应未知的子序列是连续的。小批量内不连续,但是有固定的位置差

def seq_data_iter_sequential(corpus, batch_size, num_steps):  #@save
    """使用顺序分区生成一个小批量子序列"""
    # 从随机偏移量开始划分序列
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size # corpus的词元总数(可以被batch_size整除,因为下面要进行reshape,不行的话会报错)
    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  #Xs的每行能划分成多少个子序列
    for i in range(0, num_steps * num_batches, num_steps):
        #X的每行是一个子序列,子序列的长度为num+steps。总共有batch_size行,因此是一个小批量
        X = Xs[:, i: i + num_steps]
        Y = Ys[:, i: i + num_steps]
        yield X, Y

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

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)#corpus是索引列表
        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_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

循环神经网络RNN

循环神经网络加入了隐状态。在n元语法模型, 其中单词\(x_t\)在时间步t的条件概率仅取决于前面n−1个单词。如果我们保存所有这样的单词,模型参数数量会很大,因此我们引入隐状态,使用隐变量模型,即:

image-20230226164649629

\(h_{t-1}\)即隐状态,存储了到时间步t-1的序列信息。它可以通过下式进行计算:

image-20230226164806251

在循环神经网络中,它的计算为:

image-20230226165517453

其中\(X_t\in R^{n\times d}\),也就是说,小批量内有n个序列样本,每个长度为d。\(H_t\)形状为\((n,h)\),n表示batch_size,h为num_hiddens。

对于时间步t,它的输出层可以用下面来表示:

image-20230226165747116

整个RNN可以用下图来表示:

image-20230226165927374

困惑度

这是一个衡量语言模型质量的指标。它计算公式为:

image-20230226171205732

其中P是有语言模型给出的概率,\(x_t\)是实际在时间步t观测到的词元。

从头实现RNN

采用独热编码预处理数据

由于词表中的每个词元都是由一个索引数字表示的,这样加大了学习难度。因此我们还是采用独热编码来表示每个词元。将每个索引映射为相互不同的单位向量,例如针对词表vocab,将0和2用独热编码来表示:

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

由于小批量数据是二维张量,即(批量大小,时间步数)。one_hot函数将这样一个小批量数据转换成三维张量,新加的第三维是某一时间步下的词元对应的索引对应的独热编码,长度就是词表长度。

初始化模型参数

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

RNN网络模型

  • 在初始化时返回隐状态
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device), )
  • rnn函数
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,)
  • 用类包装模型
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)
  • 预测,首先针对给定的prefix进行预热,计算好最终的\(H_t\),再进行预测。
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)

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])

梯度裁剪

对于长度为T的时间序列来说,我们需要迭代计算T个时间步上的梯度,这样可能会导致出现梯度爆炸或梯度消失等情况。因此我们可以通过将梯度g投影回给定半径 (例如\(\theta\))的球来裁剪梯度g,避免梯度爆炸:

\[g\leftarrow min(1,\frac{\theta}{||g||})\times g \]

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

训练

训练时,随机抽样和顺序分区的采样方法对应的state的初始化略有不同。顺序分区可以使得state接着使用,而随机采样则需要多次初始化。

  • 一个迭代周期的训练函数
#@save
def train_epoch_ch8(net, train_iter, loss, updater, device, use_random_iter):
    """训练网络一个迭代周期"""
    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)
        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()
  • RNN的训练函数
#@save
def train_ch8(net, train_iter, vocab, lr, num_epochs, device,
              use_random_iter=False):
    """训练模型"""
    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'))

net = RNNModelScratch(len(vocab), num_hiddens, d2l.try_gpu(), get_params,
                      init_rnn_state, rnn)
train_ch8(net, train_iter, vocab, lr, num_epochs, d2l.try_gpu(),
          use_random_iter=True)

Pytorch实现RNN

首先定义一个隐藏单元为256的rnn层,其输入特征数和前面保持一致,是独热编码数。rnn_layer的“输出”(Y)不涉及输出层的计算: 它是指每个时间步的隐状态,这些隐状态可以用作后续输出层的输入。

num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

我们使用张量来初始化隐状态,它的形状是(隐藏层数,批量大小,隐藏单元数)。

state = torch.zeros((1, batch_size, num_hiddens))
  • 一个完整的循环神经网络模型
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) #.T是将(batch_size, num_steps)的输入转置
        X = X.to(torch.float32)
        Y, state = self.rnn(X, state) #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))
  • 训练
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)
posted @ 2023-02-26 21:45  KouweiLee  阅读(167)  评论(0编辑  收藏  举报