Pytorch实战笔记(1)——BiLSTM 实现情感分析

本文展示的是使用 Pytorch 构建一个 BiLSTM 来实现情感分析。本文的架构是第一章详细介绍 BiLSTM,第二章粗略介绍 BiLSTM(就是说如果你想快速上手可以跳过第一章),第三章是核心代码部分。

1. BiLSTM的详细介绍

坦白的说,其实我也不懂 LSTM,但是我这里还是尽我最大的可能解释这个模型。这里我就盗个图 [1](懒得自己画了,而且感觉好像他也是盗的李宏毅老师课件的图)。
LSTM
简单来说,LSTM 在每个时刻的输入都是由该时刻输入的序列信息 X t X^t Xt 与上一时刻的隐藏状态 h t − 1 h^{t-1} ht1 通过四种不同的非线性变化映射而成,分别为:

  1. 遗忘门控信号:遗忘门控信号 z f z^f zf 的计算公式如下:
    z f = s i g m o i d ( W f [ X t ; h t − 1 ] ) , z^f = {\rm sigmoid}(W^f\left[ X^t; h^{t-1} \right]), zf=sigmoid(Wf[Xt;ht1]),
    其中, [ X t ; h t − 1 ] [X^t;h^{t-1}] [Xt;ht1] 是将 X t X^t Xt h t − 1 h^{t-1} ht1 拼接起来; W f W^f Wf 是权重; S i g m o i d ( ⋅ ) {\rm Sigmoid}(\cdot) Sigmoid() 是 Sigmoid 激活函数,用于将数据映射到 (0, 1) 的区间范围内。
  2. 记忆门控信号:记忆门控信号 z i z^i zi 的计算公式如下:
    z i = s i g m o i d ( W i [ X t ; h t − 1 ] ) . z^i={\rm sigmoid}(W^i\left[ X^t; h^{t-1} \right]). zi=sigmoid(Wi[Xt;ht1]).
  3. 输出门控信号:输出门控信号 z o z^o zo 的计算公式如下:
    z o = s i g m o i d ( W o [ X t ; h t − 1 ] ) . z^o = {\rm sigmoid}(W^o\left[ X^t; h^{t-1} \right]). zo=sigmoid(Wo[Xt;ht1]).
  4. 当前时刻的信息:当前时刻的信息 z z z 的计算公式如下:
    z = t a n h ( W [ X t ; h t − 1 ] ) , z = {\rm tanh}(W\left[ X^t; h^{t-1} \right]), z=tanh(W[Xt;ht1]),
    其中, t a n h ( ⋅ ) {\rm tanh}(\cdot) tanh() 是将数据放缩到 (-1, 1) 的区间内。

通过以上的公式,我们可以发现, z f , z i , z o z^f, z^i, z^o zf,zi,zo 都是 (0, 1) 区间的值,而 z z z(-1, 1) 区间的值。

接着就是 LSTM 的内部计算公式,即图上所示的那几个,分别为:

  1. 当前时刻的细胞状态 c t c^t ct 的计算公式如下:
    c t = z f ⊙ c t − 1 + z i ⊙ z , c^t = z^f \odot c^{t-1} + z^i \odot z, ct=zfct1+ziz,
    其中, ⊙ \odot 是哈达玛积,即矩阵元素对位相乘,但是需要注意的是,哈达玛积数学上不可解释,但是跑出来效果好
  2. 当前时刻的隐藏状态 h t h^t ht 的计算公式如下:
    h t = z o ⊙ t a n h ( c t ) . h^t = z^o \odot {\rm tanh} (c^t). ht=zotanh(ct).
  3. 当前时刻的输出 y t y^t yt 的计算公式如下:
    y t = σ ( W ′ h t ) . y^t = \sigma (W'h^t). yt=σ(Wht).

公式列举完后,这里说一下我对这些公式的理解(不一定是对的哈)。

  • 首先是 c t c^t ct 的计算。我们看到 c t c^t ct 的计算分为了两部分。一部分是 z f ⊙ c t − 1 z^f \odot c^{t-1} zfct1,这一部分是 LSTM 的遗忘过程,由于刚刚提到, z f z^f zf 是 (0, 1) 区间范围内的值,同时,sigmoid 函数是一个无限趋近于 0 或者 1 的函数,也就是说, c t − 1 c^{t-1} ct1 无论怎样,都会有些数据被遗弃,始终不会完全保留下来,这也就模拟了一个遗忘的过程。同理,对于记忆部分 z i ⊙ z z^i \odot z ziz,这一步也是只会保留部分 z z z 的信息,也就模拟了人的记忆是由些许失真的过程。同时,两者相加后,那么就代表了当前细胞状态 c t c^t ct 中保留的是没有被遗忘掉的过去的信息和当前时刻被记忆下来的信息
  • 接着是 h t h^t ht 的计算。首先是为什么要先对 c t c^t ct 做一次 t a n h ( ⋅ ) {\rm tanh}(\cdot) tanh(),这是因为由于 c t c^t ct 的区间范围不是 (-1, 1),因为 z i ⊙ z z^i \odot z ziz 的区间范围是 (-1, 1),再与 z f ⊙ c t − 1 z^f \odot c^{t-1} zfct1 相加,那么 c t c^t ct 的范围就有可能超出 (-1, 1),所以先用一个 tanh 将数值给放缩到 (-1, 1) 内。接着再与 z o z^o zo 做一次哈达玛积后,得到的隐藏状态就是 (-1, 1) 的数据,那么该数据放到后续模块中,就可以代表当前时刻的输入是正的还是负的,同时有多大。
  • 最后就是 y t y^t yt 的计算,实际上这就是个全连接层,将隐藏状态进行一次映射,再通过一个非线性变化的激活函数。

2. BiLSTM 的简单介绍

当然,其实你没看懂上面的部分也不重要,从使用的角度上来讲,会用就行了,就像你用手机,你不会去搞懂里面每个元器件是怎么做出来的,每个 APP 是怎么写出来的;就像你去打篮球,也不用梳个中分,穿个背带裤。

那么对于 BiLSTM,你需要了解的是什么?

  • 首先,这是一个序列模型,它接受一个序列的输入,并且输出这个序列的信息。对于序列中每个位置的输出,它会包含该位置的信息以及之前的信息。就是说 LSTM 能够捕获到位置 t t t 及其之前位置的信息。而对于 BiLSTM 的话,则能捕获到 t t t 的双向信息。
  • 如果是 BiLSTM,它的每个位置的输出,是前向 L S T M → \overrightarrow{LSTM} LSTM 的输出 y → \overrightarrow{y} y 与反向 L S T M ← \overleftarrow{LSTM} LSTM 的输出 y ← \overleftarrow{y} y 拼接在一起的, [ y → ; y ← ] [\overrightarrow{y}; \overleftarrow{y}] [y ;y ]。所以假设你设置 LSTM 的隐藏层维度为 128,那么单向 LSTM 的输出维度是 128,但是双向就是 256 (128*2).
  • 但是虽然说 LSTM 好像大概可能也许 maybe possibly 能够捕获长距离依赖信息哈,毕竟 LSTM 的全称都是 Long Short-Term Memory,但是实际上这是 LSTM 的骗局,LSTM 并没有捕获长距离依赖信息的能力!LSTM 并没有捕获长距离依赖信息的能力!LSTM 并没有捕获长距离依赖信息的能力! 从数学上说,你经过这么多次 sigmoid,还能保留个啥?当然,在《An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling》这篇论文[2]中,作者用了大量的实验来说明了,LSTM 不仅并行计算能力差(因为要上一个时间步的信息才能计算下一个时间步,所以 LSTM 不是个并行系统),同时在它最吹嘘的长距离信息捕获能力上,都不如 CNN,所以以后在跑实验的时候,可以尝试使用 TextCNN 来试试,说不定效果比 BiLSTM 好(反正我做过的实验中 TextCNN 性能一般比 BiLSTM 高8-10个点)。

3. BiLSTM 实现情感分析

在本博客中仅介绍模型部分,详细代码见 github。

模型图如图所示:
模型图
具体而言,就是输入序列输入到一个双向 LSTM 中,并将双向 LSTM 的最后一个隐藏状态(即句向量)输入到一个全连接层(也可以说是分类器)中,输出最后的分类结果,具体模型的代码如下:

import torch.nn as nn

class BiLSTM_SA(nn.Module):

    def __init__(self, embed, config):
        super().__init__()
        self.embedding = nn.Embedding.from_pretrained(embed, freeze=False)
        self.LSTM = nn.LSTM(config.embed_size, config.lstm_hidden_size,
                            num_layers=config.num_layers, batch_first=True,
                            bidirectional=True)
        # 因为是双向 LSTM, 所以要乘2
        self.ffn = nn.Linear(config.lstm_hidden_size * 2,
                             config.dense_hidden_size)
        self.relu = nn.ReLU()
        self.classifier = nn.Linear(config.dense_hidden_size,
                                    config.num_outputs)

    def forward(self, inputs):
        # shape: (batch_size, max_seq_length, embed_size)
        embed = self.embedding(inputs)
        # shape: (batch_size, max_seq_length, lstm_hidden_size * 2)
        lstm_hidden_states, _ = self.LSTM(embed)
        # LSTM 的最后一个时刻的隐藏状态, 即句向量
        # shape: (batch, lstm_hidden_size * 2)
        lstm_hidden_states = lstm_hidden_states[:, -1, :]
        # shape: (batch, dense_hidden_size)
        ffn_outputs = self.relu(self.ffn(lstm_hidden_states))
        # shape: (batch, num_outputs)
        logits = self.classifier(ffn_outputs)

        return logits

全连接层我采用了两个全连接层,一个将维度从 256 压缩到 128,另外一个是分类器。

这里有个小细节要注意一下,通常在论文的公式里面,我们都会看到别人写的分类器的公式如下: y ^ = S o f t m a x ( W h + b ) \hat{y} = {\rm Softmax}(Wh+b) y^=Softmax(Wh+b),有个 softmax 的激活函数,但是在 pytorch 中实际不需要,就比如我代码里面是写的:

logits = self.classifier(ffn_outputs)

而不是:

y_hat = self.softmax(self.classifier(ffn_outputs))

这是因为如果你后面选用交叉熵作为损失函数,而且调用的是torch中的 nn.CrossEntropyLoss(),那么就没必要在输出的时候用 softmax,这是因为 nn.CrossEntropyLoss() 中自带有 softmax 操作,虽然这样对你的分类结果不会产生任何影响,但是你得损失会变得很大。

最后的测试集的实验结果为:

test loss 0.419664 | test accuracy 0.813760 | test precision 0.804267 | test recall 0.829360 | test F1 0.816621

参考

[1] 陈诚. 人人都能看懂的LSTM[EB/OL]. https://zhuanlan.zhihu.com/p/32085405, 2018
[2] Shaojie Bai, J. Zico Kolter, Vladlen Koltun. An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling [EB/OL]. https://arxiv.org/abs/1803.01271, 2018

  • 2
    点赞
  • 78
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
下面是一个基于PyTorch的DenseNet-BiLSTM-Attention模型的五分类训练代码,适用于输入数据集为1行121列的情况。 ```python import torch import torch.nn as nn import torch.optim as optim import numpy as np class DenseNet_BiLSTM_Attention(nn.Module): def __init__(self, num_classes): super(DenseNet_BiLSTM_Attention, self).__init__() self.densenet = torchvision.models.densenet121(pretrained=True) self.densenet_features = self.densenet.features self.avgpool = nn.AdaptiveAvgPool2d((1, 1)) self.dropout = nn.Dropout(p=0.5) self.bilstm = nn.LSTM(input_size=1024, hidden_size=512, num_layers=2, bidirectional=True, batch_first=True) self.attention = nn.Sequential( nn.Linear(1024, 512), nn.Tanh(), nn.Linear(512, 1), nn.Softmax(dim=1) ) self.fc = nn.Linear(1024, num_classes) def forward(self, x): x = x.view(-1, 1, 11, 11) # reshape input to match DenseNet input shape x = self.densenet_features(x) x = self.avgpool(x) x = torch.flatten(x, 1) x = self.dropout(x) x, _ = self.bilstm(x) x = x[:, -1, :] attention_weights = self.attention(x) x = torch.sum(attention_weights * x, dim=1) x = self.fc(x) return x model = DenseNet_BiLSTM_Attention(num_classes=5) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) # Load and preprocess the data # Assume the data is stored in a numpy array X of shape (num_samples, 121) and a numpy array y of shape (num_samples,) X = np.load("data.npy") y = np.load("labels.npy") # Split into training and validation sets num_samples = X.shape[0] num_train = int(num_samples * 0.8) train_indices = np.random.choice(num_samples, num_train, replace=False) val_indices = np.setdiff1d(np.arange(num_samples), train_indices) X_train, y_train = X[train_indices], y[train_indices] X_val, y_val = X[val_indices], y[val_indices] # Convert to PyTorch tensors X_train = torch.from_numpy(X_train).float() y_train = torch.from_numpy(y_train).long() X_val = torch.from_numpy(X_val).float() y_val = torch.from_numpy(y_val).long() # Train the model num_epochs = 10 batch_size = 32 for epoch in range(num_epochs): model.train() running_loss = 0.0 for i in range(0, num_train, batch_size): optimizer.zero_grad() batch_X = X_train[i:i+batch_size] batch_y = y_train[i:i+batch_size] outputs = model(batch_X) loss = criterion(outputs, batch_y) loss.backward() optimizer.step() running_loss += loss.item() * batch_X.size(0) epoch_loss = running_loss / num_train print("Epoch {} training loss: {:.4f}".format(epoch+1, epoch_loss)) model.eval() with torch.no_grad(): val_outputs = model(X_val) val_loss = criterion(val_outputs, y_val) val_predictions = torch.argmax(val_outputs, dim=1) val_accuracy = torch.sum(val_predictions == y_val) / len(y_val) print("Epoch {} validation loss: {:.4f} accuracy: {:.4f}".format(epoch+1, val_loss.item(), val_accuracy.item())) ``` 在此代码中,我们首先定义了一个名为`DenseNet_BiLSTM_Attention`的神经网络模型,它由DenseNet、BiLSTM和Attention层组成。其中,DenseNet用于提取输入数据的特征,BiLSTM用于学习时序特征,Attention层用于加强模型对关键信息的关注。然后,我们使用PyTorch内置的交叉熵损失函数和Adam优化器来训练模型。在训练过程中,我们将数据集分成80%的训练集和20%的验证集,并使用随机梯度下降法进行优化。最后,我们在每个epoch结束时输出训练和验证的损失和精度。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值