IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 人工智能 -> 从零实现深度学习框架——前馈网络语言模型 -> 正文阅读

[人工智能]从零实现深度学习框架——前馈网络语言模型

引言

本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。

要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不使用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。

我们前面学习过n-gram语言模型,但是n-gram模型中的n不能太大,这限制了它的应用。本文我们学习如何基于前馈神经网络来构建语言模型。

前馈网络语言模型

我们知道语言模型是基于前面的上下文单词来预测下一个单词。

前馈网络语言模型1的架构图如上图所示。分为四层:

  • 输入层(Input Layer)
  • 投影层(Projection Layer)
  • 隐藏层(Hidden Layer)
  • 输出层(Output Layer)

输入是每个单词的独热编码。类似N-gram,神经概率语言模型也有一个窗口。根据投影层(嵌入层)得到窗口大小的嵌入向量。然后拼接这些嵌入向量,经过一个隐藏层,激活函数是 tanh \text{tanh} tanh。然后经过 softmax \text{softmax} softmax得到给定上下文窗口下预测下一个单词的概率分布。

我们具体来看一看。

前向推理

和Word2vec一样,也有时间步的概念。在每个时间步,假设词典大小为 ∣ V ∣ |V| V,给定窗口大小。我们使用长度为 ∣ V ∣ |V| V的独热编码表示 N N N个之前的单词。

假设 N = 3 N=3 N=3,我们这里就有三个独热编码。

image-20220530132019805

如上图所示,在时间步 t t t,都有3个独热编码,大小为 ∣ V ∣ × 3 |V| \times 3 V×3,分别乘上嵌入矩阵 E ∈ R d × ∣ V ∣ E \in \Bbb R^{d \times |V|} ERd×V。得到3个大小为 d × 1 d \times 1 d×1的嵌入向量。

image-20220530133258238

假设某个独热编码只有索引为5处的元素为1,那么乘上嵌入矩阵后,相当于选择了嵌入矩阵的第5列,这种操作我们已经在word2vec中见过了。

然后我们把这三个嵌入向量拼接起来得到 e ∈ R 3 d × 1 e \in \Bbb R ^{3d \times 1} eR3d×1,这就是投影层的结果。然后嵌入向量 e e e乘以一个权重矩阵 W ∈ R d h × 3 d W \in \Bbb R^{d_h \times 3d} WRdh?×3d d h d_h dh?为隐藏层大小,得到隐藏状态 h ∈ R d h × 1 h \in \Bbb R^{d_h \times 1} hRdh?×1。然后经过 tanh ? \tanh tanh激活函数,维度不变。最后乘以矩阵 U ∈ R ∣ V ∣ × d h U \in \Bbb R^{|V| \times d_h} URV×dh?,得到维度为 ∣ V ∣ × 1 |V| \times 1 V×1的向量,经过 softmax \text{softmax} softmax得到了整个词典内所有单词的概率分布。

总结一下,上面例子中使用的算法如下:

  1. 从嵌入矩阵 E E E中选择三个嵌入向量:给定三个之前的单词,查看索引,得到3个独热编码向量,然后乘上矩阵 E E E。假设 w t ? 3 w_{t-3} wt?3?代表索引35处单词for的独热编码,乘上矩阵 E E E,得到 d × 1 d \times 1 d×1的嵌入向量。即索引 i i i处的独热编码经过 E x i = e i Ex_i=e_i Exi?=ei?。然后拼接3个单词嵌入,得到 3 d × 1 3d \times 1 3d×1的嵌入 e e e
  2. 乘上 W W W:我们再乘上 W W W(可能加上对应偏置),经过激活函数( tanh ? \tanh tanh ReLU \text{ReLU} ReLU)得到隐藏向量 h h h
  3. 乘上 U U U h h h接着乘上 U U U,将其维度扩展成和词典大小一致。
  4. 应用 softmax \text{softmax} softmax:在经过 softmax \text{softmax} softmax,得到了词典中每个单词作为下一个单词的概率 P ( w t = i ∣ w t ? 1 , w t ? 2 , w t ? 3 ) P(w_t=i|w_{t-1},w_{t-2},w_{t-3}) P(wt?=iwt?1?,wt?2?,wt?3?)

其对应的公式为:
e = [ E x t ? 3 ; E x t ? 2 ; E x t ? 1 ] h = σ ( W e + b ) z = U h y ^ = softmax ( z ) (1) \begin{aligned} e &= [E_{x_{t-3}}; E_{x_{t-2}};E_{x_{t-1}}] \\ h &= σ(We+b) \\ z &= Uh \\ \hat y &= \text{softmax}(z) \end{aligned} \tag 1 ehzy^??=[Ext?3??;Ext?2??;Ext?1??]=σ(We+b)=Uh=softmax(z)?(1)
我们用 ; ; ;来表示拼接。我们已经知道了前向推理的过程,那么如何定义损失函数呢?

损失函数

显然,这是一个多分类问题。一般使用交叉熵损失函数。

首先,当我们有多类时,我们需要将 y y y y ^ \hat y y^?都表示为向量。假设我们处理硬分类,即只有一个正确类别。真实标签 y y y就是一个个数为 K K K的独热编码,如果真实类别为 c c c,那么只有 y c = 1 y_c=1 yc?=1。而我们的分类器会产生同样 K K K个元素的估计向量 y ^ \hat y y^?,每个元素 y ^ k \hat y_k y^?k?代表估计概率 p ( y k = 1 ∣ x ) p(y_k = 1|x) p(yk?=1x)

对于单个样本的损失函数就是 K K K个输出类别概率的负对数之和,通过类别对应的真实概率 y k y_k yk?加权:
L C E ( y ^ , y ) = ? ∑ k = 1 K y k log ? y ^ k (2) L_{CE}(\hat y, y) = - \sum^K_{k=1} y_k \log \hat y_k \tag 2 LCE?(y^?,y)=?k=1K?yk?logy^?k?(2)
我们可以进一步地简化该公式。我们先使用 1 { } \Bbb{1}\{\} 1{}重写,如果括号内的条件为真那么返回 1 1 1,否则返回 0 0 0?。那么可以得到:
L C E ( y ^ , y ) = ? ∑ k = 1 K 1 { y k = 1 } log ? y ^ k (3) L_{CE}(\hat y,y) = -\sum_{k=1}^K \Bbb{1}\{y_k=1\} \log \hat y_k \tag 3 LCE?(y^?,y)=?k=1K?1{yk?=1}logy^?k?(3)
只有真实类别 y k = 1 y_k=1 yk?=1的时候才不为 0 0 0?。换言之,交叉熵损失就是简单的负对数正确类别的输出概率,我们称为负对数似然损失:
L C E ( y ^ , y ) = ? log ? y ^ c c 是正确类别 (4) L_{CE}(\hat y,y) = -\log \hat y_c \quad \text{$c$是正确类别} \tag 4 LCE?(y^?,y)=?logy^?c?c是正确类别(4)
通过softmax函数展开,并有 K K K个类别时:
L C E ( y ^ , y ) = ? log ? exp ( z c ) ∑ j = 1 K exp ? ( z j ) c 是正确类别 (5) L_{CE}(\hat y,y) = -\log \frac{\text{exp}(z_c)}{\sum_{j=1}^K \exp(z_j)} \quad \text{$c$是正确类别} \tag 5 LCE?(y^?,y)=?logj=1K?exp(zj?)exp(zc?)?c是正确类别(5)
下面来看如何训练前馈网络语言模型。

训练

我们需要的参数 θ = E , W , U , b \theta=E,W,U,b θ=E,W,U,b

训练的目的是得到嵌入矩阵 E E E,我们就可以像使用word2vec中的嵌入矩阵一样,使用该矩阵 E E E来获取每个单词的嵌入向量。

image-20220530145202201

还是上面的例子,给定上下文for all the来预测下一个单词fish的概率。

如上小节所介绍的,我们使用交叉熵(负对数似然)损失:
L C E ( y ^ , y ) = ? log ? y ^ c c 是正确类别 (6) L_{CE}(\hat y,y) = -\log \hat y_c \quad \text{$c$是正确类别} \tag 6 LCE?(y^?,y)=?logy^?c?c是正确类别(6)
对于语言模型来说,类别就是词典中的所有单词,所以 y ^ c \hat y_c y^?c?意味着模型分配给正确单词 w t w_t wt?的概率:
L C E = ? log ? p ( w t ∣ w t ? 1 , ? ? , w t ? n + 1 ) (7) L_{CE} = -\log p(w_t|w_{t-1},\cdots,w_{t-n+1}) \tag 7 LCE?=?logp(wt?wt?1?,?,wt?n+1?)(7)
我们希望模型分配给正确单词的概率越高越好,通过最小化上面的损失来更新参数 θ \theta θ

代码实现

数据集

数据还是西游记数据集。

数据集下载 → 提取码:nap4

class NGramDataset(Dataset):
    def __init__(self, corpus, vocab, window_size=4):
        self.data = []
        self.bos = vocab[BOS_TOKEN]
        self.eos = vocab[EOS_TOKEN]
        for sentence in tqdm(corpus, desc="Dataset Construction"):
            # 插入句首句尾符号
            sentence = [self.bos] + sentence + [self.eos]
            if len(sentence) < window_size:
                continue
            for i in range(window_size, len(sentence)):
                # 模型输入:长为context_size的上文
                context = sentence[i - window_size:i]
                # 模型输出:当前词
                target = sentence[i]
                self.data.append((context, target))

        self.data = np.asarray(self.data)

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

    def __getitem__(self, i):
        return self.data[i]

    def collate_fn(self, examples):
        # 从独立样本集合中构建batch输入输出
        inputs = Tensor([ex[0] for ex in examples])
        targets = Tensor([ex[1] for ex in examples])
        return inputs, targets

模型

创建前馈网络语言模型类2,参数主要包括词嵌入层、词嵌入到隐藏层、隐藏层到输出层。

class FeedForwardNNLM(nn.Module):
    def __init__(self, vocab_size, embedding_dim, window_size, hidden_dim):
        # 单词嵌入E : 输入层 -> 嵌入层
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        # 词嵌入层 -> 隐藏层
        self.e2h = nn.Linear(window_size * embedding_dim, hidden_dim)
        #  隐藏层 -> 输出层
        self.h2o = nn.Linear(hidden_dim, vocab_size)

        self.activate = F.relu

    def forward(self, inputs) -> Tensor:
        embeds = self.embeddings(inputs).reshape((inputs.shape[0], -1))
        hidden = self.activate(self.e2h(embeds))
        output = self.h2o(hidden)
        log_probs = F.log_softmax(output, axis=1)
        return log_probs

训练

if __name__ == '__main__':
    embedding_dim = 64
    window_size = 2
    hidden_dim = 128
    batch_size = 1024
    num_epoch = 10
    min_freq = 3  # 保留单词最少出现的次数

    # 读取文本数据,构建FFNNLM训练数据集(n-grams)
    corpus, vocab = load_corpus('../../data/xiyouji.txt', min_freq)
    dataset = NGramDataset(corpus, vocab, window_size)
    data_loader = DataLoader(
        dataset,
        batch_size=batch_size,
        collate_fn=dataset.collate_fn,
        shuffle=True
    )

    # 负对数似然损失函数
    nll_loss = NLLLoss()
    # 构建FFNNLM,并加载至device
    device = cuda.get_device("cuda:0" if cuda.is_available() else "cpu")

    model = FeedForwardNNLM(len(vocab), embedding_dim, window_size, hidden_dim)
    model.to(device)

    optimizer = SGD(model.parameters(), lr=0.001)

    total_losses = []
    for epoch in range(num_epoch):
        total_loss = 0
        for batch in tqdm(data_loader, desc=f"Training Epoch {epoch}"):
            inputs, targets = [x.to(device) for x in batch]
            optimizer.zero_grad()
            log_probs = model(inputs)
            loss = nll_loss(log_probs, targets)
            loss.backward()
            optimizer.step()
            total_loss += loss.item()
        print(f"Loss: {total_loss:.2f}")
        total_losses.append(total_loss)

    # 保存词向量(model.embeddings)
    save_pretrained(vocab, model.embeddings.weight.data, "ffnnlm.vec")

完整代码

https://github.com/nlp-greyfoss/metagrad

References


  1. A Neural Probabilistic Language Model ??

  2. 自然语言处理:基于预训练模型的方法 ??

  人工智能 最新文章
2022吴恩达机器学习课程——第二课(神经网
第十五章 规则学习
FixMatch: Simplifying Semi-Supervised Le
数据挖掘Java——Kmeans算法的实现
大脑皮层的分割方法
【翻译】GPT-3是如何工作的
论文笔记:TEACHTEXT: CrossModal Generaliz
python从零学(六)
详解Python 3.x 导入(import)
【答读者问27】backtrader不支持最新版本的
上一篇文章      下一篇文章      查看所有文章
加:2022-06-03 23:58:52  更:2022-06-03 23:58:57 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 4:30:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码