推荐评论展示大作业

任务要求

本次推荐评论展示任务的目标是从真实的用户评论中,挖掘合适作为推荐理由的短句。点评软件展示的推荐理由具有长度限制,而真实用户评论语言通顺、信息完整。综合来说,两者都具有用户情感的正负向,但是展示推荐理由的内容相关性高于评论,需要较强的文本吸引力。一些真实的推荐理由如下图所示:
在这里插入图片描述
具体的,需要将文本分为两类,0代表不展示,1代表展示
在这里插入图片描述
因此整个任务是一个文本分类任务,在完成任务的过程中,共尝试使用了CNN、RNN以及BERT三类模型。

CNN

数据预处理

在实现模型前首先需要将文本处理成为可以作为模型输入的数据,需要进行分词、建立词典、将文本转化为索引等步骤。

在分词阶段,我尝试了基于字的粒度以及使用结巴分词后的词的粒度,发现基于字的粒度的模型表现要更好一些。

建立词典阶段使用了torchtext中的Vocab类建立词典并将原始文本转化为索引,然后构建成为Data.TensorDataset,并再通过Data.DataLoader进行封装,从而可以迭代获取数据。

具体代码如下:

# 读取数据
train_data = open('/home/kesci/input/Comments9120/train_shuffle.txt').readlines()
test_data = open('/home/kesci/input/Comments9120/test_handout.txt').readlines()

# 提取文本并转化为单个字符
train_d = [[s for s in st.rstrip().split('\t')[1]] for st in train_data]
test_d = [[s for s in st.rstrip()] for st in test_data]
print(train_d[:5])
print(test_d[:5])

# 引入分词处理的尝试,其中stopwords是停用词,去掉停用词可以更好地进行文本分类
train_d = [jieba.lcut(st.rstrip().split('\t')[1]) for st in train_data]
test_d = [jieba.lcut(st.rstrip()) for st in test_data]
print(train_d[:5])
print(test_d[:5])

stop_words = open('/home/kesci/work/stopwords.txt').readlines()
stop_words = [s.rstrip() for s in stop_words]
print(stop_words[:5])
for i in range(len(train_d)):
    train_d[i] = [s for s in train_d[i] if s not in stop_words]
for i in range(len(test_d)):
    test_d[i] = [s for s in test_d[i] if s not in stop_words]
print(train_d[:5])
print(test_d[:5])

# 统计字符出现次数并构建字典
tmp = train_d + test_d
counter = collections.Counter([tk for st in tmp for tk in st])
vocab = Vocab.Vocab(counter, min_freq=3)
print(len(vocab))

# 将文本转化为索引tensor
max_l = 20  

def pad(x):
    return x[:max_l] if len(x) > max_l else x + [0] * (max_l - len(x))

features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in train_d])
labels = torch.tensor([int(sen.rstrip().split('\t')[0]) for sen in train_data])
test_features = torch.tensor([pad([vocab.stoi[word] for word in words]) for words in test_d])
print(features[:5])
print(labels[:5])
print(test_features[:5])

# 构建dataset与dataloader
train_set = Data.TensorDataset(features, labels)
test_set = Data.TensorDataset(test_features)
batch_size = 32
train_loader = Data.DataLoader(train_set, batch_size)
test_loader = Data.DataLoader(test_set, batch_size)
实现模型

实现模型用了课程文本分类一章的textcnn相同的结构,对其中的超参数进行了修改,代码如下:

def corr1d(X, K):
    w = K.shape[0] 
    Y = torch.zeros((X.shape[0] - w + 1))
    for i in range(Y.shape[0]): 
        Y[i] = (X[i: i + w] * K).sum()
    return Y

def corr1d_multi_in(X, K):
    return torch.stack([corr1d(x, k) for x, k in zip(X, K)]).sum(dim=0)

class GlobalMaxPool1d(nn.Module):
    def __init__(self):
        super(GlobalMaxPool1d, self).__init__()
    def forward(self, x):
        return F.max_pool1d(x, kernel_size=x.shape[2])

class TextCNN(nn.Module):
    def __init__(self, vocab, embed_size, kernel_sizes, num_channels):
        super(TextCNN, self).__init__()
        self.embedding = nn.Embedding(len(vocab), embed_size)
        self.constant_embedding = nn.Embedding(len(vocab), embed_size)
        
        self.pool = GlobalMaxPool1d()
        self.convs = nn.ModuleList()  # 创建多个一维卷积层
        for c, k in zip(num_channels, kernel_sizes):
            self.convs.append(nn.Conv1d(in_channels = embed_size, 
                                        out_channels = c, 
                                        kernel_size = k))
            
        self.decoder = nn.Linear(sum(num_channels), 2)
        self.dropout = nn.Dropout(0.5) 

    def forward(self, inputs):
        embeddings = self.embedding(inputs)
        embeddings = embeddings.permute(0, 2, 1)
        
        encoding = torch.cat([
            self.pool(F.relu(conv(embeddings))).squeeze(-1) for conv in self.convs], dim=1)
        
        outputs = self.decoder(self.dropout(encoding))
        return outputs

embed_size, kernel_sizes, nums_channels = 160, [2, 3, 4, 5], [100, 100, 100]
net = TextCNN(vocab, embed_size, kernel_sizes, nums_channels)

可以看到我将词向量的维度改成了160,kernel的大小改成了[2,3,4,5],通道数都是100。kernel的大小增加了2是为了更多的提取到信息,增大了嵌入词向量的维度也是为了存储更多的信息。

训练过程此处不表,经过试验,我选择了采用batch_size为32,epoch为8来训练模型,最终训练完成的模型提交后得到的分数为0.94004。

RNN

数据预处理

RNN模型的数据预处理与CNN相同,采用了字粒度的处理方式,此处不再赘述。

实现模型

实现RNN我是用了课程上的双层双向LSTM模型,同样对超参数进行了一些修改,代码如下:

class BiRNN(nn.Module):
    def __init__(self, vocab, embed_size, num_hiddens, num_layers):
        super(BiRNN, self).__init__()
        self.embedding = nn.Embedding(len(vocab), embed_size)
        
        self.encoder = nn.LSTM(input_size=embed_size, 
                                hidden_size=num_hiddens, 
                                num_layers=num_layers,
                                bidirectional=True)
        self.decoder = nn.Linear(4*num_hiddens, 2) 
        
    def forward(self, inputs):
        embeddings = self.embedding(inputs.permute(1, 0)) 
        outputs, _ = self.encoder(embeddings) 
        
        encoding = torch.cat((outputs[0], outputs[-1]), -1) 
        outs = self.decoder(encoding) 
        return outs

embed_size, num_hiddens, num_layers = 128, 128, 2
net = BiRNN(vocab, embed_size, num_hiddens, num_layers)

超参数上将嵌入词向量的维度以及隐藏层的维度改为了128,可以更好的提取文本中的信息

训练过程此处不表,经过尝试,我使用了batch_size为32以及epoch为5的超参数,最终使用RNN得到的分数为0.94728,表现要比CNN好一些。

BERT

BERT是Google提出的文本预训练模型,提出了四种训练方法,使用了大量的语料进行了长时间的训练,改变了NLP深度学习训练方法的格局,使得通过简单地finetune即可达到非常好的效果。在这里我也尝试使用数据进行了finetune。

数据预处理

我使用的是github上Google的tensorflow版本,其中有一些数据处理的样例,需要继承Processer类然后根据自己的数据进行实现,实现如下:

class CmtProcessor(DataProcessor):

  def get_train_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")

  def get_test_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")

  def get_labels(self):
    """See base class."""
    return ["0", "1"]

  def _create_examples(self, lines, set_type):
    """Creates examples for the training and dev sets."""
    examples = []
    for (i, line) in enumerate(lines):
      guid = "%s-%s" % (set_type, i)
      if set_type == "test":
        text_a = tokenization.convert_to_unicode(line[0])
        label = "0"
      else:
        text_a = tokenization.convert_to_unicode(line[1])
        label = tokenization.convert_to_unicode(line[0])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
    return examples

可以看到是将训练数据以及测试数据分开读取并转化为token。

虽然kesci上也提供了训练资源,但下载预训练好的模型以及安装tensorflow的环境不太方便,因此我是在google colab上训练的。

其余暂不需要修改什么,然后在执行训练命令时设置参数:

! python run_classifier.py \
  --task_name=cmt \
  --do_train=true \
  --do_eval=false \
  --data_dir=./cmt \
  --vocab_file=./chinese_L-12_H-768_A-12/vocab.txt \
  --bert_config_file=./chinese_L-12_H-768_A-12/bert_config.json \
  --init_checkpoint=./chinese_L-12_H-768_A-12/bert_model.ckpt \
  --max_seq_length=20 \
  --train_batch_size=32 \
  --learning_rate=2e-5 \
  --num_train_epochs=2.0 \
  --output_dir=./output \

这里经过实验我使用了batch_size为32,序列最长长度为20,epoch为2进行训练,由于使用了全部训练数据进行训练,没有使用验证集,所以–do_eval为false。
最终模型得到的分数为0.96743。

模型融合

为了更好地发挥BERT模型的作用,我尝试使用了模型融合(ensemble)方法进行训练,使用的是stacking方法,即将数据分为五个部分,每次选取四个部分作为数据集,一个部分作为验证集,共训练五次,最后将五个模型得到的结果取平均得到最终结果,原理图如下:
在这里插入图片描述
图片来自这篇博客
然后就是切分数据:

def split_data():
    all_data = open('./Comments9120/train_shuffle.txt', 'r').readlines()
    n = len(all_data)
    step = n // 5


    for i in range(1, 6):
        with open('./cmt{}/train.tsv'.format(i), 'w') as f:
            assert len(all_data[:(i - 1) * step] + all_data[i * step:]) == 12800
            f.writelines(all_data[:(i - 1) * step] + all_data[i * step:])

        with open('./cmt{}/dev.tsv'.format(i), 'w') as f:
            assert len(all_data[(i - 1) * step:i * step]) == 3200
            f.writelines(all_data[(i - 1) * step:i * step])

训练时使用batch_size为32,epoch分别为2和3训练出了共10个模型,最终得到了十个结果。

训练命令为:

! python run_classifier.py \
  --task_name=cmt \
  --do_train=true \
  --do_eval=true \
  --data_dir=./cmt1 \
  --vocab_file=./chinese_L-12_H-768_A-12/vocab.txt \
  --bert_config_file=./chinese_L-12_H-768_A-12/bert_config.json \
  --init_checkpoint=./chinese_L-12_H-768_A-12/bert_model.ckpt \
  --max_seq_length=20 \
  --train_batch_size=32 \
  --learning_rate=2e-5 \
  --num_train_epochs=2.0 \
  --output_dir=./output1 \

训练完成后首先使用了epoch为2的五个数据得到的平均值作为最终结果,提交后得到的分数为0.96910。

然后使用了epoch为3的五个数据的平均值,提交后得到的分数为0.96854。

最后,也是最后一次提交机会,决定使用所有十个数据的平均值作为最后的结果,代码如下:

import pandas as pd

def getres():

    all_data = []
    for i in range(1, 6):
        a = open('./ensemble2/test_results{}.tsv'.format(i + 5), 'r').readlines()
        a = [float(l.rstrip().split('\t')[1]) for l in a]
        if not all_data:
            all_data = a
        else:
            for k in range(len(a)):
                all_data[k] += a[k]

    for i in range(1, 6):
        a = open('./ensemble1/test_results{}.tsv'.format(i), 'r').readlines()
        a = [float(l.rstrip().split('\t')[1]) for l in a]
        if not all_data:
            all_data = a
        else:
            for k in range(len(a)):
                all_data[k] += a[k]

    print(len(all_data))
    print(all_data[:5])


    res = [l / 10 for l in all_data]
    for i in range(len(res)):
        res[i] = [i, res[i]]
    print(res[:5])
    t = pd.DataFrame(res)
    t.columns = ['ID', 'Prediction']
    t.to_csv('./submission_random.csv')

最终结果提交后得到的分数为0.96975,为使用bert的最高分数,可以看到模型ensemble是具有一定的提升效果的。

至此,大作业结束。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值