任务要求
本次推荐评论展示任务的目标是从真实的用户评论中,挖掘合适作为推荐理由的短句。点评软件展示的推荐理由具有长度限制,而真实用户评论语言通顺、信息完整。综合来说,两者都具有用户情感的正负向,但是展示推荐理由的内容相关性高于评论,需要较强的文本吸引力。一些真实的推荐理由如下图所示:
具体的,需要将文本分为两类,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是具有一定的提升效果的。
至此,大作业结束。