【pytorch】循环神经网络

发布于:2025-02-11 ⋅ 阅读:(53) ⋅ 点赞:(0)

  • 序列信息是带顺序的数据。
  • 循环神经网络可以更好地处理序列信息。它通过引入状态变量存储过去的信息和当前的输入,从而可以确定当前的输出

1. 序列模型

  • 问题模型:根据历史值 x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt1,,x1预测 x t x_t xt的值,即
    x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t\sim P(x_t|x_{t-1},\dots,x_1) xtP(xtxt1,,x1)
  • 为了实现这个预测,可以使用回归模型。但存在一个主要问题:输入数据( x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt1,,x1)的数量与 t t t有关,即输入数据的数量随着数据量的增加而增加。有以下2种策略可以进行近似处理:
    • 自回归模型:假设在现实情况下相当长的序列 x t − 1 , … , x 1 x_{t-1},\dots,x_1 xt1,,x1是不必要的,我们只需要满足某个长度为 τ \tau τ的时间跨度, 即使用观测序列 x t − 1 , … , x t − τ x_{t-1},\dots,x_{t-\tau} xt1,,xtτ。这样获得的最直接的好处就是参数的数量总是不变的
    • 隐变量自回归模型:保留一些对过去观测的总结 h t − 1 h_{t-1} ht1,并且同时更新预测 x ^ t \hat{x}_t x^t和总结 h t h_{t} ht,即
      x ^ t = P ( x t ∣ h t ) ,     h t = g ( h t − 1 , x t − 1 ) \hat{x}_t=P(x_t|h_t),~~~h_t=g(h_{t-1},x_{t-1}) x^t=P(xtht),   ht=g(ht1,xt1)
      在这里插入图片描述
  • 如何生成训练数据? 经典方法是使用历史观测来预测下一个未来观测。这种方法基于以下假设:虽然特定值 x t x_t xt可能会改变, 但是序列产生逻辑不变。即
    P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 , … , x 1 ) P(x_1,\dots,x_T)=\prod_{t=1}^T P(x_t|x_{t-1},\dots,x_1) P(x1,,xT)=t=1TP(xtxt1,,x1)
    这种假设是合理的,因为新的序列产生逻辑一定受新的数据影响, 而我们不可能用目前所掌握的数据来预测新的序列产生逻辑
  • 如果使用 x t − 1 , … , x t − τ x_{t-1},\dots,x_{t-\tau} xt1,,xtτ来估计 x t x_t xt是近似精确的,则称序列满足马尔可夫条件。特别地,如果 τ = 1 \tau=1 τ=1,得到一个 一阶马尔可夫模型
    P ( x 1 , … , x T ) = ∏ t = 1 T P ( x t ∣ x t − 1 ) , P ( x 1 ∣ x 0 ) = P ( x 1 ) P(x_1,\dots,x_T)=\prod_{t=1}^T P(x_t|x_{t-1}),P(x_1|x_0)=P(x_1) P(x1,,xT)=t=1TP(xtxt1),P(x1x0)=P(x1)
  • 当假设 x t x_t xt仅是离散值时,可以使用动态规划沿着马尔可夫链精确地计算结果,简而言之,可以利用临近值的概率求解跨距离值的概率,由此我们只需要考虑历史临近值。比如
    P ( x t + 1 ∣ x t − 1 ) = ∑ x t P ( x t + 1 , x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t , x t − 1 ) P ( x t , x t − 1 ) P ( x t − 1 ) = ∑ x t P ( x t + 1 ∣ x t ) P ( x t , x t − 1 ) P ( x t − 1 ) ( 马尔可夫条件 ) = ∑ x t P ( x t + 1 ∣ x t ) P ( x t ∣ x t − 1 ) \begin{aligned} P(x_{t+1}|x_{t-1})&=\frac{\sum_{x_t}P(x_{t+1},x_t,x_{t-1})}{P(x_{t-1})}\\ &=\frac{\sum_{x_t}P(x_{t+1}|x_t,x_{t-1})P(x_t,x_{t-1})}{P(x_{t-1})}\\ &=\frac{\sum_{x_t}P(x_{t+1}|x_t)P(x_t,x_{t-1})}{P(x_{t-1})}(马尔可夫条件)\\ &=\sum_{x_t}P(x_{t+1}|x_t)P(x_t|x_{t-1}) \end{aligned} P(xt+1xt1)=P(xt1)xtP(xt+1,xt,xt1)=P(xt1)xtP(xt+1xt,xt1)P(xt,xt1)=P(xt1)xtP(xt+1xt)P(xt,xt1)(马尔可夫条件)=xtP(xt+1xt)P(xtxt1)
  • 在许多情况下,序列数据存在一个自然的方向,在时间上是前进的,未来的事件不能影响过去,因而序列数据具有因果性
  • 下一时间步的预测称为单步预测,下 k k k时间步的预测称为k步预测。k步预测必须使用我们自己的预测(而不是原始数据)来进行,由于预测错误累积,可能会相当快地偏离真实的观测结果。因此当我们试图预测更远的未来时,一旦超过某个跨度,任何预测几乎都是无用的

2. 文本预处理

  • 文本是典型的序列数据,计算机不方便直接处理文本,需要预处理转换成方便计算机处理的数字索引序列。常见的文本预处理步骤如下:
    1. 将文本作为字符串加载到内存中。
    2. 将字符串拆分为词元(如单词和字符)。
    3. 建立一个词表,将拆分的词元映射到数字索引。
    4. 将文本转换为数字索引序列,方便模型操作。
  • 收集的各种文本数据称为语料,为了方便机器处理,需要将语料中的词元替换为对应的数字索引。
    • 出现频率很低的词元通常被移除,这可以降低复杂性。
    • 不存在或已删除的任何词元都将映射到一个特定的未知词元“<unk>”
    • 可以选择增加一个列表,用于保存那些被保留的词元,如填充词元(“<pad>”); 序列开始词元(“<bos>”); 序列结束词元(“<eos>”)。
  • 文本预处理代码实现(时间机器下载
import re, collections

def readlines(file):
    """将UTF-8文件内容加载到文本行的列表中"""
    with open(file, "r", encoding="utf-8") as f:
        lines = f.readlines()
    # 为简单起见,忽略标点符号,并将所有字母转换为小写
    return [re.sub("[^A-Za-z]+", " ", line).strip().lower() for line in lines]


def tokenize(lines: list[str], token="word"):
    """将文本行拆分为单词或字符词元"""
    if token == "word":
        return [line.split() for line in lines]
    elif token == "char":
        return [list(line) for line in lines]
    else:
        raise Exception(f"不支持的词元类型:{token}")


class Vocab:
    """文本词表"""

    def __init__(
        self,
        tokens: list[list[str]],
        min_freq: int = 0,
        reserved_tokens: list[str] = [],
    ):
        # 按频率降序排序
        self.__token_freqs = sorted(
            self.count_tokens(tokens).items(), key=lambda x: x[1], reverse=True
        )

        self.__ordered_tokens = ["<unk>"] + reserved_tokens
        for token, freq in self.__token_freqs:
            if freq < min_freq:
                break
            if token not in self.__ordered_tokens:
                self.__ordered_tokens.append(token)

        self.__idx_of_tokens = {
            token: idx for idx, token in enumerate(self.__ordered_tokens)
        }

    def __len__(self):
        """词表大小"""
        return len(self.__ordered_tokens)

    def count_tokens(self, tokens: list[list[str]]):
        """统计词元的频率"""
        tokens = [token for line in tokens for token in line]
        return collections.Counter(tokens)

    def to_index(self, tokens: list[str] | tuple[str] | str):
        """根据词元获取索引"""
        if isinstance(tokens, str):
            return self.__idx_of_tokens.get(tokens, self.idx_of_unk)
        return [self.to_index(token) for token in tokens]

    def to_token(self, indices: list[int] | int):
        """根据索引获取词元"""
        if isinstance(indices, int):
            return self.__ordered_tokens[indices]
        return [self.__ordered_tokens[index] for index in indices]

    @property
    def idx_of_unk(self):
        """未知词元的索引"""
        return 0

    @property
    def token_freqs(self):
        """词元频率"""
        return self.__token_freqs
    
    @property
    def content(self):
        """词表内容"""
        return list(self.__idx_of_tokens.items())


def load_corpus(max_tokens: int = None):
    """加载文本训练语料"""
    lines = readlines("time_machine.txt")
    tokens = tokenize(lines, "char")
    vocab = Vocab(tokens)
    corpus = [vocab.to_index(token) for line in tokens for token in line]
    if max_tokens is not None:
        corpus = corpus[:max_tokens]
    return corpus, vocab


if __name__ == "__main__":
    corpus, vocab = load_corpus()
    print(len(corpus), len(vocab))
    print(vocab.token_freqs[:10])
    print(vocab.content[:10])

3. 语言模型

  • 假设长度为 T T T的文本序列中的词元依次为 x 1 , x 2 , … , x T x_1,x_2,\dots,x_T x1,x2,,xT,在给定这样的文本序列时,语言模型的目标是估计序列的联合概率 P ( x 1 , x 2 , … , x T ) P(x_1,x_2,\dots,x_T) P(x1,x2,,xT)
  • 只需要一次抽取一个词元 x t ∼ P ( x t ∣ x t − 1 , … , x 1 ) x_t\sim P(x_t|x_{t-1},\dots,x_1) xtP(xtxt1,,x1),即根据模型估计的概率分布抽取第一个词元 x 1 x_1 x1,然后基于 x 1 x_1 x1抽取第二个词元 x 2 x_2 x2,依此类推,一个理想的语言模型就能够基于模型本身生成自然文本。
  • 为了训练语言模型,我们需要计算单词的概率, 以及给定前面几个单词后出现某个单词的条件概率。 这些概率本质上就是语言模型的参数
    • 估计单词的概率可以根据给定词的相对词频来计算
    • 估计给定几个单词后出现某个单词的条件概率使用连续单词对出现频率之比的方法存在问题:连续单词对出现频率很低,很难估计条件概率。处理这种问题的常见策略是执行某种形式的拉普拉斯平滑, 具体方法是在所有计数中添加一个小常量。例如:( n n n表示训练集中单词的总数, m m m表示唯一单词的数量)
      P ^ ( x ) = n ( x ) + ϵ 1 / m n + ϵ 1 P ^ ( x ′ ∣ x ) = n ( x , x ′ ) + ϵ 2 P ^ ( x ′ ) n + ϵ 2 P ^ ( x ′ ′ ∣ x , x ′ ) = n ( x , x ′ , x ′ ′ ) + ϵ 3 P ^ ( x ′ ′ ) n + ϵ 3 \begin{aligned} \hat{P}(x)&=\frac{n(x)+\epsilon_1/m}{n+\epsilon_1}\\ \hat{P}(x'|x)&=\frac{n(x,x')+\epsilon_2 \hat{P}(x')}{n+\epsilon_2}\\ \hat{P}(x''|x,x')&=\frac{n(x,x',x'')+\epsilon_3 \hat{P}(x'')}{n+\epsilon_3}\\ \end{aligned} P^(x)P^(xx)P^(x′′x,x)=n+ϵ1n(x)+ϵ1/m=n+ϵ2n(x,x)+ϵ2P^(x)=n+ϵ3n(x,x,x′′)+ϵ3P^(x′′)其中 ϵ 1 , ϵ 2 , ϵ 3 \epsilon_1,\epsilon_2,\epsilon_3 ϵ1,ϵ2,ϵ3是超参数,取值控制平滑程度。当 ϵ i = 0 \epsilon_i=0 ϵi=0时,不应用平滑,当 ϵ i → + ∞ \epsilon_i\rightarrow+\infin ϵi+时, P ^ ( x ) \hat{P}(x) P^(x)接近 1 / m 1/m 1/m P ^ ( x ′ ∣ x ) \hat{P}(x'|x) P^(xx)接近 P ^ ( x ′ ) \hat{P}(x') P^(x) P ^ ( x ′ ′ ∣ x , x ′ ) \hat{P}(x''|x,x') P^(x′′x,x)接近 P ^ ( x ′ ′ ) \hat{P}(x'') P^(x′′),即越与上下文没有关系。
      然而,这样的模型很容易变得无效
      • 需要存储所有的计数。
      • 完全忽略了单词的意思,想根据上下文调整这类模型其实是相当困难的。
      • 长单词序列大部分是没出现过的。
  • 如果 P ( x t + 1 ∣ x t , … , x 1 ) = P ( x t + 1 ∣ x 1 ) P(x_{t+1}|x_t,\dots,x_1)=P(x_{t+1}|x_1) P(xt+1xt,,x1)=P(xt+1x1),则序列上的分布满足一阶马尔可夫性质。 阶数越高,对应的依赖关系就越长。利用这种性质可以推导出许多可以应用于序列建模的近似公式:
    P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ) P ( x 3 ) P ( x 4 ) P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 2 ) P ( x 4 ∣ x 3 ) P ( x 1 , x 2 , x 3 , x 4 ) = P ( x 1 ) P ( x 2 ∣ x 1 ) P ( x 3 ∣ x 1 , x 2 ) P ( x 4 ∣ x 2 , x 3 ) \begin{aligned} P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2)P(x_3)P(x_4)\\ P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2|x_1)P(x_3|x_2)P(x_4|x_3)\\ P(x_1,x_2,x_3,x_4)&=P(x_1)P(x_2|x_1)P(x_3|x_1,x_2)P(x_4|x_2,x_3)\\ \end{aligned} P(x1,x2,x3,x4)P(x1,x2,x3,x4)P(x1,x2,x3,x4)=P(x1)P(x2)P(x3)P(x4)=P(x1)P(x2x1)P(x3x2)P(x4x3)=P(x1)P(x2x1)P(x3x1,x2)P(x4x2,x3)通常,涉及一个、两个和三个变量的概率公式分别被称为 一元语法、二元语法和三元语法模型。

3.1. 自然语言统计

  • 时间机器使用频率前十的单词
[('the', 2477),
 ('and', 1311),
 ('of', 1285),
 ('i', 1268),
 ('a', 877),
 ('to', 766),
 ('in', 605),
 ('was', 554),
 ('that', 458),
 ('it', 452)]
  • 英语最流行的词看起来很无聊, 这些词通常被称为停顿词,另外词频衰减的速度相当地快,大致遵循双对数坐标图上的一条直线,即词频满足齐普夫定律
    n i ∝ 1 i α n_i \propto \frac{1}{i^\alpha} niiα1这告诉我们想要通过计数统计和平滑来建模单词是不可行的, 因为这样建模的结果会大大高估尾部单词的频率,也就是所谓的不常用单词
  • 英语除了一元语法词,单词序列也遵循齐普夫定律(也就是说有高频词组和低频词组);另外 n n n元组的词表大小并没有很大,这说明语言中存在相当多的结构, 这些结构给了我们应用模型的希望。

4. 读取长序列数据

  • 序列数据是连续的,可以是任意长的,因此需要拆分序列方便模型读取。我们可以指定任意偏移量作为初始位置,拆分任意时间步长度的序列(偏移量<时间步)。为了获得覆盖性和随机性,可以从随机偏移量开始划分序列,另外对于语言建模,目标是基于到目前为止我们看到的词元来预测下一个词元, 因此标签是移位了一个词元的原始序列。在此基础上有以下两种采样策略。
    • 随机采样:每个样本都是在原始的长序列上任意捕获的子序列,两个相邻的、随机的、小批量中的子序列不一定在原始序列上相邻。
    • 顺序分区:首次是随机偏移量的,后续要保证两个相邻的小批量中的子序列在原始序列上也是相邻的。

5. 循环神经网络

  • 使用 n n n元语法模型 P ( x t ∣ x t − 1 , … , x t − n + 1 ) P(x_t|x_{t-1},\dots,x_{t-n+1}) P(xtxt1,,xtn+1),只能考虑到前 n − 1 n-1 n1个词元的影响,如果想考虑前 n − 1 n-1 n1个词元之前的词的影响,则需要增加 n n n,由此模型参数的数量也会随之呈指数增长,因为词表需要存储 ∣ N x ∣ n |N_x|^n Nxn个词组。因而不如使用隐变量模型
    P ( x t ∣ x t − 1 , … , x 1 ) ≈ P ( x t ∣ h t − 1 ) P(x_t|x_{t-1},\dots,x_1)\approx P(x_t|h_{t-1}) P(xtxt1,,x1)P(xtht1)
    其中 h t − 1 h_{t-1} ht1是隐状态或者隐变量,它存储了到时间步 t − 1 t-1 t1的序列信息,我们可以基于当前输入 x t x_t xt和先前隐状态 h t − 1 h_{t-1} ht1 来计算时间步 t t t处的隐状态:
    h t = f ( x t , h t − 1 ) h_t=f(x_t,h_{t-1}) ht=f(xt,ht1)
  • 假设我们在时间步 t t t有小批量输入 X t ∈ R n × d \mathrm{X_t}\in \mathbb{R}^{n\times d} XtRn×d,用 H t ∈ R n × h \mathrm{H_t}\in \mathbb{R}^{n\times h} HtRn×h表示时间步 t t t的隐变量,并引入新的权重参数 W h h ∈ R h × h \mathrm{W_{hh}}\in \mathbb{R}^{h\times h} WhhRh×h来描述如何在当前时间步中使用前一个时间步的隐变量。 当前间步隐变量由当前时间步的输入与前一个时间步的隐变量一起计算得出( ϕ \phi ϕ是隐藏层的激活函数, W x h \mathrm{W_{xh}} Wxh是隐藏层的权重参数, b h \mathrm{b_h} bh是隐藏层的偏置参数)
    H t = ϕ ( X t W x h + H t − 1 W h h + b h ) \mathrm{H_t}=\phi(\mathrm{X_tW_{xh}+H_{t-1}W_{hh}+b_{h}}) Ht=ϕ(XtWxh+Ht1Whh+bh)
    从隐变量 H t \mathrm{H_t} Ht H t − 1 \mathrm{H_{t-1}} Ht1的的关系可知, 这些变量捕获并保留了序列直到其当前时间步的历史信息, 就如当前时间步下神经网络的状态或记忆。由于在当前时间步中, 隐变量使用的定义与前一个时间步中使用的定义相同, 因此隐变量的计算是循环的。于是基于循环计算的隐状态神经网络被命名为循环神经网络(recurrent neural network,RNN)。 在循环神经网络中执行循环计算的层称为循环层。直接在隐藏层中添加上一时间步的隐变量是构建循环层最常见的方法。
  • 时间步 t t t的输出层与多层感知机类似
    O t = H t W h q + b q \mathrm{O_t=H_tW_{hq}+b_q} Ot=HtWhq+bq
  • 循环神经网络的参数包括循环层的权重 W x h , W h h \mathrm{W_{xh},W_{hh}} Wxh,Whh和偏置 b h \mathrm{b_h} bh以及输出层的权重 W h q \mathrm{W_{hq}} Whq和偏置 b q \mathrm{b_q} bq。值得一提的是,即使在不同的时间步,循环神经网络也总是使用这些模型参数
  • 隐状态中
    X t W x h + H t − 1 W h h = c a t ( X t , H t − 1 , 1 ) c a t ( W x h , W h h , 0 ) \mathrm{X_tW_{xh}+H_{t-1}W_{hh}=cat(X_t,H_{t-1},1)cat(W_{xh},W_{hh},0)} XtWxh+Ht1Whh=cat(Xt,Ht1,1)cat(Wxh,Whh,0)
    即沿列(轴1)拼接矩阵 X t \mathrm{X_t} Xt H t − 1 \mathrm{H_{t-1}} Ht1,沿行(轴0)拼接矩阵 W x h \mathrm{W_{xh}} Wxh W h h \mathrm{W_{hh}} Whh,得到的两个矩阵再相乘与分别相乘再相加的结果相等。

6. 困惑度

  • 一个更好的语言模型应该能让我们更准确地预测下一个词元。 因此,它应该允许我们在压缩序列时花费更少的比特。 所以我们可以通过一个序列中所有的 n n n个词元的交叉熵损失的平均值来衡量:
    1 n ∑ t = 1 n ( − log ⁡ P ( x t ∣ x t − 1 , … , x 1 ) ) \frac{1}{n}\sum_{t=1}^{n}(-\log P(x_t|x_{t-1},\dots,x_1)) n1t=1n(logP(xtxt1,,x1))
    由于历史原因,自然语言处理的科学家更喜欢使用一个叫做困惑度的量,上式是它的指数
    exp ⁡ ( 1 n ∑ t = 1 n ( − log ⁡ P ( x t ∣ x t − 1 , … , x 1 ) ) ) \exp(\frac{1}{n}\sum_{t=1}^{n}(-\log P(x_t|x_{t-1},\dots,x_1))) exp(n1t=1n(logP(xtxt1,,x1)))
    困惑度的最好的理解是下一个词元的实际选择数的调和平均数
  • 在最好的情况下,模型总是完美地估计标签词元的概率为1。 在这种情况下,模型的困惑度为1。
  • 在最坏的情况下,模型总是预测标签词元的概率为0。 在这种情况下,困惑度是正无穷大。
  • 词元之间没有相关性的情况下,模型的预测是词表的所有可用词元上的均匀分布。 在这种情况下,困惑度等于词表中唯一词元的数量。 事实上,如果我们在没有任何压缩的情况下存储序列, 这将是我们能做的最好的编码方式。 因此,这种方式提供了一个重要的上限, 而任何实际模型都必须超越这个上限

网站公告

今日签到

点亮在社区的每一天
去签到