文章目录
1. Abstract
本文对使用字符级卷积网络(ConvNet)进行文本分类进行了实证探索。 我们构建了几个大型数据集,以表明字符级卷积网络可以实现最先进的或有竞争力的结果。 与传统模型(如词袋、n-gram 及其 TFIDF 变体)以及深度学习模型(如基于单词的 ConvNet 和递归神经网络)进行比较。
2. Introduction
文本分类是自然语言处理的一个经典主题,其中需要为自由文本文档分配预定义的类别。 文本分类研究的范围从设计最佳特征到选择最佳的机器学习分类器。 迄今为止,几乎所有的文本分类技术都是基于单词的,其中一些有序单词组合(例如n-gram)的简单统计通常表现最好。
另一方面,许多研究人员发现卷积网络(ConvNets)对于从原始信号中提取信息非常有用,范围从计算机视觉应用到语音识别等。 特别是,深度学习研究早期使用的时滞网络本质上是对顺序数据进行建模的卷积网络。
在本文中,我们探索将文本视为字符级别的一种原始信号,并将时间(一维)ConvNet 应用于其中。 在本文中,我们仅使用分类任务来举例说明 ConvNet 理解文本的能力。 从历史上看,我们知道卷积网络通常需要大规模数据集才能工作,因此我们也构建了其中的几个数据集。 与传统模型和其他深度学习模型进行了广泛的比较。
本文的贡献之一,构建了多个文本分类数据集,极大地推动了文本分类的研究工作。
文献中探索了将卷积网络应用于文本分类或自然语言处理。 事实证明,ConvNet 可以直接应用于分布式或离散单词嵌入,而无需了解语言的句法或语义结构。 这些方法已被证明比传统模型具有竞争力。
还有使用字符级特征进行语言处理的相关作品。 其中包括使用带有线性分类器的字符级 n-gram,以及将字符级特征合并到 ConvNets 中。 特别是,这些 ConvNet 方法使用单词作为基础,其中在单词或单词 n-gram级别提取的字符级特征形成分布式表示。 观察到词性标记和信息检索的改进。
本文是第一篇仅将 ConvNet 应用于字符的文章。 我们表明,当在大规模数据集上进行训练时,深度卷积网络不需要单词知识,此外,先前研究的结论是卷积网络不需要有关语言的句法或语义结构的知识。 这种工程简化对于可以适用于不同语言的单个系统至关重要,因为无论是否可以分割成单词,字符始终构成必要的构造。 仅处理字符还有一个优点,可以自然地学习异常字符组合,例如拼写错误和表情符号。
本文的历史意义之二,提出的ChatTextCNN方法因为只使用字符信息,所以可以用于多种语言中。
3. 传统经典算法模型
3.1 Bag of words
算法:
1.构建一个50000个词的词表
2.对于一篇文档d,统计词表中每个词在d中出现的次数
3.根据词表中每个词在d中出现的次数,构建一个词表大小的向量。
对于 TFIDF,我们使用计数作为术语频率。 逆文档频率是样本总数与训练子集中包含该单词的样本数除以对数。 通过除以最大特征值来对特征进行归一化。
3.2 基于词向量的k-means
算法:
1.首先将每个词映射成一个词向量(使用训练好的词向量)
2.在所有的词上使用k-means进行聚类,类别数为5000
3.对于每个词,都划分其属于哪个k-means类
4.对于一篇文档d中的每个词,查看它属于哪个类别,然后一篇文档就可以表示成一个5000维的向量,每个位置代表这篇文档中有多少属于这个类别的词。
5.后面接多分类的logistic回归。
3.3 基于词的卷积网络模型
可以看我前面的两篇blogTextCNN论文阅读笔记和DCNN论文阅读笔记。
3.4 长短时记忆模型
算法:
1.将训练好的word2vec词向量输入到LSTM
2.LSTM每个时间步的输出取平均作为文档的表示
3.后面接一个多分类的logisitc回归
4.Character-level Convolutional Networks
4.1 一维卷积
这里的卷积操作和TextCNN论文里所做的卷积操作是一样的。
4.2 字符量化
实际上就是把字符映射为数字,这里没用字符嵌入的表示,而是直接使用独热编码来表示。不在字符表中的字符用全零向量表示。
4.3 model design
网络由6个卷积层,3个全连接层组成。第1、2、6个卷积后有最大池化层。输入的特征数等于70,输入特征长度为1014。作者认为1014个字符已经可以捕获大部分感兴趣的文本。还在3个全连接层之间插入2个dropout 模块来正则化。dropout为0.5。
4.3.1 卷积层设置
网络提供了两种卷积尺寸,大的采用1024个卷积核,小的采用256个卷积核。使用高斯分布对权重进行初始化,large model采用的均值和标准差分别为(0, 0.02)。small model采用的均值和标准差分别为(0,0.05)。
feature相当于2d卷积中的通道数(每层的卷积核数量),kernel相当于2d卷积中的卷积核尺寸。池化操作采用非重叠的最大池化,即池化尺寸等于步长3。
4.3.2 全连接层设置
4.3.3 模型优缺点
缺点:
- 字符级别的文本长度特别长,不利于处理长文本的分类
- 只使用字符级别信息,所以模型学习到的语义方面的信息较少
- 在小语料上效果较差
优点:
4. 模型结构简单,并且在大语料上效果很好
5. 可以用于各种语言,不需要做分词处理
6. 在噪音比较多的文本上表现较好,因为基本上不存在OOV问题、
4.3.4 用PyTorch搭建Model
import torch
import torch.nn as nn
import torch.nn.functional as F
class CharTextCNN(nn.Module):
def __init__(self,config):
super(CharTextCNN,self).__init__()
in_features = [config.char_num] + config.features[0:-1]
# layer1的输入通道数为字符个数(70);
# layer2~输入通道数(论文中的 small/large Feature)依次从list读入,注意没有 list最后一个元素,因为list的第一个元素为layer2的输入通道(即第一层的输出通道数),依次...
out_features = config.features # 读入输出通道数
kernel_sizes = config.kernel_sizes # 读取卷积核尺寸(论文中的kernel)
self.convs = []
self.conv1 = nn.Sequential(
nn.Conv1d(in_features[0], out_features[0], kernel_size=kernel_sizes[0], stride=1),
nn.BatchNorm1d(out_features[0]),
nn.ReLU(),
nn.MaxPool1d(kernel_size=3, stride=3)
)
self.conv2 = nn.Sequential(
nn.Conv1d(in_features[1], out_features[1], kernel_size=kernel_sizes[1], stride=1),
nn.BatchNorm1d(out_features[1]),
nn.ReLU(),
nn.MaxPool1d(kernel_size=3, stride=3)
)
self.conv3 = nn.Sequential(
nn.Conv1d(in_features[2], out_features[2], kernel_size=kernel_sizes[2], stride=1),
nn.BatchNorm1d(out_features[2]),
nn.ReLU()
)
self.conv4 = nn.Sequential(
nn.Conv1d(in_features[3], out_features[3], kernel_size=kernel_sizes[3], stride=1),
nn.BatchNorm1d(out_features[3]),
nn.ReLU()
)
self.conv5 = nn.Sequential(
nn.Conv1d(in_features[4], out_features[4], kernel_size=kernel_sizes[4], stride=1),
nn.BatchNorm1d(out_features[4]),
nn.ReLU()
)
self.conv6 = nn.Sequential(
nn.Conv1d(in_features[5], out_features[5], kernel_size=kernel_sizes[5], stride=1),
nn.BatchNorm1d(out_features[5]),
nn.ReLU(),
nn.MaxPool1d(kernel_size=3, stride=3)
)
self.fc1 = nn.Sequential(
nn.Linear(8704, 1024), # length * channel(small/large feature):(l0-96)/27 * 256
nn.ReLU(),
nn.Dropout(p=config.dropout)
)
self.fc2 = nn.Sequential(
nn.Linear(1024, 1024),
nn.ReLU(),
nn.Dropout(p=config.dropout)
)
self.fc3 = nn.Linear(1024, config.num_classes)
def forward(self, x):
x = self.conv1(x)
x = self.conv2(x)
x = self.conv3(x)
x = self.conv4(x)
x = self.conv5(x)
x = self.conv6(x)
x = x.view(x.size(0), -1)
x = self.fc1(x)
x = self.fc2(x)
x = self.fc3(x)
return x
4.4 数据增强
同义词替换:使用两个geometric distribution来决定被替换词的概率。
5. 实验结果
5.1 数据集
AG’s News corpus :我们在网络上获得了 AG 的新闻文章语料库。 它包含来自 2000 多个新闻来源的 496,835 篇分类新闻文章。 我们从该语料库中选择 4 个最大的类来构建我们的数据集,仅使用标题和描述字段。 每类训练样本数为30000个,测试样本数为1900个。
Sogou News corpus:该数据集是SogouCA和SogouCS新闻语料库的组合,包含各个主题频道的总共2,909,551篇新闻文章。 然后,我们通过手动对域名进行分类,使用 URL 来标记每条新闻。 这为我们提供了大量带有类别标签的新闻文章语料库。 类别数量很多,但大多数只包含很少的文章。 我们选择了5个类别——“体育”、“金融”、“娱乐”、“汽车”和“科技”。 每类选取的训练样本数量为90,000个,测试样本数量为12,000个。 虽然这是一个中文数据集,但我们使用 pypinyin 包结合 jieba 中文分词系统来生成拼音——中文的拼音罗马化。 然后,英语模型可以无需更改地应用于该数据集。 使用的字段是标题和内容。
DBPedia ontology dataset:DBpedia 是一个众包社区的努力,旨在从维基百科中提取结构化信息。DBpedia 本体数据集是通过从 DBpedia 2014 中挑选 14 个不重叠的类来构建的。从这 14 个本体类中的每一个中,我们随机选择 40,000 个训练样本和 5,000 个测试样本。 我们用于此数据集的字段包含每篇维基百科文章的标题和摘要。
Yelp reviews:Yelp评论数据集是从2015年的Yelp数据集挑战赛中获得的。该数据集包含1,569,264个具有评论文本的样本。 根据该数据集构建了两个分类任务 - 一个预测用户给出的星星的完整数量,另一个通过考虑星星 1 和 2 为负,以及 3 和 4 为正来预测极性标签。 完整数据集每颗恒星有 130,000 个训练样本和 10,000 个测试样本,极性数据集每个极性有 280,000 个训练样本和 19,000 个测试样本。
Yahoo! Answers dataset:我们获得了雅虎! 通过 Yahoo! 回答综合问答 1.0 版数据集 网络范围程序。 该语料库包含 4,483,032 个问题及其答案。 我们使用 10 个最大的主要类别从此语料库构建了主题分类数据集。 每个类包含 140,000 个训练样本和 5,000 个测试样本。 我们使用的字段包括问题标题、问题内容和最佳答案。
Amazon reviews:我们从斯坦福网络分析项目 (SNAP) 获得了亚马逊评论数据集,该数据集历时 18 年,包含 6,643,669 位用户对 2,441,053 种产品的 34,686,770 条评论。 与 Yelp 评论数据集类似,我们还构建了 2 个数据集——一个满分预测和另一个极性预测。 完整数据集包含每个类别的 600,000 个训练样本和 130,000 个测试样本,而极性数据集包含每个极性情感的 1,800,000 个训练样本和 200,000 个测试样本。 使用的字段是评论标题和评论内容。
5.2 实验结果
本文提出的字符级别的文本分类模型在文本分类数据集上都能取得最好或者有竞争力的结果。红色是最差结果,蓝色是最好结果。full代表区分大小写,不进行大小写转化。th代表进行了数据扩充。
根据错误率直观地和本文提出的模型进行比较,如果大于0就是比本文的模型好,如果小于0就是比本文的模型差。
6. 讨论总结
6.1 关键点
1.卷积神经网络能够有效地提取关键的特征。
2.字符级别的特征对于自然语言处理的有效性。
3.CharTextCNN模型
6.2 创新点
1.提出了一种新的文本分类模型——CharTextCNN
2.提出了多个的大规模的文本分类数据集
3.在多个文本分类数据集上取得最好或者非常有竞争力的结果
6.3 启发点
1.基于卷积神经网络的文本分类不需要语言的语法和语义结构的知识。
2.实验结果告诉我们没有一个机器学习模型能够在各种数据集上都能表现得最好
3.本文从实验的角度分析了字符级别卷积神经网络在文本分类任务上的适用性。
7. 代码复现
data_loader.py
#coding:utf-8
from torch.utils import data
import os
import torch
import json
import csv
import numpy as np
class AG_Data(data.DataLoader):
def __init__(self,data_path,l0 = 1014):
self.path = os.path.abspath('.')
if "data" not in self.path:
self.path += "/data"
self.data_path = data_path
self.l0 = l0 #
self.load_Alphabet()
self.load(self.data_path)
def __getitem__(self, idx):
X = self.oneHotEncode(idx)
y = self.y[idx]
return X, y
def __len__(self):
return len(self.label)
def load_Alphabet(self):
# 读取包含 70个字符的字符表
with open(self.path+"/alphabet.json") as f:
self.alphabet = "".join(json.load(f)) # str.join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串
def load(self,data_path,lowercase=True):
self.label = []
self.data = []
with open(self.path+data_path,"r") as f:
datas = list(csv.reader(f,delimiter=',', quotechar='"'))
# delimiter 用于分割字段的单字符字符串,默认为","
# quotechar 用于带有特殊字符的字段的引用符号,默认为'"'
# 上述命令的含义是以逗号为分隔符,读取引号中的数据
for row in datas:
self.label.append(int(row[0])-1) # list中第一个元素为标签,转为int类型并减一变为从0开始的类别
txt = " ".join(row[1:]) # 将list的第二三个元素(分别为标题和摘要)拼接到一起并用空格分割
if lowercase:
txt = txt.lower()
self.data.append(txt)
self.y = self.label
def oneHotEncode(self,idx):
X = np.zeros([len(self.alphabet),self.l0]) # 初始化字符 one-hot 编码 shape: 70 * 1014
# torch中 1d 卷积和池化操作的输入维度为: batch_size * feature(2d: channel) * len(2d: wide * high)
# 因此将X初始化成上述形式
for index_char, char in enumerate(self.data[idx][::-1]):
# TODO:为什么倒序读取呢? 好像正序倒序其实没有影响,倒序的话超过1014会丢弃文本中前面的内容
if self.char2Index(char) != -1:
X[self.char2Index(char)][index_char] = 1.0 # 字符位置为1
return X
def char2Index(self,char):
return self.alphabet.find(char) # 字符串find函数,找到则返回index,找不到返回-1
main.py
# -*- coding: utf-8 -*-
import torch
import torch.autograd as autograd
import torch.nn as nn
import torch.optim as optim
from model import CharTextCNN
from data import AG_Data
from tqdm import tqdm
import numpy as np
import config as argumentparser
config = argumentparser.ArgumentParser()
config.features = list(map(int,config.features.split(","))) # 逗号分隔并转为int类型
config.kernel_sizes = list(map(int,config.kernel_sizes.split(",")))
config.pooling = list(map(int,config.pooling.split(",")))
if config.gpu and torch.cuda.is_available():
torch.cuda.set_device(config.gpu)
def get_test_result(data_iter,data_set):
# 生成测试结果
model.eval()
data_loss = 0
true_sample_num = 0
for data, label in data_iter:
if config.cuda and torch.cuda.is_available():
data = data.cuda()
label = label.cuda()
else:
data = torch.autograd.Variable(data).float()
out = model(data)
loss = criterion(out, autograd.Variable(label.long()))
data_loss += loss.data.item()
true_sample_num += np.sum((torch.argmax(out, 1) == label).cpu().numpy())
acc = true_sample_num / data_set.__len__()
return data_loss,acc
training_set = AG_Data(data_path="/AG/train.csv", l0=config.l0)
training_iter = torch.utils.data.DataLoader(dataset=training_set,
batch_size=config.batch_size,
shuffle=True,
num_workers=0)
test_set = AG_Data(data_path="/AG/test.csv",l0=config.l0)
test_iter = torch.utils.data.DataLoader(dataset=test_set,
batch_size=config.batch_size,
shuffle=False,
num_workers=0)
model = CharTextCNN(config)
if config.cuda and torch.cuda.is_available():
model.cuda()
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=config.learning_rate)
loss = -1
for epoch in range(config.epoch):
model.train()
process_bar = tqdm(training_iter)
for data, label in process_bar:
if config.cuda and torch.cuda.is_available():
data = data.cuda()
label = label.cuda()
else:
data = torch.autograd.Variable(data).float()
label = torch.autograd.Variable(label).squeeze()
out = model(data)
loss_now = criterion(out, autograd.Variable(label.long()))
if loss == -1:
loss = loss_now.data.item()
else:
loss = 0.95*loss+0.05*loss_now.data.item()
process_bar.set_postfix(loss=loss_now.data.item())
process_bar.update()
optimizer.zero_grad()
loss_now.backward()
optimizer.step()
if epoch % 20 == 0:
torch.save(model.state_dict(), "./checkpoints/epoch_%s" %epoch)
test_loss, test_acc = get_test_result(test_iter, test_set)
print("The test acc is: %.5f" % test_acc)
数据集下载
AG News:
https://s3.amazonaws.com/fast-ai-nlp/ag_news_csv.tgz
DBPedia:
https://s3.amazonaws.com/fast-ai-nlp/dbpedia_csv.tgz
Sogou news:
https://s3.amazonaws.com/fast-ai-nlp/sogou_news_csv.tgz
Yelp Review Polarity:
https://s3.amazonaws.com/fast-ai-nlp/yelp_review_polarity_csv.tgz
Yelp Review Full:
https://s3.amazonaws.com/fast-ai-nlp/yelp_review_full_csv.tgz
Yahoo! Answers:
https://s3.amazonaws.com/fast-ai-nlp/yahoo_answers_csv.tgz
Amazon Review Full:
https://s3.amazonaws.com/fast-ai-nlp/amazon_review_full_csv.tgz
Amazon Review Polarity:
https://s3.amazonaws.com/fast-ai-nlp/amazon_review_polarity_csv.tgz
8. 参考资料
主要参考博客:CharTextCNN学习笔记
主要参考视频:深度之眼论文带读CharTextCNN