Skip to content

Latest commit

 

History

History
447 lines (338 loc) · 14.6 KB

基于深度学习的文本分类.md

File metadata and controls

447 lines (338 loc) · 14.6 KB

任务二:基于深度学习的文本分类

任务描述

利用Pytorch重写《任务一》,实现基于CNN、RNN的文本分类。

  1. 参考

    1. https://pytorch.org/
    2. Convolutional Neural Networks for Sentence Classification https://arxiv.org/abs/1408.5882
    3. https://machinelearningmastery.com/sequence-classification-lstm-recurrent-neural-networks-python-keras/
  2. word embedding 的方式初始化

  3. 随机embedding的初始化方式

  4. 用glove 预训练的embedding进行初始化 https://nlp.stanford.edu/projects/glove/

  5. 知识点:

    1. CNN/RNN的特征抽取
    2. 词嵌入
    3. Dropout
  6. 时间:两周


任务理解

词嵌入模型:随机初始化、Glove embedding。

网络构建:TextCNN、LSTM、BiLstm。

框架:pytorch,sklearn


数据预处理

对于NLP任务,我们利用torchtext对文本数据进行封装。核心是四步

  1. 利用Field对分词方法,序列化,是否转成小写,起始字符,结束字符,补全字符以及词典等进行定义。在本次实验采用的是jieba分词。
def tokenizer(text):    
    return [wd for wd in jieba.cut(text, cut_all=False)]

en_stopwords=stopwords.words('english')
LABEL = data.Field(sequential=False, use_vocab=False)
TEXT = data.Field(sequential=True, tokenize=tokenizer, lower=True, stop_words=en_stopwords)

在后面使用的两个模型中,LSTM模型是可以处理可变长的文本的,但是TextCNN只能处理定长文本。因此如果是TextCNN需要加上fix_length参数,限定文本的长度。如果文本太短则补全,如果文本太长则截断。

  1. 利用TabularDataset 进行数据加载。
train, val = data.TabularDataset.splits(
    path='./data', train='train.csv', validation='val.csv', format='csv', skip_header=True,
    fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT), ('Sentiment', LABEL)]
)
test = data.TabularDataset('./data/test.tsv', format='tsv', skip_header=True, 
                           fields=[('PhraseId', None), ('SentenceId', None), ('Phrase', TEXT)])
  1. 构建词表。因为后续可能会用到word embedding ,因此提前先构建好词表。后续如果使用的是glove的embedding我们就需要将其加载到模型中。
# 建立vocab(加载预训练的词向量,如果路径没有该词向量,会自动下载)
TEXT.build_vocab(train, vectors='glove.6B.100d')#, max_size=30000)
# 当 corpus 中有的 token 在 vectors 中不存在时 的初始化方式.
TEXT.vocab.vectors.unk_init = init.xavier_uniform
  1. 构造迭代器,确定batch_size。
# 构造迭代器
'''
sort_key指在一个batch内根据文本长度进行排序。
'''
train_iter = data.BucketIterator(train, batch_size=128, sort_key=lambda x: len(x.Phrase), 
                                 shuffle=True,device=DEVICE)

val_iter = data.BucketIterator(val, batch_size=128, sort_key=lambda x: len(x.Phrase), 
                                 shuffle=True,device=DEVICE)

# 在 test_iter , sort一定要设置成 False, 要不然会被 torchtext 搞乱样本顺序
test_iter = data.Iterator(dataset=test, batch_size=128, train=False,
                          sort=False, device=DEVICE)

模型构建

LSTM

对lstm的理解可以参考另外一篇博客《lstm 从理论到实践》。最基本的模型构建上使用的是单向的双层LSTM,首先对数据进行embedding,进过lstm,序列的最后一个输出作为特征,接入线性层并做softmax。如下是我在实验中构建的lstm。

class LSTM_base(nn.Module):
    #定义模型中使用的所有层
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, 
                 bidirectional, dropout):
        #构造函数
        super().__init__()
        #embeddding层
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        #lstm层
        self.lstm = nn.LSTM(embedding_dim, 
                           hidden_dim,
                           num_layers=n_layers,
                           bidirectional=bidirectional, 
                           dropout=dropout,
                           batch_first=True)
        self.fc = nn.Linear(hidden_dim , output_dim)
    def forward(self, text):
        #text = [batch size,sent_length]
        embedded = self.embedding(text)        
        out,_=self.lstm(embedded)
        out=self.fc(out[:,-1,:])
        #最终激活函数
        out = F.softmax(out,-1)
        return out

其中对于output的取值,我们需要取的是lstm的最后一维,但是因为有batch_first的缘故,所以我们是取out的第二维的最后一个数。但是在上述LSTM的实验过程中,发现效果并不良好,有大神告诉了我max-pool的双层结构。如下是这种结构的描述:

class LSTM_MAX(nn.Module):
    #定义模型中使用的所有层
    def __init__(self, vocab_size, embedding_dim, hidden_dim, output_dim, n_layers, 
                 bidirectional, dropout):
        #构造函数
        super().__init__()
        #embeddding层
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        #lstm层
        self.lstm = nn.LSTM(embedding_dim, 
                           hidden_dim,
                           num_layers=1,
                           dropout=dropout,
                           batch_first=True)
        self.fc = nn.Linear(hidden_dim*2 , output_dim)
    def forward(self, text):
        #text = [batch size,sent_length]
        h_embedding = self.embedding(text)     

        h_lstm1, _ = self.lstm(h_embedding)

        h_lstm2, _ = self.lstm(h_lstm1)
        
        # global average pooling
        avg_pool = torch.mean(h_lstm2, 1)
        # global max pooling
        max_pool, _ = torch.max(h_lstm2, 1)
        h_conc = torch.cat((max_pool, avg_pool), 1)
        out=self.fc(h_conc)
        #最终激活函数
        out = F.softmax(out,-1)
        return out

在实际测试时发现效果仍然是一般。

参数设置与模型的实例化:

#定义超参数
size_of_vocab = len(TEXT.vocab)
embedding_dim = 100
num_hidden_nodes = 100
num_output_nodes = 5
num_layers = 2
bidirection = False
dropout = 0.4

#实例化模型
model = LSTM_base(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers, 
                   bidirectional = bidirection, dropout = dropout)
# model = LSTM_MAX(size_of_vocab, embedding_dim, num_hidden_nodes,num_output_nodes, num_layers, 
#                    bidirectional = bidirection, dropout = dropout)   

#模型框架
print(model)
#可训练参数的数量
def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'The model has {count_parameters(model):,} trainable parameters')
#初始化预训练的词嵌入
pretrained_embeddings = TEXT.vocab.vectors
model.embedding.weight.data.copy_(pretrained_embeddings)
print(pretrained_embeddings.shape)  

#定义优化器和损失
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()
#定义度量指标
def binary_accuracy(preds, y):

    #round预测到最接近的整数
    # rounded_preds = torch.round(preds)
    correct = (preds == y).float() 
    acc = correct.sum() / len(correct)
    return acc
#转化为cuda(如果可用)
model = model.to(DEVICE)
criterion = criterion.to(DEVICE)

TextCNN

网络架构如下:

embedding_size = 60
sequence_length = FIX_LENGTH # every sentences contains sequence_length(=3) words
num_classes = 5  # 0 or 1
batch_size = 128

vocab_size = len(TEXT.vocab)

class TextCNN(nn.Module):
    def __init__(self):
        super(TextCNN, self).__init__()
        self.W = nn.Embedding(vocab_size, embedding_size)
        output_channel = 3
        self.conv = nn.Sequential(
            # conv : [input_channel(=1), output_channel, (filter_height, filter_width), stride=1]
            nn.Conv2d(1, output_channel, (2, embedding_size)),
            nn.ReLU(),
            # pool : ((filter_height, filter_width))
            nn.MaxPool2d((FIX_LENGTH-1, 1)),
        )
        # fc
        self.fc = nn.Linear(output_channel, num_classes)

    def forward(self, X):
      '''
      X: [batch_size, sequence_length]
      '''
      batch_size = X.shape[0]
    #   print(batch_size)
      embedding_X = self.W(X) # [batch_size, sequence_length, embedding_size]
    #   print(embedding_X.shape)
      embedding_X = embedding_X.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
    #   print(embedding_X.shape)
      conved = self.conv(embedding_X) # [batch_size, output_channel, 1, 1]
    #   print(conved.shape)
      flatten = conved.view(batch_size, -1) # [batch_size, output_channel*1*1]
    #   print(flatten.shape)
      output = self.fc(flatten)
      return output

模型训练

单轮训练与测试

def Train(model, iterator, optimizer, criterion):
    #每个epoch进行初始化
    epoch_loss = 0
    epoch_acc = 0
    #将模型设置为训练阶段
    model.train()
    predictions_val=[]
    for batch in tqdm(iterator):
        #重设梯度
        optimizer.zero_grad()
        #获取文本和单词数量
        text = batch.Phrase
        text = text.permute(1,0)
        # print(text.shape)
        #转换为一维张量
        predictions = model(text).squeeze()
        #计算loss
        loss = criterion(predictions, batch.Sentiment)
        #计算二分类准确度
        predictions_val=  predictions.argmax(dim=1)


        acc = binary_accuracy(predictions_val, batch.Sentiment.float())
        #后向传播损失并计算梯度
        loss.backward()
        #更新权重
        optimizer.step()
        #损失和准确度
        
        epoch_loss += loss.item()  
        epoch_acc += acc.item()
    
    return epoch_loss / len(iterator), epoch_acc / len(iterator)

def Evaluate(model, iterator, criterion):
    #每个epoch进行初始化
    epoch_loss = 0
    epoch_acc = 0
    #停用dropout层
    model.eval()
    #停用自动求导
    with torch.no_grad():
        for batch in tqdm(iterator):
            #获取文本和单词数量
            text = batch.Phrase
            text = text.permute(1,0)
            #转换为一维张量
            predictions = model(text).squeeze()
            #计算损失和准确度
            loss = criterion(predictions, batch.Sentiment)
            predictions_val=   predictions.argmax(dim=1)

            acc = binary_accuracy(predictions_val, batch.Sentiment.float())
            #跟踪损失和准确度
            epoch_loss += loss.item()
            epoch_acc += acc.item()
    return epoch_loss / len(iterator), epoch_acc / len(iterator)

def Test(model, iterator, criterion):
    #每个epoch进行初始化
    epoch_loss = 0
    epoch_acc = 0
    #停用dropout层
    model.eval()
    predict_list=[]
    #停用自动求导
    with torch.no_grad():
        for batch in tqdm(iterator):
            #获取文本和单词数量
            text = batch.Phrase
            text = text.permute(1,0)
            #转换为一维张量
            predictions = model(text).squeeze()
            #计算损失和准确度

            predict=   predictions.argmax(dim=1).cpu()
            predict_list+=predict.numpy().flatten().tolist()

    return predict_list

整体训练代码

N_EPOCHS = 50
best_valid_loss = float('inf')

train_loss_list=[]
valid_loss_list=[]
train_acc_list=[]
valid_acc_list=[]

for epoch in range(N_EPOCHS):
    #训练模型
    print('epoch:',epoch)
    train_loss, train_acc = Train(model, train_iter, optimizer, criterion)
    #评估模型
    valid_loss, valid_acc = Evaluate(model, val_iter, criterion)
    #保存模型
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'saved_weights.pt')

    train_loss_list.append(train_loss)
    valid_loss_list.append(valid_loss)
    train_acc_list.append(train_acc)
    valid_acc_list.append(valid_acc)

    print(f'\tTrain Loss: {train_loss:.3f} | Train Acc: {train_acc*100:.2f}%')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. Acc: {valid_acc*100:.2f}%')

实验结果

总共做了5组实验:

模型 词嵌入方法(维度) 轮数 Dropout 编号
TextCNN random(60) 50 / 1
LSTM glove 50 0.5 2
LSTM glove 50 0.8 3
LSTM random 50 0.4 4
MAX-LSTM glove 50 0.4 5

因为设备问题,所以并没有很好的控制变量,所进行的实验大多也仅针对实验本身分析。如下是实验结果:

实验一

TextCNN收敛是最慢的,但是收敛曲线非常平滑。

textCNN-random-loss-50

textCNN-random-acc-50

实验二

droup out取0.8的时候可以看到收敛曲线也比较平滑,但是训练集和测试集的acc存在着差异越来越大的问题,我认为是过拟合了,所以减小了droupout的值以增大droupout的比例。

lstm-glove-loss-50-0.8

lstm-glove-acc-50-0.8

实验三

droupout=0.5的时候,收敛曲线变得非常陡峭,但是acc的最终结果也没有很大的变化。

lstm-glove-loss-50-0.5

实验四

采用random的embedding方式,收敛曲线最为平坦,同样的收敛速度较慢,50轮仍然没有收敛完全。

lstm-glove-loss-50-0.4-init

lstm-glove-acc-50-0.4-init

实验五

采用MaxLSTM的方式进行实验,结果很遗憾,仍然没有所谓的更新。

lstmmax-glove-loss-50

lstmmax-glove-acc-50

引用

  1. pack_padded_sequence 和 pad_packed_sequence的区别

  2. Python中 list, numpy.array, torch.Tensor 格式相互转化

  3. 使用pytorch进行英文文本分类(代码实战)

  4. 文本分类实战(四)—— Bi-LSTM模型