【Deep Learning A情感文本分类实战】2023 Pytorch+Bert、Roberta+TextCNN、BiLstm、Lstm等实现IMDB情感文本分类完整项目(项目已开源)

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时数仓Hologres,5000CU*H 100GB 3个月
实时计算 Flink 版,1000CU*H 3个月
简介: 亮点:代码开源+结构清晰+准确率高+保姆级解析🍊本项目使用Pytorch框架,使用上游语言模型+下游网络模型的结构实现IMDB情感分析🍊语言模型可选择Bert、Roberta🍊神经网络模型可选择BiLstm、LSTM、TextCNN、Rnn、Gru、Fnn共6种🍊语言模型和网络模型扩展性较好,方便读者自己对模型进行修改

 image.gif编辑

 

🍊作者最近在看了大量论文的源代码后,被它们干净利索的代码风格深深吸引,因此也想做一个结构比较规范而且内容较为经典的任务

🍊本项目使用Pytorch框架,使用上游语言模型+下游网络模型的结构实现IMDB情感分析

🍊语言模型可选择Bert、Roberta

🍊主神经网络模型可选择BiLstm、LSTM、TextCNN、Rnn、Gru、FNN、Attention共7种

🍊语言模型和网络模型扩展性较好

🍊最终的准确率均在90%以上

🍊项目已开源,clone下来再配个简单环境就能跑

很高兴能够帮到大家,也希望大家可以动动小手,在Github上给个小星星

一、Introduction

1.1 网络架构图

该网络主要使用上游预训练模型+下游情感分类模型组成

image.gif编辑

1.2 快速使用

该项目已开源在Github上,地址为 sentiment_analysis_Imdb

image.gif编辑

主要环境要求如下(环境不要太老基本没啥问题的)

image.gif编辑

下载该项目后,配置相对应的环境,在config.py文件中选择所需的语言模型和神经网络模型如下图所示,运行main.py文件即可

image.gif编辑

1.3 工程结构

image.gif编辑

    • logs  每次运行程序后的日志文件集合
    • config.py 全局配置文件
    • data.py 数据读取、数据清洗、数据格式转换、制作DataSet和DataLoader
    • main.py 主函数,负责全流程项目运行,包括语言模型的转换,模型的训练和测试
    • model.py 神经网络模型的设计和读取

    二、Config

    看了很多论文源代码中都使用parser容器进行全局变量的配置,因此作者也照葫芦画瓢编写了config.py文件

    import argparse
    import logging
    import os
    import random
    import sys
    import time
    from datetime import datetime
    import torch
    def get_config():
        parser = argparse.ArgumentParser()
        '''Base'''
        parser.add_argument('--num_classes', type=int, default=2)
        parser.add_argument('--model_name', type=str, default='bert',
                            choices=['bert', 'roberta'])
        parser.add_argument('--method_name', type=str, default='fnn',
                            choices=['gru', 'rnn', 'bilstm', 'lstm', 'fnn', 'textcnn', 'attention', 'lstm+textcnn',
                                     'lstm_textcnn_attention'])
        '''Optimization'''
        parser.add_argument('--train_batch_size', type=int, default=4)
        parser.add_argument('--test_batch_size', type=int, default=16)
        parser.add_argument('--num_epoch', type=int, default=50)
        parser.add_argument('--lr', type=float, default=1e-5)
        parser.add_argument('--weight_decay', type=float, default=0.01)
        '''Environment'''
        parser.add_argument('--device', type=str, default='cpu')
        parser.add_argument('--backend', default=False, action='store_true')
        parser.add_argument('--workers', type=int, default=0)
        parser.add_argument('--timestamp', type=int, default='{:.0f}{:03}'.format(time.time(), random.randint(0, 999)))
        args = parser.parse_args()
        args.device = torch.device(args.device)
        '''logger'''
        args.log_name = '{}_{}_{}.log'.format(args.model_name, args.method_name,
                                              datetime.now().strftime('%Y-%m-%d_%H-%M-%S')[2:])
        if not os.path.exists('logs'):
            os.mkdir('logs')
        logger = logging.getLogger()
        logger.setLevel(logging.INFO)
        logger.addHandler(logging.StreamHandler(sys.stdout))
        logger.addHandler(logging.FileHandler(os.path.join('logs', args.log_name)))
        return args, logger

    image.gif

    三、Data

    3.1 数据准备

    首先需要下载IMDB数据集,并对其进行初步处理,其处理过程可参考一下文章IMDB数据预处理

    也可以直接从Github上获取已处理好的数据集,处理好的数据格式如下

    image.gif编辑

    3.2 数据预处理

    由于IMDB数据量非常庞大,使用全数据的训练时间非常长(算力好的小伙伴可忽略),因此这里使用10%的数据量进行训练

    data = pd.read_csv('datasets.csv', sep=None, header=0, encoding='utf-8', engine='python')
        len1 = int(len(list(data['labels'])) * 0.1)
        labels = list(data['labels'])[0:len1]
        sentences = list(data['sentences'])[0:len1]
        # split train_set and test_set
        tr_sen, te_sen, tr_lab, te_lab = train_test_split(sentences, labels, train_size=0.8)

    image.gif

    3.3 制作DataSet

    划分训练集和测试集之后就可以制作自己的DataSet

    # Dataset
        train_set = MyDataset(tr_sen, tr_lab, method_name, model_name)
        test_set = MyDataset(te_sen, te_lab, method_name, model_name)

    image.gif

    MyDataset的结构如下

      • 使用split方法将每个单词提取出来作为后续bertToken的输入
      • 后续制作DataLoader需要使用collate_fn函数因此需要重写__getitem__方法
      class MyDataset(Dataset):
          def __init__(self, sentences, labels, method_name, model_name):
              self.sentences = sentences
              self.labels = labels
              self.method_name = method_name
              self.model_name = model_name
              dataset = list()
              index = 0
              for data in sentences:
                  tokens = data.split(' ')
                  labels_id = labels[index]
                  index += 1
                  dataset.append((tokens, labels_id))
              self._dataset = dataset
          def __getitem__(self, index):
              return self._dataset[index]
          def __len__(self):
              return len(self.sentences)

      image.gif

      3.4 制作DataLoader

      得到DataSet之后就可以制作DataLoader了

        • 首先需要编写my_collate函数,该函数的功能是对每一个batch的数据进行处理
        • 在这里的数据处理是将文本数据进行Tokenizer化作为后续Bert模型的输入
        • 通过计算可得知80%句子的长度低于320,因此将句子长度固定为320,多截少补
        • partial是Python偏函数,使用该函数后,my_collate的输入参数只有一个batch
        def my_collate(batch, tokenizer):
            tokens, label_ids = map(list, zip(*batch))
            text_ids = tokenizer(tokens,
                                 padding=True,
                                 truncation=True,
                                 max_length=320,
                                 is_split_into_words=True,
                                 add_special_tokens=True,
                                 return_tensors='pt')
            return text_ids, torch.tensor(label_ids)

        image.gif

        # DataLoader
            collate_fn = partial(my_collate, tokenizer=tokenizer)
            train_loader = DataLoader(train_set, batch_size=train_batch_size, shuffle=True, num_workers=workers,
                                      collate_fn=collate_fn, pin_memory=True)
            test_loader = DataLoader(test_set, batch_size=test_batch_size, shuffle=True, num_workers=workers,
                                     collate_fn=collate_fn, pin_memory=True)

        image.gif

        到此我们就完成制作了DataLoader,后续从DataLoader中可获取一个个batch经Tokenizer化后的数据

        四、Language model

        对于网络模型来说,只能接受数字数据类型,因此我们需要建立一个语言模型,目的是将每个单词变成一个向量,每个句子变成一个矩阵。关于语言模型,其已经发展历史非常悠久了(发展历史如下),其中Bert模型是Google大神出的具有里程碑性质的模型,因此本篇博客也主要采用此模型

        image.gif编辑

        所用的模型都是通过API从  Hugging Face官网 中直接下载的

        Hugging Face中有非常多的好用的语言模型,小伙伴们也可尝试其他模型

        image.gif编辑

        使用AutoModel.from_pretrained接口下载预训练模型

        使用AutoTokenizere.from_pretrained接口下载预训练模型的分词器

        # Create model
                if args.model_name == 'bert':
                    self.tokenizer = AutoTokenizer.from_pretrained('bert-base-uncased')
                    self.input_size = 768
                    base_model = AutoModel.from_pretrained('bert-base-uncased')
                elif args.model_name == 'roberta':
                    self.tokenizer = AutoTokenizer.from_pretrained('roberta-base', add_prefix_space=True)
                    self.input_size = 768
                    base_model = AutoModel.from_pretrained('roberta-base')
                else:
                    raise ValueError('unknown model')

        image.gif

        下载创建好Bert之后,在训练和测试的时候,每次从DataLoadr中获取一个个经过Tokenizer分词之后Batch的数据,随后将其投放到语言模型中

          • ** input :input的数据是Tokenizer化后的数据如{ 'input_id' : ~ , 'token_type_ids' : ~ , 'attention_mask' : ~},**是将里面的三个dict分成一个个独立的dict,即{ 'input_id' : ~} ,{ 'token_type_ids' : ~} ,{ 'attention_mask' : ~}
          • raw_outputs获取的是Bert的输出,Bert的输出主要有四个,其中last_hidden_state表示的是最后一层隐藏层的状态,也就是每个单词的Token的集合
          raw_outputs = self.base_model(**inputs)
          tokens = raw_outputs.last_hidden_state

          image.gif

          到此,上游语言模型全部结束,得到了一个个经过Bert后维度为[batch大小,句子长度,单词维度]的数据

          五、Neural network model

          关于RNN的原理解释,小伙伴可以看以下文章

          【Deep Learning 7】RNN循环神经网络

          5.1 RNN

          Bilstm、lstm、gru本质上来说都是属于RNN模型,因此我们就以RNN模型为例子看看上游任务的数据是如何进入到下游文本分类的

          RNN输入参数

            • input_size:每个单词维度
            • hidden_size:隐含层的维度(一般设置与句子长度一致)
            • num_layers:RNN层数,默认是1,单层LSTM
            • bias:是否使用bias
            • batch_first:默认为False,如果设置为True,则表示第一个维度表示的是batch_size
            • dropout:随机失活
            • bidirectional:是否使用BiLSTM

            由于是情感分析文本二分类任务,因此还需要一个FNN+Softmax模块进行分类预测

            class Rnn_Model(nn.Module):
                def __init__(self, base_model, num_classes, input_size):
                    super().__init__()
                    self.base_model = base_model
                    self.num_classes = num_classes
                    self.input_size = input_size
                    self.Rnn = nn.RNN(input_size=self.input_size,
                                      hidden_size=320,
                                      num_layers=1,
                                      batch_first=True)
                    self.fc = nn.Sequential(nn.Dropout(0.5),
                                            nn.Linear(320, 80),
                                            nn.Linear(80, 20),
                                            nn.Linear(20, self.num_classes),
                                            nn.Softmax(dim=1))
                    for param in base_model.parameters():
                        param.requires_grad = (True)

            image.gif

            再来看看RNN的传播过程

            RNN输出参数

            output, (hn, cn) = lstm(inputs)

            output_last = output[:,-1,:]

              • output:每个时间步输出
              • output_last:最后一个时间步隐藏层神经元输出,也就是最终的特征表示
              • hn:最后一个时间步隐藏层的状态
              • cn:最后一个时间步隐藏层的遗忘门值

              由于我们用不到hn和cn,因此直接使用_来代替

              def forward(self, inputs):
                      # 上游任务
                      raw_outputs = self.base_model(**inputs)
                      cls_feats = raw_outputs.last_hidden_state
                      # 下游任务
                      outputs, _ = self.Rnn(cls_feats)
                      outputs = outputs[:, -1, :]
                      outputs = self.fc(outputs)
                      return outputs

              image.gif

              输出的outputs就是预测结果

              5.2 GRU

              其实熟悉了RNN网络模块之后,其他几个网络模块的也就非常好理解了

              将nn.RNN修改为nn.GRU

              class Gru_Model(nn.Module):
                  def __init__(self, base_model, num_classes, input_size):
                      super().__init__()
                      self.base_model = base_model
                      self.num_classes = num_classes
                      self.input_size = input_size
                      self.Gru = nn.GRU(input_size=self.input_size,
                                        hidden_size=320,
                                        num_layers=1,
                                        batch_first=True)
                      self.fc = nn.Sequential(nn.Dropout(0.5),
                                              nn.Linear(320, 80),
                                              nn.Linear(80, 20),
                                              nn.Linear(20, self.num_classes),
                                              nn.Softmax(dim=1))
                      for param in base_model.parameters():
                          param.requires_grad = (True)
                  def forward(self, inputs):
                      raw_outputs = self.base_model(**inputs)
                      tokens = raw_outputs.last_hidden_state
                      gru_output, _ = self.Gru(tokens)
                      outputs = gru_output[:, -1, :]
                      outputs = self.fc(outputs)
                      return outputs

              image.gif

              5.3 LSTM

              将nn.RNN修改为nn.LSTM

              class Lstm_Model(nn.Module):
                  def __init__(self, base_model, num_classes, input_size):
                      super().__init__()
                      self.base_model = base_model
                      self.num_classes = num_classes
                      self.input_size = input_size
                      self.Lstm = nn.LSTM(input_size=self.input_size,
                                          hidden_size=320,
                                          num_layers=1,
                                          batch_first=True)
                      self.fc = nn.Sequential(nn.Dropout(0.5),
                                              nn.Linear(320, 80),
                                              nn.Linear(80, 20),
                                              nn.Linear(20, self.num_classes),
                                              nn.Softmax(dim=1))
                      for param in base_model.parameters():
                          param.requires_grad = (True)
                  def forward(self, inputs):
                      raw_outputs = self.base_model(**inputs)
                      tokens = raw_outputs.last_hidden_state
                      lstm_output, _ = self.Lstm(tokens)
                      outputs = lstm_output[:, -1, :]
                      outputs = self.fc(outputs)
                      return outputs

              image.gif

              5.4 BILSTM

              bilstm与其他几个网络模型稍微有点不同,需要修改的地方有三处

                • 将nn.RNN修改为nn.LSTM
                • 在nn.LSTM中添加 bidirectional=True
                • 一个BILSTM是由两个LSTM组合而成的,因此FNN输入的维度也要乘2,即 nn.Linear(320 * 2, 80)
                class BiLstm_Model(nn.Module):
                    def __init__(self, base_model, num_classes, input_size):
                        super().__init__()
                        self.base_model = base_model
                        self.num_classes = num_classes
                        self.input_size = input_size
                        # Open the bidirectional
                        self.BiLstm = nn.LSTM(input_size=self.input_size,
                                              hidden_size=320,
                                              num_layers=1,
                                              batch_first=True,
                                              bidirectional=True)
                        self.fc = nn.Sequential(nn.Dropout(0.5),
                                                nn.Linear(320 * 2, 80),
                                                nn.Linear(80, 20),
                                                nn.Linear(20, self.num_classes),
                                                nn.Softmax(dim=1))
                        for param in base_model.parameters():
                            param.requires_grad = (True)
                    def forward(self, inputs):
                        raw_outputs = self.base_model(**inputs)
                        cls_feats = raw_outputs.last_hidden_state
                        outputs, _ = self.BiLstm(cls_feats)
                        outputs = outputs[:, -1, :]
                        outputs = self.fc(outputs)
                        return outputs

                image.gif

                5.5 TextCNN

                既然RNN可以做文本分类,那CNN呢?答案当然是可以的,早在2014年就出现了TextCNN,原模型如下

                image.gif编辑

                欸,看起来可能有点抽象,看另一篇解释该模型的图可能好理解多了

                image.gif编辑

                  • 首先原句与卷积核分别为[2,768]、[3,768]、[4,768]且channels为2的filtet进行卷积运算得到6个一维向量
                  • 随后将每个一维向量中取出最大值,将这6个最大值拼接成[6,2]的Tensor
                  • 最后进行常规的分类预测
                  • 注意nn.ModuleList的Pytorch代码技巧,ModuleList可以理解为可存储卷积核的List
                  class TextCNN_Model(nn.Module):
                      def __init__(self, base_model, num_classes):
                          super().__init__()
                          self.base_model = base_model
                          self.num_classes = num_classes
                          for param in base_model.parameters():
                              param.requires_grad = (True)
                          # Define the hyperparameters
                          self.filter_sizes = [2, 3, 4]
                          self.num_filters = 2
                          self.encode_layer = 12
                          # TextCNN
                          self.convs = nn.ModuleList(
                              [nn.Conv2d(in_channels=1, out_channels=self.num_filters,
                                         kernel_size=(K, self.base_model.config.hidden_size)) for K in self.filter_sizes]
                          )
                          self.block = nn.Sequential(
                              nn.Dropout(0.5),
                              nn.Linear(self.num_filters * len(self.filter_sizes), self.num_classes),
                              nn.Softmax(dim=1)
                          )
                      def conv_pool(self, tokens, conv):
                          tokens = conv(tokens)
                          tokens = F.relu(tokens)
                          tokens = tokens.squeeze(3)
                          tokens = F.max_pool1d(tokens, tokens.size(2))
                          out = tokens.squeeze(2)
                          return out
                      def forward(self, inputs):
                          raw_outputs = self.base_model(**inputs)
                          tokens = raw_outputs.last_hidden_state.unsqueeze(1)
                          out = torch.cat([self.conv_pool(tokens, conv) for conv in self.convs],
                                          1)
                          predicts = self.block(out)
                          return predicts

                  image.gif

                  5.7 FNN

                  因为我们使用的是Bert模型,其模型本身是由12层Transformer组成,每层Transformer又由复杂的Attention网络组成,所以Bert模型本身就是一个非常好的网络模型,所以可能我不太需要加RNN、CNN这些操作,直接使用FNN或许也可以实现不错的效果(7.Result部分的消融实验也证实了该想法)。

                  在讲解这个代码之前,不得不再次提起Bert的输出了,输入一个句子,Bert的输出是

                  【CLS】token1 token 2 token3 token4 ... token n 【SEP】

                  token表示每个输入单词的向量 ,【CLS】表示整个句子的向量,做FNN需要整个句子的输入,因此我们需要获取的是【CLS】。【CLS】是隐层0号位的数据,因此具体获取【CLS】的代码是

                  cls_feats = raw_outputs.last_hidden_state[:, 0, :]

                  image.gif

                  完整FNN网络模块代码如下

                  class Transformer(nn.Module):
                      def __init__(self, base_model, num_classes, input_size):
                          super().__init__()
                          self.base_model = base_model
                          self.num_classes = num_classes
                          self.input_size = input_size
                          self.linear = nn.Linear(base_model.config.hidden_size, num_classes)
                          self.dropout = nn.Dropout(0.5)
                          self.softmax = nn.Softmax()
                          for param in base_model.parameters():
                              param.requires_grad = (True)
                      def forward(self, inputs):
                          raw_outputs = self.base_model(**inputs)
                          cls_feats = raw_outputs.last_hidden_state[:, 0, :]
                          predicts = self.softmax(self.linear(self.dropout(cls_feats)))
                          return predicts

                  image.gif

                  5.8 Self-Attention

                  RNN和CNN都介绍过了,那作为当下最主流的Attention机制怎么能少呢?

                  关于Self-Attention的细节,可以参考这篇文章

                  在这里,为了方便对着公式阅读代码,我将Attention的公式放置如下

                  class Transformer_Attention(nn.Module):
                      def __init__(self, base_model, num_classes):
                          super().__init__()
                          self.base_model = base_model
                          self.num_classes = num_classes
                          for param in base_model.parameters():
                              param.requires_grad = (True)
                          # Self-Attention
                          self.key_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self.query_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self.value_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self._norm_fact = 1 / math.sqrt(self.base_model.config.hidden_size)
                          self.block = nn.Sequential(
                              nn.Dropout(0.5),
                              nn.Linear(768, 128),
                              nn.Linear(128, 16),
                              nn.Linear(16, num_classes),
                              nn.Softmax(dim=1)
                          )
                      def forward(self, inputs):
                          raw_outputs = self.base_model(**inputs)
                          tokens = raw_outputs.last_hidden_state
                          K = self.key_layer(tokens)
                          Q = self.query_layer(tokens)
                          V = self.value_layer(tokens)
                          attention = nn.Softmax(dim=-1)((torch.bmm(Q, K.permute(0, 2, 1))) * self._norm_fact)
                          attention_output = torch.bmm(attention, V)
                          attention_output = torch.mean(attention_output, dim=1)
                          predicts = self.block(attention_output)
                          return predicts

                  image.gif

                  5.9 TextCNN+LSTM

                  在这里,我先抛出一个问题,我们为什么要用CNN和LSTM,它究竟有什么好处?

                  TextCNN是使用了卷积机制,提取了文本中最关键的信息,也就是最具有区分性的信息,比如5.5中的TextCNN展示图,一个7*5如此大的矩阵,最终竟然只用6*1的向量就可表征了

                  而LSTM恰巧相反,它是不断地关注了时间序列中每个隐藏层状态,目的是挖掘出深藏的语义信息

                  因此,我们可以将TextCNN和LSTM结合起来,就像你考CET6阅读理解一样一样,既要抓取最显著的信息快速判断,又要细读文章内容,理解内含的语义信息,二者结合来便于判断

                  说了这么多,看看代码到底如何实现

                  class Transformer_CNN_RNN(nn.Module):
                      def __init__(self, base_model, num_classes):
                          super().__init__()
                          self.base_model = base_model
                          self.num_classes = num_classes
                          for param in base_model.parameters():
                              param.requires_grad = (True)
                          # Define the hyperparameters
                          self.filter_sizes = [3, 4, 5]
                          self.num_filters = 100
                          # TextCNN
                          self.convs = nn.ModuleList(
                              [nn.Conv2d(in_channels=1, out_channels=self.num_filters,
                                         kernel_size=(K, self.base_model.config.hidden_size)) for K in self.filter_sizes]
                          )
                          # LSTM
                          self.lstm = nn.LSTM(input_size=self.base_model.config.hidden_size,
                                              hidden_size=320,
                                              num_layers=1,
                                              batch_first=True)
                          self.block = nn.Sequential(
                              nn.Dropout(0.5),
                              nn.Linear(620, 128),
                              nn.Linear(128, 16),
                              nn.Linear(16, num_classes),
                              nn.Softmax(dim=1)
                          )
                      def conv_pool(self, tokens, conv):
                          # x -> [batch,1,text_length,768]
                          tokens = conv(tokens)  # shape [batch_size, out_channels, x.shape[2] - conv.kernel_size[0] + 1, 1]
                          tokens = F.relu(tokens)
                          tokens = tokens.squeeze(3)  # shape [batch_size, out_channels, x.shape[2] - conv.kernel_size[0] + 1]
                          tokens = F.max_pool1d(tokens, tokens.size(2))  # shape[batch, out_channels, 1]
                          out = tokens.squeeze(2)  # shape[batch, out_channels]
                          return out
                      def forward(self, inputs):
                          raw_outputs = self.base_model(**inputs)
                          cnn_tokens = raw_outputs.last_hidden_state.unsqueeze(1)  # shape [batch_size, 1, max_len, hidden_size]
                          cnn_out = torch.cat([self.conv_pool(cnn_tokens, conv) for conv in self.convs],
                                              1)  # shape  [batch_size, self.num_filters * len(self.filter_sizes]
                          rnn_tokens = raw_outputs.last_hidden_state
                          rnn_outputs, _ = self.lstm(rnn_tokens)
                          rnn_out = rnn_outputs[:, -1, :]
                          # cnn_out --> [batch,300]
                          # rnn_out --> [batch,320]
                          out = torch.cat((cnn_out, rnn_out), 1)
                          predicts = self.block(out)
                          return predict

                  image.gif

                  5.10 Attention+LSTM+TextCNN

                  既然我们的网络模型同时加了LSTM和TextCNN,那作为当前的前沿模型Attention怎么能少呢?

                  从IO角度来看Self-Attention,它输入是什么尺寸的矩阵,输出也是什么尺寸的矩阵,因此一个简单的做法就是将Bert的输出,先放到Attention模块中,将Attention的输出放到后续的Lstm和TexCNN

                  代码如下

                  class Transformer_CNN_RNN_Attention(nn.Module):
                      def __init__(self, base_model, num_classes):
                          super().__init__()
                          self.base_model = base_model
                          self.num_classes = num_classes
                          for param in base_model.parameters():
                              param.requires_grad = (True)
                          # Define the hyperparameters
                          self.filter_sizes = [3, 4, 5]
                          self.num_filters = 100
                          # TextCNN
                          self.convs = nn.ModuleList(
                              [nn.Conv2d(in_channels=1, out_channels=self.num_filters,
                                         kernel_size=(K, self.base_model.config.hidden_size)) for K in self.filter_sizes]
                          )
                          # LSTM
                          self.lstm = nn.LSTM(input_size=self.base_model.config.hidden_size,
                                              hidden_size=320,
                                              num_layers=1,
                                              batch_first=True)
                          # Self-Attention
                          self.key_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self.query_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self.value_layer = nn.Linear(self.base_model.config.hidden_size, self.base_model.config.hidden_size)
                          self._norm_fact = 1 / math.sqrt(self.base_model.config.hidden_size)
                          self.block = nn.Sequential(
                              nn.Dropout(0.5),
                              nn.Linear(620, 128),
                              nn.Linear(128, 16),
                              nn.Linear(16, num_classes),
                              nn.Softmax(dim=1)
                          )
                      def conv_pool(self, tokens, conv):
                          # x -> [batch,1,text_length,768]
                          tokens = conv(tokens)  # shape [batch_size, out_channels, x.shape[2] - conv.kernel_size[0] + 1, 1]
                          tokens = F.relu(tokens)
                          tokens = tokens.squeeze(3)  # shape [batch_size, out_channels, x.shape[2] - conv.kernel_size[0] + 1]
                          tokens = F.max_pool1d(tokens, tokens.size(2))  # shape[batch, out_channels, 1]
                          out = tokens.squeeze(2)  # shape[batch, out_channels]
                          return out
                      def forward(self, inputs):
                          raw_outputs = self.base_model(**inputs)
                          tokens = raw_outputs.last_hidden_state
                          # Self-Attention
                          K = self.key_layer(tokens)
                          Q = self.query_layer(tokens)
                          V = self.value_layer(tokens)
                          attention = nn.Softmax(dim=-1)((torch.bmm(Q, K.permute(0, 2, 1))) * self._norm_fact)
                          attention_output = torch.bmm(attention, V)
                          # TextCNN
                          cnn_tokens = attention_output.unsqueeze(1)  # shape [batch_size, 1, max_len, hidden_size]
                          cnn_out = torch.cat([self.conv_pool(cnn_tokens, conv) for conv in self.convs],
                                              1)  # shape  [batch_size, self.num_filters * len(self.filter_sizes]
                          rnn_tokens = tokens
                          rnn_outputs, _ = self.lstm(rnn_tokens)
                          rnn_out = rnn_outputs[:, -1, :]
                          # cnn_out --> [batch,300]
                          # rnn_out --> [batch,320]
                          out = torch.cat((cnn_out, rnn_out), 1)
                          predicts = self.block(out)
                          return predicts

                  image.gif

                  六、Train and Test

                  最后就是编写对应的训练函数和测试函数啦

                  可能有些小伙伴不懂tqdm函数,它的功能就是能显示动态进度,如下图所示

                  image.gif编辑

                  训练函数

                  注意要开启训练模式,即self.Mymodel.train(),如此有些层如dropout层参数可以进行更新

                  def _train(self, dataloader, criterion, optimizer):
                          train_loss, n_correct, n_train = 0, 0, 0
                          # Turn on the train mode
                          self.Mymodel.train()
                          for inputs, targets in tqdm(dataloader, disable=self.args.backend, ascii='>='):
                              inputs = {k: v.to(self.args.device) for k, v in inputs.items()}
                              targets = targets.to(self.args.device)
                              predicts = self.Mymodel(inputs)
                              loss = criterion(predicts, targets)
                              optimizer.zero_grad()
                              loss.backward()
                              optimizer.step()
                              train_loss += loss.item() * targets.size(0)
                              n_correct += (torch.argmax(predicts, dim=1) == targets).sum().item()
                              n_train += targets.size(0)
                          return train_loss / n_train, n_correct / n_train

                  image.gif

                  测试函数

                  注意要开启验证模式,即self.Mymodel.eval(),如此有些层如dropout参数不会更新了

                  def _test(self, dataloader, criterion):
                          test_loss, n_correct, n_test = 0, 0, 0
                          # Turn on the eval mode
                          self.Mymodel.eval()
                          with torch.no_grad():
                              for inputs, targets in tqdm(dataloader, disable=self.args.backend, ascii=' >='):
                                  inputs = {k: v.to(self.args.device) for k, v in inputs.items()}
                                  targets = targets.to(self.args.device)
                                  predicts = self.Mymodel(inputs)
                                  loss = criterion(predicts, targets)
                                  test_loss += loss.item() * targets.size(0)
                                  n_correct += (torch.argmax(predicts, dim=1) == targets).sum().item()
                                  n_test += targets.size(0)
                          return test_loss / n_test, n_correct / n_test

                  image.gif

                  最后在run函数中进行多次训练和获取最佳训练准确率

                  # Get the best_loss and the best_acc
                  best_loss, best_acc = 0, 0
                  for epoch in range(self.args.num_epoch):
                      train_loss, train_acc = self._train(train_dataloader, criterion, optimizer)
                      test_loss, test_acc = self._test(test_dataloader, criterion)
                      if test_acc > best_acc or (test_acc == best_acc and test_loss < best_loss):
                            best_acc, best_loss = test_acc, test_loss

                  image.gif

                  七、Result

                  到了最快乐的炼丹时间,看看最终的效果怎么样

                  image.gif编辑

                  分析

                    • LSTM与BiLSTM的效果总体上要优于其他模型
                    • Roberta比Bert的效果好,Roberta不愧是升级版Bert
                    • FNN的效果好是因为Transformers本身就是LSTM的改进版
                    • Roberta+LSTM/BiLSTM的组合效果是最优的
                    • 纯TextCNN的效果有点鸡肋,因为CNN主要关注局部而RNN关注全局
                    • 新增的Attention、TextCNN+LSTM、Attention+TexCNN+LSTM还没有出结果,如果有其他小伙伴跑出来欢迎在评论区告诉我们

                    八、Conclusion

                    目前该数据集的SOTA是使用XLNet模型跑的96.21%,本模型只是用了10%的数据集+简单的网络架构+未调参就可以达到93%的准确率,效果还是不错的😁😁😁

                    image.gif编辑

                    在白嫖Github项目的时候,麻烦大家动动鼠标给个star,这对我很重要🥰🥰🥰

                    九、Reference

                    [1]  Yoon Kim. 2014. Convolutional Neural Networks for Sentence Classification. In Proceedings of the 2014 Conference on Empirical Methods in Natural Language Processing (EMNLP), pages 1746–1751, Doha, Qatar. Association for Computational Linguistics.

                    [2] Vaswani, A. ,  Shazeer, N. ,  Parmar, N. ,  Uszkoreit, J. ,  Jones, L. , &  Gomez, A. N. , et al. (2017). Attention is all you need. arXiv.

                    十、Another question

                    在这里都是读者私发给我,抑或是在评论区中出现的问题,因此在此进行汇总

                    Question 1:How to add precision,recall,specificity and f1_scall

                    在文本分类中一般是只使用Accuracy作为评判标准,因此其他的标准就没有放到开源代码中了,若想实现这些功能,在train()、test()中进行修改代码即可,在这里我以train()函数为例

                    现放上修改后完整的train()代码

                    def _train(self, dataloader, criterion, optimizer):
                            train_loss, n_correct, n_train = 0, 0, 0
                            # Confusion matrix
                            TP, TN, FP, FN = 0, 0, 0, 0
                            # Turn on the train mode
                            self.Mymodel.train()
                            for inputs, targets in tqdm(dataloader, disable=self.args.backend, ascii='>='):
                                inputs = {k: v.to(self.args.device) for k, v in inputs.items()}
                                targets = targets.to(self.args.device)
                                predicts = self.Mymodel(inputs)
                                loss = criterion(predicts, targets)
                                optimizer.zero_grad()
                                loss.backward()
                                optimizer.step()
                                train_loss += loss.item() * targets.size(0)
                                n_correct += (torch.argmax(predicts, dim=1) == targets).sum().item()
                                n_train += targets.size(0)
                                ground_truth = targets
                                predictions = torch.argmax(predicts, dim=1)
                                TP += torch.logical_and(predictions.bool(), ground_truth.bool()).sum().item()
                                FP += torch.logical_and(predictions.bool(), ~ground_truth.bool()).sum().item()
                                FN += torch.logical_and(~predictions.bool(), ground_truth.bool()).sum().item()
                                TN += torch.logical_and(~predictions.bool(), ~ground_truth.bool()).sum().item()
                            precision = TP / (TP + FP)
                            recall = TP / (TP + FN)
                            specificity = TN / (TN + FP)
                            f1_score = 2 * precision * recall / (precision + recall)
                            return train_loss / n_train, n_correct / n_train, precision, recall, specificity, f1_score

                    image.gif

                    targets就是真实的标签,那预测结果呢,是predicts吗?当然不是,因为predicts中是预测每条句子的情感类别是positive和negative分别概率是多少,比如一条句子的预测结果是【0.2,0.8】,那么我们需要将该句的判定结果为0.8,也就是下标1,所以torch.argmax(predicts,dim=1)才是真正的预测结果。

                    有了这两个之后,我们便可以计算混淆矩阵,这里我使用了logical_and和bool()函数的技巧去实现它,而没有直接用各种库函数,目的是为了展示我是如何处理数据的,这样如果大家想要有其他评判标准也可以由此借鉴

                    最后计算各个指标,返回即可。

                    Question 2:Why run so slowly

                    在config.py,我设置的默认环境是CPU,以及train_batch_size和test_batch_size都设置的比较小,目的是让大家都能先跑起来这个程序,然后根据自己设备资源的情况,来扩大batch_size以及将cpu改成cuda

                    image.gif编辑

                    Question 3:How to improve the acc on my dataset

                    可能很多小伙伴在用了自己的数据集后发现准确率可能停在80%,上不去了,其实这是非常正常的情况,该博客的目的就是让大家直接尝试学习最先进的大模型以及Encoder-Decoder结构,这是目前的趋势。

                    在该框架基础上可以衍生出无数的做法,你尽可尝试不同的网络模块,加些其他技巧,比如当下最前沿的Prompt learning、Contrastive learning、Diffusion model、Knowledge模型等等

                    当然你也可以尝试最简单的模块,比如Layer Normalization、Batch Normalization、新的激活函数等等

                    在这里我也展示Layer Normalization的技巧模块

                    以下取自LSTM的网络部分,大家需要关注的是#Layer Normalization下的两行代码

                    def forward(self, inputs):
                            raw_outputs = self.base_model(**inputs)
                            tokens = raw_outputs.last_hidden_state
                            lstm_output, _ = self.Lstm(tokens)
                            # Layer Normalization
                            norm_lstm = nn.LayerNorm([lstm_output.shape[1], lstm_output.shape[2]], eps=1e-8).cuda()
                            ln_lstm = norm_lstm(lstm_output)
                            outputs = lstm_output[:, -1, :]
                            outputs = self.fc(outputs)
                            return outputs

                    image.gif


                    相关实践学习
                    【AI破次元壁合照】少年白马醉春风,函数计算一键部署AI绘画平台
                    本次实验基于阿里云函数计算产品能力开发AI绘画平台,可让您实现“破次元壁”与角色合照,为角色换背景效果,用AI绘图技术绘出属于自己的少年江湖。
                    从 0 入门函数计算
                    在函数计算的架构中,开发者只需要编写业务代码,并监控业务运行情况就可以了。这将开发者从繁重的运维工作中解放出来,将精力投入到更有意义的开发任务上。
                    目录
                    相关文章
                    |
                    30天前
                    |
                    PyTorch 算法框架/工具 异构计算
                    PyTorch 2.0性能优化实战:4种常见代码错误严重拖慢模型
                    我们将深入探讨图中断(graph breaks)和多图问题对性能的负面影响,并分析PyTorch模型开发中应当避免的常见错误模式。
                    108 9
                    |
                    3月前
                    |
                    机器学习/深度学习 存储 PyTorch
                    PyTorch + MLFlow 实战:从零构建可追踪的深度学习模型训练系统
                    本文通过使用 Kaggle 数据集训练情感分析模型的实例,详细演示了如何将 PyTorch 与 MLFlow 进行深度集成,实现完整的实验跟踪、模型记录和结果可复现性管理。文章将系统性地介绍训练代码的核心组件,展示指标和工件的记录方法,并提供 MLFlow UI 的详细界面截图。
                    122 2
                    PyTorch + MLFlow 实战:从零构建可追踪的深度学习模型训练系统
                    |
                    6月前
                    |
                    机器学习/深度学习 自然语言处理 算法
                    PyTorch PINN实战:用深度学习求解微分方程
                    物理信息神经网络(PINN)是一种将深度学习与物理定律结合的创新方法,特别适用于微分方程求解。传统神经网络依赖大规模标记数据,而PINN通过将微分方程约束嵌入损失函数,显著提高数据效率。它能在流体动力学、量子力学等领域实现高效建模,弥补了传统数值方法在高维复杂问题上的不足。尽管计算成本较高且对超参数敏感,PINN仍展现出强大的泛化能力和鲁棒性,为科学计算提供了新路径。文章详细介绍了PINN的工作原理、技术优势及局限性,并通过Python代码演示了其在微分方程求解中的应用,验证了其与解析解的高度一致性。
                    899 5
                    PyTorch PINN实战:用深度学习求解微分方程
                    |
                    9月前
                    |
                    人工智能 安全 PyTorch
                    SPDL:Meta AI 推出的开源高性能AI模型数据加载解决方案,兼容主流 AI 框架 PyTorch
                    SPDL是Meta AI推出的开源高性能AI模型数据加载解决方案,基于多线程技术和异步事件循环,提供高吞吐量、低资源占用的数据加载功能,支持分布式系统和主流AI框架PyTorch。
                    323 10
                    SPDL:Meta AI 推出的开源高性能AI模型数据加载解决方案,兼容主流 AI 框架 PyTorch
                    |
                    机器学习/深度学习 算法 PyTorch
                    【从零开始学习深度学习】38. Pytorch实战案例:梯度下降、随机梯度下降、小批量随机梯度下降3种优化算法对比【含数据集与源码】
                    【从零开始学习深度学习】38. Pytorch实战案例:梯度下降、随机梯度下降、小批量随机梯度下降3种优化算法对比【含数据集与源码】
                    |
                    11月前
                    |
                    自然语言处理 PyTorch 算法框架/工具
                    掌握从零到一的进阶攻略:让你轻松成为BERT微调高手——详解模型微调全流程,含实战代码与最佳实践秘籍,助你应对各类NLP挑战!
                    【10月更文挑战第1天】随着深度学习技术的进步,预训练模型已成为自然语言处理(NLP)领域的常见实践。这些模型通过大规模数据集训练获得通用语言表示,但需进一步微调以适应特定任务。本文通过简化流程和示例代码,介绍了如何选择预训练模型(如BERT),并利用Python库(如Transformers和PyTorch)进行微调。文章详细说明了数据准备、模型初始化、损失函数定义及训练循环等关键步骤,并提供了评估模型性能的方法。希望本文能帮助读者更好地理解和实现模型微调。
                    765 2
                    掌握从零到一的进阶攻略:让你轻松成为BERT微调高手——详解模型微调全流程,含实战代码与最佳实践秘籍,助你应对各类NLP挑战!
                    |
                    机器学习/深度学习 人工智能 PyTorch
                    【深度学习】使用PyTorch构建神经网络:深度学习实战指南
                    PyTorch是一个开源的Python机器学习库,特别专注于深度学习领域。它由Facebook的AI研究团队开发并维护,因其灵活的架构、动态计算图以及在科研和工业界的广泛支持而受到青睐。PyTorch提供了强大的GPU加速能力,使得在处理大规模数据集和复杂模型时效率极高。
                    341 59
                    |
                    机器学习/深度学习 PyTorch TensorFlow
                    【PyTorch】PyTorch深度学习框架实战(一):实现你的第一个DNN网络
                    【PyTorch】PyTorch深度学习框架实战(一):实现你的第一个DNN网络
                    428 2
                    |
                    机器学习/深度学习 数据挖掘 TensorFlow
                    解锁Python数据分析新技能,TensorFlow&PyTorch双引擎驱动深度学习实战盛宴
                    在数据驱动时代,Python凭借简洁的语法和强大的库支持,成为数据分析与机器学习的首选语言。Pandas和NumPy是Python数据分析的基础,前者提供高效的数据处理工具,后者则支持科学计算。TensorFlow与PyTorch作为深度学习领域的两大框架,助力数据科学家构建复杂神经网络,挖掘数据深层价值。通过Python打下的坚实基础,结合TensorFlow和PyTorch的强大功能,我们能在数据科学领域探索无限可能,解决复杂问题并推动科研进步。
                    196 0
                    |
                    机器学习/深度学习 数据挖掘 TensorFlow
                    解锁Python数据分析新技能,TensorFlow&PyTorch双引擎驱动深度学习实战盛宴
                    【7月更文挑战第31天】在数据驱动时代,Python凭借其简洁性与强大的库支持,成为数据分析与机器学习的首选语言。**数据分析基础**从Pandas和NumPy开始,Pandas简化了数据处理和清洗,NumPy支持高效的数学运算。例如,加载并清洗CSV数据、计算总销售额等。
                    160 2

                    推荐镜像

                    更多