首页 > 其他分享 >3.2-LSTM的代码实现

3.2-LSTM的代码实现

时间:2024-07-18 15:26:30浏览次数:13  
标签:tanh 代码 boldsymbol next 3.2 np LSTM self

文章目录

1 LSTM结构整理

  1. 一个LSTM层结构如下图:包括输出门、遗忘门、新增信息的tanh节点,以及输入门:

    在这里插入图片描述

  2. 涉及如下公式:
    f = σ ( x t W x ( f ) + h t − 1 W h ( f ) + b ( f ) ) (1) \boldsymbol{f}=\sigma\left(\boldsymbol{x}_t \boldsymbol{W}_x^{(\mathrm{f})}+\boldsymbol{h}_{t-1} \boldsymbol{W}_h^{(\mathrm{f})}+\boldsymbol{b}^{(\mathrm{f})}\right) \tag{1} f=σ(xt​Wx(f)​+ht−1​Wh(f)​+b(f))(1)

    g = tanh ⁡ ( x t W x ( g ) + h t − 1 W h ( g ) + b ( g ) ) (2) \boldsymbol{g}=\tanh \left(\boldsymbol{x}_t \boldsymbol{W}_x^{(\mathrm{g})}+\boldsymbol{h}_{t-1} \boldsymbol{W}_h^{(\mathrm{g})}+\boldsymbol{b}^{(\mathrm{g})}\right) \tag{2} g=tanh(xt​Wx(g)​+ht−1​Wh(g)​+b(g))(2)

    i = σ ( x t W x ( i ) + h t − 1 W h ( i ) + b ( i ) ) (3) \boldsymbol{i}=\sigma\left(\boldsymbol{x}_t \boldsymbol{W}_x^{(\mathrm{i})}+\boldsymbol{h}_{t-1} \boldsymbol{W}_h^{(\mathrm{i})}+\boldsymbol{b}^{(\mathrm{i})}\right) \tag{3} i=σ(xt​Wx(i)​+ht−1​Wh(i)​+b(i))(3)

    o = σ ( x t W x ( o ) + h t − 1 W h ( o ) + b ( o ) ) (4) \boldsymbol{o}=\sigma\left(\boldsymbol{x}_t \boldsymbol{W}_x^{(\mathrm{o})}+\boldsymbol{h}_{t-1} \boldsymbol{W}_h^{(\mathrm{o})}+\boldsymbol{b}^{(\mathrm{o})}\right) \tag{4} o=σ(xt​Wx(o)​+ht−1​Wh(o)​+b(o))(4)

    c t = f ⊙ c t − 1 + g ⊙ i (5) \boldsymbol{c}_t=\boldsymbol{f}\odot\boldsymbol{c}_{\boldsymbol{t}-1}+\boldsymbol{g}\odot i \tag{5} ct​=f⊙ct−1​+g⊙i(5)

    h t = o ⊙ tanh ⁡ ( c t ) (6) \boldsymbol{h}_t=\boldsymbol{o}\odot\tanh(\boldsymbol{c}_t) \tag{6} ht​=o⊙tanh(ct​)(6)

  3. 前面四个公式都是两个权重矩阵和一个偏置参数;为了充分利用矩阵计算进行加速,可以将这四组矩阵(向量)合在一起,如下图所示:

    1. 对于一个LSTM层来说,由于 x t \boldsymbol{x}_t xt​和 h t − 1 \boldsymbol{h}_{t-1} ht−1​都是行向量,因此将几个仿射变换在列方向上进行堆叠和分开计算的效果是一样的;
    2. 每一个 b \boldsymbol{b} b在加法的时候都是广播到前面计算结果的每一行上,因此将四个 b \boldsymbol{b} b​在列方向堆叠也是没有问题的。

    在这里插入图片描述

    1. 整合之后的仿射变换维度变化如下图所示

      在这里插入图片描述

  4. 将四个仿射变换整合到一起,就可以将LSTM的计算图变成下面这样:

    1. 左侧部分其实又好像回到了RNN的结构图,即两个矩阵乘法+一个偏置加法;
    2. 图中的slice节点用于取出对应的结果;虽然每个权重矩阵不同,但是形状是一样的,因此取出的过程也是均等分成四份(列方向上);

    在这里插入图片描述

2 LSTM的代码实现

代码项目目录:https://1drv.ms/f/s!AvF6gzVaw0cNjpx9BAtQYGAHFT3gZA?e=fLpzP6;

代码位于:LSTM_model/LSTM.py

2.1初始化和前向传播

  1. 初始化代码如下:

    class LSTM:
        def __init__(self, Wx, Wh, b):
            '''输入参数都是整合了四个仿射变换的参数
            @param Wx: (D, 4H)
            @param Wh: (H, 4H)
            @param b: (1, 4H)'''
            self.params = [Wx, Wh, b]
            self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
            self.cache = None
    
  2. 前向计算代码如下:

    1. 和前面一样: N N N表示批处理大小, D D D表示输入数据的维度,记忆单元和隐藏状态的维数都是 H H H;
        def forward(self, x, h_prev, c_prev):
            '''
            @param x: (N, D)
            @param h_prev: (N, H)
            @param c_prev: (N, H)'''
            Wx, Wh, b = self.params
            N, H = h_prev.shape
    
            A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b # A:(N,4H)
    
            # 从整合的结果中取出对应的结果;从列方向上进行切片
            f = A[:, :H] # (N,H)
            g = A[:, H:2*H] # (N,H)
            i = A[:, 2*H:3*H] # (N,H)
            o = A[:, 3*H:] # (N,H)
            
            # 将三个门的输出转换为权重
            f = sigmoid(f) # 遗忘门  (N,H)
            i = sigmoid(i) # 输入门  (N,H)
            o = sigmoid(o) # 输出门  (N,H)
            # 计算新增记忆
            g = np.tanh(g) # (N,H)
    
            # 计算遗忘了一些信息&加入新的信息之后的当前时刻的记忆单元
            c_next = f * c_prev + g * i  # (N,H)
            # 基于新的记忆单元计算当前时刻的输出,也即隐藏状态
            h_next = o * np.tanh(c_next) # (N,H)
    
            self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
            return h_next, c_next
    

2.2反向传播

  1. 反向传播的计算图参考下图:

    在这里插入图片描述

  2. 在三个门以及一个tanh节点之前的梯度传播情况如下图所示;代码如下:

        def backward(self, dh_next, dc_next):
            '''
            @param dh_next: (N,H);水平方向上后一个时刻传递来的&垂直方向上后面的层传递来的梯度两者的累加
            @param dc_next: (N,H);水平方向上后一个时刻传递来的记忆单元的梯度'''
            Wx, Wh, b = self.params
            x, h_prev, c_prev, i, f, g, o, c_next = self.cache
    
            tanh_c_next = np.tanh(c_next)
    
            ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)
    
            dc_prev = ds * f
    
            di = ds * g
            df = ds * c_prev
            do = dh_next * tanh_c_next
            dg = ds * i
    

    在这里插入图片描述

  3. 然后计算一下三个门以及tanh节点处的输入侧梯度;sigmoid导数以及tanh的导数都知道了;所以直接局部梯度*输出侧梯度即可;

            # sigmoid函数和tanh节点处输入侧的梯度;=局部梯度*各自输出侧梯度
            di *= i * (1 - i)
            df *= f * (1 - f)
            do *= o * (1 - o)
            dg *= (1 - g ** 2)
    
  4. slice节点就是从整合的矩阵中取出对应的列;不涉及计算;因此反向传播时梯度直接整合回原样即可;

            # 按列将梯度拼接;拼接顺序要和forward的时候取出来的顺序保持一致
            dA = np.hstack((df, dg, di, do))
    
  5. 然后就和RNN部分的反向传播一样了:

    1. db的计算由于b会进行广播,因此db也是需要按列累加的;
  6. 全部的反向传播代码+注释如下:

        def backward(self, dh_next, dc_next):
            '''
            @param dh_next: (N,H);水平方向上后一个时刻传递来的&垂直方向上后面的层传递来的梯度两者的累加
            @param dc_next: (N,H);水平方向上后一个时刻传递来的记忆单元的梯度
            @return dx: (N,D);输入数据的梯度
            @return dh_prev: (N,H);水平方向上上一个时刻隐藏状态的梯度;
            @return dc_prev: (N,H);水平方向上上一个时刻记忆单元的梯度;'''
            Wx, Wh, b = self.params
            x, h_prev, c_prev, i, f, g, o, c_next = self.cache
    
            tanh_c_next = np.tanh(c_next) # (N,H)
    
            ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2) # (N,H)
    
            dc_prev = ds * f # (N,H)
    
            di = ds * g # (N,H)
            df = ds * c_prev # (N,H)
            do = dh_next * tanh_c_next # (N,H)
            dg = ds * i # (N,H)
    
            # sigmoid函数和tanh节点处输入侧的梯度;=局部梯度*各自输出侧梯度
            di *= i * (1 - i) # (N,H)
            df *= f * (1 - f) # (N,H)
            do *= o * (1 - o) # (N,H)
            dg *= (1 - g ** 2) # (N,H)
    
            # 按列将梯度拼接;拼接顺序要和forward的时候取出来的顺序保持一致
            dA = np.hstack((df, dg, di, do)) # (N,4H)
    
            dWh = np.dot(h_prev.T, dA) # (H,4H)=(H,N)x(N,4H)
            dWx = np.dot(x.T, dA) # (D,4H)=(D,N)x(N,4H)
            db = dA.sum(axis=0) # (1,4H)
    
            self.grads[0][...] = dWx
            self.grads[1][...] = dWh
            self.grads[2][...] = db
    
            dx = np.dot(dA, Wx.T) # (N,D)=(N,4H)x(4H,D)
            dh_prev = np.dot(dA, Wh.T) # (N,H)=(N,4H)x(4H,H)
    
            return dx, dh_prev, dc_prev
    

3 Time LSTM层的实现

和TRNN一样,TLSTM层是整体处理 T T T​​个时序数据的层;

代码位于:LSTM_model/LSTM.py

3.1 Time LSTM层的结构

  1. TLSTM层如下图所示:

    在这里插入图片描述

  2. 为了forward时,在每个TLSTM层之间传递隐藏状态和记忆单元,需要保存中间结果,如下图所示:

    在这里插入图片描述

3.2 Time LSTM层的代码实现

3.2.1初始化

  1. 代码如下:

    class TimeLSTM:
        def __init__(self, Wx, Wh, b, stateful=False):
            # 和TRNN一样,每个LSTM层都用同一组[Wx, Wh, b];这一组[Wx, Wh, b]是整合了四个仿射变换的大矩阵
            self.params = [Wx, Wh, b]
            self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
            self.layers = None
    
            self.h, self.c = None, None # 保存中间状态,在TimeLSTM层间传播
            self.dh = None
            self.stateful = stateful
    

3.2.2前向传播

  1. 和TRNN一样,TLSTM的每个LSTM层实际上是本身,只是在水平方向上展现了一种数据输入的顺序;因此TLSTM每次forward时会生成 T T T​个LSTM层,计算之后,在backward时会将每个LSTM层的的梯度累加;

  2. 所以代码也无需多解释;如下所示:

        def forward(self, xs):
            Wx, Wh, b = self.params
            N, T, D = xs.shape
            H = Wh.shape[0]
    
            self.layers = [] # 每次调用forward都是重新生成T个层;
            hs = np.empty((N, T, H), dtype='f')
    
            if not self.stateful or self.h is None:
                self.h = np.zeros((N, H), dtype='f')
            if not self.stateful or self.c is None:
                self.c = np.zeros((N, H), dtype='f')
    
            for t in range(T):
                layer = LSTM(*self.params)
                self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
                hs[:, t, :] = self.h
    
                self.layers.append(layer)
    
            return hs
    

3.2.3反向传播

  1. 代码如下:

        def backward(self, dhs):
            '''
            @param dhs: (N, T, H)
            @return dxs: (N, T, D)'''
            Wx, Wh, b = self.params
            N, T, H = dhs.shape
            D = Wx.shape[0]
    
            dxs = np.empty((N, T, D), dtype='f')
            dh, dc = 0, 0 # 每次反向传播时都是不接收上一个TLSTM层的梯度的
    
            grads = [0, 0, 0]
            for t in reversed(range(T)):
                layer = self.layers[t]
                dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
                dxs[:, t, :] = dx
                for i, grad in enumerate(layer.grads):
                    # 将每一个时间步的权重参数梯度累加
                    grads[i] += grad
    
            for i, grad in enumerate(grads):
                self.grads[i][...] = grad
            self.dh = dh # 其实也没用,因为下一次backward的时候也不考虑上一个TLSTM层的梯度
            return dxs
    

4使用LSTM对语言模型建模

  1. 这里我们仍然称之为RNN;前面用RNN进行语言建模的我们叫simple RNN;
  2. 代码位于:LSTM_model/LSTMLM.py
  1. 与RNN相比,这里的区别就在于将TRNN层替换为TLSTM层;如下图所示:

    在这里插入图片描述

4.1初始化

  1. 包括初始化权重和初始化层,过程基本和RNNLM一样,详见2.3-基于RNN的语言模型的学习与评价;需要指出的是:

    1. 由于LSTM层在实现时将四个仿射变换整合在了一起,因此这里在初始化权重时就需要初始化整合之后的矩阵;
    2. 初始化代码如下
    class Rnnlm:
        def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
            V, D, H = vocab_size, wordvec_size, hidden_size
            rn = np.random.randn
    
            # 初始化权重
            embed_W = (rn(V, D) / 100).astype('f')
            # LSTM层的整合之后的四个仿射变换
            lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
            lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
            lstm_b = np.zeros(4 * H).astype('f') # (4H,)
            affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
            affine_b = np.zeros(V).astype('f')
    
            # 生成层
            self.layers = [
                TimeEmbedding(embed_W),
                TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True), # forward时信息流不中断
                TimeAffine(affine_W, affine_b)
            ]
            self.loss_layer = TimeSoftmaxWithLoss()
            self.lstm_layer = self.layers[1]
    
            # 将所有的权重和梯度整理到列表中
            self.params, self.grads = [], []
            for layer in self.layers:
                self.params += layer.params
                self.grads += layer.grads
    

4.2前向计算

  1. 单从语言模型建模这个层面看,LSTMLM和RNNLM的前向计算完全一样;这里在实现的时候拆成了两个部分:一是计算得分的过程;二是计算损失的部分;代码如下:

        def predict(self, xs):
            '''
            @param xs: (N, T);输入的数据;
            @return xs: (N, V);Affine层之后的输出,即得分;'''
            # 具体每个Time xxx层进行前向传播(不包括损失计算)
            for layer in self.layers:
                xs = layer.forward(xs)
            return xs
    
        def forward(self, xs, ts):
            '''
            @param xs: (N, T);输入的数据;
            @param ts: (N, T);监督数据;(N,T)时不是独热编码形式;
            @return loss: 损失值;'''
            score = self.predict(xs)
            loss = self.loss_layer.forward(score, ts)
            return loss
    

4.3反向传播

  1. 和RNNLM也是一样的,如下图所示:

    在这里插入图片描述

4.4其他

  1. 另外,增加了读取和保存训练参数的函数load_params、save_params;详见代码;

5在PTB数据集上进行训练和测试

  1. 主要过程和用RNNLM是一样的;这里主要说一下区别;
  2. 代码位于:LSTM_model/train_LSTMLM.py
  1. 读取数据之后,使用前面构建的TLSTM版本的语言模型来构建模型,生成的model如下图所示:

    在这里插入图片描述

  2. 由于LSTMLM与RNNLM的区别只是将Time RNN换成了Time LSTM,其他都不变(包括mini-batch数据的组织),因此训练类就不需要变了,延用RNNLM使用的训练类;

    1. 这里将PTB训练集的数据都进行了训练;还是和RNNLM一样,将训练集视为一个长句子,然后根据batch_size的大小分成相应数量的句子;
  3. 为了能够在GPU上运行,在各个需要转移数据和权重到GPU上的地方,加了是否使用GPU的判断;

  4. 另外,在运行的时候,可能存在overflow的警告:

    1. RuntimeWarning: overflow encountered in exp
    2. 看了这个博客,将sigmoid函数换成了tanh的方法;如果采用循环的方法,会非常慢,所以不建议;
  5. 而且,这里梯度裁剪非常重要;LSTM里面也是有矩阵乘法的,所以需要用上梯度裁剪;否则梯度变化幅度大,模型训练不稳定,困惑度跑不出书上的那个结果;

  6. 如下图所示是一开始的实验结果:

    1. 可以看到左图,存在溢出的情况,导致困惑度巨大;
    2. 将sigmoid函数换成了tanh的方法之后正常了,右图;但是困惑度只能降到一千多;

    在这里插入图片描述

  7. 然后在训练时,加入了梯度裁剪,能够跑出与书本上差不多的实验结果;

    在这里插入图片描述

标签:tanh,代码,boldsymbol,next,3.2,np,LSTM,self
From: https://blog.csdn.net/colleges/article/details/140524117

相关文章

  • 代码随想录算法训练营第28天 | 回溯4:491.递增子序列、46.全排列、47.全排列 II
    代码随想录算法训练营第28天|回溯4:491.递增子序列、46.全排列、47.全排列II491.递增子序列https://leetcode.cn/problems/non-decreasing-subsequences/代码随想录https://programmercarl.com/0491.递增子序列.html#算法公开课46.全排列https://leetcode.cn/problems/pe......
  • Code Review:提升代码质量与团队能力的利器
    1.引言CodeReview(下文简称CR),即代码审查,是一种通过评审代码以发现并修正错误的实践。它不是一个新概念,但在软件开发中,它的重要性毋庸置疑。首先,它可以显著降低软件中的缺陷比例;其次,它促进了知识共享,通过评审的过程,团队成员可以相互学习,增强对系统的整体理解;最后,CR是一种预防措施......
  • VS快速全局查找Unity死循环代码
    1、编写一个死循环方法,然后运行调试vsusingUnityEngine;publicclassDeadLoop:MonoBehaviour{//StartiscalledbeforethefirstframeupdatevoidStart(){DeadLoopMethod();}voidDeadLoopMethod(){while(t......
  • 吐血整理如何在Google Earth Engine上写循环 五个代码实例详细拆解
    在这里同步一篇本人的原创文章。原文发布于2023年发布在知乎专栏,转移过来时略有修改。全文共计3万余字,希望帮助到GEE小白快速进阶。引言这篇文章主要解答GEE中.map()和.iterate()函数的用法。首先解答一个疑问,为什么需要自己写循环?确实,GEE为各种数据类型提供了无数常用的内置......
  • 一些有趣的代码注释
    在网上冲浪的时候看到了一些很好玩的注释,分享一波~用处:好玩,例如放到SpringBoot的banner里,输出到浏览器控制台里…‍佛祖保佑/***_ooOoo_*o8888888o*88"."88*......
  • Java使用流实现浏览器自动下载文件(附前端请求代码)
    一、注意事项1.需要注意后端的响应头设置,告诉浏览器下载文件类型(Content-Type)以及文件名称。2.不同环境下中文路径以及文件名会出现乱码情况路径解决:filePath=newString((filePath).getBytes("ISO8859-1"),"UTF-8");文件名解决:response.setHeader("Content-Dispositi......
  • kimi写代码:处理msgrcv返回E2BIG
    #include<stdio.h>#include<sys/ipc.h>#include<sys/msg.h>#include<string.h>#include<errno.h>typedefstruct{longmtype;charmtext[1024];//假设消息文本的最大长度为1024字节}message;intmain(){key_tkey=ftok(&......
  • 分页前端代码实现
    总共三步:-----1:css-----具体css引入代码可以看我另外发布的“js/css相关引入文件”<linkhref="~/media/fenye/page.css"rel="stylesheet"/><styletype="text/css">.tdContextClass{text-align-last:auto;color:#000000;......
  • 量化交易策略:波动性指标Keltner通道(附python代码)
    Keltner通道是由著名技术分析师ChesterW.Keltner在上个世纪60年代发明的一种技术指标。它基于价格和某种市场波动性度量(通常为移动平均)之间的关系,用于预测价格趋势的变化。Keltner通道的上下轨分别表示了价格的上限和下限,反映了市场在一定周期内的波动范围。这使得Keltner......
  • 【python学习】第三方库之tensorflow的定义、功能、使用场景、代码示例和解释
    引言tensorFlow是一个开源的软件库,最初由GoogleBrain团队的研究员和工程师开发,用于数值计算和大规模机器学习tensorFlow提供了丰富的API来支持各种计算任务,并且能够轻松地在多种计算设备上进行部署,包括CPU、GPU和TPU文章目录引言一、安装`tensorflow`第三方......