深度学习处理文本分类

Lee
Lee
发布于 2024-03-01 / 59 阅读
0
0

深度学习处理文本分类

配置文件

Config = {
    "model_path": "output",
    'ret_data_path': "output/ret_data.csv",
    "train_data_path": "test_data.csv",
    "valid_data_path": "validation_data.csv",
    "vocab_path": "chars.txt",
    "model_type": "fast_text",
    "max_length": 30,
    "hidden_size": 256,
    "kernel_size": 3,
    "num_layers": 2,
    "epoch": 15,
    "batch_size": 128,
    "pooling_style": "max",
    "optimizer": "adam",
    "learning_rate": 1e-3,
    "pretrain_model_path": "/bert-base-chinese",
    "seed": 987
}

方法一:fast_text

利用ngram特征、embedding和mean pooling进行分类。

1.初始化输入数据

通过词表找每句话中每个字的索引,

也就是每句话shape=[batch_size,max_length]=128*30

2.接下来创建模型

embedding层设置需要词表长度和每个词的维度

self.embedding = nn.Embedding(vocab_size, hidden_size, padding_idx=0)

数据进入embedding层后,shape变为[128*30*256]

我们的最终想要的输出shape为[128*2]这里是二分类任务

也就是将数据pooling池化(提取一个固定长度的向量来表示整个句子或段落)变为[128*256*1]:

池化有两种方式:

池化最大池化(Max Pooling):

优点:能够捕获序列中最显著的特征,对于那些需要识别关键信息的任务可能更有利。

缺点:可能会丢失序列中其他重要的但不那么突出的信息。

平均池化(Avg Pooling):

优点:考虑了整个序列的所有特征,提供了对输入序列的整体平均表示,有助于保留更多的全局上下文信息。

缺点:可能无法强调特别重要的特征,特别是在噪声较大的情况下,平均值可能会被稀释。

在文本处理任务中:

对于某些任务,如关键词抽取或情感分析,最大池化可能更能捕捉到决定性的词语特征。

对于需要理解整体语义或主题的任务,平均池化可能更合适,因为它可以提供一个平滑、均衡的句子表示。

其中squeeze()是从张量的形状(shape)中去除所有维度为1的尺寸,变为

[128*256]

self.pooling_layer = nn.MaxPool1d(x.shape[1])
x = self.pooling_layer(x.transpose(1, 2)).squeeze()

再接入线性层

self.classify = nn.Linear(hidden_size, class_num)

输出就变为[128*2]

3.核心步骤

optimizer.zero_grad() # 优化器清零之前累积的梯度
input_ids, labels = batch_data  #  [128*30]
loss = model(input_ids, labels) # 计算损失,经过ebedding,池化
loss.backward() # 计算梯度
optimizer.step() #利用之前计算出的梯度更新模型参数

具体训练步骤:


#训练
for epoch in range(config["epoch"]):
    epoch += 1
    model.train()
    logger.info("epoch %d begin" % epoch)
    train_loss = []
    for index, batch_data in enumerate(train_data):
        if cuda_flag:
            batch_data = [d.cuda() for d in batch_data]

        optimizer.zero_grad()
        input_ids, labels = batch_data   #输入变化时这里需要修改,比如多输入,多输出的情况
        loss = model(input_ids, labels)
        loss.backward()
        optimizer.step()

        train_loss.append(loss.item())
        if index % int(len(train_data) / 2) == 0:
            logger.info("batch loss %f" % loss)
    logger.info("epoch average loss: %f" % np.mean(train_loss))
    acc, use_time = evaluator.eval(epoch)
model_path = os.path.join(config["model_path"], "{}.pth".format(config['model_type']))
ret_data_path = config["ret_data_path"]
torch.save(model.state_dict(), model_path)  #保存模型权重

方法二:循环神经网络(RNN)/长短期记忆网络(LSTM)/门控循环单元(GRU)

利用RNN编码文本,取最后一个状态进行分类。

1.LSTM

遗忘门(Forget Gate):决定从上一时间步的记忆细胞状态中丢弃哪些信息。

输入门(Input Gate):控制当前时间步新输入的数据哪些应该被添加到记忆细胞状态中。

输出门(Output Gate):决定基于当前记忆细胞状态生成什么内容作为当前时间步的隐藏状态输出。

记忆细胞(Cell State):可以理解为一种线性通道,它允许信息不受衰减地流动,从而避免了梯度长时间连乘带来的消失问题。

LSTM中的每个门都由一个sigmoid激活函数控制,该函数输出值范围在0至1之间,有助于稳定梯度的大小。同时,记忆细胞状态的更新是通过加法和乘法而非连续乘积完成的,因此即使在网络深度增加时,也能较好地保留和传播信息。

2.GRU

重置门(Reset Gate):决定是否忽略过去的时间步信息,更新当前的候选隐藏状态。

更新门(Update Gate):确定当前隐藏状态多大程度上应由新的候选隐藏状态替换。

候选隐藏状态(Candidate Hidden State):结合当前输入和过去隐藏状态计算出一个新的潜在隐藏状态。

同样地,GRU也使用sigmoid函数控制门的状态,确保梯度不会由于连乘效应而快速消失或增长过大。通过合理地组合过去的隐藏状态与当前输入信号,GRU能够有效地管理信息流并减少梯度消失现象。

encode层选取:

self.encoder = nn.RNN(hidden_size, hidden_size, num_layers=num_layers)

self.encoder = nn.LSTM(hidden_size, hidden_size, num_layers=num_layers)

self.encoder = nn.GRU(hidden_size, hidden_size, num_layers=num_layers)

送入编码前还是需要embedding,然后进入encoder,因为rnn的encode会返回2个输出,我们仅需第一个, 然后pooling层,后进入全连接层进行分类:

序列输出结果:

形状为 (batch_size, sen_len, hidden_size) 的张量。这表示模型在每个时间步生成的隐藏状态向量,其中hidden_size是RNN层的隐藏单元个数。

最后一个时间步的隐藏状态(仅当需要时使用):

形状为 (num_layers, batch_size, hidden_size) 的张量,这里num_layers是RNN的层数。

if isinstance(x, tuple):  #RNN类的模型会同时返回隐单元向量,我们只取序列结果
    x = x[0]

代码如方法一,只不过将encode更换

方法三:TextCNN

利用一维卷积编码文本

送入编码前还是需要embedding

encoder层:(输入什么维度输出什么维度)

hidden_size = config["hidden_size"]
kernel_size = config["kernel_size"]
pad = int((kernel_size - 1)/2)
self.cnn = nn.Conv1d(hidden_size, hidden_size, kernel_size, bias=False, padding=pad)
self.cnn(x.transpose(1, 2)).transpose(1, 2)

封装成类,方便其他组合:

class CNN(nn.Module):
    def __init__(self, config):
        super(CNN, self).__init__()
        hidden_size = config["hidden_size"]
        kernel_size = config["kernel_size"]
        pad = int((kernel_size - 1)/2)
        self.cnn = nn.Conv1d(hidden_size, hidden_size, kernel_size, bias=False, padding=pad)

    def forward(self, x): #x : (batch_size, max_len, embeding_size)
        return self.cnn(x.transpose(1, 2)).transpose(1, 2)

方法四:Gated CNN

在CNN基础上增加门控机制

方法解读:

输入 x 是形状为 (batch_size, max_len, embedding_size) 的张量。

首先通过 self.cnn(x) 计算得到一个特征图 a,这一步相当于对输入数据进行一次基础的卷积操作以提取特征。

接着使用 self.gate(x) 计算另一个特征图 b。这里将 CNN 模块用作门控机制来学习何时开启或关闭前面的基础卷积输出的各个部分。

对特征图 b 应用 sigmoid 函数 torch.sigmoid(b),使得 b 的每个元素范围在0到1之间,起到类似于开关的作用,决定是否让对应位置的 a 的信息通过。

最后,将 a 和经过sigmoid激活后的 b 进行逐元素相乘操作 torch.mul(a, b),生成最终的输出。这样做的目的是让模型根据 gate 输出的权重值动态地控制哪些基础卷积特征会被保留并传递到后续层中,从而增强模型的表达能力和学习效果。

class GatedCNN(nn.Module):
    def __init__(self, config):
        super(GatedCNN, self).__init__()
        self.cnn = CNN(config)
        self.gate = CNN(config)

    def forward(self, x):
        a = self.cnn(x)
        b = self.gate(x)
        b = torch.sigmoid(b)
        return torch.mul(a, b)

方法五:BERT

BERT模型处理输入序列的过程包括以下步骤,从原始的词索引转换为最终的输出 (batch_size, max_len, hidden_size):

Tokenization

输入文本首先通过分词器(如WordPiece)进行分词,并将每个单词或子词映射到词汇表中的唯一ID。

Input Formatting

对于每个样本,生成一个由token ID、segment ID(如果有两个句子需要区分)和position ID组成的序列。

形状变为 (batch_size, max_len),其中 max_len 是经过截断或填充后的最长序列长度,保证批次内所有序列长度相同。

Embeddings Layer

BERT模型对每个token ID应用嵌入层得到词嵌入向量。

同时,为每个token加上位置嵌入和段落嵌入(如果适用),这一步后每个token都有了综合表示。

通过相加这三个嵌入(词嵌入、位置嵌入、段落嵌入),生成形状为 (batch_size, max_len, embedding_size) 的张量。

Transformer Layers

将嵌入后的序列输入到多层Transformer编码器结构中,每一层包含自注意力机制(Self-Attention)和前馈神经网络(Feed-Forward Network)。

每个Transformer层会保持输入的形状不变,即 (batch_size, max_len, hidden_size),这里的 hidden_size 可能与 embedding_size 相同或者不同,取决于BERT模型的具体配置。

Sequence Output

经过多层Transformer编码器之后,得到的最后一个隐藏状态序列就是 sequence_output,其形状为 (batch_size, max_len, hidden_size)。

因此,BERT模型通过上述流程,将原始的词索引形式的输入转化为具有上下文信息丰富的高维特征向量。

self.encoder = BertModel.from_pretrained(config["pretrain_model_path"], return_dict=False)

在BERT模型中,默认情况下(即 return_dict=True),

model(**inputs) 的返回值是一个字典,包含诸如 "last_hidden_state"、"pooler_output" 等键值对。

但如果设置了 return_dict=False,则模型将返回一个包含相应张量的元组。

具体到BERT模型,如果 return_dict=False:

BERT模型的前向传播会返回一个元组,通常格式是 (sequence_output, pooled_output):

sequence_output:形状为 (batch_size, sequence_length, hidden_size) 的张量,表示每个输入token经过BERT编码器后的隐藏状态。

pooled_output:形状为 (batch_size, hidden_size) 的张量,这是CLS标记([CLS] token)对应的隐藏状态,常用于分类任务。

不需要embedding,即可进行encode

其他方法

当然我们可以用bert和cnn,lstm进行组合,这里就不再赘述


评论