Convolutional Neural Networks for Sentence Classification 基于卷积神经网络的句子分类
文章目录
这是纽约大学的Yoon Kim在2014年发布在EMNLP上的,提出 用预训练的词向量和卷积神经网络做句子的文本分类,简单有效,在多个数据集上取得不错的效果。
背景知识
自98年Lecun使用卷积神经网络用于处理邮政系统手写数字识别后,12年Alexnet技惊四座,13-15年深度学习和卷积神经网络的热度呈爆炸式增长;03年Bengio提出了词向量,13年Bengio徒弟Mikolov提出word2vec模型,14年Jeffrey提出了GloVe,预训练的词向量得以被大众使用。可以说恰逢其会,14年-15年大量研究者在文本领域使用了卷积神经网络,TextCNN应运而生。
关于卷积神经网络、预训练词向量的知识可以自行查看。
模型架构
基本上在预训练词向量上微调得到任务指向的词向量就可以获取很好的效果,但作者也提出了既使用静态预训练词向量又使用任务指向词向量的分类模型。在7个文本分类任务里的4个都获得了比之前模型更好的效果。
(一)TextCNN
可以看到模型还是很简单的,而且有详细注释。首先把每个词映射为词向量,做一维卷积,对得到的特征图做最大池化,每个特征图得到一个特征,最后接一个dropout和softmax的全连接。
TextCNN详细过程如下:
Embedding:第一层是图中最左边的7乘5的句子矩阵,每行是词向量,维度=5,这个可以类比为图像中的原始像素点。
Convolution:然后经过 kernel_sizes=(2,3,4) 的一维卷积层,每个kernel_size 有两个输出 channel。
MaxPolling:第三层是一个1-max pooling层,这样不同长度句子经过pooling层之后都能变成定长的表示。
FullConnection and Softmax:最后接一层全连接的 softmax 层,输出每个类别的概率。
通道(Channels):
图像中可以利用 (R, G, B) 作为不同channel;
文本的输入的channel通常是不同方式的embedding方式(比如 word2vec或Glove),实践中也有利用静态词向量和fine-tunning词向量作为不同channel的做法。
一维卷积(conv-1d):
图像是二维数据;文本是一维数据,因此在TextCNN卷积用的是一维卷积(在word-level上是一维卷积;虽然文本经过词向量表达后是二维数据,但是在embedding-level上的二维卷积没有意义)。一维卷积带来的问题是需要通过设计不同 kernel_size 的 filter 获取不同宽度的视野。
Pooling层:
利用CNN解决文本分类问题的文章还是很多的,比如这篇 A Convolutional Neural Network for Modelling Sentences 最有意思的输入是在 pooling 改成 (dynamic) k-max pooling ,pooling阶段保留 k 个最大的信息,保留了全局的序列信息。
比如在情感分析场景,举个例子:
“我觉得这个地方景色还不错,但是人也实在太多了”
虽然前半部分体现情感是正向的,全局文本表达的是偏负面的情感,利用 k-max pooling能够很好捕捉这类信息。
(二)参数选择与分析
本文通过在SST-2数据验证集上网格搜索,得到以下超参,用于所有数据集:使用relu激活,卷积窗口3、4、5,特征图100,dropout失活率0.5,l2正则约束条件(s)为3,mini-batchsize是50,打乱顺序,优化器SGD,验证集从训练集选百分之十。
预训练词向量是在1000亿的google news上用CBOW训练的,embedding-size是300,没有在预训练词向量出现的随机初始化。
那,这一定是最好的么?奥斯汀大学的Ye Zhang等做了超参分析,发现即使保持以上的都不变但10折交叉得出的平均值还是有很大波动。
1、Embedding方式:word2vec或者glove都行,不同任务可以多试试,但是两个都用一般效果介于二者之间,并不能互补。
2、卷积核方式:对结果影响很大。但大小取决于数据集,有些size为1时最好,有的越大越好,大多数size为10的时候最好。
3、卷积核个数:影响也很大,feature map个数大于等于100为佳。
4、激活函数:推荐多试几种,一般tanh和Relu最好。单层CNN可以考虑不使用激活函数。
5、Dropout:大多数数据集不使用dropout也差的不多,dropout可能导致提取特征简单。失活率大于0.7会普遍下降。
6、L2正则:使用与否和参数大小取决于数据集,大多数据集影响也不大。TextCNN仅在最后一层进行了L2正则。
7、1-max pooling是最好的pooling方式。
(三)一些建议
1、建议多跑几次,看初始化是否有较大影响,如果有固定一下初始化。固定后如果效果仍然波动,则跑多次记录最小最大平均值等,增加信服度。
2、建议用基础超参设置先试试,准确率差不多可以微调,准确率差得多就需要进行其他方面的思考。
3、可以先用单卷积核线性搜索,然后选最好的为主,在附近进行超参搜索。长句子可以开始就调大一点进行搜索。一般多个不同的核组合会好于多个相同的核组合,但这里是(7,7,7,7)的组合最好。
4、feature map可以在100-600之间选择,选好后用(0.0-0.5)的dropout和大的l2正则s参数。用的越大训练时间越久,这需要权衡。但如果你找的值在600附近,则需要考虑试试更大的,在100附近就试试更小的。增大feature map相当于增加模型复杂度,如果效果变差,则加大dropout、减小s参数。
历史意义
1、开启了基于深度学习文本分类的先河。
2、推动卷积神经网络在自然语言处理的发展。
炼丹真诀
# -*- coding: utf-8 -*-
# @Software: PyCharm
# @Author: Beilop Jiao
# @Contact: 781933206@qq.com
# @Time: 2021/3/3 8:57
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as opt
import numpy as np
data = ["i love you", "he loves me", "she likes baseball", "i hate you", "sorry for that", "this is awful"]
label = [1,1,1,0,0,0]
word_list = " ".join(data).split()
word_list = list(set(word_list))
vocab_size = len(set(word_list))
word2id = {w:i for i,w in enumerate(word_list)}
#由于数据少,所以参数也都很小
embedding_size = 2
filters_num = 3
filters = [2,2,2]
classes = 2
class TextCNN(nn.Module):
def __init__(self):
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(vocab_size,embedding_size)
# 有兴趣可以换成2d卷积核,提取特征更多,效果和收敛速度会快,但是训练速度会慢
self.convs = [nn.Conv1d(embedding_size,filters_num,filter_size) for filter_size in filters]
self.dropout = nn.Dropout(0.5)
self.fc = nn.Linear(filters_num*len(filters),classes)
def conv_and_pool(self,x,conv):
x = F.relu(conv(x))
x = F.max_pool1d(x,x.size(2)).squeeze(2)
return x
def forward(self,x):
x = self.embedding(x)
x = x.transpose(1,2)
x = torch.cat([self.conv_and_pool(x,conv) for conv in self.convs],1)
x = self.dropout(x)
x = self.fc(x)
return x
m = TextCNN()
train = opt.Adam(m.parameters(),0.001)
loss = nn.CrossEntropyLoss()
# 转为Longtensor是因为其他类型在交叉熵损失不可运行
input = torch.LongTensor([[word2id[n] for n in sen.split()]for sen in data])
target = torch.LongTensor([out for out in label])
for i in range(5000):
train.zero_grad()
output = m(input)
cost = loss(output, target)
if (i + 1) % 1000 == 0:
print('Epoch:', '%04d' % (i + 1), 'cost =', '{:.6f}'.format(cost))
cost.backward()
train.step()
test_text = 'i love you'
tests = torch.LongTensor([[word2id[n] for n in test_text.split()]])
# Predict
predict = m(tests).max(1)
if predict == 0:
print(test_text, "is Bad Mean...")
else:
print(test_text, "is Good Mean!!")